Compare commits
8 changed files with 43 additions and 91 deletions
|
|
@ -11,7 +11,6 @@ import { useApplyThemeOverride } from '@hive/shared/theme-apply.js';
|
||||||
import { useApplyMotionOverride } from '@hive/shared/motion-apply.js';
|
import { useApplyMotionOverride } from '@hive/shared/motion-apply.js';
|
||||||
import { Header } from './components/Header.js';
|
import { Header } from './components/Header.js';
|
||||||
import { MetaNav } from './components/MetaNav.js';
|
import { MetaNav } from './components/MetaNav.js';
|
||||||
import { ExpandDetailsSetting } from './components/ExpandDetailsSetting.js';
|
|
||||||
import { StatusChips } from './components/StatusChips.js';
|
import { StatusChips } from './components/StatusChips.js';
|
||||||
import { LiveStream, type LiveStreamHandle } from './components/LiveStream.js';
|
import { LiveStream, type LiveStreamHandle } from './components/LiveStream.js';
|
||||||
import { HeaderPill } from './components/HeaderPill.js';
|
import { HeaderPill } from './components/HeaderPill.js';
|
||||||
|
|
@ -114,9 +113,7 @@ export function Root() {
|
||||||
dashboardBase={resolveDashboardBase(state.dashboard_port)}
|
dashboardBase={resolveDashboardBase(state.dashboard_port)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<SettingsMenu themeKey={THEME_KEY} motionKey={MOTION_KEY}>
|
<SettingsMenu themeKey={THEME_KEY} motionKey={MOTION_KEY} />
|
||||||
<ExpandDetailsSetting />
|
|
||||||
</SettingsMenu>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
// Kept mounted regardless of `openPanel` (open/closed toggles just the
|
// Kept mounted regardless of `openPanel` (open/closed toggles just the
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
// Agent-terminal-only settings-menu row: "expand tool output panels".
|
|
||||||
// Rendered as a `<SettingsMenu>` 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 (
|
|
||||||
<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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
// text loses its client-side tick. Revisit if that's missed in practice.
|
// text loses its client-side tick. Revisit if that's missed in practice.
|
||||||
import type { StreamRow, StreamRowMeta } from './streamRow.js';
|
import type { StreamRow, StreamRowMeta } from './streamRow.js';
|
||||||
import { fmtAge, fmtClock } from './format.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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- stream-json
|
||||||
// content is dynamically-shaped JSON, same as app.js's untyped handling.
|
// 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),
|
// `_category === 'rich'` tools get an expandable row: diff body (Edit),
|
||||||
// default-open markdown body (send/ask/answer/recv-shaped, always open
|
// default-open markdown body (send/ask/answer/recv-shaped), or a plain
|
||||||
// regardless of the preference below — matches app.js), or a plain
|
// collapsed body — all pre-computed server-side, no per-tool JS needed.
|
||||||
// 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 {
|
function classifyToolUse(c: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx): StreamRow {
|
||||||
const icon = c._icon || '🔧';
|
const icon = c._icon || '🔧';
|
||||||
const name = c.name || '';
|
const name = c.name || '';
|
||||||
if (c._category === 'rich' && c._body != null) {
|
if (c._category === 'rich' && c._body != null) {
|
||||||
const summary = c._summary || name || '?';
|
const summary = c._summary || name || '?';
|
||||||
if (c._body_type === 'diff') {
|
if (c._body_type === 'diff') {
|
||||||
return {
|
return { key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, icon, text: summary, diffBody: c._body };
|
||||||
key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, defaultOpen: getExpandDetailsPref(),
|
|
||||||
icon, text: summary, diffBody: c._body,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (c._body_type === 'markdown') {
|
if (c._body_type === 'markdown') {
|
||||||
return {
|
return {
|
||||||
|
|
@ -196,10 +187,7 @@ function classifyToolUse(c: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx): S
|
||||||
icon, text: summary, markdownBody: c._body,
|
icon, text: summary, markdownBody: c._body,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return { key: nextKey(ctx), cssClass: 'tool-use', fromHistory, details: true, icon, text: summary, plainBody: c._body };
|
||||||
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 || '?' };
|
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) {
|
if (!txt.trim() || txt.length <= 120) {
|
||||||
return { key: nextKey(ctx), cssClass: 'tool-result error', fromHistory, text: '✗ ' + summaryBody };
|
return { key: nextKey(ctx), cssClass: 'tool-result error', fromHistory, text: '✗ ' + summaryBody };
|
||||||
}
|
}
|
||||||
return {
|
return { key: nextKey(ctx), cssClass: 'tool-result-block error', fromHistory, details: true, text: summaryBody, plainBody: txt };
|
||||||
key: nextKey(ctx), cssClass: 'tool-result-block error', fromHistory, details: true,
|
|
||||||
defaultOpen: getExpandDetailsPref(), text: summaryBody, plainBody: txt,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
if (isMessageBearing && txt.trim()) {
|
if (isMessageBearing && txt.trim()) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -240,10 +225,7 @@ function classifyToolResult(c: AnyEvent, fromHistory: boolean, ctx: ClassifyCtx)
|
||||||
if (!txt.trim() || txt.length <= 120) {
|
if (!txt.trim() || txt.length <= 120) {
|
||||||
return { key: nextKey(ctx), cssClass: 'tool-result', fromHistory, text: '← ' + summaryBody };
|
return { key: nextKey(ctx), cssClass: 'tool-result', fromHistory, text: '← ' + summaryBody };
|
||||||
}
|
}
|
||||||
return {
|
return { key: nextKey(ctx), cssClass: 'tool-result-block', fromHistory, details: true, text: summaryBody, plainBody: txt };
|
||||||
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
|
// Subagent (claude `Task`-tool) activity — dead path for agents today
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,11 @@
|
||||||
<span id="notif-status" class="meta" hidden></span>
|
<span id="notif-status" class="meta" hidden></span>
|
||||||
</div>
|
</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>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/static/settings.js" defer></script>
|
<script type="module" src="/static/settings.js" defer></script>
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,31 @@
|
||||||
// browser-level permission, so firing still works from the dashboard
|
// browser-level permission, so firing still works from the dashboard
|
||||||
// even though the toggle UI now lives on its own page.
|
// 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,
|
// initServerWarnings() renders the shared top-of-page warnings banner,
|
||||||
// matching the other stand-alone pages (FL0W / L0GS / H0M3).
|
// matching the other stand-alone pages (FL0W / L0GS / H0M3).
|
||||||
import { NOTIF, initServerWarnings } from './common.js';
|
import { NOTIF, initServerWarnings } from './common.js';
|
||||||
|
import { getExpandDetailsPref, setExpandDetailsPref } from '@hive/shared/prefs.js';
|
||||||
|
|
||||||
initServerWarnings();
|
initServerWarnings();
|
||||||
NOTIF.bind();
|
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",
|
"./chrome.css": "./src/chrome.css",
|
||||||
"./pill.css": "./src/pill.css",
|
"./pill.css": "./src/pill.css",
|
||||||
"./forms.js": "./src/forms.js",
|
"./forms.js": "./src/forms.js",
|
||||||
"./prefs.js": "./src/prefs.ts",
|
"./prefs.js": "./src/prefs.js",
|
||||||
"./dom.js": "./src/dom.js",
|
"./dom.js": "./src/dom.js",
|
||||||
"./modal.js": "./src/modal.js",
|
"./modal.js": "./src/modal.js",
|
||||||
"./shadow-css.js": "./src/shadow-css.js",
|
"./shadow-css.js": "./src/shadow-css.js",
|
||||||
|
|
|
||||||
|
|
@ -13,20 +13,15 @@ const EXPAND_DETAILS_KEY = 'hive-agent-expand-details';
|
||||||
// client-side — no backend field, nothing round-trips through
|
// client-side — no backend field, nothing round-trips through
|
||||||
// `/api/*`. Read live (not cached) by the shared terminal factory's
|
// `/api/*`. Read live (not cached) by the shared terminal factory's
|
||||||
// `expandDetails` option — see docs/web-ui/shape.md::Shared terminal
|
// `expandDetails` option — see docs/web-ui/shape.md::Shared terminal
|
||||||
// pane — so a preference change applies to the next rendered row in
|
// pane — so a preference change on /settings.html applies to the next
|
||||||
// any already-open agent tab without a reload.
|
// rendered row in any already-open agent tab without a reload.
|
||||||
export function getExpandDetailsPref(): boolean {
|
export function getExpandDetailsPref() {
|
||||||
try {
|
try { return localStorage.getItem(EXPAND_DETAILS_KEY) === '1'; }
|
||||||
return localStorage.getItem(EXPAND_DETAILS_KEY) === '1';
|
catch { return false; }
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
export function setExpandDetailsPref(v: boolean): void {
|
export function setExpandDetailsPref(v) {
|
||||||
try {
|
try {
|
||||||
if (v) localStorage.setItem(EXPAND_DETAILS_KEY, '1');
|
if (v) localStorage.setItem(EXPAND_DETAILS_KEY, '1');
|
||||||
else localStorage.removeItem(EXPAND_DETAILS_KEY);
|
else localStorage.removeItem(EXPAND_DETAILS_KEY);
|
||||||
} catch {
|
} catch { /* localStorage unavailable — preference is session-only */ }
|
||||||
/* localStorage unavailable — preference is session-only */
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
// component, so the two stay in sync without this component owning any
|
// component, so the two stay in sync without this component owning any
|
||||||
// page-specific naming decision.
|
// page-specific naming decision.
|
||||||
import { useEffect, useRef, useState } from 'preact/hooks';
|
import { useEffect, useRef, useState } from 'preact/hooks';
|
||||||
import type { ComponentChildren } from 'preact';
|
|
||||||
import { Badge } from '../badge/Badge.js';
|
import { Badge } from '../badge/Badge.js';
|
||||||
import { GearIcon } from '../icons.js';
|
import { GearIcon } from '../icons.js';
|
||||||
import { useThemeOverride, type ThemeOverride } from './theme-apply.js';
|
import { useThemeOverride, type ThemeOverride } from './theme-apply.js';
|
||||||
|
|
@ -37,21 +36,9 @@ const MOTION_OPTIONS: MotionOverride[] = ['system', 'allow', 'reduce'];
|
||||||
export interface SettingsMenuProps {
|
export interface SettingsMenuProps {
|
||||||
themeKey: string;
|
themeKey: string;
|
||||||
motionKey: 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 [open, setOpen] = useState(false);
|
||||||
const rootRef = useRef<HTMLDivElement>(null);
|
const rootRef = useRef<HTMLDivElement>(null);
|
||||||
const [theme, setTheme] = useThemeOverride(themeKey);
|
const [theme, setTheme] = useThemeOverride(themeKey);
|
||||||
|
|
@ -111,7 +98,6 @@ export function SettingsMenu({ themeKey, motionKey, children }: SettingsMenuProp
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{children}
|
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue