centaurus-cli 2.9.8 → 3.0.0

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 (88) hide show
  1. package/dist/cli-adapter.d.ts +7 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +249 -155
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/context/handlers/docker-handler.d.ts.map +1 -1
  6. package/dist/context/handlers/docker-handler.js +2 -2
  7. package/dist/context/handlers/docker-handler.js.map +1 -1
  8. package/dist/context/handlers/ssh-handler.d.ts.map +1 -1
  9. package/dist/context/handlers/ssh-handler.js +3 -0
  10. package/dist/context/handlers/ssh-handler.js.map +1 -1
  11. package/dist/context/handlers/wsl-handler.d.ts.map +1 -1
  12. package/dist/context/handlers/wsl-handler.js +9 -3
  13. package/dist/context/handlers/wsl-handler.js.map +1 -1
  14. package/dist/index.js +49 -7
  15. package/dist/index.js.map +1 -1
  16. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  17. package/dist/mcp/mcp-server-manager.js +17 -3
  18. package/dist/mcp/mcp-server-manager.js.map +1 -1
  19. package/dist/services/api-client.d.ts +4 -0
  20. package/dist/services/api-client.d.ts.map +1 -1
  21. package/dist/services/api-client.js +27 -18
  22. package/dist/services/api-client.js.map +1 -1
  23. package/dist/services/clipboard-service.d.ts +9 -14
  24. package/dist/services/clipboard-service.d.ts.map +1 -1
  25. package/dist/services/clipboard-service.js +83 -83
  26. package/dist/services/clipboard-service.js.map +1 -1
  27. package/dist/services/conversation-manager.d.ts.map +1 -1
  28. package/dist/services/conversation-manager.js +3 -2
  29. package/dist/services/conversation-manager.js.map +1 -1
  30. package/dist/tools/enter-remote-session.d.ts +35 -0
  31. package/dist/tools/enter-remote-session.d.ts.map +1 -1
  32. package/dist/tools/enter-remote-session.js +5 -5
  33. package/dist/tools/enter-remote-session.js.map +1 -1
  34. package/dist/tools/read-binary-file.js +6 -6
  35. package/dist/tools/read-binary-file.js.map +1 -1
  36. package/dist/ui/components/App.d.ts +2 -0
  37. package/dist/ui/components/App.d.ts.map +1 -1
  38. package/dist/ui/components/App.js +105 -95
  39. package/dist/ui/components/App.js.map +1 -1
  40. package/dist/ui/components/ClipboardFileAutocomplete.d.ts +10 -0
  41. package/dist/ui/components/ClipboardFileAutocomplete.d.ts.map +1 -0
  42. package/dist/ui/components/{ClipboardImageAutocomplete.js → ClipboardFileAutocomplete.js} +14 -12
  43. package/dist/ui/components/ClipboardFileAutocomplete.js.map +1 -0
  44. package/dist/ui/components/DetailedPlanReviewScreen.js +1 -1
  45. package/dist/ui/components/DetailedPlanReviewScreen.js.map +1 -1
  46. package/dist/ui/components/InputBox.d.ts +2 -2
  47. package/dist/ui/components/InputBox.d.ts.map +1 -1
  48. package/dist/ui/components/InputBox.js +41 -21
  49. package/dist/ui/components/InputBox.js.map +1 -1
  50. package/dist/ui/components/KeyboardHelp.d.ts.map +1 -1
  51. package/dist/ui/components/KeyboardHelp.js +2 -0
  52. package/dist/ui/components/KeyboardHelp.js.map +1 -1
  53. package/dist/ui/components/MessageDisplay.d.ts +4 -4
  54. package/dist/ui/components/MessageDisplay.d.ts.map +1 -1
  55. package/dist/ui/components/MessageDisplay.js +33 -31
  56. package/dist/ui/components/MessageDisplay.js.map +1 -1
  57. package/dist/ui/components/PlanAcceptedMessage.js +2 -2
  58. package/dist/ui/components/PlanAcceptedMessage.js.map +1 -1
  59. package/dist/ui/components/TaskCompletedMessage.js +2 -2
  60. package/dist/ui/components/TaskCompletedMessage.js.map +1 -1
  61. package/dist/ui/components/ToolExecutionMessage.js +7 -7
  62. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  63. package/dist/ui/components/ToolResult.js +1 -1
  64. package/dist/ui/components/ToolResult.js.map +1 -1
  65. package/dist/utils/context-sanitizer.d.ts +50 -0
  66. package/dist/utils/context-sanitizer.d.ts.map +1 -0
  67. package/dist/utils/context-sanitizer.js +119 -0
  68. package/dist/utils/context-sanitizer.js.map +1 -0
  69. package/dist/utils/shell.d.ts.map +1 -1
  70. package/dist/utils/shell.js +12 -1
  71. package/dist/utils/shell.js.map +1 -1
  72. package/dist/utils/syntax-checker.d.ts.map +1 -1
  73. package/dist/utils/syntax-checker.js +26 -0
  74. package/dist/utils/syntax-checker.js.map +1 -1
  75. package/dist/utils/terminal-output.d.ts.map +1 -1
  76. package/dist/utils/terminal-output.js +10 -8
  77. package/dist/utils/terminal-output.js.map +1 -1
  78. package/dist/utils/text-clipboard.d.ts.map +1 -1
  79. package/dist/utils/text-clipboard.js +21 -8
  80. package/dist/utils/text-clipboard.js.map +1 -1
  81. package/package.json +6 -2
  82. package/dist/config/ConfigManager.d.ts +0 -62
  83. package/dist/config/ConfigManager.d.ts.map +0 -1
  84. package/dist/config/ConfigManager.js +0 -234
  85. package/dist/config/ConfigManager.js.map +0 -1
  86. package/dist/ui/components/ClipboardImageAutocomplete.d.ts +0 -14
  87. package/dist/ui/components/ClipboardImageAutocomplete.d.ts.map +0 -1
  88. package/dist/ui/components/ClipboardImageAutocomplete.js.map +0 -1
