diff --git a/frontend/packages/agent/src/Root.tsx b/frontend/packages/agent/src/Root.tsx
index d18c7546..b586c428 100644
--- a/frontend/packages/agent/src/Root.tsx
+++ b/frontend/packages/agent/src/Root.tsx
@@ -11,7 +11,6 @@ import { useApplyThemeOverride } from '@hive/shared/theme-apply.js';
import { useApplyMotionOverride } from '@hive/shared/motion-apply.js';
import { Header } from './components/Header.js';
import { MetaNav } from './components/MetaNav.js';
-import { ExpandDetailsSetting } from './components/ExpandDetailsSetting.js';
import { StatusChips } from './components/StatusChips.js';
import { LiveStream, type LiveStreamHandle } from './components/LiveStream.js';
import { HeaderPill } from './components/HeaderPill.js';
@@ -114,9 +113,7 @@ export function Root() {
dashboardBase={resolveDashboardBase(state.dashboard_port)}
/>
) : null}
-
-
-
+
>
);
// Kept mounted regardless of `openPanel` (open/closed toggles just the
diff --git a/frontend/packages/agent/src/components/ExpandDetailsSetting.tsx b/frontend/packages/agent/src/components/ExpandDetailsSetting.tsx
deleted file mode 100644
index 3a7a939b..00000000
--- a/frontend/packages/agent/src/components/ExpandDetailsSetting.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-// Agent-terminal-only settings-menu row: "expand tool output panels".
-// Rendered as a `` child (`@hive/shared/settings-menu.js`)
-// rather than a prop on that shared component — `SettingsMenu` is also
-// used by swarm-ui, which has no terminal to apply this to, and a prior
-// attempt to gate it behind a boolean prop (`showExpandDetails`) on the
-// shared component drew mara's review: "you cannot just add it like
-// this - if we have more and more options there in different places we
-// will keep accumulating cruft in the shared component." This file owns
-// its own state/storage entirely; `SettingsMenu` just renders it as a
-// child, no different from any other consumer's markup.
-import { useState } from 'preact/hooks';
-import { getExpandDetailsPref, setExpandDetailsPref } from '@hive/shared/prefs.js';
-
-export function ExpandDetailsSetting() {
- // Plain localStorage read, not a hook-managed override like
- // theme/motion — `getExpandDetailsPref`/`setExpandDetailsPref`
- // (`@hive/shared/prefs.js`) are the existing shared-storage-key pair
- // the terminal itself already reads live (no round-trip needed here
- // beyond re-rendering this row's own checkbox state on toggle).
- const [expandDetails, setExpandDetailsState] = useState(() => getExpandDetailsPref());
- return (
-
- );
-}
diff --git a/frontend/packages/agent/src/lib/classifyEvent.ts b/frontend/packages/agent/src/lib/classifyEvent.ts
index 48d49544..07aa5784 100644
--- a/frontend/packages/agent/src/lib/classifyEvent.ts
+++ b/frontend/packages/agent/src/lib/classifyEvent.ts
@@ -15,7 +15,6 @@
// text loses its client-side tick. Revisit if that's missed in practice.
import type { StreamRow, StreamRowMeta } from './streamRow.js';
import { fmtAge, fmtClock } from './format.js';
-import { getExpandDetailsPref } from '@hive/shared/prefs.js';
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- stream-json
// content is dynamically-shaped JSON, same as app.js's untyped handling.
@@ -172,23 +171,15 @@ function classifyStream(v: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx): St
}
// `_category === 'rich'` tools get an expandable row: diff body (Edit),
-// default-open markdown body (send/ask/answer/recv-shaped, always open
-// regardless of the preference below — matches app.js), or a plain
-// body (collapsed unless the operator's "expand tool output" preference
-// says otherwise — @hive/shared/prefs.js's getExpandDetailsPref(), read
-// fresh per row so a mid-session preference change applies going
-// forward without a reload) — all pre-computed server-side, no per-tool
-// JS needed.
+// default-open markdown body (send/ask/answer/recv-shaped), or a plain
+// collapsed body — all pre-computed server-side, no per-tool JS needed.
function classifyToolUse(c: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx): StreamRow {
const icon = c._icon || '🔧';
const name = c.name || '';
if (c._category === 'rich' && c._body != null) {
const summary = c._summary || name || '?';
if (c._body_type === 'diff') {
- return {
- key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, defaultOpen: getExpandDetailsPref(),
- icon, text: summary, diffBody: c._body,
- };
+ return { key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, icon, text: summary, diffBody: c._body };
}
if (c._body_type === 'markdown') {
return {
@@ -196,10 +187,7 @@ function classifyToolUse(c: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx): S
icon, text: summary, markdownBody: c._body,
};
}
- return {
- key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, defaultOpen: getExpandDetailsPref(),
- icon, text: summary, plainBody: c._body,
- };
+ return { key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, icon, text: summary, plainBody: c._body };
}
return { key: nextKey(ctx), cssClass: 'tool-use', fromHistory, icon, text: c._summary || name || '?' };
}
@@ -226,10 +214,7 @@ function classifyToolResult(c: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx)
if (!txt.trim() || txt.length <= 120) {
return { key: nextKey(ctx), cssClass: 'tool-result error', fromHistory, text: '✗ ' + summaryBody };
}
- return {
- key: nextKey(ctx), cssClass: 'tool-result-block error', fromHistory, details: true,
- defaultOpen: getExpandDetailsPref(), text: summaryBody, plainBody: txt,
- };
+ return { key: nextKey(ctx), cssClass: 'tool-result-block error', fromHistory, details: true, text: summaryBody, plainBody: txt };
}
if (isMessageBearing && txt.trim()) {
return {
@@ -240,10 +225,7 @@ function classifyToolResult(c: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx)
if (!txt.trim() || txt.length <= 120) {
return { key: nextKey(ctx), cssClass: 'tool-result', fromHistory, text: '← ' + summaryBody };
}
- return {
- key: nextKey(ctx), cssClass: 'tool-result-block', fromHistory, details: true,
- defaultOpen: getExpandDetailsPref(), text: summaryBody, plainBody: txt,
- };
+ return { key: nextKey(ctx), cssClass: 'tool-result-block', fromHistory, details: true, text: summaryBody, plainBody: txt };
}
// Subagent (claude `Task`-tool) activity — dead path for agents today
diff --git a/frontend/packages/dashboard/src/settings.html b/frontend/packages/dashboard/src/settings.html
index a7ea4da1..d1fd0fbd 100644
--- a/frontend/packages/dashboard/src/settings.html
+++ b/frontend/packages/dashboard/src/settings.html
@@ -32,6 +32,11 @@
+
◇ 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 298378d1..aad5c1e6 100644
--- a/frontend/packages/dashboard/src/settings.js
+++ b/frontend/packages/dashboard/src/settings.js
@@ -7,9 +7,31 @@
// 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 6f157263..69b3859d 100644
--- a/frontend/packages/shared/package.json
+++ b/frontend/packages/shared/package.json
@@ -21,7 +21,7 @@
"./chrome.css": "./src/chrome.css",
"./pill.css": "./src/pill.css",
"./forms.js": "./src/forms.js",
- "./prefs.js": "./src/prefs.ts",
+ "./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.ts b/frontend/packages/shared/src/prefs.js
similarity index 71%
rename from frontend/packages/shared/src/prefs.ts
rename to frontend/packages/shared/src/prefs.js
index de4fa5bf..f73fb005 100644
--- a/frontend/packages/shared/src/prefs.ts
+++ b/frontend/packages/shared/src/prefs.js
@@ -13,20 +13,15 @@ const EXPAND_DETAILS_KEY = 'hive-agent-expand-details';
// 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 applies to the next rendered row in
-// any already-open agent tab without a reload.
-export function getExpandDetailsPref(): boolean {
- try {
- return localStorage.getItem(EXPAND_DETAILS_KEY) === '1';
- } catch {
- return false;
- }
+// 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: boolean): void {
+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 */
- }
+ } catch { /* localStorage unavailable — preference is session-only */ }
}
diff --git a/frontend/packages/shared/src/settings/SettingsMenu.tsx b/frontend/packages/shared/src/settings/SettingsMenu.tsx
index 233d5677..ac643132 100644
--- a/frontend/packages/shared/src/settings/SettingsMenu.tsx
+++ b/frontend/packages/shared/src/settings/SettingsMenu.tsx
@@ -24,7 +24,6 @@
// component, so the two stay in sync without this component owning any
// page-specific naming decision.
import { useEffect, useRef, useState } from 'preact/hooks';
-import type { ComponentChildren } from 'preact';
import { Badge } from '../badge/Badge.js';
import { GearIcon } from '../icons.js';
import { useThemeOverride, type ThemeOverride } from './theme-apply.js';
@@ -37,21 +36,9 @@ const MOTION_OPTIONS: MotionOverride[] = ['system', 'allow', 'reduce'];
export interface SettingsMenuProps {
themeKey: string;
motionKey: string;
- /**
- * Consumer-owned extra row(s), rendered inside the popover after the
- * built-in theme/motion rows. Not a named boolean prop per setting —
- * a `showExpandDetails?: boolean` attempt drew mara's review: "you
- * cannot just add it like this - if we have more and more options
- * there in different places we will keep accumulating cruft in the
- * shared component." A caller owns its row(s) entirely (state,
- * storage key, markup); this component never learns they exist. Use
- * `settings-menu-row` (this file's CSS) on each row's outer element
- * to match spacing/typography — see `Root.tsx` for a worked example.
- */
- children?: ComponentChildren;
}
-export function SettingsMenu({ themeKey, motionKey, children }: SettingsMenuProps) {
+export function SettingsMenu({ themeKey, motionKey }: SettingsMenuProps) {
const [open, setOpen] = useState(false);
const rootRef = useRef(null);
const [theme, setTheme] = useThemeOverride(themeKey);
@@ -111,7 +98,6 @@ export function SettingsMenu({ themeKey, motionKey, children }: SettingsMenuProp
))}
- {children}
) : null}