diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index a8cfaff2..ae22d0b3 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -622,10 +622,9 @@ frontend reads. Operator-local preferences. State lives in the browser's `localStorage` — preferences do NOT sync between devices and -do NOT survive a profile wipe. Today the tab holds one section -(browser notifications); future preferences (theme, density, -etc.) land here as sibling `

` blocks -under the same `
`. +do NOT survive a profile wipe. Two sections today (browser +notifications, agent terminal); future preferences (theme, density, +etc.) land here as sibling `

` blocks in `settings.html`. **◇ browser notifications** — `🔔 enable notifications` button when permission ungranted; `🔕 mute / 🔔 unmute` toggle once granted @@ -636,6 +635,19 @@ single status line explains why. See `### Browser notifications` below for the dispatch model + the three signals the dashboard emits OS notifications on. +**◇ agent terminal** — a single `☐/☑ expand tool output panels` +toggle button (`role="switch"`, live `aria-checked`). Controls whether +every per-agent page's terminal defaults otherwise-collapsed +`
` rows (long tool-results, Write/Edit diffs, …) open; rows +that already default open regardless (send/ask/answer/recv) are +unaffected either way. Pure client-side — the key + get/set live in +`@hive/shared/prefs.js` (`getExpandDetailsPref`/`setExpandDetailsPref`) +so this page and every agent page's `app.js` read/write the exact same +`localStorage` key without a backend field; the shared terminal +factory reads it live via its `expandDetails` option (see +docs/web-ui/shape.md::Shared terminal pane), so a change here applies +to any already-open agent tab's next rendered row without a reload. + The FL0W page does NOT host this pane — settings live only on the dashboard's S3TT1NGS tab (reach it via the FL0W page's `← home` back-link → Dashboard). Notifications still fire on the FL0W page when diff --git a/docs/web-ui/shape.md b/docs/web-ui/shape.md index bd657f15..0de8445c 100644 --- a/docs/web-ui/shape.md +++ b/docs/web-ui/shape.md @@ -115,7 +115,11 @@ event picture), `onBackfillDone?(count)` (one-shot after replay; `count=0` on failed/skipped fetch), `onStreamOpen?()` (fires on every EventSource (re)connect — use to re-sync snapshot-derived state after a reconnect gap), `pillAnchor?` (parent element for the "↓ N new" pill; defaults to -`logEl.parentElement`). +`logEl.parentElement`), `expandDetails?` (boolean or zero-arg function +returning one, re-read on every `api.details`/`api.detailsDiff` call rather +than captured once — lets a page default otherwise-collapsed panels open +per a live browser-local preference; renderers that force a row open +regardless, e.g. message-bearing tool_use, are unaffected either way). **Sticky-bottom + snap animation.** `stickToBottom` is the operator's intent: true means "keep snapping to bottom on every diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 33e0af8c..94661e0e 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -6,6 +6,7 @@ import { create as termCreate, linkify as termLinkify } from '@hive/shared/termi import { asyncBtn, bindAsyncForms } from '@hive/shared/forms.js'; import { themedConfirm } from '@hive/shared/modal.js'; import { el } from '@hive/shared/dom.js'; +import { getExpandDetailsPref } from '@hive/shared/prefs.js'; import '@hive/shared/side-panel.js'; // registers — side-effect import import { marked } from 'marked'; import DOMPurify from 'dompurify'; @@ -1613,6 +1614,9 @@ window.marked = marked; // (e.g. /agent//) still hits the right SSE upstream. historyUrl: 'events/history', streamUrl: 'events/stream', + // Re-read live (not captured once) so flipping the overflow-menu + // toggle mid-session affects the next rendered row immediately. + expandDetails: () => getExpandDetailsPref(), renderers: { turn_start(ev, api) { if (api.fromHistory) openTurnsFromHistory += 1; diff --git a/frontend/packages/dashboard/src/settings.html b/frontend/packages/dashboard/src/settings.html index 1f98dfec..7fd6ed28 100644 --- a/frontend/packages/dashboard/src/settings.html +++ b/frontend/packages/dashboard/src/settings.html @@ -31,6 +31,12 @@ + +

◇ agent terminal

+

applies to every agent's terminal viewed in this browser — whether otherwise-collapsed tool-output panels (long results, Write/Edit diffs, …) default open. rows that already default open (send/ask/answer/recv) are unaffected either way.

+
+ +
diff --git a/frontend/packages/dashboard/src/settings.js b/frontend/packages/dashboard/src/settings.js index c6d5fc29..3a4f4a6e 100644 --- a/frontend/packages/dashboard/src/settings.js +++ b/frontend/packages/dashboard/src/settings.js @@ -1,15 +1,37 @@ // /settings.html — operator-local preferences page. // -// Extracted from the dashboard S3TT1NGS tab. Today the only setting is -// the browser-notification toggle: NOTIF.bind() wires the enable / mute / -// unmute buttons and persists state to localStorage. The dashboard's -// NOTIF.show() calls (approvals / questions) read that same localStorage -// state + the browser-level permission, so firing still works from the -// dashboard even though the toggle UI now lives on its own page. +// Extracted from the dashboard S3TT1NGS tab. The browser-notification +// toggle: NOTIF.bind() wires the enable / mute / unmute buttons and +// persists state to localStorage. The dashboard's NOTIF.show() calls +// (approvals / questions) read that same localStorage state + the +// browser-level permission, so firing still works from the dashboard +// even though the toggle UI now lives on its own page. +// +// The agent-terminal-verbosity toggle below is a second, unrelated +// browser-local preference — @hive/shared/prefs.js owns the key name + +// get/set so this page and every per-agent page's app.js agree on it +// without a backend field. Both preferences are per-origin localStorage, +// so they only apply within this browser (per the page's own copy). // // initServerWarnings() renders the shared top-of-page warnings banner, // matching the other stand-alone pages (FL0W / L0GS / H0M3). import { NOTIF, initServerWarnings } from './common.js'; +import { getExpandDetailsPref, setExpandDetailsPref } from '@hive/shared/prefs.js'; initServerWarnings(); NOTIF.bind(); + +(function bindExpandDetailsToggle() { + const btn = document.getElementById('expand-details-toggle'); + if (!btn) return; + function render() { + const on = getExpandDetailsPref(); + btn.textContent = on ? '☑ expand tool output panels' : '☐ expand tool output panels'; + btn.setAttribute('aria-checked', String(on)); + } + btn.addEventListener('click', () => { + setExpandDetailsPref(!getExpandDetailsPref()); + render(); + }); + render(); +})(); diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json index b0dde490..5134f006 100644 --- a/frontend/packages/shared/package.json +++ b/frontend/packages/shared/package.json @@ -17,6 +17,7 @@ "./terminal.css": "./src/terminal/terminal.css", "./chrome.css": "./src/chrome.css", "./forms.js": "./src/forms.js", + "./prefs.js": "./src/prefs.js", "./dom.js": "./src/dom.js", "./modal.js": "./src/modal.js", "./shadow-css.js": "./src/shadow-css.js", diff --git a/frontend/packages/shared/src/prefs.js b/frontend/packages/shared/src/prefs.js new file mode 100644 index 00000000..f73fb005 --- /dev/null +++ b/frontend/packages/shared/src/prefs.js @@ -0,0 +1,27 @@ +// Tiny browser-local (localStorage) preference helpers shared by the +// dashboard's /settings.html and the per-agent page — both need the +// exact same key name to actually talk to each other via the browser's +// shared per-origin storage (settings.html is where the operator sets +// the preference; every per-agent page's terminal reads it), so the +// get/set pair — and the key itself — live here once rather than as +// independent copies in each package that could drift out of sync. + +const EXPAND_DETAILS_KEY = 'hive-agent-expand-details'; + +// Whether a per-agent terminal's otherwise-collapsed `
` panels +// (long tool-results, Write/Edit diffs, …) should default open. Pure +// client-side — no backend field, nothing round-trips through +// `/api/*`. Read live (not cached) by the shared terminal factory's +// `expandDetails` option — see docs/web-ui/shape.md::Shared terminal +// pane — so a preference change on /settings.html applies to the next +// rendered row in any already-open agent tab without a reload. +export function getExpandDetailsPref() { + try { return localStorage.getItem(EXPAND_DETAILS_KEY) === '1'; } + catch { return false; } +} +export function setExpandDetailsPref(v) { + try { + if (v) localStorage.setItem(EXPAND_DETAILS_KEY, '1'); + else localStorage.removeItem(EXPAND_DETAILS_KEY); + } catch { /* localStorage unavailable — preference is session-only */ } +} diff --git a/frontend/packages/shared/src/terminal/terminal.js b/frontend/packages/shared/src/terminal/terminal.js index e00a55cd..5830e0cb 100644 --- a/frontend/packages/shared/src/terminal/terminal.js +++ b/frontend/packages/shared/src/terminal/terminal.js @@ -231,11 +231,23 @@ export function create(opts) { afterAppend(wasNearBottom); return [e, tn]; } + // `opts.expandDetails` (boolean or zero-arg function returning one) lets + // the caller default every otherwise-collapsed `
` row open — + // e.g. an "expand panels by default" browser-local preference. Read + // live (not captured once) so a mid-session preference flip takes + // effect on the next row without recreating the terminal. Renderers + // that need a row open regardless (message-bearing tool_use/tool_result) + // already set `d.open = true` themselves after calling this — this only + // changes the *default* for panels that would otherwise start closed. + function wantsExpandedDefault() { + return typeof opts.expandDetails === 'function' ? !!opts.expandDetails() : !!opts.expandDetails; + } function details(cls, summary, body, icon) { clearPlaceholder(); const wasNearBottom = isNearBottom(); const d = document.createElement('details'); d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); + if (wantsExpandedDefault()) d.open = true; d.appendChild(buildSummary(summary, icon)); const pre = document.createElement('pre'); pre.className = 'tool-body'; @@ -250,6 +262,7 @@ export function create(opts) { const wasNearBottom = isNearBottom(); const d = document.createElement('details'); d.className = 'row ' + (cls || '') + (currentNoAnim ? ' no-anim' : ''); + if (wantsExpandedDefault()) d.open = true; d.appendChild(buildSummary(summary, icon)); const pre = document.createElement('pre'); pre.className = 'tool-body diff-body';