centaurus-cli 2.9.0 → 2.9.2

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 (148) hide show
  1. package/dist/cli-adapter.d.ts +78 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +566 -165
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/build-config.d.ts +1 -1
  6. package/dist/config/build-config.js +1 -1
  7. package/dist/config/mcp-config-manager.d.ts +21 -0
  8. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  9. package/dist/config/mcp-config-manager.js +184 -1
  10. package/dist/config/mcp-config-manager.js.map +1 -1
  11. package/dist/config/models.d.ts +1 -0
  12. package/dist/config/models.d.ts.map +1 -1
  13. package/dist/config/models.js +7 -2
  14. package/dist/config/models.js.map +1 -1
  15. package/dist/config/slash-commands.d.ts.map +1 -1
  16. package/dist/config/slash-commands.js +5 -3
  17. package/dist/config/slash-commands.js.map +1 -1
  18. package/dist/index.js +66 -11
  19. package/dist/index.js.map +1 -1
  20. package/dist/mcp/mcp-command-handler.d.ts +34 -3
  21. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  22. package/dist/mcp/mcp-command-handler.js +171 -83
  23. package/dist/mcp/mcp-command-handler.js.map +1 -1
  24. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  25. package/dist/mcp/mcp-server-manager.js +9 -23
  26. package/dist/mcp/mcp-server-manager.js.map +1 -1
  27. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  28. package/dist/mcp/mcp-tool-wrapper.js +42 -5
  29. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  30. package/dist/services/ai-service-client.d.ts +6 -1
  31. package/dist/services/ai-service-client.d.ts.map +1 -1
  32. package/dist/services/ai-service-client.js +6 -6
  33. package/dist/services/ai-service-client.js.map +1 -1
  34. package/dist/services/api-client.d.ts +20 -0
  35. package/dist/services/api-client.d.ts.map +1 -1
  36. package/dist/services/api-client.js +35 -0
  37. package/dist/services/api-client.js.map +1 -1
  38. package/dist/services/input-detection-agent.d.ts +40 -0
  39. package/dist/services/input-detection-agent.d.ts.map +1 -0
  40. package/dist/services/input-detection-agent.js +213 -0
  41. package/dist/services/input-detection-agent.js.map +1 -0
  42. package/dist/services/input-requirement-detector.d.ts +28 -0
  43. package/dist/services/input-requirement-detector.d.ts.map +1 -0
  44. package/dist/services/input-requirement-detector.js +203 -0
  45. package/dist/services/input-requirement-detector.js.map +1 -0
  46. package/dist/services/monitored-shell-manager.d.ts +120 -0
  47. package/dist/services/monitored-shell-manager.d.ts.map +1 -0
  48. package/dist/services/monitored-shell-manager.js +239 -0
  49. package/dist/services/monitored-shell-manager.js.map +1 -0
  50. package/dist/services/session-quota-manager.d.ts +101 -0
  51. package/dist/services/session-quota-manager.d.ts.map +1 -0
  52. package/dist/services/session-quota-manager.js +242 -0
  53. package/dist/services/session-quota-manager.js.map +1 -0
  54. package/dist/services/shell-input-agent.d.ts +89 -0
  55. package/dist/services/shell-input-agent.d.ts.map +1 -0
  56. package/dist/services/shell-input-agent.js +361 -0
  57. package/dist/services/shell-input-agent.js.map +1 -0
  58. package/dist/services/sub-agent-manager.d.ts +139 -0
  59. package/dist/services/sub-agent-manager.d.ts.map +1 -0
  60. package/dist/services/sub-agent-manager.js +517 -0
  61. package/dist/services/sub-agent-manager.js.map +1 -0
  62. package/dist/tools/background-command.d.ts.map +1 -1
  63. package/dist/tools/background-command.js +33 -13
  64. package/dist/tools/background-command.js.map +1 -1
  65. package/dist/tools/command.d.ts.map +1 -1
  66. package/dist/tools/command.js +78 -4
  67. package/dist/tools/command.js.map +1 -1
  68. package/dist/tools/file-ops.d.ts.map +1 -1
  69. package/dist/tools/file-ops.js +33 -19
  70. package/dist/tools/file-ops.js.map +1 -1
  71. package/dist/tools/get-diff.d.ts.map +1 -1
  72. package/dist/tools/get-diff.js +5 -2
  73. package/dist/tools/get-diff.js.map +1 -1
  74. package/dist/tools/grep-search.d.ts.map +1 -1
  75. package/dist/tools/grep-search.js +41 -15
  76. package/dist/tools/grep-search.js.map +1 -1
  77. package/dist/tools/plan-mode.js +3 -3
  78. package/dist/tools/plan-mode.js.map +1 -1
  79. package/dist/tools/registry.js +1 -1
  80. package/dist/tools/registry.js.map +1 -1
  81. package/dist/tools/sub-agent.d.ts +9 -0
  82. package/dist/tools/sub-agent.d.ts.map +1 -0
  83. package/dist/tools/sub-agent.js +232 -0
  84. package/dist/tools/sub-agent.js.map +1 -0
  85. package/dist/tools/task-complete.d.ts.map +1 -1
  86. package/dist/tools/task-complete.js +14 -32
  87. package/dist/tools/task-complete.js.map +1 -1
  88. package/dist/ui/components/App.d.ts +45 -0
  89. package/dist/ui/components/App.d.ts.map +1 -1
  90. package/dist/ui/components/App.js +598 -95
  91. package/dist/ui/components/App.js.map +1 -1
  92. package/dist/ui/components/CircularSelectInput.d.ts +24 -0
  93. package/dist/ui/components/CircularSelectInput.d.ts.map +1 -0
  94. package/dist/ui/components/CircularSelectInput.js +71 -0
  95. package/dist/ui/components/CircularSelectInput.js.map +1 -0
  96. package/dist/ui/components/ErrorBoundary.d.ts +3 -2
  97. package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
  98. package/dist/ui/components/ErrorBoundary.js +29 -1
  99. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  100. package/dist/ui/components/InputBox.d.ts +4 -0
  101. package/dist/ui/components/InputBox.d.ts.map +1 -1
  102. package/dist/ui/components/InputBox.js +40 -2
  103. package/dist/ui/components/InputBox.js.map +1 -1
  104. package/dist/ui/components/InteractiveShell.d.ts +6 -0
  105. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  106. package/dist/ui/components/InteractiveShell.js +57 -6
  107. package/dist/ui/components/InteractiveShell.js.map +1 -1
  108. package/dist/ui/components/MCPAddScreen.d.ts +13 -0
  109. package/dist/ui/components/MCPAddScreen.d.ts.map +1 -0
  110. package/dist/ui/components/MCPAddScreen.js +54 -0
  111. package/dist/ui/components/MCPAddScreen.js.map +1 -0
  112. package/dist/ui/components/MCPListScreen.d.ts +17 -0
  113. package/dist/ui/components/MCPListScreen.d.ts.map +1 -0
  114. package/dist/ui/components/MCPListScreen.js +50 -0
  115. package/dist/ui/components/MCPListScreen.js.map +1 -0
  116. package/dist/ui/components/MCPServerListScreen.d.ts +16 -0
  117. package/dist/ui/components/MCPServerListScreen.d.ts.map +1 -0
  118. package/dist/ui/components/MCPServerListScreen.js +59 -0
  119. package/dist/ui/components/MCPServerListScreen.js.map +1 -0
  120. package/dist/ui/components/MonitorModeAIPanel.d.ts +23 -0
  121. package/dist/ui/components/MonitorModeAIPanel.d.ts.map +1 -0
  122. package/dist/ui/components/MonitorModeAIPanel.js +69 -0
  123. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -0
  124. package/dist/ui/components/MultiLineInput.d.ts +13 -0
  125. package/dist/ui/components/MultiLineInput.d.ts.map +1 -0
  126. package/dist/ui/components/MultiLineInput.js +223 -0
  127. package/dist/ui/components/MultiLineInput.js.map +1 -0
  128. package/dist/ui/components/StatusBar.d.ts +2 -0
  129. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  130. package/dist/ui/components/StatusBar.js +33 -2
  131. package/dist/ui/components/StatusBar.js.map +1 -1
  132. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  133. package/dist/ui/components/ToolExecutionMessage.js +271 -12
  134. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  135. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  136. package/dist/ui/components/VersionUpdatePrompt.js +3 -2
  137. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  138. package/dist/utils/editor-utils.d.ts +3 -3
  139. package/dist/utils/editor-utils.d.ts.map +1 -1
  140. package/dist/utils/editor-utils.js +15 -12
  141. package/dist/utils/editor-utils.js.map +1 -1
  142. package/dist/utils/input-classifier.d.ts.map +1 -1
  143. package/dist/utils/input-classifier.js +1 -0
  144. package/dist/utils/input-classifier.js.map +1 -1
  145. package/dist/utils/terminal-output.d.ts.map +1 -1
  146. package/dist/utils/terminal-output.js +198 -171
  147. package/dist/utils/terminal-output.js.map +1 -1
  148. package/package.json +2 -1
