fix(voice): dictation with the tile grid open reaches the focused tile

With the tile grid open the main terminal is parked (display: none), but
local echo stays on, so direct-mode dictation for the focused tile's
session (which is activeSessionId) was appended to the main terminal's
hidden local-echo overlay. Nothing appeared in the tile, Enter in the tile
submitted without the dictated text, and the stranded text was later
flushed into whichever tile had focus when the grid closed, or dropped.

- _insertText: skip the overlay while _tilesOwnTerminal() is true, so the
  text goes through _sendToTarget to the session itself.
- The post-insert refocus gives the keyboard to the focused tile (only if
  it is still the dictation target) instead of the parked main terminal.
  Outside the grid it still focuses the main terminal, so split view keeps
  its behaviour even when Pane B took focus mid-dictation.
- Green send button: with tiles open, send only Enter to the target and
  leave the parked overlay and main-terminal predictions alone.

The gate is _tilesOwnTerminal(), not _focusedPane().isPrimary: in split
view a target equal to activeSessionId is Pane A with a visible overlay,
and focus read at transcript time could otherwise push Pane A's dictation
past its own unflushed overlay text.

Tests: tile-grid dictation and green-send cases (both fail without the
fix) plus a split-view pin in test/voice-input-target.test.ts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-09 08:51:08 +02:00
parent 18c8b5c280
commit 37ddcbe2f0
2 changed files with 106 additions and 9 deletions
+22 -6
View File
@@ -1007,15 +1007,30 @@ const VoiceInput = {
} else {
// Direct mode: inject into local echo overlay if available, else send to PTY.
// The overlay belongs to the ACTIVE session's terminal, so text dictated
// for any other session must not be typed into it.
// for any other session must not be typed into it. It also belongs to the
// MAIN terminal, which the tile grid parks (display: none): with tiles
// open the text would sit in an invisible overlay the focused tile never
// sees, so it goes straight to the session instead.
const isActive = target === app.activeSessionId;
if (isActive && app._localEchoEnabled && app._localEchoOverlay) {
const tilesOpen = !!app._tilesOwnTerminal?.();
if (isActive && !tilesOpen && app._localEchoEnabled && app._localEchoOverlay) {
app._localEchoOverlay.appendText(trimmed);
} else {
this._sendToTarget(target, trimmed).catch(() => {});
}
this._showVoiceSendBtn();
setTimeout(() => { if (isActive && app.terminal) app.terminal.focus(); }, 150);
setTimeout(() => {
if (!isActive) return;
// With the grid open the keyboard belongs to the focused tile; the
// parked main terminal cannot take focus. Split view keeps the main
// terminal, even if Pane B took focus meanwhile (it is not the target).
if (app._tilesOwnTerminal?.()) {
const pane = app._focusedPane?.();
if (pane?.sessionId === target) pane.terminal?.focus();
} else if (app.terminal) {
app.terminal.focus();
}
}, 150);
}
},
@@ -1045,9 +1060,10 @@ const VoiceInput = {
if (!target) return;
// Simulate Enter key: if local echo is active, flush its buffer + send \r;
// otherwise just send \r directly to the PTY. Both the overlay and the
// predictions belong to the ACTIVE session's terminal, so a dictation
// for another session just sends its Enter there.
if (target !== app.activeSessionId) {
// predictions belong to the ACTIVE session's MAIN terminal, so a
// dictation for another session, or for a tile while the grid has the
// main terminal parked, just sends its Enter there.
if (target !== app.activeSessionId || app._tilesOwnTerminal?.()) {
this._sendToTarget(target, '\r').catch(() => {});
} else if (app._localEchoEnabled && app._localEchoOverlay) {
const text = app._localEchoOverlay.pendingText || '';