From 566e4fa0cb2657e5333dceed3cfaba384c3fe4f9 Mon Sep 17 00:00:00 2001 From: Pixel Perfect Date: Fri, 10 Jul 2026 11:59:37 -0700 Subject: [PATCH 1/4] feat(subagents): disclose projected results consistently --- .../testkit/fixtures/subagent/codex_output.ts | 8 +++ .../fixtures/subagent_v2/codex_output.ts | 8 +++ .../components/chat/MessagesTimeline.test.tsx | 56 +++++++++++++++++++ .../src/components/chat/V2LifecycleRow.tsx | 45 ++++++++++++++- 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_output.ts index 143b9bd9002..6e9160624ba 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent/codex_output.ts @@ -68,6 +68,14 @@ export function assertSubagentOutput( assert.isNotNull(subagent.providerThreadId); assert.isNotNull(subagent.nativeTaskRef); assert.isNotNull(subagent.completedAt); + const parentItem = projection.turnItems.find( + (item) => item.type === "subagent" && item.subagentId === subagent.id, + ); + assert.isDefined(parentItem); + if (parentItem?.type !== "subagent") { + throw new Error(`Missing parent lifecycle item for subagent ${subagent.id}`); + } + assert.equal(parentItem.result, subagent.result); if (subagent.childThreadId === null) { throw new Error(`Subagent ${subagent.id} is missing its child thread`); } diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/codex_output.ts b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/codex_output.ts index b3154161a67..aa73387fa84 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/codex_output.ts +++ b/apps/server/src/orchestration-v2/testkit/fixtures/subagent_v2/codex_output.ts @@ -47,6 +47,14 @@ export function assertSubagentV2Output( assert.isNotNull(subagent.providerThreadId); assert.isNotNull(subagent.nativeTaskRef); assert.isNotNull(subagent.completedAt); + const parentItem = projection.turnItems.find( + (item) => item.type === "subagent" && item.subagentId === subagent.id, + ); + assert.isDefined(parentItem); + if (parentItem?.type !== "subagent") { + throw new Error(`Missing parent lifecycle item for subagent ${subagent.id}`); + } + assert.equal(parentItem.result, subagent.result); if (subagent.childThreadId === null) { throw new Error(`Subagent ${subagent.id} is missing its child thread`); } diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index b784a1cebaf..186133e9897 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -759,9 +759,65 @@ describe("MessagesTimeline", () => { expect(markup).toContain('aria-label="Open Package audit"'); expect(markup).toContain("Reading src/index.ts"); expect(markup).not.toContain("Inspect the package"); + expect(markup).not.toContain('data-v2-subagent-result-disclosure="true"'); expect(markup).not.toContain("Work Log"); }); + it("discloses the full Codex subagent result without projecting child events", async () => { + const { MessagesTimeline } = await import("./MessagesTimeline"); + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain('data-v2-item-type="subagent"'); + expect(markup).toContain('data-v2-subagent-result-disclosure="true"'); + expect(markup).toContain('data-v2-subagent-result="true"'); + expect(markup).toContain('aria-label="Show full result for Isolation report"'); + expect(markup).toContain('aria-label="Open Isolation report"'); + expect(markup).toContain("Tests should be isolated."); + expect(markup).toContain("Result: no shared state."); + expect(markup).not.toContain("Explain test isolation"); + }); + it("renders V2 provider failures as standalone error rows", async () => { const { MessagesTimeline } = await import("./MessagesTimeline"); const markup = renderToStaticMarkup( diff --git a/apps/web/src/components/chat/V2LifecycleRow.tsx b/apps/web/src/components/chat/V2LifecycleRow.tsx index e19ebd9ee1c..e1460a6eb61 100644 --- a/apps/web/src/components/chat/V2LifecycleRow.tsx +++ b/apps/web/src/components/chat/V2LifecycleRow.tsx @@ -2,6 +2,7 @@ import type { OrchestrationV2TurnItem, ThreadId } from "@t3tools/contracts"; import type { TimestampFormat } from "@t3tools/contracts/settings"; import { BotIcon, + ChevronDownIcon, ExternalLinkIcon, GitForkIcon, MessageSquareIcon, @@ -123,6 +124,7 @@ export function V2LifecycleRow(props: { detail={item.result ?? item.progress ?? item.prompt} badge={item.status} threadId={item.childThreadId} + expandedDetail={item.result} onOpenThread={props.onOpenThread} /> ); @@ -135,12 +137,14 @@ function RelatedThreadCard(props: { readonly icon: LucideIcon; readonly title: string; readonly detail: string; + readonly expandedDetail?: string | null; readonly badge: string; readonly threadId: ThreadId | null; readonly onOpenThread: (threadId: ThreadId) => void; }) { const Icon = props.icon; const threadId = props.threadId; + const expandedDetail = props.expandedDetail?.trim() ? props.expandedDetail : null; const content = ( <> @@ -149,12 +153,46 @@ function RelatedThreadCard(props: { {props.badge} - {threadId === null ? null : ( -