@@ -1,6 +1,8 @@
1
1
  import React, { useState, useCallback } from 'react';
2
2
  import { Box, Text, useApp, useInput, Static } from 'ink';
3
3
  import SelectInput from 'ink-select-input';
4
+ import { CircularSelectInput } from './CircularSelectInput.js';
5
+ import stripAnsi from 'strip-ansi';
4
6
  import TextInput from 'ink-text-input';
5
7
  import { quickLog } from '../../utils/conversation-logger.js';
6
8
  import { WelcomeBanner } from './WelcomeBanner.js';
@@ -22,8 +24,15 @@ import { runInteractiveEditor, runWSLEditor, runDockerEditor, runSSHEditor } fro
22
24
  import { DetailedPlanReviewScreen } from './DetailedPlanReviewScreen.js';
23
25
  import { TaskCompletedMessage } from './TaskCompletedMessage.js';
24
26
  import { PlanAcceptedMessage } from './PlanAcceptedMessage.js';
27
+ import { MCPAddScreen } from './MCPAddScreen.js';
28
+ import { MCPServerListScreen } from './MCPServerListScreen.js';
29
+ import { MCPListScreen } from './MCPListScreen.js';
25
30
  import { processTerminalOutput } from '../../utils/terminal-output.js';
26
31
  import { BackgroundTaskManager } from '../../services/background-task-manager.js';
32
+ import { MonitorModeAIPanel } from './MonitorModeAIPanel.js';
33
+ import { detectInputRequirement, extractPromptContext, getInputTypeDescription } from '../../services/input-requirement-detector.js';
34
+ import { ShellInputAgent } from '../../services/shell-input-agent.js';
35
+ import { InputDetectionAgent } from '../../services/input-detection-agent.js';
27
36
  import { useConnectivity } from '../../hooks/useConnectivity.js';
28
37
  import { apiClient } from '../../services/api-client.js';
29
38
  import { conversationManager } from '../../services/conversation-manager.js';
@@ -137,12 +146,18 @@ const RenameInputScreen = ({ currentTitle, onRename, onCancel }) => {
137
146
  React.createElement(Box, { marginTop: 1 },
138
147
  React.createElement(Text, { dimColor: true }, "Press Enter to save, ESC to cancel"))));
139
148
  };
140
- export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode, onResponseReceived, onDirectMessage, onResponseStream, onThoughtStream, onThoughtComplete, onPickerSetup, onPickerSelection, onToolExecutionUpdate, onToolApprovalRequest, onToolStreamingOutput, onPlanModeChange, onPlanApprovalRequest, onPlanCreated, onTaskCompleted, onCommandModeChange, onToggleCommandMode, onBackgroundModeChange, onToggleBackgroundMode, onBackgroundTaskCountChange, onSetAutoModeSetup, onCwdChange, onModelChange, onSubshellContextChange, onPasswordRequest, onShellInput, onShellSignal, onKillProcess, onInteractiveEditorMode, onConnectionStatusUpdate, onTokenCountUpdate, onChatPickerSetup, onChatPickerSelection, onChatDeletePickerSetup, onChatDeletePickerSelection, onChatListSetup, onChatRenamePickerSetup, onChatRename, onRestoreMessagesSetup, onUIMessageHistoryUpdate, onBackgroundTaskListSetup, onBackgroundTaskSelection, onBackgroundTaskCancelSetup, onBackgroundTaskCancel, onBackgroundTaskViewSetup }) => {
149
+ 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 }) => {
141
150
  const { exit } = useApp();
151
+ // Calculate limit for paginated lists (75% of terminal height)
152
+ const listLimit = Math.max(5, Math.floor((process.stdout.rows || 24) * 0.75));
142
153
  const autoAcceptRef = React.useRef(false);
143
154
  const setAutoModeCallbackRef = React.useRef(null);
144
155
  // Connectivity State
145
156
  const isConnected = useConnectivity();
157
+ // Agent control mode refs for input detection
158
+ const lastOutputTimeRef = React.useRef(Date.now());
159
+ const inputDetectionPendingRef = React.useRef(false);
160
+ const detectionDebounceRef = React.useRef(null);
146
161
  // Helper to clear screen
147
162
  const clearScreen = useCallback(() => {
148
163
  process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
@@ -195,6 +210,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
195
210
  isAiWorking: false,
196
211
  currentTokens: 0,
197
212
  maxTokens: getMaxTokensForModel(initialModel || 'gemini-2.5-flash'),
213
+ contextLimitReached: false,
198
214
  shellState: undefined,
199
215
  isInteractiveEditorMode: false,
200
216
  pickerOptions: undefined,
@@ -203,6 +219,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
203
219
  passwordRequest: undefined,
204
220
  connectionStatus: undefined,
205
221
  backgroundTaskCount: 0,
222
+ subAgentCount: 0,
223
+ sessionQuotaExhausted: false,
224
+ sessionQuotaTimeRemaining: '',
206
225
  });
207
226
  // Track last terminal width to detect actual width changes
208
227
  const lastTerminalWidthRef = React.useRef(process.stdout.columns || 80);
@@ -268,6 +287,20 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
268
287
  }
269
288
  };
270
289
  }, [clearScreen]);
290
+ // Handle global keyboard shortcuts
291
+ useInput((input, key) => {
292
+ // Escape key handling for background task screens
293
+ if (key.escape) {
294
+ if (state.screen === 'background-task-list' || state.screen === 'background-task-cancel') {
295
+ clearScreen();
296
+ setState(prev => ({
297
+ ...prev,
298
+ screen: 'chat',
299
+ backgroundTasks: undefined // Clear the list
300
+ }));
301
+ }
302
+ }
303
+ });
271
304
  // Check for version updates on mount
