mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-11-15 02:49:33 +01:00
feat(palette): choose the agent when starting a session from Cmd+K
The palette's "New session" row now shows the agent it will start and lets Left/Right (or the row's arrows) switch it. The agents come from the injected CLI catalog, filtered to enabled and installed entries, so no CLI id list lives in the palette. The pick is a one-shot launch through run() with a temporary _runMode swap: the Run button's mode is left unchanged.
This commit is contained in:
1 parent
002de32a08
commit
c6a5995ac6
4 files changed
+305
-5
No files matched your search
+111
-5
@@ -424,6 +424,7 @@ Object.assign(CodemanApp.prototype, {
|
||||
if (!modal || !search) return;
|
||||
|
||||
this.commandPaletteActiveIndex = 0;
|
||||
this._resetCommandPaletteAgent();
|
||||
search.value = '';
|
||||
modal.classList.add('active');
|
||||
|
||||
@@ -465,6 +466,11 @@ Object.assign(CodemanApp.prototype, {
|
||||
});
|
||||
|
||||
search?.addEventListener('keydown', async (e) => {
|
||||
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && this._commandPaletteArrowCyclesAgent(e)) {
|
||||
e.preventDefault();
|
||||
this._cycleCommandPaletteAgent(e.key === 'ArrowRight' ? 1 : -1);
|
||||
return;
|
||||
}
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
this.moveCommandPaletteSelection(1);
|
||||
@@ -494,7 +500,22 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
});
|
||||
|
||||
// Clicking an agent arrow must leave focus in the search box, or the
|
||||
// Left/Right/Enter keys stop reaching the palette after the first click.
|
||||
list?.addEventListener?.('mousedown', (e) => {
|
||||
if (e.target?.closest?.('[data-palette-agent-cycle]')) e.preventDefault?.();
|
||||
});
|
||||
|
||||
list?.addEventListener?.('click', (e) => {
|
||||
// The agent arrows sit inside the new-session row: they change the agent
|
||||
// and must not also activate (launch) the row they are drawn in.
|
||||
const cycle = e.target?.closest?.('[data-palette-agent-cycle]');
|
||||
if (cycle) {
|
||||
e.preventDefault?.();
|
||||
e.stopPropagation?.();
|
||||
this._cycleCommandPaletteAgent(cycle.dataset.paletteAgentCycle === 'prev' ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
const row = e.target?.closest?.('[data-command-index]');
|
||||
if (!row) return;
|
||||
this.commandPaletteActiveIndex = Number(row.dataset.commandIndex) || 0;
|
||||
@@ -535,18 +556,75 @@ Object.assign(CodemanApp.prototype, {
|
||||
},
|
||||
|
||||
_buildCommandPaletteNewSessionItem(query = '') {
|
||||
const mode = this.runMode || this._runMode || 'claude';
|
||||
const agent = this._commandPaletteAgent();
|
||||
const mode = agent?.id || this.runMode || this._runMode || 'claude';
|
||||
const labels = { claude: 'Claude', opencode: 'OpenCode', codex: 'Codex', gemini: 'Gemini', antigravity: 'Antigravity', pi: 'Pi', grok: 'Grok', deepseek: 'DeepSeek', omp: 'OMP', copilot: 'Copilot' };
|
||||
const caseName = this._findCommandPaletteCaseMatch(query) || document.getElementById('quickStartCase')?.value || 'testcase';
|
||||
return {
|
||||
id: 'new-session',
|
||||
type: 'new-session',
|
||||
caseName,
|
||||
mode,
|
||||
title: 'New session',
|
||||
subtitle: `Run ${labels[mode] || mode} in ${caseName}`,
|
||||
subtitle: `Run ${agent?.label || labels[mode] || mode} in ${caseName}`,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* The agents the new-session row can start: the server-injected CLI catalog's
|
||||
* enabled entries that are installed here, in catalog order, the same list the
|
||||
* welcome screen's Run buttons offer (renderWelcomeCliActions). The ids are
|
||||
* DATA from the registry, never a list kept here. Empty on a page rendered
|
||||
* without the catalog, which leaves the row on the Run button's mode.
|
||||
*/
|
||||
_commandPaletteAgentChoices() {
|
||||
const catalog = typeof window !== 'undefined' && Array.isArray(window.__codemanCliCatalog) ? window.__codemanCliCatalog : [];
|
||||
return catalog
|
||||
.filter((cli) => cli && typeof cli.id === 'string' && cli.enabled && (this.isCliAvailable?.(cli.id) ?? true))
|
||||
.map((cli) => ({ id: cli.id, label: (typeof cli.label === 'string' && cli.label.trim()) || cli.id }));
|
||||
},
|
||||
|
||||
/** The agent the new-session row will start, or null when the catalog offers none. */
|
||||
_commandPaletteAgent() {
|
||||
const choices = this._commandPaletteAgentChoices();
|
||||
if (!choices.length) return null;
|
||||
return choices.find((c) => c.id === this._commandPaletteAgentId) || choices[0];
|
||||
},
|
||||
|
||||
/** Each time the palette opens, the row starts on the Run button's mode (or the first offered agent). */
|
||||
_resetCommandPaletteAgent() {
|
||||
const choices = this._commandPaletteAgentChoices();
|
||||
const current = this.runMode || this._runMode;
|
||||
this._commandPaletteAgentId = (choices.find((c) => c.id === current) || choices[0])?.id || null;
|
||||
},
|
||||
|
||||
_cycleCommandPaletteAgent(delta) {
|
||||
const choices = this._commandPaletteAgentChoices();
|
||||
if (choices.length < 2) return;
|
||||
const at = Math.max(0, choices.findIndex((c) => c.id === this._commandPaletteAgent()?.id));
|
||||
this._commandPaletteAgentId = choices[(((at + delta) % choices.length) + choices.length) % choices.length].id;
|
||||
this.renderCommandPalette();
|
||||
},
|
||||
|
||||
/**
|
||||
* Left/Right change the agent only while the new-session row is highlighted,
|
||||
* and only when the key would not move the caret anyway (Left at the start of
|
||||
* the query, Right at its end, no selection), so editing a typed case name
|
||||
* keeps working.
|
||||
*/
|
||||
_commandPaletteArrowCyclesAgent(e) {
|
||||
const active = (this.commandPaletteItems || [])[this.commandPaletteActiveIndex || 0];
|
||||
if (active?.type !== 'new-session') return false;
|
||||
if (this._commandPaletteAgentChoices().length < 2) return false;
|
||||
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return false;
|
||||
const input = e.target;
|
||||
const value = typeof input?.value === 'string' ? input.value : '';
|
||||
const start = Number.isFinite(input?.selectionStart) ? input.selectionStart : value.length;
|
||||
const end = Number.isFinite(input?.selectionEnd) ? input.selectionEnd : start;
|
||||
if (start !== end) return false;
|
||||
return e.key === 'ArrowRight' ? end >= value.length : start <= 0;
|
||||
},
|
||||
|
||||
_findCommandPaletteCaseMatch(query = '') {
|
||||
const needle = query.trim().toLowerCase();
|
||||
if (!needle || !Array.isArray(this.cases)) return null;
|
||||
@@ -600,19 +678,38 @@ Object.assign(CodemanApp.prototype, {
|
||||
const active = index === this.commandPaletteActiveIndex ? ' active' : '';
|
||||
const icon = item.type === 'new-session' ? '+' : item.type === 'browse-sessions' ? '≡' : '›';
|
||||
const browse = item.type === 'browse-sessions' ? ' command-palette-item--browse' : '';
|
||||
const agent = item.type === 'new-session' ? this._renderCommandPaletteAgentPicker(item.mode) : '';
|
||||
return `
|
||||
<button class="command-palette-item${active}${browse}" type="button" data-command-index="${index}">
|
||||
<button class="command-palette-item${active}${browse}${agent ? ' command-palette-item--agent' : ''}" type="button" data-command-index="${index}">
|
||||
<span class="command-palette-icon" aria-hidden="true">${icon}</span>
|
||||
<span class="command-palette-text">
|
||||
<span class="command-palette-title">${escapeHtml(item.title)}</span>
|
||||
<span class="command-palette-subtitle">${escapeHtml(item.subtitle || '')}</span>
|
||||
</span>
|
||||
</span>${agent}
|
||||
</button>
|
||||
`;
|
||||
})
|
||||
.join('');
|
||||
},
|
||||
|
||||
/**
|
||||
* The new-session row's agent switch: arrows either side of the agent's logo
|
||||
* and name. Spans, not buttons: the row itself is a <button>. Nothing is drawn
|
||||
* when there is nothing to choose between.
|
||||
*/
|
||||
_renderCommandPaletteAgentPicker(mode) {
|
||||
const choices = this._commandPaletteAgentChoices();
|
||||
if (choices.length < 2) return '';
|
||||
const agent = choices.find((c) => c.id === mode) || choices[0];
|
||||
return `
|
||||
<span class="command-palette-agent" title="Agent for the new session (Left/Right to change)">
|
||||
<span class="command-palette-agent-arrow" role="button" data-palette-agent-cycle="prev" aria-label="Previous agent">‹</span>
|
||||
<span class="run-mode-dot ${escapeHtml(agent.id)}" aria-hidden="true"></span>
|
||||
<span class="command-palette-agent-label">${escapeHtml(agent.label)}</span>
|
||||
<span class="command-palette-agent-arrow" role="button" data-palette-agent-cycle="next" aria-label="Next agent">›</span>
|
||||
</span>`;
|
||||
},
|
||||
|
||||
moveCommandPaletteSelection(delta) {
|
||||
const items = this.commandPaletteItems || this.buildCommandPaletteItems(document.getElementById('commandPaletteSearch')?.value || '');
|
||||
if (!items.length) return;
|
||||
@@ -655,7 +752,16 @@ Object.assign(CodemanApp.prototype, {
|
||||
caseSelect.value = item.caseName;
|
||||
}
|
||||
}
|
||||
await this.run();
|
||||
// A one-shot launch of the picked agent: the same temporary _runMode swap
|
||||
// the custom-model entries and worktree launches use, so run() keeps its
|
||||
// launch lock and per-mode paths and the Run button's mode is untouched.
|
||||
const previousRunMode = this._runMode;
|
||||
if (item.mode) this._runMode = item.mode;
|
||||
try {
|
||||
await this.run();
|
||||
} finally {
|
||||
this._runMode = previousRunMode;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -8970,6 +8970,37 @@ kbd {
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
/* New-session row: the agent it will start, switchable with Left/Right or the arrows */
|
||||
.command-palette-item--agent {
|
||||
grid-template-columns: 26px minmax(0, 1fr) auto;
|
||||
}
|
||||
.command-palette-agent {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
padding: 0.15rem 0.2rem;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.command-palette-agent-arrow {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text-muted);
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.command-palette-agent-arrow:hover {
|
||||
color: var(--text);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* COD-192: "Browse all sessions…" escape hatch — visually secondary */
|
||||
.command-palette-item--browse {
|
||||
margin-top: 4px;
|
||||
|
||||
Reference in new issue
Block a user