diff --git a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.test.ts b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.test.ts index 2ec361ddae..f133dc0cb0 100644 --- a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.test.ts +++ b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.test.ts @@ -134,6 +134,74 @@ describe("CodexAppServerAgent", () => { }); }); + it("nests subagent updates and ignores child turn completion", async () => { + const stub = makeStubRpc({ + initialize: {}, + "thread/start": { thread: { id: "thr_1" } }, + "turn/start": { turn: { id: "turn_1", status: "inProgress" } }, + }); + const { client, sessionUpdates } = makeFakeClient(); + const agent = new CodexAppServerAgent(client, { + processOptions: { binaryPath: "/bundle/codex" }, + model: "gpt-5.5", + rpcFactory: stub.factory, + }); + + await agent.initialize(init); + await agent.newSession({ cwd: "/repo" } as unknown as NewSessionRequest); + let promptSettled = false; + const promptDone = agent + .prompt({ + sessionId: "thr_1", + prompt: [{ type: "text", text: "review this" }], + } as unknown as PromptRequest) + .then((result) => { + promptSettled = true; + return result; + }); + + stub.emit("item/started", { + threadId: "thr_1", + turnId: "turn_1", + item: { + type: "collabAgentToolCall", + id: "spawn-1", + tool: "spawnAgent", + status: "inProgress", + senderThreadId: "thr_1", + receiverThreadIds: ["child-1"], + prompt: "Review auth", + }, + }); + stub.emit("item/agentMessage/delta", { + threadId: "child-1", + turnId: "child-turn", + itemId: "child-message", + delta: "I found an issue.", + }); + stub.emit("turn/completed", { + threadId: "child-1", + turn: { id: "child-turn", status: "completed" }, + }); + + await Promise.resolve(); + expect(promptSettled).toBe(false); + expect(sessionUpdates).toContainEqual({ + sessionId: "thr_1", + update: { + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: "I found an issue." }, + _meta: { posthog: { parentToolCallId: "spawn-1" } }, + }, + }); + + stub.emit("turn/completed", { + threadId: "thr_1", + turn: { id: "turn_1", status: "completed" }, + }); + await expect(promptDone).resolves.toMatchObject({ stopReason: "end_turn" }); + }); + it("includes buffered command output when completion omits aggregatedOutput", async () => { const stub = makeStubRpc({ initialize: {}, diff --git a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts index b39035fc32..a6e3b41ff7 100644 --- a/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts +++ b/packages/agent/src/adapters/codex-app-server/codex-app-server-agent.ts @@ -15,11 +15,16 @@ import type { RequestPermissionResponse, ResumeSessionRequest, ResumeSessionResponse, + SessionNotification, SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, StopReason, } from "@agentclientprotocol/sdk"; -import { mcpToolKey, posthogToolMeta } from "@posthog/shared"; +import { + mcpToolKey, + parentToolCallMeta, + posthogToolMeta, +} from "@posthog/shared"; import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions"; import { DEFAULT_CODEX_MODEL } from "../../gateway-models"; import type { ProcessSpawnedCallback } from "../../types"; @@ -143,6 +148,7 @@ export class CodexAppServerAgent extends BaseAcpAgent { /** Deployment environment; on "cloud" a non-danger sandbox would panic, so we skip the override. */ private environment?: "local" | "cloud"; private readonly commandOutputs = new Map(); + private readonly subagentParentToolCalls = new Map(); /** Extra writable roots for this session, folded into workspaceWrite sandbox turns. */ private additionalDirectories?: string[]; /** The session workspace stays writable when extra roots are applied per turn. */ @@ -361,6 +367,7 @@ export class CodexAppServerAgent extends BaseAcpAgent { additionalDirectories?: string[]; }, ): Promise<{ threadId: string; thread: AppServerThread | undefined }> { + this.subagentParentToolCalls.clear(); this.jsonSchema = params.meta?.jsonSchema ?? undefined; this.taskRunId = params.meta?.taskRunId; this.environment = params.meta?.environment; @@ -935,25 +942,36 @@ export class CodexAppServerAgent extends BaseAcpAgent { private handleNotification(method: string, params: unknown): void { const mappedParams = this.withBufferedCommandOutput(method, params); - if (this.sessionId && !this.session.cancelled) { + this.trackSubagentThreads(mappedParams); + const notificationThreadId = readNotificationThreadId(mappedParams); + const isMainThread = + !notificationThreadId || notificationThreadId === this.threadId; + const parentToolCallId = notificationThreadId + ? this.subagentParentToolCalls.get(notificationThreadId) + : undefined; + + if ( + this.sessionId && + !this.session.cancelled && + (isMainThread || + (parentToolCallId && shouldSurfaceSubagentNotification(method))) + ) { const notification = mapAppServerNotification( this.sessionId, method, mappedParams, ); if (notification) { + const routedNotification = parentToolCallId + ? withParentToolCallId(notification, parentToolCallId) + : notification; void this.client - .sessionUpdate(notification) + .sessionUpdate(routedNotification) .catch((err) => this.logger.warn("sessionUpdate failed", err)); - this.appendNotification(this.sessionId, notification); + this.appendNotification(this.sessionId, routedNotification); } } - if (method === APP_SERVER_NOTIFICATIONS.TURN_STARTED) { - // Capture the active turn id (steer precondition / interrupt target). - this.turns.onStarted((params as { turn?: { id?: string } })?.turn?.id); - } - if (method === APP_SERVER_NOTIFICATIONS.ITEM_STARTED) { this.mcp.capture(params); } @@ -961,6 +979,12 @@ export class CodexAppServerAgent extends BaseAcpAgent { this.mcp.release(params); } + if (!isMainThread) return; + + if (method === APP_SERVER_NOTIFICATIONS.TURN_STARTED) { + this.turns.onStarted((params as { turn?: { id?: string } })?.turn?.id); + } + // codex auto-compaction surfaces as a contextCompaction item: item/started → in progress, // item/completed → boundary (codex emits no separate thread/compacted; that's a guarded // fallback). compactionActive dedupes to one boundary per compaction. @@ -1032,6 +1056,20 @@ export class CodexAppServerAgent extends BaseAcpAgent { } } + private trackSubagentThreads(params: unknown): void { + const item = (params as { item?: AppServerItem } | undefined)?.item; + if ( + item?.type !== "collabAgentToolCall" || + item.tool !== "spawnAgent" || + !item.id + ) { + return; + } + for (const receiverThreadId of item.receiverThreadIds ?? []) { + this.subagentParentToolCalls.set(receiverThreadId, item.id); + } + } + private withBufferedCommandOutput(method: string, params: unknown): unknown { if (!params || typeof params !== "object") { return params; @@ -1434,6 +1472,53 @@ function mapTurnStopReason(status: string | undefined): StopReason { return "end_turn"; } +function readNotificationThreadId(params: unknown): string | undefined { + if (!params || typeof params !== "object") return undefined; + const threadId = (params as { threadId?: unknown }).threadId; + return typeof threadId === "string" ? threadId : undefined; +} + +function shouldSurfaceSubagentNotification(method: string): boolean { + const surfacedMethods: string[] = [ + APP_SERVER_NOTIFICATIONS.AGENT_MESSAGE_DELTA, + APP_SERVER_NOTIFICATIONS.REASONING_TEXT_DELTA, + APP_SERVER_NOTIFICATIONS.REASONING_SUMMARY_TEXT_DELTA, + APP_SERVER_NOTIFICATIONS.PLAN_DELTA, + APP_SERVER_NOTIFICATIONS.ITEM_STARTED, + APP_SERVER_NOTIFICATIONS.ITEM_COMPLETED, + APP_SERVER_NOTIFICATIONS.COMMAND_OUTPUT_DELTA, + APP_SERVER_NOTIFICATIONS.TERMINAL_INTERACTION, + APP_SERVER_NOTIFICATIONS.FILE_CHANGE_PATCH_UPDATED, + ]; + return surfacedMethods.includes(method); +} + +function withParentToolCallId( + notification: SessionNotification, + parentToolCallId: string, +): SessionNotification { + const update = notification.update as SessionNotification["update"] & { + _meta?: Record; + }; + const existingPosthog = + update._meta?.posthog && typeof update._meta.posthog === "object" + ? (update._meta.posthog as Record) + : {}; + return { + ...notification, + update: { + ...update, + _meta: { + ...update._meta, + posthog: { + ...existingPosthog, + ...parentToolCallMeta(parentToolCallId).posthog, + }, + }, + }, + } as SessionNotification; +} + /** The codex thread config override map: folds in MCP servers + makes extra workspace roots writable. Undefined when empty. */ function buildThreadConfig( mcpServers: ReturnType, diff --git a/packages/agent/src/adapters/codex-app-server/mapping.test.ts b/packages/agent/src/adapters/codex-app-server/mapping.test.ts index 776efab120..288e8732bf 100644 --- a/packages/agent/src/adapters/codex-app-server/mapping.test.ts +++ b/packages/agent/src/adapters/codex-app-server/mapping.test.ts @@ -230,6 +230,74 @@ describe("mapAppServerNotification", () => { }); }); + it("maps a spawned Codex agent to an explicit subagent tool call", () => { + const result = mapAppServerNotification( + "s-1", + APP_SERVER_NOTIFICATIONS.ITEM_STARTED, + { + item: { + type: "collabAgentToolCall", + id: "spawn-1", + tool: "spawnAgent", + status: "inProgress", + senderThreadId: "main-thread", + receiverThreadIds: ["child-thread"], + prompt: "Review the authentication changes\nFocus on security.", + model: "gpt-5.5", + reasoningEffort: "high", + }, + }, + ); + + expect(result).toEqual({ + sessionId: "s-1", + update: { + sessionUpdate: "tool_call", + toolCallId: "spawn-1", + title: "Review the authentication changes", + kind: "other", + status: "in_progress", + rawInput: { + prompt: "Review the authentication changes\nFocus on security.", + receiverThreadIds: ["child-thread"], + model: "gpt-5.5", + reasoningEffort: "high", + }, + _meta: { posthog: { toolName: "spawn_agent" } }, + }, + }); + }); + + it("keeps a completed spawn tool call active while its subagent is running", () => { + const result = mapAppServerNotification( + "s-1", + APP_SERVER_NOTIFICATIONS.ITEM_COMPLETED, + { + item: { + type: "collabAgentToolCall", + id: "spawn-1", + tool: "spawnAgent", + status: "completed", + senderThreadId: "main-thread", + receiverThreadIds: ["child-thread"], + prompt: "Review the authentication changes", + agentsStates: { + "child-thread": { status: "running", message: null }, + }, + }, + }, + ); + + expect(result).toEqual({ + sessionId: "s-1", + update: { + sessionUpdate: "tool_call_update", + toolCallId: "spawn-1", + status: "in_progress", + }, + }); + }); + it("drops agent message items (their deltas already streamed)", () => { expect( mapAppServerNotification("s-1", APP_SERVER_NOTIFICATIONS.ITEM_COMPLETED, { diff --git a/packages/agent/src/adapters/codex-app-server/mapping.ts b/packages/agent/src/adapters/codex-app-server/mapping.ts index af9cd206a7..ccb0d7b6e0 100644 --- a/packages/agent/src/adapters/codex-app-server/mapping.ts +++ b/packages/agent/src/adapters/codex-app-server/mapping.ts @@ -206,6 +206,15 @@ export type AppServerItem = { // Present on message/reasoning items replayed from thread history. text?: string; content?: unknown; + senderThreadId?: string; + receiverThreadIds?: string[]; + prompt?: string | null; + model?: string | null; + reasoningEffort?: string | null; + agentsStates?: Record< + string, + { status?: string; message?: string | null } | undefined + >; }; function mcpResultText( @@ -295,14 +304,20 @@ export function mapHistoryItem( status: mapStatus(item.status), ...(tool.rawInput !== undefined ? { rawInput: tool.rawInput } : {}), ...(tool.locations?.length ? { locations: tool.locations } : {}), - ...(tool.mcp + ...(item.type === "collabAgentToolCall" ? { _meta: posthogToolMeta({ - toolName: mcpToolKey(tool.mcp), - mcp: tool.mcp, + toolName: collabAgentToolName(item.tool), }), } - : {}), + : tool.mcp + ? { + _meta: posthogToolMeta({ + toolName: mcpToolKey(tool.mcp), + mcp: tool.mcp, + }), + } + : {}), ...(content ? { content } : {}), }, }, @@ -394,6 +409,21 @@ function describeTool(item: AppServerItem): ToolDescriptor | null { rawInput: item.arguments, output: dynamicToolText(item.contentItems), }; + case "collabAgentToolCall": + return { + title: collabAgentTitle(item), + kind: "other", + rawInput: { + ...(item.prompt ? { prompt: item.prompt } : {}), + ...(item.receiverThreadIds?.length + ? { receiverThreadIds: item.receiverThreadIds } + : {}), + ...(item.model ? { model: item.model } : {}), + ...(item.reasoningEffort + ? { reasoningEffort: item.reasoningEffort } + : {}), + }, + }; case "webSearch": return { title: item.query ?? "Web search", kind: "fetch" }; default: @@ -401,6 +431,42 @@ function describeTool(item: AppServerItem): ToolDescriptor | null { } } +function collabAgentTitle(item: AppServerItem): string { + switch (item.tool) { + case "spawnAgent": + return item.prompt + ? item.prompt.split("\n", 1)[0].slice(0, 120) + : "Spawn subagent"; + case "sendInput": + return "Message subagent"; + case "resumeAgent": + return "Resume subagent"; + case "wait": + return "Wait for subagents"; + case "closeAgent": + return "Close subagent"; + default: + return "Subagent"; + } +} + +function collabAgentToolName(tool: string | undefined): string { + switch (tool) { + case "spawnAgent": + return "spawn_agent"; + case "sendInput": + return "send_input"; + case "resumeAgent": + return "resume_agent"; + case "wait": + return "wait_agent"; + case "closeAgent": + return "close_agent"; + default: + return "subagent"; + } +} + /** Distinct, non-empty changed paths for a fileChange item, order-preserved. */ export function changePaths(changes: AppServerItem["changes"]): string[] { if (!changes?.length) return []; @@ -459,14 +525,20 @@ function mapItem( status: "in_progress", ...(tool.rawInput !== undefined ? { rawInput: tool.rawInput } : {}), ...(tool.locations?.length ? { locations: tool.locations } : {}), - ...(tool.mcp + ...(item.type === "collabAgentToolCall" ? { _meta: posthogToolMeta({ - toolName: mcpToolKey(tool.mcp), - mcp: tool.mcp, + toolName: collabAgentToolName(item.tool), }), } - : {}), + : tool.mcp + ? { + _meta: posthogToolMeta({ + toolName: mcpToolKey(tool.mcp), + mcp: tool.mcp, + }), + } + : {}), }, }; } @@ -477,7 +549,7 @@ function mapItem( update: { sessionUpdate: "tool_call_update", toolCallId: item.id, - status: mapStatus(item.status), + status: mapToolStatus(item), ...(content ? { content } : {}), }, }; @@ -523,6 +595,20 @@ function mapStatus( return "in_progress"; } +function mapToolStatus( + item: AppServerItem, +): "completed" | "failed" | "in_progress" { + if ( + item.type === "collabAgentToolCall" && + Object.values(item.agentsStates ?? {}).some( + (agent) => agent?.status === "pendingInit" || agent?.status === "running", + ) + ) { + return "in_progress"; + } + return mapStatus(item.status); +} + function readItem(params: unknown): AppServerItem | null { if (params && typeof params === "object" && "item" in params) { const item = (params as Record).item; diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 35fbca6805..e00ca75a46 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -247,11 +247,13 @@ export { export { mcpToolKey, type PosthogToolMeta, + parentToolCallMeta, parseMcpToolName, posthogToolMeta, readAgentToolName, readMcpToolDescriptor, readMcpToolName, + readParentToolCallId, } from "./tool-meta"; export { TypedEventEmitter } from "./typed-event-emitter"; export { isSafeExternalUrl } from "./url"; diff --git a/packages/shared/src/tool-meta.test.ts b/packages/shared/src/tool-meta.test.ts index 8e718d8616..9e9c594388 100644 --- a/packages/shared/src/tool-meta.test.ts +++ b/packages/shared/src/tool-meta.test.ts @@ -4,6 +4,7 @@ import { readAgentToolName, readMcpToolDescriptor, readMcpToolName, + readParentToolCallId, } from "./tool-meta"; describe("parseMcpToolName", () => { @@ -49,6 +50,25 @@ describe("readAgentToolName", () => { }); }); +describe("readParentToolCallId", () => { + it("prefers the posthog channel over the Claude fallback", () => { + expect( + readParentToolCallId({ + posthog: { parentToolCallId: "codex-parent" }, + claudeCode: { parentToolCallId: "claude-parent" }, + }), + ).toBe("codex-parent"); + }); + + it("supports existing Claude subagent metadata", () => { + expect( + readParentToolCallId({ + claudeCode: { parentToolCallId: "claude-parent" }, + }), + ).toBe("claude-parent"); + }); +}); + describe("readMcpToolDescriptor / readMcpToolName", () => { it("uses the structured mcp descriptor when present (no name parsing)", () => { const meta = { diff --git a/packages/shared/src/tool-meta.ts b/packages/shared/src/tool-meta.ts index 8ef62cc324..819af4cb7f 100644 --- a/packages/shared/src/tool-meta.ts +++ b/packages/shared/src/tool-meta.ts @@ -12,6 +12,8 @@ export interface PosthogToolMeta { toolName: string; /** Set only for MCP tool calls — the originating server + tool. */ mcp?: { server: string; tool: string }; + /** Parent tool call when this update belongs to a delegated agent. */ + parentToolCallId?: string; } /** `_meta` fragment for adapters to spread onto a tool_call update. */ @@ -21,6 +23,12 @@ export function posthogToolMeta(meta: PosthogToolMeta): { return { posthog: meta }; } +export function parentToolCallMeta(parentToolCallId: string): { + posthog: Pick; +} { + return { posthog: { parentToolCallId } }; +} + /** Build the canonical `mcp____` key. */ export function mcpToolKey(mcp: { server: string; tool: string }): string { return `mcp__${mcp.server}__${mcp.tool}`; @@ -43,9 +51,9 @@ export function parseMcpToolName( } interface ToolCallMeta { - posthog?: PosthogToolMeta; + posthog?: Partial; /** Legacy Claude-adapter channel, read only as a fallback. */ - claudeCode?: { toolName?: string }; + claudeCode?: { toolName?: string; parentToolCallId?: string }; } function asToolCallMeta(meta: unknown): ToolCallMeta | undefined { @@ -58,6 +66,11 @@ export function readAgentToolName(meta: unknown): string | undefined { return m?.posthog?.toolName ?? m?.claudeCode?.toolName; } +export function readParentToolCallId(meta: unknown): string | undefined { + const m = asToolCallMeta(meta); + return m?.posthog?.parentToolCallId ?? m?.claudeCode?.parentToolCallId; +} + /** * The MCP `{ server, tool }` descriptor for a tool call, or undefined for a * non-MCP call. Prefers the structured channel, else parses the legacy diff --git a/packages/ui/src/features/sessions/components/buildConversationItems.ts b/packages/ui/src/features/sessions/components/buildConversationItems.ts index d4386b921e..459ba591ab 100644 --- a/packages/ui/src/features/sessions/components/buildConversationItems.ts +++ b/packages/ui/src/features/sessions/components/buildConversationItems.ts @@ -12,6 +12,7 @@ import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, + readParentToolCallId, type UserShellExecuteParams, } from "@posthog/shared"; import { @@ -747,10 +748,7 @@ function extractUserPrompt(params: unknown): { } function getParentToolCallId(update: SessionUpdate): string | undefined { - const meta = (update as Record)?._meta as - | { claudeCode?: { parentToolCallId?: string } } - | undefined; - return meta?.claudeCode?.parentToolCallId; + return readParentToolCallId((update as Record)?._meta); } function pushChildItem(b: ItemBuilder, parentId: string, update: RenderItem) { diff --git a/packages/ui/src/features/sessions/components/incrementalConversationItems.test.ts b/packages/ui/src/features/sessions/components/incrementalConversationItems.test.ts index d793141e2c..9001805db0 100644 --- a/packages/ui/src/features/sessions/components/incrementalConversationItems.test.ts +++ b/packages/ui/src/features/sessions/components/incrementalConversationItems.test.ts @@ -133,6 +133,7 @@ const childToolCallMsg = ( ts: number, toolCallId: string, parentToolCallId: string, + meta: "claudeCode" | "posthog" = "claudeCode", ) => updateMsg(ts, { sessionUpdate: "tool_call", @@ -140,7 +141,7 @@ const childToolCallMsg = ( kind: "read", status: "pending", title: toolCallId, - _meta: { claudeCode: { parentToolCallId } }, + _meta: { [meta]: { parentToolCallId } }, }); // --- normalization (cycle-free, Map-resolved) ----------------------------- @@ -475,4 +476,22 @@ describe("createIncrementalConversationBuilder", () => { expect(row2.turnContext.childItems).not.toBe(row1.turnContext.childItems); expect(row2.turnContext.childItems.get("agent1")?.length).toBe(1); }); + + it("nests Codex child tool calls using canonical PostHog metadata", () => { + const inc = createIncrementalConversationBuilder(); + const events = [ + userPromptMsg(1, 1, "go"), + toolCallMsg(2, "agent1", { + _meta: { posthog: { toolName: "spawn_agent" } }, + }), + childToolCallMsg(3, "child1", "agent1", "posthog"), + ]; + + const result = inc.update(events, true); + const row = result.items.find((item) => item.type === "session_update"); + if (row?.type !== "session_update") { + throw new Error("expected agent session_update row"); + } + expect(row.turnContext.childItems.get("agent1")?.length).toBe(1); + }); }); diff --git a/packages/ui/src/features/sessions/components/new-thread/buildThreadGroups.test.ts b/packages/ui/src/features/sessions/components/new-thread/buildThreadGroups.test.ts index 5824702632..f338ac3bc4 100644 --- a/packages/ui/src/features/sessions/components/new-thread/buildThreadGroups.test.ts +++ b/packages/ui/src/features/sessions/components/new-thread/buildThreadGroups.test.ts @@ -73,4 +73,26 @@ describe("buildThreadGroups MCP detection", () => { expect(grouping.idToRowIndex.get("t1")).toBe(0); expect(grouping.idToRowIndex.get("t2")).toBe(0); }); + + it("counts only spawned agents as subagents", () => { + const items = [ + toolCallItem("spawn-1", { + posthog: { toolName: "spawn_agent" }, + }), + toolCallItem("wait-1", { + posthog: { toolName: "wait_agent" }, + }), + toolCallItem("close-1", { + posthog: { toolName: "close_agent" }, + }), + ]; + + const grouping = buildThreadGroups(items, "all", {}); + const row = grouping.rows[0]; + expect(row.kind).toBe("tool_group"); + if (row.kind !== "tool_group") return; + expect(row.summary.counts.subagents).toBe(1); + expect(row.summary.counts.other).toBe(2); + expect(row.summary.doneLabel).toBe("1 subagent, 2 tool calls"); + }); }); diff --git a/packages/ui/src/features/sessions/components/new-thread/conversationThreadConfig.ts b/packages/ui/src/features/sessions/components/new-thread/conversationThreadConfig.ts index 1ef473979b..9b7a01aea1 100644 --- a/packages/ui/src/features/sessions/components/new-thread/conversationThreadConfig.ts +++ b/packages/ui/src/features/sessions/components/new-thread/conversationThreadConfig.ts @@ -55,10 +55,19 @@ export const COLLAPSE_MODE_OPTIONS: { export const grouping = { /** - * Tool names that spawn a subagent. Counted separately in the chip summary. - * @see ToolCallBlock — same names drive the SubagentToolView branch. + * Tool names that create a subagent. Counted separately in the chip summary. */ - subagentToolNames: new Set(["Task", "Agent"]), + subagentToolNames: new Set(["Task", "Agent", "spawn_agent"]), + /** Collaboration tools rendered with the dedicated subagent view. */ + collaborationToolNames: new Set([ + "Task", + "Agent", + "spawn_agent", + "send_input", + "resume_agent", + "wait_agent", + "close_agent", + ]), /** * MCP-app tool calls are excluded from collapsing so their iframes stay * mounted (the `keepMounted` contract). Flip to false to fold them in. diff --git a/packages/ui/src/features/sessions/components/session-update/SubagentToolView.tsx b/packages/ui/src/features/sessions/components/session-update/SubagentToolView.tsx index 615be6857d..ca1308fc78 100644 --- a/packages/ui/src/features/sessions/components/session-update/SubagentToolView.tsx +++ b/packages/ui/src/features/sessions/components/session-update/SubagentToolView.tsx @@ -86,7 +86,8 @@ export function SubagentToolView({ - {title || "Subagent"} + Subagent + {title && title !== "Subagent" ? ` · ${title}` : ""} @@ -135,7 +136,10 @@ export function SubagentToolView({ wasCancelled={wasCancelled} content={childContent} > - {title || "Subagent"} + + Subagent + {title && title !== "Subagent" ? ` · ${title}` : ""} + ); diff --git a/packages/ui/src/features/sessions/components/session-update/ToolCallBlock.tsx b/packages/ui/src/features/sessions/components/session-update/ToolCallBlock.tsx index dcfe1732db..31620d09c7 100644 --- a/packages/ui/src/features/sessions/components/session-update/ToolCallBlock.tsx +++ b/packages/ui/src/features/sessions/components/session-update/ToolCallBlock.tsx @@ -16,6 +16,7 @@ import type { ToolCall } from "@posthog/ui/features/sessions/types"; import { Box } from "@radix-ui/themes"; import type { ConversationItem, TurnContext } from "../buildConversationItems"; import { useChatThreadChrome } from "../chat-thread/chatThreadChrome"; +import { grouping } from "../new-thread/conversationThreadConfig"; import { MCP_TOOL_BLOCK_COMPONENT, type McpToolBlockComponent, @@ -47,13 +48,10 @@ export function ToolCallBlock({ const props = { toolCall, turnCancelled, turnComplete }; - if ( - (toolName === "Task" || toolName === "Agent") && - childItems && - childItems.length > 0 - ) { + if (isSubagentTool(toolName)) { + const subagentChildItems = childItems ?? []; const turnContext: TurnContext = { - toolCalls: buildChildToolCallsMap(childItems), + toolCalls: buildChildToolCallsMap(subagentChildItems), childItems: childItemsMap ?? new Map(), turnCancelled: turnCancelled ?? false, turnComplete: turnComplete ?? false, @@ -62,7 +60,7 @@ export function ToolCallBlock({ @@ -111,6 +109,10 @@ export function ToolCallBlock({ return {content}; } +function isSubagentTool(toolName: string | undefined): boolean { + return grouping.collaborationToolNames.has(toolName ?? ""); +} + function buildChildToolCallsMap( childItems: ConversationItem[], ): Map {