Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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> = {}): 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(
<Theme>
<PlanApprovalView {...props} />
</Theme>,
);
}

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();
});
});
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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;
Expand All @@ -33,30 +34,61 @@ export function PlanApprovalView({
return null;
}, [content, toolCall.rawInput]);

const showPlanContent = !isComplete && !wasCancelled;
const showResult = isComplete || wasCancelled;
const canTogglePlan = showResult && !!planText;
const planContentId = `plan-content-${toolCall.toolCallId}`;

if (!planText && !showResult) return null;

const statusContent = isComplete ? (
<>
<CheckCircle size={14} weight="fill" className="text-green-9" />
<Text className="text-[13px] text-green-11">
Plan approved — proceeding with implementation
</Text>
</>
) : wasCancelled ? (
<Text className="text-[13px] text-gray-10">(Plan rejected)</Text>
) : null;

return (
<Box className="my-3">
{showPlanContent && planText && (
{!showResult && planText && (
<PlanContent id={toolCall.toolCallId} plan={planText} />
)}

{showResult && (
<Flex align="center" gap="2" className="px-1">
{isComplete ? (
<>
<CheckCircle size={14} weight="fill" className="text-green-9" />
<Text className="text-[13px] text-green-11">
Plan approved — proceeding with implementation
<Box>
{canTogglePlan ? (
<button
type="button"
onClick={() => setIsPlanExpanded((v) => !v)}
aria-expanded={isPlanExpanded}
aria-controls={planContentId}
className="flex items-center gap-2 rounded-sm px-1 text-left hover:bg-gray-3"
>
{isPlanExpanded ? (
<CaretDown size={12} className="text-gray-10" />
) : (
<CaretRight size={12} className="text-gray-10" />
)}
{statusContent}
<Text className="text-[13px] text-gray-10">
· {isPlanExpanded ? "hide plan" : "show plan"}
</Text>
</>
) : wasCancelled ? (
<Text className="text-[13px] text-gray-10">(Plan rejected)</Text>
) : null}
</Flex>
</button>
) : (
<Flex align="center" gap="2" className="px-1">
{statusContent}
</Flex>
)}

{canTogglePlan && isPlanExpanded && (
<Box id={planContentId} className="mt-2">
<PlanContent id={toolCall.toolCallId} plan={planText} />
</Box>
)}
</Box>
)}
</Box>
);
Expand Down
Loading