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
+3 -2
View File
@@ -722,8 +722,9 @@
<select id="quickStartCase" class="toolbar-select case-native-select" title="Select case" aria-hidden="true" tabindex="-1">
<option value="testcase">testcase</option>
</select>
<button class="btn-case-add" onclick="app.showCreateCaseModal()" title="Create new case">+</button>
<button class="btn-case-settings" onclick="app.toggleCaseSettings()" title="Case settings">&#x2699;</button>
<!-- "New or link a case…" and "Case settings…" are the last two rows of
the case picker's own list (CASE_PICKER_ACTIONS, session-ui.js); the
"+" and gear buttons that sat here were removed (owner, 1.36.0). -->
<div class="case-settings-popover hidden" id="caseSettingsPopover">
<label class="checkbox-inline">
<input type="checkbox" id="caseAgentTeams" onchange="app.onCaseSettingChanged()">
+2 -38
View File
@@ -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);
+61 -7
View File
@@ -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 `
<button
type="button"
id="quickStartCaseOption-${index}"
class="case-combobox-action ${active ? 'active' : ''}"
role="option"
aria-selected="false"
data-case-action="${action.id}">
<span class="case-combobox-action-icon" aria-hidden="true">${action.icon}</span>
<span class="case-combobox-option-label">${escapeHtml(action.label)}</span>
</button>
`;
}).join('');
const actionsBlock = `<div class="case-combobox-actions" role="presentation">${actionRows}</div>`;
if (options.length === 0) {
list.innerHTML = '<div class="case-combobox-empty">No cases match</div>';
list.innerHTML = '<div class="case-combobox-empty">No cases match</div>' + 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, {
</button>
`;
})
.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);
}
+44 -46
View File
@@ -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)); }