Skip to content

Commit d31267c

Browse files
milispclaude
andcommitted
refactor: replace approval dialog with inline approval messages
- Move ApprovalDialog.tsx functionality into inline ApprovalMessage component - Integrate approval UI directly into message stream for better UX - Add proper approval request parsing and handling for exec/patch types - Extract unique ID generation into reusable utility - Improve command execution message display with cleaner output formatting - Update stores and types to support new approval message flow 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 335cc92 commit d31267c

File tree

12 files changed

+572
-266
lines changed

12 files changed

+572
-266
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import React, { useState } from 'react';
2+
import { Button } from '../ui/button';
3+
import { AlertTriangle, CheckCircle, XCircle } from 'lucide-react';
4+
import { ApprovalRequest } from '@/types/codex';
5+
6+
interface ApprovalMessageProps {
7+
approvalRequest: ApprovalRequest;
8+
onApproval: (approved: boolean) => void;
9+
}
10+
11+
export const ApprovalMessage: React.FC<ApprovalMessageProps> = ({
12+
approvalRequest,
13+
onApproval
14+
}) => {
15+
const [decision, setDecision] = useState<'approved' | 'denied' | null>(null);
16+
17+
const handleApproval = (approved: boolean) => {
18+
setDecision(approved ? 'approved' : 'denied');
19+
onApproval(approved);
20+
};
21+
22+
return (
23+
<div className="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded-lg border border-yellow-200 dark:border-yellow-800/50 my-2">
24+
<div className="flex items-start gap-3">
25+
<AlertTriangle className="w-5 h-5 text-yellow-600 dark:text-yellow-400 mt-0.5" />
26+
<div className="flex-1">
27+
<h3 className="font-medium text-yellow-800 dark:text-yellow-200 mb-2">
28+
{approvalRequest.type === 'exec'
29+
? 'Command Execution Request'
30+
: approvalRequest.type === 'apply_patch'
31+
? 'Apply Code Changes Request'
32+
: 'Code Patch Request'
33+
}
34+
</h3>
35+
36+
{approvalRequest.type === 'exec' ? (
37+
<div>
38+
<p className="text-sm text-yellow-700 dark:text-yellow-300 mb-1">Command:</p>
39+
<code className="block bg-yellow-100 dark:bg-yellow-800/30 p-2 rounded text-sm mb-2">
40+
{approvalRequest.command}
41+
</code>
42+
<p className="text-xs text-yellow-600 dark:text-yellow-400">
43+
Working directory: {approvalRequest.cwd}
44+
</p>
45+
</div>
46+
) : approvalRequest.type === 'apply_patch' ? (
47+
<div>
48+
<p className="text-sm text-yellow-700 dark:text-yellow-300 mb-1">Changes to be applied:</p>
49+
<div className="bg-yellow-100 dark:bg-yellow-800/30 p-2 rounded text-sm max-h-40 overflow-y-auto">
50+
{approvalRequest.changes ? (
51+
typeof approvalRequest.changes === 'string' ? (
52+
<pre className="text-xs whitespace-pre-wrap">
53+
{approvalRequest.changes}
54+
</pre>
55+
) : (
56+
<div className="space-y-2">
57+
{Object.entries(approvalRequest.changes).map(([file, change]: [string, any], idx) => (
58+
<div key={idx} className="border-l-2 border-yellow-300 pl-2">
59+
<div className="font-mono text-xs font-medium text-yellow-700 dark:text-yellow-300">
60+
{file}
61+
</div>
62+
<pre className="text-xs mt-1 whitespace-pre-wrap text-yellow-600 dark:text-yellow-400">
63+
{change.add ? `+ ${change.add.content}` :
64+
change.remove ? `- ${change.remove.content}` :
65+
change.modify ? `~ ${change.modify.content}` :
66+
JSON.stringify(change, null, 2)}
67+
</pre>
68+
</div>
69+
))}
70+
</div>
71+
)
72+
) : (
73+
<span className="text-yellow-600 dark:text-yellow-400">No change details available</span>
74+
)}
75+
</div>
76+
</div>
77+
) : (
78+
<div>
79+
<p className="text-sm text-yellow-700 dark:text-yellow-300 mb-1">Files to be modified:</p>
80+
<ul className="list-disc list-inside text-sm text-yellow-600 dark:text-yellow-400">
81+
{approvalRequest.files?.map((file, idx) => (
82+
<li key={idx}>{file}</li>
83+
))}
84+
</ul>
85+
</div>
86+
)}
87+
88+
<div className="flex gap-2 mt-3">
89+
{decision ? (
90+
<div className="flex items-center gap-2">
91+
{decision === 'approved' ? (
92+
<>
93+
<CheckCircle className="w-4 h-4 text-green-600 dark:text-green-400" />
94+
<span className="text-sm font-medium text-green-800 dark:text-green-200">
95+
Request Approved
96+
</span>
97+
</>
98+
) : (
99+
<>
100+
<XCircle className="w-4 h-4 text-red-600 dark:text-red-400" />
101+
<span className="text-sm font-medium text-red-800 dark:text-red-200">
102+
Request Denied
103+
</span>
104+
</>
105+
)}
106+
</div>
107+
) : (
108+
<>
109+
<Button
110+
size="sm"
111+
variant="destructive"
112+
onClick={() => handleApproval(false)}
113+
>
114+
Deny
115+
</Button>
116+
<Button
117+
size="sm"
118+
onClick={() => handleApproval(true)}
119+
>
120+
Allow
121+
</Button>
122+
</>
123+
)}
124+
</div>
125+
</div>
126+
</div>
127+
</div>
128+
);
129+
};

