mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19:42 +02:00
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:
@@ -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 |
|
||||
| ----------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">⚙</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()">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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)); }
|
||||
|
||||
@@ -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' },
|
||||
|
||||
Reference in New Issue
Block a user