feat(toolbar): move "+" and the case gear into the case picker

The owner asked to drop the "+" (Add Case) and gear (case settings) buttons
beside the toolbar case picker. Their two actions now close the picker's own
list instead, as "New or link a case…" and "Case settings…" rows in a sticky
footer, so nothing becomes unreachable: on desktop the "+" was the only door to
Add Case (create, link, clone, manage), and the gear the only one to the
per-case Agent Teams / 1M Opus overrides.

The rows are part of the arrow-key walk (after the last case) and Enter runs
them, they also show when nothing matches the filter, and the case settings
popover still opens anchored to the case group. The picker input gets its
right-hand corners back, and the dead .btn-case-add / .btn-case-settings rules
(desktop, phone and two skin selector lists) are gone. Phones keep their own
case sheet and gear.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-08 22:22:40 +02:00
parent 6644962d70
commit 2accd804f9
7 changed files with 194 additions and 95 deletions
+80
View File
@@ -731,6 +731,86 @@ describe('case selector refresh', () => {
expect(app.run).toHaveBeenCalledTimes(1);
});
it('ends the picker with "New or link a case…" and "Case settings…" rows that replaced the toolbar buttons', () => {
const elements: Record<string, any> = {};
const listeners: Record<string, (event: any) => void> = {};
let listClick: ((event: any) => void) | null = null;
const CodemanApp = function CodemanApp(this: any) {};
elements.quickStartCase = { value: 'Alpha', dataset: {} };
elements.quickStartCaseSearch = {
value: '',
dataset: {},
setAttribute: vi.fn(),
removeAttribute: vi.fn(),
addEventListener: vi.fn((event: string, handler: (event: any) => void) => {
listeners[event] = handler;
}),
select: vi.fn(),
blur: vi.fn(),
};
elements.quickStartCaseList = {
innerHTML: '',
classList: { add: vi.fn(), remove: vi.fn() },
addEventListener: vi.fn((event: string, handler: (event: any) => void) => {
if (event === 'click') listClick = handler;
}),
};
elements.quickStartCasePicker = { contains: () => true };
const context = vm.createContext({
CodemanApp,
localStorage: { getItem: () => null, setItem: () => {} },
document: { getElementById: (id: string) => elements[id] ?? null, addEventListener: vi.fn() },
console,
escapeHtml: (s: string) => s,
setInterval: () => 1,
clearInterval: () => {},
fetch: () => new Promise(() => {}),
});
const sessionUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
vm.runInContext(sessionUi, context, { filename: 'session-ui.js' });
const app = new (CodemanApp as any)();
app.cases = [{ name: 'Alpha' }, { name: 'zeta' }];
app.updateDirDisplayForCase = vi.fn();
app.updateMobileCaseLabel = vi.fn();
app.saveLastUsedCase = vi.fn();
app.updateCasePickerInput = vi.fn();
app.run = vi.fn(async () => {});
app.showCreateCaseModal = vi.fn();
app.toggleCaseSettings = vi.fn();
app.setupQuickStartCasePicker();
// Both rows render after the last case, also when nothing matches.
app.openCasePicker('');
const html = elements.quickStartCaseList.innerHTML;
const lastCase = html.lastIndexOf('data-case="');
expect(html.indexOf('data-case-action="add"')).toBeGreaterThan(lastCase);
expect(html.indexOf('data-case-action="settings"')).toBeGreaterThan(html.indexOf('data-case-action="add"'));
app.openCasePicker('no-such-case');
expect(elements.quickStartCaseList.innerHTML).toContain('No cases match');
expect(elements.quickStartCaseList.innerHTML).toContain('data-case-action="add"');
// The arrow keys walk past the cases (Alpha, testcase, zeta) onto the rows; Enter runs one.
app.openCasePicker('');
for (let i = 0; i < 3; i++) listeners.keydown({ key: 'ArrowDown', preventDefault: vi.fn() });
expect(elements.quickStartCaseList.innerHTML).toMatch(
/class="case-combobox-action active"[^>]*data-case-action="add"/
);
listeners.keydown({ key: 'Enter', preventDefault: vi.fn() });
expect(app.showCreateCaseModal).toHaveBeenCalledTimes(1);
expect(app.run).not.toHaveBeenCalled();
// A click on the settings row opens the case settings popover.
app.openCasePicker('');
const settingsRow = { dataset: { caseAction: 'settings' } };
listClick!({ target: { closest: (sel: string) => (sel === '.case-combobox-action' ? settingsRow : null) } });
expect(app.toggleCaseSettings).toHaveBeenCalledTimes(1);
// The toolbar buttons themselves are gone.
const html2 = readFileSync(resolve(import.meta.dirname, '../src/web/public/index.html'), 'utf8');
expect(html2).not.toContain('class="btn-case-add"');
expect(html2).not.toContain('class="btn-case-settings"');
});
it('creates remote shell sessions by caseName instead of remote display path', async () => {
const elements: Record<string, any> = {
quickStartCase: { value: 'gpu-work' },