Skip to content
26 changes: 26 additions & 0 deletions src/browser/features/Messages/MessageRenderer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,32 @@ Found the **message renderer** and its responsive story coverage.
expect(parseSubagentReportEnvelope(formatSubagentReportEnvelope(expected))).toEqual(expected);
});

test("shows the sub-agent model and thinking level when the envelope carries them", () => {
const withMetadata = createReportMessage(`<mux_subagent_report>
{"taskId":"task-model","agentType":"explore","status":"completed","title":"Model exposed","reportMarkdown":"Body","model":"anthropic:claude-opus-5","thinkingLevel":"high"}
</mux_subagent_report>`);

const metadataView = render(
<TooltipProvider>
<MessageRenderer message={withMetadata} />
</TooltipProvider>
);
expect(metadataView.getByText("Opus 5")).toBeDefined();
expect(metadataView.getByText("thinking: high")).toBeDefined();
metadataView.unmount();

const withoutMetadata = createReportMessage(`<mux_subagent_report>
{"taskId":"task-no-model","agentType":"explore","status":"completed","title":"No model","reportMarkdown":"Body"}
</mux_subagent_report>`);

const plainView = render(
<TooltipProvider>
<MessageRenderer message={withoutMetadata} />
</TooltipProvider>
);
expect(plainView.queryByText(/thinking:/)).toBeNull();
});

