centaurus-cli 2.8.5 → 2.8.7

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