@@ -8,7 +8,7 @@ import * as path from 'path';
8
8
  import { quickLog } from '../../utils/conversation-logger.js';
9
9
  import { WelcomeBanner } from './WelcomeBanner.js';
10
10
  import { InputBox } from './InputBox.js';
11
- import { MessageDisplay, countImagesInMessage } from './MessageDisplay.js';
11
+ import { MessageDisplay, countFilesInMessage } from './MessageDisplay.js';
12
12
  import { StreamingMessageDisplay } from './StreamingMessageDisplay.js';
13
13
  import { LoadingIndicator } from './LoadingIndicator.js';
14
14
  import { AgentTimer } from './AgentTimer.js';
@@ -69,21 +69,21 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
69
69
  const staticItems = React.useMemo(() => {
70
70
  return showBanner ? [BANNER_ITEM, ...history] : history;
71
71
  }, [history, showBanner]);
72
- // Calculate cumulative image counts for each message (for global numbering)
73
- const imageCountsUpTo = React.useMemo(() => {
72
+ // Calculate cumulative file counts for each message (for global numbering)
73
+ const fileCountsUpTo = React.useMemo(() => {
74
74
  const counts = [];
75
75
  let cumulative = 0;
76
76
  for (const msg of history) {
77
77
  counts.push(cumulative);
78
78
  if (msg.role === 'user') {
79
- cumulative += countImagesInMessage(msg.content);
79
+ cumulative += countFilesInMessage(msg.content);
80
80
  }
81
81
  }
82
82
  return counts;
83
83
  }, [history]);
84
- // Get image count for current message (all images in history)
85
- const totalHistoryImages = imageCountsUpTo.length > 0
86
- ? imageCountsUpTo[imageCountsUpTo.length - 1] + (history[history.length - 1]?.role === 'user' ? countImagesInMessage(history[history.length - 1].content) : 0)
84
+ // Get file count for current message (all files in history)
85
+ const totalHistoryFiles = fileCountsUpTo.length > 0
86
+ ? fileCountsUpTo[fileCountsUpTo.length - 1] + (history[history.length - 1]?.role === 'user' ? countFilesInMessage(history[history.length - 1].content) : 0)
87
87
  : 0;
88
88
  return (React.createElement(Box, { flexDirection: "column", marginY: 1 },
89
89
  React.createElement(Static, { items: staticItems }, (item, index) => {
@@ -94,7 +94,7 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
94
94
  const msg = item;
95
95
  // Calculate history index (account for banner if present)
96
96
  const historyIndex = showBanner ? index - 1 : index;
97
- const imageCountBefore = imageCountsUpTo[historyIndex] || 0;
97
+ const fileCountBefore = fileCountsUpTo[historyIndex] || 0;
98
98
  if (msg.taskCompletion) {
99
99
  return (React.createElement(TaskCompletedMessage, { key: item.id, taskNumber: msg.taskCompletion.taskNumber, totalTasks: msg.taskCompletion.totalTasks, taskDescription: msg.taskCompletion.taskDescription, completionNote: msg.taskCompletion.completionNote }));
100
100
  }
@@ -102,7 +102,7 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
102
102
  if (msg.planAccepted) {
103
103
  return (React.createElement(PlanAcceptedMessage, { key: item.id, planTitle: msg.planAccepted.planTitle, totalTasks: msg.planAccepted.totalTasks, tasks: msg.planAccepted.tasks }));
104
104
  }
105
- return React.createElement(MessageDisplay, { key: item.id, message: item, imageCountBefore: imageCountBefore });
105
+ return React.createElement(MessageDisplay, { key: item.id, message: item, fileCountBefore: fileCountBefore });
106
106
  }),
107
107
  current && !history.some(msg => msg.id === current.id) && (() => {
108
108
  // Get terminal dimensions to determine if streaming should be enabled
@@ -114,7 +114,7 @@ const MessageList = React.memo(({ history, current, showBanner }) => {
114
114
  return (React.createElement(StreamingMessageDisplay, { key: current.id, message: current, maxLines: dimensions.maxStreamingLines }));
115
115
  }
116
116
  else {
117
- return React.createElement(MessageDisplay, { key: current.id, message: current, imageCountBefore: totalHistoryImages });
117
+ return React.createElement(MessageDisplay, { key: current.id, message: current, fileCountBefore: totalHistoryFiles });
118
118
  }
119
119
  })()));
120
120
  }, (prevProps, nextProps) => {
@@ -175,7 +175,7 @@ const RenameInputScreen = ({ currentTitle, onRename, onCancel }) => {
175
175
  React.createElement(Box, { marginTop: 1 },
176
176
  React.createElement(Text, { dimColor: true }, "Press Enter to save, ESC to cancel"))));
177
177
  };
178
- export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onClearStreamedResponse, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onPlanCreated, onTaskCompleted, onCommandModeChange, onToggleCommandMode, onBackgroundModeChange, onToggleBackgroundMode, onBackgroundTaskCountChange, onSubAgentCountChange, onSetAutoModeSetup, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode, onConnectionStatusUpdate, onTokenCountUpdate, onContextLimitReached, onChatPickerSetup, onChatPickerSelection, onChatDeletePickerSetup, onChatDeletePickerSelection, onChatListSetup, onChatRenamePickerSetup, onChatRename, onRestoreMessagesSetup, onUIMessageHistoryUpdate, onBackgroundTaskListSetup, onBackgroundTaskSelection, onBackgroundTaskCancelSetup, onBackgroundTaskCancel, onBackgroundTaskViewSetup, onSessionQuotaUpdate, onMCPAddScreenSetup, onMCPRemoveScreenSetup, onMCPEnableScreenSetup, onMCPDisableScreenSetup, onMCPListScreenSetup, onMCPAddServer, onMCPRemoveServer, onMCPEnableServer, onMCPDisableServer, onMCPValidateConfig, onPromptAnswered, getMainConversation, onWarpifySession, onAiAutoSuggestChange, onWorkflowCreatorSetup, onWorkflowSave, getCheckpoints, onRevertToCheckpointSetup, onSetInputSetup }) => {
178
+ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onClearStreamedResponse, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onPlanCreated, onTaskCompleted, onCommandModeChange, onToggleCommandMode, onBackgroundModeChange, onToggleBackgroundMode, onBackgroundTaskCountChange, onSubAgentCountChange, onSetAutoModeSetup, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode, onConnectionStatusUpdate, onTokenCountUpdate, onContextLimitReached, onChatPickerSetup, onChatPickerSelection, onChatDeletePickerSetup, onChatDeletePickerSelection, onChatListSetup, onChatRenamePickerSetup, onChatRename, onRestoreMessagesSetup, onUIMessageHistoryUpdate, onBackgroundTaskListSetup, onBackgroundTaskSelection, onBackgroundTaskCancelSetup, onBackgroundTaskCancel, onBackgroundTaskViewSetup, onSessionQuotaUpdate, onMCPAddScreenSetup, onMCPRemoveScreenSetup, onMCPEnableScreenSetup, onMCPDisableScreenSetup, onMCPListScreenSetup, onMCPAddServer, onMCPRemoveServer, onMCPEnableServer, onMCPDisableServer, onMCPValidateConfig, onPromptAnswered, getMainConversation, onWarpifySession, onAiAutoSuggestChange, onWorkflowCreatorSetup, onWorkflowSave, getCheckpoints, onRevertToCheckpointSetup, onSetInputSetup, onInterruptQueueUpdate, onQueuedMessageDispatched }) => {
179
179
  const { exit } = useApp();
180
180
  // Calculate limit for paginated lists (75% of terminal height)
181
181
  const listLimit = Math.max(5, Math.floor((process.stdout.rows || 24) * 0.75));
@@ -254,6 +254,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
254
254
  sessionQuotaExhausted: false,
255
255
  sessionQuotaTimeRemaining: '',
256
256
  aiAutoSuggest: false,
257
+ interruptQueue: [],
257
258
  });
