mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 00:49:41 +02:00
Close Session was bound to Ctrl+W by default. Ctrl+W is delete-word in every shell, readline prompt and agent CLI, so muscle memory killed the session (its tmux pane and CLI, with no confirm) mid-sentence, and with the split pane open it was not even the pane being typed in. Close Session now has no default key: the capture-phase handler lets Ctrl+W through and xterm sends ^W to whichever pane is focused. The action stays in the registry and can be bound in App Settings -> Shortcuts; the shortcut overlay shows it as not bound. The Help modal, CLAUDE.md, the split and tile-grid specs and three wiki pages stop advertising Ctrl+W as kill. Owner decision (tile-grid decision 5). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
151 lines
5.1 KiB
TypeScript
151 lines
5.1 KiB
TypeScript
/**
|
|
* @fileoverview App-level terminal shortcuts act on the pane the keyboard is in.
|
|
*
|
|
* With the split pane open, the global shortcut handler (app.js, capture phase)
|
|
* used to resolve every terminal action against the PRIMARY pane: 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()` (terminal-ui.js)
|
|
* now answers with the pane whose terminal was focused last, and the actions
|
|
* that are about a TERMINAL (clear, restore size) go through it.
|
|
*
|
|
* Close Session is not one of them: it has no default key any more (Ctrl+W is
|
|
* left to the terminal as delete-word, see ctrl-w-never-closes.test.ts), and a
|
|
* key a user binds to it closes the active session, as it always did.
|
|
*
|
|
* Real code under test: constants.js + terminal-ui.js in a `vm` context.
|
|
*/
|
|
import { readFileSync } from 'node:fs';
|
|
import { resolve } from 'node:path';
|
|
import vm from 'node:vm';
|
|
import { describe, expect, it, vi } from 'vitest';
|
|
|
|
const read = (f: string) => readFileSync(resolve(import.meta.dirname, `../src/web/public/${f}`), 'utf8');
|
|
|
|
function loadApp() {
|
|
const CodemanApp = function CodemanApp(this: unknown) {};
|
|
const context = vm.createContext({
|
|
window: {},
|
|
document: {
|
|
body: { classList: { contains: () => false } },
|
|
activeElement: null,
|
|
addEventListener: vi.fn(),
|
|
getElementById: () => null,
|
|
},
|
|
CodemanApp,
|
|
console: { warn: vi.fn(), log: vi.fn(), debug: vi.fn(), error: vi.fn() },
|
|
_crashDiag: { log: vi.fn() },
|
|
performance: { now: () => 0 },
|
|
requestAnimationFrame: () => 1,
|
|
setTimeout: () => 1,
|
|
MobileDetection: { isTouchDevice: () => false },
|
|
DEC_SYNC_STRIP_RE: /\x1b\[\?2026[hl]/g,
|
|
TERMINAL_CHUNK_SIZE: 32 * 1024,
|
|
});
|
|
vm.runInContext(read('constants.js'), context, { filename: 'constants.js' });
|
|
vm.runInContext(read('terminal-ui.js'), context, { filename: 'terminal-ui.js' });
|
|
const app = new (CodemanApp as unknown as new () => Record<string, any>)();
|
|
app.activeSessionId = 'session-a';
|
|
app.terminal = { clear: vi.fn(), cols: 120, rows: 40 };
|
|
app.showToast = vi.fn();
|
|
app.sendResize = vi.fn(async () => true);
|
|
app.getTerminalDimensions = () => ({ cols: 120, rows: 40 });
|
|
return app;
|
|
}
|
|
|
|
function paneB(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
sessionId: 'session-b',
|
|
terminal: { clear: vi.fn(), cols: 60, rows: 30 },
|
|
fit: vi.fn(),
|
|
_destroyed: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('_focusedPane()', () => {
|
|
it('is the primary pane until another pane claims the keyboard', () => {
|
|
const app = loadApp();
|
|
|
|
const pane = app._focusedPane();
|
|
|
|
expect(pane.isPrimary).toBe(true);
|
|
expect(pane.terminal).toBe(app.terminal);
|
|
expect(pane.sessionId).toBe('session-a');
|
|
});
|
|
|
|
it('is the second pane after its terminal was focused, and the primary again after the primary was', () => {
|
|
const app = loadApp();
|
|
const tile = paneB();
|
|
|
|
app._noteFocusedTile(tile);
|
|
expect(app._focusedPane()).toMatchObject({ isPrimary: false, sessionId: 'session-b', terminal: tile.terminal });
|
|
|
|
app._noteFocusedTile(null);
|
|
expect(app._focusedPane().isPrimary).toBe(true);
|
|
});
|
|
|
|
it('never answers with a destroyed pane', () => {
|
|
const app = loadApp();
|
|
const tile = paneB();
|
|
app._noteFocusedTile(tile);
|
|
|
|
tile._destroyed = true;
|
|
|
|
expect(app._focusedPane().isPrimary).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('terminal shortcuts follow the focused pane', () => {
|
|
it('Ctrl+L clears the focused second pane, not the primary', () => {
|
|
const app = loadApp();
|
|
const tile = paneB();
|
|
app._noteFocusedTile(tile);
|
|
|
|
app.clearTerminal();
|
|
|
|
expect(tile.terminal.clear).toHaveBeenCalledTimes(1);
|
|
expect(app.terminal.clear).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('Ctrl+L still clears the primary when it holds the keyboard', () => {
|
|
const app = loadApp();
|
|
|
|
app.clearTerminal();
|
|
|
|
expect(app.terminal.clear).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("Ctrl+Shift+R forces the focused second pane's size onto its own PTY", async () => {
|
|
const app = loadApp();
|
|
const tile = paneB();
|
|
app._noteFocusedTile(tile);
|
|
|
|
await app.restoreTerminalSize();
|
|
|
|
expect(tile.fit).toHaveBeenCalledWith({ force: true });
|
|
expect(app.sendResize).not.toHaveBeenCalled();
|
|
expect(app.showToast).toHaveBeenCalledWith('Terminal restored to 60x30', 'success');
|
|
});
|
|
|
|
it('Ctrl+Shift+R keeps restoring the primary when it holds the keyboard', async () => {
|
|
const app = loadApp();
|
|
|
|
await app.restoreTerminalSize();
|
|
|
|
expect(app.sendResize).toHaveBeenCalledWith('session-a', { force: true });
|
|
});
|
|
});
|
|
|
|
describe('Close Session (user-bound key only) stays on the active session', () => {
|
|
it('killActiveSession closes activeSessionId and never consults the focused pane', () => {
|
|
const appSource = read('app.js');
|
|
const body = appSource.slice(
|
|
appSource.indexOf('async killActiveSession() {'),
|
|
appSource.indexOf('async killAllSessions() {')
|
|
);
|
|
|
|
expect(body).toContain('await this.closeSession(this.activeSessionId);');
|
|
expect(body).not.toContain('_focusedPane');
|
|
});
|
|
});
|