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:
parent
22db09ec66
commit
385d4b6fd7
7 changed files with 70 additions and 43 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -32,11 +32,6 @@
|
|||
<span id="notif-status" class="meta" hidden></span>
|
||||
</div>
|
||||
|
||||
<h3>◇ agent terminal</h3>
|
||||
<p class="meta">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.</p>
|
||||
<div id="verbosity-row" class="notif-row">
|
||||
<button type="button" id="expand-details-toggle" class="btn btn-notif" role="switch"></button>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<script type="module" src="/static/settings.js" defer></script>
|
||||
|
|
|
|||
|
|
@ -7,31 +7,9 @@
|
|||
// 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();
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
"./chrome.css": "./src/chrome.css",
|
||||
"./pill.css": "./src/pill.css",
|
||||
"./forms.js": "./src/forms.js",
|
||||
"./prefs.js": "./src/prefs.js",
|
||||
"./prefs.js": "./src/prefs.ts",
|
||||
"./dom.js": "./src/dom.js",
|
||||
"./modal.js": "./src/modal.js",
|
||||
"./shadow-css.js": "./src/shadow-css.js",
|
||||
|
|
|
|||
|
|
@ -13,15 +13,20 @@ 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 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; }
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
export function setExpandDetailsPref(v) {
|
||||
export function setExpandDetailsPref(v: boolean): void {
|
||||
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 */
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ import { Badge } from '../badge/Badge.js';
|
|||
import { GearIcon } from '../icons.js';
|
||||
import { useThemeOverride, type ThemeOverride } from './theme-apply.js';
|
||||
import { useMotionOverride, type MotionOverride } from './motion-apply.js';
|
||||
import { getExpandDetailsPref, setExpandDetailsPref } from '../prefs.js';
|
||||
import './SettingsMenu.css';
|
||||
|
||||
const THEME_OPTIONS: ThemeOverride[] = ['system', 'light', 'dark'];
|
||||
|
|
@ -36,13 +37,29 @@ const MOTION_OPTIONS: MotionOverride[] = ['system', 'allow', 'reduce'];
|
|||
export interface SettingsMenuProps {
|
||||
themeKey: string;
|
||||
motionKey: string;
|
||||
/**
|
||||
* Show the "expand tool output panels" toggle — agent-terminal-only,
|
||||
* formerly its own section on the (separate) hive dashboard's
|
||||
* `/settings.html`. `SettingsMenu` is shared with swarm-ui too, which
|
||||
* has no terminal to apply this to (mara: "settigs dropdown is
|
||||
* shared component, but the setting is only in the agent term") — so
|
||||
* it's opt-in per consumer rather than always rendered. Only the
|
||||
* agent page passes this; swarm-ui's `Shell` doesn't.
|
||||
*/
|
||||
showExpandDetails?: boolean;
|
||||
}
|
||||
|
||||
export function SettingsMenu({ themeKey, motionKey }: SettingsMenuProps) {
|
||||
export function SettingsMenu({ themeKey, motionKey, showExpandDetails }: SettingsMenuProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const [theme, setTheme] = useThemeOverride(themeKey);
|
||||
const [motion, setMotion] = useMotionOverride(motionKey);
|
||||
// Plain localStorage read, not a hook-managed override like theme/motion
|
||||
// above — `getExpandDetailsPref`/`setExpandDetailsPref` (`../prefs.js`)
|
||||
// are the existing shared-storage-key pair the terminal itself already
|
||||
// reads live (no round-trip needed here beyond re-rendering this menu's
|
||||
// own checkbox state on toggle).
|
||||
const [expandDetails, setExpandDetailsState] = useState(() => getExpandDetailsPref());
|
||||
|
||||
// Close on an outside click or Escape — only listens while open.
|
||||
useEffect(() => {
|
||||
|
|
@ -98,6 +115,20 @@ export function SettingsMenu({ themeKey, motionKey }: SettingsMenuProps) {
|
|||
))}
|
||||
</select>
|
||||
</label>
|
||||
{showExpandDetails ? (
|
||||
<label class="settings-menu-row">
|
||||
<span>expand tool output</span>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={expandDetails}
|
||||
onChange={(e) => {
|
||||
const v = (e.target as HTMLInputElement).checked;
|
||||
setExpandDetailsPref(v);
|
||||
setExpandDetailsState(v);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue