mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 08:59:40 +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:
@@ -91,6 +91,18 @@ class FakeTerminal {
|
||||
this.dataCb = cb;
|
||||
}
|
||||
keyHandler: ((ev: Record<string, unknown>) => boolean) | null = null;
|
||||
focusListeners: Array<() => void> = [];
|
||||
textarea = {
|
||||
addEventListener: (type: string, fn: () => void) => {
|
||||
if (type === 'focus') this.focusListeners.push(fn);
|
||||
},
|
||||
removeEventListener: (type: string, fn: () => void) => {
|
||||
if (type === 'focus') this.focusListeners = this.focusListeners.filter((f) => f !== fn);
|
||||
},
|
||||
};
|
||||
focusTextarea() {
|
||||
for (const fn of this.focusListeners) fn();
|
||||
}
|
||||
attachCustomKeyEventHandler(fn: (ev: Record<string, unknown>) => boolean) {
|
||||
this.keyHandler = fn;
|
||||
}
|
||||
@@ -199,6 +211,13 @@ type Tile = {
|
||||
};
|
||||
const TerminalTile = windowStub.TerminalTile as new (id: string, mount: unknown, opts?: object) => Tile;
|
||||
|
||||
/**
|
||||
* Every tile a test creates, destroyed after it. A tile closed with real timers
|
||||
* schedules a real reconnect, and one firing during a LATER test opens a socket
|
||||
* there (FakeSocket.instances is shared), which flaked under full-suite load.
|
||||
*/
|
||||
const liveTiles: Tile[] = [];
|
||||
|
||||
async function connectTile(app: App, opts: Record<string, unknown> = {}) {
|
||||
windowStub.app = app;
|
||||
const tile = new TerminalTile(
|
||||
@@ -206,12 +225,14 @@ async function connectTile(app: App, opts: Record<string, unknown> = {}) {
|
||||
{ addEventListener: vi.fn(), removeEventListener: vi.fn() },
|
||||
{ mode: 'claude', ...opts }
|
||||
);
|
||||
liveTiles.push(tile);
|
||||
await tile.connect();
|
||||
const ws = FakeSocket.instances.at(-1)!;
|
||||
return { tile, ws, term: FakeTerminal.last! };
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const tile of liveTiles.splice(0)) tile.destroy();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
@@ -640,3 +661,18 @@ describe('TerminalTile links and paste follow THIS pane', () => {
|
||||
expect(paste).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('TerminalTile claims the keyboard for the app-level shortcuts', () => {
|
||||
it('focusing its terminal makes it the focused pane; destroy() hands the keyboard back', async () => {
|
||||
const app = makeApp();
|
||||
const { tile, term } = await connectTile(app);
|
||||
expect(app._focusedTile ?? null).toBeNull();
|
||||
|
||||
term.focusTextarea();
|
||||
expect(app._focusedTile).toBe(tile);
|
||||
|
||||
tile.destroy();
|
||||
expect(app._focusedTile).toBeNull();
|
||||
expect(term.focusListeners).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user