fix: cycle Command-K CLI picker at query edges (johoja12/Codeman#21)

This commit is contained in:
Saqeb Akhter committed 2026-10-26 01:14:48 +04:00
1 parent 3e06f3c141
commit 0cc9332f3d
3 files changed
+122 -6

No files matched your search

@@ -0,0 +1,87 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { chromium, type Browser } from 'playwright';
import { WebServer } from '../src/web/server.js';
describe('Command-K agent arrow keys in Chromium', () => {
let server: WebServer;
let browser: Browser;
beforeAll(async () => {
server = new WebServer(0, false, true);
await server.start();
browser = await chromium.launch({ headless: true });
}, 60000);
afterAll(async () => {
await browser?.close();
await server?.stop();
});
it('cycles both directions at query boundaries while preserving text editing and selection', async () => {
const page = await browser.newPage({ viewport: { width: 1400, height: 900 } });
const errors: string[] = [];
page.on('pageerror', (e) => errors.push(e.message));
try {
await page.goto(`http://127.0.0.1:${server.boundPort}`, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => (window as any).app?.openCommandPalette);
await page.waitForTimeout(3500);
await page.evaluate(() => {
const a = (window as any).app;
a.cases = [{ name: 'buildmesh', path: '/fixture/buildmesh' }];
a.sessions = new Map();
a.sessionOrder = [];
a._runMode = 'codex';
(window as any).__codemanCliCatalog = [
{ id: 'claude', label: 'Claude', enabled: true, kind: 'agent' },
{ id: 'codex', label: 'Codex', enabled: true, kind: 'agent' },
{ id: 'shell', label: 'Shell', enabled: true, kind: 'shell' },
];
a._runModeAvailableForCase = () => true;
a._sendInputEphemeral = () => {
throw new Error('Palette leaked terminal input');
};
a.run = () => {
throw new Error('Arrow key launched a session');
};
});
await page.keyboard.press('Control+k');
await page.locator('#commandPaletteModal.active').waitFor();
const input = page.locator('#commandPaletteSearch');
await input.fill('buildmesh');
const label = page.locator('.command-palette-agent-label');
expect(await label.innerText()).toBe('Codex');
await page.keyboard.press('ArrowLeft');
expect(await label.innerText()).toBe('Claude');
await page.keyboard.press('ArrowRight');
expect(await label.innerText()).toBe('Codex');
expect(await input.inputValue()).toBe('buildmesh');
expect(await input.evaluate((e: HTMLInputElement) => e.selectionStart)).toBe(9);
await page.keyboard.press('Home');
await page.keyboard.press('ArrowRight');
expect(await label.innerText()).toBe('Shell');
await page.keyboard.press('ArrowLeft');
expect(await label.innerText()).toBe('Codex');
await input.evaluate((e: HTMLInputElement) => e.setSelectionRange(4, 4));
await page.keyboard.press('ArrowLeft');
expect(await input.evaluate((e: HTMLInputElement) => e.selectionStart)).toBe(3);
await page.keyboard.press('ArrowRight');
expect(await input.evaluate((e: HTMLInputElement) => e.selectionStart)).toBe(4);
await page.keyboard.press('Shift+End');
await page.keyboard.press('ArrowLeft');
expect(await label.innerText()).toBe('Codex');
await page.screenshot({ path: '/tmp/codeman-21-palette.png' });
await page.keyboard.press('ArrowDown');
expect(
await page.evaluate(
() => (window as any).app.commandPaletteItems[(window as any).app.commandPaletteActiveIndex].type
)
).toBe('browse-sessions');
await page.keyboard.press('End');
await page.keyboard.press('ArrowLeft');
expect(await label.innerText()).toBe('Codex');
await page.keyboard.press('Escape');
expect(await page.locator('#commandPaletteModal').evaluate((e) => e.classList.contains('active'))).toBe(false);
expect(errors).toEqual([]);
} finally {
await page.close();
}
});
});
+31 -2
View File
@@ -437,13 +437,28 @@ describe('Command-K new-session agent picker', () => {
expect(app.commandPaletteItems[app.commandPaletteActiveIndex]).toMatchObject({ mode: 'opencode' });
});
it('leaves Left/Right to the caret when they would move it', async () => {
it('cycles both ways at the end of a typed case query without moving its caret', async () => {
const { app, listeners, elements } = loadAgentPalette();
app.openCommandPalette();
elements.commandPaletteSearch.value = 'nomatch';
app.renderCommandPalette();
const left = arrow('ArrowLeft', 7, 'nomatch');
await listeners['search:keydown'](left);
expect(left.preventDefault).toHaveBeenCalled();
expect(app.commandPaletteItems[app.commandPaletteActiveIndex].mode).toBe('opencode');
const right = arrow('ArrowRight', 7, 'nomatch');
await listeners['search:keydown'](right);
expect(right.preventDefault).toHaveBeenCalled();
expect(app.commandPaletteItems[app.commandPaletteActiveIndex].mode).toBe('claude');
});
it('leaves Left/Right to the caret inside the query', async () => {
const { app, listeners, elements } = loadAgentPalette();
app.openCommandPalette();
elements.commandPaletteSearch.value = 'nomatch';
app.renderCommandPalette();
const left = arrow('ArrowLeft', 7, 'nomatch');
const left = arrow('ArrowLeft', 3, 'nomatch');
const right = arrow('ArrowRight', 3, 'nomatch');
await listeners['search:keydown'](left);
await listeners['search:keydown'](right);
@@ -453,6 +468,20 @@ describe('Command-K new-session agent picker', () => {
expect(app.commandPaletteItems[app.commandPaletteActiveIndex].mode).toBe('claude');
});
it.each(['altKey', 'ctrlKey', 'metaKey', 'shiftKey'])(
'preserves %s arrow editing at query boundaries',
async (modifier) => {
const { app, listeners, elements } = loadAgentPalette();
app.openCommandPalette();
elements.commandPaletteSearch.value = 'nomatch';
app.renderCommandPalette();
const event = { ...arrow('ArrowLeft', 7, 'nomatch'), [modifier]: true };
await listeners['search:keydown'](event);
expect(event.preventDefault).not.toHaveBeenCalled();
expect(app.commandPaletteItems[app.commandPaletteActiveIndex].mode).toBe('claude');
}
);
it('does not cycle while a session row is highlighted', async () => {
const { app, listeners } = loadAgentPalette();
app.openCommandPalette();