272
305
  React.useEffect(() => {
273
306
  checkForUpdates().then(versionInfo => {
@@ -321,6 +354,17 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
321
354
  React.useEffect(() => {
322
355
  autoAcceptRef.current = state.autoAcceptMode;
323
356
  }, [state.autoAcceptMode]);
357
+ // Push shell output updates to InputDetectionAgent when agent control is active
358
+ // This ensures the detection agent always has the latest output for polling
359
+ React.useEffect(() => {
360
+ if (state.shellState?.isAgentControlled && state.shellState?.shellId && state.shellState?.output) {
361
+ InputDetectionAgent.updateOutput(state.shellState.shellId, state.shellState.output);
362
+ }
363
+ }, [state.shellState?.output, state.shellState?.isAgentControlled, state.shellState?.shellId]);
364
+ // Ref to track if we should strictly block incoming AI updates (Zombie Agent Protection)
365
+ // This is set to true when an Agent-Controlled shell exits, essentially killing the "AI Agent"
366
+ // associated with it from the UI perspective. It is reset when the user manually inputs a new message.
367
+ const ignoreIncomingAIUpdatesRef = React.useRef(false);
324
368
  // NOTE: Token count is now updated via onTokenCountUpdate callback from cli-adapter
325
369
  // which tracks the actual AI conversation history including system prompt.
326
370
  // The old UI-based calculation has been removed to avoid overwriting correct values.
@@ -333,6 +377,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
333
377
  // Set up callback to receive streaming chunks - only once on mount
334
378
  React.useEffect(() => {
335
379
  onResponseStream((chunk) => {
380
+ // CIRCUIT BREAKER: Block incoming messages from "Zombie Agents"
381
+ if (ignoreIncomingAIUpdatesRef.current)
382
+ return;
336
383
  isStreamingRef.current = true; // Mark that we're streaming
337
384
  // Check if terminal is large enough for streaming
338
385
  const dimensions = getTerminalDimensions();
@@ -406,11 +453,39 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
406
453
  });
407
454
  });
408
455
  }, [onResponseStream]);
456
+ // Set up callback to clear streamed response when task_complete has a summary
457
+ // This prevents showing both the streamed text AND the task_complete summary
458
+ React.useEffect(() => {
459
+ onClearStreamedResponse(() => {
460
+ try {
461
+ quickLog(`[${new Date().toISOString()}] [App] onClearStreamedResponse called - clearing current message content\n`);
462
+ }
463
+ catch (e) {
464
+ // Ignore logging errors
465
+ }
466
+ setState(prev => {
467
+ // Clear the current message's content (not the whole message, so thoughts and thinking are preserved)
468
+ if (prev.currentMessage && prev.currentMessage.role === 'assistant') {
469
+ return {
470
+ ...prev,
471
+ currentMessage: {
472
+ ...prev.currentMessage,
473
+ content: '' // Clear the content - task_complete will stream the summary next
474
+ }
475
+ };
476
+ }
477
+ return prev;
478
+ });
479
+ });
480
+ }, [onClearStreamedResponse]);
409
481
  // Ref to accumulate all thought text across multiple chunks
410
482
  const thoughtAccumulatorRef = React.useRef('');
411
483
  // Set up callback to receive thought chunks
412
484
  React.useEffect(() => {
413
485
  onThoughtStream((thought) => {
486
+ // CIRCUIT BREAKER: Block incoming thoughts from "Zombie Agents"
487
+ if (ignoreIncomingAIUpdatesRef.current)
488
+ return;
414
489
  // Debug logging to file
415
490
  try {
416
491
  quickLog(`[${new Date().toISOString()}] [App] Received thought chunk (${thought.length} chars): ${thought.substring(0, 100)}\n`);
@@ -502,6 +577,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
502
577
  // Set up callback for when thinking completes
503
578
  React.useEffect(() => {
504
579
  onThoughtComplete((durationSeconds) => {
580
+ // CIRCUIT BREAKER: Block incoming thought completion from "Zombie Agents"
581
+ if (ignoreIncomingAIUpdatesRef.current)
582
+ return;
505
583
  try {
506
584
  quickLog(`[${new Date().toISOString()}] [App] onThoughtComplete called with ${durationSeconds}s\n`);
507
585
  }
@@ -538,12 +616,28 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
538
616
  });
539
617
  });
540
618
  }, [onThoughtComplete]);
619
+ // Helper function to check if a message should be skipped (thought-only with no content or tools)
620
+ const shouldSkipMessage = (message) => {
621
+ if (!message)
622
+ return false;
623
+ // Skip if it's an assistant message with only thinking duration and no content or tool execution
624
+ if (message.role === 'assistant' &&
625
+ message.thinkingDuration !== undefined &&
626
+ message.content.trim() === '' &&
627
+ !message.toolExecution) {
628
+ return true;
629
+ }
630
+ return false;
631
+ };
541
632
  // Set up callback for direct messages (slash commands) - add directly to history
542
633
  React.useEffect(() => {
543
634
  onDirectMessage((message) => {
635
+ // CIRCUIT BREAKER: Block incoming direct messages from "Zombie Agents"
636
+ if (ignoreIncomingAIUpdatesRef.current)
637
+ return;
544
638
  setState(prev => {
545
- // Move current message to history if exists
546
- const newHistory = prev.currentMessage
639
+ // Move current message to history if exists (skip thought-only messages)
640
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
547
641
  ? [...prev.messageHistory, prev.currentMessage]
548
642
  : prev.messageHistory;
549
643
  // Create system message for slash command response
@@ -567,6 +661,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
567
661
  // Set up callback to receive complete responses (for slash commands and non-streaming responses)
568
662
  React.useEffect(() => {
569
663
  onResponseReceived((message) => {
664
+ // CIRCUIT BREAKER: Block incoming responses from "Zombie Agents"
665
+ if (ignoreIncomingAIUpdatesRef.current)
666
+ return;
570
667
  // If we were streaming, move the current message to history immediately
571
668
  if (isStreamingRef.current) {
572
669
  isStreamingRef.current = false; // Reset for next message
@@ -675,8 +772,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
675
772
  timestamp: now,
676
773
  shouldStream: false // Command responses should appear instantly
677
774
  };
678
- // If we have a current message that's an assistant message, move it to history
679
- if (prev.currentMessage && prev.currentMessage.role === 'assistant') {
775
+ // If we have a current message that's an assistant message, move it to history (skip thought-only)
776
+ if (prev.currentMessage && prev.currentMessage.role === 'assistant' && !shouldSkipMessage(prev.currentMessage)) {
680
777
  return {
681
778
  ...prev,
682
779
  messageHistory: [...prev.messageHistory, prev.currentMessage],
@@ -779,6 +876,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
779
876
  }));
780
877
  });
781
878
  }, []); // Empty dependency array - only register once
879
+ // Set up callback for sub-agent count change
880
+ React.useEffect(() => {
881
+ onSubAgentCountChange((count) => {
882
+ setState(prev => ({
883
+ ...prev,
884
+ subAgentCount: count
885
+ }));
886
+ });
887
+ }, []); // Empty dependency array - only register once
782
888
  // Set up callback for setting Auto mode (used after background task starts)
783
889
  React.useEffect(() => {
784
890
  onSetAutoModeSetup((enabled) => {
@@ -916,6 +1022,65 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
916
1022
  });
917
1023
  });
918
1024
  }, []); // Empty dependency array - only register once
1025
+ // Set up callback for MCP Add screen
1026
+ React.useEffect(() => {
1027
+ onMCPAddScreenSetup(() => {
1028
+ clearScreen();
1029
+ setState(prev => ({
1030
+ ...prev,
1031
+ screen: 'mcp-add',
1032
+ isLoading: false
1033
+ }));
1034
+ });
1035
+ }, []); // Empty dependency array - only register once
1036
+ // Set up callback for MCP Remove screen
1037
+ React.useEffect(() => {
1038
+ onMCPRemoveScreenSetup((servers) => {
1039
+ clearScreen();
1040
+ setState(prev => ({
1041
+ ...prev,
1042
+ screen: 'mcp-remove',
1043
+ mcpServers: servers,
1044
+ isLoading: false
1045
+ }));
1046
+ });
1047
+ }, []); // Empty dependency array - only register once
1048
+ // Set up callback for MCP Enable screen
1049
+ React.useEffect(() => {
1050
+ onMCPEnableScreenSetup((servers) => {
1051
+ clearScreen();
1052
+ setState(prev => ({
1053
+ ...prev,
1054
+ screen: 'mcp-enable',
1055
+ mcpServers: servers,
1056
+ isLoading: false
1057
+ }));
1058
+ });
1059
+ }, []); // Empty dependency array - only register once
1060
+ // Set up callback for MCP Disable screen
1061
+ React.useEffect(() => {
1062
+ onMCPDisableScreenSetup((servers) => {
1063
+ clearScreen();
1064
+ setState(prev => ({
1065
+ ...prev,
1066
+ screen: 'mcp-disable',
1067
+ mcpServers: servers,
1068
+ isLoading: false
1069
+ }));
1070
+ });
1071
+ }, []); // Empty dependency array - only register once
1072
+ // Set up callback for MCP List screen
1073
+ React.useEffect(() => {
1074
+ onMCPListScreenSetup((servers) => {
1075
+ clearScreen();
1076
+ setState(prev => ({
1077
+ ...prev,
1078
+ screen: 'mcp-list',
1079
+ mcpListData: servers,
1080
+ isLoading: false
1081
+ }));
1082
+ });
1083
+ }, []); // Empty dependency array - only register once
919
1084
  // Sync UI message history to CLI adapter whenever it changes
920
1085
  React.useEffect(() => {
921
1086
  onUIMessageHistoryUpdate(state.messageHistory);
@@ -923,6 +1088,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
923
1088
  // Set up callback for tool execution updates
924
1089
  React.useEffect(() => {
925
1090
  onToolExecutionUpdate((update) => {
1091
+ // CIRCUIT BREAKER: Block incoming tool updates from "Zombie Agents"
1092
+ if (ignoreIncomingAIUpdatesRef.current)
1093
+ return;
926
1094
  // Special handling for execute_command
927
1095
  if (update.toolName === 'execute_command') {
928
1096
  try {
@@ -931,6 +1099,28 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
931
1099
  catch (e) { }
932
1100
  // STARTING EXECUTION
933
1101
  if (update.status === 'executing') {
1102
+ // Special handling for shell_input - DO NOT overwrite shell state
1103
+ // The agent is sending input to an EXISTING shell, so we must preserve the current shell state
1104
+ if (update.arguments?.shell_input) {
1105
+ setState(prev => ({
1106
+ ...prev,
1107
+ // Move current message to history if needed
1108
+ messageHistory: prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1109
+ ? [...prev.messageHistory, prev.currentMessage]
1110
+ : prev.messageHistory,
1111
+ // Update current message to show the tool execution
1112
+ currentMessage: {
1113
+ id: `tool-execute_command-${Date.now()}`,
1114
+ role: 'tool',
1115
+ content: '',
1116
+ timestamp: new Date(),
1117
+ toolExecution: { ...update }
1118
+ },
1119
+ isLoading: false,
1120
+ isAiWorking: true
1121
+ }));
1122
+ return;
1123
+ }
934
1124
  const command = update.arguments?.command || update.arguments?.CommandLine || update.arguments?.commandLine || '';
935
1125
  // Start a pending shell with a short delay
936
1126
  // This prevents the UI from flickering for very fast commands (like ls)
@@ -946,6 +1136,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
946
1136
  setState(prev => ({
947
1137
  ...prev,
948
1138
  shellState: {
1139
+ shellId: `shell-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
949
1140
  command: cmd,
950
1141
  cwd: cwd,
951
1142
  output: bufferedOutput,
@@ -954,10 +1145,11 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
954
1145
  error: undefined,
955
1146
  isFocused: false,
956
1147
  isPty: update.arguments?.isPty || false,
957
- remoteContext: update.arguments?.remoteContext
1148
+ remoteContext: update.arguments?.remoteContext,
1149
+ isAgentStarted: true // Mark as agent-started since it came from execute_command tool
958
1150
  },
959
- // IMPORTANT: Always preserve current message by moving it to history first
960
- messageHistory: prev.currentMessage
1151
+ // IMPORTANT: Always preserve current message by moving it to history first (skip thought-only)
1152
+ messageHistory: prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
961
1153
  ? [...prev.messageHistory, prev.currentMessage]
962
1154
  : prev.messageHistory,
963
1155
  currentMessage: {
@@ -1013,8 +1205,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1013
1205
  arguments: update.arguments
1014
1206
  }
1015
1207
  };
1016
- // Move current message to history if exists
1017
- const newHistory = prev.currentMessage
1208
+ // Move current message to history if exists (skip thought-only)
1209
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1018
1210
  ? [...prev.messageHistory, prev.currentMessage]
1019
1211
  : prev.messageHistory;
1020
1212
  return {
@@ -1051,12 +1243,44 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1051
1243
  arguments: update.arguments
1052
1244
  }
1053
1245
  };
1246
+ // Check if this was just an input string sent to an existing shell
1247
+ const isShellInput = update.arguments?.shell_input;
1248
+ // Reset input detection pending flag so we can detect NEXT prompt
1249
+ if (update.status === 'completed' || update.status === 'error') {
1250
+ inputDetectionPendingRef.current = false;
1251
+ }
1252
+ // Check if we hit a dead shell error during agent control
1253
+ const isDeadShellError = update.status === 'error' && update.error?.includes('No active interactive shell');
1254
+ // STRICT ISOLATION TRIGGER: If agent-controlled shell is done (completed or dead), kill the agent loop
1255
+ // LOGIC SPLIT:
1256
+ // 1. Manual Shell (User started, then Alt+I): Strict Sub-Agent behavior. Kill IMMEDIATELY on exit/completion.
1257
+ // 2. Agent Shell (Agent started via execute_command): Main Agent behavior. Kill only on ERROR. Allow success pass-through.
1258
+ const isAgentStarted = prev.shellState?.isAgentStarted;
1259
+ const shouldKill = isAgentStarted ? isDeadShellError : (update.status === 'completed' || isDeadShellError);
1260
+ if (prev.shellState?.isAgentControlled && shouldKill) {
1261
+ try {
1262
+ quickLog(`[${new Date().toISOString()}] [App] Strict Isolation: Agent Controlled shell exited/died. Engaging circuit breaker. (AgentStarted: ${isAgentStarted})\n`);
1263
+ }
1264
+ catch (e) { }
1265
+ ignoreIncomingAIUpdatesRef.current = true;
1266
+ // Terminate the shell input agent session
1267
+ if (prev.shellState?.shellId) {
1268
+ ShellInputAgent.terminateSession(prev.shellState.shellId);
1269
+ }
1270
+ }
1054
1271
  return {
1055
1272
  ...prev,
1056
- shellState: undefined,
1273
+ // If it's just shell input, preserve the shell state UNLESS it's a dead shell error
1274
+ shellState: (isShellInput && !isDeadShellError) ? prev.shellState : undefined,
1057
1275
  messageHistory: [...prev.messageHistory, shellMessage],
1058
1276
  currentMessage: null,
1059
- isAiWorking: prev.isAiWorking // Preserve existing state
1277
+ // Determine if AI should keep working:
1278
+ // - Normal shell input: preserve state (keep thrusting if in chain)
1279
+ // - Agent-started shell completed successfully: preserve state (agent loop continues)
1280
+ // - Dead shell error or manual shell completed: STOP thrusting (reset to false)
1281
+ isAiWorking: (isShellInput && !isDeadShellError) ? prev.isAiWorking
1282
+ : (isAgentStarted && !shouldKill) ? prev.isAiWorking
1283
+ : false
1060
1284
  };
1061
1285
  });
1062
1286
  return;
@@ -1150,7 +1374,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1150
1374
  // Carry the thinkingDuration to the tool message, don't add empty message to history
1151
1375
  pendingThinkingDuration = prev.currentMessage.thinkingDuration;
1152
1376
  try {
1153
- quickLog(`[${new Date().toISOString()}] [App] Found thought-only message, saving thinkingDuration ${pendingThinkingDuration}s for tool ${update.toolName}\n`);
1377
+ quickLog(`[${new Date().toISOString()}] [App] Skipping thought-only message (no content), saving thinkingDuration ${pendingThinkingDuration}s for tool ${update.toolName}\n`);
1154
1378
  }
1155
1379
  catch (e) { }
1156
1380
  }
@@ -1182,7 +1406,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1182
1406
  }
1183
1407
  catch (e) { }
1184
1408
  let newHistory = prev.messageHistory;
1185
- if (prev.currentMessage) {
1409
+ if (prev.currentMessage && !shouldSkipMessage(prev.currentMessage)) {
1186
1410
  newHistory = [...prev.messageHistory, prev.currentMessage];
1187
1411
  }
1188
1412
  return {
@@ -1208,8 +1432,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1208
1432
  timestamp: new Date(),
1209
1433
  connectionStatus: status
1210
1434
  };
1211
- // Move current message to history if exists
1212
- const newHistory = prev.currentMessage
1435
+ // Move current message to history if exists (skip thought-only messages)
1436
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1213
1437
  ? [...prev.messageHistory, prev.currentMessage]
1214
1438
  : prev.messageHistory;
1215
1439
  return {
@@ -1238,8 +1462,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1238
1462
  currentMessage: null
1239
1463
  };
1240
1464
  }
1241
- // Otherwise just add to history
1242
- const newHistory = prev.currentMessage
1465
+ // Otherwise just add to history (skip thought-only messages)
1466
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1243
1467
  ? [...prev.messageHistory, prev.currentMessage, finalConnectionMessage]
1244
1468
  : [...prev.messageHistory, finalConnectionMessage];
1245
1469
  return {
@@ -1334,14 +1558,65 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1334
1558
  pendingShellRef.current.output += update.chunk;
1335
1559
  return;
1336
1560
  }
1561
+ // Update last output time for stall detection
1562
+ lastOutputTimeRef.current = Date.now();
1337
1563
  setState(prev => {
1338
1564
  // Update shell state if it's an execute_command
1339
1565
  if (update.toolName === 'execute_command' && prev.shellState) {
1566
+ const newOutput = prev.shellState.output + update.chunk;
1567
+ // If agent control is enabled, schedule input detection
1568
+ if (prev.shellState.isAgentControlled && prev.shellState.isRunning && !inputDetectionPendingRef.current) {
1569
+ // Clear previous debounce timer
1570
+ if (detectionDebounceRef.current) {
1571
+ clearTimeout(detectionDebounceRef.current);
1572
+ }
1573
+ // Debounce: wait 500ms after last output before checking for input requirement
1574
+ // This ensures we don't trigger prematurely during fast output
1575
+ detectionDebounceRef.current = setTimeout(() => {
1576
+ const timeSinceLastOutput = Date.now() - lastOutputTimeRef.current;
1577
+ const detection = detectInputRequirement(newOutput, timeSinceLastOutput, 1000);
1578
+ if (detection.requiresInput && detection.confidence !== 'low') {
1579
+ inputDetectionPendingRef.current = true;
1580
+ // Add monitoring message
1581
+ const promptContext = extractPromptContext(newOutput);
1582
+ const inputType = getInputTypeDescription(detection);
1583
+ setState(innerPrev => {
1584
+ if (!innerPrev.shellState)
1585
+ return innerPrev;
1586
+ const newMessage = {
1587
+ id: `msg-${Date.now()}`,
1588
+ type: 'thinking',
1589
+ content: `Detected input prompt: "${promptContext}" (${inputType})`,
1590
+ timestamp: new Date()
1591
+ };
1592
+ return {
1593
+ ...innerPrev,
1594
+ shellState: {
1595
+ ...innerPrev.shellState,
1596
+ monitoringMessages: [...(innerPrev.shellState.monitoringMessages || []), newMessage]
1597
+ }
1598
+ };
1599
+ });
1600
+ // Trigger AI re-prompting with shell context via isolated ShellInputAgent
1601
+ const shellId = prev.shellState?.shellId;
1602
+ if (shellId && ShellInputAgent.isSessionActive(shellId)) {
1603
+ ShellInputAgent.handleInputPrompt(shellId, newOutput, promptContext, inputType, detection.confidence);
1604
+ }
1605
+ else {
1606
+ quickLog(`[${new Date().toISOString()}] [Debounce] ShellInputAgent session not active! shellId=${shellId}\n`);
1607
+ }
1608
+ // Reset pending flag after a delay to allow for next detection
1609
+ setTimeout(() => {
1610
+ inputDetectionPendingRef.current = false;
1611
+ }, 3000);
1612
+ }
1613
+ }, 500);
1614
+ }
1340
1615
  return {
1341
1616
  ...prev,
1342
1617
  shellState: {
1343
1618
  ...prev.shellState,
1344
- output: prev.shellState.output + update.chunk
1619
+ output: newOutput
1345
1620
  }
1346
1621
  };
1347
1622
  }
@@ -1363,7 +1638,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1363
1638
  return prev;
1364
1639
  });
1365
1640
  });
1366
- }, []);
1641
+ }, [onMessage]);
1367
1642
  // Set up callback to receive plan mode changes
1368
1643
  React.useEffect(() => {
1369
1644
  onPlanModeChange((planMode) => {
@@ -1421,6 +1696,55 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1421
1696
  }));
1422
1697
  });
1423
1698
  }, [onTokenCountUpdate]);
1699
+ // Set up callback to receive context limit state from cli-adapter
1700
+ React.useEffect(() => {
1701
+ onContextLimitReached((reached) => {
1702
+ setState(prev => ({
1703
+ ...prev,
1704
+ contextLimitReached: reached
1705
+ }));
1706
+ });
1707
+ }, [onContextLimitReached]);
1708
+ // Set up callback to receive session quota updates from cli-adapter
1709
+ React.useEffect(() => {
1710
+ onSessionQuotaUpdate((remaining, canSend, timeRemaining) => {
1711
+ setState(prev => ({
1712
+ ...prev,
1713
+ sessionQuotaExhausted: !canSend,
1714
+ sessionQuotaTimeRemaining: timeRemaining
1715
+ }));
1716
+ });
1717
+ }, [onSessionQuotaUpdate]);
1718
+ // Set up callback for when AI answers a shell prompt (updates thinking message to show tick)
1719
+ React.useEffect(() => {
1720
+ onPromptAnswered((shellId) => {
1721
+ setState(prev => {
1722
+ if (!prev.shellState || prev.shellState.shellId !== shellId) {
1723
+ return prev;
1724
+ }
1725
+ // Find the last 'thinking' message that isn't already answered and mark it
1726
+ const messages = prev.shellState.monitoringMessages || [];
1727
+ let updated = false;
1728
+ const updatedMessages = [...messages].reverse().map((msg, idx) => {
1729
+ if (!updated && msg.type === 'thinking' && !msg.isAnswered) {
1730
+ updated = true;
1731
+ return { ...msg, isAnswered: true };
1732
+ }
1733
+ return msg;
1734
+ }).reverse();
1735
+ if (!updated) {
1736
+ return prev;
1737
+ }
1738
+ return {
1739
+ ...prev,
1740
+ shellState: {
1741
+ ...prev.shellState,
1742
+ monitoringMessages: updatedMessages
1743
+ }
1744
+ };
1745
+ });
1746
+ });
1747
+ }, [onPromptAnswered]);
1424
1748
  // Set up callback for plan approval requests
1425
1749
  React.useEffect(() => {
1426
1750
  onPlanApprovalRequest(async (plan) => {
@@ -1468,8 +1792,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1468
1792
  completionNote
1469
1793
  }
1470
1794
  };
1471
- // Move current message to history if exists
1472
- const newHistory = prev.currentMessage
1795
+ // Move current message to history if exists (skip thought-only)
1796
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1473
1797
  ? [...prev.messageHistory, prev.currentMessage, taskCompletedMessage]
1474
1798
  : [...prev.messageHistory, taskCompletedMessage];
1475
1799
  return {
@@ -1630,6 +1954,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1630
1954
  }));
1631
1955
  return;
1632
1956
  }
1957
+ // If in an MCP screen, return to chat
1958
+ if (state.screen === 'mcp-add' || state.screen === 'mcp-remove' || state.screen === 'mcp-enable' || state.screen === 'mcp-disable' || state.screen === 'mcp-list') {
1959
+ setState(prev => ({
1960
+ ...prev,
1961
+ screen: 'chat',
1962
+ mcpServers: undefined
1963
+ }));
1964
+ return;
1965
+ }
1633
1966
  // If AI is working, cancel the operation
1634
1967
  if (state.isLoading || state.isAiWorking) {
1635
1968
  // Cancel the current AI request
@@ -1774,8 +2107,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1774
2107
  // In command mode, add command to history immediately
1775
2108
  if (state.commandMode) {
1776
2109
  setState(prev => {
1777
- // Move current message to history if exists
1778
- const newHistory = prev.currentMessage
2110
+ // Move current message to history if exists (skip thought-only)
2111
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1779
2112
  ? [...prev.messageHistory, prev.currentMessage]
1780
2113
  : prev.messageHistory;
1781
2114
  const commandMessage = {
@@ -1813,7 +2146,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1813
2146
  // Generate a stable message ID that we can use to update the message later
1814
2147
  const userMessageId = `user-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
1815
2148
  setState(prev => {
1816
- const newHistory = prev.currentMessage
2149
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1817
2150
  ? [...prev.messageHistory, prev.currentMessage]
1818
2151
  : prev.messageHistory;
1819
2152
  const userMessage = {
@@ -1956,8 +2289,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1956
2289
  content: `❌ Error: ${error.message || 'Unknown error occurred'}`,
1957
2290
  timestamp: new Date()
1958
2291
  };
1959
- // Move current to history if exists and add error
1960
- const newHistory = prev.currentMessage
2292
+ // Move current to history if exists and add error (skip thought-only)
2293
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1961
2294
  ? [...prev.messageHistory, prev.currentMessage, errorMessage]
1962
2295
  : [...prev.messageHistory, errorMessage];
1963
2296
  return {
@@ -2042,7 +2375,55 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2042
2375
  return;
2043
2376
  }
2044
2377
  onShellSignal(signal);
2378
+ }, shellId: state.shellState.shellId, isAgentControlled: state.shellState.isAgentControlled, onToggleAgentControl: () => {
2379
+ // Toggle agent control mode
2380
+ const wasAgentControlled = state.shellState?.isAgentControlled;
2381
+ const willBeAgentControlled = !wasAgentControlled;
2382
+ const currentOutput = state.shellState?.output || '';
2383
+ const isShellRunning = state.shellState?.isRunning;
2384
+ // Log the toggle using the imported quickLog
2385
+ quickLog(`[${new Date().toISOString()}] [AgentControl] Toggle: ${wasAgentControlled} -> ${willBeAgentControlled}, output length: ${currentOutput.length}, isRunning: ${isShellRunning}\n`);
2386
+ setState(prev => ({
2387
+ ...prev,
2388
+ shellState: prev.shellState ? {
2389
+ ...prev.shellState,
2390
+ isAgentControlled: willBeAgentControlled,
2391
+ monitoringMessages: willBeAgentControlled ? [] : prev.shellState.monitoringMessages
2392
+ } : undefined
2393
+ }));
2394
+ // Start or terminate ShellInputAgent session
2395
+ const shellId = state.shellState?.shellId;
2396
+ const shellCommand = state.shellState?.command || '';
2397
+ const shellCwd = state.shellState?.cwd || process.cwd();
2398
+ if (willBeAgentControlled && shellId) {
2399
+ // Start isolated shell input agent session with main conversation context
2400
+ const mainConversation = getMainConversation();
2401
+ ShellInputAgent.startSession(shellId, shellCommand, shellCwd, mainConversation);
2402
+ quickLog(`[${new Date().toISOString()}] [AgentControl] Started ShellInputAgent session for ${shellId}\n`);
2403
+ // Start AI-based input detection polling
2404
+ InputDetectionAgent.startPolling(shellId, currentOutput, // Pass current output directly
2405
+ shellCommand, shellCwd);
2406
+ quickLog(`[${new Date().toISOString()}] [AgentControl] Started AI-based input detection polling for ${shellId}\n`);
2407
+ }
2408
+ else if (!willBeAgentControlled && shellId) {
2409
+ // Stop AI-based input detection polling
2410
+ InputDetectionAgent.stopPolling(shellId);
2411
+ quickLog(`[${new Date().toISOString()}] [AgentControl] Stopped AI-based input detection polling for ${shellId}\n`);
2412
+ // Terminate shell input agent session
2413
+ ShellInputAgent.terminateSession(shellId);
2414
+ quickLog(`[${new Date().toISOString()}] [AgentControl] Terminated ShellInputAgent session for ${shellId}\n`);
2415
+ }
2045
2416
  } })),
2417
+ state.shellState?.isAgentControlled && state.shellState?.isFocused && state.shellState?.isRunning && (React.createElement(MonitorModeAIPanel, { messages: [
2418
+ ...(state.shellState.monitoringMessages || []),
2419
+ // Inject current assistant message if present (so AI thoughts/responses appear in panel)
2420
+ ...(state.currentMessage?.role === 'assistant' && state.currentMessage.content ? [{
2421
+ id: state.currentMessage.id,
2422
+ type: 'text', // Cast to literal type to match MonitorMessage interface
2423
+ content: state.currentMessage.content,
2424
+ timestamp: state.currentMessage.timestamp
2425
+ }] : [])
2426
+ ], maxHeight: Math.floor((process.stdout.rows || 24) / 2) - 5, isActive: true })),
2046
2427
  state.isAiWorking && !state.shellState && !state.approvalRequest &&
2047
2428
  !(state.currentMessage?.toolExecution?.status === 'executing') && (React.createElement(Box, { marginBottom: 1, paddingLeft: 1 },
2048
2429
  React.createElement(LoadingIndicator, { key: "loading-indicator" }),
@@ -2053,12 +2434,14 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2053
2434
  resolve(approved);
2054
2435
  }
2055
2436
  } })) : (React.createElement(InputBox, { key: "input-box", onSubmit: (value, clipboardImages) => {
2437
+ // Reset "Zombie Agent" protection on new manual input
2438
+ ignoreIncomingAIUpdatesRef.current = false;
2056
2439
  // Clear preserved input on submit
2057
2440
  preservedInputTextRef.current = '';
2058
2441
  handleSubmit(value, clipboardImages);
2059
- }, autoAcceptMode: state.autoAcceptMode, model: state.currentModel, planMode: state.planMode, commandMode: state.commandMode, backgroundMode: state.backgroundMode, currentWorkingDirectory: state.currentWorkingDirectory, commandHistory: state.commandHistory, onToggleAutoAccept: handleToggleAutoAccept, onToggleCommandMode: onToggleCommandMode, onToggleBackgroundMode: onToggleBackgroundMode, isActive: true, subshellContext: state.subshellContext, currentTokens: state.currentTokens, maxTokens: state.maxTokens, isShellRunning: state.shellState?.isRunning, backgroundTaskCount: state.backgroundTaskCount, initialValue: preservedInputTextRef.current, onValueChange: handleInputValueChange, onSetAutoModeSetup: (callback) => {
2442
+ }, autoAcceptMode: state.autoAcceptMode, model: state.currentModel, planMode: state.planMode, commandMode: state.commandMode, backgroundMode: state.backgroundMode, currentWorkingDirectory: state.currentWorkingDirectory, commandHistory: state.commandHistory, onToggleAutoAccept: handleToggleAutoAccept, onToggleCommandMode: onToggleCommandMode, onToggleBackgroundMode: onToggleBackgroundMode, isActive: true, subshellContext: state.subshellContext, currentTokens: state.currentTokens, maxTokens: state.maxTokens, contextLimitReached: state.contextLimitReached, isShellRunning: state.shellState?.isRunning, backgroundTaskCount: state.backgroundTaskCount, subAgentCount: state.subAgentCount, initialValue: preservedInputTextRef.current, onValueChange: handleInputValueChange, onSetAutoModeSetup: (callback) => {
2060
2443
  setAutoModeCallbackRef.current = callback;
2061
- } }))),
2444
+ }, sessionQuotaExhausted: state.sessionQuotaExhausted, sessionQuotaTimeRemaining: state.sessionQuotaTimeRemaining }))),
2062
2445
  state.showExitWarning && (React.createElement(Box, { marginTop: 1 },
2063
2446
  React.createElement(Text, { color: "#ffaa00", bold: true }, "\u26A0\uFE0F Press Ctrl+C again to exit"))),
2064
2447
  !isConnected && (React.createElement(Box, { marginTop: 0, marginLeft: state.showExitWarning ? 2 : 0 },
@@ -2103,7 +2486,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2103
2486
  state.screen === 'chat-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
2104
2487
  React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDCDA Resume a Previous Chat"),
2105
2488
  React.createElement(Box, { marginTop: 1 },
2106
- React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
2489
+ React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
2107
2490
  const date = new Date(chat.updatedAt).toLocaleDateString();
2108
2491
  const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2109
2492
  const isCurrent = chat.id === state.currentChatId;
@@ -2162,7 +2545,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2162
2545
  state.screen === 'chat-delete-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
2163
2546
  React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDDD1\uFE0F Select a Chat to Delete"),
2164
2547
  React.createElement(Box, { marginTop: 1 },
2165
- React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
2548
+ React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
2166
2549
  const date = new Date(chat.updatedAt).toLocaleDateString();
2167
2550
  const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2168
2551
  const isCurrent = chat.id === state.currentChatId;
@@ -2217,35 +2600,42 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2217
2600
  "\uD83D\uDCDA Saved Chats (",
2218
2601
  state.chatPickerChats.length,
2219
2602
  ")"),
2220
- React.createElement(Box, { marginTop: 1, flexDirection: "column" }, state.chatPickerChats.map((chat) => {
2221
- const date = new Date(chat.updatedAt).toLocaleDateString();
2222
- const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2223
- const isCurrent = chat.id === state.currentChatId;
2224
- const env = chat.environment || 'local';
2225
- return (React.createElement(Box, { key: chat.id },
2226
- React.createElement(Text, { color: isCurrent ? '#00cc66' : 'white', bold: isCurrent },
2227
- isCurrent ? '● ' : ' ',
2228
- chat.title),
2229
- React.createElement(Text, { color: env === 'local' ? 'gray' : (env.startsWith('ssh') ? '#ff9966' : env.startsWith('wsl') ? '#66ff99' : '#66ccff') },
2230
- " [",
2231
- env,
2232
- "]"),
2233
- React.createElement(Text, { color: "gray" },
2234
- " ",
2235
- date,
2236
- " ",
2237
- time),
2238
- React.createElement(Text, { color: "gray" }, " - "),
2239
- React.createElement(Text, { color: "gray" },
2240
- chat.messageCount,
2241
- " msgs")));
2242
- })),
2603
+ React.createElement(Box, { marginTop: 1 },
2604
+ React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
2605
+ const date = new Date(chat.updatedAt).toLocaleDateString();
2606
+ const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2607
+ const isCurrent = chat.id === state.currentChatId;
2608
+ const env = chat.environment || 'local';
2609
+ return {
2610
+ label: `${isCurrent ? '● ' : ' '}${chat.title}`,
2611
+ envLabel: `[${env}]`,
2612
+ dateLabel: `${date} ${time}`,
2613
+ msgLabel: `${chat.messageCount} msgs`,
2614
+ value: chat.id,
2615
+ isCurrent,
2616
+ environment: env
2617
+ };
2618
+ }), itemComponent: ({ isSelected, label, isCurrent, envLabel, dateLabel, msgLabel, environment }) => (React.createElement(Box, null,
2619
+ React.createElement(Text, { color: isSelected ? '#00ccff' : (isCurrent ? '#00cc66' : 'white'), bold: isSelected || isCurrent },
2620
+ isSelected ? '> ' : ' ',
2621
+ label),
2622
+ React.createElement(Text, { color: environment === 'local' ? 'gray' : (environment?.startsWith('ssh') ? '#ff9966' : environment?.startsWith('wsl') ? '#66ff99' : '#66ccff') },
2623
+ " ",
2624
+ envLabel),
2625
+ React.createElement(Text, { color: "gray" },
2626
+ " ",
2627
+ dateLabel),
2628
+ React.createElement(Text, { color: "gray" }, " - "),
2629
+ React.createElement(Text, { color: "gray" }, msgLabel))), onSelect: () => {
2630
+ // Just close the view
2631
+ setState(prev => ({ ...prev, screen: 'chat' }));
2632
+ } })),
2243
2633
  React.createElement(Box, { marginTop: 1 },
2244
2634
  React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
2245
2635
  state.screen === 'chat-rename-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
2246
2636
  React.createElement(Text, { color: "#00ccff", bold: true }, "\u270F\uFE0F Select a Chat to Rename"),
2247
2637
  React.createElement(Box, { marginTop: 1 },
2248
- React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
2638
+ React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
2249
2639
  const date = new Date(chat.updatedAt).toLocaleDateString();
2250
2640
  const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2251
2641
  const isCurrent = chat.id === state.currentChatId;
@@ -2361,7 +2751,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2361
2751
  }));
2362
2752
  } }))),
2363
2753
  state.screen === 'background-task-list' && state.backgroundTasks && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#9966ff", paddingX: 1 },
2364
- React.createElement(Text, { color: "#9966ff", bold: true }, "\uD83D\uDD04 Running Background Tasks"),
2754
+ React.createElement(Text, { color: "#9966ff", bold: true }, "Running Background Tasks"),
2365
2755
  React.createElement(Text, { dimColor: true }, "Select a task to view its output in focus mode"),
2366
2756
  React.createElement(Box, { marginTop: 1 },
2367
2757
  React.createElement(SelectInput, { items: state.backgroundTasks.map((task) => {
@@ -2372,35 +2762,49 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2372
2762
  cwd: task.cwd,
2373
2763
  duration: `${durationSec}s`,
2374
2764
  isRunning: task.isRunning,
2375
- outputPreview: task.outputPreview.length > 60 ? task.outputPreview.slice(0, 57) + '...' : task.outputPreview
2765
+ outputPreview: task.outputPreview // Pass raw preview, sanitize in component
2376
2766
  };
2377
- }), itemComponent: ({ isSelected, label, cwd, duration, isRunning, outputPreview }) => (React.createElement(Box, { flexDirection: "column" },
2378
- React.createElement(Box, null,
2379
- React.createElement(Text, { color: isSelected ? '#9966ff' : 'white', bold: isSelected },
2380
- isSelected ? '> ' : ' ',
2381
- label),
2382
- React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
2383
- " [",
2384
- isRunning ? 'running' : 'done',
2385
- "]"),
2386
- React.createElement(Text, { color: "gray" },
2387
- " \u23F1\uFE0F ",
2388
- duration)),
2389
- React.createElement(Box, { paddingLeft: 4 },
2390
- React.createElement(Text, { color: "gray" },
2391
- "\uD83D\uDCC1 ",
2392
- cwd)),
2393
- outputPreview && (React.createElement(Box, { paddingLeft: 4 },
2394
- React.createElement(Text, { color: "gray", dimColor: true },
2395
- "\u2514\u2500 ",
2396
- outputPreview))))), onSelect: (item) => {
2767
+ }), itemComponent: ({ isSelected, label, cwd, duration, isRunning, outputPreview }) => {
2768
+ // Calculate max width for wrapping (terminal width - border padding - left padding)
2769
+ const terminalWidth = process.stdout.columns || 80;
2770
+ const maxTextWidth = Math.max(terminalWidth - 10, 40);
2771
+ // Truncate cwd if too long
2772
+ const truncatedCwd = cwd && cwd.length > maxTextWidth
2773
+ ? cwd.slice(0, maxTextWidth - 3) + '...'
2774
+ : cwd;
2775
+ // Truncate output preview if too long (accounting for └─ prefix)
2776
+ const maxPreviewWidth = maxTextWidth - 3;
2777
+ // Sanitize: Strip ANSI codes and replace newlines with spaces
2778
+ const cleanPreview = outputPreview ? stripAnsi(outputPreview).replace(/[\r\n]+/g, ' ').trim() : '';
2779
+ const truncatedPreview = cleanPreview.length > maxPreviewWidth
2780
+ ? cleanPreview.slice(0, maxPreviewWidth - 3) + '...'
2781
+ : cleanPreview;
2782
+ return (React.createElement(Box, { flexDirection: "column" },
2783
+ React.createElement(Box, null,
2784
+ React.createElement(Text, { color: isSelected ? '#9966ff' : 'white', bold: isSelected },
2785
+ isSelected ? '> ' : ' ',
2786
+ label),
2787
+ React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
2788
+ " [",
2789
+ isRunning ? 'running' : 'done',
2790
+ "]"),
2791
+ React.createElement(Text, { color: "gray" },
2792
+ " ",
2793
+ duration)),
2794
+ React.createElement(Box, { paddingLeft: 4 },
2795
+ React.createElement(Text, { color: "gray", wrap: "truncate" }, truncatedCwd)),
2796
+ truncatedPreview && (React.createElement(Box, { paddingLeft: 4 },
2797
+ React.createElement(Text, { color: "gray", dimColor: true, wrap: "truncate" },
2798
+ "\u2514\u2500 ",
2799
+ truncatedPreview)))));
2800
+ }, onSelect: (item) => {
2397
2801
  setState(prev => ({ ...prev, screen: 'chat', isLoading: false }));
2398
2802
  onBackgroundTaskSelection(item.value);
2399
2803
  } })),
