mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
feat(split): terminal shortcuts, voice and paste follow the focused pane
With the split open, every app-level terminal action resolved against Pane A: Ctrl+L typed into Pane B cleared Pane A's display while xterm sent the ^L into Pane B's PTY, and Ctrl+Shift+R restored Pane A's size. _focusedPane() now answers with the terminal focused LAST (a mic or header click moves DOM focus but not the user's pane): Pane B claims it from its own textarea's focus, the primary terminal's focus gives it back, and a destroyed pane never holds it. Ctrl+L, Ctrl+Shift+R, voice dictation and image paste act on the focused pane. Ctrl+W deliberately still closes the active session: it kills with no confirm, so moving it is an owner decision (docs/tile-grid-plan.md, decision 5). Also destroys every tile after each terminal-tile-input test: a real reconnect timer from one test opened a socket in a later one and flaked under full-suite load. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -115,6 +115,7 @@
|
||||
// Whether the pointer is over a link in THIS pane (the primary pane's own
|
||||
// flag, app._linkHovered, belongs to its terminal alone).
|
||||
this._linkHovered = false;
|
||||
this._onFocusIn = null;
|
||||
}
|
||||
|
||||
async connect() {
|
||||
@@ -151,6 +152,11 @@
|
||||
|
||||
this._installWheelListener();
|
||||
|
||||
// Focusing this terminal makes it the pane the keyboard is in, so the
|
||||
// app-level shortcuts, voice and paste act on it (app._focusedPane).
|
||||
this._onFocusIn = () => global.app?._noteFocusedTile?.(this);
|
||||
this.terminal.textarea?.addEventListener('focus', this._onFocusIn);
|
||||
|
||||
this.terminal.onData((data) => this._onTerminalData(data));
|
||||
|
||||
// Pane B has no gates of its own by default, so every app-level chord
|
||||
@@ -850,6 +856,13 @@
|
||||
this._onWheel = null;
|
||||
}
|
||||
this._detachSocket();
|
||||
if (this._onFocusIn) {
|
||||
this.terminal?.textarea?.removeEventListener('focus', this._onFocusIn);
|
||||
this._onFocusIn = null;
|
||||
}
|
||||
// A destroyed pane cannot hold the keyboard: shortcuts fall back to the
|
||||
// primary terminal (_focusedPane also skips a destroyed tile on its own).
|
||||
if (global.app?._focusedTile === this) global.app._noteFocusedTile?.(null);
|
||||
if (this.terminal) {
|
||||
this.terminal.dispose();
|
||||
this.terminal = null;
|
||||
|
||||
@@ -544,6 +544,9 @@ Object.assign(CodemanApp.prototype, {
|
||||
this._installMobileTapMouseGuard();
|
||||
this._installShiftDragSelection();
|
||||
this._installTouchSelectionFocusGuard();
|
||||
// Focus coming back to the primary terminal ends a second pane's claim on
|
||||
// the keyboard (see _focusedPane).
|
||||
this.terminal.textarea?.addEventListener('focus', () => this._noteFocusedTile(null));
|
||||
|
||||
// Let xterm's CompositionHelper own IME key events. In particular, a
|
||||
// non-composing keyCode 229 is how an active IME commits numbers and
|
||||
@@ -4498,15 +4501,27 @@ Object.assign(CodemanApp.prototype, {
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
/**
|
||||
* The terminal the keyboard is in, as `{ terminal, sessionId, isPrimary }`.
|
||||
* The terminal the keyboard is in, as `{ terminal, sessionId, isPrimary, tile }`.
|
||||
*
|
||||
* The ONE place a shortcut, voice or paste should ask "which pane?", rather
|
||||
* than reading `this.terminal` / `this.activeSessionId`, which always mean the
|
||||
* primary pane. Today it always answers with the primary pane; the split
|
||||
* pane's second terminal plugs in here once it tracks its own focus.
|
||||
* primary pane. It answers with the pane whose terminal was focused LAST, not
|
||||
* with `document.activeElement`: clicking the mic or a header button moves
|
||||
* DOM focus to that button, and the dictation it starts still belongs to the
|
||||
* pane the user was typing in. A second pane (the split pane's Pane B) claims
|
||||
* it from its own terminal's focus; the primary terminal's focus gives it back.
|
||||
*/
|
||||
_focusedPane() {
|
||||
return { terminal: this.terminal, sessionId: this.activeSessionId, isPrimary: true };
|
||||
const tile = this._focusedTile;
|
||||
if (tile && !tile._destroyed && tile.terminal) {
|
||||
return { terminal: tile.terminal, sessionId: tile.sessionId, isPrimary: false, tile };
|
||||
}
|
||||
return { terminal: this.terminal, sessionId: this.activeSessionId, isPrimary: true, tile: null };
|
||||
},
|
||||
|
||||
/** Record which second pane holds the keyboard (null: the primary terminal). */
|
||||
_noteFocusedTile(tile) {
|
||||
this._focusedTile = tile || null;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -4519,8 +4534,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (this._splitPane?.terminal) fn(this._splitPane);
|
||||
},
|
||||
|
||||
// Clears the pane the keyboard is in. The chord itself also reaches that
|
||||
// pane's xterm (the capture handler only preventDefault()s), so the ^L lands
|
||||
// in the same pane whose display is cleared, never a different one.
|
||||
clearTerminal() {
|
||||
this.terminal.clear();
|
||||
this._focusedPane().terminal?.clear();
|
||||
},
|
||||
|
||||
/** Insert editable text at the active prompt without pressing Enter. */
|
||||
@@ -4584,6 +4602,14 @@ Object.assign(CodemanApp.prototype, {
|
||||
* Ctrl+L is NOT sent here (Claude Code 2.x treats it as "clear conversation").
|
||||
*/
|
||||
async restoreTerminalSize() {
|
||||
// A second pane owns its own geometry: refit it and force its PTY to the
|
||||
// size it renders at (TerminalTile.fit), whatever another device set.
|
||||
const pane = this._focusedPane();
|
||||
if (!pane.isPrimary) {
|
||||
pane.tile.fit({ force: true });
|
||||
this.showToast(`Terminal restored to ${pane.terminal.cols}x${pane.terminal.rows}`, 'success');
|
||||
return;
|
||||
}
|
||||
if (!this.activeSessionId) {
|
||||
this.showToast('No active session', 'warning');
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user