hyperhive/frontend/packages/agent/src/components/Row.tsx
iris 589ace3438 agent term: add a setting to hide debug-level output
hyperhive#4008, mara: 'add a setting to not display verbose output...
like the grey colored debug stuff.'

Same shape as the existing expand-tool-output preference
(ExpandDetailsSetting.tsx): a new shared/src/prefs.ts key pair
(getHideDebugPref/setHideDebugPref), a new settings-menu-row component
owning its own state (HideDebugSetting.tsx, not a prop threaded through
the shared SettingsMenu component — per mara's earlier review on the
first one, more per-page options as props there is how that component
accumulates cruft), mounted next to ExpandDetailsSetting in Root.tsx.

Row.tsx skips (returns null for, not CSS display:none) any TermMsg
whose level is 'debug' when the pref is set — matches the muted 'debug'
row this issue is about (see docs/web-ui/terminal-rendering.md's Levels
table). Read live per-row, same as expand-details, so toggling applies
to newly streamed rows in an already-open tab without a reload; already
-rendered rows are unaffected either way, same non-retroactive
precedent the existing preference already sets.
2026-09-02 20:40:22 +02:00

101 lines
3.4 KiB
TypeScript

// Renders one `TermRow` — flat `<div class="row …">` or expandable
// `<details class="row …">`, driven straight off the wire shape
// (`lib/termMsg.ts`'s `TermMsg`, mirroring hive-agent's `term_msg.rs`):
// `level` picks the colour class, an empty `summary` + markdown `body`
// is a flat row with just the body (assistant text), anything else with
// a body is an expandable details row gated by the operator's
// expand-tool-output preference. No separate classification step —
// mara: "StreamRow should now match what the server sends in TermMsg."
import { useEffect, useRef } from "preact/hooks";
import type { TermRow } from "../lib/termMsg.js";
import { linkifyToNodes } from "../lib/linkify.js";
import { renderMarkdown } from "../lib/markdown.js";
import { getExpandDetailsPref, getHideDebugPref } from "@hive/shared/prefs.js";
function MarkdownBody({ text }: { text: string }) {
const ref = useRef<HTMLDivElement>(null);
const html = renderMarkdown(text);
useEffect(() => {
// marked autolinks URLs but leaves them same-tab — open externally
// so a click never navigates the terminal away.
ref.current?.querySelectorAll("a[href]").forEach((a) => {
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener noreferrer");
});
}, [html]);
// eslint-disable-next-line react/no-danger -- sanitized by DOMPurify in renderMarkdown
return (
<div className="md" ref={ref} dangerouslySetInnerHTML={{ __html: html }} />
);
}
function DiffBody({ text }: { text: string }) {
const lines = String(text).split("\n");
return (
<pre className="tool-body diff-body">
{lines.map((line, i) => {
const cls = line.startsWith("+ ")
? "diff-add"
: line.startsWith("- ")
? "diff-del"
: "diff-ctx";
return (
<span key={i} className={cls}>
{line}
{"\n"}
</span>
);
})}
</pre>
);
}
export function Row({ row }: { row: TermRow }) {
// "hide debug output" is a skip, not a dimmer — a hidden debug row
// still occupies no DOM node at all, same as it never streamed, rather
// than a CSS `display: none` that would keep it in the layout/DOM for
// no benefit.
if (row.level === "debug" && getHideDebugPref()) return null;
const cssClass = "level-" + row.level;
const icon = row.icon != null && row.icon !== "" && (
<span className="row-glyph">{row.icon}</span>
);
if (row.body == null) {
return (
<div className={`row ${cssClass}`}>
{icon}
{linkifyToNodes(row.summary)}
</div>
);
}
// Empty summary + markdown body → the body itself is the whole row
// (assistant text), no summary prefix line, never collapsible.
if (row.body_format === "markdown" && row.summary === "") {
return (
<div className={`row ${cssClass}`}>
{icon}
<MarkdownBody text={row.body} />
</div>
);
}
return (
<details
className={`row ${cssClass}`}
open={getExpandDetailsPref() || undefined}
>
<summary>
{icon}
<span className="summary-text">{row.summary}</span>
</summary>
{row.body_format === "diff" && <DiffBody text={row.body} />}
{row.body_format === "markdown" && <MarkdownBody text={row.body} />}
{row.body_format == null && (
<pre className="tool-body">{linkifyToNodes(row.body)}</pre>
)}
</details>
);
}