centaurus-cli 2.9.1 → 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 (127) hide show
  1. package/dist/cli-adapter.d.ts +70 -0
  2. package/dist/cli-adapter.d.ts.map +1 -1
  3. package/dist/cli-adapter.js +349 -156
  4. package/dist/cli-adapter.js.map +1 -1
  5. package/dist/config/mcp-config-manager.d.ts +21 -0
  6. package/dist/config/mcp-config-manager.d.ts.map +1 -1
  7. package/dist/config/mcp-config-manager.js +184 -1
  8. package/dist/config/mcp-config-manager.js.map +1 -1
  9. package/dist/config/models.d.ts +1 -0
  10. package/dist/config/models.d.ts.map +1 -1
  11. package/dist/config/models.js +7 -2
  12. package/dist/config/models.js.map +1 -1
  13. package/dist/config/slash-commands.d.ts.map +1 -1
  14. package/dist/config/slash-commands.js +4 -3
  15. package/dist/config/slash-commands.js.map +1 -1
  16. package/dist/index.js +60 -11
  17. package/dist/index.js.map +1 -1
  18. package/dist/mcp/mcp-command-handler.d.ts +34 -3
  19. package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
  20. package/dist/mcp/mcp-command-handler.js +171 -83
  21. package/dist/mcp/mcp-command-handler.js.map +1 -1
  22. package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
  23. package/dist/mcp/mcp-server-manager.js +9 -23
  24. package/dist/mcp/mcp-server-manager.js.map +1 -1
  25. package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
  26. package/dist/mcp/mcp-tool-wrapper.js +42 -5
  27. package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
  28. package/dist/services/api-client.d.ts +9 -0
  29. package/dist/services/api-client.d.ts.map +1 -1
  30. package/dist/services/api-client.js +25 -0
  31. package/dist/services/api-client.js.map +1 -1
  32. package/dist/services/input-detection-agent.d.ts +40 -0
  33. package/dist/services/input-detection-agent.d.ts.map +1 -0
  34. package/dist/services/input-detection-agent.js +213 -0
  35. package/dist/services/input-detection-agent.js.map +1 -0
  36. package/dist/services/input-requirement-detector.d.ts +28 -0
  37. package/dist/services/input-requirement-detector.d.ts.map +1 -0
  38. package/dist/services/input-requirement-detector.js +203 -0
  39. package/dist/services/input-requirement-detector.js.map +1 -0
  40. package/dist/services/monitored-shell-manager.d.ts +120 -0
  41. package/dist/services/monitored-shell-manager.d.ts.map +1 -0
  42. package/dist/services/monitored-shell-manager.js +239 -0
  43. package/dist/services/monitored-shell-manager.js.map +1 -0
  44. package/dist/services/shell-input-agent.d.ts +89 -0
  45. package/dist/services/shell-input-agent.d.ts.map +1 -0
  46. package/dist/services/shell-input-agent.js +361 -0
  47. package/dist/services/shell-input-agent.js.map +1 -0
  48. package/dist/services/sub-agent-manager.d.ts +139 -0
  49. package/dist/services/sub-agent-manager.d.ts.map +1 -0
  50. package/dist/services/sub-agent-manager.js +517 -0
  51. package/dist/services/sub-agent-manager.js.map +1 -0
  52. package/dist/tools/background-command.d.ts.map +1 -1
  53. package/dist/tools/background-command.js +33 -13
  54. package/dist/tools/background-command.js.map +1 -1
  55. package/dist/tools/command.d.ts.map +1 -1
  56. package/dist/tools/command.js +64 -1
  57. package/dist/tools/command.js.map +1 -1
  58. package/dist/tools/file-ops.d.ts.map +1 -1
  59. package/dist/tools/file-ops.js +33 -19
  60. package/dist/tools/file-ops.js.map +1 -1
  61. package/dist/tools/get-diff.js +1 -1
  62. package/dist/tools/get-diff.js.map +1 -1
  63. package/dist/tools/grep-search.d.ts.map +1 -1
  64. package/dist/tools/grep-search.js +41 -15
  65. package/dist/tools/grep-search.js.map +1 -1
  66. package/dist/tools/plan-mode.js +3 -3
  67. package/dist/tools/plan-mode.js.map +1 -1
  68. package/dist/tools/registry.js +1 -1
  69. package/dist/tools/registry.js.map +1 -1
  70. package/dist/tools/sub-agent.d.ts +9 -0
  71. package/dist/tools/sub-agent.d.ts.map +1 -0
  72. package/dist/tools/sub-agent.js +232 -0
  73. package/dist/tools/sub-agent.js.map +1 -0
  74. package/dist/tools/task-complete.d.ts.map +1 -1
  75. package/dist/tools/task-complete.js +14 -26
  76. package/dist/tools/task-complete.js.map +1 -1
  77. package/dist/ui/components/App.d.ts +43 -0
  78. package/dist/ui/components/App.d.ts.map +1 -1
  79. package/dist/ui/components/App.js +560 -94
  80. package/dist/ui/components/App.js.map +1 -1
  81. package/dist/ui/components/CircularSelectInput.d.ts +24 -0
  82. package/dist/ui/components/CircularSelectInput.d.ts.map +1 -0
  83. package/dist/ui/components/CircularSelectInput.js +71 -0
  84. package/dist/ui/components/CircularSelectInput.js.map +1 -0
  85. package/dist/ui/components/ErrorBoundary.d.ts +3 -2
  86. package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
  87. package/dist/ui/components/ErrorBoundary.js +29 -1
  88. package/dist/ui/components/ErrorBoundary.js.map +1 -1
  89. package/dist/ui/components/InputBox.d.ts +2 -0
  90. package/dist/ui/components/InputBox.d.ts.map +1 -1
  91. package/dist/ui/components/InputBox.js +23 -3
  92. package/dist/ui/components/InputBox.js.map +1 -1
  93. package/dist/ui/components/InteractiveShell.d.ts +6 -0
  94. package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
  95. package/dist/ui/components/InteractiveShell.js +57 -6
  96. package/dist/ui/components/InteractiveShell.js.map +1 -1
  97. package/dist/ui/components/MCPAddScreen.d.ts +13 -0
  98. package/dist/ui/components/MCPAddScreen.d.ts.map +1 -0
  99. package/dist/ui/components/MCPAddScreen.js +54 -0
  100. package/dist/ui/components/MCPAddScreen.js.map +1 -0
  101. package/dist/ui/components/MCPListScreen.d.ts +17 -0
  102. package/dist/ui/components/MCPListScreen.d.ts.map +1 -0
  103. package/dist/ui/components/MCPListScreen.js +50 -0
  104. package/dist/ui/components/MCPListScreen.js.map +1 -0
  105. package/dist/ui/components/MCPServerListScreen.d.ts +16 -0
  106. package/dist/ui/components/MCPServerListScreen.d.ts.map +1 -0
  107. package/dist/ui/components/MCPServerListScreen.js +59 -0
  108. package/dist/ui/components/MCPServerListScreen.js.map +1 -0
  109. package/dist/ui/components/MonitorModeAIPanel.d.ts +23 -0
  110. package/dist/ui/components/MonitorModeAIPanel.d.ts.map +1 -0
  111. package/dist/ui/components/MonitorModeAIPanel.js +69 -0
  112. package/dist/ui/components/MonitorModeAIPanel.js.map +1 -0
  113. package/dist/ui/components/MultiLineInput.d.ts +13 -0
  114. package/dist/ui/components/MultiLineInput.d.ts.map +1 -0
  115. package/dist/ui/components/MultiLineInput.js +223 -0
  116. package/dist/ui/components/MultiLineInput.js.map +1 -0
  117. package/dist/ui/components/StatusBar.d.ts +2 -0
  118. package/dist/ui/components/StatusBar.d.ts.map +1 -1
  119. package/dist/ui/components/StatusBar.js +33 -2
  120. package/dist/ui/components/StatusBar.js.map +1 -1
  121. package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
  122. package/dist/ui/components/ToolExecutionMessage.js +226 -12
  123. package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
  124. package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
  125. package/dist/ui/components/VersionUpdatePrompt.js +3 -2
  126. package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
  127. 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, onClearStreamedResponse, 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, onSessionQuotaUpdate }) => {
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,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
203
219
  passwordRequest: undefined,
204
220
  connectionStatus: undefined,
205
221
  backgroundTaskCount: 0,
222
+ subAgentCount: 0,
206
223
  sessionQuotaExhausted: false,
207
224
  sessionQuotaTimeRemaining: '',
208
225
  });
