diff --git a/src/browser/features/Messages/MessageRenderer.test.tsx b/src/browser/features/Messages/MessageRenderer.test.tsx index 187bcb4cac..2e8dabe71b 100644 --- a/src/browser/features/Messages/MessageRenderer.test.tsx +++ b/src/browser/features/Messages/MessageRenderer.test.tsx @@ -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(` +{"taskId":"task-model","agentType":"explore","status":"completed","title":"Model exposed","reportMarkdown":"Body","model":"anthropic:claude-opus-5","thinkingLevel":"high"} +`); + + const metadataView = render( + + + + ); + expect(metadataView.getByText("Opus 5")).toBeDefined(); + expect(metadataView.getByText("thinking: high")).toBeDefined(); + metadataView.unmount(); + + const withoutMetadata = createReportMessage(` +{"taskId":"task-no-model","agentType":"explore","status":"completed","title":"No model","reportMarkdown":"Body"} +`); + + const plainView = render( + + + + ); + expect(plainView.queryByText(/thinking:/)).toBeNull(); + }); + test("normalizes multiline report titles instead of exposing the envelope", () => { const message = createReportMessage(` task-multiline-title diff --git a/src/browser/features/Messages/SubagentReportMessageContent.tsx b/src/browser/features/Messages/SubagentReportMessageContent.tsx index 106d0cfa97..dae51c7f25 100644 --- a/src/browser/features/Messages/SubagentReportMessageContent.tsx +++ b/src/browser/features/Messages/SubagentReportMessageContent.tsx @@ -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"; @@ -40,6 +42,22 @@ export function SubagentReportMessageContent(
{props.report.agentType} + {props.report.model && ( + <> + + + + + + )} + {props.report.thinkingLevel != null && ( + <> + + + thinking: {getThinkingOptionLabel(props.report.thinkingLevel, props.report.model)} + + + )} ( +
+ +
+ ), + 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((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: () => ( diff --git a/src/browser/features/Tools/TaskToolCall.test.tsx b/src/browser/features/Tools/TaskToolCall.test.tsx index ffc6fe7b83..ca4f63fd13 100644 --- a/src/browser/features/Tools/TaskToolCall.test.tsx +++ b/src/browser/features/Tools/TaskToolCall.test.tsx @@ -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( + + + + ); + + 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( + + + + ); + + 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( + + + + ); + + fireEvent.click(view.getByText("task")); + + const settings = view.container.querySelector("[data-task-ai-settings]"); + expect(settings?.textContent).toContain("thinking: low"); + }); }); describe("TaskAwaitToolCall", () => { diff --git a/src/browser/features/Tools/TaskToolCall.tsx b/src/browser/features/Tools/TaskToolCall.tsx index 63f5a88993..6aeadcd03d 100644 --- a/src/browser/features/Tools/TaskToolCall.tsx +++ b/src/browser/features/Tools/TaskToolCall.tsx @@ -60,6 +60,8 @@ import { import { resolvePersistedAgentId } from "@/common/utils/agentIds"; import { formatDuration } from "@/common/utils/formatDuration"; import { ElapsedTimeDisplay } from "./Shared/ElapsedTimeDisplay"; +import { ModelDisplay } from "../Messages/ModelDisplay"; +import { getThinkingOptionLabel, type ThinkingLevel } from "@/common/types/thinking"; /** * Clean SVG icon for task tools - represents spawning/branching work @@ -449,8 +451,43 @@ interface TaskToolDisplayEntry { openWorkspaceId?: string; groupKind?: TaskGroupKind; label?: string; + modelString?: string; + thinkingLevel?: ThinkingLevel; } +interface TaskAiSettingsInfo { + modelString?: string; + thinkingLevel?: ThinkingLevel; +} + +const TaskAiSettingsDisplay: React.FC = (props) => { + if (!props.modelString && props.thinkingLevel == null) { + return null; + } + return ( + // min-w-0 at both flex levels + break-words let long custom model IDs wrap inside + // narrow cards instead of forcing right-edge overflow. + + {props.modelString && ( + + + + )} + {props.thinkingLevel != null && ( + + thinking: {getThinkingOptionLabel(props.thinkingLevel, props.modelString)} + + )} + + ); +}; + interface TaskToolOwnReport { reportMarkdown: string; title?: string; @@ -654,12 +691,14 @@ function collectTaskToolResultDisplayData(result: TaskToolSuccessResult | null): ownReportsByTaskId: Map; taskGroupsByTaskId: Map; workspaceIdByTaskId: Map; + aiSettingsByTaskId: Map; } { const taskIds = new Set(); const statusByTaskId = new Map(); const ownReportsByTaskId = new Map(); const taskGroupsByTaskId = new Map(); const workspaceIdByTaskId = new Map(); + const aiSettingsByTaskId = new Map(); if (!result) { return { taskIds: [], @@ -667,6 +706,7 @@ function collectTaskToolResultDisplayData(result: TaskToolSuccessResult | null): ownReportsByTaskId, taskGroupsByTaskId, workspaceIdByTaskId, + aiSettingsByTaskId, }; } @@ -699,10 +739,23 @@ function collectTaskToolResultDisplayData(result: TaskToolSuccessResult | null): } }; + const rememberAiSettings = (taskId: string, settings: TaskAiSettingsInfo): void => { + const modelString = trimToNonEmptyString(settings.modelString) ?? undefined; + if (!modelString && settings.thinkingLevel == null) { + return; + } + const existing = aiSettingsByTaskId.get(taskId); + aiSettingsByTaskId.set(taskId, { + modelString: existing?.modelString ?? modelString, + thinkingLevel: existing?.thinkingLevel ?? settings.thinkingLevel, + }); + }; + const taskStatuses = "tasks" in result && Array.isArray(result.tasks) ? result.tasks : undefined; const singleTaskId = rememberTaskId(result.taskId); if (singleTaskId) { rememberWorkspace(singleTaskId, result.workspaceId); + rememberAiSettings(singleTaskId, result); } if (singleTaskId && result.status === "completed" && typeof result.reportMarkdown === "string") { ownReportsByTaskId.set(singleTaskId, { @@ -724,6 +777,7 @@ function collectTaskToolResultDisplayData(result: TaskToolSuccessResult | null): statusByTaskId.set(taskId, task.status); rememberWorkspace(taskId, task.workspaceId); rememberTaskGroup(taskId, { groupKind: task.groupKind, label: task.label }); + rememberAiSettings(taskId, task); } } } @@ -740,6 +794,7 @@ function collectTaskToolResultDisplayData(result: TaskToolSuccessResult | null): }); rememberWorkspace(taskId, report.workspaceId); rememberTaskGroup(taskId, { groupKind: report.groupKind, label: report.label }); + rememberAiSettings(taskId, report); } } } @@ -757,6 +812,7 @@ function collectTaskToolResultDisplayData(result: TaskToolSuccessResult | null): ownReportsByTaskId, taskGroupsByTaskId, workspaceIdByTaskId, + aiSettingsByTaskId, }; } @@ -808,6 +864,11 @@ const TaskToolCandidateCard: React.FC<{ {entry.title && ( {entry.title} )} + {canViewTranscript && (