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.
Files changed (128) hide show
  1. package/dist/cli-adapter.d.ts +85 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +773 -31
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  6. package/dist/config/mcp-config-manager.js +9 -8
  7. package/dist/config/mcp-config-manager.js.map +1 -1
  8. package/dist/config/slash-commands.d.ts +2 -0
  9. package/dist/config/slash-commands.d.ts.map +1 -1
  10. package/dist/config/slash-commands.js +31 -1
  11. package/dist/config/slash-commands.js.map +1 -1
  12. package/dist/context/handlers/docker-handler.js.map +1 -1
  13. package/dist/context/handlers/ssh-handler.d.ts +16 -1
  14. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  15. package/dist/context/handlers/ssh-handler.js +57 -12
  16. package/dist/context/handlers/ssh-handler.js.map +1 -1
  17. package/dist/context/subshell-handler.d.ts +14 -0
  18. package/dist/context/subshell-handler.d.ts.map +1 -1
  19. package/dist/hooks/useTerminalDimensions.d.ts +41 -0
  20. package/dist/hooks/useTerminalDimensions.d.ts.map +1 -0
  21. package/dist/hooks/useTerminalDimensions.js +84 -0
  22. package/dist/hooks/useTerminalDimensions.js.map +1 -0
  23. package/dist/index.js +57 -5
  24. package/dist/index.js.map +1 -1
  25. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  26. package/dist/mcp/mcp-command-handler.js +3 -1
  27. package/dist/mcp/mcp-command-handler.js.map +1 -1
  28. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  29. package/dist/mcp/mcp-server-manager.js +5 -3
  30. package/dist/mcp/mcp-server-manager.js.map +1 -1
  31. package/dist/services/api-client.d.ts +24 -0
  32. package/dist/services/api-client.d.ts.map +1 -1
  33. package/dist/services/api-client.js +27 -0
  34. package/dist/services/api-client.js.map +1 -1
  35. package/dist/services/auth-handler.js +1 -1
  36. package/dist/services/auth-handler.js.map +1 -1
  37. package/dist/services/clipboard-service.d.ts +42 -0
  38. package/dist/services/clipboard-service.d.ts.map +1 -0
  39. package/dist/services/clipboard-service.js +217 -0
  40. package/dist/services/clipboard-service.js.map +1 -0
  41. package/dist/services/local-chat-storage.d.ts +154 -0
  42. package/dist/services/local-chat-storage.d.ts.map +1 -0
  43. package/dist/services/local-chat-storage.js +258 -0
  44. package/dist/services/local-chat-storage.js.map +1 -0
  45. package/dist/tools/grep-search.d.ts +5 -0
  46. package/dist/tools/grep-search.d.ts.map +1 -1
  47. package/dist/tools/grep-search.js +68 -16
  48. package/dist/tools/grep-search.js.map +1 -1
  49. package/dist/tools/plan-mode.d.ts +57 -6
  50. package/dist/tools/plan-mode.d.ts.map +1 -1
  51. package/dist/tools/plan-mode.js +297 -46
  52. package/dist/tools/plan-mode.js.map +1 -1
  53. package/dist/tools/read-binary-file.d.ts +10 -0
  54. package/dist/tools/read-binary-file.d.ts.map +1 -0
  55. package/dist/tools/read-binary-file.js +210 -0
  56. package/dist/tools/read-binary-file.js.map +1 -0
  57. package/dist/types/index.d.ts +7 -1
  58. package/dist/types/index.d.ts.map +1 -1
  59. package/dist/ui/components/App.d.ts +35 -0
  60. package/dist/ui/components/App.d.ts.map +1 -1
  61. package/dist/ui/components/App.js +622 -43
  62. package/dist/ui/components/App.js.map +1 -1
  63. package/dist/ui/components/ClipboardImageAutocomplete.d.ts +14 -0
  64. package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +1 -0
  65. package/dist/ui/components/ClipboardImageAutocomplete.js +39 -0
  66. package/dist/ui/components/ClipboardImageAutocomplete.js.map +1 -0
  67. package/dist/ui/components/ConnectionStatusMessage.d.ts +1 -1
  68. package/dist/ui/components/ConnectionStatusMessage.d.ts.map +1 -1
  69. package/dist/ui/components/ConnectionStatusMessage.js +21 -0
  70. package/dist/ui/components/ConnectionStatusMessage.js.map +1 -1
  71. package/dist/ui/components/DetailedPlanReviewScreen.d.ts +17 -0
  72. package/dist/ui/components/DetailedPlanReviewScreen.d.ts.map +1 -0
  73. package/dist/ui/components/DetailedPlanReviewScreen.js +110 -0
  74. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -0
  75. package/dist/ui/components/InputBox.d.ts +4 -1
  76. package/dist/ui/components/InputBox.d.ts.map +1 -1
  77. package/dist/ui/components/InputBox.js +419 -30
  78. package/dist/ui/components/InputBox.js.map +1 -1
  79. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  80. package/dist/ui/components/InteractiveShell.js +20 -6
  81. package/dist/ui/components/InteractiveShell.js.map +1 -1
  82. package/dist/ui/components/MessageDisplay.d.ts +6 -0
  83. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  84. package/dist/ui/components/MessageDisplay.js +66 -3
  85. package/dist/ui/components/MessageDisplay.js.map +1 -1
  86. package/dist/ui/components/PlanAcceptedMessage.d.ts +8 -0
  87. package/dist/ui/components/PlanAcceptedMessage.d.ts.map +1 -1
  88. package/dist/ui/components/PlanAcceptedMessage.js +26 -8
  89. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  90. package/dist/ui/components/StreamingMessageDisplay.d.ts +3 -0
  91. package/dist/ui/components/StreamingMessageDisplay.d.ts.map +1 -1
  92. package/dist/ui/components/StreamingMessageDisplay.js +10 -6
  93. package/dist/ui/components/StreamingMessageDisplay.js.map +1 -1
  94. package/dist/ui/components/TaskCompletedMessage.d.ts.map +1 -1
  95. package/dist/ui/components/TaskCompletedMessage.js +4 -4
  96. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  97. package/dist/ui/components/TaskProgressIndicator.d.ts +18 -0
  98. package/dist/ui/components/TaskProgressIndicator.d.ts.map +1 -0
  99. package/dist/ui/components/TaskProgressIndicator.js +72 -0
  100. package/dist/ui/components/TaskProgressIndicator.js.map +1 -0
  101. package/dist/ui/components/ThinkingDisplay.d.ts +3 -0
  102. package/dist/ui/components/ThinkingDisplay.d.ts.map +1 -1
  103. package/dist/ui/components/ThinkingDisplay.js +6 -4
  104. package/dist/ui/components/ThinkingDisplay.js.map +1 -1
  105. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  106. package/dist/ui/components/ToolExecutionMessage.js +85 -15
  107. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  108. package/dist/ui/components/VersionUpdatePrompt.d.ts +1 -2
  109. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  110. package/dist/ui/components/VersionUpdatePrompt.js +108 -27
  111. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  112. package/dist/utils/custom-commands-manager.d.ts +59 -0
  113. package/dist/utils/custom-commands-manager.d.ts.map +1 -0
  114. package/dist/utils/custom-commands-manager.js +142 -0
  115. package/dist/utils/custom-commands-manager.js.map +1 -0
  116. package/dist/utils/input-classifier.d.ts +10 -11
  117. package/dist/utils/input-classifier.d.ts.map +1 -1
  118. package/dist/utils/input-classifier.js +299 -75
  119. package/dist/utils/input-classifier.js.map +1 -1
  120. package/dist/utils/terminal-output.d.ts.map +1 -1
  121. package/dist/utils/terminal-output.js +110 -14
  122. package/dist/utils/terminal-output.js.map +1 -1
  123. package/dist/utils/unicode-sanitizer.d.ts +44 -0
  124. package/dist/utils/unicode-sanitizer.d.ts.map +1 -0
  125. package/dist/utils/unicode-sanitizer.js +211 -0
  126. package/dist/utils/unicode-sanitizer.js.map +1 -0
  127. package/models-config.json +2 -3
  128. 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 { spawn } from 'child_process';
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 { PlanReviewScreen } from './PlanReviewScreen.js';
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) && (current.role === 'assistant' && current.shouldStream !== false ? (React.createElement(StreamingMessageDisplay, { key: current.id, message: current })) : (React.createElement(MessageDisplay, { key: current.id, message: current })))));
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
- 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 }) => {
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
- shouldStream: true // AI responses should stream
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 error, update the current message and move to history
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
- await onMessage(trimmedValue);
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: handleSubmit, 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 }))),
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 === 'plan-approval' && state.planApprovalRequest && (React.createElement(PlanReviewScreen, { plan: state.planApprovalRequest.plan, onApprove: () => {
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, onInstall: () => {
1492
- // Exit the app and let the user know to run the install command
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'