From 5e2e9bb8335d3ab8e2f82914edff78ea4952e662 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 9 Oct 2026 10:22:23 +0200 Subject: [PATCH] fix(terminal): the Path and Clear keys act on the tile or Pane B that has the keyboard The phone keyboard's Path key (insertTerminalText) and its clear-prompt key (clearTerminalInput) always wrote to the main pane: into its local-echo overlay, or to the active session. With the tile grid open that overlay is parked behind the grid, so a picked path landed there unseen and only reached the session later, and with the split open a path meant for Pane B went to Pane A. Both now ask _focusedPane() first. When a tile or Pane B holds the keyboard, the path is sent to that pane's session through the exactly-once queue, and clearing sends Ctrl+U there (those panes have no overlay, so the TUI owns the line), then the pane's own terminal takes focus. The main pane's behaviour is unchanged. Left over from the final checkup's dictation fix (c10), which covered voice input only. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/terminal-ui.js | 19 ++++++++++++ test/path-picker-ui.test.ts | 54 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/web/public/terminal-ui.js b/src/web/public/terminal-ui.js index 3ec27a55..d95960b2 100644 --- a/src/web/public/terminal-ui.js +++ b/src/web/public/terminal-ui.js @@ -4605,6 +4605,15 @@ Object.assign(CodemanApp.prototype, { /** Insert editable text at the active prompt without pressing Enter. */ insertTerminalText(text) { if (!this.activeSessionId || !text) return; + // A tile or the split's Pane B holds the keyboard: the text belongs to that + // pane's session. Those panes have no local-echo overlay, and the main + // overlay is parked behind the grid, so send it straight to the pane. + const pane = this._focusedPane?.(); + if (pane && !pane.isPrimary) { + this._sendInputAsync(pane.sessionId, text); + pane.terminal?.focus(); + return; + } // Under predict the text goes out via sendInput (bypasses onData), so the // hook never sees it: clear outstanding predictions here instead. if (this._localEchoPolicy === 'predict') this._predictiveEcho?.clearPredictions(); @@ -4628,6 +4637,16 @@ Object.assign(CodemanApp.prototype, { if (!this.activeSessionId) return; if (typeof CjkInput !== 'undefined') CjkInput.clear(); + // A tile or Pane B holds the keyboard: its TUI owns the editable buffer + // (no local-echo overlay there), so kill the line in that pane's session, + // never in the parked main pane's. + const pane = this._focusedPane?.(); + if (pane && !pane.isPrimary) { + this._sendInputAsync(pane.sessionId, '\x15'); + this.showToast?.('Input cleared', 'success'); + pane.terminal?.focus(); + return; + } if (this._inputFlushTimeout) { clearTimeout(this._inputFlushTimeout); this._inputFlushTimeout = null; diff --git a/test/path-picker-ui.test.ts b/test/path-picker-ui.test.ts index 8dcb3c4a..6fa30b3a 100644 --- a/test/path-picker-ui.test.ts +++ b/test/path-picker-ui.test.ts @@ -225,6 +225,60 @@ describe('mobile filesystem picker actions', () => { expect(terminalHarness.cjkClear).toHaveBeenCalled(); }); + it('sends a picked path to the tile or Pane B that holds the keyboard, never the parked main overlay', () => { + const appendText = vi.fn(); + const sendInput = vi.fn(); + const _sendInputAsync = vi.fn(); + const mainFocus = vi.fn(); + const tileFocus = vi.fn(); + const app = { + activeSessionId: 'session-1', + _localEchoEnabled: true, + _localEchoOverlay: { appendText }, + terminal: { focus: mainFocus }, + sendInput, + _sendInputAsync, + _focusedPane: () => ({ terminal: { focus: tileFocus }, sessionId: 'tile-2', isPrimary: false, tile: {} }), + }; + + terminalHarness.mixin.insertTerminalText.call(app, '/mnt/d/AI/project'); + + expect(_sendInputAsync).toHaveBeenCalledWith('tile-2', '/mnt/d/AI/project'); + expect(appendText).not.toHaveBeenCalled(); + expect(sendInput).not.toHaveBeenCalled(); + expect(tileFocus).toHaveBeenCalledOnce(); + expect(mainFocus).not.toHaveBeenCalled(); + }); + + it('clears the prompt of the tile or Pane B that holds the keyboard with Ctrl+U, leaving the main overlay alone', () => { + const clear = vi.fn(); + const sendInput = vi.fn(() => Promise.resolve()); + const _sendInputAsync = vi.fn(); + const showToast = vi.fn(); + const tileFocus = vi.fn(); + const app = { + activeSessionId: 'session-1', + _inputFlushTimeout: null, + _pendingInput: 'main pane text', + _localEchoEnabled: true, + _localEchoOverlay: { getFlushed: () => ({ count: 0, text: '' }), clear, suppressBufferDetection: vi.fn() }, + sendInput, + _sendInputAsync, + showToast, + terminal: { focus: vi.fn() }, + _focusedPane: () => ({ terminal: { focus: tileFocus }, sessionId: 'tile-2', isPrimary: false, tile: {} }), + }; + + terminalHarness.mixin.clearTerminalInput.call(app); + + expect(_sendInputAsync).toHaveBeenCalledWith('tile-2', '\x15'); + expect(sendInput).not.toHaveBeenCalled(); + expect(clear).not.toHaveBeenCalled(); + expect(app._pendingInput).toBe('main pane text'); + expect(showToast).toHaveBeenCalledWith('Input cleared', 'success'); + expect(tileFocus).toHaveBeenCalledOnce(); + }); + it('uses Ctrl+U to clear the TUI-owned prompt when local echo is disabled', () => { const sendInput = vi.fn(() => Promise.resolve()); const app = {