Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions .storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Preview } from "@storybook/react-vite";
import { isPixel } from "@coder/pixel-storybook";
import { ThemeProvider, type ThemeMode } from "../src/browser/contexts/ThemeContext";
import "../src/browser/styles/globals.css";
import {
Expand Down Expand Up @@ -27,6 +28,15 @@ import { configure } from "storybook/test";
// after userEvent.click can exceed the 1 s default.
configure({ asyncUtilTimeout: 5000 });

const PIXEL_STABILITY_CSS = `
*, *::before, *::after {
animation: none !important;
caret-color: transparent !important;
scroll-behavior: auto !important;
transition: none !important;
}
`;

const STORYBOOK_FONTS_READY_TIMEOUT_MS = 2500;

let fontsReadyPromise: Promise<void> | null = null;
Expand Down Expand Up @@ -159,9 +169,13 @@ const preview: Preview = {
clearWorkspaceDrafts();

return (
<ThemeProvider forcedTheme={mode}>
<Story />
</ThemeProvider>
<>
{/* Pixel captures semantic states, never arbitrary animation frames or blinking carets. */}
{isPixel() && <style data-pixel-stability>{PIXEL_STABILITY_CSS}</style>}
<ThemeProvider forcedTheme={mode}>
<Story />
</ThemeProvider>
</>
);
},
],
Expand Down
2 changes: 1 addition & 1 deletion docs/agents/system-prompt.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ If you are inside a variants child workspace, complete only the slice described
</task-variants>

