From 6c74c6dafac2646433e127acccebb15e4cd3b4bc Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 5 Jun 2026 11:56:47 +0200 Subject: [PATCH 1/2] feat(agent): model quick-picker in overflow menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 in the compose box. The overflow menu provides a discoverability path for operators who don't remember slash commands. --- frontend/packages/agent/src/agent.css | 26 ++++++++++++ frontend/packages/agent/src/app.js | 59 +++++++++++++++++++++++++-- 2 files changed, 81 insertions(+), 4 deletions(-) diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index 544cfc27..2636b4ba 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -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 { diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index b9320108..c47683df 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -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 From df1ffdd5046a2ceb44fa41b80c133f137e2e4de7 Mon Sep 17 00:00:00 2001 From: iris Date: Fri, 5 Jun 2026 13:01:27 +0200 Subject: [PATCH 2/2] docs(agent): document model quick-picker in overflow menu --- docs/web-ui/agent.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/web-ui/agent.md b/docs/web-ui/agent.md index b9bbd8b7..c8271f67 100644 --- a/docs/web-ui/agent.md +++ b/docs/web-ui/agent.md @@ -63,18 +63,28 @@ through. Three flex columns: click opens the loose-ends flyout. - **Overflow button** (`⋯`): always visible. Opens a frosted popover (`#overflow-menu`, positioned outside the header to escape any - stacking context) with four rows: `↑ dashboard` (link), `↻ rebuild - container` (POST confirm, same action as the dashboard R3BU1LD - button), `↻ new claude session` (POST confirm → `POST - /api/new-session`; next turn drops `--continue`), `🔓 logout` - (POST confirm → `POST /api/logout`; SIGINTs any in-flight turn, - wipes OAuth credential files, flips the agent to `needs_login` - — session history preserved). All destructive actions require - one extra click to acknowledge — rare ops shouldn't live in the - primary state strip. The popover's display rules are scoped to - `:not([hidden])` so the `[hidden]` HTML attribute's UA `display: - none` isn't overridden by the author CSS's `display: flex` — - the popover stays hidden until JS removes the attribute. + stacking context) with four management rows followed by a model + quick-picker section: `↑ dashboard` (link), `↻ rebuild container` + (POST confirm, same action as the dashboard R3BU1LD button), + `↻ new claude session` (POST confirm → `POST /api/new-session`; + next turn drops `--continue`), `🔓 logout` (POST confirm → `POST + /api/logout`; SIGINTs any in-flight turn, wipes OAuth credential + files, flips the agent to `needs_login` — session history + preserved). All destructive actions require one extra click to + acknowledge — rare ops shouldn't live in the primary state strip. + Below a separator, a **model quick-picker** section labelled + `model` renders three one-click shortcuts: `haiku (fast)`, + `sonnet (balanced)`, `opus (powerful)`. Clicking a button POSTs + `/api/model` with the alias (same path as the `/model ` + slash command). The button for the currently-active model is + highlighted via the `active` class; `renderModelChip` keeps the + picker state in sync with live `model_changed` events so it stays + accurate when the model is changed from another session. Clicking + the already-active model closes the menu without an extra POST. + The popover's display rules are scoped to `:not([hidden])` so the + `[hidden]` HTML attribute's UA `display: none` isn't overridden by + the author CSS's `display: flex` — the popover stays hidden until + JS removes the attribute. `/api/state` is fetched once on cold load (+ while `status === 'needs_login_in_progress'`); all other updates arrive via