Skip to content

Commit 0f92403

Browse files
committed
Redesign chat interface sandbox and approval controls
Fix selector label fallback in Sandbox component
1 parent 283afa9 commit 0f92403

File tree

7 files changed

+335
-126
lines changed

7 files changed

+335
-126
lines changed

src/components/chat/ChatInterface.tsx

Lines changed: 76 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import React, { useState, useEffect } from "react";
22
import { invoke } from "@tauri-apps/api/core";
3-
import { ApprovalRequest } from "@/types/codex";
3+
import { ApprovalRequest, CodexConfig } from "@/types/codex";
44
import type { Conversation } from "@/types/chat";
55
import { useConversationStore } from "@/stores/ConversationStore";
66
import { useCodexStore } from "@/stores/CodexStore";
@@ -9,13 +9,13 @@ import { useModelStore } from "@/stores/ModelStore";
99
import { sessionManager } from "@/services/sessionManager";
1010
import { ChatInput } from "./ChatInput";
1111
import { MessageList } from "./MessageList";
12-
import { useCodexEvents } from "../../hooks/useCodexEvents";
13-
import { ReasoningEffortSelector } from './ReasoningEffortSelector';
12+
import { useCodexEvents } from "@/hooks/useCodexEvents";
13+
import { ReasoningEffortSelector } from "./ReasoningEffortSelector";
1414
import { Sandbox } from "./Sandbox";
1515
import { generateUniqueId } from "@/utils/genUniqueId";
16-
import { ForkOriginBanner } from './ForkOriginBanner';
17-
import { useEphemeralStore } from '@/stores/EphemeralStore';
18-
import { ChangesSummary } from './ChangesSummary';
16+
import { ForkOriginBanner } from "./ForkOriginBanner";
17+
import { useEphemeralStore } from "@/stores/EphemeralStore";
18+
import { ChangesSummary } from "./ChangesSummary";
1919
import { ModelSelector } from "./ModelSelector";
2020
import TokenCountInfo from "@/components/common/TokenCountInfo";
2121

@@ -46,19 +46,19 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
4646
setCurrentConversation,
4747
getCurrentConversation,
4848
} = useConversationStore();
49-
49+
5050
// Get conversations filtered by current project
5151
const conversations = getCurrentProjectConversations();
52-
53-
const setSandboxMode = (mode: typeof config.sandboxMode) => {
54-
updateConfig({ sandboxMode: mode });
52+
53+
const setSandboxConfig = (
54+
updates: Partial<Pick<CodexConfig, "sandboxMode" | "approvalPolicy">>,
55+
) => {
56+
updateConfig(updates);
5557
};
5658

5759
// Simplified: Use session_id to find conversation data
5860
// Priority: selectedConversation (from disk/history) > store currentConversation (unfiltered)
59-
const currentConversation =
60-
selectedConversation ||
61-
getCurrentConversation();
61+
const currentConversation = selectedConversation || getCurrentConversation();
6262

6363
// Convert conversation messages to chat messages format
6464
const sessionMessages = currentConversation
@@ -68,14 +68,17 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
6868
role: msg.role,
6969
content: msg.content,
7070
title: msg.title,
71-
timestamp: typeof msg.timestamp === "number" ? msg.timestamp : Date.now(),
71+
timestamp:
72+
typeof msg.timestamp === "number" ? msg.timestamp : Date.now(),
7273
model: msg.role === "assistant" ? currentModel : undefined,
7374
approvalRequest: msg.approvalRequest,
7475
// Preserve optional rendering metadata
75-
...(msg as any).messageType && { messageType: (msg as any).messageType },
76-
...(msg as any).eventType && { eventType: (msg as any).eventType },
76+
...((msg as any).messageType && {
77+
messageType: (msg as any).messageType,
78+
}),
79+
...((msg as any).eventType && { eventType: (msg as any).eventType }),
7780
// Preserve structured plan payload for plan_update messages
78-
...(msg as any).plan && { plan: (msg as any).plan },
81+
...((msg as any).plan && { plan: (msg as any).plan }),
7982
};
8083
})
8184
: [];
@@ -107,8 +110,12 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
107110

108111
const messages = [...sessionMessages];
109112
const isLoading = currentConversation?.isLoading || false;
110-
const fileDiffMap = useEphemeralStore((s) => s.sessionFileDiffs[activeSessionId]);
111-
const sessionUsage = useEphemeralStore((s) => s.sessionTokenUsage[activeSessionId]);
113+
const fileDiffMap = useEphemeralStore(
114+
(s) => s.sessionFileDiffs[activeSessionId],
115+
);
116+
const sessionUsage = useEphemeralStore(
117+
(s) => s.sessionTokenUsage[activeSessionId],
118+
);
112119

113120
// Update activeSessionId when sessionId prop changes
114121
useEffect(() => {
@@ -132,7 +139,10 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
132139
useEphemeralStore.getState().clearTurnDiffs(activeSessionId);
133140
}
134141
} catch (e) {
135-
console.error('Failed to clear diffs during new conversation setup:', e);
142+
console.error(
143+
"Failed to clear diffs during new conversation setup:",
144+
e,
145+
);
136146
}
137147
// Ensure we don't keep showing old diffs
138148
if (activeSessionId !== sessionId) {
@@ -271,7 +281,10 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
271281
useEphemeralStore.getState().clearTurnDiffs(actualSessionId);
272282
}
273283
} catch (e) {
274-
console.error('Failed to clear diffs before sending in new conversation:', e);
284+
console.error(
285+
"Failed to clear diffs before sending in new conversation:",
286+
e,
287+
);
275288
}
276289

277290
// Ensure session is running before sending message
@@ -318,30 +331,36 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
318331
}
319332

