diff --git a/docs/wiki/Core-Concepts.md b/docs/wiki/Core-Concepts.md
index 3695340f..7f1a6404 100644
--- a/docs/wiki/Core-Concepts.md
+++ b/docs/wiki/Core-Concepts.md
@@ -15,7 +15,8 @@ Codeman-side configuration:
- Per-case toggles (Agent Teams, 1M Opus context).
- Where it runs, if it is not the local filesystem: see [Location overlays](#location-overlays).
-Three ways to get one, all under **+** next to the case picker:
+Three ways to get one, all under **New or link a case…** at the bottom of the case picker
+(the case dropdown in the bottom toolbar; on a phone, the case sheet's **Create New Case**):
| How | Result |
| ----------------- | ------------------------------------------------------------------------------------------------------ |
diff --git a/docs/wiki/Watching-Agents-Work.md b/docs/wiki/Watching-Agents-Work.md
index f982d355..e746acf9 100644
--- a/docs/wiki/Watching-Agents-Work.md
+++ b/docs/wiki/Watching-Agents-Work.md
@@ -47,7 +47,8 @@ in the CLI's own environment:
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
```
-and turn the per-case **Agent Teams** toggle on in the case settings gear.
+and turn the per-case **Agent Teams** toggle on under **Case settings…** at the bottom of the case
+picker (the gear beside the case button on a phone).
Codeman watches the team directory and matches teammates to the session leading them.
Teammates are in-process threads rather than separate CLI processes, so they show up as
diff --git a/src/web/public/index.html b/src/web/public/index.html
index bdb5f9b9..994e08c2 100644
--- a/src/web/public/index.html
+++ b/src/web/public/index.html
@@ -722,8 +722,9 @@
testcase
- +
- ⚙
+
diff --git a/src/web/public/mobile.css b/src/web/public/mobile.css
index a96e3b20..af764a67 100644
--- a/src/web/public/mobile.css
+++ b/src/web/public/mobile.css
@@ -1468,15 +1468,6 @@ html.mobile-init .file-browser-panel {
padding: 6px 14px;
}
- /* Add case button - compact (keeping for when shown via menu) */
- .btn-case-add {
- display: none !important;
- }
-
- .btn-case-settings {
- display: none !important;
- }
-
/* When keyboard is visible, also move accessory bar up */
.keyboard-visible .keyboard-accessory-bar.visible {
/* Position is handled by JS transform along with toolbar */
@@ -1490,33 +1481,6 @@ html.mobile-init .file-browser-panel {
display: none !important;
}
- /* Keep btn-case-add styling for tablet/future use */
- .toolbar .btn-case-add {
- min-width: 26px !important;
- max-width: 26px !important;
- width: 26px !important;
- min-height: 26px !important;
- max-height: 26px !important;
- height: 26px !important;
- padding: 0 !important;
- font-size: 0.9rem;
- font-weight: bold;
- display: inline-flex !important;
- align-items: center;
- justify-content: center;
- line-height: 1;
- border-radius: 4px;
- background: transparent;
- border: 1px solid rgba(255, 255, 255, 0.15);
- color: #9ca3af;
- }
-
- .btn-case-add:hover,
- .btn-case-add:active {
- background: rgba(255, 255, 255, 0.1);
- color: #fff;
- }
-
/* Panels on mobile - full width, positioned above toolbar, visibility controlled by JS */
.monitor-panel,
.subagents-panel {
@@ -3231,13 +3195,13 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
both stylesheets and repainted the armed modifier back to a resting button on
all four light skins. Excluding the state here fixes phone and tablet at once;
adding a class to the armed rules would only have moved the tie. */
-html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(.btn-voice-mobile, .btn-settings-mobile, .btn-toolbar.btn-shell, .toolbar .btn-case-add, .accessory-btn:not(.armed)) {
+html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(.btn-voice-mobile, .btn-settings-mobile, .btn-toolbar.btn-shell, .accessory-btn:not(.armed)) {
background: var(--control-bg);
border-color: var(--control-border);
color: var(--text-dim);
}
-html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(.btn-voice-mobile:active, .btn-settings-mobile:active, .btn-toolbar.btn-shell:hover, .btn-toolbar.btn-shell:active, .btn-case-add:hover, .btn-case-add:active, .accessory-btn:active) {
+html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="catppuccin-latte"], [data-skin="rose-pine-dawn"]) :is(.btn-voice-mobile:active, .btn-settings-mobile:active, .btn-toolbar.btn-shell:hover, .btn-toolbar.btn-shell:active, .accessory-btn:active) {
background: var(--control-bg-hover);
border-color: var(--control-border-hover);
color: var(--text);
diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js
index 7eee2b09..614f3939 100644
--- a/src/web/public/session-ui.js
+++ b/src/web/public/session-ui.js
@@ -125,6 +125,17 @@ const RUN_MODE_LAUNCH = {
/** How often the OPEN case picker re-reads /api/cases (it also refreshes once on open). */
const CASE_PICKER_REFRESH_MS = 5000;
+/**
+ * Action rows at the bottom of the toolbar case picker. They replaced the "+"
+ * and gear buttons that sat beside the picker (owner, 1.36.0 beta): the same two
+ * actions, one click away, without two extra controls in the toolbar. The arrow
+ * keys reach them after the last case; Enter or a click runs `run`.
+ */
+const CASE_PICKER_ACTIONS = [
+ { id: 'add', icon: '+', label: 'New or link a case…', run: (app) => app.showCreateCaseModal() },
+ { id: 'settings', icon: '\u2699', label: 'Case settings…', run: (app) => app.toggleCaseSettings() },
+];
+
const EXTERNAL_CLI_MODES = new Set(Object.keys(RUN_MODE_LAUNCH));
const BUILT_IN_RUN_MODES = new Set(['claude', 'shell', ...Object.keys(RUN_MODE_LAUNCH)]);
@@ -343,13 +354,32 @@ Object.assign(CodemanApp.prototype, {
const options = this.filterCasePickerOptions(this.getCasePickerOptions(), this._casePickerFilter || '');
const selectedName = select.value || 'testcase';
- const maxIndex = Math.max(0, options.length - 1);
+ // The arrow keys walk the cases, then the action rows under them.
+ const maxIndex = options.length + CASE_PICKER_ACTIONS.length - 1;
this._casePickerActiveIndex = Math.min(Math.max(this._casePickerActiveIndex || 0, 0), maxIndex);
+ const actionRows = CASE_PICKER_ACTIONS.map((action, i) => {
+ const index = options.length + i;
+ const active = index === this._casePickerActiveIndex;
+ return `
+
+ ${action.icon}
+ ${escapeHtml(action.label)}
+
+ `;
+ }).join('');
+ const actionsBlock = `${actionRows}
`;
+
if (options.length === 0) {
- list.innerHTML = 'No cases match
';
+ list.innerHTML = 'No cases match
' + actionsBlock;
list.classList.remove('hidden');
- input.removeAttribute('aria-activedescendant');
+ input.setAttribute('aria-activedescendant', `quickStartCaseOption-${this._casePickerActiveIndex}`);
return;
}
@@ -372,11 +402,22 @@ Object.assign(CodemanApp.prototype, {
`;
})
- .join('');
+ .join('') + actionsBlock;
list.classList.remove('hidden');
input.setAttribute('aria-activedescendant', `quickStartCaseOption-${this._casePickerActiveIndex}`);
},
+ /** Run a case picker action row (`CASE_PICKER_ACTIONS`): close the list first, then act. */
+ runCasePickerAction(id) {
+ const action = CASE_PICKER_ACTIONS.find((a) => a.id === id);
+ if (!action) return;
+ const select = document.getElementById('quickStartCase');
+ if (select) this.updateCasePickerInput(select.value);
+ this.closeCasePicker();
+ document.getElementById('quickStartCaseSearch')?.blur?.();
+ action.run(this);
+ },
+
selectQuickStartCase(caseName, { save = true } = {}) {
const select = document.getElementById('quickStartCase');
if (!select) return;
@@ -419,18 +460,26 @@ Object.assign(CodemanApp.prototype, {
const options = this.filterCasePickerOptions(this.getCasePickerOptions(), this._casePickerFilter || input.value);
if (event.key === 'ArrowDown') {
event.preventDefault();
- this._casePickerActiveIndex = Math.min((this._casePickerActiveIndex || 0) + 1, Math.max(0, options.length - 1));
+ this._casePickerActiveIndex = Math.min(
+ (this._casePickerActiveIndex || 0) + 1,
+ options.length + CASE_PICKER_ACTIONS.length - 1
+ );
this._casePickerOpen ? this.renderCasePickerList() : this.openCasePicker(input.value);
} else if (event.key === 'ArrowUp') {
event.preventDefault();
this._casePickerActiveIndex = Math.max((this._casePickerActiveIndex || 0) - 1, 0);
this._casePickerOpen ? this.renderCasePickerList() : this.openCasePicker(input.value);
} else if (event.key === 'Enter') {
- const option = options[this._casePickerActiveIndex || 0];
+ const index = this._casePickerActiveIndex || 0;
+ const option = options[index];
+ const action = this._casePickerOpen ? CASE_PICKER_ACTIONS[index - options.length] : undefined;
if (option) {
event.preventDefault();
this.selectQuickStartCase(option.name);
this.run?.();
+ } else if (action) {
+ event.preventDefault();
+ this.runCasePickerAction(action.id);
}
} else if (event.key === 'Escape') {
event.preventDefault();
@@ -443,6 +492,11 @@ Object.assign(CodemanApp.prototype, {
});
list.addEventListener('mousedown', event => event.preventDefault());
list.addEventListener('click', event => {
+ const action = event.target.closest?.('.case-combobox-action');
+ if (action?.dataset?.caseAction) {
+ this.runCasePickerAction(action.dataset.caseAction);
+ return;
+ }
const option = event.target.closest?.('.case-combobox-option');
if (option?.dataset?.case) {
this.selectQuickStartCase(option.dataset.case);
@@ -3083,7 +3137,7 @@ Object.assign(CodemanApp.prototype, {
// Close on outside click (one-shot listener)
const closeHandler = (e) => {
- if (!popover.contains(e.target) && !e.target.classList.contains('btn-case-settings')) {
+ if (!popover.contains(e.target) && !e.target.closest?.('.case-combobox-action')) {
popover.classList.add('hidden');
document.removeEventListener('click', closeHandler);
}
diff --git a/src/web/public/styles.css b/src/web/public/styles.css
index d7b08472..3e83742b 100644
--- a/src/web/public/styles.css
+++ b/src/web/public/styles.css
@@ -6269,7 +6269,7 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
padding: 0.4rem 1.6rem 0.4rem 0.65rem;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.07);
- border-radius: var(--btn-radius) 0 0 var(--btn-radius);
+ border-radius: var(--btn-radius);
color: var(--text-dim);
font-size: 0.75rem;
font-family: inherit;
@@ -6354,6 +6354,49 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
text-align: center;
}
+/* The picker's own action rows ("New or link a case…", "Case settings…"),
+ which replaced the "+" and gear buttons beside it. Sticky at the bottom so
+ a long case list scrolls under them, a hairline above, quieter than a case. */
+.case-combobox-actions {
+ position: sticky;
+ bottom: -0.35rem;
+ margin: 0.3rem -0.35rem -0.35rem;
+ padding: 0.3rem 0.35rem 0.35rem;
+ border-top: 1px solid var(--control-border, rgba(255, 255, 255, 0.08));
+ background: inherit;
+ border-radius: 0 0 8px 8px;
+}
+
+.case-combobox-action {
+ display: grid;
+ grid-template-columns: 18px minmax(0, 1fr);
+ align-items: center;
+ width: 100%;
+ min-height: 30px;
+ padding: 0.3rem 0.45rem;
+ background: transparent;
+ border: 1px solid transparent;
+ border-radius: 6px;
+ color: var(--text-muted);
+ font: inherit;
+ font-size: 0.75rem;
+ text-align: left;
+ cursor: pointer;
+}
+
+.case-combobox-action:hover,
+.case-combobox-action.active {
+ background: rgba(59, 130, 246, 0.16);
+ border-color: rgba(96, 165, 250, 0.28);
+ color: var(--text);
+}
+
+.case-combobox-action-icon {
+ color: var(--accent);
+ font-weight: 600;
+ text-align: center;
+}
+
.toolbar-select {
padding: 0.4rem 1.5rem 0.4rem 0.6rem;
background: rgba(255, 255, 255, 0.05);
@@ -6393,50 +6436,6 @@ body.touch-device .terminal-container .xterm .xterm-helper-textarea {
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.2);
}
-.btn-case-add {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.07);
- border-left: none;
- border-radius: 0;
- color: var(--green);
- font-size: 1.1rem;
- font-weight: 500;
- cursor: pointer;
- transition: all var(--transition-smooth);
-}
-
-.btn-case-add:hover {
- background: var(--green);
- color: white;
- border-color: var(--green);
- box-shadow: 0 0 8px rgba(34, 197, 94, 0.3);
-}
-
-.btn-case-settings {
- display: flex;
- align-items: center;
- justify-content: center;
- width: 28px;
- background: rgba(255, 255, 255, 0.05);
- border: 1px solid rgba(255, 255, 255, 0.07);
- border-left: none;
- border-radius: 0 var(--btn-radius) var(--btn-radius) 0;
- color: var(--text-dim);
- font-size: 1rem;
- cursor: pointer;
- transition: all var(--transition-smooth);
-}
-
-.btn-case-settings:hover {
- background: var(--accent);
- color: white;
- border-color: var(--accent);
-}
-
.case-settings-popover {
position: absolute;
bottom: 100%;
@@ -15885,7 +15884,6 @@ html:not([data-skin="og"]) {
border-color: var(--accent);
box-shadow: 0 0 0 1px rgba(var(--accent-rgb), 0.25);
}
-.btn-case-add { color: var(--accent-soft); }
/* ---- Header system stats: cohesive accent meters ---- */
.stat-bar-mem { background: linear-gradient(90deg, var(--accent), var(--accent-hover)); }
diff --git a/test/run-mode-ui.test.ts b/test/run-mode-ui.test.ts
index 21c1c8af..d5555ad3 100644
--- a/test/run-mode-ui.test.ts
+++ b/test/run-mode-ui.test.ts
@@ -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 = {};
+ const listeners: Record 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 = {
quickStartCase: { value: 'gpu-work' },