diff --git a/docs/swarm/ui.md b/docs/swarm/ui.md
index e6c3ce37..71258581 100644
--- a/docs/swarm/ui.md
+++ b/docs/swarm/ui.md
@@ -35,6 +35,12 @@ An agent's terminal opens with its last 100 rows from the past 24 hours, then
follows new ones; a reconnect picks up after the last row it showed. A subagent
terminal shows rows published while it's open, and has no turn-state badges.
+The settings menu in the header has a **hide debug output** toggle. It drops
+the muted debug rows (thinking, coalesced ticks, harness chatter) from every
+terminal, the preview and the tabs, and rows already on screen hide or show as
+you flip it. The browser keeps the choice for the swarm UI, apart from the
+per-agent pages'.
+
A terminal's own tab takes the agent's icon as its browser-tab icon, so tabs of
different agents tell apart. A subagent's tab shows its agent's icon. An agent
with no icon keeps the hyperhive mark.
diff --git a/docs/web-ui/terminal-rendering.md b/docs/web-ui/terminal-rendering.md
index 1eb32c31..aae8b9af 100644
--- a/docs/web-ui/terminal-rendering.md
+++ b/docs/web-ui/terminal-rendering.md
@@ -53,8 +53,11 @@ that motivated the change.
The settings menu's "hide debug output" toggle (`getHideDebugPref()`,
`@hive/shared/prefs.js`) skips `debug`-level rows entirely rather than
-muting them further — client-side only, same live-read-no-reload shape
-as the expand-tool-output preference above.
+muting them further — client-side only, no reload. The per-agent page and
+the swarm UI both have the toggle, as one shared row
+(`@hive/shared/hide-debug-setting.js`). Each browser origin keeps its own
+setting. A `debug` row follows the toggle as it changes, so rows already
+on screen hide or show at once, not only the rows that arrive after it.
Per-tool icon + summary formatting (what a `Read`/`Edit`/`send` call's row
actually says) lives in `stream_enrich.rs`'s `fmt_tool_use()` family —
diff --git a/frontend/packages/agent/src/Root.tsx b/frontend/packages/agent/src/Root.tsx
index bf98c97c..f4eec760 100644
--- a/frontend/packages/agent/src/Root.tsx
+++ b/frontend/packages/agent/src/Root.tsx
@@ -12,7 +12,7 @@ import { useApplyMotionOverride } from "@hive/shared/motion-apply.js";
import { Header } from "./components/Header.js";
import { MetaNav } from "./components/MetaNav.js";
import { ExpandDetailsSetting } from "./components/ExpandDetailsSetting.js";
-import { HideDebugSetting } from "./components/HideDebugSetting.js";
+import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
import { StatusChips } from "./components/StatusChips.js";
import { LiveStream, type LiveStreamHandle } from "./components/LiveStream.js";
import { HeaderPill } from "./components/HeaderPill.js";
diff --git a/frontend/packages/agent/src/components/HideDebugSetting.tsx b/frontend/packages/agent/src/components/HideDebugSetting.tsx
deleted file mode 100644
index bff063ae..00000000
--- a/frontend/packages/agent/src/components/HideDebugSetting.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-// Agent-terminal-only settings-menu row: "hide debug output". Same shape
-// as `ExpandDetailsSetting` (its own file's header comment explains why
-// this lives here rather than as a `SettingsMenu` prop) — mara: "add a
-// setting to not display verbose output... like the grey colored debug
-// stuff."
-import { useState } from "preact/hooks";
-import { getHideDebugPref, setHideDebugPref } from "@hive/shared/prefs.js";
-
-export function HideDebugSetting() {
- const [hideDebug, setHideDebugState] = useState(() => getHideDebugPref());
- return (
-
- );
-}
diff --git a/frontend/packages/shared/package.json b/frontend/packages/shared/package.json
index 2e98b15a..47a8ba34 100644
--- a/frontend/packages/shared/package.json
+++ b/frontend/packages/shared/package.json
@@ -52,6 +52,7 @@
"./theme-apply.js": "./src/settings/theme-apply.ts",
"./motion-apply.js": "./src/settings/motion-apply.ts",
"./settings-menu.js": "./src/settings/SettingsMenu.tsx",
+ "./hide-debug-setting.js": "./src/settings/HideDebugSetting.tsx",
"./settings-menu.css": "./src/settings/SettingsMenu.css"
},
"files": [
diff --git a/frontend/packages/shared/src/prefs.ts b/frontend/packages/shared/src/prefs.ts
index 4ba17362..c542e1df 100644
--- a/frontend/packages/shared/src/prefs.ts
+++ b/frontend/packages/shared/src/prefs.ts
@@ -2,9 +2,9 @@
// so a future second consumer (e.g. swarm-ui's own terminal) gets the
// exact same key name for free via the browser's shared per-origin
// storage, rather than an independent copy that could drift.
-// `ExpandDetailsSetting` / `HideDebugSetting` (the per-agent page's own
-// settings popover rows) are the only writers today; `Row.tsx` is the
-// only reader of either.
+// `ExpandDetailsSetting` / `HideDebugSetting` (the settings popover rows
+// of the per-agent page and of the swarm UI) are the only writers today;
+// `Row.tsx` is the only reader of either.
const EXPAND_DETAILS_KEY = "hive-agent-expand-details";
@@ -53,4 +53,24 @@ export function setHideDebugPref(v: boolean): void {
} catch {
/* localStorage unavailable — preference is session-only */
}
+ for (const listener of hideDebugListeners) listener();
+}
+
+const hideDebugListeners = new Set<() => void>();
+
+// Calls `listener` whenever the preference changes: in this tab through
+// `setHideDebugPref`, in another tab of the same origin through the
+// browser's `storage` event. Returns the unsubscribe. This is what lets a
+// terminal that is already showing rows follow the toggle, instead of only
+// the rows that arrive after it.
+export function subscribeHideDebugPref(listener: () => void): () => void {
+ hideDebugListeners.add(listener);
+ const onStorage = (e: StorageEvent) => {
+ if (e.key === HIDE_DEBUG_KEY || e.key === null) listener();
+ };
+ window.addEventListener("storage", onStorage);
+ return () => {
+ hideDebugListeners.delete(listener);
+ window.removeEventListener("storage", onStorage);
+ };
}
diff --git a/frontend/packages/shared/src/settings/HideDebugSetting.tsx b/frontend/packages/shared/src/settings/HideDebugSetting.tsx
new file mode 100644
index 00000000..b0c98025
--- /dev/null
+++ b/frontend/packages/shared/src/settings/HideDebugSetting.tsx
@@ -0,0 +1,26 @@
+// Settings-menu row: "hide debug output". Rendered as a ``
+// child (`./SettingsMenu.js`) by each page that has a terminal, the
+// per-agent page and the swarm UI, rather than as a prop on the menu: the
+// caller owns its row. Both read the same preference (`../prefs.js`), which
+// `../terminal/Row.tsx` follows as it changes — mara: "add a setting to not
+// display verbose output... like the grey colored debug stuff."
+import { useState } from "preact/hooks";
+import { getHideDebugPref, setHideDebugPref } from "../prefs.js";
+
+export function HideDebugSetting() {
+ const [hideDebug, setHideDebugState] = useState(() => getHideDebugPref());
+ return (
+
+ );
+}
diff --git a/frontend/packages/shared/src/terminal/Row.tsx b/frontend/packages/shared/src/terminal/Row.tsx
index 8109213e..a199365d 100644
--- a/frontend/packages/shared/src/terminal/Row.tsx
+++ b/frontend/packages/shared/src/terminal/Row.tsx
@@ -10,11 +10,15 @@
// Shared between `@hive/agent`'s local stream and `@hive/swarm-ui`'s
// relay consumer (see `./termMsg.js`'s doc) — both hand this the same
// `TermRow` shape regardless of which wire framing they read it off of.
-import { useEffect, useRef } from "preact/hooks";
+import { useEffect, useRef, useState } from "preact/hooks";
import type { TermRow } from "./termMsg.js";
import { linkifyToNodes } from "./linkify.js";
import { renderMarkdown } from "./markdown.js";
-import { getExpandDetailsPref, getHideDebugPref } from "../prefs.js";
+import {
+ getExpandDetailsPref,
+ getHideDebugPref,
+ subscribeHideDebugPref,
+} from "../prefs.js";
function MarkdownBody({ text }: { text: string }) {
const ref = useRef(null);
@@ -54,13 +58,27 @@ function DiffBody({ text }: { text: string }) {
);
}
-export function Row({ row }: { row: TermRow }) {
+// A `debug` row, which follows the "hide debug output" preference as it
+// changes. Only debug rows subscribe, so a long terminal does not hold a
+// listener per row.
+function DebugRow({ row }: { row: TermRow }) {
+ const [hidden, setHidden] = useState(() => getHideDebugPref());
+ useEffect(
+ () => subscribeHideDebugPref(() => setHidden(getHideDebugPref())),
+ [],
+ );
// "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;
+ return hidden ? null : ;
+}
+export function Row({ row }: { row: TermRow }) {
+ return row.level === "debug" ? : ;
+}
+
+function RowBody({ row }: { row: TermRow }) {
const cssClass = "level-" + row.level;
const icon = row.icon != null && row.icon !== "" && (
{row.icon}
diff --git a/frontend/packages/swarm-ui/src/shell/Shell.tsx b/frontend/packages/swarm-ui/src/shell/Shell.tsx
index 324d7ad9..7cab64c1 100644
--- a/frontend/packages/swarm-ui/src/shell/Shell.tsx
+++ b/frontend/packages/swarm-ui/src/shell/Shell.tsx
@@ -32,6 +32,7 @@ import { Link, useLocation } from "wouter-preact";
import { LinksMenu } from "./LinksMenu.js";
import { UserMenu } from "./UserMenu.js";
import { SettingsMenu } from "@hive/shared/settings-menu.js";
+import { HideDebugSetting } from "@hive/shared/hide-debug-setting.js";
import { useApplyThemeOverride } from "@hive/shared/theme-apply.js";
import { useApplyMotionOverride } from "@hive/shared/motion-apply.js";
import { useDynamicTabs } from "./useDynamicTabs.js";
@@ -444,7 +445,9 @@ export function Shell({ children }: { children: ComponentChildren }) {
`margin-left: auto` split the available space between them
instead of sitting flush together at the right edge. */}