agent term: move expand-tool-output setting into its own settings popup

mara, #3778: the toggle lived on the separate hive-dashboard's
/settings.html even though it's agent-terminal-only. Moved it into
SettingsMenu (shared with swarm-ui) behind an opt-in showExpandDetails
prop — only the agent page passes it, matching her caution that the
component is shared but this setting isn't.

Also found and fixed a real regression while touching this: the
Preact rewrite's classifyEvent.ts hardcodes defaultOpen: true only for
the always-open markdown-bearing rows (send/ask/answer/recv) and never
reads the preference at all for the rows it's actually meant to
control (diffs, plain tool output, long errors) — so the toggle
currently has zero effect on the live page. Wired
getExpandDetailsPref() into those four sites; the always-open rows are
untouched, matching the documented pre-rewrite behavior.

Converted prefs.js to prefs.ts (TS couldn't resolve types for a plain
.js import) — same public @hive/shared/prefs.js export path, matching
how badge.js/icons.js etc. already map a .js export name to a
.tsx/.ts source file.
This commit is contained in:
iris 2026-08-30 15:41:07 +02:00 committed by mara
commit 385d4b6fd7
7 changed files with 70 additions and 43 deletions

View file

@ -113,7 +113,7 @@ export function Root() {
dashboardBase={resolveDashboardBase(state.dashboard_port)}
/>
) : null}
<SettingsMenu themeKey={THEME_KEY} motionKey={MOTION_KEY} />
<SettingsMenu themeKey={THEME_KEY} motionKey={MOTION_KEY} showExpandDetails />
</>
);
// Kept mounted regardless of `openPanel` (open/closed toggles just the

View file

@ -15,6 +15,7 @@
// 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.
@ -171,15 +172,23 @@ 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), or a plain
// collapsed body — all pre-computed server-side, no per-tool JS needed.
// 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.
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, icon, text: summary, diffBody: c._body };
return {
key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, defaultOpen: getExpandDetailsPref(),
icon, text: summary, diffBody: c._body,
};
}
if (c._body_type === 'markdown') {
return {
@ -187,7 +196,10 @@ 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, icon, text: summary, plainBody: 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, icon, text: c._summary || name || '?' };
}
@ -214,7 +226,10 @@ 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, text: summaryBody, plainBody: txt };
return {
key: nextKey(ctx), cssClass: 'tool-result-block error', fromHistory, details: true,
defaultOpen: getExpandDetailsPref(), text: summaryBody, plainBody: txt,
};
}
if (isMessageBearing && txt.trim()) {
return {
@ -225,7 +240,10 @@ 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, text: summaryBody, plainBody: txt };
return {
key: nextKey(ctx), cssClass: 'tool-result-block', fromHistory, details: true,
defaultOpen: getExpandDetailsPref(), text: summaryBody, plainBody: txt,
};
}
// Subagent (claude `Task`-tool) activity — dead path for agents today