feat(agent): model quick-picker in overflow menu
Adds three one-click model shortcuts (haiku / sonnet / opus) at the bottom of the agent overflow menu (⋯), below a separator and a small 'model' section label. - The active model alias is highlighted with .active (purple border + tint) so the operator can see at a glance which model is current. Matching tolerates both short aliases (haiku) and full API names (claude-3-5-haiku-20241022) via an endsWith check. - Clicking the active model is a no-op (closes menu, no POST). - Clicking a different alias calls postModel() and closes the menu. - renderModelChip() keeps the picker buttons in sync on every /api/state refresh without rebuilding the menu. Before this change the operator had to type /model <name> in the compose box. The overflow menu provides a discoverability path for operators who don't remember slash commands.
This commit is contained in:
parent
1c4619015d
commit
6c74c6dafa
2 changed files with 81 additions and 4 deletions
|
|
@ -263,6 +263,32 @@ h2, h3 {
|
|||
opacity: 0.4;
|
||||
cursor: progress;
|
||||
}
|
||||
.overflow-sep {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 0.3em 0.4em;
|
||||
}
|
||||
.overflow-section-label {
|
||||
font-size: 0.72em;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--muted);
|
||||
padding: 0 0.7em 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.overflow-item-model {
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.overflow-item-model:hover {
|
||||
color: var(--fg);
|
||||
background: rgba(203, 166, 247, 0.08);
|
||||
border-color: var(--purple-dim);
|
||||
}
|
||||
.overflow-item-model.active {
|
||||
color: var(--purple);
|
||||
border-color: var(--purple-dim);
|
||||
background: rgba(203, 166, 247, 0.06);
|
||||
}
|
||||
|
||||
/* Header pill — inbox / loose-ends triggers. Compact, count-prominent. */
|
||||
.header-pill {
|
||||
|
|
|
|||
|
|
@ -272,6 +272,39 @@ window.marked = marked;
|
|||
});
|
||||
menu.append(logoutBtn);
|
||||
|
||||
// ─── model quick-picker ────────────────────────────────────────
|
||||
// Three one-click shortcuts for the most common model aliases.
|
||||
// The active model is highlighted via the `active` class; see
|
||||
// `renderModelChip` which updates `modelPickerBtns` live.
|
||||
const modelSep = el('div', { class: 'overflow-sep', 'aria-hidden': 'true' });
|
||||
const modelLabel = el('div', { class: 'overflow-section-label' }, 'model');
|
||||
menu.append(modelSep, modelLabel);
|
||||
const MODEL_ALIASES = [
|
||||
{ name: 'haiku', label: 'haiku (fast)' },
|
||||
{ name: 'sonnet', label: 'sonnet (balanced)' },
|
||||
{ name: 'opus', label: 'opus (powerful)' },
|
||||
];
|
||||
modelPickerBtns = [];
|
||||
for (const m of MODEL_ALIASES) {
|
||||
const btn = el('button', {
|
||||
type: 'button',
|
||||
class: 'overflow-item overflow-item-model',
|
||||
role: 'menuitem',
|
||||
title: `/model ${m.name}`,
|
||||
'data-model': m.name,
|
||||
},
|
||||
el('span', { class: 'overflow-item-icon', 'aria-hidden': 'true' }, '⊞'),
|
||||
m.label,
|
||||
);
|
||||
btn.addEventListener('click', () => {
|
||||
if (currentModel === m.name) { closeOverflowMenu(); return; }
|
||||
closeOverflowMenu();
|
||||
postModel(m.name);
|
||||
});
|
||||
modelPickerBtns.push(btn);
|
||||
menu.append(btn);
|
||||
}
|
||||
|
||||
overflowMenuPopulated = true;
|
||||
}
|
||||
|
||||
|
|
@ -431,6 +464,13 @@ window.marked = marked;
|
|||
// bus-driven `status_changed` handler so it can re-enable the
|
||||
// composer without waiting for the next snapshot fetch.
|
||||
let currentLabel = '';
|
||||
// Tracked so the overflow-menu model picker can highlight the active
|
||||
// model and avoid a redundant `/api/model` POST on same-model click.
|
||||
let currentModel = null;
|
||||
// References to the model picker buttons populated in the overflow
|
||||
// menu so renderModelChip can update their `active` state without
|
||||
// rebuilding the whole menu.
|
||||
let modelPickerBtns = [];
|
||||
|
||||
const SLASH_COMMANDS = [
|
||||
{ name: '/help', desc: 'list slash commands' },
|
||||
|
|
@ -969,12 +1009,23 @@ window.marked = marked;
|
|||
}
|
||||
|
||||
function renderModelChip(model) {
|
||||
currentModel = model || null;
|
||||
const el_ = $('model-chip');
|
||||
if (!el_) return;
|
||||
if (!model) { el_.hidden = true; return; }
|
||||
el_.hidden = false;
|
||||
el_.textContent = 'model · ' + model;
|
||||
el_.title = `claude --model ${model}\nset via the operator's /model command; persists across turns until changed`;
|
||||
if (!model) { el_.hidden = true; } else {
|
||||
el_.hidden = false;
|
||||
el_.textContent = 'model · ' + model;
|
||||
el_.title = `claude --model ${model}\nset via the operator's /model command; persists across turns until changed`;
|
||||
}
|
||||
// Sync model picker buttons: highlight the active alias. A short alias
|
||||
// (haiku/sonnet/opus) matches if `model` ends with that string; full
|
||||
// API names like `claude-3-5-haiku-20241022` match on the suffix too.
|
||||
for (const btn of modelPickerBtns) {
|
||||
const alias = btn.dataset.model || '';
|
||||
const isActive = !!model && (model === alias || model.endsWith(alias));
|
||||
btn.classList.toggle('active', isActive);
|
||||
btn.setAttribute('aria-pressed', String(isActive));
|
||||
}
|
||||
}
|
||||
// Token badges — two separate chips:
|
||||
// ctx · N last inference's prompt size = current context window
|
||||
|
|
|
|||
Loading…
Reference in a new issue