fix(terminal): review of the Codex menu wheel guard

This commit is contained in:
Devvyn committed 2026-10-24 10:10:38 +08:00
1 parent 9fa6d98c7c
commit 372df864b9
4 files changed
+62 -3

No files matched your search

+12
View File
@@ -115,6 +115,18 @@ describe('isCodexSelectionMenu on live captures', () => {
expect(isMenu(name), name).toBe(false);
});
it('still reads a narrow question whose option descriptions wrap over many rows', () => {
// The selected option sat 16 rows up from the bottom at 36 columns; sentence-length descriptions on
// a phone-width pane push it further, and an earlier row cap lost it (the menu was missed and the
// wheel moved the selection again).
const rows = fixture('question-w36.txt').split('\n');
const at = rows.findIndex((row) => /^\s{0,2}› 1\./.test(row));
expect(at).toBeGreaterThan(-1);
rows.splice(at + 1, 0, ...Array.from({ length: 12 }, (_, i) => ` wrapped description row ${i + 1}`));
const terminal = terminalShowing(rows.join('\n'), 60);
expect(windowRef.CodemanTerminalInput.isCodexSelectionMenu(terminal)).toBe(true);
});
it('is false for a terminal it cannot read', () => {
expect(windowRef.CodemanTerminalInput.isCodexSelectionMenu(undefined)).toBe(false);
expect(windowRef.CodemanTerminalInput.isCodexSelectionMenu({ rows: 10, buffer: { active: { baseY: 0 } } })).toBe(
+48
View File
@@ -843,6 +843,54 @@ describe("the dialog check reads the TILE's session, never the active tab's", ()
});
});
describe("a Codex selection menu on the tile's own screen", () => {
const MENU = [
' Implement this plan?',
'',
'› 1. Yes, implement this plan Switch to Default and start coding',
' 2. Yes, clear context and implement Start a fresh thread (current context: 2% used)',
' 3. No, stay in Plan mode Continue planning with the model',
'',
' enter select · esc back',
];
/** Puts `rows` on the bottom of the tile's 24-row screen, the way xterm's buffer answers getLine. */
function showOnScreen(term: { rows: number; buffer: { active: Record<string, unknown> } }, rows: string[]) {
const screen = [...Array(term.rows - rows.length).fill(''), ...rows];
term.buffer.active.getLine = (y: number) => ({ translateToString: () => screen[y] ?? '' });
}
it('consumes the wheel without paging, and pages again once the menu is gone', async () => {
const app = makeApp({ other: { mode: 'claude' }, 's-tile': { mode: 'codex' } }, 'other');
const { ws, term, mount } = await connectTile(app, { mode: 'codex' });
showOnScreen(term, MENU);
for (const lines of [-18, 18, -18]) {
const ev = wheelLines(lines);
mount.fire('wheel', ev);
// Consumed (xterm does not scroll the stale rows under the menu either), and no key was sent.
expect(ev.preventDefault).toHaveBeenCalled();
}
expect(flushed(ws)).toEqual([]);
// Menu closed: the composer is back on screen and the same wheel pages the transcript.
showOnScreen(term, ['› Ask Codex to do anything', '', ' GPT-6-Astra medium · ~/cases/x', ' ? for shortcuts']);
mount.fire('wheel', wheelLines(-18));
expect(flushed(ws)).toEqual([{ t: 'i', d: PAGE_UP }]);
});
it("asks about THIS tile's terminal, not the active tab's", async () => {
const app = makeApp({ other: { mode: 'codex' }, 's-tile': { mode: 'codex' } }, 'other');
(app as App & { terminal: unknown }).terminal = {
rows: 24,
buffer: { active: { getLine: () => ({ translateToString: () => '' }) } },
};
const { ws, term, mount } = await connectTile(app, { mode: 'codex' });
showOnScreen(term, MENU);
mount.fire('wheel', wheelLines(-18));
expect(flushed(ws)).toEqual([]);
});
});
describe('rows the tile pushed above the screen itself are not history', () => {
it('pages after a one-screen capture taken at a taller size (the first load)', async () => {
serveCapture(lines(40), 40); // the PTY was 40 rows when it was captured