@@ -270,6 +287,20 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
270
287
  }
271
288
  };
272
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
+ });
273
304
  // Check for version updates on mount
274
305
  React.useEffect(() => {
275
306
  checkForUpdates().then(versionInfo => {
@@ -323,6 +354,17 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
323
354
  React.useEffect(() => {
324
355
  autoAcceptRef.current = state.autoAcceptMode;
325
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);
326
368
  // NOTE: Token count is now updated via onTokenCountUpdate callback from cli-adapter
327
369
  // which tracks the actual AI conversation history including system prompt.
328
370
  // The old UI-based calculation has been removed to avoid overwriting correct values.
@@ -335,6 +377,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
335
377
  // Set up callback to receive streaming chunks - only once on mount
336
378
  React.useEffect(() => {
337
379
  onResponseStream((chunk) => {
380
+ // CIRCUIT BREAKER: Block incoming messages from "Zombie Agents"
381
+ if (ignoreIncomingAIUpdatesRef.current)
382
+ return;
338
383
  isStreamingRef.current = true; // Mark that we're streaming
339
384
  // Check if terminal is large enough for streaming
340
385
  const dimensions = getTerminalDimensions();
@@ -438,6 +483,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
438
483
  // Set up callback to receive thought chunks
439
484
  React.useEffect(() => {
440
485
  onThoughtStream((thought) => {
486
+ // CIRCUIT BREAKER: Block incoming thoughts from "Zombie Agents"
487
+ if (ignoreIncomingAIUpdatesRef.current)
488
+ return;
441
489
  // Debug logging to file
442
490
  try {
443
491
  quickLog(`[${new Date().toISOString()}] [App] Received thought chunk (${thought.length} chars): ${thought.substring(0, 100)}\n`);
@@ -529,6 +577,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
529
577
  // Set up callback for when thinking completes
530
578
  React.useEffect(() => {
531
579
  onThoughtComplete((durationSeconds) => {
580
+ // CIRCUIT BREAKER: Block incoming thought completion from "Zombie Agents"
581
+ if (ignoreIncomingAIUpdatesRef.current)
582
+ return;
532
583
  try {
533
584
  quickLog(`[${new Date().toISOString()}] [App] onThoughtComplete called with ${durationSeconds}s\n`);
534
585
  }
@@ -565,12 +616,28 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
565
616
  });
566
617
  });
567
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
+ };
568
632
  // Set up callback for direct messages (slash commands) - add directly to history
569
633
  React.useEffect(() => {
570
634
  onDirectMessage((message) => {
635
+ // CIRCUIT BREAKER: Block incoming direct messages from "Zombie Agents"
636
+ if (ignoreIncomingAIUpdatesRef.current)
637
+ return;
571
638
  setState(prev => {
572
- // Move current message to history if exists
573
- const newHistory = prev.currentMessage
639
+ // Move current message to history if exists (skip thought-only messages)
640
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
574
641
  ? [...prev.messageHistory, prev.currentMessage]
575
642
  : prev.messageHistory;
576
643
  // Create system message for slash command response
@@ -594,6 +661,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
594
661
  // Set up callback to receive complete responses (for slash commands and non-streaming responses)
595
662
  React.useEffect(() => {
596
663
  onResponseReceived((message) => {
664
+ // CIRCUIT BREAKER: Block incoming responses from "Zombie Agents"
665
+ if (ignoreIncomingAIUpdatesRef.current)
666
+ return;
597
667
  // If we were streaming, move the current message to history immediately
598
668
  if (isStreamingRef.current) {
599
669
  isStreamingRef.current = false; // Reset for next message
@@ -702,8 +772,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
702
772
  timestamp: now,
703
773
  shouldStream: false // Command responses should appear instantly
704
774
  };
705
- // If we have a current message that's an assistant message, move it to history
706
- 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)) {
707
777
  return {
708
778
  ...prev,
709
779
  messageHistory: [...prev.messageHistory, prev.currentMessage],
@@ -806,6 +876,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
806
876
  }));
807
877
  });
808
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
809
888
  // Set up callback for setting Auto mode (used after background task starts)
810
889
  React.useEffect(() => {
811
890
  onSetAutoModeSetup((enabled) => {
@@ -943,6 +1022,65 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
943
1022
  });
944
1023
  });
945
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
946
1084
  // Sync UI message history to CLI adapter whenever it changes
947
1085
  React.useEffect(() => {
948
1086
  onUIMessageHistoryUpdate(state.messageHistory);
@@ -950,6 +1088,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
950
1088
  // Set up callback for tool execution updates
951
1089
  React.useEffect(() => {
952
1090
  onToolExecutionUpdate((update) => {
1091
+ // CIRCUIT BREAKER: Block incoming tool updates from "Zombie Agents"
1092
+ if (ignoreIncomingAIUpdatesRef.current)
1093
+ return;
953
1094
  // Special handling for execute_command
954
1095
  if (update.toolName === 'execute_command') {
955
1096
  try {
@@ -958,6 +1099,28 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
958
1099
  catch (e) { }
959
1100
  // STARTING EXECUTION
960
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
+ }
961
1124
  const command = update.arguments?.command || update.arguments?.CommandLine || update.arguments?.commandLine || '';
962
1125
  // Start a pending shell with a short delay
963
1126
  // This prevents the UI from flickering for very fast commands (like ls)
@@ -973,6 +1136,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
973
1136
  setState(prev => ({
974
1137
  ...prev,
975
1138
  shellState: {
1139
+ shellId: `shell-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
976
1140
  command: cmd,
977
1141
  cwd: cwd,
978
1142
  output: bufferedOutput,
@@ -981,10 +1145,11 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
981
1145
  error: undefined,
982
1146
  isFocused: false,
983
1147
  isPty: update.arguments?.isPty || false,
984
- remoteContext: update.arguments?.remoteContext
1148
+ remoteContext: update.arguments?.remoteContext,
1149
+ isAgentStarted: true // Mark as agent-started since it came from execute_command tool
985
1150
  },
986
- // IMPORTANT: Always preserve current message by moving it to history first
987
- 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)
988
1153
  ? [...prev.messageHistory, prev.currentMessage]
989
1154
  : prev.messageHistory,
990
1155
  currentMessage: {
@@ -1040,8 +1205,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1040
1205
  arguments: update.arguments
1041
1206
  }
1042
1207
  };
1043
- // Move current message to history if exists
1044
- const newHistory = prev.currentMessage
1208
+ // Move current message to history if exists (skip thought-only)
1209
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1045
1210
  ? [...prev.messageHistory, prev.currentMessage]
1046
1211
  : prev.messageHistory;
1047
1212
  return {
@@ -1078,12 +1243,44 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1078
1243
  arguments: update.arguments
1079
1244
  }
1080
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
+ }
1081
1271
  return {
1082
1272
  ...prev,
1083
- 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,
1084
1275
  messageHistory: [...prev.messageHistory, shellMessage],
1085
1276
  currentMessage: null,
1086
- 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
1087
1284
  };
1088
1285
  });
1089
1286
  return;
@@ -1177,7 +1374,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1177
1374
  // Carry the thinkingDuration to the tool message, don't add empty message to history
1178
1375
  pendingThinkingDuration = prev.currentMessage.thinkingDuration;
1179
1376
  try {
1180
- 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`);
1181
1378
  }
1182
1379
  catch (e) { }
1183
1380
  }
@@ -1209,7 +1406,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1209
1406
  }
1210
1407
  catch (e) { }
1211
1408
  let newHistory = prev.messageHistory;
1212
- if (prev.currentMessage) {
1409
+ if (prev.currentMessage && !shouldSkipMessage(prev.currentMessage)) {
1213
1410
  newHistory = [...prev.messageHistory, prev.currentMessage];
1214
1411
  }
1215
1412
  return {
@@ -1235,8 +1432,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1235
1432
  timestamp: new Date(),
1236
1433
  connectionStatus: status
1237
1434
  };
1238
- // Move current message to history if exists
1239
- const newHistory = prev.currentMessage
1435
+ // Move current message to history if exists (skip thought-only messages)
1436
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1240
1437
  ? [...prev.messageHistory, prev.currentMessage]
1241
1438
  : prev.messageHistory;
1242
1439
  return {
@@ -1265,8 +1462,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1265
1462
  currentMessage: null
1266
1463
  };
1267
1464
  }
1268
- // Otherwise just add to history
1269
- const newHistory = prev.currentMessage
1465
+ // Otherwise just add to history (skip thought-only messages)
1466
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1270
1467
  ? [...prev.messageHistory, prev.currentMessage, finalConnectionMessage]
1271
1468
  : [...prev.messageHistory, finalConnectionMessage];
1272
1469
  return {
@@ -1361,14 +1558,65 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1361
1558
  pendingShellRef.current.output += update.chunk;
1362
1559
  return;
1363
1560
  }
1561
+ // Update last output time for stall detection
1562
+ lastOutputTimeRef.current = Date.now();
1364
1563
  setState(prev => {
1365
1564
  // Update shell state if it's an execute_command
1366
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
+ }
1367
1615
  return {
1368
1616
  ...prev,
1369
1617
  shellState: {
1370
1618
  ...prev.shellState,
1371
- output: prev.shellState.output + update.chunk
1619
+ output: newOutput
1372
1620
  }
1373
1621
  };
1374
1622
  }
@@ -1390,7 +1638,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1390
1638
  return prev;
1391
1639
  });
1392
1640
  });
1393
- }, []);
1641
+ }, [onMessage]);
1394
1642
  // Set up callback to receive plan mode changes
1395
1643
  React.useEffect(() => {
1396
1644
  onPlanModeChange((planMode) => {
@@ -1448,6 +1696,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1448
1696
  }));
1449
1697
  });
1450
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]);
1451
1708
  // Set up callback to receive session quota updates from cli-adapter
1452
1709
  React.useEffect(() => {
1453
1710
  onSessionQuotaUpdate((remaining, canSend, timeRemaining) => {
@@ -1458,6 +1715,36 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1458
1715
  }));
1459
1716
  });
1460
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]);
1461
1748
  // Set up callback for plan approval requests
1462
1749
  React.useEffect(() => {
1463
1750
  onPlanApprovalRequest(async (plan) => {
@@ -1505,8 +1792,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1505
1792
  completionNote
1506
1793
  }
1507
1794
  };
1508
- // Move current message to history if exists
1509
- const newHistory = prev.currentMessage
1795
+ // Move current message to history if exists (skip thought-only)
1796
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1510
1797
  ? [...prev.messageHistory, prev.currentMessage, taskCompletedMessage]
1511
1798
  : [...prev.messageHistory, taskCompletedMessage];
1512
1799
  return {
@@ -1667,6 +1954,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1667
1954
  }));
1668
1955
  return;
1669
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
+ }
1670
1966
  // If AI is working, cancel the operation
1671
1967
  if (state.isLoading || state.isAiWorking) {
1672
1968
  // Cancel the current AI request
@@ -1811,8 +2107,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1811
2107
  // In command mode, add command to history immediately
1812
2108
  if (state.commandMode) {
1813
2109
  setState(prev => {
1814
- // Move current message to history if exists
1815
- const newHistory = prev.currentMessage
2110
+ // Move current message to history if exists (skip thought-only)
2111
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1816
2112
  ? [...prev.messageHistory, prev.currentMessage]
1817
2113
  : prev.messageHistory;
1818
2114
  const commandMessage = {
@@ -1850,7 +2146,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1850
2146
  // Generate a stable message ID that we can use to update the message later
1851
2147
  const userMessageId = `user-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
1852
2148
  setState(prev => {
1853
- const newHistory = prev.currentMessage
2149
+ const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
1854
2150
  ? [...prev.messageHistory, prev.currentMessage]
1855
2151
  : prev.messageHistory;
1856
2152
  const userMessage = {
@@ -1993,8 +2289,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
1993
2289
  content: `❌ Error: ${error.message || 'Unknown error occurred'}`,
1994
2290
  timestamp: new Date()
1995
2291
  };
1996
- // Move current to history if exists and add error
1997
- 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)
1998
2294
  ? [...prev.messageHistory, prev.currentMessage, errorMessage]
1999
2295
  : [...prev.messageHistory, errorMessage];
2000
2296
  return {
@@ -2079,7 +2375,55 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2079
2375
  return;
2080
2376
  }
2081
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
+ }
2082
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 })),
2083
2427
  state.isAiWorking && !state.shellState && !state.approvalRequest &&
2084
2428
  !(state.currentMessage?.toolExecution?.status === 'executing') && (React.createElement(Box, { marginBottom: 1, paddingLeft: 1 },
2085
2429
  React.createElement(LoadingIndicator, { key: "loading-indicator" }),
@@ -2090,10 +2434,12 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2090
2434
  resolve(approved);
2091
2435
  }
2092
2436
  } })) : (React.createElement(InputBox, { key: "input-box", onSubmit: (value, clipboardImages) => {
2437
+ // Reset "Zombie Agent" protection on new manual input
2438
+ ignoreIncomingAIUpdatesRef.current = false;
2093
2439
  // Clear preserved input on submit
2094
2440
  preservedInputTextRef.current = '';
2095
2441
  handleSubmit(value, clipboardImages);
2096
- }, 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) => {
2097
2443
  setAutoModeCallbackRef.current = callback;
2098
2444
  }, sessionQuotaExhausted: state.sessionQuotaExhausted, sessionQuotaTimeRemaining: state.sessionQuotaTimeRemaining }))),
2099
2445
  state.showExitWarning && (React.createElement(Box, { marginTop: 1 },
@@ -2140,7 +2486,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2140
2486
  state.screen === 'chat-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
2141
2487
  React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDCDA Resume a Previous Chat"),
2142
2488
  React.createElement(Box, { marginTop: 1 },
2143
- React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
2489
+ React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
2144
2490
  const date = new Date(chat.updatedAt).toLocaleDateString();
2145
2491
  const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2146
2492
  const isCurrent = chat.id === state.currentChatId;
@@ -2199,7 +2545,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2199
2545
  state.screen === 'chat-delete-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
2200
2546
  React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDDD1\uFE0F Select a Chat to Delete"),
2201
2547
  React.createElement(Box, { marginTop: 1 },
2202
- React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
2548
+ React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
2203
2549
  const date = new Date(chat.updatedAt).toLocaleDateString();
2204
2550
  const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2205
2551
  const isCurrent = chat.id === state.currentChatId;
@@ -2254,35 +2600,42 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2254
2600
  "\uD83D\uDCDA Saved Chats (",
2255
2601
  state.chatPickerChats.length,
2256
2602
  ")"),
2257
- React.createElement(Box, { marginTop: 1, flexDirection: "column" }, state.chatPickerChats.map((chat) => {
2258
- const date = new Date(chat.updatedAt).toLocaleDateString();
2259
- const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2260
- const isCurrent = chat.id === state.currentChatId;
2261
- const env = chat.environment || 'local';
2262
- return (React.createElement(Box, { key: chat.id },
2263
- React.createElement(Text, { color: isCurrent ? '#00cc66' : 'white', bold: isCurrent },
2264
- isCurrent ? '● ' : ' ',
2265
- chat.title),
2266
- React.createElement(Text, { color: env === 'local' ? 'gray' : (env.startsWith('ssh') ? '#ff9966' : env.startsWith('wsl') ? '#66ff99' : '#66ccff') },
2267
- " [",
2268
- env,
2269
- "]"),
2270
- React.createElement(Text, { color: "gray" },
2271
- " ",
2272
- date,
2273
- " ",
2274
- time),
2275
- React.createElement(Text, { color: "gray" }, " - "),
2276
- React.createElement(Text, { color: "gray" },
2277
- chat.messageCount,
2278
- " msgs")));
2279
- })),
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
+ } })),
2280
2633
  React.createElement(Box, { marginTop: 1 },
2281
2634
  React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
2282
2635
  state.screen === 'chat-rename-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
2283
2636
  React.createElement(Text, { color: "#00ccff", bold: true }, "\u270F\uFE0F Select a Chat to Rename"),
2284
2637
  React.createElement(Box, { marginTop: 1 },
2285
- React.createElement(SelectInput, { items: state.chatPickerChats.map((chat) => {
2638
+ React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
2286
2639
  const date = new Date(chat.updatedAt).toLocaleDateString();
2287
2640
  const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2288
2641
  const isCurrent = chat.id === state.currentChatId;
@@ -2398,7 +2751,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2398
2751
  }));
2399
2752
  } }))),
2400
2753
  state.screen === 'background-task-list' && state.backgroundTasks && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#9966ff", paddingX: 1 },
2401
- React.createElement(Text, { color: "#9966ff", bold: true }, "\uD83D\uDD04 Running Background Tasks"),
2754
+ React.createElement(Text, { color: "#9966ff", bold: true }, "Running Background Tasks"),
2402
2755
  React.createElement(Text, { dimColor: true }, "Select a task to view its output in focus mode"),
2403
2756
  React.createElement(Box, { marginTop: 1 },
2404
2757
  React.createElement(SelectInput, { items: state.backgroundTasks.map((task) => {
@@ -2409,35 +2762,49 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2409
2762
  cwd: task.cwd,
2410
2763
  duration: `${durationSec}s`,
2411
2764
  isRunning: task.isRunning,
2412
- outputPreview: task.outputPreview.length > 60 ? task.outputPreview.slice(0, 57) + '...' : task.outputPreview
2765
+ outputPreview: task.outputPreview // Pass raw preview, sanitize in component
2413
2766
  };
2414
- }), itemComponent: ({ isSelected, label, cwd, duration, isRunning, outputPreview }) => (React.createElement(Box, { flexDirection: "column" },
2415
- React.createElement(Box, null,
2416
- React.createElement(Text, { color: isSelected ? '#9966ff' : 'white', bold: isSelected },
2417
- isSelected ? '> ' : ' ',
2418
- label),
2419
- React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
2420
- " [",
2421
- isRunning ? 'running' : 'done',
2422
- "]"),
2423
- React.createElement(Text, { color: "gray" },
2424
- " \u23F1\uFE0F ",
2425
- duration)),
2426
- React.createElement(Box, { paddingLeft: 4 },
2427
- React.createElement(Text, { color: "gray" },
2428
- "\uD83D\uDCC1 ",
2429
- cwd)),
2430
- outputPreview && (React.createElement(Box, { paddingLeft: 4 },
2431
- React.createElement(Text, { color: "gray", dimColor: true },
2432
- "\u2514\u2500 ",
2433
- 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) => {
2434
2801
  setState(prev => ({ ...prev, screen: 'chat', isLoading: false }));
2435
2802
  onBackgroundTaskSelection(item.value);
2436
2803
  } })),
2437
2804
  React.createElement(Box, { marginTop: 1 },
2438
2805
  React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
2439
2806
  state.screen === 'background-task-cancel' && state.backgroundTasks && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#ff6666", paddingX: 1 },
2440
- 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"),
2441
2808
  React.createElement(Text, { dimColor: true }, "Select a task to terminate it"),
2442
2809
  React.createElement(Box, { marginTop: 1 },
2443
2810
  React.createElement(SelectInput, { items: state.backgroundTasks.map((task) => {
@@ -2449,26 +2816,125 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
2449
2816
  duration: `${durationSec}s`,
2450
2817
  isRunning: task.isRunning
2451
2818
  };
2452
- }), itemComponent: ({ isSelected, label, cwd, duration, isRunning }) => (React.createElement(Box, { flexDirection: "column" },
2453
- React.createElement(Box, null,
2454
- React.createElement(Text, { color: isSelected ? '#ff6666' : 'white', bold: isSelected },
2455
- isSelected ? '> ' : ' ',
2456
- label),
2457
- React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
2458
- " [",
2459
- isRunning ? 'running' : 'done',
2460
- "]"),
2461
- React.createElement(Text, { color: "gray" },
2462
- " \u23F1\uFE0F ",
2463
- duration)),
2464
- React.createElement(Box, { paddingLeft: 4 },
2465
- React.createElement(Text, { color: "gray" },
2466
- "\uD83D\uDCC1 ",
2467
- 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) => {
2468
2842
  setState(prev => ({ ...prev, screen: 'chat', isLoading: false }));
2469
2843
  onBackgroundTaskCancel(item.value);
2470
2844
  } })),
2471
2845
  React.createElement(Box, { marginTop: 1 },
2472
- 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
+ } }))));
2473
2939
  };
2474
2940
  //# sourceMappingURL=App.js.map