258
259
  // Track last terminal width to detect actual width changes
259
260
  const lastTerminalWidthRef = React.useRef(process.stdout.columns || 80);
@@ -405,6 +406,38 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
405
406
  InputDetectionAgent.updateOutput(state.shellState.shellId, state.shellState.output);
406
407
  }
407
408
  }, [state.shellState?.output, state.shellState?.isAgentControlled, state.shellState?.shellId]);
409
+ // Handle interrupt queue updates
410
+ React.useEffect(() => {
411
+ onInterruptQueueUpdate((queue) => {
412
+ setState(prev => ({ ...prev, interruptQueue: [...queue] }));
413
+ });
414
+ }, [onInterruptQueueUpdate]);
415
+ // Handle queued message dispatch: add user message to history when it's actually being processed
416
+ // This ensures the correct order: only show the message when it's the AI's next turn, not when queued
417
+ React.useEffect(() => {
418
+ onQueuedMessageDispatched((message) => {
419
+ setState(prev => {
420
+ const userMessage = {
421
+ id: `user-queued-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
422
+ role: 'user',
423
+ content: message,
424
+ timestamp: new Date()
425
+ };
426
+ // Move current message to history if present (skip thought-only)
427
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
428
+ ? [...prev.messageHistory, prev.currentMessage, userMessage]
429
+ : [...prev.messageHistory, userMessage];
430
+ return {
431
+ ...prev,
432
+ messageHistory: newHistory,
433
+ currentMessage: null,
434
+ isLoading: true,
435
+ loadingMessage: 'Processing your request...',
436
+ isAiWorking: true
437
+ };
438
+ });
439
+ });
440
+ }, [onQueuedMessageDispatched]);
408
441
  // Ref to track if we should strictly block incoming AI updates (Zombie Agent Protection)
409
442
  // This is set to true when an Agent-Controlled shell exits, essentially killing the "AI Agent"
410
443
  // associated with it from the UI perspective. It is reset when the user manually inputs a new message.
@@ -2268,7 +2301,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2268
2301
  return;
2269
2302
  }
2270
2303
  }, { isActive: !state.isInteractiveEditorMode });
2271
- const handleSubmit = useCallback(async (value, clipboardImages) => {
2304
+ const handleSubmit = useCallback(async (value, clipboardFiles) => {
2272
2305
  // Trim the value to remove any leading/trailing whitespace or newlines
2273
2306
  const trimmedValue = value.trim();
2274
2307
  if (!trimmedValue)
@@ -2351,69 +2384,37 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2351
2384
  });
2352
2385
  }
2353
2386
  else if (!isSlashCommand) {
2354
- // Only add user message to display if it's not a slash command
2355
- // INTERRUPT HANDLING: If AI is currently working, cancel and clean up first
2356
- // This allows the new message to replace the interrupted turn
2357
- if (state.isAiWorking || state.isLoading) {
2358
- // Cancel current request (cli-adapter will clean up its history)
2359
- onCancelRequest();
2360
- // Small delay to allow abort to propagate before new request starts
2361
- await new Promise(resolve => setTimeout(resolve, 50));
2362
- // Clean up UI state: remove any partial/interrupted messages
2363
- setState(prev => {
2364
- // Filter out the last user message (the one we're replacing)
2365
- let cleanedHistory = [...prev.messageHistory];
2366
- for (let i = cleanedHistory.length - 1; i >= 0; i--) {
2367
- if (cleanedHistory[i].role === 'user') {
2368
- cleanedHistory.splice(i, 1);
2369
- break; // Only remove the most recent user message
2370
- }
2371
- }
2372
- return {
2373
- ...prev,
2374
- messageHistory: cleanedHistory,
2375
- currentMessage: null,
2376
- isLoading: false,
2377
- isAiWorking: false,
2378
- approvalRequest: undefined, // Clear any pending approval
2379
- };
2380
- });
2381
- }
2382
- // Show user message IMMEDIATELY with image breadcrumbs (uploading state)
2383
- // Images will be uploaded in background, then AI will be called
2384
- const hasImagesToUpload = clipboardImages && clipboardImages.length > 0;
2385
- // Build initial content with placeholder image markers for immediate display
2387
+ // Only add user message to display if it's not a slash command.
2388
+ // The user message is added to messageHistory via the onQueuedMessageDispatched
2389
+ // callback (fired by cli-adapter at the very start of processing, for both direct
2390
+ // messages and queued interrupts). This ensures correct ordering and no duplicates.
2391
+ // Show user message IMMEDIATELY with file breadcrumbs (uploading state)
2392
+ // Files will be uploaded in background, then AI will be called
2393
+ const hasFilesToUpload = clipboardFiles && clipboardFiles.length > 0;
2394
+ // Build initial content with placeholder file markers for immediate display
2386
2395
  let initialDisplayContent = trimmedValue;
2387
- if (hasImagesToUpload) {
2396
+ if (hasFilesToUpload) {
2388
2397
  // Add placeholder markers that will be shown immediately
2389
- for (let i = 0; i < clipboardImages.length; i++) {
2390
- const image = clipboardImages[i];
2398
+ for (let i = 0; i < clipboardFiles.length; i++) {
2399
+ const file = clipboardFiles[i];
2391
2400
  // Use a placeholder format that MessageDisplay can render as breadcrumb
2392
- // Format: [IMAGE: filename (uploading...)] - will be updated after upload
2393
- initialDisplayContent = `${initialDisplayContent} [IMAGE: ${image.displayName} (gs://uploading)]`;
2401
+ // Format: [FILE: filename (uploading...)] - will be updated after upload
2402
+ const markerType = file.mimeType?.startsWith('image/') ? 'IMAGE' : 'FILE';
2403
+ initialDisplayContent = `${initialDisplayContent} [${markerType}: ${file.displayName} (gs://uploading)]`;
2394
2404
  }
2395
2405
  }
2396
2406
  // Generate a stable message ID that we can use to update the message later
2397
2407
  const userMessageId = `user-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
2398
- setState(prev => {
2399
- const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
2400
- ? [...prev.messageHistory, prev.currentMessage]
2401
- : prev.messageHistory;
2402
- const userMessage = {
2403
- id: userMessageId,
2404
- role: 'user',
2405
- content: initialDisplayContent,
2406
- timestamp: new Date()
2407
- };
2408
- return {
2409
- ...prev,
2410
- messageHistory: [...newHistory, userMessage],
2411
- currentMessage: null,
2412
- isLoading: true,
2413
- loadingMessage: hasImagesToUpload ? 'Uploading image(s)...' : 'Processing your request...',
2414
- isAiWorking: true
2415
- };
2416
- });
2408
+ // Update loading state immediately so the UI shows "processing" feedback.
2409
+ // Note: we do NOT add to messageHistory here — that is handled by the
2410
+ // onQueuedMessageDispatched callback which fires synchronously inside
2411
+ // cli-adapter.handleMessage before AI processing begins.
2412
+ setState(prev => ({
2413
+ ...prev,
2414
+ isLoading: true,
2415
+ loadingMessage: hasFilesToUpload ? 'Uploading file(s)...' : 'Processing your request...',
2416
+ isAiWorking: true
2417
+ }));
2417
2418
  }
2418
2419
  else {
2419
2420
  // For slash commands, just show loading state
@@ -2432,12 +2433,12 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2432
2433
  : [...prev.commandHistory, trimmedValue]
2433
2434
  }));
2434
2435
  try {
2435
- // Upload clipboard images if present
2436
- let messageWithImages = trimmedValue;
2437
- // Debug logging for clipboard images
2438
- logDebug(`[CLIPBOARD] clipboardImages count: ${clipboardImages?.length || 0}`);
2436
+ // Upload clipboard files if present
2437
+ let messageWithFiles = trimmedValue;
2438
+ // Debug logging for clipboard files
2439
+ logDebug(`[CLIPBOARD] clipboardFiles count: ${clipboardFiles?.length || 0}`);
2439
2440
  logDebug(`[CLIPBOARD] currentChatId: ${state.currentChatId || 'null'}`);
2440
- if (clipboardImages && clipboardImages.length > 0) {
2441
+ if (clipboardFiles && clipboardFiles.length > 0) {
2441
2442
  // Check for existing conversation ID - conversationManager is the single source of truth
2442
2443
  // This ensures we use the same conversation that cli-adapter uses
2443
2444
  let conversationId = conversationManager.getCurrentConversationId() || state.currentChatId;
@@ -2446,7 +2447,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2446
2447
  if (!conversationId) {
2447
2448
  // No conversation exists anywhere - create one via conversationManager
2448
2449
  // This will set the internal state that cli-adapter's ensureConversationStarted checks
2449
- logDebug(`[CLIPBOARD] No existing conversation - creating new one for image upload`);
2450
+ logDebug(`[CLIPBOARD] No existing conversation - creating new one for file upload`);
2450
2451
  try {
2451
2452
  const newConversation = await conversationManager.startNewConversation('New Chat', state.currentModel || 'gemini-2-flash-preview', 'google', undefined // workingDirectory - will use process.cwd()
2452
2453
  );
@@ -2461,38 +2462,40 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2461
2462
  logDebug(`[CLIPBOARD] Proceeding with upload`);
2462
2463
  }
2463
2464
  catch (createError) {
2464
- logError('Failed to create conversation for image upload', createError instanceof Error ? createError : undefined);
2465
+ logError('Failed to create conversation for file upload', createError instanceof Error ? createError : undefined);
2465
2466
  // Mark the upload as failed - don't use temp IDs that will fail FK constraint
2466
- for (let i = 0; i < clipboardImages.length; i++) {
2467
- messageWithImages = messageWithImages.replace(/#image\b/i, '[IMAGE UPLOAD FAILED - No conversation]');
2467
+ for (let i = 0; i < clipboardFiles.length; i++) {
2468
+ messageWithFiles = messageWithFiles.replace(/#image\b/i, '[FILE UPLOAD FAILED - No conversation]');
2468
2469
  }
2469
2470
  logDebug(`[CLIPBOARD] Skipping upload due to conversation creation failure`);
2470
2471
  }
2471
2472
  }
2472
2473
  // Only attempt upload if we have a valid conversation ID
2473
2474
  if (conversationId && !conversationId.startsWith('temp_')) {
2474
- logDebug(`[CLIPBOARD] Starting upload for ${clipboardImages.length} images with conversationId: ${conversationId}`);
2475
- // Upload each clipboard image and build the final message content
2476
- const uploadedImageMarkers = [];
2477
- for (const image of clipboardImages) {
2475
+ logDebug(`[CLIPBOARD] Starting upload for ${clipboardFiles.length} files with conversationId: ${conversationId}`);
2476
+ // Upload each clipboard file and build the final message content
2477
+ const uploadedFileMarkers = [];
2478
+ for (const file of clipboardFiles) {
2478
2479
  try {
2479
- logDebug(`[CLIPBOARD] Uploading image: ${image.displayName}, size: ${image.sizeBytes} bytes`);
2480
- const uploadResult = await apiClient.uploadFile(conversationId, image.displayName, image.mimeType, image.base64Data);
2480
+ logDebug(`[CLIPBOARD] Uploading file: ${file.displayName}, size: ${file.sizeBytes} bytes`);
2481
+ const uploadResult = await apiClient.uploadFile(conversationId, file.displayName, file.mimeType, file.base64Data);
2481
2482
  logDebug(`[CLIPBOARD] Upload success! gcsUri: ${uploadResult.gcsUri || 'none'}`);
2482
- // Append image reference info to message (for AI to know an image was attached)
2483
+ const markerType = file.mimeType?.startsWith('image/') ? 'IMAGE' : 'FILE';
2484
+ // Append file reference info to message (for AI to know a file was attached)
2483
2485
  // The backend will use gcsUri for Vertex AI multimodal input
2484
2486
  if (uploadResult.gcsUri) {
2485
- // Build the real image marker with actual GCS URI
2486
- messageWithImages = `${messageWithImages} [IMAGE: ${uploadResult.fileName} (${uploadResult.gcsUri})]`;
2487
- uploadedImageMarkers.push(`[IMAGE: ${uploadResult.fileName} (${uploadResult.gcsUri})]`);
2488
- logDebug(`[CLIPBOARD] Appended image marker: [IMAGE: ${uploadResult.fileName} (${uploadResult.gcsUri})]`);
2487
+ // Build the real file marker with actual GCS URI
2488
+ messageWithFiles = `${messageWithFiles} [${markerType}: ${uploadResult.fileName} (${uploadResult.gcsUri})]`;
2489
+ uploadedFileMarkers.push(`[${markerType}: ${uploadResult.fileName} (${uploadResult.gcsUri})]`);
2490
+ logDebug(`[CLIPBOARD] Appended file marker: [${markerType}: ${uploadResult.fileName} (${uploadResult.gcsUri})]`);
2489
2491
  }
2490
2492
  }
2491
2493
  catch (uploadError) {
2492
- // Log error but continue - image upload is optional
2493
- logError('Failed to upload clipboard image', uploadError instanceof Error ? uploadError : undefined);
2494
+ // Log error but continue - file upload is optional
2495
+ const markerType = file.mimeType?.startsWith('image/') ? 'IMAGE' : 'FILE';
2496
+ logError('Failed to upload clipboard file', uploadError instanceof Error ? uploadError : undefined);
2494
2497
  // Add a failed marker so user knows upload failed
2495
- uploadedImageMarkers.push(`[IMAGE: ${image.displayName} (upload failed)]`);
2498
+ uploadedFileMarkers.push(`[${markerType}: ${file.displayName} (upload failed)]`);
2496
2499
  }
2497
2500
  }
2498
2501
  // Update the user message in history with actual GCS URIs
@@ -2501,9 +2504,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2501
2504
  const updatedHistory = prev.messageHistory.map(msg => {
2502
2505
  // Find the user message we just added (it has placeholder markers)
2503
2506
  if (msg.role === 'user' && msg.content.includes('(gs://uploading)')) {
2504
- // Build new content: original text + real image markers
2507
+ // Build new content: original text + real file markers
2505
2508
  let newContent = trimmedValue;
2506
- for (const marker of uploadedImageMarkers) {
2509
+ for (const marker of uploadedFileMarkers) {
2507
2510
  newContent = `${newContent} ${marker}`;
2508
2511
  }
2509
2512
  return { ...msg, content: newContent };
@@ -2527,8 +2530,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2527
2530
  return { ...prev, messageHistory: updatedHistory };
2528
2531
  });
2529
2532
  } // End: if (conversationId && !startsWith temp_)
2530
- } // End: if (clipboardImages && clipboardImages.length > 0)
2531
- await onMessage(messageWithImages);
2533
+ } // End: if (clipboardFiles && clipboardFiles.length > 0)
2534
+ await onMessage(messageWithFiles);
2532
2535
  }
2533
2536
  catch (error) {
2534
2537
  // Add error message
@@ -2772,6 +2775,13 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2772
2775
  !(state.currentMessage?.toolExecution?.status === 'executing') && (React.createElement(Box, { marginBottom: 1, paddingLeft: 1 },
2773
2776
  React.createElement(LoadingIndicator, { key: "loading-indicator" }),
2774
2777
  React.createElement(AgentTimer, { key: "agent-timer" }))),
2778
+ state.interruptQueue && state.interruptQueue.length > 0 && (React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, state.interruptQueue.map((msg, i) => (React.createElement(Text, { key: `interrupt-${i}`, color: "gray", dimColor: true },
2779
+ "[Queued Interrupt ",
2780
+ i + 1,
2781
+ "/",
2782
+ state.interruptQueue.length,
2783
+ "] ",
2784
+ msg))))),
2775
2785
  !state.shellState?.isFocused && (state.passwordRequest ? (React.createElement(PasswordPrompt, { message: state.passwordRequest.message, onSubmit: (password) => {
2776
2786
  const resolve = state.passwordRequest?.resolve;
2777
2787
  if (resolve) {