src/components/chat/ChatInterface.tsx

Lines changed: 54 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ import { useModelStore } from "@/stores/ModelStore";
99
import { sessionManager } from "@/services/sessionManager";
1010
import { ChatInput } from "./ChatInput";
1111
import { MessageList } from "./MessageList";
12-
import { ApprovalDialog } from "../dialogs/ApprovalDialog";
1312
import { useCodexEvents } from "../../hooks/useCodexEvents";
1413
import { ReasoningEffortSelector } from './ReasoningEffortSelector';
1514
import { Sandbox } from "./Sandbox";
15+
import { generateUniqueId } from "@/utils/genUniqueId";
1616

1717
interface ChatInterfaceProps {
1818
sessionId: string;
@@ -25,8 +25,6 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
2525
}) => {
2626
const { inputValue, setInputValue } = useChatInputStore();
2727
const { currentModel, currentProvider, reasoningEffort } = useModelStore();
28-
const [pendingApproval, setPendingApproval] =
29-
useState<ApprovalRequest | null>(null);
3028
const [isConnected, setIsConnected] = useState(false);
3129
const [tempSessionId, setTempSessionId] = useState<string | null>(null);
3230
const [activeSessionId, setActiveSessionId] = useState<string>(sessionId);
@@ -79,7 +77,6 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
7977

8078
useCodexEvents({
8179
sessionId: activeSessionId,
82-
onApprovalRequest: setPendingApproval,
8380
});
8481

8582
const messages = [...sessionMessages];
@@ -150,7 +147,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
150147
} catch (error) {
151148
console.error("Failed to auto-start session:", error);
152149
const errorMessage = {
153-
id: `${sessionId}-auto-start-error-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
150+
id: `${sessionId}-auto-start-error-${generateUniqueId()}`,
154151
role: "system" as const,
155152
content: `Failed to start Codex session: ${error}`,
156153
timestamp: Date.now(),
@@ -218,7 +215,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
218215
await sessionManager.closeAllSessions();
219216
}
220217

221-
actualSessionId = `codex-event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
218+
actualSessionId = `codex-event-${generateUniqueId()}`;
222219
setTempSessionId(actualSessionId);
223220
setPendingNewConversation(false);
224221
isPendingSession = true;
@@ -230,7 +227,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
230227
);
231228
if (!conversationExists) {
232229
// Always use timestamp format for consistency
233-
actualSessionId = `codex-event-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
230+
actualSessionId = `codex-event-${generateUniqueId()}`;
234231
createConversation("New Chat", "agent", actualSessionId);
235232
}
236233
}
@@ -268,7 +265,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
268265
console.error("Failed to start session:", error);
269266
setSessionStarting(false);
270267
const errorMessage = {
271-
id: `${actualSessionId}-startup-error-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
268+
id: `${actualSessionId}-startup-error-${generateUniqueId()}`,
272269
role: "system" as const,
273270
content: `Failed to start Codex session: ${error}`,
274271
timestamp: Date.now(),
@@ -280,7 +277,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
280277

281278
// Add user message to conversation store
282279
const userMessage = {
283-
id: `${actualSessionId}-user-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
280+
id: `${actualSessionId}-user-${generateUniqueId()}`,
284281
role: "user" as const,
285282
content: messageContent,
286283
timestamp: Date.now(),
@@ -303,7 +300,7 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
303300
} catch (error) {
304301
console.error("Failed to send message:", error);
305302
const errorMessage = {
306-
id: `${actualSessionId}-send-error-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
303+
id: `${actualSessionId}-send-error-${generateUniqueId()}`,
307304
role: "system" as const,
308305
content: `Failed to send message: ${error}`,
309306
timestamp: Date.now(),
@@ -313,33 +310,64 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
313310
}
314311
};
315312

316-
const handleApproval = async (approved: boolean) => {
317-
if (!pendingApproval) return;
318-
313+
const handleApproval = async (approved: boolean, approvalRequest: ApprovalRequest) => {
319314
try {
320315
// Extract raw session ID for backend communication
321316
const rawSessionId = sessionId.startsWith("codex-event-")
322317
? sessionId.replace("codex-event-", "")
323318
: sessionId;
324319

325-
if (pendingApproval.type === 'apply_patch') {
326-
// For apply_patch, use event.id (stored in pendingApproval.id)
327-
await invoke("approve_patch", {
328-
sessionId: rawSessionId,
329-
approvalId: pendingApproval.id,
330-
approved,
331-
});
332-
} else {
333-
// For exec, use call_id (stored in pendingApproval.id)
334-
await invoke("approve_execution", {
320+
// Handle different approval types with appropriate backend calls
321+
switch (approvalRequest.type) {
322+
case 'exec':
323+
await invoke("approve_execution", {
324+
sessionId: rawSessionId,
325+
approvalId: approvalRequest.id,
326+
approved,
327+
});
328+
break;
329+
330+
case 'patch':
331+
await invoke("approve_patch", {
332+
sessionId: rawSessionId,
333+
approvalId: approvalRequest.id,
334+
approved,
335+
});
336+
break;
337+
338+
case 'apply_patch':
339+
await invoke("approve_patch", {
340+
sessionId: rawSessionId,
341+
approvalId: approvalRequest.id,
342+
approved,
343+
});
344+
break;
345+
346+
default:
347+
console.error('Unknown approval request type:', approvalRequest.type);
348+
return;
349+
}
350+
351+
console.log(`✅ Approval ${approved ? 'granted' : 'denied'} for ${approvalRequest.type} request ${approvalRequest.id}`);
352+
353+
// If denied, pause the session to stop further execution
354+
if (!approved) {
355+
console.log('🛑 Pausing session due to denied approval');
356+
await invoke("pause_session", {
335357
sessionId: rawSessionId,
336-
approvalId: pendingApproval.id,
337-
approved,
338358
});
339359
}
340-
setPendingApproval(null);
341360
} catch (error) {
342361
console.error("Failed to send approval:", error);
362+
363+
// Add error message to conversation
364+
const errorMessage = {
365+
id: `${sessionId}-approval-error-${generateUniqueId()}`,
366+
role: "system" as const,
367+
content: `Failed to process approval: ${error}`,
368+
timestamp: Date.now(),
369+
};
370+
addMessage(sessionId, errorMessage);
343371
}
344372
};
345373

@@ -370,10 +398,6 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
370398
messages={messages}
371399
isLoading={isLoading}
372400
isPendingNewConversation={pendingNewConversation || !sessionId.trim()}
373-
/>
374-
375-
<ApprovalDialog
376-
pendingApproval={pendingApproval}
377401
onApproval={handleApproval}
378402
/>
379403

0 commit comments

Comments
 (0)