diff --git a/AGENTS.md b/AGENTS.md
index b1f11bd3db..0c43bd473c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -256,6 +256,24 @@ scripts build the required E2E bridge before running Playwright.
See [TESTING.md](TESTING.md) for the full multi-agent E2E guide.
+### Never assert a DOM element against `null` with `assert.equal`
+
+In the desktop jsdom tests (`desktop/src/**/*.test.mjs`), write
+
+```js
+assert.ok(container.querySelector(sel) === null, "no header row");
+```
+
+**not** `assert.equal(container.querySelector(sel), null, ...)`. Both pass
+identically, but when the `assert.equal` form *fails*, node serializes the
+matched element and its entire subtree to build a diff. On a real transcript
+that exhausts memory and the runner dies with SIGKILL after ~100s instead of
+printing the assertion message — so a genuine regression is unreadable and
+looks like a hang or an OOM in unrelated code. The `assert.ok(x === null)` form
+fails in milliseconds with the message you wrote.
+
+Comparing `getAttribute(...)` to `null` is fine — attributes are strings.
+
### PR Screenshots
> **Do NOT use `buzz upload`, the relay media endpoint, or any third-party
diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs
index 73abe860e8..a0676857d8 100644
--- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs
+++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.conversation.test.mjs
@@ -435,6 +435,26 @@ function fencedCodeItems() {
];
}
+/** One *human prompt* whose body is a fenced code block. */
+function fencedCodePromptItems() {
+ return [
+ {
+ channelId: "chan-1",
+ sessionId: "sess-1",
+ turnId: "turn-1",
+ id: "msg:user",
+ type: "message",
+ renderClass: "message",
+ role: "user",
+ title: TRIGGER_TITLE,
+ text: "fix this\n\n```ts\nconst a = 1;\nconst b = 2;\n```\n",
+ timestamp: "2026-06-14T19:00:00.000Z",
+ messageId: "event-1",
+ authorPubkey: AUTHOR,
+ },
+ ];
+}
+
/**
* Same mount, but the caller can swap the list props afterwards. Needed for the
* contracts that are only visible across a rerender: a streaming thought
@@ -619,11 +639,42 @@ test("conversation frames fenced code with berd's header row", async () => {
assert.equal(code.querySelectorAll("[data-line]").length, 2);
});
-test("channel-message code blocks are untouched by the focus recipe", async () => {
+test("conversation applies the code recipe to a fenced human prompt too", async () => {
+ // Regression guard for a real bug quality caught. The provider was first
+ // mounted inside `MessageActivity`, which only handles assistant items — the
+ // user bubble returns before it, so a fence inside a prompt kept the legacy
+ // 16px muted frame nested inside the new 12px bubble. The recipe is a
+ // property of the *surface*, not of a role, so the provider now sits at the
+ // transcript boundary and both roles inherit it.
+ const { container } = await renderTranscriptWithCodeChrome("conversation", {
+ items: fencedCodePromptItems(),
+ });
+ const bubble = container.querySelector(
+ '[data-testid="transcript-user-message"]',
+ );
+ assert.ok(bubble, "the prompt should render");
+ assert.ok(
+ bubble.querySelector('[data-testid="markdown-code-block-header"]'),
+ "a fence inside the prompt gets berd's header row",
+ );
+ const frame = bubble.querySelector("pre");
+ assert.match(frame.className, /rounded-lg/);
+ assert.doesNotMatch(
+ frame.className,
+ /rounded-2xl/,
+ "the legacy 16px frame must not nest inside the 12px bubble",
+ );
+ assert.doesNotMatch(frame.className, /pr-12/);
+});
+
+test("the default transcript variant keeps the legacy code chrome", async () => {
// The markdown renderer is shared with channel messages, so `focusProse` is
- // opt-in per surface. Rendering the same fenced block through `default` must
- // still produce the original chrome: no header row, 16px radius, muted fill,
- // and the absolutely-positioned copy button.
+ // opt-in per surface. Rendering the same fenced block through the `default`
+ // transcript variant must still produce the original chrome: no header row,
+ // 16px radius, muted fill, and the absolutely-positioned copy button.
+ //
+ // This proves the *variant gate*, not the channel-message row itself — those
+ // rows are covered by the markdown tests in `shared/ui/markdown`.
const { container } = await renderTranscriptWithCodeChrome("default", {
items: fencedCodeItems(),
});
diff --git a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
index 779763989d..9a5135c6f0 100644
--- a/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
+++ b/desktop/src/features/agents/ui/AgentSessionTranscriptList.tsx
@@ -16,6 +16,7 @@ import { useStableArrayShallow } from "@/shared/hooks/useStableReference";
import { cn } from "@/shared/lib/cn";
import { AnimatedCount } from "@/shared/ui/AnimatedCount";
import { FuzzyLogo } from "@/shared/ui/buzz-logo/FuzzyLogo";
+import { CodeBlockVariantContext } from "@/shared/ui/markdown/CodeBlock";
import type { TranscriptItem } from "./agentSessionTypes";
import { TurnLivenessIndicator } from "./TurnLivenessIndicator";
import {
@@ -229,41 +230,53 @@ export function AgentSessionTranscriptList({
ref={autoTail ? contentRef : undefined}
role="log"
>
-
-
- {displayBlocks.map((block) => {
- const blockKey = getDisplayBlockKey(block);
- return (
-
- {/* content-visibility stays on a non-animated child: motion
+ {/* The berd code-block recipe is a property of the *surface*, not of a
+ role: a fenced block in a human prompt must get the same chrome as
+ one in an agent reply. Provided at the transcript boundary so every
+ descendant markdown render inherits it. A context provider emits no
+ DOM, so `default`/`compactPreview` markup is unaffected. */}
+
+
+
+ {displayBlocks.map((block) => {
+ const blockKey = getDisplayBlockKey(block);
+ return (
+
+ {/* content-visibility stays on a non-animated child: motion
measures the outer wrapper for layout animations, which
would otherwise force skipped offscreen rows to render. */}
-
-
-
-
- );
- })}
- {isTurnLive && !isCompactPreview ? : null}
-
-
+
+
+
+
+ );
+ })}
+ {isTurnLive && !isCompactPreview ? (
+
+ ) : null}
+
+
+
);
diff --git a/desktop/src/features/agents/ui/activityRenderClasses/MessageActivity.tsx b/desktop/src/features/agents/ui/activityRenderClasses/MessageActivity.tsx
index 84d4a4cd35..8451eaa9ce 100644
--- a/desktop/src/features/agents/ui/activityRenderClasses/MessageActivity.tsx
+++ b/desktop/src/features/agents/ui/activityRenderClasses/MessageActivity.tsx
@@ -1,6 +1,5 @@
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { Markdown } from "@/shared/ui/markdown";
-import { CodeBlockVariantContext } from "@/shared/ui/markdown/CodeBlock";
import { UserAvatar } from "@/shared/ui/UserAvatar";
import { useAgentSessionTranscriptVariant } from "../agentSessionTranscriptContext";
import { formatTranscriptTimestampTitle } from "../agentSessionUtils";
@@ -99,25 +98,19 @@ function MessageItem({
}
title={formatTranscriptTimestampTitle(item.timestamp)}
>
- {/* A context provider renders no DOM, so wrapping unconditionally
- keeps `default`/`compactPreview` markup byte-identical. */}
-
-
-
+
diff --git a/desktop/src/shared/ui/markdown/CodeBlock.tsx b/desktop/src/shared/ui/markdown/CodeBlock.tsx
index d804faa1a3..30748d236e 100644
--- a/desktop/src/shared/ui/markdown/CodeBlock.tsx
+++ b/desktop/src/shared/ui/markdown/CodeBlock.tsx
@@ -28,7 +28,10 @@ import { getReactNodeText } from "./utils";
* border, and no shadow.
*
* It is opt-in because this renderer is shared with channel messages —
- * restyling the default would change every code block in the app.
+ * restyling the default would change every code block in the app. The variant
+ * is a property of the *surface*, not of a role: `AgentSessionTranscriptList`
+ * provides it once at the transcript boundary, so a fence in a human prompt
+ * gets the same chrome as one in an agent reply.
*
* Delivered through context rather than a `MarkdownCodeBlock` prop for the same
* reason as `VideoReviewMarkdownContext`: the component map handed to