diff --git a/src/components/chat/ChatView.tsx b/src/components/chat/ChatView.tsx index e12c78b..8357255 100644 --- a/src/components/chat/ChatView.tsx +++ b/src/components/chat/ChatView.tsx @@ -210,9 +210,9 @@ export function ChatView({ sessionId, initialMessages = [], initialHasMore = fal // Permission response — delegates to manager const handlePermissionResponse = useCallback( - async (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record) => { + async (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record, denyMessage?: string) => { setPendingApprovalSessionId(''); - await respondToPermission(sessionId, decision, updatedInput); + await respondToPermission(sessionId, decision, updatedInput, denyMessage); }, [sessionId, setPendingApprovalSessionId] ); diff --git a/src/components/chat/MessageList.tsx b/src/components/chat/MessageList.tsx index 202496e..50d62b5 100644 --- a/src/components/chat/MessageList.tsx +++ b/src/components/chat/MessageList.tsx @@ -62,7 +62,7 @@ interface MessageListProps { streamingToolOutput?: string; statusText?: string; pendingPermission?: PermissionRequestEvent | null; - onPermissionResponse?: (decision: 'allow' | 'allow_session' | 'deny') => void; + onPermissionResponse?: (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record, denyMessage?: string) => void; permissionResolved?: 'allow' | 'deny' | null; onForceStop?: () => void; hasMore?: boolean; diff --git a/src/components/chat/StreamingMessage.tsx b/src/components/chat/StreamingMessage.tsx index 8f2965e..83c574f 100644 --- a/src/components/chat/StreamingMessage.tsx +++ b/src/components/chat/StreamingMessage.tsx @@ -18,6 +18,13 @@ import { ConfirmationAction, } from '@/components/ai-elements/confirmation'; import { Shimmer } from '@/components/ai-elements/shimmer'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogFooter, +} from '@/components/ui/dialog'; import { ImageGenConfirmation } from './ImageGenConfirmation'; import { BatchPlanInlinePreview } from './batch-image-gen/BatchPlanInlinePreview'; import { PENDING_KEY, buildReferenceImages } from '@/lib/image-ref-store'; @@ -114,7 +121,7 @@ interface StreamingMessageProps { streamingToolOutput?: string; statusText?: string; pendingPermission?: PermissionRequestEvent | null; - onPermissionResponse?: (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record) => void; + onPermissionResponse?: (decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record, denyMessage?: string) => void; permissionResolved?: 'allow' | 'deny' | null; onForceStop?: () => void; } @@ -269,15 +276,39 @@ function AskUserQuestionUI({ ); } +function extractPlanFilePath(toolUses: ToolUseInfo[]): string | null { + // Walk backwards to find the most recent Write or Edit targeting a plans/*.md file + for (let i = toolUses.length - 1; i >= 0; i--) { + const tool = toolUses[i]; + const input = tool.input as Record; + if ((tool.name === 'Write' || tool.name === 'Edit') && typeof input.file_path === 'string') { + const fp = input.file_path; + if (fp.endsWith('.md') && (fp.includes('plans/') || fp.includes('plans\\'))) { + return fp; + } + } + } + return null; +} + function ExitPlanModeUI({ toolInput, + toolUses, onApprove, onDeny, + onDenyWithMessage, }: { toolInput: Record; + toolUses: ToolUseInfo[]; onApprove: () => void; onDeny: () => void; + onDenyWithMessage: (message: string) => void; }) { + const [planOpen, setPlanOpen] = useState(false); + const [planContent, setPlanContent] = useState(null); + const [planLoading, setPlanLoading] = useState(false); + const [feedback, setFeedback] = useState(''); + const planFilePath = extractPlanFilePath(toolUses); const allowedPrompts = (toolInput.allowedPrompts || []) as Array<{ tool: string; prompt: string; @@ -309,6 +340,30 @@ function ExitPlanModeUI({ > Reject + {planFilePath && ( + + )} + + + {planOpen && planContent && ( + + + + Plan + +
+ {planContent} +
+ +
+
+ )} ); } @@ -443,8 +535,10 @@ export function StreamingMessage({ {pendingPermission?.toolName === 'ExitPlanMode' && !permissionResolved && ( } + toolUses={toolUses} onApprove={() => onPermissionResponse?.('allow')} onDeny={() => onPermissionResponse?.('deny')} + onDenyWithMessage={(msg) => onPermissionResponse?.('deny', undefined, msg)} /> )} {pendingPermission?.toolName === 'ExitPlanMode' && permissionResolved === 'allow' && ( diff --git a/src/lib/stream-session-manager.ts b/src/lib/stream-session-manager.ts index 36bbd5a..e0548aa 100644 --- a/src/lib/stream-session-manager.ts +++ b/src/lib/stream-session-manager.ts @@ -568,6 +568,7 @@ export async function respondToPermission( sessionId: string, decision: 'allow' | 'allow_session' | 'deny', updatedInput?: Record, + denyMessage?: string, ): Promise { const stream = getStreamsMap().get(sessionId); if (!stream || !stream.snapshot.pendingPermission) return; @@ -577,7 +578,7 @@ export async function respondToPermission( const body = { permissionRequestId: perm.permissionRequestId, decision: decision === 'deny' - ? { behavior: 'deny' as const, message: 'User denied permission' } + ? { behavior: 'deny' as const, message: denyMessage || 'User denied permission' } : { behavior: 'allow' as const, ...(decision === 'allow_session' && perm.suggestions