test("normalizes multiline report titles instead of exposing the envelope", () => {
const message = createReportMessage(`<mux_subagent_report>
<task_id>task-multiline-title</task_id>
Expand Down
18 changes: 18 additions & 0 deletions src/browser/features/Messages/SubagentReportMessageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ import { useState, type ReactElement } from "react";
import { Bot, Braces, ChevronRight, CircleCheck, Radio } from "lucide-react";

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

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

Expand Down Expand Up @@ -40,6 +42,22 @@ export function SubagentReportMessageContent(
</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>
{props.report.model && (
<>
<span aria-hidden="true">·</span>
<span className="truncate">
<ModelDisplay modelString={props.report.model} />
</span>
</>
)}
{props.report.thinkingLevel != null && (
<>
<span aria-hidden="true">·</span>
<span className="shrink-0">
thinking: {getThinkingOptionLabel(props.report.thinkingLevel, props.report.model)}
</span>
</>
)}
<span aria-hidden="true">·</span>
<span
className={cn(
Expand Down
60 changes: 60 additions & 0 deletions src/browser/features/Tools/TaskToolCall.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { waitFor, within } from "@storybook/test";
import type { ReactNode } from "react";
import { TaskApplyGitPatchToolCall } from "@/browser/features/Tools/TaskApplyGitPatchToolCall";
import { TaskToolCall } from "@/browser/features/Tools/TaskToolCall";
Expand Down Expand Up @@ -36,6 +37,8 @@ export const TaskWorkflowStates: Story = {
result={{
status: "running",
taskId: "task-fe-001",
modelString: "anthropic:claude-opus-5",
thinkingLevel: "high",
note: "Use task_await to monitor progress.",
}}
status="completed"
Expand Down Expand Up @@ -75,6 +78,8 @@ export const TaskWithReport: Story = {
status: "completed",
taskId: "task-abc123",
title: "Test File Analysis",
modelString: "openai:gpt-5.6-sol",
thinkingLevel: "xhigh",
reportMarkdown: `# Test File Analysis

Found **47 test files** across the project.
Expand Down Expand Up @@ -111,6 +116,8 @@ export const BestOfTaskGroup: Story = {
title: "Option 1",
agentId: "explore",
agentType: "explore",
modelString: "anthropic:claude-sonnet-5",
thinkingLevel: "medium",
reportMarkdown: "Use **shared helper utilities** for tree coalescing.",
},
{
Expand All @@ -135,6 +142,59 @@ export const BestOfTaskGroup: Story = {
),
};

/** long custom model IDs must wrap inside a narrow card instead of overflowing */
export const TaskNarrowLongModelId: Story = {
render: () => (
<div data-testid="narrow-task-card" className="bg-background w-[320px] p-2">
<TaskToolCall
args={{
subagent_type: "explore",
prompt: "Analyze the frontend React components in src/browser/",
title: "Frontend analysis",
run_in_background: true,
}}
result={{
status: "running",
taskId: "task-fe-001",
// Deliberately hyphen-free: only an unbroken token exercises the wrap fix.
modelString:
"openrouter:acmelabs/somextremelylongcustommodelidentifierwithoutanybreakopportunitieswhatsoeverv2instruct",
thinkingLevel: "high",
note: "Use task_await to monitor progress.",
}}
status="completed"
/>
</div>
),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
canvas.getByText("task").click();
await waitFor(() => {
if (!canvasElement.querySelector("[data-task-ai-settings]")) {
throw new Error("task AI settings did not render after expanding");
}
});
const container = canvasElement.querySelector('[data-testid="narrow-task-card"]');
if (!(container instanceof HTMLElement)) {
throw new Error("narrow task card container not found");
}
await new Promise<void>((resolve) =>
requestAnimationFrame(() => requestAnimationFrame(() => resolve()))
);
const containerRight = container.getBoundingClientRect().right;
const settings = container.querySelector("[data-task-ai-settings]");
const settingsRight = settings?.getBoundingClientRect().right ?? Number.POSITIVE_INFINITY;
// Right-edge containment, not scrollWidth: ancestors clip overflow, which would
// hide a too-wide settings row from scrollWidth-based checks.
if (settingsRight > containerRight + 1) {
throw new Error(
`task AI settings overflowed the ${container.clientWidth}px card by ` +
`${Math.round(settingsRight - containerRight)}px`
);
}
},
};

/** task_apply_git_patch states: executing, dry-run, success, and failure */
export const TaskApplyGitPatchStates: Story = {
render: () => (
Expand Down
126 changes: 126 additions & 0 deletions src/browser/features/Tools/TaskToolCall.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,132 @@ describe("TaskToolCall", () => {
expect(setSelectedWorkspace).toHaveBeenCalledTimes(1);
expect(setSelectedWorkspace.mock.calls[0][0]).toEqual(workspace);
});

test("prefers live workspace settings over the result snapshot", () => {
// A plan child's auto-handoff to exec rewrites live metadata after launch; the
// result snapshot keeps the stale plan-phase settings.
const workspace = createWorkspaceMetadata({
id: "task-child-1",
taskModelString: "anthropic:claude-opus-5",
taskThinkingLevel: "high",
});
workspaceContextMock = {
workspaceMetadata: new Map([[workspace.id, workspace]]),
};

const agentTaskArgs = {
subagent_type: "plan",
prompt: "Plan then implement.",
title: "Plan task",
run_in_background: true,
};
const AgentTaskToolCall = getToolComponent("task", agentTaskArgs);
const view = render(
<TooltipProvider>
<AgentTaskToolCall
args={agentTaskArgs}
result={{
status: "running",
taskId: "task-child-1",
modelString: "openai:gpt-5.2",
thinkingLevel: "low",
note: "Task started in background.",
}}
status="completed"
/>
</TooltipProvider>
);

fireEvent.click(view.getByText("task"));

const settings = view.container.querySelector("[data-task-ai-settings]");
expect(settings?.textContent).toContain("Opus 5");
expect(settings?.textContent).toContain("thinking: high");
expect(settings?.textContent).not.toContain("thinking: low");
});

test("prefers linked report settings over the spawn snapshot after cleanup", () => {
// Workspace already cleaned up; the task_await-linked report carries the exec
// settings while the spawn result kept the stale plan-phase ones.
workspaceContextMock = { workspaceMetadata: new Map() };

const agentTaskArgs = {
subagent_type: "plan",
prompt: "Plan then implement.",
title: "Plan task",
run_in_background: true,
};
const AgentTaskToolCall = getToolComponent("task", agentTaskArgs);
const view = render(
<TooltipProvider>
<AgentTaskToolCall
args={agentTaskArgs}
result={{
status: "running",
taskId: "task-child-3",
modelString: "openai:gpt-5.2",
thinkingLevel: "low",
note: "Task started in background.",
}}
taskReportLinking={{
reportByTaskId: new Map([
[
"task-child-3",
{
taskId: "task-child-3",
reportMarkdown: "done",
modelString: "anthropic:claude-opus-5",
thinkingLevel: "high",
},
],
]),
suppressReportInAwaitTaskIds: new Set(["task-child-3"]),
spawnTitleByTaskId: new Map(),
}}
status="completed"
/>
</TooltipProvider>
);

fireEvent.click(view.getByText("task"));

const settings = view.container.querySelector("[data-task-ai-settings]");
expect(settings?.textContent).toContain("Opus 5");
expect(settings?.textContent).toContain("thinking: high");
expect(settings?.textContent).not.toContain("thinking: low");
});

test("falls back to result-carried settings after workspace cleanup", () => {
workspaceContextMock = { workspaceMetadata: new Map() };

const agentTaskArgs = {
subagent_type: "explore",
prompt: "Look around.",
title: "Explore task",
run_in_background: true,
};
const AgentTaskToolCall = getToolComponent("task", agentTaskArgs);
const view = render(
<TooltipProvider>
<AgentTaskToolCall
args={agentTaskArgs}
result={{
status: "running",
taskId: "task-child-2",
modelString: "openai:gpt-5.2",
thinkingLevel: "low",
note: "Task started in background.",
}}
status="completed"
/>
</TooltipProvider>
);

fireEvent.click(view.getByText("task"));

const settings = view.container.querySelector("[data-task-ai-settings]");
expect(settings?.textContent).toContain("thinking: low");
});
});

describe("TaskAwaitToolCall", () => {
Expand Down
Loading
Loading