From 5d442c4a51ffd3421055e10a7a0c9a8c248cf2cb Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:51:00 +0000 Subject: [PATCH 1/3] feat: show task kind and spawn intent in single-task task_await summary --- .../features/Tools/TaskToolCall.test.tsx | 124 ++++++++++++++++++ src/browser/features/Tools/TaskToolCall.tsx | 29 ++++ .../utils/messages/taskReportLinking.ts | 96 +++++++++++++- 3 files changed, 246 insertions(+), 3 deletions(-) diff --git a/src/browser/features/Tools/TaskToolCall.test.tsx b/src/browser/features/Tools/TaskToolCall.test.tsx index 3bc3d70305..7e41424fc3 100644 --- a/src/browser/features/Tools/TaskToolCall.test.tsx +++ b/src/browser/features/Tools/TaskToolCall.test.tsx @@ -4,7 +4,9 @@ import { GlobalWindow } from "happy-dom"; import { TooltipProvider } from "@/browser/components/Tooltip/Tooltip"; +import type { DisplayedMessage } from "@/common/types/message"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; +import { computeTaskReportLinking } from "@/browser/utils/messages/taskReportLinking"; let workspaceContextMock: { workspaceMetadata: Map; @@ -69,6 +71,23 @@ function createWorkspaceMetadata( const taskAwaitArgs = { task_ids: ["task-1"], timeout_secs: 70 }; const TaskAwaitToolCall = getToolComponent("task_await", taskAwaitArgs); +function createToolMessage(overrides: { + toolName: string; + args: unknown; + result?: unknown; +}): DisplayedMessage { + return { + type: "tool", + id: "tool-msg-1", + historyId: "hist-1", + toolCallId: "call-1", + status: "completed", + isPartial: false, + historySequence: 1, + ...overrides, + }; +} + function renderTaskAwaitToolCall(props: Record = {}) { return render( @@ -363,6 +382,111 @@ describe("TaskAwaitToolCall", () => { expect(view.getByText("Task service unavailable")).toBeDefined(); }); + test("shows bash kind and spawn model_intent for a single completed bash task", () => { + const bashSpawn = createToolMessage({ + toolName: "bash", + args: { + script: "./scripts/wait_pr_ready.sh 27330", + display_name: "PR ready watcher", + model_intent: "watching PR 27330 until it is ready", + timeout_secs: 3600, + run_in_background: true, + }, + result: { + success: true, + output: "Started", + exitCode: 0, + wall_duration_ms: 10, + taskId: "bash:pr-ready-watcher-a1b2", + backgroundProcessId: "pr-ready-watcher-a1b2", + }, + }); + + const view = renderTaskAwaitToolCall({ + status: "completed", + args: { task_ids: ["bash:pr-ready-watcher-a1b2"] }, + result: { + results: [ + { + status: "completed", + taskId: "bash:pr-ready-watcher-a1b2", + title: "PR ready watcher", + reportMarkdown: "exit 0", + }, + ], + }, + taskReportLinking: computeTaskReportLinking([bashSpawn]), + }); + + expect(view.getByText("1 task completed")).toBeDefined(); + expect(view.getByText(/bash · Watching PR 27330 until it is ready/)).toBeDefined(); + }); + + test("falls back to the completed task title when no spawn intent is linked", () => { + const view = renderTaskAwaitToolCall({ + status: "completed", + args: { task_ids: ["bash:pr-ready-watcher-a1b2"] }, + result: { + results: [ + { + status: "completed", + taskId: "bash:pr-ready-watcher-a1b2", + title: "PR ready watcher", + reportMarkdown: "exit 0", + }, + ], + }, + }); + + expect(view.getByText(/bash · PR ready watcher/)).toBeDefined(); + }); + + test("shows agent type and title for a single completed sub-agent task", () => { + const taskSpawn = createToolMessage({ + toolName: "task", + args: { + agentId: "explore", + prompt: "Find pagination helpers.", + title: "Pagination exploration", + run_in_background: true, + }, + result: { status: "queued", taskId: "task-1" }, + }); + + const view = renderTaskAwaitToolCall({ + status: "completed", + result: { + results: [ + { + status: "completed", + taskId: "task-1", + title: "Pagination exploration", + reportMarkdown: "Report", + }, + ], + }, + taskReportLinking: computeTaskReportLinking([taskSpawn]), + }); + + expect(view.getByText(/explore · Pagination exploration/)).toBeDefined(); + }); + + test("keeps multi-task completion summaries count-only", () => { + const view = renderTaskAwaitToolCall({ + status: "completed", + args: { task_ids: ["task-1", "task-2"] }, + result: { + results: [ + { status: "completed", taskId: "task-1", title: "First task", reportMarkdown: "a" }, + { status: "completed", taskId: "task-2", title: "Second task", reportMarkdown: "b" }, + ], + }, + }); + + expect(view.getByText("2 tasks completed")).toBeDefined(); + expect(view.queryByText(/First task/)).toBeNull(); + }); + test("uses valid legacy agentType for task_await rows when agentId is invalid", () => { workspaceContextMock = { workspaceMetadata: new Map([ diff --git a/src/browser/features/Tools/TaskToolCall.tsx b/src/browser/features/Tools/TaskToolCall.tsx index b89402fd40..93773354a9 100644 --- a/src/browser/features/Tools/TaskToolCall.tsx +++ b/src/browser/features/Tools/TaskToolCall.tsx @@ -46,6 +46,7 @@ import type { } from "@/common/types/tools"; import type { TaskReportLinking } from "@/browser/utils/messages/taskReportLinking"; import { formatGitPatchArtifactSummary } from "./taskPatchSummary"; +import { sanitizeModelIntent } from "./bashCollapsedSummary"; import { formatTaskGroupCreationLabel, formatTaskGroupHeader, @@ -460,6 +461,10 @@ function isWorkspaceTurnTaskHandleId(taskId: string): boolean { return /^wst_[a-z0-9][a-z0-9_-]*$/.test(taskId); } +function isWorkflowRunTaskHandleId(taskId: string): boolean { + return taskId.startsWith("wfr_"); +} + function fromBashTaskId(taskId: string): string | null { const prefix = "bash:"; if (!taskId.startsWith(prefix)) { @@ -1334,6 +1339,29 @@ export const TaskAwaitToolCall: React.FC = ({ const targetCount = totalCount > 0 ? totalCount : taskIds?.length; const formatTasks = (count: number) => `${count} ${count === 1 ? "task" : "tasks"}`; + // "1 task completed" alone says nothing about what finished; for single-task awaits, + // surface the task's kind plus its spawn intent/title in the collapsed row. + const firstResult = results[0]; + let singleTaskDetail: string | undefined; + if (results.length === 1 && firstResult.status === "completed") { + const completedTaskId = firstResult.taskId; + const bashSpawn = taskReportLinking?.bashSpawnByTaskId.get(completedTaskId); + const kind = fromBashTaskId(completedTaskId) + ? "bash" + : isWorkflowRunTaskHandleId(completedTaskId) + ? "workflow" + : isWorkspaceTurnTaskHandleId(completedTaskId) || + firstResult.handleKind === "workspace_turn" + ? "workspace" + : taskReportLinking?.spawnAgentTypeByTaskId.get(completedTaskId); + const description = + (bashSpawn ? sanitizeModelIntent(bashSpawn.modelIntent, bashSpawn.script) : undefined) ?? + trimToNonEmptyString(firstResult.title) ?? + trimToNonEmptyString(taskReportLinking?.spawnTitleByTaskId.get(completedTaskId)); + const detail = [kind, description].filter((part): part is string => part != null).join(" · "); + singleTaskDetail = detail.length > 0 ? detail : undefined; + } + let summaryTitle: string; let summaryDetail: string | undefined; let summaryTone: "active" | "danger" | "interrupted" | "success" | "waiting"; @@ -1369,6 +1397,7 @@ export const TaskAwaitToolCall: React.FC = ({ summaryTone = "waiting"; } else if (completedCount > 0) { summaryTitle = `${formatTasks(completedCount)} completed`; + summaryDetail = singleTaskDetail; summaryTone = "success"; } else { summaryTitle = "Checked task status"; diff --git a/src/browser/utils/messages/taskReportLinking.ts b/src/browser/utils/messages/taskReportLinking.ts index 221d3f9364..69376ff8da 100644 --- a/src/browser/utils/messages/taskReportLinking.ts +++ b/src/browser/utils/messages/taskReportLinking.ts @@ -11,6 +11,12 @@ export interface LinkedTaskReport { thinkingLevel?: ThinkingLevel; } +export interface BashTaskSpawnInfo { + script: string; + displayName?: string; + modelIntent?: string; +} + export interface TaskReportLinking { /** * Completed task reports indexed by taskId. @@ -33,6 +39,18 @@ export interface TaskReportLinking { * (e.g. older agent_report payloads). */ spawnTitleByTaskId: Map; + + /** + * Agent types from the original `task` tool call input (`args.agentId` / `args.subagent_type`), + * indexed by taskId. + */ + spawnAgentTypeByTaskId: Map; + + /** + * Spawn args of background `bash` tool calls, indexed by taskId. Lets task_await rows + * surface the spawning command's model_intent / display_name. + */ + bashSpawnByTaskId: Map; } function getTaskIdsFromToolResult(result: unknown): string[] { @@ -76,6 +94,54 @@ function getTitleFromTaskToolArgs(args: unknown): string | null { return typeof title === "string" && title.trim().length > 0 ? title.trim() : null; } +function getAgentTypeFromTaskToolArgs(args: unknown): string | null { + if (typeof args !== "object" || args === null) return null; + + const candidates = [ + (args as { agentId?: unknown }).agentId, + (args as { subagent_type?: unknown }).subagent_type, + ]; + for (const candidate of candidates) { + if (typeof candidate === "string" && candidate.trim().length > 0) { + return candidate.trim(); + } + } + return null; +} + +// Only background bash spawn results carry a taskId, so its presence identifies them. +function getBashSpawnTaskId(result: unknown): string | null { + if (typeof result !== "object" || result === null) return null; + + const taskId = (result as { taskId?: unknown }).taskId; + return typeof taskId === "string" && taskId.trim().length > 0 ? taskId.trim() : null; +} + +function getBashSpawnInfoFromArgs(args: unknown): BashTaskSpawnInfo | null { + if (typeof args !== "object" || args === null) return null; + + const { script, display_name, model_intent } = args as { + script?: unknown; + display_name?: unknown; + model_intent?: unknown; + }; + const displayName = + typeof display_name === "string" && display_name.trim().length > 0 + ? display_name.trim() + : undefined; + const modelIntent = + typeof model_intent === "string" && model_intent.trim().length > 0 + ? model_intent.trim() + : undefined; + if (displayName === undefined && modelIntent === undefined) return null; + + return { + script: typeof script === "string" ? script : "", + displayName, + modelIntent, + }; +} + /** * Render-time helper that links completed task reports (from `task_await`) back to the * original `task` tool call that spawned the background work. @@ -84,21 +150,39 @@ function getTitleFromTaskToolArgs(args: unknown): string | null { * helps the renderer place the final report in a more intuitive location. */ export function computeTaskReportLinking(messages: DisplayedMessage[]): TaskReportLinking { - // First pass: record which taskIds have a visible `task` tool call (and capture spawn titles). + // First pass: record which taskIds have a visible `task` tool call (and capture spawn + // titles/agent types), plus spawn args of background `bash` tool calls. const taskToolCallTaskIds = new Set(); const spawnTitleByTaskId = new Map(); + const spawnAgentTypeByTaskId = new Map(); + const bashSpawnByTaskId = new Map(); for (const msg of messages) { - if (msg.type !== "tool" || msg.toolName !== "task") continue; + if (msg.type !== "tool") continue; + + if (msg.toolName === "bash") { + const taskId = getBashSpawnTaskId(msg.result); + const spawnInfo = taskId ? getBashSpawnInfoFromArgs(msg.args) : null; + if (taskId && spawnInfo) { + bashSpawnByTaskId.set(taskId, spawnInfo); + } + continue; + } + + if (msg.toolName !== "task") continue; const taskIds = getTaskIdsFromToolResult(msg.result); if (taskIds.length === 0) continue; const title = getTitleFromTaskToolArgs(msg.args); + const agentType = getAgentTypeFromTaskToolArgs(msg.args); for (const taskId of taskIds) { taskToolCallTaskIds.add(taskId); if (title) { spawnTitleByTaskId.set(taskId, title); } + if (agentType) { + spawnAgentTypeByTaskId.set(taskId, agentType); + } } } @@ -158,5 +242,11 @@ export function computeTaskReportLinking(messages: DisplayedMessage[]): TaskRepo suppressReportInAwaitTaskIds.add(taskId); } - return { reportByTaskId, suppressReportInAwaitTaskIds, spawnTitleByTaskId }; + return { + reportByTaskId, + suppressReportInAwaitTaskIds, + spawnTitleByTaskId, + spawnAgentTypeByTaskId, + bashSpawnByTaskId, + }; } From aaaa78529eb095b4edba2d53b208a4130d0b9b86 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:51:47 +0000 Subject: [PATCH 2/3] fix: fall back to task title when spawn intent restates the command Review findings: reuse the intent-vs-command comparison from buildBashCollapsedSummary via a shared sanitizeDisplayableModelIntent helper, and drop the unused BashTaskSpawnInfo.displayName field (bash task_await results already carry display_name as their title). --- .../features/Tools/TaskToolCall.test.tsx | 40 +++++++++++++++++++ src/browser/features/Tools/TaskToolCall.tsx | 6 ++- .../features/Tools/bashCollapsedSummary.ts | 17 +++++--- .../utils/messages/taskReportLinking.ts | 16 ++------ 4 files changed, 60 insertions(+), 19 deletions(-) diff --git a/src/browser/features/Tools/TaskToolCall.test.tsx b/src/browser/features/Tools/TaskToolCall.test.tsx index 7e41424fc3..9696fc9b10 100644 --- a/src/browser/features/Tools/TaskToolCall.test.tsx +++ b/src/browser/features/Tools/TaskToolCall.test.tsx @@ -422,6 +422,46 @@ describe("TaskAwaitToolCall", () => { expect(view.getByText(/bash · Watching PR 27330 until it is ready/)).toBeDefined(); }); + test("falls back to the task title when the spawn intent merely restates the command", () => { + const bashSpawn = createToolMessage({ + toolName: "bash", + args: { + script: "git status", + display_name: "Repo State", + model_intent: "git status", + timeout_secs: 30, + run_in_background: true, + }, + result: { + success: true, + output: "Started", + exitCode: 0, + wall_duration_ms: 10, + taskId: "bash:repo-state-a1b2", + backgroundProcessId: "repo-state-a1b2", + }, + }); + + const view = renderTaskAwaitToolCall({ + status: "completed", + args: { task_ids: ["bash:repo-state-a1b2"] }, + result: { + results: [ + { + status: "completed", + taskId: "bash:repo-state-a1b2", + title: "Repo State", + reportMarkdown: "exit 0", + }, + ], + }, + taskReportLinking: computeTaskReportLinking([bashSpawn]), + }); + + expect(view.getByText(/bash · Repo State/)).toBeDefined(); + expect(view.queryByText(/bash · Git status/)).toBeNull(); + }); + test("falls back to the completed task title when no spawn intent is linked", () => { const view = renderTaskAwaitToolCall({ status: "completed", diff --git a/src/browser/features/Tools/TaskToolCall.tsx b/src/browser/features/Tools/TaskToolCall.tsx index 93773354a9..c99b893857 100644 --- a/src/browser/features/Tools/TaskToolCall.tsx +++ b/src/browser/features/Tools/TaskToolCall.tsx @@ -46,7 +46,7 @@ import type { } from "@/common/types/tools"; import type { TaskReportLinking } from "@/browser/utils/messages/taskReportLinking"; import { formatGitPatchArtifactSummary } from "./taskPatchSummary"; -import { sanitizeModelIntent } from "./bashCollapsedSummary"; +import { sanitizeDisplayableModelIntent } from "./bashCollapsedSummary"; import { formatTaskGroupCreationLabel, formatTaskGroupHeader, @@ -1355,7 +1355,9 @@ export const TaskAwaitToolCall: React.FC = ({ ? "workspace" : taskReportLinking?.spawnAgentTypeByTaskId.get(completedTaskId); const description = - (bashSpawn ? sanitizeModelIntent(bashSpawn.modelIntent, bashSpawn.script) : undefined) ?? + (bashSpawn + ? sanitizeDisplayableModelIntent(bashSpawn.modelIntent, bashSpawn.script) + : undefined) ?? trimToNonEmptyString(firstResult.title) ?? trimToNonEmptyString(taskReportLinking?.spawnTitleByTaskId.get(completedTaskId)); const detail = [kind, description].filter((part): part is string => part != null).join(" · "); diff --git a/src/browser/features/Tools/bashCollapsedSummary.ts b/src/browser/features/Tools/bashCollapsedSummary.ts index 49c164e0f7..2859241d9c 100644 --- a/src/browser/features/Tools/bashCollapsedSummary.ts +++ b/src/browser/features/Tools/bashCollapsedSummary.ts @@ -37,11 +37,7 @@ export function buildBashCollapsedSummary( return { kind: "command", command }; } - const intent = sanitizeModelIntent(options.args.model_intent, command); - const displayIntent = - intent && normalizeForComparison(intent) !== normalizeForComparison(command) - ? intent - : undefined; + const displayIntent = sanitizeDisplayableModelIntent(options.args.model_intent, command); if (mode === "intent") { return { kind: "intent", @@ -91,6 +87,17 @@ export function sanitizeModelIntent(rawIntent: unknown, command: string): string return capitalize(intent); } +/** Sanitized intent, or undefined when it merely restates the command. */ +export function sanitizeDisplayableModelIntent( + rawIntent: unknown, + command: string +): string | undefined { + const intent = sanitizeModelIntent(rawIntent, command); + return intent && normalizeForComparison(intent) !== normalizeForComparison(command) + ? intent + : undefined; +} + function getIntentOnlyFallback(args: BashToolArgs, command: string): string { const displayName = typeof args.display_name === "string" ? args.display_name.trim() : ""; if (displayName && normalizeForComparison(displayName) !== normalizeForComparison(command)) { diff --git a/src/browser/utils/messages/taskReportLinking.ts b/src/browser/utils/messages/taskReportLinking.ts index 69376ff8da..fb31bf3f84 100644 --- a/src/browser/utils/messages/taskReportLinking.ts +++ b/src/browser/utils/messages/taskReportLinking.ts @@ -13,7 +13,6 @@ export interface LinkedTaskReport { export interface BashTaskSpawnInfo { script: string; - displayName?: string; modelIntent?: string; } @@ -48,7 +47,7 @@ export interface TaskReportLinking { /** * Spawn args of background `bash` tool calls, indexed by taskId. Lets task_await rows - * surface the spawning command's model_intent / display_name. + * surface the spawning command's model_intent, which task_await results do not carry. */ bashSpawnByTaskId: Map; } @@ -120,24 +119,18 @@ function getBashSpawnTaskId(result: unknown): string | null { function getBashSpawnInfoFromArgs(args: unknown): BashTaskSpawnInfo | null { if (typeof args !== "object" || args === null) return null; - const { script, display_name, model_intent } = args as { + const { script, model_intent } = args as { script?: unknown; - display_name?: unknown; model_intent?: unknown; }; - const displayName = - typeof display_name === "string" && display_name.trim().length > 0 - ? display_name.trim() - : undefined; const modelIntent = typeof model_intent === "string" && model_intent.trim().length > 0 ? model_intent.trim() : undefined; - if (displayName === undefined && modelIntent === undefined) return null; + if (modelIntent === undefined) return null; return { script: typeof script === "string" ? script : "", - displayName, modelIntent, }; } @@ -150,8 +143,7 @@ function getBashSpawnInfoFromArgs(args: unknown): BashTaskSpawnInfo | null { * helps the renderer place the final report in a more intuitive location. */ export function computeTaskReportLinking(messages: DisplayedMessage[]): TaskReportLinking { - // First pass: record which taskIds have a visible `task` tool call (and capture spawn - // titles/agent types), plus spawn args of background `bash` tool calls. + // First pass: record which taskIds have a visible `task` tool call (and capture spawn titles). const taskToolCallTaskIds = new Set(); const spawnTitleByTaskId = new Map(); const spawnAgentTypeByTaskId = new Map(); From b7e05300e34e4864dda1f595908ddba80b85855e Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Tue, 4 Aug 2026 15:07:18 +0000 Subject: [PATCH 3/3] fix: prefer spawn title over sub-agent report title in await summary Codex review: the collapsed row should describe the task's spawn intent; the report heading is only a fallback. --- .../features/Tools/TaskToolCall.test.tsx | 31 +++++++++++++++++++ src/browser/features/Tools/TaskToolCall.tsx | 6 ++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/browser/features/Tools/TaskToolCall.test.tsx b/src/browser/features/Tools/TaskToolCall.test.tsx index 9696fc9b10..da4f548182 100644 --- a/src/browser/features/Tools/TaskToolCall.test.tsx +++ b/src/browser/features/Tools/TaskToolCall.test.tsx @@ -511,6 +511,37 @@ describe("TaskAwaitToolCall", () => { expect(view.getByText(/explore · Pagination exploration/)).toBeDefined(); }); + test("prefers the spawn title over the sub-agent's own report title", () => { + const taskSpawn = createToolMessage({ + toolName: "task", + args: { + agentId: "explore", + prompt: "Find pagination helpers.", + title: "Pagination exploration", + run_in_background: true, + }, + result: { status: "queued", taskId: "task-1" }, + }); + + const view = renderTaskAwaitToolCall({ + status: "completed", + result: { + results: [ + { + status: "completed", + taskId: "task-1", + title: "Pagination Helpers Investigation Complete", + reportMarkdown: "Report", + }, + ], + }, + taskReportLinking: computeTaskReportLinking([taskSpawn]), + }); + + expect(view.getByText(/explore · Pagination exploration/)).toBeDefined(); + expect(view.queryByText(/Investigation Complete/)).toBeNull(); + }); + test("keeps multi-task completion summaries count-only", () => { const view = renderTaskAwaitToolCall({ status: "completed", diff --git a/src/browser/features/Tools/TaskToolCall.tsx b/src/browser/features/Tools/TaskToolCall.tsx index c99b893857..7184aa6e1c 100644 --- a/src/browser/features/Tools/TaskToolCall.tsx +++ b/src/browser/features/Tools/TaskToolCall.tsx @@ -1354,12 +1354,14 @@ export const TaskAwaitToolCall: React.FC = ({ firstResult.handleKind === "workspace_turn" ? "workspace" : taskReportLinking?.spawnAgentTypeByTaskId.get(completedTaskId); + // Spawn-side intent first (bash model_intent, task spawn title); the result's own + // title (report heading, bash display_name) is only a fallback. const description = (bashSpawn ? sanitizeDisplayableModelIntent(bashSpawn.modelIntent, bashSpawn.script) : undefined) ?? - trimToNonEmptyString(firstResult.title) ?? - trimToNonEmptyString(taskReportLinking?.spawnTitleByTaskId.get(completedTaskId)); + trimToNonEmptyString(taskReportLinking?.spawnTitleByTaskId.get(completedTaskId)) ?? + trimToNonEmptyString(firstResult.title); const detail = [kind, description].filter((part): part is string => part != null).join(" · "); singleTaskDetail = detail.length > 0 ? detail : undefined; }