Skip to content

Commit 24968fb

Browse files
authored
Integrate plan mode into permissionselector and plan mode fixes (#690)
1 parent 41983a5 commit 24968fb

File tree

18 files changed

+228
-181
lines changed

18 files changed

+228
-181
lines changed

apps/twig/src/renderer/components/permissions/PermissionSelector.stories.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { toolInfoFromToolUse } from "@posthog/agent/adapters/claude/conversion/tool-use-to-acp";
2-
import { buildPermissionOptions } from "@posthog/agent/adapters/claude/permissions/permission-options";
2+
import {
3+
buildExitPlanModePermissionOptions,
4+
buildPermissionOptions,
5+
} from "@posthog/agent/adapters/claude/permissions/permission-options";
36
import {
47
buildQuestionOptions,
58
buildQuestionToolCallData,
@@ -474,6 +477,46 @@ export const Default: Story = {
474477
},
475478
};
476479

480+
const exitPlanModeInput = {
481+
plan: `# Add Dark Mode Support
482+
483+
## Overview
484+
Add dark mode toggle to Twig app with theme persistence.
485+
486+
## Implementation Steps
487+
- Create \`useThemeStore\` Zustand store with theme state (\`light\` | \`dark\` | \`system\`)
488+
- Add theme toggle button to settings panel that cycles through theme options
489+
- Update Radix UI Theme component to accept \`appearance\` prop from store
490+
- Add CSS variables for dark mode colors in global styles
491+
- Test theme switching persists across app restarts
492+
493+
## Critical Files
494+
- \`apps/twig/src/renderer/stores/theme-store.ts\` (new)
495+
- \`apps/twig/src/renderer/App.tsx\` (modify Theme provider)
496+
- \`apps/twig/src/renderer/features/settings/SettingsPanel.tsx\` (add toggle)
497+
498+
## Verification
499+
- Launch app, toggle dark mode, verify colors change
500+
- Restart app, verify theme persists
501+
- Test system theme option follows OS preference`,
502+
};
503+
export const ExitPlanMode: Story = {
504+
args: {
505+
toolCall: {
506+
toolCallId: `story-${Date.now()}`,
507+
title: "Approve this plan to proceed?",
508+
kind: "switch_mode",
509+
content: [
510+
{
511+
type: "content",
512+
content: { type: "text", text: exitPlanModeInput.plan },
513+
},
514+
],
515+
},
516+
options: buildExitPlanModePermissionOptions(),
517+
},
518+
};
519+
477520
const singleQuestion: QuestionItem[] = [
478521
{
479522
question: "Which testing framework do you prefer?",
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Box } from "@radix-ui/themes";
2+
import ReactMarkdown from "react-markdown";
3+
import remarkGfm from "remark-gfm";
4+
5+
interface PlanContentProps {
6+
plan: string;
7+
}
8+
9+
export function PlanContent({ plan }: PlanContentProps) {
10+
return (
11+
<Box className="rounded-lg border-2 border-blue-6 bg-blue-2 p-4">
12+
<Box className="plan-markdown text-blue-12">
13+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{plan}</ReactMarkdown>
14+
</Box>
15+
</Box>
16+
);
17+
}

apps/twig/src/renderer/components/permissions/SwitchModePermission.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { ActionSelector } from "@components/ActionSelector";
2+
import { useMemo } from "react";
3+
import { PlanContent } from "./PlanContent";
24
import { type BasePermissionProps, toSelectorOptions } from "./types";
35

46
export function SwitchModePermission({
@@ -7,10 +9,29 @@ export function SwitchModePermission({
79
onSelect,
810
onCancel,
911
}: BasePermissionProps) {
12+
const planText = useMemo(() => {
13+
const rawPlan = (toolCall.rawInput as { plan?: string } | undefined)?.plan;
14+
if (rawPlan) return rawPlan;
15+
16+
const content = toolCall.content;
17+
if (!content || content.length === 0) return null;
18+
const textContent = content.find((c) => c.type === "content");
19+
if (textContent && "content" in textContent) {
20+
const inner = textContent.content as
21+
| { type?: string; text?: string }
22+
| undefined;
23+
if (inner?.type === "text" && inner.text) {
24+
return inner.text;
25+
}
26+
}
27+
return null;
28+
}, [toolCall.rawInput, toolCall.content]);
29+
1030
return (
1131
<ActionSelector
12-
title={toolCall.title ?? "Switch mode"}
13-
question="Approve this mode change?"
32+
title="Implementation Plan"
33+
pendingAction={planText ? <PlanContent plan={planText} /> : undefined}
34+
question="Approve this plan to proceed?"
1435
options={toSelectorOptions(options)}
1536
onSelect={onSelect}
1637
onCancel={onCancel}

apps/twig/src/renderer/features/sessions/components/ConversationView.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,6 @@ export function ConversationView({
143143
turn={item}
144144
repoPath={repoPath}
145145
isCloud={isCloud}
146-
taskId={taskId}
147146
/>
148147
) : (
149148
<UserShellExecuteView key={item.id} item={item} />
@@ -184,7 +183,6 @@ interface TurnViewProps {
184183
turn: Turn;
185184
repoPath?: string | null;
186185
isCloud?: boolean;
187-
taskId?: string;
188186
}
189187

190188
function getInterruptMessage(reason?: string): string {
@@ -200,7 +198,6 @@ const TurnView = memo(function TurnView({
200198
turn,
201199
repoPath,
202200
isCloud = false,
203-
taskId,
204201
}: TurnViewProps) {
205202
const wasCancelled = turn.stopReason === "cancelled";
206203
const gitAction = parseGitActionMessage(turn.userContent);
@@ -241,7 +238,6 @@ const TurnView = memo(function TurnView({
241238
key={`${item.sessionUpdate}-${i}`}
242239
item={renderItem}
243240
toolCalls={turn.toolCalls}
244-
taskId={taskId}
245241
turnCancelled={wasCancelled}
246242
/>
247243
);

apps/twig/src/renderer/features/sessions/components/session-update/CurrentModeView.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.

apps/twig/src/renderer/features/sessions/components/session-update/PlanApprovalView.tsx

Lines changed: 9 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,27 @@
1-
import { DotsCircleSpinner } from "@components/DotsCircleSpinner";
2-
import { usePendingPermissionsForTask } from "@features/sessions/stores/sessionStore";
1+
import { PlanContent } from "@components/permissions/PlanContent";
32
import type { ToolCall } from "@features/sessions/types";
4-
import { CheckCircle, ClockCounterClockwise } from "@phosphor-icons/react";
3+
import { CheckCircle } from "@phosphor-icons/react";
54
import { Box, Flex, Text } from "@radix-ui/themes";
65
import { useMemo } from "react";
7-
import ReactMarkdown from "react-markdown";
8-
import remarkGfm from "remark-gfm";
96

107
interface PlanApprovalViewProps {
118
toolCall: ToolCall;
12-
taskId: string;
139
turnCancelled?: boolean;
1410
}
1511

1612
export function PlanApprovalView({
1713
toolCall,
18-
taskId,
1914
turnCancelled,
2015
}: PlanApprovalViewProps) {
21-
const { toolCallId, status, content } = toolCall;
22-
const pendingPermissions = usePendingPermissionsForTask(taskId);
23-
24-
const pendingPermission = pendingPermissions.get(toolCallId);
16+
const { status, content } = toolCall;
2517
const isComplete = status === "completed";
26-
const isPending = !!pendingPermission && !isComplete;
2718
const wasCancelled =
2819
(status === "pending" || status === "in_progress") && turnCancelled;
2920

30-
// Extract plan text from content or rawInput
3121
const planText = useMemo(() => {
32-
// Try rawInput first (where Claude SDK puts the plan)
3322
const rawPlan = (toolCall.rawInput as { plan?: string } | undefined)?.plan;
3423
if (rawPlan) return rawPlan;
3524

36-
// Fallback: check content array
3725
if (!content || content.length === 0) return null;
3826
const textContent = content.find((c) => c.type === "content");
3927
if (textContent && "content" in textContent) {
@@ -47,34 +35,14 @@ export function PlanApprovalView({
4735
return null;
4836
}, [content, toolCall.rawInput]);
4937

38+
if (!isComplete && !wasCancelled) return null;
39+
5040
return (
5141
<Box className="my-3">
52-
{/* Plan content in highlighted box */}
53-
{planText && (
54-
<Box className="mb-3 rounded-lg border-2 border-amber-6 bg-amber-2 p-4">
55-
<Flex align="center" gap="2" className="mb-2">
56-
<Text size="2" weight="bold" className="text-amber-11">
57-
Implementation Plan
58-
</Text>
59-
</Flex>
60-
<Box className="plan-markdown text-amber-12">
61-
<ReactMarkdown remarkPlugins={[remarkGfm]}>
62-
{planText}
63-
</ReactMarkdown>
64-
</Box>
65-
</Box>
66-
)}
42+
{planText && <PlanContent plan={planText} />}
6743

68-
{/* Status indicator */}
69-
<Flex align="center" gap="2" className="px-1">
70-
{isPending ? (
71-
<>
72-
<ClockCounterClockwise size={14} className="text-amber-9" />
73-
<Text size="1" className="text-amber-11">
74-
Waiting for approval — use the selector below to continue
75-
</Text>
76-
</>
77-
) : isComplete ? (
44+
<Flex align="center" gap="2" className="mt-2 px-1">
45+
{isComplete ? (
7846
<>
7947
<CheckCircle size={14} weight="fill" className="text-green-9" />
8048
<Text size="1" className="text-green-11">
@@ -85,14 +53,7 @@ export function PlanApprovalView({
8553
<Text size="1" className="text-gray-9">
8654
(Cancelled)
8755
</Text>
88-
) : (
89-
<>
90-
<DotsCircleSpinner size={14} className="text-gray-9" />
91-
<Text size="1" className="text-gray-9">
92-
Preparing plan...
93-
</Text>
94-
</>
95-
)}
56+
) : null}
9657
</Flex>
9758
</Box>
9859
);

apps/twig/src/renderer/features/sessions/components/session-update/PlanView.tsx

Lines changed: 0 additions & 45 deletions
This file was deleted.

apps/twig/src/renderer/features/sessions/components/session-update/SessionUpdateView.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { SessionUpdate, ToolCall } from "@features/sessions/types";
33
import { AgentMessage } from "./AgentMessage";
44
import { CompactBoundaryView } from "./CompactBoundaryView";
55
import { ConsoleMessage } from "./ConsoleMessage";
6-
import { CurrentModeView } from "./CurrentModeView";
76
import { ErrorNotificationView } from "./ErrorNotificationView";
87
import { StatusNotificationView } from "./StatusNotificationView";
98
import { TaskNotificationView } from "./TaskNotificationView";
@@ -44,14 +43,12 @@ export type RenderItem =
4443
interface SessionUpdateViewProps {
4544
item: RenderItem;
4645
toolCalls?: Map<string, ToolCall>;
47-
taskId?: string;
4846
turnCancelled?: boolean;
4947
}
5048

5149
export function SessionUpdateView({
5250
item,
5351
toolCalls,
54-
taskId,
5552
turnCancelled,
5653
}: SessionUpdateViewProps) {
5754
switch (item.sessionUpdate) {
@@ -69,7 +66,6 @@ export function SessionUpdateView({
6966
return (
7067
<ToolCallBlock
7168
toolCall={toolCalls?.get(item.toolCallId) ?? item}
72-
taskId={taskId}
7369
turnCancelled={turnCancelled}
7470
/>
7571
);
@@ -80,7 +76,7 @@ export function SessionUpdateView({
8076
case "available_commands_update":
8177
return null;
8278
case "current_mode_update":
83-
return <CurrentModeView update={item} />;
79+
return null;
8480
case "console":
8581
return (
8682
<ConsoleMessage

apps/twig/src/renderer/features/sessions/components/session-update/ToolCallBlock.tsx

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,25 @@ import { ToolCallView } from "./ToolCallView";
55

66
interface ToolCallBlockProps {
77
toolCall: ToolCall;
8-
taskId?: string;
98
turnCancelled?: boolean;
109
}
1110

12-
export function ToolCallBlock({
13-
toolCall,
14-
taskId,
15-
turnCancelled,
16-
}: ToolCallBlockProps) {
17-
if (toolCall.kind === "switch_mode" && taskId) {
11+
export function ToolCallBlock({ toolCall, turnCancelled }: ToolCallBlockProps) {
12+
const meta = toolCall._meta as
13+
| { claudeCode?: { toolName?: string } }
14+
| undefined;
15+
const toolName = meta?.claudeCode?.toolName;
16+
17+
if (toolCall.kind === "switch_mode") {
1818
return (
19-
<PlanApprovalView
20-
toolCall={toolCall}
21-
taskId={taskId}
22-
turnCancelled={turnCancelled}
23-
/>
19+
<PlanApprovalView toolCall={toolCall} turnCancelled={turnCancelled} />
2420
);
2521
}
2622

23+
if (toolName === "EnterPlanMode" || toolName === "ExitPlanMode") {
24+
return null;
25+
}
26+
2727
if (toolCall.kind === "execute") {
2828
return (
2929
<ExecuteToolView toolCall={toolCall} turnCancelled={turnCancelled} />

0 commit comments

Comments
 (0)