2400
2804
  React.createElement(Box, { marginTop: 1 },
2401
2805
  React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
2402
2806
  state.screen === 'background-task-cancel' && state.backgroundTasks && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#ff6666", paddingX: 1 },
2403
- React.createElement(Text, { color: "#ff6666", bold: true }, "\uD83D\uDED1 Cancel a Background Task"),
2807
+ React.createElement(Text, { color: "#ff6666", bold: true }, "Cancel a Background Task"),
2404
2808
  React.createElement(Text, { dimColor: true }, "Select a task to terminate it"),
2405
2809
  React.createElement(Box, { marginTop: 1 },
2406
2810
  React.createElement(SelectInput, { items: state.backgroundTasks.map((task) => {
@@ -2412,26 +2816,125 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2412
2816
  duration: `${durationSec}s`,
2413
2817
  isRunning: task.isRunning
2414
2818
  };
2415
- }), itemComponent: ({ isSelected, label, cwd, duration, isRunning }) => (React.createElement(Box, { flexDirection: "column" },
2416
- React.createElement(Box, null,
2417
- React.createElement(Text, { color: isSelected ? '#ff6666' : 'white', bold: isSelected },
2418
- isSelected ? '> ' : ' ',
2419
- label),
2420
- React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
2421
- " [",
2422
- isRunning ? 'running' : 'done',
2423
- "]"),
2424
- React.createElement(Text, { color: "gray" },
2425
- " \u23F1\uFE0F ",
2426
- duration)),
2427
- React.createElement(Box, { paddingLeft: 4 },
2428
- React.createElement(Text, { color: "gray" },
2429
- "\uD83D\uDCC1 ",
2430
- cwd)))), onSelect: (item) => {
2819
+ }), itemComponent: ({ isSelected, label, cwd, duration, isRunning }) => {
2820
+ // Calculate max width for wrapping (terminal width - border padding - left padding)
2821
+ const terminalWidth = process.stdout.columns || 80;
2822
+ const maxTextWidth = Math.max(terminalWidth - 10, 40);
2823
+ // Truncate cwd if too long
2824
+ const truncatedCwd = cwd && cwd.length > maxTextWidth
2825
+ ? cwd.slice(0, maxTextWidth - 3) + '...'
2826
+ : cwd;
2827
+ return (React.createElement(Box, { flexDirection: "column" },
2828
+ React.createElement(Box, null,
2829
+ React.createElement(Text, { color: isSelected ? '#ff6666' : 'white', bold: isSelected },
2830
+ isSelected ? '> ' : ' ',
2831
+ label),
2832
+ React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
2833
+ " [",
2834
+ isRunning ? 'running' : 'done',
2835
+ "]"),
2836
+ React.createElement(Text, { color: "gray" },
2837
+ " ",
2838
+ duration)),
2839
+ React.createElement(Box, { paddingLeft: 4 },
2840
+ React.createElement(Text, { color: "gray", wrap: "truncate" }, truncatedCwd))));
2841
+ }, onSelect: (item) => {
2431
2842
  setState(prev => ({ ...prev, screen: 'chat', isLoading: false }));
2432
2843
  onBackgroundTaskCancel(item.value);
2433
2844
  } })),
