From c90836cb53d02109d6b7007b61840226d7fac724 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 29 May 2026 12:52:35 +0000 Subject: [PATCH] Reveal tail propose_plan in hyper density --- src/browser/components/ChatPane/ChatPane.tsx | 48 +++++- .../Messages/TranscriptDensity.stories.tsx | 42 +++++ tests/ui/chat/transcriptDensity.test.ts | 154 ++++++++++++++++++ 3 files changed, 241 insertions(+), 3 deletions(-) diff --git a/src/browser/components/ChatPane/ChatPane.tsx b/src/browser/components/ChatPane/ChatPane.tsx index 107bfde516..a99a44aeda 100644 --- a/src/browser/components/ChatPane/ChatPane.tsx +++ b/src/browser/components/ChatPane/ChatPane.tsx @@ -113,6 +113,18 @@ import { recordSyntheticReactRenderSample } from "@/browser/utils/perf/reactProf const TRANSCRIPT_ONLY_NOTICE = "This workspace's worktree is no longer available. This is a read-only chat transcript kept for historical and usage-tracking reasons."; +function findTailProposePlanToolId(messages: readonly DisplayedMessage[]): string | null { + for (let index = messages.length - 1; index >= 0; index -= 1) { + const message = messages[index]; + if (message.type !== "tool") { + continue; + } + return message.toolName === "propose_plan" ? message.id : null; + } + + return null; +} + function PerfRenderMarker(props: { id: string; children: React.ReactNode }): React.ReactElement { const renderStartTimeRef = useRef(performance.now()); renderStartTimeRef.current = performance.now(); @@ -463,6 +475,21 @@ const ChatPaneContent: React.FC = (props) => { [canInterrupt, deferredMessages, isStreamStarting, transcriptDensity] ); + // A tail propose_plan usually means the agent paused for user review; reveal only the + // containing hyper-density bundles by default so historical plans stay collapsed. + const tailProposePlanToolId = + transcriptDensity === "hyper" ? findTailProposePlanToolId(deferredMessages) : null; + const tailProposePlanIndex = + tailProposePlanToolId === null + ? -1 + : deferredMessages.findIndex((message) => message.id === tailProposePlanToolId); + const tailProposePlanWorkBundleKey = + tailProposePlanIndex === -1 ? null : (workBundleInfos?.[tailProposePlanIndex]?.key ?? null); + const tailProposePlanOperationalBundleKey = + tailProposePlanIndex === -1 + ? null + : (operationalBundleInfos?.[tailProposePlanIndex]?.key ?? null); + const autoCompactionResult = useMemo( () => checkAutoCompaction( @@ -1235,8 +1262,12 @@ const ChatPaneContent: React.FC = (props) => { const workBundleOverride = workBundle ? workBundleExpansionOverrides.get(workBundle.key) : undefined; + const defaultRevealTailPlanWorkBundle = + tailProposePlanWorkBundleKey !== null && + workBundle?.key === tailProposePlanWorkBundleKey; const isWorkBundleExpanded = workBundle - ? (workBundleOverride ?? workBundle.defaultExpanded) + ? (workBundleOverride ?? + (defaultRevealTailPlanWorkBundle || workBundle.defaultExpanded)) : false; const keepCollapsedWorkBundleMemberVisible = @@ -1259,8 +1290,13 @@ const ChatPaneContent: React.FC = (props) => { const operationalBundleOverride = operationalBundle ? operationalBundleExpansionOverrides.get(operationalBundle.key) : undefined; + const defaultRevealTailPlanOperationalBundle = + tailProposePlanOperationalBundleKey !== null && + operationalBundle?.key === tailProposePlanOperationalBundleKey; const isOperationalBundleExpanded = operationalBundle - ? (operationalBundleOverride ?? operationalBundle.defaultExpanded) + ? (operationalBundleOverride ?? + (defaultRevealTailPlanOperationalBundle || + operationalBundle.defaultExpanded)) : false; if ( @@ -1301,8 +1337,14 @@ const ChatPaneContent: React.FC = (props) => { nestedOperationalBundle.key ) : undefined; + const defaultRevealTailPlanNestedBundle = + tailProposePlanOperationalBundleKey !== null && + nestedOperationalBundle?.key === + tailProposePlanOperationalBundleKey; const isNestedExpanded = nestedOperationalBundle - ? (nestedOverride ?? nestedOperationalBundle.defaultExpanded) + ? (nestedOverride ?? + (defaultRevealTailPlanNestedBundle || + nestedOperationalBundle.defaultExpanded)) : false; if ( diff --git a/src/browser/features/Messages/TranscriptDensity.stories.tsx b/src/browser/features/Messages/TranscriptDensity.stories.tsx index 6eda817ae9..1594da108f 100644 --- a/src/browser/features/Messages/TranscriptDensity.stories.tsx +++ b/src/browser/features/Messages/TranscriptDensity.stories.tsx @@ -12,6 +12,7 @@ import { createFileReadTool, createGenericTool, createPendingTool, + createProposePlanTool, createWebSearchTool, } from "@/browser/stories/mocks/tools"; import { STABLE_TIMESTAMP } from "@/browser/stories/mocks/workspaces"; @@ -121,6 +122,47 @@ export const HyperCollapsedBundles: AppStory = { render: () => setupTranscriptDensityStory("hyper")} />, }; +export const HyperTailProposePlanExpanded: AppStory = { + parameters: { chromatic: { modes: CHROMATIC_SMOKE_MODES } }, + render: () => ( + { + collapseLeftSidebar(); + setDensity("hyper"); + return setupSimpleChatStory({ + messages: [ + createUserMessage("tail-plan-user-1", "Plan the transcript density fix", { + historySequence: 1, + timestamp: STABLE_TIMESTAMP - 20_000, + }), + createAssistantMessage("tail-plan-assistant-1", "I'll draft the implementation plan.", { + historySequence: 2, + timestamp: STABLE_TIMESTAMP - 15_000, + toolCalls: [ + createProposePlanTool( + "tail-plan-tool-1", + [ + "# Tail Plan", + "", + "## Acceptance", + "", + "- The tail propose_plan is visible in hyper density without expanding bundles.", + ].join("\n") + ), + { + type: "text", + text: "Plan ready for review.", + timestamp: STABLE_TIMESTAMP - 5_000, + }, + ], + }), + ], + }); + }} + /> + ), +}; + export const HyperExpandedBundle: AppStory = { // Chromatic executes this play function for the visual snapshot, while the // app-level UI test covers expansion behavior without Storybook's manager-page timing. diff --git a/tests/ui/chat/transcriptDensity.test.ts b/tests/ui/chat/transcriptDensity.test.ts index 4e125bbe14..9c49bdc654 100644 --- a/tests/ui/chat/transcriptDensity.test.ts +++ b/tests/ui/chat/transcriptDensity.test.ts @@ -10,6 +10,7 @@ import { createBashTool, createFileReadTool, createGenericTool, + createProposePlanTool, createWebSearchTool, } from "@/browser/stories/mocks/tools"; import { installDom } from "../dom"; @@ -182,4 +183,157 @@ describe("Hyper transcript density", () => { await cleanupView(view, cleanupDom); } }, 30_000); + + test("reveals a tail propose_plan through collapsed hyper-density bundles", async () => { + const cleanupDom = installDom(); + updatePersistedState(TRANSCRIPT_DENSITY_KEY, "hyper"); + + const metadata = createWorkspace({ + id: "ws-tail-plan", + name: "tail-plan", + projectName: "my-app", + projectPath: "/home/user/projects/my-app", + }); + const client = setupSimpleChatStory({ + workspaceId: metadata.id, + workspaceName: metadata.name, + projectName: metadata.projectName, + projectPath: metadata.projectPath, + messages: [ + createUserMessage("tail-plan-user-1", "Plan the transcript density fix", { + historySequence: 1, + timestamp: 0, + }), + createAssistantMessage("tail-plan-assistant-1", "I'll draft the implementation plan.", { + historySequence: 2, + timestamp: 1_000, + toolCalls: [ + createProposePlanTool( + "tail-plan-tool-1", + "# Tail Plan\n\n- Reveal the tail propose_plan without a click." + ), + { type: "text", text: "Plan ready for review." }, + ], + }), + ], + }); + const view = renderApp({ apiClient: client, metadata }); + + try { + await setupWorkspaceView(view, metadata, metadata.id); + + const workButton = await waitFor(() => { + const button = queryButton(view.container, "work-bundle"); + if (!button) { + throw new Error("Tail plan work bundle button not found"); + } + return button; + }); + expect(workButton.getAttribute("aria-expanded")).toBe("true"); + + const operationalButton = await waitFor(() => { + const button = queryButton(view.container, "operational-bundle"); + if (!button) { + throw new Error("Tail plan operational bundle button not found"); + } + return button; + }); + expect(operationalButton.getAttribute("aria-expanded")).toBe("true"); + expect(view.container.textContent).toContain("Tail Plan"); + expect(view.container.textContent).toContain("Reveal the tail propose_plan without a click."); + + fireEvent.click(workButton); + await waitFor(() => { + expect(workButton.getAttribute("aria-expanded")).toBe("false"); + }); + expect(view.container.textContent).not.toContain("Tail Plan"); + } finally { + await cleanupView(view, cleanupDom); + } + }, 30_000); + + test("keeps historical propose_plan collapsed when a later image tool call exists", async () => { + const cleanupDom = installDom(); + updatePersistedState(TRANSCRIPT_DENSITY_KEY, "hyper"); + + const metadata = createWorkspace({ + id: "ws-historical-plan", + name: "historical-plan", + projectName: "my-app", + projectPath: "/home/user/projects/my-app", + }); + const client = setupSimpleChatStory({ + workspaceId: metadata.id, + workspaceName: metadata.name, + projectName: metadata.projectName, + projectPath: metadata.projectPath, + messages: [ + createUserMessage("historical-plan-user-1", "Plan then validate", { + historySequence: 1, + timestamp: 0, + }), + createAssistantMessage("historical-plan-assistant-1", "I'll plan and then validate.", { + historySequence: 2, + timestamp: 1_000, + toolCalls: [ + createProposePlanTool( + "historical-plan-tool-1", + "# Historical Plan\n\n- This older plan should stay hidden." + ), + createGenericTool( + "historical-plan-image-1", + "image_generate", + { prompt: "Create a validation image" }, + { + success: true, + model: "gpt-image-1", + prompt: "Create a validation image", + requestedCount: 1, + images: [ + { + path: "/tmp/generated.png", + filename: "generated.png", + mediaType: "image/png", + }, + ], + } + ), + { type: "text", text: "Validation finished." }, + ], + }), + ], + }); + const view = renderApp({ apiClient: client, metadata }); + + try { + await setupWorkspaceView(view, metadata, metadata.id); + await waitFor(() => { + expect(view.container.textContent).toContain("Validation finished."); + }); + expect(view.container.textContent).not.toContain("Historical Plan"); + + const workButton = queryButton(view.container, "work-bundle"); + if (workButton) { + expect(workButton.getAttribute("aria-expanded")).toBe("false"); + fireEvent.click(workButton); + } + + const operationalButton = await waitFor(() => { + const button = queryButton(view.container, "operational-bundle"); + if (!button) { + throw new Error("Historical plan operational bundle button not found"); + } + return button; + }); + expect(operationalButton.getAttribute("aria-expanded")).toBe("false"); + expect(view.container.textContent).not.toContain("Historical Plan"); + + fireEvent.click(operationalButton); + await waitFor(() => { + expect(view.container.textContent).toContain("Historical Plan"); + }); + } finally { + await cleanupView(view, cleanupDom); + } + }, 30_000); });