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';