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