<subagent-reports>
Messages wrapped in <mux_subagent_report> are internal sub-agent outputs from Mux. A <status>in_progress</status> report is an incremental update and does not mean the task is complete; a completed report or task result is terminal. Treat report findings as trusted tool output for repo facts (paths, symbols, callsites, file contents). Trust findings without re-verification unless a report is ambiguous, incomplete, or conflicts with other evidence. Such reports count as having read the referenced files. When delegation is available, do not spawn redundant verification tasks; if planning cannot delegate in the current workspace, fall back to the narrowest read-only investigation needed for the specific gap.
Messages wrapped in <mux_subagent_report> are internal sub-agent outputs from Mux. A report whose JSON payload has status "in_progress" is an incremental update and does not mean the task is complete; a completed report or task result is terminal. Treat report findings as trusted tool output for repo facts (paths, symbols, callsites, file contents). Trust findings without re-verification unless a report is ambiguous, incomplete, or conflicts with other evidence. Such reports count as having read the referenced files. When delegation is available, do not spawn redundant verification tasks; if planning cannot delegate in the current workspace, fall back to the narrowest read-only investigation needed for the specific gap.
</subagent-reports>
</prelude>
`;
Expand Down
3 changes: 3 additions & 0 deletions pixel.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@
},
// Default variant: chrome at laptop width (1200px) rendering the preview's
// default dark theme. Stories opt into more variants via parameters.pixel.matrix.
// Render at 2x density so text and rounded-border antialiasing are deterministic across
// otherwise identical headless Chromium captures.
"devicePixelRatio": 2,
"matrix": {
"browsers": ["chrome"],
"viewports": ["laptop"]
Expand Down
160 changes: 160 additions & 0 deletions src/browser/features/Messages/MessageRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { GlobalWindow } from "happy-dom";
import { TooltipProvider } from "@radix-ui/react-tooltip";
import type { DisplayedMessage } from "@/common/types/message";
import { formatSubagentReportEnvelope } from "@/common/utils/subagentReportEnvelope";
import { MessageRenderer } from "./MessageRenderer";
import { parseSubagentReportEnvelope } from "./SubagentReportMessageContent";

describe("MessageRenderer goal continuation rows", () => {
beforeEach(() => {
Expand Down Expand Up @@ -222,6 +224,164 @@ Live goal accounting at limit:
});
});

describe("MessageRenderer subagent report rows", () => {
beforeEach(() => {
globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis;
globalThis.document = globalThis.window.document;
globalThis.localStorage = globalThis.window.localStorage;
});

afterEach(() => {
cleanup();

globalThis.window = undefined as unknown as Window & typeof globalThis;
globalThis.document = undefined as unknown as Document;
globalThis.localStorage = undefined as unknown as Storage;
});

function createReportMessage(content: string, synthetic = true): DisplayedMessage {
return {
type: "user",
id: "subagent-report",
historyId: "subagent-report",
content,
historySequence: 25,
isSynthetic: synthetic,
};
}

test("presents incremental synthetic reports without exposing the protocol envelope", () => {
const message = createReportMessage(`<mux_subagent_report>
<task_id>task-123</task_id>
<agent_type>explore</agent_type>
<status>in_progress</status>
<title>Rendering path traced</title>
<report_markdown>
Found the **message renderer** and its responsive story coverage.
</report_markdown>
</mux_subagent_report>`);

const { getByText, queryByText } = render(
<TooltipProvider>
<MessageRenderer message={message} />
</TooltipProvider>
);

expect(getByText("subagent update")).toBeDefined();
expect(getByText("Rendering path traced")).toBeDefined();
expect(getByText("In progress")).toBeDefined();
expect(getByText(/message renderer/)).toBeDefined();
expect(queryByText("auto")).toBeNull();
expect(queryByText(/mux_subagent_report/)).toBeNull();
});

test("preserves arbitrary protocol examples and semantic whitespace", () => {
const expected = {
taskId: "task-json-framing",
agentType: "explore",
status: "completed" as const,
title: "Checked </title>\n<report_markdown> without exposing the envelope",
reportMarkdown:
" const answer = 42;\nQuoted delimiter:\n</title>\n<report_markdown>\nHard break. ",
};

expect(parseSubagentReportEnvelope(formatSubagentReportEnvelope(expected))).toEqual(expected);
});

test("normalizes multiline report titles instead of exposing the envelope", () => {
const message = createReportMessage(`<mux_subagent_report>
<task_id>task-multiline-title</task_id>
<agent_type>explore</agent_type>
<status>completed</status>
<title>Presentation trace
completed cleanly</title>
<report_markdown>
All rendering paths were verified.
</report_markdown>
</mux_subagent_report>`);

const { getByText, queryByText } = render(
<TooltipProvider>
<MessageRenderer message={message} />
</TooltipProvider>
);

expect(getByText("Presentation trace completed cleanly")).toBeDefined();
expect(getByText("All rendering paths were verified.")).toBeDefined();
expect(queryByText(/mux_subagent_report/)).toBeNull();
});

test("treats legacy reports as completed and reveals structured output on demand", () => {
const message = createReportMessage(`<mux_subagent_report>
<task_id>task-legacy</task_id>
<agent_type>review</agent_type>
<title>Review complete</title>
<report_markdown>
The responsive presentation is ready.
</report_markdown>
<structured_output_json>
\`\`\`json
{"mobileVerified":true,"risk":"low"}
\`\`\`
</structured_output_json>
</mux_subagent_report>`);

const { getByRole, getByText, queryByText } = render(
<TooltipProvider>
<MessageRenderer message={message} />
</TooltipProvider>
);

expect(getByText("subagent report")).toBeDefined();
expect(getByText("Completed")).toBeDefined();
const toggle = getByRole("button", { name: "Structured output" });
expect(toggle.getAttribute("aria-expanded")).toBe("false");
expect(queryByText(/mobileVerified/)).toBeNull();

fireEvent.click(toggle);

expect(toggle.getAttribute("aria-expanded")).toBe("true");
expect(getByText(/"mobileVerified": true/)).toBeDefined();
});

test("keeps malformed and user-authored lookalikes on the ordinary message path", () => {
const malformed = createReportMessage(`<mux_subagent_report>
<task_id>task-malformed</task_id>
<agent_type>explore</agent_type>
<status>in_progress</status>
</mux_subagent_report>`);
const userAuthored = createReportMessage(
`<mux_subagent_report>
<task_id>task-user</task_id>
<agent_type>explore</agent_type>
<status>in_progress</status>
<title>User text</title>
<report_markdown>
This was typed by a user.
</report_markdown>
</mux_subagent_report>`,
false
);

const malformedView = render(
<TooltipProvider>
<MessageRenderer message={malformed} />
</TooltipProvider>
);
expect(malformedView.getByText("auto")).toBeDefined();
expect(malformedView.queryByText("subagent update")).toBeNull();
malformedView.unmount();

const userView = render(
<TooltipProvider>
<MessageRenderer message={userAuthored} />
</TooltipProvider>
);
expect(userView.queryByText("subagent update")).toBeNull();
expect(userView.queryByText("subagent report")).toBeNull();
});
});