320333
// If this is a forked conversation and not yet applied, prepend context
321-
if (currentConversation?.forkMeta && !currentConversation.forkMeta.applied) {
334+
if (
335+
currentConversation?.forkMeta &&
336+
!currentConversation.forkMeta.applied
337+
) {
322338
try {
323339
const meta = currentConversation.forkMeta;
324340
const historyText = meta.history
325-
.filter((m) => m.role === 'user' || m.role === 'assistant')
341+
.filter((m) => m.role === "user" || m.role === "assistant")
326342
.map((m) => `${m.role}:\n${m.content}`)
327343
.join("\n\n");
328344
const forkHeader = `parentId: ${meta.parentMessageId}\nsourceSession: ${meta.fromConversationId}`;
329345
const combined = `${forkHeader}\n\nConversation history:\n${historyText}\n\nUser:\n${messageContent}`;
330346
messageContent = combined;
331347
} catch (e) {
332-
console.error('Failed to build fork context:', e);
348+
console.error("Failed to build fork context:", e);
333349
}
334350
}
335351

336352
// If resending from an edited message, truncate messages from that point
337353
try {
338-
const { editingTarget, clearEditingTarget } = useChatInputStore.getState();
354+
const { editingTarget, clearEditingTarget } =
355+
useChatInputStore.getState();
339356
if (editingTarget && editingTarget.conversationId === actualSessionId) {
340-
useConversationStore.getState().truncateMessagesFrom(actualSessionId, editingTarget.messageId);
357+
useConversationStore
358+
.getState()
359+
.truncateMessagesFrom(actualSessionId, editingTarget.messageId);
341360
clearEditingTarget();
342361
}
343362
} catch (e) {
344-
console.error('Failed to handle edit-resend truncation:', e);
363+
console.error("Failed to handle edit-resend truncation:", e);
345364
}
346365

347366
// Add user message to conversation store
@@ -369,7 +388,10 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
369388
console.log("📤 ChatInterface: Sending text message:", messageContent);
370389

371390
// Mark fork context as applied so future sends don't include it
372-
if (currentConversation?.forkMeta && !currentConversation.forkMeta.applied) {
391+
if (
392+
currentConversation?.forkMeta &&
393+
!currentConversation.forkMeta.applied
394+
) {
373395
useConversationStore.getState().setForkMetaApplied(actualSessionId);
374396
}
375397
} catch (error) {
@@ -385,7 +407,10 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
385407
}
386408
};
387409

388-
const handleApproval = async (approved: boolean, approvalRequest: ApprovalRequest) => {
410+
const handleApproval = async (
411+
approved: boolean,
412+
approvalRequest: ApprovalRequest,
413+
) => {
389414
try {
390415
// Extract raw session ID for backend communication
391416
const rawSessionId = sessionId.startsWith("codex-event-")
@@ -394,47 +419,42 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
394419

395420
// Handle different approval types with appropriate backend calls
396421
switch (approvalRequest.type) {
397-
case 'exec':
422+
case "exec":
398423
await invoke("approve_execution", {
399424
sessionId: rawSessionId,
400425
approvalId: approvalRequest.id,
401426
approved,
402427
});
403428
break;
404-
405-
case 'patch':
406-
await invoke("approve_patch", {
407-
sessionId: rawSessionId,
408-
approvalId: approvalRequest.id,
409-
approved,
410-
});
411-
break;
412-
413-
case 'apply_patch':
429+
430+
case "patch":
431+
case "apply_patch":
414432
await invoke("approve_patch", {
415433
sessionId: rawSessionId,
416434
approvalId: approvalRequest.id,
417435
approved,
418436
});
419437
break;
420-
438+
421439
default:
422-
console.error('Unknown approval request type:', approvalRequest.type);
440+
console.error("Unknown approval request type:", approvalRequest.type);
423441
return;
424442
}
425-
426-
console.log(`✅ Approval ${approved ? 'granted' : 'denied'} for ${approvalRequest.type} request ${approvalRequest.id}`);
427-
443+
444+
console.log(
445+
`✅ Approval ${approved ? "granted" : "denied"} for ${approvalRequest.type} request ${approvalRequest.id}`,
446+
);
447+
428448
// If denied, pause the session to stop further execution
429449
if (!approved) {
430-
console.log('🛑 Pausing session due to denied approval');
450+
console.log("🛑 Pausing session due to denied approval");
431451
await invoke("pause_session", {
432452
sessionId: rawSessionId,
433453
});
434454
}
435455
} catch (error) {
436456
console.error("Failed to send approval:", error);
437-
457+
438458
// Add error message to conversation
439459
const errorMessage = {
440460
id: `${sessionId}-approval-error-${generateUniqueId()}`,
@@ -472,13 +492,18 @@ export const ChatInterface: React.FC<ChatInterfaceProps> = ({
472492
onStopStreaming={handleStopStreaming}
473493
disabled={!!selectedConversation && !selectedConversation.filePath}
474494
isLoading={isLoading}
475-
placeholderOverride={editingTarget ? 'Editing message and resending from here' : undefined}
495+
placeholderOverride={
496+
editingTarget
497+
? "Editing message and resending from here"
498+
: undefined
499+
}
476500
/>
477-
501+
478502
<div className="flex px-2 pt-0.5">
479-
<Sandbox
503+
<Sandbox
480504
sandboxMode={config.sandboxMode}
481-
onModeChange={setSandboxMode}
505+
approvalPolicy={config.approvalPolicy}
506+
onConfigChange={setSandboxConfig}
482507
/>
483508
<ModelSelector />
484509
<ReasoningEffortSelector />

0 commit comments

Comments
 (0)