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}