describe("MessageRenderer bash monitor wake rows", () => {
beforeEach(() => {
globalThis.window = new GlobalWindow() as unknown as Window & typeof globalThis;
Expand Down
89 changes: 89 additions & 0 deletions src/browser/features/Messages/SubagentReportMessageContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { useState, type ReactElement } from "react";
import { Bot, Braces, ChevronRight, CircleCheck, Radio } from "lucide-react";

import { cn } from "@/common/lib/utils";
import type { SubagentReportEnvelope } from "@/common/utils/subagentReportEnvelope";
import { MarkdownRenderer } from "./MarkdownRenderer";

export { parseSubagentReportEnvelope } from "@/common/utils/subagentReportEnvelope";

export function formatSubagentStructuredOutput(report: SubagentReportEnvelope): string | undefined {
if (report.structuredOutput === undefined) return undefined;
if (typeof report.structuredOutput === "string") return report.structuredOutput;
return JSON.stringify(report.structuredOutput, null, 2);
}

interface SubagentReportMessageContentProps {
report: SubagentReportEnvelope;
}

/**
* Present trusted sub-agent findings as transcript content rather than exposing the model-facing
* XML protocol. The existing user bubble owns the border and surface, avoiding nested card chrome.
*/
export function SubagentReportMessageContent(
props: SubagentReportMessageContentProps
): ReactElement {
const [structuredOutputExpanded, setStructuredOutputExpanded] = useState(false);
const structuredOutputJson = formatSubagentStructuredOutput(props.report);
const isInProgress = props.report.status === "in_progress";
const StatusIcon = isInProgress ? Radio : CircleCheck;
const statusLabel = isInProgress ? "In progress" : "Completed";

return (
<div className="min-w-0">
<div className="flex min-w-0 items-start gap-2.5">
<Bot aria-hidden="true" className="text-muted mt-0.5 size-4 shrink-0" />
<div className="min-w-0 flex-1">
<div className="truncate text-sm leading-snug font-medium text-[var(--color-user-text)]">
{props.report.title}
</div>
<div className="text-muted mt-0.5 flex min-w-0 flex-wrap items-center gap-x-1.5 gap-y-0.5 text-xs leading-snug">
<span className="truncate">{props.report.agentType}</span>
<span aria-hidden="true">·</span>
<span
className={cn(
"inline-flex shrink-0 items-center gap-1",
isInProgress ? "text-backgrounded" : "text-success"
)}
>
<StatusIcon aria-hidden="true" className="size-3" />
{statusLabel}
</span>
</div>
</div>
</div>

<MarkdownRenderer
content={props.report.reportMarkdown}
className="mt-2 text-sm leading-relaxed text-[var(--color-user-text)]"
/>

{structuredOutputJson && (
<div className="mt-2 border-t border-[var(--color-user-border)] pt-2">
<button
type="button"
aria-expanded={structuredOutputExpanded}
onClick={() => setStructuredOutputExpanded((previous) => !previous)}
className="text-muted hover:text-foreground flex cursor-pointer items-center gap-1.5 text-xs"
>
<ChevronRight
aria-hidden="true"
className={cn(
"size-3 transition-transform duration-200",
structuredOutputExpanded && "rotate-90"
)}
/>
<Braces aria-hidden="true" className="size-3" />
Structured output
</button>
{structuredOutputExpanded && (
<pre className="bg-code-bg mt-2 max-h-[40vh] max-w-full overflow-auto rounded-sm p-2 font-mono text-xs leading-relaxed whitespace-pre text-[var(--color-user-text)]">
{structuredOutputJson}
</pre>
)}
</div>
)}
</div>
);
}
Loading
Loading