centaurus-cli 2.8.6 → 2.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-adapter.d.ts +85 -0
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +769 -28
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/slash-commands.d.ts +2 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +31 -1
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/context/handlers/docker-handler.js.map +1 -1
- package/dist/context/handlers/ssh-handler.d.ts +16 -1
- package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
- package/dist/context/handlers/ssh-handler.js +57 -12
- package/dist/context/handlers/ssh-handler.js.map +1 -1
- package/dist/context/subshell-handler.d.ts +14 -0
- package/dist/context/subshell-handler.d.ts.map +1 -1
- package/dist/hooks/useTerminalDimensions.d.ts +41 -0
- package/dist/hooks/useTerminalDimensions.d.ts.map +1 -0
- package/dist/hooks/useTerminalDimensions.js +84 -0
- package/dist/hooks/useTerminalDimensions.js.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -1
- package/dist/services/api-client.d.ts +24 -0
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +27 -0
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/auth-handler.js +1 -1
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/clipboard-service.d.ts +42 -0
- package/dist/services/clipboard-service.d.ts.map +1 -0
- package/dist/services/clipboard-service.js +217 -0
- package/dist/services/clipboard-service.js.map +1 -0
- package/dist/services/local-chat-storage.d.ts +154 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -0
- package/dist/services/local-chat-storage.js +258 -0
- package/dist/services/local-chat-storage.js.map +1 -0
- package/dist/tools/grep-search.d.ts +5 -0
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +68 -16
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/plan-mode.d.ts +57 -6
- package/dist/tools/plan-mode.d.ts.map +1 -1
- package/dist/tools/plan-mode.js +297 -46
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/read-binary-file.d.ts +10 -0
- package/dist/tools/read-binary-file.d.ts.map +1 -0
- package/dist/tools/read-binary-file.js +210 -0
- package/dist/tools/read-binary-file.js.map +1 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/ui/components/App.d.ts +35 -0
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +608 -16
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/ClipboardImageAutocomplete.d.ts +14 -0
- package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +1 -0
- package/dist/ui/components/ClipboardImageAutocomplete.js +39 -0
- package/dist/ui/components/ClipboardImageAutocomplete.js.map +1 -0
- package/dist/ui/components/ConnectionStatusMessage.d.ts +1 -1
- package/dist/ui/components/ConnectionStatusMessage.d.ts.map +1 -1
- package/dist/ui/components/ConnectionStatusMessage.js +21 -0
- package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts +17 -0
- package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +1 -0
- package/dist/ui/components/DetailedPlanReviewScreen.js +110 -0
- package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -0
- package/dist/ui/components/InputBox.d.ts +2 -1
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +399 -28
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +20 -6
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MessageDisplay.d.ts +6 -0
- package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/MessageDisplay.js +66 -3
- package/dist/ui/components/MessageDisplay.js.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.d.ts +8 -0
- package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -1
- package/dist/ui/components/PlanAcceptedMessage.js +26 -8
- package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.d.ts +3 -0
- package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
- package/dist/ui/components/StreamingMessageDisplay.js +10 -6
- package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -1
- package/dist/ui/components/TaskCompletedMessage.js +4 -4
- package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
- package/dist/ui/components/TaskProgressIndicator.d.ts +18 -0
- package/dist/ui/components/TaskProgressIndicator.d.ts.map +1 -0
- package/dist/ui/components/TaskProgressIndicator.js +72 -0
- package/dist/ui/components/TaskProgressIndicator.js.map +1 -0
- package/dist/ui/components/ThinkingDisplay.d.ts +3 -0
- package/dist/ui/components/ThinkingDisplay.d.ts.map +1 -1
- package/dist/ui/components/ThinkingDisplay.js +6 -4
- package/dist/ui/components/ThinkingDisplay.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +85 -15
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/utils/custom-commands-manager.d.ts +59 -0
- package/dist/utils/custom-commands-manager.d.ts.map +1 -0
- package/dist/utils/custom-commands-manager.js +142 -0
- package/dist/utils/custom-commands-manager.js.map +1 -0
- package/dist/utils/input-classifier.d.ts +10 -11
- package/dist/utils/input-classifier.d.ts.map +1 -1
- package/dist/utils/input-classifier.js +299 -75
- package/dist/utils/input-classifier.js.map +1 -1
- package/dist/utils/terminal-output.d.ts.map +1 -1
- package/dist/utils/terminal-output.js +110 -14
- package/dist/utils/terminal-output.js.map +1 -1
- package/dist/utils/unicode-sanitizer.d.ts +44 -0
- package/dist/utils/unicode-sanitizer.d.ts.map +1 -0
- package/dist/utils/unicode-sanitizer.js +211 -0
- package/dist/utils/unicode-sanitizer.js.map +1 -0
- package/models-config.json +2 -3
- package/package.json +4 -1
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import React, { useState, useCallback } from 'react';
|
|
2
2
|
import { Box, Text, useApp, useInput, Static } from 'ink';
|
|
3
|
+
import SelectInput from 'ink-select-input';
|
|
4
|
+
import TextInput from 'ink-text-input';
|
|
3
5
|
import { spawn } from 'child_process';
|
|
4
6
|
import * as fs from 'fs';
|
|
5
7
|
import { WelcomeBanner } from './WelcomeBanner.js';
|
|
6
8
|
import { InputBox } from './InputBox.js';
|
|
7
|
-
import { MessageDisplay } from './MessageDisplay.js';
|
|
9
|
+
import { MessageDisplay, countImagesInMessage } from './MessageDisplay.js';
|
|
8
10
|
import { StreamingMessageDisplay } from './StreamingMessageDisplay.js';
|
|
9
11
|
import { LoadingIndicator } from './LoadingIndicator.js';
|
|
10
12
|
import { AgentTimer } from './AgentTimer.js';
|
|
@@ -18,10 +20,14 @@ import { VersionUpdatePrompt } from './VersionUpdatePrompt.js';
|
|
|
18
20
|
import { InteractiveShell } from './InteractiveShell.js';
|
|
19
21
|
import { checkForUpdates } from '../../utils/version-checker.js';
|
|
20
22
|
import { runInteractiveEditor, runWSLEditor, runDockerEditor, runSSHEditor } from '../../utils/editor-utils.js';
|
|
21
|
-
import {
|
|
23
|
+
import { DetailedPlanReviewScreen } from './DetailedPlanReviewScreen.js';
|
|
22
24
|
import { TaskCompletedMessage } from './TaskCompletedMessage.js';
|
|
23
25
|
import { PlanAcceptedMessage } from './PlanAcceptedMessage.js';
|
|
24
26
|
import { processTerminalOutput } from '../../utils/terminal-output.js';
|
|
27
|
+
import { apiClient } from '../../services/api-client.js';
|
|
28
|
+
import { conversationManager } from '../../services/conversation-manager.js';
|
|
29
|
+
import { logDebug, logError } from '../../utils/logger.js';
|
|
30
|
+
import { getTerminalDimensions } from '../../hooks/useTerminalDimensions.js';
|
|
25
31
|
// Banner item with stable timestamp - created once outside component
|
|
26
32
|
const BANNER_ITEM = { id: '__banner__', role: '__banner__', content: '', timestamp: new Date(0) };
|
|
27
33
|
const MessageList = React.memo(({ history, current, showBanner }) => {
|
|
@@ -29,23 +35,54 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
|
|
|
29
35
|
const staticItems = React.useMemo(() => {
|
|
30
36
|
return showBanner ? [BANNER_ITEM, ...history] : history;
|
|
31
37
|
}, [history, showBanner]);
|
|
38
|
+
// Calculate cumulative image counts for each message (for global numbering)
|
|
39
|
+
const imageCountsUpTo = React.useMemo(() => {
|
|
40
|
+
const counts = [];
|
|
41
|
+
let cumulative = 0;
|
|
42
|
+
for (const msg of history) {
|
|
43
|
+
counts.push(cumulative);
|
|
44
|
+
if (msg.role === 'user') {
|
|
45
|
+
cumulative += countImagesInMessage(msg.content);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return counts;
|
|
49
|
+
}, [history]);
|
|
50
|
+
// Get image count for current message (all images in history)
|
|
51
|
+
const totalHistoryImages = imageCountsUpTo.length > 0
|
|
52
|
+
? imageCountsUpTo[imageCountsUpTo.length - 1] + (history[history.length - 1]?.role === 'user' ? countImagesInMessage(history[history.length - 1].content) : 0)
|
|
53
|
+
: 0;
|
|
32
54
|
return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
|
|
33
|
-
React.createElement(Static, { items: staticItems }, (item) => {
|
|
55
|
+
React.createElement(Static, { items: staticItems }, (item, index) => {
|
|
34
56
|
if (item.id === '__banner__') {
|
|
35
57
|
return React.createElement(WelcomeBanner, { key: "__banner__" });
|
|
36
58
|
}
|
|
37
59
|
// Special rendering for task completion messages
|
|
38
60
|
const msg = item;
|
|
61
|
+
// Calculate history index (account for banner if present)
|
|
62
|
+
const historyIndex = showBanner ? index - 1 : index;
|
|
63
|
+
const imageCountBefore = imageCountsUpTo[historyIndex] || 0;
|
|
39
64
|
if (msg.taskCompletion) {
|
|
40
65
|
return (React.createElement(TaskCompletedMessage, { key: item.id, taskNumber: msg.taskCompletion.taskNumber, totalTasks: msg.taskCompletion.totalTasks, taskDescription: msg.taskCompletion.taskDescription, completionNote: msg.taskCompletion.completionNote }));
|
|
41
66
|
}
|
|
42
67
|
// Special rendering for plan accepted messages
|
|
43
68
|
if (msg.planAccepted) {
|
|
44
|
-
return (React.createElement(PlanAcceptedMessage, { key: item.id, planTitle: msg.planAccepted.planTitle, totalTasks: msg.planAccepted.totalTasks }));
|
|
69
|
+
return (React.createElement(PlanAcceptedMessage, { key: item.id, planTitle: msg.planAccepted.planTitle, totalTasks: msg.planAccepted.totalTasks, tasks: msg.planAccepted.tasks }));
|
|
45
70
|
}
|
|
46
|
-
return React.createElement(MessageDisplay, { key: item.id, message: item });
|
|
71
|
+
return React.createElement(MessageDisplay, { key: item.id, message: item, imageCountBefore: imageCountBefore });
|
|
47
72
|
}),
|
|
48
|
-
current && !history.some(msg => msg.id === current.id) && (
|
|
73
|
+
current && !history.some(msg => msg.id === current.id) && (() => {
|
|
74
|
+
// Get terminal dimensions to determine if streaming should be enabled
|
|
75
|
+
const dimensions = getTerminalDimensions();
|
|
76
|
+
const canStream = current.role === 'assistant' &&
|
|
77
|
+
current.shouldStream !== false &&
|
|
78
|
+
dimensions.shouldEnableStreaming;
|
|
79
|
+
if (canStream) {
|
|
80
|
+
return (React.createElement(StreamingMessageDisplay, { key: current.id, message: current, maxLines: dimensions.maxStreamingLines }));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
return React.createElement(MessageDisplay, { key: current.id, message: current, imageCountBefore: totalHistoryImages });
|
|
84
|
+
}
|
|
85
|
+
})()));
|
|
49
86
|
}, (prevProps, nextProps) => {
|
|
50
87
|
// Custom comparison to prevent unnecessary re-renders
|
|
51
88
|
// Only re-render if history length changed, current message changed, or showBanner changed
|
|
@@ -75,7 +112,31 @@ const ApprovalSection = React.memo(({ approvalRequest, onApprove }) => {
|
|
|
75
112
|
// Only re-render if the approval request message changes
|
|
76
113
|
return prevProps.approvalRequest.message === nextProps.approvalRequest.message;
|
|
77
114
|
});
|
|
78
|
-
|
|
115
|
+
// Simple rename input screen component
|
|
116
|
+
const RenameInputScreen = ({ currentTitle, onRename, onCancel }) => {
|
|
117
|
+
const [value, setValue] = React.useState(currentTitle);
|
|
118
|
+
useInput((input, key) => {
|
|
119
|
+
if (key.escape) {
|
|
120
|
+
onCancel();
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
return (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
124
|
+
React.createElement(Text, { color: "#00ccff", bold: true }, "\u270F\uFE0F Rename Chat"),
|
|
125
|
+
React.createElement(Box, { marginTop: 1 },
|
|
126
|
+
React.createElement(Text, null,
|
|
127
|
+
"Current name: ",
|
|
128
|
+
React.createElement(Text, { color: "#00cc66" }, currentTitle))),
|
|
129
|
+
React.createElement(Box, { marginTop: 1 },
|
|
130
|
+
React.createElement(Text, null, "New name: "),
|
|
131
|
+
React.createElement(TextInput, { value: value, onChange: setValue, onSubmit: (newTitle) => {
|
|
132
|
+
if (newTitle.trim()) {
|
|
133
|
+
onRename(newTitle.trim());
|
|
134
|
+
}
|
|
135
|
+
} })),
|
|
136
|
+
React.createElement(Box, { marginTop: 1 },
|
|
137
|
+
React.createElement(Text, { dimColor: true }, "Press Enter to save, ESC to cancel"))));
|
|
138
|
+
};
|
|
139
|
+
export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onPlanCreated, onTaskCompleted, onCommandModeChange, onToggleCommandMode, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode, onConnectionStatusUpdate, onChatPickerSetup, onChatPickerSelection, onChatDeletePickerSetup, onChatDeletePickerSelection, onChatListSetup, onChatRenamePickerSetup, onChatRename, onRestoreMessagesSetup, onUIMessageHistoryUpdate }) => {
|
|
79
140
|
const { exit } = useApp();
|
|
80
141
|
const autoAcceptRef = React.useRef(false);
|
|
81
142
|
// Helper to clear screen
|
|
@@ -139,6 +200,67 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
139
200
|
passwordRequest: undefined,
|
|
140
201
|
connectionStatus: undefined,
|
|
141
202
|
});
|
|
203
|
+
// Track last terminal width to detect actual width changes
|
|
204
|
+
const lastTerminalWidthRef = React.useRef(process.stdout.columns || 80);
|
|
205
|
+
// Track if we're currently in the middle of a clean-ui cycle to prevent re-entrancy
|
|
206
|
+
const isCleaningRef = React.useRef(false);
|
|
207
|
+
// Ref to store history during clean cycle (avoids closure issues)
|
|
208
|
+
const savedHistoryRef = React.useRef([]);
|
|
209
|
+
const savedCurrentRef = React.useRef(null);
|
|
210
|
+
// Debounce timer ref
|
|
211
|
+
const resizeDebounceRef = React.useRef(null);
|
|
212
|
+
// Handle terminal resize events using the same logic as /clean-ui command
|
|
213
|
+
// Uses debounce to wait until width is stable before triggering clean-ui
|
|
214
|
+
React.useEffect(() => {
|
|
215
|
+
const DEBOUNCE_MS = 300; // Wait 300ms after last resize before acting
|
|
216
|
+
const handleResize = () => {
|
|
217
|
+
const newWidth = process.stdout.columns || 80;
|
|
218
|
+
// Clear any existing debounce timer
|
|
219
|
+
if (resizeDebounceRef.current) {
|
|
220
|
+
clearTimeout(resizeDebounceRef.current);
|
|
221
|
+
}
|
|
222
|
+
// Set new debounce timer - only act when resizing stops
|
|
223
|
+
resizeDebounceRef.current = setTimeout(() => {
|
|
224
|
+
const oldWidth = lastTerminalWidthRef.current;
|
|
225
|
+
// Only act if width actually changed and we're not already cleaning
|
|
226
|
+
if (newWidth !== oldWidth && !isCleaningRef.current) {
|
|
227
|
+
lastTerminalWidthRef.current = newWidth;
|
|
228
|
+
isCleaningRef.current = true;
|
|
229
|
+
// Step 1: Clear screen and save current state to refs
|
|
230
|
+
clearScreen();
|
|
231
|
+
// Save state before clearing
|
|
232
|
+
setState(prev => {
|
|
233
|
+
savedHistoryRef.current = [...prev.messageHistory];
|
|
234
|
+
savedCurrentRef.current = prev.currentMessage;
|
|
235
|
+
// Return cleared state to force unmount of all message components
|
|
236
|
+
return {
|
|
237
|
+
...prev,
|
|
238
|
+
messageHistory: [],
|
|
239
|
+
currentMessage: null
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
// Step 2: After delay, clear again and restore from refs
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
clearScreen();
|
|
245
|
+
setState(prev => ({
|
|
246
|
+
...prev,
|
|
247
|
+
messageHistory: savedHistoryRef.current,
|
|
248
|
+
currentMessage: savedCurrentRef.current
|
|
249
|
+
}));
|
|
250
|
+
isCleaningRef.current = false;
|
|
251
|
+
}, 100);
|
|
252
|
+
}
|
|
253
|
+
}, DEBOUNCE_MS);
|
|
254
|
+
};
|
|
255
|
+
process.stdout.on('resize', handleResize);
|
|
256
|
+
return () => {
|
|
257
|
+
process.stdout.off('resize', handleResize);
|
|
258
|
+
// Clear debounce timer on cleanup
|
|
259
|
+
if (resizeDebounceRef.current) {
|
|
260
|
+
clearTimeout(resizeDebounceRef.current);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}, [clearScreen]);
|
|
142
264
|
// Check for version updates on mount
|
|
143
265
|
React.useEffect(() => {
|
|
144
266
|
checkForUpdates().then(versionInfo => {
|
|
@@ -176,10 +298,52 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
176
298
|
}, [state.messageHistory, state.currentMessage, calculateTotalTokens]);
|
|
177
299
|
// Track if we're currently streaming
|
|
178
300
|
const isStreamingRef = React.useRef(false);
|
|
301
|
+
// Buffer content when terminal is too small for streaming display
|
|
302
|
+
// Content will only be shown once complete to prevent flickering
|
|
303
|
+
const bufferedContentRef = React.useRef('');
|
|
304
|
+
const bufferedMessageIdRef = React.useRef(null);
|
|
179
305
|
// Set up callback to receive streaming chunks - only once on mount
|
|
180
306
|
React.useEffect(() => {
|
|
181
307
|
onResponseStream((chunk) => {
|
|
182
308
|
isStreamingRef.current = true; // Mark that we're streaming
|
|
309
|
+
// Check if terminal is large enough for streaming
|
|
310
|
+
const dimensions = getTerminalDimensions();
|
|
311
|
+
const enableStreaming = dimensions.shouldEnableStreaming;
|
|
312
|
+
// If streaming is disabled, buffer content silently
|
|
313
|
+
// Content will only appear once complete (in onResponseReceived)
|
|
314
|
+
if (!enableStreaming) {
|
|
315
|
+
// Accumulate content in buffer
|
|
316
|
+
bufferedContentRef.current += chunk;
|
|
317
|
+
// Create or update a minimal placeholder message (shows loading state)
|
|
318
|
+
setState(prev => {
|
|
319
|
+
// If we already have a message with buffered content, just maintain working state
|
|
320
|
+
if (prev.currentMessage && prev.currentMessage.role === 'assistant' && prev.currentMessage.shouldStream === false) {
|
|
321
|
+
return {
|
|
322
|
+
...prev,
|
|
323
|
+
isAiWorking: true
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
// Create minimal placeholder message (content hidden while streaming)
|
|
327
|
+
const now = new Date();
|
|
328
|
+
const messageId = `assistant-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
329
|
+
bufferedMessageIdRef.current = messageId;
|
|
330
|
+
const newMessage = {
|
|
331
|
+
id: messageId,
|
|
332
|
+
role: 'assistant',
|
|
333
|
+
content: '', // Empty - content will be set when complete
|
|
334
|
+
timestamp: now,
|
|
335
|
+
shouldStream: false // Never show streaming display
|
|
336
|
+
};
|
|
337
|
+
return {
|
|
338
|
+
...prev,
|
|
339
|
+
currentMessage: newMessage,
|
|
340
|
+
isLoading: false,
|
|
341
|
+
isAiWorking: true
|
|
342
|
+
};
|
|
343
|
+
});
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Normal streaming mode - update content in real-time
|
|
183
347
|
setState(prev => {
|
|
184
348
|
// If we have a current assistant message (possibly created by thoughts), append to it
|
|
185
349
|
if (prev.currentMessage && prev.currentMessage.role === 'assistant') {
|
|
@@ -187,7 +351,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
187
351
|
...prev,
|
|
188
352
|
currentMessage: {
|
|
189
353
|
...prev.currentMessage,
|
|
190
|
-
content: prev.currentMessage.content + chunk
|
|
354
|
+
content: prev.currentMessage.content + chunk,
|
|
355
|
+
// Update shouldStream based on current terminal size
|
|
356
|
+
shouldStream: enableStreaming
|
|
191
357
|
},
|
|
192
358
|
isAiWorking: true // Keep AI working state active
|
|
193
359
|
};
|
|
@@ -199,7 +365,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
199
365
|
role: 'assistant',
|
|
200
366
|
content: chunk,
|
|
201
367
|
timestamp: now,
|
|
202
|
-
|
|
368
|
+
// Set shouldStream based on terminal size
|
|
369
|
+
// If terminal is too small, disable streaming to prevent flickering
|
|
370
|
+
shouldStream: enableStreaming
|
|
203
371
|
};
|
|
204
372
|
return {
|
|
205
373
|
...prev,
|
|
@@ -374,6 +542,12 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
374
542
|
// If we were streaming, move the current message to history immediately
|
|
375
543
|
if (isStreamingRef.current) {
|
|
376
544
|
isStreamingRef.current = false; // Reset for next message
|
|
545
|
+
// Check if we have buffered content (from disabled streaming mode)
|
|
546
|
+
const hasBufferedContent = bufferedContentRef.current.length > 0;
|
|
547
|
+
const bufferedContent = bufferedContentRef.current;
|
|
548
|
+
// Clear buffer for next use
|
|
549
|
+
bufferedContentRef.current = '';
|
|
550
|
+
bufferedMessageIdRef.current = null;
|
|
377
551
|
setState(prev => {
|
|
378
552
|
// Move the completed streaming message to history
|
|
379
553
|
if (prev.currentMessage && prev.currentMessage.role === 'assistant') {
|
|
@@ -389,8 +563,10 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
389
563
|
};
|
|
390
564
|
}
|
|
391
565
|
// Create complete message with full content
|
|
566
|
+
// If we had buffered content, use that instead of the (empty) currentMessage content
|
|
392
567
|
const completeMessage = {
|
|
393
568
|
...prev.currentMessage,
|
|
569
|
+
content: hasBufferedContent ? bufferedContent : prev.currentMessage.content,
|
|
394
570
|
shouldStream: false
|
|
395
571
|
};
|
|
396
572
|
// Add the complete message to history
|
|
@@ -402,6 +578,38 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
402
578
|
isAiWorking: false // AI finished working
|
|
403
579
|
};
|
|
404
580
|
}
|
|
581
|
+
// CRITICAL: If currentMessage is NOT an assistant (e.g., it's a tool message),
|
|
582
|
+
// but we have a message to finalize (from task_complete), we need to find and update
|
|
583
|
+
// the most recent assistant message in history with the full content.
|
|
584
|
+
if (message && message.length > 0) {
|
|
585
|
+
// Find the most recent assistant message in history to update
|
|
586
|
+
const lastAssistantIndex = prev.messageHistory.findIndex((msg, i) => msg.role === 'assistant' &&
|
|
587
|
+
i === prev.messageHistory.map((m, idx) => m.role === 'assistant' ? idx : -1).filter(idx => idx >= 0).pop());
|
|
588
|
+
if (lastAssistantIndex >= 0) {
|
|
589
|
+
const lastAssistant = prev.messageHistory[lastAssistantIndex];
|
|
590
|
+
// Only update if the current content is shorter than what we're providing
|
|
591
|
+
// (meaning the message was moved to history mid-stream)
|
|
592
|
+
if (lastAssistant.content.length < message.length) {
|
|
593
|
+
try {
|
|
594
|
+
fs.appendFileSync('cli_frontend_logs.txt', `[${new Date().toISOString()}] [App] onResponseReceived: Updating history assistant message from ${lastAssistant.content.length} chars to ${message.length} chars\n`);
|
|
595
|
+
}
|
|
596
|
+
catch (e) { }
|
|
597
|
+
const updatedHistory = [...prev.messageHistory];
|
|
598
|
+
updatedHistory[lastAssistantIndex] = {
|
|
599
|
+
...lastAssistant,
|
|
600
|
+
content: message,
|
|
601
|
+
shouldStream: false
|
|
602
|
+
};
|
|
603
|
+
return {
|
|
604
|
+
...prev,
|
|
605
|
+
messageHistory: updatedHistory,
|
|
606
|
+
currentMessage: null,
|
|
607
|
+
isLoading: false,
|
|
608
|
+
isAiWorking: false
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
405
613
|
return {
|
|
406
614
|
...prev,
|
|
407
615
|
isLoading: false,
|
|
@@ -473,6 +681,75 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
473
681
|
}));
|
|
474
682
|
});
|
|
475
683
|
}, []); // Empty dependency array - only register once
|
|
684
|
+
// Set up callback for chat picker
|
|
685
|
+
React.useEffect(() => {
|
|
686
|
+
onChatPickerSetup((chats, currentChatId) => {
|
|
687
|
+
clearScreen();
|
|
688
|
+
setState(prev => ({
|
|
689
|
+
...prev,
|
|
690
|
+
screen: 'chat-picker',
|
|
691
|
+
chatPickerChats: chats,
|
|
692
|
+
currentChatId: currentChatId,
|
|
693
|
+
isLoading: false
|
|
694
|
+
}));
|
|
695
|
+
});
|
|
696
|
+
}, []); // Empty dependency array - only register once
|
|
697
|
+
// Set up callback for chat delete picker
|
|
698
|
+
React.useEffect(() => {
|
|
699
|
+
onChatDeletePickerSetup((chats, currentChatId) => {
|
|
700
|
+
clearScreen();
|
|
701
|
+
setState(prev => ({
|
|
702
|
+
...prev,
|
|
703
|
+
screen: 'chat-delete-picker',
|
|
704
|
+
chatPickerChats: chats,
|
|
705
|
+
currentChatId: currentChatId,
|
|
706
|
+
isLoading: false
|
|
707
|
+
}));
|
|
708
|
+
});
|
|
709
|
+
}, []); // Empty dependency array - only register once
|
|
710
|
+
// Set up callback for read-only chat list view
|
|
711
|
+
React.useEffect(() => {
|
|
712
|
+
onChatListSetup((chats, currentChatId) => {
|
|
713
|
+
clearScreen();
|
|
714
|
+
setState(prev => ({
|
|
715
|
+
...prev,
|
|
716
|
+
screen: 'chat-list-view',
|
|
717
|
+
chatPickerChats: chats,
|
|
718
|
+
currentChatId: currentChatId,
|
|
719
|
+
isLoading: false
|
|
720
|
+
}));
|
|
721
|
+
});
|
|
722
|
+
}, []); // Empty dependency array - only register once
|
|
723
|
+
// Set up callback for chat rename picker
|
|
724
|
+
React.useEffect(() => {
|
|
725
|
+
onChatRenamePickerSetup((chats, currentChatId) => {
|
|
726
|
+
clearScreen();
|
|
727
|
+
setState(prev => ({
|
|
728
|
+
...prev,
|
|
729
|
+
screen: 'chat-rename-picker',
|
|
730
|
+
chatPickerChats: chats,
|
|
731
|
+
currentChatId: currentChatId,
|
|
732
|
+
isLoading: false
|
|
733
|
+
}));
|
|
734
|
+
});
|
|
735
|
+
}, []); // Empty dependency array - only register once
|
|
736
|
+
// Set up callback for message restoration (when resuming a chat)
|
|
737
|
+
React.useEffect(() => {
|
|
738
|
+
onRestoreMessagesSetup((restoredMessages) => {
|
|
739
|
+
// Clear the screen and restore messages
|
|
740
|
+
clearScreen();
|
|
741
|
+
setState(prev => ({
|
|
742
|
+
...prev,
|
|
743
|
+
messageHistory: restoredMessages,
|
|
744
|
+
screen: 'chat',
|
|
745
|
+
isLoading: false
|
|
746
|
+
}));
|
|
747
|
+
});
|
|
748
|
+
}, []); // Empty dependency array - only register once
|
|
749
|
+
// Sync UI message history to CLI adapter whenever it changes
|
|
750
|
+
React.useEffect(() => {
|
|
751
|
+
onUIMessageHistoryUpdate(state.messageHistory);
|
|
752
|
+
}, [state.messageHistory]);
|
|
476
753
|
// Set up callback for tool execution updates
|
|
477
754
|
React.useEffect(() => {
|
|
478
755
|
onToolExecutionUpdate((update) => {
|
|
@@ -772,8 +1049,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
772
1049
|
currentMessage: connectionMessage
|
|
773
1050
|
};
|
|
774
1051
|
}
|
|
775
|
-
// If connected or
|
|
776
|
-
if (status.status === 'connected' || status.status === 'error') {
|
|
1052
|
+
// If connected, error, or disconnected, update the current message and move to history
|
|
1053
|
+
if (status.status === 'connected' || status.status === 'error' || status.status === 'disconnected') {
|
|
777
1054
|
// Create the final connection status message
|
|
778
1055
|
const finalConnectionMessage = {
|
|
779
1056
|
id: `connection-${Date.now()}`,
|
|
@@ -1150,6 +1427,26 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1150
1427
|
}
|
|
1151
1428
|
// ESC key handling - cancel AI execution
|
|
1152
1429
|
if (key.escape) {
|
|
1430
|
+
// If in a picker screen, return to chat
|
|
1431
|
+
// If in chat-rename-input, go back to rename picker
|
|
1432
|
+
if (state.screen === 'chat-rename-input') {
|
|
1433
|
+
setState(prev => ({
|
|
1434
|
+
...prev,
|
|
1435
|
+
screen: 'chat-rename-picker',
|
|
1436
|
+
renameChatId: undefined,
|
|
1437
|
+
renameChatTitle: undefined
|
|
1438
|
+
}));
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
// If in a picker screen, return to chat
|
|
1442
|
+
if (state.screen === 'chat-picker' || state.screen === 'chat-delete-picker' || state.screen === 'chat-list-view' || state.screen === 'chat-rename-picker') {
|
|
1443
|
+
setState(prev => ({
|
|
1444
|
+
...prev,
|
|
1445
|
+
screen: 'chat',
|
|
1446
|
+
chatPickerChats: undefined
|
|
1447
|
+
}));
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1153
1450
|
// If AI is working, cancel the operation
|
|
1154
1451
|
if (state.isLoading || state.isAiWorking) {
|
|
1155
1452
|
// Cancel the current AI request
|
|
@@ -1237,7 +1534,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1237
1534
|
return;
|
|
1238
1535
|
}
|
|
1239
1536
|
}, { isActive: !state.isInteractiveEditorMode });
|
|
1240
|
-
const handleSubmit = useCallback(async (value) => {
|
|
1537
|
+
const handleSubmit = useCallback(async (value, clipboardImages) => {
|
|
1241
1538
|
// Trim the value to remove any leading/trailing whitespace or newlines
|
|
1242
1539
|
const trimmedValue = value.trim();
|
|
1243
1540
|
if (!trimmedValue)
|
|
@@ -1264,6 +1561,33 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1264
1561
|
}
|
|
1265
1562
|
return;
|
|
1266
1563
|
}
|
|
1564
|
+
// Check if this is the /clean-ui command (refresh UI without clearing history)
|
|
1565
|
+
if (trimmedValue === '/clean-ui' || trimmedValue === '/refresh-ui' || trimmedValue === '/redraw') {
|
|
1566
|
+
// Clear the terminal screen first
|
|
1567
|
+
clearScreen();
|
|
1568
|
+
// Use functional setState to capture current history, clear it, then restore
|
|
1569
|
+
// This forces a complete re-render of all messages with proper numbering
|
|
1570
|
+
setState(prev => {
|
|
1571
|
+
const savedHistory = [...prev.messageHistory];
|
|
1572
|
+
const savedCurrent = prev.currentMessage;
|
|
1573
|
+
// Schedule restoration after the clear takes effect
|
|
1574
|
+
setTimeout(() => {
|
|
1575
|
+
clearScreen(); // Clear again to ensure clean slate
|
|
1576
|
+
setState(innerPrev => ({
|
|
1577
|
+
...innerPrev,
|
|
1578
|
+
messageHistory: savedHistory,
|
|
1579
|
+
currentMessage: savedCurrent
|
|
1580
|
+
}));
|
|
1581
|
+
}, 100);
|
|
1582
|
+
// Return cleared state to force unmount of all message components
|
|
1583
|
+
return {
|
|
1584
|
+
...prev,
|
|
1585
|
+
messageHistory: [],
|
|
1586
|
+
currentMessage: null
|
|
1587
|
+
};
|
|
1588
|
+
});
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1267
1591
|
// In command mode, add command to history immediately
|
|
1268
1592
|
if (state.commandMode) {
|
|
1269
1593
|
setState(prev => {
|
|
@@ -1328,7 +1652,71 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1328
1652
|
: [...prev.commandHistory, trimmedValue]
|
|
1329
1653
|
}));
|
|
1330
1654
|
try {
|
|
1331
|
-
|
|
1655
|
+
// Upload clipboard images if present
|
|
1656
|
+
let messageWithImages = trimmedValue;
|
|
1657
|
+
// Debug logging for clipboard images
|
|
1658
|
+
logDebug(`[CLIPBOARD] clipboardImages count: ${clipboardImages?.length || 0}`);
|
|
1659
|
+
logDebug(`[CLIPBOARD] currentChatId: ${state.currentChatId || 'null'}`);
|
|
1660
|
+
if (clipboardImages && clipboardImages.length > 0) {
|
|
1661
|
+
// Check for existing conversation ID - conversationManager is the single source of truth
|
|
1662
|
+
// This ensures we use the same conversation that cli-adapter uses
|
|
1663
|
+
let conversationId = conversationManager.getCurrentConversationId() || state.currentChatId;
|
|
1664
|
+
logDebug(`[CLIPBOARD] conversationManager ID: ${conversationManager.getCurrentConversationId() || 'null'}`);
|
|
1665
|
+
logDebug(`[CLIPBOARD] state.currentChatId: ${state.currentChatId || 'null'}`);
|
|
1666
|
+
if (!conversationId) {
|
|
1667
|
+
// No conversation exists anywhere - create one via conversationManager
|
|
1668
|
+
// This will set the internal state that cli-adapter's ensureConversationStarted checks
|
|
1669
|
+
logDebug(`[CLIPBOARD] No existing conversation - creating new one for image upload`);
|
|
1670
|
+
try {
|
|
1671
|
+
const newConversation = await conversationManager.startNewConversation('New Chat', state.currentModel || 'gemini-2-flash-preview', 'google', undefined // workingDirectory - will use process.cwd()
|
|
1672
|
+
);
|
|
1673
|
+
conversationId = newConversation.id;
|
|
1674
|
+
// Update UI state with the new chat ID
|
|
1675
|
+
setState(prev => ({ ...prev, currentChatId: conversationId }));
|
|
1676
|
+
logDebug(`[CLIPBOARD] Created new conversation: ${conversationId}`);
|
|
1677
|
+
// Delay to ensure DB transaction is fully committed before file upload
|
|
1678
|
+
// Note: Backend now handles FK constraint violations gracefully, but extra delay helps ensure consistency
|
|
1679
|
+
logDebug(`[CLIPBOARD] Waiting for DB commit...`);
|
|
1680
|
+
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
1681
|
+
logDebug(`[CLIPBOARD] Proceeding with upload`);
|
|
1682
|
+
}
|
|
1683
|
+
catch (createError) {
|
|
1684
|
+
logError('Failed to create conversation for image upload', createError instanceof Error ? createError : undefined);
|
|
1685
|
+
// Mark the upload as failed - don't use temp IDs that will fail FK constraint
|
|
1686
|
+
for (let i = 0; i < clipboardImages.length; i++) {
|
|
1687
|
+
messageWithImages = messageWithImages.replace(/#image\b/i, '[IMAGE UPLOAD FAILED - No conversation]');
|
|
1688
|
+
}
|
|
1689
|
+
logDebug(`[CLIPBOARD] Skipping upload due to conversation creation failure`);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
// Only attempt upload if we have a valid conversation ID
|
|
1693
|
+
if (conversationId && !conversationId.startsWith('temp_')) {
|
|
1694
|
+
logDebug(`[CLIPBOARD] Starting upload for ${clipboardImages.length} images with conversationId: ${conversationId}`);
|
|
1695
|
+
// Upload each clipboard image
|
|
1696
|
+
for (const image of clipboardImages) {
|
|
1697
|
+
try {
|
|
1698
|
+
logDebug(`[CLIPBOARD] Uploading image: ${image.displayName}, size: ${image.sizeBytes} bytes`);
|
|
1699
|
+
const uploadResult = await apiClient.uploadFile(conversationId, image.displayName, image.mimeType, image.base64Data);
|
|
1700
|
+
logDebug(`[CLIPBOARD] Upload success! gcsUri: ${uploadResult.gcsUri || 'none'}`);
|
|
1701
|
+
// Append image reference info to message (for AI to know an image was attached)
|
|
1702
|
+
// The backend will use gcsUri for Vertex AI multimodal input
|
|
1703
|
+
if (uploadResult.gcsUri) {
|
|
1704
|
+
// Replace #image with the actual gcsUri marker
|
|
1705
|
+
// Case insensitive, first occurrence only
|
|
1706
|
+
messageWithImages = messageWithImages.replace(/#image\b/i, `[IMAGE: ${uploadResult.fileName} (${uploadResult.gcsUri})]`);
|
|
1707
|
+
logDebug(`[CLIPBOARD] Replaced #image with: [IMAGE: ${uploadResult.fileName} (${uploadResult.gcsUri})]`);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
catch (uploadError) {
|
|
1711
|
+
// Log error but continue - image upload is optional
|
|
1712
|
+
logError('Failed to upload clipboard image', uploadError instanceof Error ? uploadError : undefined);
|
|
1713
|
+
// Remove the #image placeholder if upload failed
|
|
1714
|
+
messageWithImages = messageWithImages.replace(/#image\b/i, '[IMAGE UPLOAD FAILED]');
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
} // End: if (conversationId && !startsWith temp_)
|
|
1718
|
+
} // End: if (clipboardImages && clipboardImages.length > 0)
|
|
1719
|
+
await onMessage(messageWithImages);
|
|
1332
1720
|
}
|
|
1333
1721
|
catch (error) {
|
|
1334
1722
|
// Add error message
|
|
@@ -1374,7 +1762,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1374
1762
|
return (React.createElement(Box, { flexDirection: "column" },
|
|
1375
1763
|
state.screen === 'chat' && (React.createElement(React.Fragment, null,
|
|
1376
1764
|
!state.shellState?.isFocused && (React.createElement(MessageList, { history: state.messageHistory, current: state.currentMessage, showBanner: true })),
|
|
1377
|
-
state.shellState && (React.createElement(InteractiveShell, { command: state.shellState.command, cwd: state.shellState.cwd, isRunning: state.shellState.isRunning, output: state.shellState.output, exitCode: state.shellState.exitCode, error: state.shellState.error, isFocused: state.shellState.isFocused, onResize: state.shellState.onResize, remoteContext: state.shellState.remoteContext, onInput: (input) => {
|
|
1765
|
+
state.shellState && (getTerminalDimensions().shouldEnableStreaming || !state.shellState.isRunning) && (React.createElement(InteractiveShell, { command: state.shellState.command, cwd: state.shellState.cwd, isRunning: state.shellState.isRunning, output: state.shellState.output, exitCode: state.shellState.exitCode, error: state.shellState.error, isFocused: state.shellState.isFocused, onResize: state.shellState.onResize, remoteContext: state.shellState.remoteContext, onInput: (input) => {
|
|
1378
1766
|
// PTY MODE: Send everything immediately
|
|
1379
1767
|
onShellInput(input);
|
|
1380
1768
|
}, onFocusChange: (focused) => {
|
|
@@ -1439,7 +1827,207 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1439
1827
|
});
|
|
1440
1828
|
}
|
|
1441
1829
|
} })),
|
|
1442
|
-
state.screen === '
|
|
1830
|
+
state.screen === 'chat-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
1831
|
+
React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDCDA Resume a Previous Chat"),
|
|
1832
|
+
React.createElement(Box, { marginTop: 1 },
|
|
1833
|
+
React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
|
|
1834
|
+
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
1835
|
+
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1836
|
+
const isCurrent = chat.id === state.currentChatId;
|
|
1837
|
+
const env = chat.environment || 'local';
|
|
1838
|
+
// Format: Title [environment] date time - N msgs
|
|
1839
|
+
return {
|
|
1840
|
+
label: `${isCurrent ? '● ' : ' '}${chat.title}`,
|
|
1841
|
+
envLabel: `[${env}]`,
|
|
1842
|
+
dateLabel: `${date} ${time}`,
|
|
1843
|
+
msgLabel: `${chat.messageCount} msgs`,
|
|
1844
|
+
value: chat.id,
|
|
1845
|
+
isCurrent,
|
|
1846
|
+
environment: env
|
|
1847
|
+
};
|
|
1848
|
+
}), itemComponent: ({ isSelected, label, isCurrent, envLabel, dateLabel, msgLabel, environment }) => (React.createElement(Box, null,
|
|
1849
|
+
React.createElement(Text, { color: isSelected ? '#00ccff' : (isCurrent ? '#00cc66' : 'white'), bold: isSelected || isCurrent },
|
|
1850
|
+
isSelected ? '> ' : ' ',
|
|
1851
|
+
label),
|
|
1852
|
+
React.createElement(Text, { color: environment === 'local' ? 'gray' : (environment?.startsWith('ssh') ? '#ff9966' : environment?.startsWith('wsl') ? '#66ff99' : '#66ccff') },
|
|
1853
|
+
" ",
|
|
1854
|
+
envLabel),
|
|
1855
|
+
React.createElement(Text, { color: "gray" },
|
|
1856
|
+
" ",
|
|
1857
|
+
dateLabel),
|
|
1858
|
+
React.createElement(Text, { color: "gray" }, " - "),
|
|
1859
|
+
React.createElement(Text, { color: "gray" }, msgLabel))), onSelect: async (item) => {
|
|
1860
|
+
setState(prev => ({ ...prev, screen: 'chat', isLoading: true }));
|
|
1861
|
+
try {
|
|
1862
|
+
await onChatPickerSelection(item.value);
|
|
1863
|
+
setState(prev => ({
|
|
1864
|
+
...prev,
|
|
1865
|
+
isLoading: false,
|
|
1866
|
+
isAiWorking: false
|
|
1867
|
+
}));
|
|
1868
|
+
}
|
|
1869
|
+
catch (error) {
|
|
1870
|
+
setState(prev => {
|
|
1871
|
+
const errorMessage = {
|
|
1872
|
+
id: `system-error-${Date.now()}`,
|
|
1873
|
+
role: 'system',
|
|
1874
|
+
content: `❌ Error: ${error.message || 'Failed to load chat'}`,
|
|
1875
|
+
timestamp: new Date()
|
|
1876
|
+
};
|
|
1877
|
+
return {
|
|
1878
|
+
...prev,
|
|
1879
|
+
messageHistory: [...prev.messageHistory, errorMessage],
|
|
1880
|
+
currentMessage: null,
|
|
1881
|
+
isLoading: false,
|
|
1882
|
+
isAiWorking: false
|
|
1883
|
+
};
|
|
1884
|
+
});
|
|
1885
|
+
}
|
|
1886
|
+
} })),
|
|
1887
|
+
React.createElement(Box, { marginTop: 1 },
|
|
1888
|
+
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
|
|
1889
|
+
state.screen === 'chat-delete-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
1890
|
+
React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDDD1\uFE0F Select a Chat to Delete"),
|
|
1891
|
+
React.createElement(Box, { marginTop: 1 },
|
|
1892
|
+
React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
|
|
1893
|
+
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
1894
|
+
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1895
|
+
const isCurrent = chat.id === state.currentChatId;
|
|
1896
|
+
const env = chat.environment || 'local';
|
|
1897
|
+
return {
|
|
1898
|
+
label: `${isCurrent ? '● ' : ' '}${chat.title}`,
|
|
1899
|
+
envLabel: `[${env}]`,
|
|
1900
|
+
dateLabel: `${date} ${time}`,
|
|
1901
|
+
msgLabel: `${chat.messageCount} msgs`,
|
|
1902
|
+
value: chat.id,
|
|
1903
|
+
isCurrent,
|
|
1904
|
+
environment: env
|
|
1905
|
+
};
|
|
1906
|
+
}), itemComponent: ({ isSelected, label, isCurrent, envLabel, dateLabel, msgLabel, environment }) => (React.createElement(Box, null,
|
|
1907
|
+
React.createElement(Text, { color: isSelected ? '#00ccff' : (isCurrent ? '#00cc66' : 'white'), bold: isSelected || isCurrent },
|
|
1908
|
+
isSelected ? '> ' : ' ',
|
|
1909
|
+
label),
|
|
1910
|
+
React.createElement(Text, { color: environment === 'local' ? 'gray' : (environment?.startsWith('ssh') ? '#ff9966' : environment?.startsWith('wsl') ? '#66ff99' : '#66ccff') },
|
|
1911
|
+
" ",
|
|
1912
|
+
envLabel),
|
|
1913
|
+
React.createElement(Text, { color: "gray" },
|
|
1914
|
+
" ",
|
|
1915
|
+
dateLabel),
|
|
1916
|
+
React.createElement(Text, { color: "gray" }, " - "),
|
|
1917
|
+
React.createElement(Text, { color: "gray" }, msgLabel))), onSelect: async (item) => {
|
|
1918
|
+
setState(prev => ({ ...prev, screen: 'chat', isLoading: false }));
|
|
1919
|
+
try {
|
|
1920
|
+
await onChatDeletePickerSelection(item.value);
|
|
1921
|
+
}
|
|
1922
|
+
catch (error) {
|
|
1923
|
+
setState(prev => {
|
|
1924
|
+
const errorMessage = {
|
|
1925
|
+
id: `system-error-${Date.now()}`,
|
|
1926
|
+
role: 'system',
|
|
1927
|
+
content: `❌ Error: ${error.message || 'Failed to delete chat'}`,
|
|
1928
|
+
timestamp: new Date()
|
|
1929
|
+
};
|
|
1930
|
+
return {
|
|
1931
|
+
...prev,
|
|
1932
|
+
messageHistory: [...prev.messageHistory, errorMessage],
|
|
1933
|
+
currentMessage: null,
|
|
1934
|
+
isLoading: false,
|
|
1935
|
+
isAiWorking: false
|
|
1936
|
+
};
|
|
1937
|
+
});
|
|
1938
|
+
}
|
|
1939
|
+
} })),
|
|
1940
|
+
React.createElement(Box, { marginTop: 1 },
|
|
1941
|
+
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
|
|
1942
|
+
state.screen === 'chat-list-view' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
1943
|
+
React.createElement(Text, { color: "#00ccff", bold: true },
|
|
1944
|
+
"\uD83D\uDCDA Saved Chats (",
|
|
1945
|
+
state.chatPickerChats.length,
|
|
1946
|
+
")"),
|
|
1947
|
+
React.createElement(Box, { marginTop: 1, flexDirection: "column" }, state.chatPickerChats.map((chat) => {
|
|
1948
|
+
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
1949
|
+
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1950
|
+
const isCurrent = chat.id === state.currentChatId;
|
|
1951
|
+
const env = chat.environment || 'local';
|
|
1952
|
+
return (React.createElement(Box, { key: chat.id },
|
|
1953
|
+
React.createElement(Text, { color: isCurrent ? '#00cc66' : 'white', bold: isCurrent },
|
|
1954
|
+
isCurrent ? '● ' : ' ',
|
|
1955
|
+
chat.title),
|
|
1956
|
+
React.createElement(Text, { color: env === 'local' ? 'gray' : (env.startsWith('ssh') ? '#ff9966' : env.startsWith('wsl') ? '#66ff99' : '#66ccff') },
|
|
1957
|
+
" [",
|
|
1958
|
+
env,
|
|
1959
|
+
"]"),
|
|
1960
|
+
React.createElement(Text, { color: "gray" },
|
|
1961
|
+
" ",
|
|
1962
|
+
date,
|
|
1963
|
+
" ",
|
|
1964
|
+
time),
|
|
1965
|
+
React.createElement(Text, { color: "gray" }, " - "),
|
|
1966
|
+
React.createElement(Text, { color: "gray" },
|
|
1967
|
+
chat.messageCount,
|
|
1968
|
+
" msgs")));
|
|
1969
|
+
})),
|
|
1970
|
+
React.createElement(Box, { marginTop: 1 },
|
|
1971
|
+
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
|
|
1972
|
+
state.screen === 'chat-rename-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
1973
|
+
React.createElement(Text, { color: "#00ccff", bold: true }, "\u270F\uFE0F Select a Chat to Rename"),
|
|
1974
|
+
React.createElement(Box, { marginTop: 1 },
|
|
1975
|
+
React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
|
|
1976
|
+
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
1977
|
+
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
1978
|
+
const isCurrent = chat.id === state.currentChatId;
|
|
1979
|
+
const env = chat.environment || 'local';
|
|
1980
|
+
return {
|
|
1981
|
+
label: `${isCurrent ? '● ' : ' '}${chat.title}`,
|
|
1982
|
+
envLabel: `[${env}]`,
|
|
1983
|
+
dateLabel: `${date} ${time}`,
|
|
1984
|
+
msgLabel: `${chat.messageCount} msgs`,
|
|
1985
|
+
value: chat.id,
|
|
1986
|
+
title: chat.title,
|
|
1987
|
+
isCurrent,
|
|
1988
|
+
environment: env
|
|
1989
|
+
};
|
|
1990
|
+
}), itemComponent: ({ isSelected, label, isCurrent, envLabel, dateLabel, msgLabel, environment }) => (React.createElement(Box, null,
|
|
1991
|
+
React.createElement(Text, { color: isSelected ? '#00ccff' : (isCurrent ? '#00cc66' : 'white'), bold: isSelected || isCurrent },
|
|
1992
|
+
isSelected ? '> ' : ' ',
|
|
1993
|
+
label),
|
|
1994
|
+
React.createElement(Text, { color: environment === 'local' ? 'gray' : (environment?.startsWith('ssh') ? '#ff9966' : environment?.startsWith('wsl') ? '#66ff99' : '#66ccff') },
|
|
1995
|
+
" ",
|
|
1996
|
+
envLabel),
|
|
1997
|
+
React.createElement(Text, { color: "gray" },
|
|
1998
|
+
" ",
|
|
1999
|
+
dateLabel),
|
|
2000
|
+
React.createElement(Text, { color: "gray" }, " - "),
|
|
2001
|
+
React.createElement(Text, { color: "gray" }, msgLabel))), onSelect: (item) => {
|
|
2002
|
+
setState(prev => ({
|
|
2003
|
+
...prev,
|
|
2004
|
+
screen: 'chat-rename-input',
|
|
2005
|
+
renameChatId: item.value,
|
|
2006
|
+
renameChatTitle: item.title
|
|
2007
|
+
}));
|
|
2008
|
+
} })),
|
|
2009
|
+
React.createElement(Box, { marginTop: 1 },
|
|
2010
|
+
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
|
|
2011
|
+
state.screen === 'chat-rename-input' && state.renameChatId && (React.createElement(RenameInputScreen, { currentTitle: state.renameChatTitle || '', onRename: (newTitle) => {
|
|
2012
|
+
if (newTitle.trim()) {
|
|
2013
|
+
onChatRename(state.renameChatId, newTitle.trim());
|
|
2014
|
+
}
|
|
2015
|
+
setState(prev => ({
|
|
2016
|
+
...prev,
|
|
2017
|
+
screen: 'chat',
|
|
2018
|
+
chatPickerChats: undefined,
|
|
2019
|
+
renameChatId: undefined,
|
|
2020
|
+
renameChatTitle: undefined
|
|
2021
|
+
}));
|
|
2022
|
+
}, onCancel: () => {
|
|
2023
|
+
setState(prev => ({
|
|
2024
|
+
...prev,
|
|
2025
|
+
screen: 'chat-rename-picker',
|
|
2026
|
+
renameChatId: undefined,
|
|
2027
|
+
renameChatTitle: undefined
|
|
2028
|
+
}));
|
|
2029
|
+
} })),
|
|
2030
|
+
state.screen === 'plan-approval' && state.planApprovalRequest && (React.createElement(DetailedPlanReviewScreen, { plan: state.planApprovalRequest.plan, onApprove: () => {
|
|
1443
2031
|
const resolve = state.planApprovalRequest?.resolve;
|
|
1444
2032
|
const plan = state.planApprovalRequest?.plan;
|
|
1445
2033
|
if (resolve) {
|
|
@@ -1453,7 +2041,11 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1453
2041
|
timestamp: new Date(),
|
|
1454
2042
|
planAccepted: {
|
|
1455
2043
|
planTitle: plan?.title,
|
|
1456
|
-
totalTasks: plan?.steps?.length
|
|
2044
|
+
totalTasks: plan?.steps?.length,
|
|
2045
|
+
tasks: plan?.steps?.map(step => ({
|
|
2046
|
+
description: step.description,
|
|
2047
|
+
subtasks: step.subtasks?.map(st => ({ description: st.description }))
|
|
2048
|
+
}))
|
|
1457
2049
|
}
|
|
1458
2050
|
};
|
|
1459
2051
|
setState(prev => ({
|