2434
2845
  React.createElement(Box, { marginTop: 1 },
2435
- React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))))));
2846
+ React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
2847
+ state.screen === 'mcp-add' && (React.createElement(MCPAddScreen, { onAdd: (config) => {
2848
+ const result = onMCPAddServer(config);
2849
+ if (result.success) {
2850
+ // Return to chat with success message
2851
+ const successMessage = {
2852
+ id: `mcp-add-${Date.now()}`,
2853
+ role: 'system',
2854
+ content: `✅ MCP server "${config.name}" added successfully!\n\nRun \`/mcp refresh\` to connect to the new server.`,
2855
+ timestamp: new Date()
2856
+ };
2857
+ setState(prev => ({
2858
+ ...prev,
2859
+ screen: 'chat',
2860
+ messageHistory: [...prev.messageHistory, successMessage]
2861
+ }));
2862
+ }
2863
+ }, onCancel: () => {
2864
+ setState(prev => ({
2865
+ ...prev,
2866
+ screen: 'chat'
2867
+ }));
2868
+ }, validateConfig: onMCPValidateConfig })),
2869
+ state.screen === 'mcp-remove' && state.mcpServers && (React.createElement(MCPServerListScreen, { mode: "remove", servers: state.mcpServers, onSelect: (serverName) => {
2870
+ onMCPRemoveServer(serverName);
2871
+ const successMessage = {
2872
+ id: `mcp-remove-${Date.now()}`,
2873
+ role: 'system',
2874
+ content: `✅ MCP server "${serverName}" removed from configuration.`,
2875
+ timestamp: new Date()
2876
+ };
2877
+ setState(prev => ({
2878
+ ...prev,
2879
+ screen: 'chat',
2880
+ mcpServers: undefined,
2881
+ messageHistory: [...prev.messageHistory, successMessage]
2882
+ }));
2883
+ }, onCancel: () => {
2884
+ setState(prev => ({
2885
+ ...prev,
2886
+ screen: 'chat',
2887
+ mcpServers: undefined
2888
+ }));
2889
+ } })),
2890
+ state.screen === 'mcp-enable' && state.mcpServers && (React.createElement(MCPServerListScreen, { mode: "enable", servers: state.mcpServers, onSelect: (serverName) => {
2891
+ onMCPEnableServer(serverName);
2892
+ const successMessage = {
2893
+ id: `mcp-enable-${Date.now()}`,
2894
+ role: 'system',
2895
+ content: `✅ MCP server "${serverName}" enabled!\n\nRun \`/mcp refresh\` to connect.`,
2896
+ timestamp: new Date()
2897
+ };
2898
+ setState(prev => ({
2899
+ ...prev,
2900
+ screen: 'chat',
2901
+ mcpServers: undefined,
2902
+ messageHistory: [...prev.messageHistory, successMessage]
2903
+ }));
2904
+ }, onCancel: () => {
2905
+ setState(prev => ({
2906
+ ...prev,
2907
+ screen: 'chat',
2908
+ mcpServers: undefined
2909
+ }));
2910
+ } })),
2911
+ state.screen === 'mcp-disable' && state.mcpServers && (React.createElement(MCPServerListScreen, { mode: "disable", servers: state.mcpServers, onSelect: (serverName) => {
2912
+ onMCPDisableServer(serverName);
2913
+ const successMessage = {
2914
+ id: `mcp-disable-${Date.now()}`,
2915
+ role: 'system',
2916
+ content: `⏸️ MCP server "${serverName}" disabled. AI will no longer use its tools.`,
2917
+ timestamp: new Date()
2918
+ };
2919
+ setState(prev => ({
2920
+ ...prev,
2921
+ screen: 'chat',
2922
+ mcpServers: undefined,
2923
+ messageHistory: [...prev.messageHistory, successMessage]
2924
+ }));
2925
+ }, onCancel: () => {
2926
+ setState(prev => ({
2927
+ ...prev,
2928
+ screen: 'chat',
2929
+ mcpServers: undefined
2930
+ }));
2931
+ } })),
2932
+ state.screen === 'mcp-list' && state.mcpListData && (React.createElement(MCPListScreen, { servers: state.mcpListData, onClose: () => {
2933
+ setState(prev => ({
2934
+ ...prev,
2935
+ screen: 'chat',
2936
+ mcpListData: undefined
2937
+ }));
2938
+ } }))));
2436
2939
  };
2437
2940
  //# sourceMappingURL=App.js.map