From ab8bdaf3431a43bab21c1acd47acaee3d2c17283 Mon Sep 17 00:00:00 2001 From: PostHog Code Date: Wed, 13 May 2026 20:56:48 +0000 Subject: [PATCH 1/2] feat(sessions): keep full plan accessible after approval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Once a plan was approved or rejected, the full plan markdown was unmounted and only the "Plan approved" line remained — making it impossible to revisit the plan when handing work off to a parallel or master agent. The plan text is still carried on `toolCall.rawInput.plan`; add a `show plan` / `hide plan` toggle next to the status line so the full plan stays accessible (collapsed by default to avoid dominating the turn view). Generated-By: PostHog Code Task-Id: fa647642-8b10-4bf8-a05d-896f5e355489 --- .../session-update/PlanApprovalView.tsx | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx b/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx index cb88a68e6..5fe20929c 100644 --- a/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx +++ b/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx @@ -1,7 +1,7 @@ import { PlanContent } from "@components/permissions/PlanContent"; -import { CheckCircle } from "@phosphor-icons/react"; +import { CaretDown, CaretRight, CheckCircle } from "@phosphor-icons/react"; import { Box, Flex, Text } from "@radix-ui/themes"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import { type ToolViewProps, useToolCallStatus } from "./toolCallUtils"; export function PlanApprovalView({ @@ -15,6 +15,7 @@ export function PlanApprovalView({ turnCancelled, turnComplete, ); + const [isPlanExpanded, setIsPlanExpanded] = useState(false); const planText = useMemo(() => { const rawPlan = (toolCall.rawInput as { plan?: string } | undefined)?.plan; @@ -33,30 +34,57 @@ export function PlanApprovalView({ return null; }, [content, toolCall.rawInput]); - const showPlanContent = !isComplete && !wasCancelled; const showResult = isComplete || wasCancelled; + const showPlanInline = !showResult; + const canTogglePlan = showResult && !!planText; if (!planText && !showResult) return null; return ( - {showPlanContent && planText && ( + {showPlanInline && planText && ( )} {showResult && ( - - {isComplete ? ( - <> - - - Plan approved — proceeding with implementation + + setIsPlanExpanded((v) => !v) : undefined + } + > + {canTogglePlan && + (isPlanExpanded ? ( + + ) : ( + + ))} + {isComplete ? ( + <> + + + Plan approved — proceeding with implementation + + + ) : wasCancelled ? ( + (Plan rejected) + ) : null} + {canTogglePlan && ( + + · {isPlanExpanded ? "hide plan" : "show plan"} - - ) : wasCancelled ? ( - (Plan rejected) - ) : null} - + )} + + + {canTogglePlan && isPlanExpanded && ( + + + + )} + )} ); From 1567415690a9a2cbc23500f9bcf6557dc06f6e57 Mon Sep 17 00:00:00 2001 From: Charles Vien Date: Wed, 20 May 2026 19:22:41 -0700 Subject: [PATCH 2/2] make plan toggle a button and add tests --- .../session-update/PlanApprovalView.test.tsx | 103 ++++++++++++++++++ .../session-update/PlanApprovalView.tsx | 64 ++++++----- 2 files changed, 137 insertions(+), 30 deletions(-) create mode 100644 apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.test.tsx diff --git a/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.test.tsx b/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.test.tsx new file mode 100644 index 000000000..1e96c3403 --- /dev/null +++ b/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.test.tsx @@ -0,0 +1,103 @@ +import type { ToolCall } from "@features/sessions/types"; +import { Theme } from "@radix-ui/themes"; +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { describe, expect, it } from "vitest"; +import { PlanApprovalView } from "./PlanApprovalView"; + +const PLAN_MARKER = "Sentinel plan body for testing"; + +function makeToolCall(overrides: Partial = {}): ToolCall { + return { + toolCallId: "tc-1", + title: "Ready to code?", + kind: "switch_mode", + status: "pending", + rawInput: { plan: PLAN_MARKER }, + ...overrides, + }; +} + +function renderView(props: { + toolCall: ToolCall; + turnCancelled?: boolean; + turnComplete?: boolean; +}) { + return render( + + + , + ); +} + +describe("PlanApprovalView", () => { + it("renders the full plan and no toggle while pending", () => { + renderView({ toolCall: makeToolCall({ status: "pending" }) }); + + expect(screen.getByText(PLAN_MARKER)).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /show plan/i }), + ).not.toBeInTheDocument(); + }); + + it("hides the plan once approved and exposes a show plan toggle", () => { + renderView({ toolCall: makeToolCall({ status: "completed" }) }); + + expect( + screen.getByText(/plan approved — proceeding with implementation/i), + ).toBeInTheDocument(); + expect(screen.queryByText(PLAN_MARKER)).not.toBeInTheDocument(); + + const toggle = screen.getByRole("button", { name: /show plan/i }); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + }); + + it("expands and collapses the plan when the toggle is clicked", async () => { + const user = userEvent.setup(); + renderView({ toolCall: makeToolCall({ status: "completed" }) }); + + const toggle = screen.getByRole("button", { name: /show plan/i }); + await user.click(toggle); + + expect(toggle).toHaveAttribute("aria-expanded", "true"); + expect( + screen.getByRole("button", { name: /hide plan/i }), + ).toBeInTheDocument(); + expect(screen.getByText(PLAN_MARKER)).toBeInTheDocument(); + + await user.click(toggle); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + expect(screen.queryByText(PLAN_MARKER)).not.toBeInTheDocument(); + }); + + it("shows the rejected status with a working toggle when cancelled", async () => { + const user = userEvent.setup(); + renderView({ + toolCall: makeToolCall({ status: "pending" }), + turnCancelled: true, + }); + + expect(screen.getByText(/\(plan rejected\)/i)).toBeInTheDocument(); + const toggle = screen.getByRole("button", { name: /show plan/i }); + + await user.click(toggle); + expect(screen.getByText(PLAN_MARKER)).toBeInTheDocument(); + }); + + it("omits the toggle when there is no plan text available", () => { + renderView({ + toolCall: makeToolCall({ + status: "completed", + rawInput: undefined, + content: [], + }), + }); + + expect( + screen.getByText(/plan approved — proceeding with implementation/i), + ).toBeInTheDocument(); + expect( + screen.queryByRole("button", { name: /show plan/i }), + ).not.toBeInTheDocument(); + }); +}); diff --git a/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx b/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx index 5fe20929c..3846bed64 100644 --- a/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx +++ b/apps/code/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx @@ -35,52 +35,56 @@ export function PlanApprovalView({ }, [content, toolCall.rawInput]); const showResult = isComplete || wasCancelled; - const showPlanInline = !showResult; const canTogglePlan = showResult && !!planText; + const planContentId = `plan-content-${toolCall.toolCallId}`; if (!planText && !showResult) return null; + const statusContent = isComplete ? ( + <> + + + Plan approved — proceeding with implementation + + + ) : wasCancelled ? ( + (Plan rejected) + ) : null; + return ( - {showPlanInline && planText && ( + {!showResult && planText && ( )} {showResult && ( - setIsPlanExpanded((v) => !v) : undefined - } - > - {canTogglePlan && - (isPlanExpanded ? ( - + {canTogglePlan ? ( + + ) : ( + + {statusContent} + + )} {canTogglePlan && isPlanExpanded && ( - + )}