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 a44aa3b927..830e8c21cf 100644 --- a/packages/agent/src/adapters/codex-app-server/mapping.test.ts +++ b/packages/agent/src/adapters/codex-app-server/mapping.test.ts @@ -262,6 +262,49 @@ describe("mapAppServerNotification", () => { }); }); + it("maps a started wait tool call so the UI shows the main thread waiting on its subagents", () => { + const result = mapAppServerNotification( + "s-1", + APP_SERVER_NOTIFICATIONS.ITEM_STARTED, + { + item: { + type: "collabAgentToolCall", + id: "wait-1", + tool: "wait", + status: "inProgress", + senderThreadId: "main-thread", + }, + }, + ); + + expect(result).toEqual({ + sessionId: "s-1", + update: { + sessionUpdate: "tool_call", + toolCallId: "wait-1", + title: "Wait for subagents", + kind: "other", + status: "in_progress", + rawInput: {}, + _meta: { posthog: { toolName: "wait_agent" } }, + }, + }); + }); + + it("still drops closeAgent lifecycle events", () => { + expect( + mapAppServerNotification("s-1", APP_SERVER_NOTIFICATIONS.ITEM_STARTED, { + item: { + type: "collabAgentToolCall", + id: "close-1", + tool: "closeAgent", + status: "inProgress", + senderThreadId: "main-thread", + }, + }), + ).toBeNull(); + }); + it("keeps a completed spawn tool call terminal while its subagent is running", () => { const result = mapAppServerNotification( "s-1", diff --git a/packages/agent/src/adapters/codex-app-server/mapping.ts b/packages/agent/src/adapters/codex-app-server/mapping.ts index 6f5dfc8d51..ff7ee96d72 100644 --- a/packages/agent/src/adapters/codex-app-server/mapping.ts +++ b/packages/agent/src/adapters/codex-app-server/mapping.ts @@ -411,7 +411,7 @@ function describeTool(item: AppServerItem): ToolDescriptor | null { output: dynamicToolText(item.contentItems), }; case "collabAgentToolCall": - if (item.tool === "wait" || item.tool === "closeAgent") { + if (item.tool === "closeAgent") { return null; } return { diff --git a/packages/ui/src/features/sessions/components/ConversationView.tsx b/packages/ui/src/features/sessions/components/ConversationView.tsx index 5a049e930a..b988d136a2 100644 --- a/packages/ui/src/features/sessions/components/ConversationView.tsx +++ b/packages/ui/src/features/sessions/components/ConversationView.tsx @@ -15,6 +15,7 @@ import type { ConversationItem, TurnContext, } from "@posthog/ui/features/sessions/components/buildConversationItems"; +import { conversationHasActiveSubagent } from "@posthog/ui/features/sessions/components/buildConversationItems"; import { ConversationSearchBar } from "@posthog/ui/features/sessions/components/ConversationSearchBar"; import { PROMPT_RECALL_HINT_KEY, @@ -197,6 +198,15 @@ export function ConversationView({ [conversationItems, optimisticItems, isCloud], ); + // A subagent keeps running detached after the parent prompt completes; keep the + // footer's generating indicator alive until its nested child work settles. This + // is display-only: the prompt-lifecycle gates (queue/steer) still read the raw + // `isPromptPending`, so a follow-up message sends instead of queueing. + const footerPromptPending = useMemo( + () => !!isPromptPending || conversationHasActiveSubagent(items), + [isPromptPending, items], + ); + // Fold each completed turn's tool-call work into a collapsible chip, and emit // the keepMounted indices (standalone MCP-app rows, whose iframes must survive // scrolling) + the item→row map in the same pass. @@ -454,7 +464,7 @@ export function ConversationView({