Watch
0
0
Fork
You've already forked hyperhive
0
hyperhive/frontend/packages/agent/src/components/Row.tsx
atlas 39b95c2ede treefmt: apply prettier
Pure `nix fmt` output from the commit before this one — no hand edits.
203 files: 52 md, 42 tsx, 32 js, 32 css, 21 ts, 13 html, 8 json, 3 mjs.

Reproduce with `nix develop -c nix fmt` on the parent commit; the result
should be byte-identical to this tree.

None of the 13 `.prettierignore` entries appears here — verified by
intersecting the changed-file list against the ignore file, with a
control proving the intersection finds a match when one exists.
2026-09-02 15:25:07 +02:00

95 lines
3.1 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 } 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 }) {
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>
);
}