centaurus-cli 2.9.1 → 2.9.3
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.
- package/dist/cli-adapter.d.ts +76 -3
- package/dist/cli-adapter.d.ts.map +1 -1
- package/dist/cli-adapter.js +593 -230
- package/dist/cli-adapter.js.map +1 -1
- package/dist/config/mcp-config-manager.d.ts +21 -0
- package/dist/config/mcp-config-manager.d.ts.map +1 -1
- package/dist/config/mcp-config-manager.js +184 -1
- package/dist/config/mcp-config-manager.js.map +1 -1
- package/dist/config/models.d.ts +1 -0
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +9 -2
- package/dist/config/models.js.map +1 -1
- package/dist/config/slash-commands.d.ts +3 -0
- package/dist/config/slash-commands.d.ts.map +1 -1
- package/dist/config/slash-commands.js +39 -4
- package/dist/config/slash-commands.js.map +1 -1
- package/dist/config/types.d.ts +2 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +1 -0
- package/dist/config/types.js.map +1 -1
- package/dist/index.js +60 -11
- package/dist/index.js.map +1 -1
- package/dist/mcp/mcp-command-handler.d.ts +34 -3
- package/dist/mcp/mcp-command-handler.d.ts.map +1 -1
- package/dist/mcp/mcp-command-handler.js +171 -83
- package/dist/mcp/mcp-command-handler.js.map +1 -1
- package/dist/mcp/mcp-server-manager.d.ts.map +1 -1
- package/dist/mcp/mcp-server-manager.js +9 -23
- package/dist/mcp/mcp-server-manager.js.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.d.ts.map +1 -1
- package/dist/mcp/mcp-tool-wrapper.js +42 -5
- package/dist/mcp/mcp-tool-wrapper.js.map +1 -1
- package/dist/services/ai-autocomplete-agent.d.ts +39 -0
- package/dist/services/ai-autocomplete-agent.d.ts.map +1 -0
- package/dist/services/ai-autocomplete-agent.js +189 -0
- package/dist/services/ai-autocomplete-agent.js.map +1 -0
- package/dist/services/ai-service-client.d.ts +25 -0
- package/dist/services/ai-service-client.d.ts.map +1 -1
- package/dist/services/ai-service-client.js +162 -1
- package/dist/services/ai-service-client.js.map +1 -1
- package/dist/services/api-client.d.ts +9 -0
- package/dist/services/api-client.d.ts.map +1 -1
- package/dist/services/api-client.js +25 -0
- package/dist/services/api-client.js.map +1 -1
- package/dist/services/auth-handler.js +1 -1
- package/dist/services/auth-handler.js.map +1 -1
- package/dist/services/input-detection-agent.d.ts +40 -0
- package/dist/services/input-detection-agent.d.ts.map +1 -0
- package/dist/services/input-detection-agent.js +213 -0
- package/dist/services/input-detection-agent.js.map +1 -0
- package/dist/services/input-requirement-detector.d.ts +28 -0
- package/dist/services/input-requirement-detector.d.ts.map +1 -0
- package/dist/services/input-requirement-detector.js +203 -0
- package/dist/services/input-requirement-detector.js.map +1 -0
- package/dist/services/local-chat-storage.d.ts +21 -0
- package/dist/services/local-chat-storage.d.ts.map +1 -1
- package/dist/services/local-chat-storage.js +138 -43
- package/dist/services/local-chat-storage.js.map +1 -1
- package/dist/services/monitored-shell-manager.d.ts +120 -0
- package/dist/services/monitored-shell-manager.d.ts.map +1 -0
- package/dist/services/monitored-shell-manager.js +239 -0
- package/dist/services/monitored-shell-manager.js.map +1 -0
- package/dist/services/ollama-service.d.ts +197 -0
- package/dist/services/ollama-service.d.ts.map +1 -0
- package/dist/services/ollama-service.js +324 -0
- package/dist/services/ollama-service.js.map +1 -0
- package/dist/services/shell-input-agent.d.ts +89 -0
- package/dist/services/shell-input-agent.d.ts.map +1 -0
- package/dist/services/shell-input-agent.js +361 -0
- package/dist/services/shell-input-agent.js.map +1 -0
- package/dist/services/sub-agent-manager.d.ts +139 -0
- package/dist/services/sub-agent-manager.d.ts.map +1 -0
- package/dist/services/sub-agent-manager.js +517 -0
- package/dist/services/sub-agent-manager.js.map +1 -0
- package/dist/tools/background-command.d.ts.map +1 -1
- package/dist/tools/background-command.js +33 -13
- package/dist/tools/background-command.js.map +1 -1
- package/dist/tools/command.d.ts.map +1 -1
- package/dist/tools/command.js +64 -1
- package/dist/tools/command.js.map +1 -1
- package/dist/tools/file-ops.d.ts.map +1 -1
- package/dist/tools/file-ops.js +33 -19
- package/dist/tools/file-ops.js.map +1 -1
- package/dist/tools/get-diff.js +1 -1
- package/dist/tools/get-diff.js.map +1 -1
- package/dist/tools/grep-search.d.ts.map +1 -1
- package/dist/tools/grep-search.js +41 -15
- package/dist/tools/grep-search.js.map +1 -1
- package/dist/tools/plan-mode.js +3 -3
- package/dist/tools/plan-mode.js.map +1 -1
- package/dist/tools/registry.js +1 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/sub-agent.d.ts +9 -0
- package/dist/tools/sub-agent.d.ts.map +1 -0
- package/dist/tools/sub-agent.js +232 -0
- package/dist/tools/sub-agent.js.map +1 -0
- package/dist/tools/task-complete.d.ts.map +1 -1
- package/dist/tools/task-complete.js +14 -26
- package/dist/tools/task-complete.js.map +1 -1
- package/dist/ui/components/App.d.ts +45 -2
- package/dist/ui/components/App.d.ts.map +1 -1
- package/dist/ui/components/App.js +605 -96
- package/dist/ui/components/App.js.map +1 -1
- package/dist/ui/components/CircularSelectInput.d.ts +24 -0
- package/dist/ui/components/CircularSelectInput.d.ts.map +1 -0
- package/dist/ui/components/CircularSelectInput.js +71 -0
- package/dist/ui/components/CircularSelectInput.js.map +1 -0
- package/dist/ui/components/ErrorBoundary.d.ts +3 -2
- package/dist/ui/components/ErrorBoundary.d.ts.map +1 -1
- package/dist/ui/components/ErrorBoundary.js +29 -1
- package/dist/ui/components/ErrorBoundary.js.map +1 -1
- package/dist/ui/components/InputBox.d.ts +4 -0
- package/dist/ui/components/InputBox.d.ts.map +1 -1
- package/dist/ui/components/InputBox.js +343 -21
- package/dist/ui/components/InputBox.js.map +1 -1
- package/dist/ui/components/InteractiveShell.d.ts +6 -0
- package/dist/ui/components/InteractiveShell.d.ts.map +1 -1
- package/dist/ui/components/InteractiveShell.js +57 -6
- package/dist/ui/components/InteractiveShell.js.map +1 -1
- package/dist/ui/components/MCPAddScreen.d.ts +13 -0
- package/dist/ui/components/MCPAddScreen.d.ts.map +1 -0
- package/dist/ui/components/MCPAddScreen.js +54 -0
- package/dist/ui/components/MCPAddScreen.js.map +1 -0
- package/dist/ui/components/MCPListScreen.d.ts +17 -0
- package/dist/ui/components/MCPListScreen.d.ts.map +1 -0
- package/dist/ui/components/MCPListScreen.js +50 -0
- package/dist/ui/components/MCPListScreen.js.map +1 -0
- package/dist/ui/components/MCPServerListScreen.d.ts +16 -0
- package/dist/ui/components/MCPServerListScreen.d.ts.map +1 -0
- package/dist/ui/components/MCPServerListScreen.js +59 -0
- package/dist/ui/components/MCPServerListScreen.js.map +1 -0
- package/dist/ui/components/MonitorModeAIPanel.d.ts +23 -0
- package/dist/ui/components/MonitorModeAIPanel.d.ts.map +1 -0
- package/dist/ui/components/MonitorModeAIPanel.js +69 -0
- package/dist/ui/components/MonitorModeAIPanel.js.map +1 -0
- package/dist/ui/components/MultiLineInput.d.ts +13 -0
- package/dist/ui/components/MultiLineInput.d.ts.map +1 -0
- package/dist/ui/components/MultiLineInput.js +289 -0
- package/dist/ui/components/MultiLineInput.js.map +1 -0
- package/dist/ui/components/StatusBar.d.ts +2 -0
- package/dist/ui/components/StatusBar.d.ts.map +1 -1
- package/dist/ui/components/StatusBar.js +33 -2
- package/dist/ui/components/StatusBar.js.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.d.ts.map +1 -1
- package/dist/ui/components/ToolExecutionMessage.js +231 -13
- package/dist/ui/components/ToolExecutionMessage.js.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.d.ts.map +1 -1
- package/dist/ui/components/VersionUpdatePrompt.js +3 -2
- package/dist/ui/components/VersionUpdatePrompt.js.map +1 -1
- package/dist/utils/command-history.d.ts +12 -2
- package/dist/utils/command-history.d.ts.map +1 -1
- package/dist/utils/command-history.js +57 -13
- package/dist/utils/command-history.js.map +1 -1
- package/dist/utils/input-classifier.js +1 -1
- package/dist/utils/input-classifier.js.map +1 -1
- package/package.json +2 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import React, { useState, useCallback } from 'react';
|
|
1
|
+
import React, { useState, useCallback, useMemo } 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,13 +24,21 @@ 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';
|
|
30
39
|
import { logDebug, logError } from '../../utils/logger.js';
|
|
31
40
|
import { getTerminalDimensions } from '../../hooks/useTerminalDimensions.js';
|
|
41
|
+
import { ConfigManager } from '../../config/manager.js';
|
|
32
42
|
// Banner item with stable timestamp - created once outside component
|
|
33
43
|
const BANNER_ITEM = { id: '__banner__', role: '__banner__', content: '', timestamp: new Date(0) };
|
|
34
44
|
const MessageList = React.memo(({ history, current, showBanner }) => {
|
|
@@ -137,12 +147,18 @@ const RenameInputScreen = ({ currentTitle, onRename, onCancel }) => {
|
|
|
137
147
|
React.createElement(Box, { marginTop: 1 },
|
|
138
148
|
React.createElement(Text, { dimColor: true }, "Press Enter to save, ESC to cancel"))));
|
|
139
149
|
};
|
|
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 }) => {
|
|
150
|
+
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
151
|
const { exit } = useApp();
|
|
152
|
+
// Calculate limit for paginated lists (75% of terminal height)
|
|
153
|
+
const listLimit = Math.max(5, Math.floor((process.stdout.rows || 24) * 0.75));
|
|
142
154
|
const autoAcceptRef = React.useRef(false);
|
|
143
155
|
const setAutoModeCallbackRef = React.useRef(null);
|
|
144
156
|
// Connectivity State
|
|
145
157
|
const isConnected = useConnectivity();
|
|
158
|
+
// Agent control mode refs for input detection
|
|
159
|
+
const lastOutputTimeRef = React.useRef(Date.now());
|
|
160
|
+
const inputDetectionPendingRef = React.useRef(false);
|
|
161
|
+
const detectionDebounceRef = React.useRef(null);
|
|
146
162
|
// Helper to clear screen
|
|
147
163
|
const clearScreen = useCallback(() => {
|
|
148
164
|
process.stdout.write('\x1b[2J\x1b[3J\x1b[H');
|
|
@@ -195,6 +211,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
195
211
|
isAiWorking: false,
|
|
196
212
|
currentTokens: 0,
|
|
197
213
|
maxTokens: getMaxTokensForModel(initialModel || 'gemini-2.5-flash'),
|
|
214
|
+
contextLimitReached: false,
|
|
198
215
|
shellState: undefined,
|
|
199
216
|
isInteractiveEditorMode: false,
|
|
200
217
|
pickerOptions: undefined,
|
|
@@ -203,8 +220,10 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
203
220
|
passwordRequest: undefined,
|
|
204
221
|
connectionStatus: undefined,
|
|
205
222
|
backgroundTaskCount: 0,
|
|
223
|
+
subAgentCount: 0,
|
|
206
224
|
sessionQuotaExhausted: false,
|
|
207
225
|
sessionQuotaTimeRemaining: '',
|
|
226
|
+
aiAutoSuggest: false,
|
|
208
227
|
});
|
|
209
228
|
// Track last terminal width to detect actual width changes
|
|
210
229
|
const lastTerminalWidthRef = React.useRef(process.stdout.columns || 80);
|
|
@@ -270,6 +289,20 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
270
289
|
}
|
|
271
290
|
};
|
|
272
291
|
}, [clearScreen]);
|
|
292
|
+
// Handle global keyboard shortcuts
|
|
293
|
+
useInput((input, key) => {
|
|
294
|
+
// Escape key handling for background task screens
|
|
295
|
+
if (key.escape) {
|
|
296
|
+
if (state.screen === 'background-task-list' || state.screen === 'background-task-cancel') {
|
|
297
|
+
clearScreen();
|
|
298
|
+
setState(prev => ({
|
|
299
|
+
...prev,
|
|
300
|
+
screen: 'chat',
|
|
301
|
+
backgroundTasks: undefined // Clear the list
|
|
302
|
+
}));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
});
|
|
273
306
|
// Check for version updates on mount
|
|
274
307
|
React.useEffect(() => {
|
|
275
308
|
checkForUpdates().then(versionInfo => {
|
|
@@ -292,6 +325,14 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
292
325
|
React.useEffect(() => {
|
|
293
326
|
const loadModelsConfig = async () => {
|
|
294
327
|
try {
|
|
328
|
+
// Initialize ConfigManager to load general settings
|
|
329
|
+
const configManager = new ConfigManager();
|
|
330
|
+
const generalConfig = configManager.load();
|
|
331
|
+
setState(prev => ({
|
|
332
|
+
...prev,
|
|
333
|
+
aiAutoSuggest: generalConfig.aiAutoSuggest === true
|
|
334
|
+
}));
|
|
335
|
+
// Import quickLog for debugging
|
|
295
336
|
// Import quickLog for debugging
|
|
296
337
|
const { quickLog } = await import('../../utils/conversation-logger.js');
|
|
297
338
|
quickLog(`[${new Date().toISOString()}] [App] loadModelsConfig effect starting, currentModel: ${initialModel}\n`);
|
|
@@ -323,6 +364,17 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
323
364
|
React.useEffect(() => {
|
|
324
365
|
autoAcceptRef.current = state.autoAcceptMode;
|
|
325
366
|
}, [state.autoAcceptMode]);
|
|
367
|
+
// Push shell output updates to InputDetectionAgent when agent control is active
|
|
368
|
+
// This ensures the detection agent always has the latest output for polling
|
|
369
|
+
React.useEffect(() => {
|
|
370
|
+
if (state.shellState?.isAgentControlled && state.shellState?.shellId && state.shellState?.output) {
|
|
371
|
+
InputDetectionAgent.updateOutput(state.shellState.shellId, state.shellState.output);
|
|
372
|
+
}
|
|
373
|
+
}, [state.shellState?.output, state.shellState?.isAgentControlled, state.shellState?.shellId]);
|
|
374
|
+
// Ref to track if we should strictly block incoming AI updates (Zombie Agent Protection)
|
|
375
|
+
// This is set to true when an Agent-Controlled shell exits, essentially killing the "AI Agent"
|
|
376
|
+
// associated with it from the UI perspective. It is reset when the user manually inputs a new message.
|
|
377
|
+
const ignoreIncomingAIUpdatesRef = React.useRef(false);
|
|
326
378
|
// NOTE: Token count is now updated via onTokenCountUpdate callback from cli-adapter
|
|
327
379
|
// which tracks the actual AI conversation history including system prompt.
|
|
328
380
|
// The old UI-based calculation has been removed to avoid overwriting correct values.
|
|
@@ -335,6 +387,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
335
387
|
// Set up callback to receive streaming chunks - only once on mount
|
|
336
388
|
React.useEffect(() => {
|
|
337
389
|
onResponseStream((chunk) => {
|
|
390
|
+
// CIRCUIT BREAKER: Block incoming messages from "Zombie Agents"
|
|
391
|
+
if (ignoreIncomingAIUpdatesRef.current)
|
|
392
|
+
return;
|
|
338
393
|
isStreamingRef.current = true; // Mark that we're streaming
|
|
339
394
|
// Check if terminal is large enough for streaming
|
|
340
395
|
const dimensions = getTerminalDimensions();
|
|
@@ -438,6 +493,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
438
493
|
// Set up callback to receive thought chunks
|
|
439
494
|
React.useEffect(() => {
|
|
440
495
|
onThoughtStream((thought) => {
|
|
496
|
+
// CIRCUIT BREAKER: Block incoming thoughts from "Zombie Agents"
|
|
497
|
+
if (ignoreIncomingAIUpdatesRef.current)
|
|
498
|
+
return;
|
|
441
499
|
// Debug logging to file
|
|
442
500
|
try {
|
|
443
501
|
quickLog(`[${new Date().toISOString()}] [App] Received thought chunk (${thought.length} chars): ${thought.substring(0, 100)}\n`);
|
|
@@ -529,6 +587,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
529
587
|
// Set up callback for when thinking completes
|
|
530
588
|
React.useEffect(() => {
|
|
531
589
|
onThoughtComplete((durationSeconds) => {
|
|
590
|
+
// CIRCUIT BREAKER: Block incoming thought completion from "Zombie Agents"
|
|
591
|
+
if (ignoreIncomingAIUpdatesRef.current)
|
|
592
|
+
return;
|
|
532
593
|
try {
|
|
533
594
|
quickLog(`[${new Date().toISOString()}] [App] onThoughtComplete called with ${durationSeconds}s\n`);
|
|
534
595
|
}
|
|
@@ -565,12 +626,28 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
565
626
|
});
|
|
566
627
|
});
|
|
567
628
|
}, [onThoughtComplete]);
|
|
629
|
+
// Helper function to check if a message should be skipped (thought-only with no content or tools)
|
|
630
|
+
const shouldSkipMessage = (message) => {
|
|
631
|
+
if (!message)
|
|
632
|
+
return false;
|
|
633
|
+
// Skip if it's an assistant message with only thinking duration and no content or tool execution
|
|
634
|
+
if (message.role === 'assistant' &&
|
|
635
|
+
message.thinkingDuration !== undefined &&
|
|
636
|
+
message.content.trim() === '' &&
|
|
637
|
+
!message.toolExecution) {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
return false;
|
|
641
|
+
};
|
|
568
642
|
// Set up callback for direct messages (slash commands) - add directly to history
|
|
569
643
|
React.useEffect(() => {
|
|
570
644
|
onDirectMessage((message) => {
|
|
645
|
+
// CIRCUIT BREAKER: Block incoming direct messages from "Zombie Agents"
|
|
646
|
+
if (ignoreIncomingAIUpdatesRef.current)
|
|
647
|
+
return;
|
|
571
648
|
setState(prev => {
|
|
572
|
-
// Move current message to history if exists
|
|
573
|
-
const newHistory = prev.currentMessage
|
|
649
|
+
// Move current message to history if exists (skip thought-only messages)
|
|
650
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
574
651
|
? [...prev.messageHistory, prev.currentMessage]
|
|
575
652
|
: prev.messageHistory;
|
|
576
653
|
// Create system message for slash command response
|
|
@@ -594,6 +671,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
594
671
|
// Set up callback to receive complete responses (for slash commands and non-streaming responses)
|
|
595
672
|
React.useEffect(() => {
|
|
596
673
|
onResponseReceived((message) => {
|
|
674
|
+
// CIRCUIT BREAKER: Block incoming responses from "Zombie Agents"
|
|
675
|
+
if (ignoreIncomingAIUpdatesRef.current)
|
|
676
|
+
return;
|
|
597
677
|
// If we were streaming, move the current message to history immediately
|
|
598
678
|
if (isStreamingRef.current) {
|
|
599
679
|
isStreamingRef.current = false; // Reset for next message
|
|
@@ -702,8 +782,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
702
782
|
timestamp: now,
|
|
703
783
|
shouldStream: false // Command responses should appear instantly
|
|
704
784
|
};
|
|
705
|
-
// If we have a current message that's an assistant message, move it to history
|
|
706
|
-
if (prev.currentMessage && prev.currentMessage.role === 'assistant') {
|
|
785
|
+
// If we have a current message that's an assistant message, move it to history (skip thought-only)
|
|
786
|
+
if (prev.currentMessage && prev.currentMessage.role === 'assistant' && !shouldSkipMessage(prev.currentMessage)) {
|
|
707
787
|
return {
|
|
708
788
|
...prev,
|
|
709
789
|
messageHistory: [...prev.messageHistory, prev.currentMessage],
|
|
@@ -806,6 +886,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
806
886
|
}));
|
|
807
887
|
});
|
|
808
888
|
}, []); // Empty dependency array - only register once
|
|
889
|
+
// Set up callback for sub-agent count change
|
|
890
|
+
React.useEffect(() => {
|
|
891
|
+
onSubAgentCountChange((count) => {
|
|
892
|
+
setState(prev => ({
|
|
893
|
+
...prev,
|
|
894
|
+
subAgentCount: count
|
|
895
|
+
}));
|
|
896
|
+
});
|
|
897
|
+
}, []); // Empty dependency array - only register once
|
|
809
898
|
// Set up callback for setting Auto mode (used after background task starts)
|
|
810
899
|
React.useEffect(() => {
|
|
811
900
|
onSetAutoModeSetup((enabled) => {
|
|
@@ -943,6 +1032,65 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
943
1032
|
});
|
|
944
1033
|
});
|
|
945
1034
|
}, []); // Empty dependency array - only register once
|
|
1035
|
+
// Set up callback for MCP Add screen
|
|
1036
|
+
React.useEffect(() => {
|
|
1037
|
+
onMCPAddScreenSetup(() => {
|
|
1038
|
+
clearScreen();
|
|
1039
|
+
setState(prev => ({
|
|
1040
|
+
...prev,
|
|
1041
|
+
screen: 'mcp-add',
|
|
1042
|
+
isLoading: false
|
|
1043
|
+
}));
|
|
1044
|
+
});
|
|
1045
|
+
}, []); // Empty dependency array - only register once
|
|
1046
|
+
// Set up callback for MCP Remove screen
|
|
1047
|
+
React.useEffect(() => {
|
|
1048
|
+
onMCPRemoveScreenSetup((servers) => {
|
|
1049
|
+
clearScreen();
|
|
1050
|
+
setState(prev => ({
|
|
1051
|
+
...prev,
|
|
1052
|
+
screen: 'mcp-remove',
|
|
1053
|
+
mcpServers: servers,
|
|
1054
|
+
isLoading: false
|
|
1055
|
+
}));
|
|
1056
|
+
});
|
|
1057
|
+
}, []); // Empty dependency array - only register once
|
|
1058
|
+
// Set up callback for MCP Enable screen
|
|
1059
|
+
React.useEffect(() => {
|
|
1060
|
+
onMCPEnableScreenSetup((servers) => {
|
|
1061
|
+
clearScreen();
|
|
1062
|
+
setState(prev => ({
|
|
1063
|
+
...prev,
|
|
1064
|
+
screen: 'mcp-enable',
|
|
1065
|
+
mcpServers: servers,
|
|
1066
|
+
isLoading: false
|
|
1067
|
+
}));
|
|
1068
|
+
});
|
|
1069
|
+
}, []); // Empty dependency array - only register once
|
|
1070
|
+
// Set up callback for MCP Disable screen
|
|
1071
|
+
React.useEffect(() => {
|
|
1072
|
+
onMCPDisableScreenSetup((servers) => {
|
|
1073
|
+
clearScreen();
|
|
1074
|
+
setState(prev => ({
|
|
1075
|
+
...prev,
|
|
1076
|
+
screen: 'mcp-disable',
|
|
1077
|
+
mcpServers: servers,
|
|
1078
|
+
isLoading: false
|
|
1079
|
+
}));
|
|
1080
|
+
});
|
|
1081
|
+
}, []); // Empty dependency array - only register once
|
|
1082
|
+
// Set up callback for MCP List screen
|
|
1083
|
+
React.useEffect(() => {
|
|
1084
|
+
onMCPListScreenSetup((servers) => {
|
|
1085
|
+
clearScreen();
|
|
1086
|
+
setState(prev => ({
|
|
1087
|
+
...prev,
|
|
1088
|
+
screen: 'mcp-list',
|
|
1089
|
+
mcpListData: servers,
|
|
1090
|
+
isLoading: false
|
|
1091
|
+
}));
|
|
1092
|
+
});
|
|
1093
|
+
}, []); // Empty dependency array - only register once
|
|
946
1094
|
// Sync UI message history to CLI adapter whenever it changes
|
|
947
1095
|
React.useEffect(() => {
|
|
948
1096
|
onUIMessageHistoryUpdate(state.messageHistory);
|
|
@@ -950,6 +1098,9 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
950
1098
|
// Set up callback for tool execution updates
|
|
951
1099
|
React.useEffect(() => {
|
|
952
1100
|
onToolExecutionUpdate((update) => {
|
|
1101
|
+
// CIRCUIT BREAKER: Block incoming tool updates from "Zombie Agents"
|
|
1102
|
+
if (ignoreIncomingAIUpdatesRef.current)
|
|
1103
|
+
return;
|
|
953
1104
|
// Special handling for execute_command
|
|
954
1105
|
if (update.toolName === 'execute_command') {
|
|
955
1106
|
try {
|
|
@@ -958,6 +1109,28 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
958
1109
|
catch (e) { }
|
|
959
1110
|
// STARTING EXECUTION
|
|
960
1111
|
if (update.status === 'executing') {
|
|
1112
|
+
// Special handling for shell_input - DO NOT overwrite shell state
|
|
1113
|
+
// The agent is sending input to an EXISTING shell, so we must preserve the current shell state
|
|
1114
|
+
if (update.arguments?.shell_input) {
|
|
1115
|
+
setState(prev => ({
|
|
1116
|
+
...prev,
|
|
1117
|
+
// Move current message to history if needed
|
|
1118
|
+
messageHistory: prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1119
|
+
? [...prev.messageHistory, prev.currentMessage]
|
|
1120
|
+
: prev.messageHistory,
|
|
1121
|
+
// Update current message to show the tool execution
|
|
1122
|
+
currentMessage: {
|
|
1123
|
+
id: `tool-execute_command-${Date.now()}`,
|
|
1124
|
+
role: 'tool',
|
|
1125
|
+
content: '',
|
|
1126
|
+
timestamp: new Date(),
|
|
1127
|
+
toolExecution: { ...update }
|
|
1128
|
+
},
|
|
1129
|
+
isLoading: false,
|
|
1130
|
+
isAiWorking: true
|
|
1131
|
+
}));
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
961
1134
|
const command = update.arguments?.command || update.arguments?.CommandLine || update.arguments?.commandLine || '';
|
|
962
1135
|
// Start a pending shell with a short delay
|
|
963
1136
|
// This prevents the UI from flickering for very fast commands (like ls)
|
|
@@ -973,6 +1146,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
973
1146
|
setState(prev => ({
|
|
974
1147
|
...prev,
|
|
975
1148
|
shellState: {
|
|
1149
|
+
shellId: `shell-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`,
|
|
976
1150
|
command: cmd,
|
|
977
1151
|
cwd: cwd,
|
|
978
1152
|
output: bufferedOutput,
|
|
@@ -981,10 +1155,11 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
981
1155
|
error: undefined,
|
|
982
1156
|
isFocused: false,
|
|
983
1157
|
isPty: update.arguments?.isPty || false,
|
|
984
|
-
remoteContext: update.arguments?.remoteContext
|
|
1158
|
+
remoteContext: update.arguments?.remoteContext,
|
|
1159
|
+
isAgentStarted: true // Mark as agent-started since it came from execute_command tool
|
|
985
1160
|
},
|
|
986
|
-
// IMPORTANT: Always preserve current message by moving it to history first
|
|
987
|
-
messageHistory: prev.currentMessage
|
|
1161
|
+
// IMPORTANT: Always preserve current message by moving it to history first (skip thought-only)
|
|
1162
|
+
messageHistory: prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
988
1163
|
? [...prev.messageHistory, prev.currentMessage]
|
|
989
1164
|
: prev.messageHistory,
|
|
990
1165
|
currentMessage: {
|
|
@@ -1040,8 +1215,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1040
1215
|
arguments: update.arguments
|
|
1041
1216
|
}
|
|
1042
1217
|
};
|
|
1043
|
-
// Move current message to history if exists
|
|
1044
|
-
const newHistory = prev.currentMessage
|
|
1218
|
+
// Move current message to history if exists (skip thought-only)
|
|
1219
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1045
1220
|
? [...prev.messageHistory, prev.currentMessage]
|
|
1046
1221
|
: prev.messageHistory;
|
|
1047
1222
|
return {
|
|
@@ -1078,12 +1253,44 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1078
1253
|
arguments: update.arguments
|
|
1079
1254
|
}
|
|
1080
1255
|
};
|
|
1256
|
+
// Check if this was just an input string sent to an existing shell
|
|
1257
|
+
const isShellInput = update.arguments?.shell_input;
|
|
1258
|
+
// Reset input detection pending flag so we can detect NEXT prompt
|
|
1259
|
+
if (update.status === 'completed' || update.status === 'error') {
|
|
1260
|
+
inputDetectionPendingRef.current = false;
|
|
1261
|
+
}
|
|
1262
|
+
// Check if we hit a dead shell error during agent control
|
|
1263
|
+
const isDeadShellError = update.status === 'error' && update.error?.includes('No active interactive shell');
|
|
1264
|
+
// STRICT ISOLATION TRIGGER: If agent-controlled shell is done (completed or dead), kill the agent loop
|
|
1265
|
+
// LOGIC SPLIT:
|
|
1266
|
+
// 1. Manual Shell (User started, then Alt+I): Strict Sub-Agent behavior. Kill IMMEDIATELY on exit/completion.
|
|
1267
|
+
// 2. Agent Shell (Agent started via execute_command): Main Agent behavior. Kill only on ERROR. Allow success pass-through.
|
|
1268
|
+
const isAgentStarted = prev.shellState?.isAgentStarted;
|
|
1269
|
+
const shouldKill = isAgentStarted ? isDeadShellError : (update.status === 'completed' || isDeadShellError);
|
|
1270
|
+
if (prev.shellState?.isAgentControlled && shouldKill) {
|
|
1271
|
+
try {
|
|
1272
|
+
quickLog(`[${new Date().toISOString()}] [App] Strict Isolation: Agent Controlled shell exited/died. Engaging circuit breaker. (AgentStarted: ${isAgentStarted})\n`);
|
|
1273
|
+
}
|
|
1274
|
+
catch (e) { }
|
|
1275
|
+
ignoreIncomingAIUpdatesRef.current = true;
|
|
1276
|
+
// Terminate the shell input agent session
|
|
1277
|
+
if (prev.shellState?.shellId) {
|
|
1278
|
+
ShellInputAgent.terminateSession(prev.shellState.shellId);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1081
1281
|
return {
|
|
1082
1282
|
...prev,
|
|
1083
|
-
|
|
1283
|
+
// If it's just shell input, preserve the shell state UNLESS it's a dead shell error
|
|
1284
|
+
shellState: (isShellInput && !isDeadShellError) ? prev.shellState : undefined,
|
|
1084
1285
|
messageHistory: [...prev.messageHistory, shellMessage],
|
|
1085
1286
|
currentMessage: null,
|
|
1086
|
-
|
|
1287
|
+
// Determine if AI should keep working:
|
|
1288
|
+
// - Normal shell input: preserve state (keep thrusting if in chain)
|
|
1289
|
+
// - Agent-started shell completed successfully: preserve state (agent loop continues)
|
|
1290
|
+
// - Dead shell error or manual shell completed: STOP thrusting (reset to false)
|
|
1291
|
+
isAiWorking: (isShellInput && !isDeadShellError) ? prev.isAiWorking
|
|
1292
|
+
: (isAgentStarted && !shouldKill) ? prev.isAiWorking
|
|
1293
|
+
: false
|
|
1087
1294
|
};
|
|
1088
1295
|
});
|
|
1089
1296
|
return;
|
|
@@ -1177,7 +1384,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1177
1384
|
// Carry the thinkingDuration to the tool message, don't add empty message to history
|
|
1178
1385
|
pendingThinkingDuration = prev.currentMessage.thinkingDuration;
|
|
1179
1386
|
try {
|
|
1180
|
-
quickLog(`[${new Date().toISOString()}] [App]
|
|
1387
|
+
quickLog(`[${new Date().toISOString()}] [App] Skipping thought-only message (no content), saving thinkingDuration ${pendingThinkingDuration}s for tool ${update.toolName}\n`);
|
|
1181
1388
|
}
|
|
1182
1389
|
catch (e) { }
|
|
1183
1390
|
}
|
|
@@ -1209,7 +1416,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1209
1416
|
}
|
|
1210
1417
|
catch (e) { }
|
|
1211
1418
|
let newHistory = prev.messageHistory;
|
|
1212
|
-
if (prev.currentMessage) {
|
|
1419
|
+
if (prev.currentMessage && !shouldSkipMessage(prev.currentMessage)) {
|
|
1213
1420
|
newHistory = [...prev.messageHistory, prev.currentMessage];
|
|
1214
1421
|
}
|
|
1215
1422
|
return {
|
|
@@ -1235,8 +1442,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1235
1442
|
timestamp: new Date(),
|
|
1236
1443
|
connectionStatus: status
|
|
1237
1444
|
};
|
|
1238
|
-
// Move current message to history if exists
|
|
1239
|
-
const newHistory = prev.currentMessage
|
|
1445
|
+
// Move current message to history if exists (skip thought-only messages)
|
|
1446
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1240
1447
|
? [...prev.messageHistory, prev.currentMessage]
|
|
1241
1448
|
: prev.messageHistory;
|
|
1242
1449
|
return {
|
|
@@ -1265,8 +1472,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1265
1472
|
currentMessage: null
|
|
1266
1473
|
};
|
|
1267
1474
|
}
|
|
1268
|
-
// Otherwise just add to history
|
|
1269
|
-
const newHistory = prev.currentMessage
|
|
1475
|
+
// Otherwise just add to history (skip thought-only messages)
|
|
1476
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1270
1477
|
? [...prev.messageHistory, prev.currentMessage, finalConnectionMessage]
|
|
1271
1478
|
: [...prev.messageHistory, finalConnectionMessage];
|
|
1272
1479
|
return {
|
|
@@ -1361,14 +1568,65 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1361
1568
|
pendingShellRef.current.output += update.chunk;
|
|
1362
1569
|
return;
|
|
1363
1570
|
}
|
|
1571
|
+
// Update last output time for stall detection
|
|
1572
|
+
lastOutputTimeRef.current = Date.now();
|
|
1364
1573
|
setState(prev => {
|
|
1365
1574
|
// Update shell state if it's an execute_command
|
|
1366
1575
|
if (update.toolName === 'execute_command' && prev.shellState) {
|
|
1576
|
+
const newOutput = prev.shellState.output + update.chunk;
|
|
1577
|
+
// If agent control is enabled, schedule input detection
|
|
1578
|
+
if (prev.shellState.isAgentControlled && prev.shellState.isRunning && !inputDetectionPendingRef.current) {
|
|
1579
|
+
// Clear previous debounce timer
|
|
1580
|
+
if (detectionDebounceRef.current) {
|
|
1581
|
+
clearTimeout(detectionDebounceRef.current);
|
|
1582
|
+
}
|
|
1583
|
+
// Debounce: wait 500ms after last output before checking for input requirement
|
|
1584
|
+
// This ensures we don't trigger prematurely during fast output
|
|
1585
|
+
detectionDebounceRef.current = setTimeout(() => {
|
|
1586
|
+
const timeSinceLastOutput = Date.now() - lastOutputTimeRef.current;
|
|
1587
|
+
const detection = detectInputRequirement(newOutput, timeSinceLastOutput, 1000);
|
|
1588
|
+
if (detection.requiresInput && detection.confidence !== 'low') {
|
|
1589
|
+
inputDetectionPendingRef.current = true;
|
|
1590
|
+
// Add monitoring message
|
|
1591
|
+
const promptContext = extractPromptContext(newOutput);
|
|
1592
|
+
const inputType = getInputTypeDescription(detection);
|
|
1593
|
+
setState(innerPrev => {
|
|
1594
|
+
if (!innerPrev.shellState)
|
|
1595
|
+
return innerPrev;
|
|
1596
|
+
const newMessage = {
|
|
1597
|
+
id: `msg-${Date.now()}`,
|
|
1598
|
+
type: 'thinking',
|
|
1599
|
+
content: `Detected input prompt: "${promptContext}" (${inputType})`,
|
|
1600
|
+
timestamp: new Date()
|
|
1601
|
+
};
|
|
1602
|
+
return {
|
|
1603
|
+
...innerPrev,
|
|
1604
|
+
shellState: {
|
|
1605
|
+
...innerPrev.shellState,
|
|
1606
|
+
monitoringMessages: [...(innerPrev.shellState.monitoringMessages || []), newMessage]
|
|
1607
|
+
}
|
|
1608
|
+
};
|
|
1609
|
+
});
|
|
1610
|
+
// Trigger AI re-prompting with shell context via isolated ShellInputAgent
|
|
1611
|
+
const shellId = prev.shellState?.shellId;
|
|
1612
|
+
if (shellId && ShellInputAgent.isSessionActive(shellId)) {
|
|
1613
|
+
ShellInputAgent.handleInputPrompt(shellId, newOutput, promptContext, inputType, detection.confidence);
|
|
1614
|
+
}
|
|
1615
|
+
else {
|
|
1616
|
+
quickLog(`[${new Date().toISOString()}] [Debounce] ShellInputAgent session not active! shellId=${shellId}\n`);
|
|
1617
|
+
}
|
|
1618
|
+
// Reset pending flag after a delay to allow for next detection
|
|
1619
|
+
setTimeout(() => {
|
|
1620
|
+
inputDetectionPendingRef.current = false;
|
|
1621
|
+
}, 3000);
|
|
1622
|
+
}
|
|
1623
|
+
}, 500);
|
|
1624
|
+
}
|
|
1367
1625
|
return {
|
|
1368
1626
|
...prev,
|
|
1369
1627
|
shellState: {
|
|
1370
1628
|
...prev.shellState,
|
|
1371
|
-
output:
|
|
1629
|
+
output: newOutput
|
|
1372
1630
|
}
|
|
1373
1631
|
};
|
|
1374
1632
|
}
|
|
@@ -1390,7 +1648,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1390
1648
|
return prev;
|
|
1391
1649
|
});
|
|
1392
1650
|
});
|
|
1393
|
-
}, []);
|
|
1651
|
+
}, [onMessage]);
|
|
1394
1652
|
// Set up callback to receive plan mode changes
|
|
1395
1653
|
React.useEffect(() => {
|
|
1396
1654
|
onPlanModeChange((planMode) => {
|
|
@@ -1448,6 +1706,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1448
1706
|
}));
|
|
1449
1707
|
});
|
|
1450
1708
|
}, [onTokenCountUpdate]);
|
|
1709
|
+
// Set up callback to receive context limit state from cli-adapter
|
|
1710
|
+
React.useEffect(() => {
|
|
1711
|
+
onContextLimitReached((reached) => {
|
|
1712
|
+
setState(prev => ({
|
|
1713
|
+
...prev,
|
|
1714
|
+
contextLimitReached: reached
|
|
1715
|
+
}));
|
|
1716
|
+
});
|
|
1717
|
+
}, [onContextLimitReached]);
|
|
1451
1718
|
// Set up callback to receive session quota updates from cli-adapter
|
|
1452
1719
|
React.useEffect(() => {
|
|
1453
1720
|
onSessionQuotaUpdate((remaining, canSend, timeRemaining) => {
|
|
@@ -1458,6 +1725,36 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1458
1725
|
}));
|
|
1459
1726
|
});
|
|
1460
1727
|
}, [onSessionQuotaUpdate]);
|
|
1728
|
+
// Set up callback for when AI answers a shell prompt (updates thinking message to show tick)
|
|
1729
|
+
React.useEffect(() => {
|
|
1730
|
+
onPromptAnswered((shellId) => {
|
|
1731
|
+
setState(prev => {
|
|
1732
|
+
if (!prev.shellState || prev.shellState.shellId !== shellId) {
|
|
1733
|
+
return prev;
|
|
1734
|
+
}
|
|
1735
|
+
// Find the last 'thinking' message that isn't already answered and mark it
|
|
1736
|
+
const messages = prev.shellState.monitoringMessages || [];
|
|
1737
|
+
let updated = false;
|
|
1738
|
+
const updatedMessages = [...messages].reverse().map((msg, idx) => {
|
|
1739
|
+
if (!updated && msg.type === 'thinking' && !msg.isAnswered) {
|
|
1740
|
+
updated = true;
|
|
1741
|
+
return { ...msg, isAnswered: true };
|
|
1742
|
+
}
|
|
1743
|
+
return msg;
|
|
1744
|
+
}).reverse();
|
|
1745
|
+
if (!updated) {
|
|
1746
|
+
return prev;
|
|
1747
|
+
}
|
|
1748
|
+
return {
|
|
1749
|
+
...prev,
|
|
1750
|
+
shellState: {
|
|
1751
|
+
...prev.shellState,
|
|
1752
|
+
monitoringMessages: updatedMessages
|
|
1753
|
+
}
|
|
1754
|
+
};
|
|
1755
|
+
});
|
|
1756
|
+
});
|
|
1757
|
+
}, [onPromptAnswered]);
|
|
1461
1758
|
// Set up callback for plan approval requests
|
|
1462
1759
|
React.useEffect(() => {
|
|
1463
1760
|
onPlanApprovalRequest(async (plan) => {
|
|
@@ -1505,8 +1802,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1505
1802
|
completionNote
|
|
1506
1803
|
}
|
|
1507
1804
|
};
|
|
1508
|
-
// Move current message to history if exists
|
|
1509
|
-
const newHistory = prev.currentMessage
|
|
1805
|
+
// Move current message to history if exists (skip thought-only)
|
|
1806
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1510
1807
|
? [...prev.messageHistory, prev.currentMessage, taskCompletedMessage]
|
|
1511
1808
|
: [...prev.messageHistory, taskCompletedMessage];
|
|
1512
1809
|
return {
|
|
@@ -1667,6 +1964,15 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1667
1964
|
}));
|
|
1668
1965
|
return;
|
|
1669
1966
|
}
|
|
1967
|
+
// If in an MCP screen, return to chat
|
|
1968
|
+
if (state.screen === 'mcp-add' || state.screen === 'mcp-remove' || state.screen === 'mcp-enable' || state.screen === 'mcp-disable' || state.screen === 'mcp-list') {
|
|
1969
|
+
setState(prev => ({
|
|
1970
|
+
...prev,
|
|
1971
|
+
screen: 'chat',
|
|
1972
|
+
mcpServers: undefined
|
|
1973
|
+
}));
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1670
1976
|
// If AI is working, cancel the operation
|
|
1671
1977
|
if (state.isLoading || state.isAiWorking) {
|
|
1672
1978
|
// Cancel the current AI request
|
|
@@ -1811,8 +2117,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1811
2117
|
// In command mode, add command to history immediately
|
|
1812
2118
|
if (state.commandMode) {
|
|
1813
2119
|
setState(prev => {
|
|
1814
|
-
// Move current message to history if exists
|
|
1815
|
-
const newHistory = prev.currentMessage
|
|
2120
|
+
// Move current message to history if exists (skip thought-only)
|
|
2121
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1816
2122
|
? [...prev.messageHistory, prev.currentMessage]
|
|
1817
2123
|
: prev.messageHistory;
|
|
1818
2124
|
const commandMessage = {
|
|
@@ -1833,6 +2139,33 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1833
2139
|
}
|
|
1834
2140
|
else if (!isSlashCommand) {
|
|
1835
2141
|
// Only add user message to display if it's not a slash command
|
|
2142
|
+
// INTERRUPT HANDLING: If AI is currently working, cancel and clean up first
|
|
2143
|
+
// This allows the new message to replace the interrupted turn
|
|
2144
|
+
if (state.isAiWorking || state.isLoading) {
|
|
2145
|
+
// Cancel current request (cli-adapter will clean up its history)
|
|
2146
|
+
onCancelRequest();
|
|
2147
|
+
// Small delay to allow abort to propagate before new request starts
|
|
2148
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
2149
|
+
// Clean up UI state: remove any partial/interrupted messages
|
|
2150
|
+
setState(prev => {
|
|
2151
|
+
// Filter out the last user message (the one we're replacing)
|
|
2152
|
+
let cleanedHistory = [...prev.messageHistory];
|
|
2153
|
+
for (let i = cleanedHistory.length - 1; i >= 0; i--) {
|
|
2154
|
+
if (cleanedHistory[i].role === 'user') {
|
|
2155
|
+
cleanedHistory.splice(i, 1);
|
|
2156
|
+
break; // Only remove the most recent user message
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
return {
|
|
2160
|
+
...prev,
|
|
2161
|
+
messageHistory: cleanedHistory,
|
|
2162
|
+
currentMessage: null,
|
|
2163
|
+
isLoading: false,
|
|
2164
|
+
isAiWorking: false,
|
|
2165
|
+
approvalRequest: undefined, // Clear any pending approval
|
|
2166
|
+
};
|
|
2167
|
+
});
|
|
2168
|
+
}
|
|
1836
2169
|
// Show user message IMMEDIATELY with image breadcrumbs (uploading state)
|
|
1837
2170
|
// Images will be uploaded in background, then AI will be called
|
|
1838
2171
|
const hasImagesToUpload = clipboardImages && clipboardImages.length > 0;
|
|
@@ -1850,7 +2183,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1850
2183
|
// Generate a stable message ID that we can use to update the message later
|
|
1851
2184
|
const userMessageId = `user-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
1852
2185
|
setState(prev => {
|
|
1853
|
-
const newHistory = prev.currentMessage
|
|
2186
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1854
2187
|
? [...prev.messageHistory, prev.currentMessage]
|
|
1855
2188
|
: prev.messageHistory;
|
|
1856
2189
|
const userMessage = {
|
|
@@ -1993,8 +2326,8 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
1993
2326
|
content: `❌ Error: ${error.message || 'Unknown error occurred'}`,
|
|
1994
2327
|
timestamp: new Date()
|
|
1995
2328
|
};
|
|
1996
|
-
// Move current to history if exists and add error
|
|
1997
|
-
const newHistory = prev.currentMessage
|
|
2329
|
+
// Move current to history if exists and add error (skip thought-only)
|
|
2330
|
+
const newHistory = prev.currentMessage && !shouldSkipMessage(prev.currentMessage)
|
|
1998
2331
|
? [...prev.messageHistory, prev.currentMessage, errorMessage]
|
|
1999
2332
|
: [...prev.messageHistory, errorMessage];
|
|
2000
2333
|
return {
|
|
@@ -2024,6 +2357,12 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2024
2357
|
const handleInputValueChange = useCallback((value) => {
|
|
2025
2358
|
preservedInputTextRef.current = value;
|
|
2026
2359
|
}, []);
|
|
2360
|
+
// Memoize session commands to prevent unnecessary re-renders in InputBox
|
|
2361
|
+
const sessionCommands = useMemo(() => {
|
|
2362
|
+
return state.messageHistory
|
|
2363
|
+
.filter(m => m.role === 'user')
|
|
2364
|
+
.map(m => m.content);
|
|
2365
|
+
}, [state.messageHistory]);
|
|
2027
2366
|
// If in interactive editor mode, render minimal UI to keep Ink mounted
|
|
2028
2367
|
// but don't render any visible content - the editor has the screen
|
|
2029
2368
|
if (state.isInteractiveEditorMode) {
|
|
@@ -2079,7 +2418,55 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2079
2418
|
return;
|
|
2080
2419
|
}
|
|
2081
2420
|
onShellSignal(signal);
|
|
2421
|
+
}, shellId: state.shellState.shellId, isAgentControlled: state.shellState.isAgentControlled, onToggleAgentControl: () => {
|
|
2422
|
+
// Toggle agent control mode
|
|
2423
|
+
const wasAgentControlled = state.shellState?.isAgentControlled;
|
|
2424
|
+
const willBeAgentControlled = !wasAgentControlled;
|
|
2425
|
+
const currentOutput = state.shellState?.output || '';
|
|
2426
|
+
const isShellRunning = state.shellState?.isRunning;
|
|
2427
|
+
// Log the toggle using the imported quickLog
|
|
2428
|
+
quickLog(`[${new Date().toISOString()}] [AgentControl] Toggle: ${wasAgentControlled} -> ${willBeAgentControlled}, output length: ${currentOutput.length}, isRunning: ${isShellRunning}\n`);
|
|
2429
|
+
setState(prev => ({
|
|
2430
|
+
...prev,
|
|
2431
|
+
shellState: prev.shellState ? {
|
|
2432
|
+
...prev.shellState,
|
|
2433
|
+
isAgentControlled: willBeAgentControlled,
|
|
2434
|
+
monitoringMessages: willBeAgentControlled ? [] : prev.shellState.monitoringMessages
|
|
2435
|
+
} : undefined
|
|
2436
|
+
}));
|
|
2437
|
+
// Start or terminate ShellInputAgent session
|
|
2438
|
+
const shellId = state.shellState?.shellId;
|
|
2439
|
+
const shellCommand = state.shellState?.command || '';
|
|
2440
|
+
const shellCwd = state.shellState?.cwd || process.cwd();
|
|
2441
|
+
if (willBeAgentControlled && shellId) {
|
|
2442
|
+
// Start isolated shell input agent session with main conversation context
|
|
2443
|
+
const mainConversation = getMainConversation();
|
|
2444
|
+
ShellInputAgent.startSession(shellId, shellCommand, shellCwd, mainConversation);
|
|
2445
|
+
quickLog(`[${new Date().toISOString()}] [AgentControl] Started ShellInputAgent session for ${shellId}\n`);
|
|
2446
|
+
// Start AI-based input detection polling
|
|
2447
|
+
InputDetectionAgent.startPolling(shellId, currentOutput, // Pass current output directly
|
|
2448
|
+
shellCommand, shellCwd);
|
|
2449
|
+
quickLog(`[${new Date().toISOString()}] [AgentControl] Started AI-based input detection polling for ${shellId}\n`);
|
|
2450
|
+
}
|
|
2451
|
+
else if (!willBeAgentControlled && shellId) {
|
|
2452
|
+
// Stop AI-based input detection polling
|
|
2453
|
+
InputDetectionAgent.stopPolling(shellId);
|
|
2454
|
+
quickLog(`[${new Date().toISOString()}] [AgentControl] Stopped AI-based input detection polling for ${shellId}\n`);
|
|
2455
|
+
// Terminate shell input agent session
|
|
2456
|
+
ShellInputAgent.terminateSession(shellId);
|
|
2457
|
+
quickLog(`[${new Date().toISOString()}] [AgentControl] Terminated ShellInputAgent session for ${shellId}\n`);
|
|
2458
|
+
}
|
|
2082
2459
|
} })),
|
|
2460
|
+
state.shellState?.isAgentControlled && state.shellState?.isFocused && state.shellState?.isRunning && (React.createElement(MonitorModeAIPanel, { messages: [
|
|
2461
|
+
...(state.shellState.monitoringMessages || []),
|
|
2462
|
+
// Inject current assistant message if present (so AI thoughts/responses appear in panel)
|
|
2463
|
+
...(state.currentMessage?.role === 'assistant' && state.currentMessage.content ? [{
|
|
2464
|
+
id: state.currentMessage.id,
|
|
2465
|
+
type: 'text', // Cast to literal type to match MonitorMessage interface
|
|
2466
|
+
content: state.currentMessage.content,
|
|
2467
|
+
timestamp: state.currentMessage.timestamp
|
|
2468
|
+
}] : [])
|
|
2469
|
+
], maxHeight: Math.floor((process.stdout.rows || 24) / 2) - 5, isActive: true })),
|
|
2083
2470
|
state.isAiWorking && !state.shellState && !state.approvalRequest &&
|
|
2084
2471
|
!(state.currentMessage?.toolExecution?.status === 'executing') && (React.createElement(Box, { marginBottom: 1, paddingLeft: 1 },
|
|
2085
2472
|
React.createElement(LoadingIndicator, { key: "loading-indicator" }),
|
|
@@ -2090,12 +2477,14 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2090
2477
|
resolve(approved);
|
|
2091
2478
|
}
|
|
2092
2479
|
} })) : (React.createElement(InputBox, { key: "input-box", onSubmit: (value, clipboardImages) => {
|
|
2480
|
+
// Reset "Zombie Agent" protection on new manual input
|
|
2481
|
+
ignoreIncomingAIUpdatesRef.current = false;
|
|
2093
2482
|
// Clear preserved input on submit
|
|
2094
2483
|
preservedInputTextRef.current = '';
|
|
2095
2484
|
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) => {
|
|
2485
|
+
}, 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
2486
|
setAutoModeCallbackRef.current = callback;
|
|
2098
|
-
}, sessionQuotaExhausted: state.sessionQuotaExhausted, sessionQuotaTimeRemaining: state.sessionQuotaTimeRemaining }))),
|
|
2487
|
+
}, sessionQuotaExhausted: state.sessionQuotaExhausted, sessionQuotaTimeRemaining: state.sessionQuotaTimeRemaining, aiAutoSuggestEnabled: state.aiAutoSuggest, sessionCommands: sessionCommands }))),
|
|
2099
2488
|
state.showExitWarning && (React.createElement(Box, { marginTop: 1 },
|
|
2100
2489
|
React.createElement(Text, { color: "#ffaa00", bold: true }, "\u26A0\uFE0F Press Ctrl+C again to exit"))),
|
|
2101
2490
|
!isConnected && (React.createElement(Box, { marginTop: 0, marginLeft: state.showExitWarning ? 2 : 0 },
|
|
@@ -2140,7 +2529,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2140
2529
|
state.screen === 'chat-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
2141
2530
|
React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDCDA Resume a Previous Chat"),
|
|
2142
2531
|
React.createElement(Box, { marginTop: 1 },
|
|
2143
|
-
React.createElement(
|
|
2532
|
+
React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
|
|
2144
2533
|
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
2145
2534
|
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2146
2535
|
const isCurrent = chat.id === state.currentChatId;
|
|
@@ -2199,7 +2588,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2199
2588
|
state.screen === 'chat-delete-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
2200
2589
|
React.createElement(Text, { color: "#00ccff", bold: true }, "\uD83D\uDDD1\uFE0F Select a Chat to Delete"),
|
|
2201
2590
|
React.createElement(Box, { marginTop: 1 },
|
|
2202
|
-
React.createElement(
|
|
2591
|
+
React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
|
|
2203
2592
|
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
2204
2593
|
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2205
2594
|
const isCurrent = chat.id === state.currentChatId;
|
|
@@ -2254,35 +2643,42 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2254
2643
|
"\uD83D\uDCDA Saved Chats (",
|
|
2255
2644
|
state.chatPickerChats.length,
|
|
2256
2645
|
")"),
|
|
2257
|
-
React.createElement(Box, { marginTop: 1
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2646
|
+
React.createElement(Box, { marginTop: 1 },
|
|
2647
|
+
React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
|
|
2648
|
+
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
2649
|
+
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2650
|
+
const isCurrent = chat.id === state.currentChatId;
|
|
2651
|
+
const env = chat.environment || 'local';
|
|
2652
|
+
return {
|
|
2653
|
+
label: `${isCurrent ? '● ' : ' '}${chat.title}`,
|
|
2654
|
+
envLabel: `[${env}]`,
|
|
2655
|
+
dateLabel: `${date} ${time}`,
|
|
2656
|
+
msgLabel: `${chat.messageCount} msgs`,
|
|
2657
|
+
value: chat.id,
|
|
2658
|
+
isCurrent,
|
|
2659
|
+
environment: env
|
|
2660
|
+
};
|
|
2661
|
+
}), itemComponent: ({ isSelected, label, isCurrent, envLabel, dateLabel, msgLabel, environment }) => (React.createElement(Box, null,
|
|
2662
|
+
React.createElement(Text, { color: isSelected ? '#00ccff' : (isCurrent ? '#00cc66' : 'white'), bold: isSelected || isCurrent },
|
|
2663
|
+
isSelected ? '> ' : ' ',
|
|
2664
|
+
label),
|
|
2665
|
+
React.createElement(Text, { color: environment === 'local' ? 'gray' : (environment?.startsWith('ssh') ? '#ff9966' : environment?.startsWith('wsl') ? '#66ff99' : '#66ccff') },
|
|
2666
|
+
" ",
|
|
2667
|
+
envLabel),
|
|
2668
|
+
React.createElement(Text, { color: "gray" },
|
|
2669
|
+
" ",
|
|
2670
|
+
dateLabel),
|
|
2671
|
+
React.createElement(Text, { color: "gray" }, " - "),
|
|
2672
|
+
React.createElement(Text, { color: "gray" }, msgLabel))), onSelect: () => {
|
|
2673
|
+
// Just close the view
|
|
2674
|
+
setState(prev => ({ ...prev, screen: 'chat' }));
|
|
2675
|
+
} })),
|
|
2280
2676
|
React.createElement(Box, { marginTop: 1 },
|
|
2281
2677
|
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
|
|
2282
2678
|
state.screen === 'chat-rename-picker' && state.chatPickerChats && (React.createElement(Box, { flexDirection: "column", borderStyle: "round", borderColor: "#00ccff", paddingX: 1 },
|
|
2283
2679
|
React.createElement(Text, { color: "#00ccff", bold: true }, "\u270F\uFE0F Select a Chat to Rename"),
|
|
2284
2680
|
React.createElement(Box, { marginTop: 1 },
|
|
2285
|
-
React.createElement(
|
|
2681
|
+
React.createElement(CircularSelectInput, { limit: listLimit, items: state.chatPickerChats.map((chat) => {
|
|
2286
2682
|
const date = new Date(chat.updatedAt).toLocaleDateString();
|
|
2287
2683
|
const time = new Date(chat.updatedAt).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2288
2684
|
const isCurrent = chat.id === state.currentChatId;
|
|
@@ -2398,7 +2794,7 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2398
2794
|
}));
|
|
2399
2795
|
} }))),
|
|
2400
2796
|
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 }, "
|
|
2797
|
+
React.createElement(Text, { color: "#9966ff", bold: true }, "Running Background Tasks"),
|
|
2402
2798
|
React.createElement(Text, { dimColor: true }, "Select a task to view its output in focus mode"),
|
|
2403
2799
|
React.createElement(Box, { marginTop: 1 },
|
|
2404
2800
|
React.createElement(SelectInput, { items: state.backgroundTasks.map((task) => {
|
|
@@ -2409,35 +2805,49 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2409
2805
|
cwd: task.cwd,
|
|
2410
2806
|
duration: `${durationSec}s`,
|
|
2411
2807
|
isRunning: task.isRunning,
|
|
2412
|
-
outputPreview: task.outputPreview
|
|
2808
|
+
outputPreview: task.outputPreview // Pass raw preview, sanitize in component
|
|
2413
2809
|
};
|
|
2414
|
-
}), itemComponent: ({ isSelected, label, cwd, duration, isRunning, outputPreview }) =>
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2810
|
+
}), itemComponent: ({ isSelected, label, cwd, duration, isRunning, outputPreview }) => {
|
|
2811
|
+
// Calculate max width for wrapping (terminal width - border padding - left padding)
|
|
2812
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
2813
|
+
const maxTextWidth = Math.max(terminalWidth - 10, 40);
|
|
2814
|
+
// Truncate cwd if too long
|
|
2815
|
+
const truncatedCwd = cwd && cwd.length > maxTextWidth
|
|
2816
|
+
? cwd.slice(0, maxTextWidth - 3) + '...'
|
|
2817
|
+
: cwd;
|
|
2818
|
+
// Truncate output preview if too long (accounting for └─ prefix)
|
|
2819
|
+
const maxPreviewWidth = maxTextWidth - 3;
|
|
2820
|
+
// Sanitize: Strip ANSI codes and replace newlines with spaces
|
|
2821
|
+
const cleanPreview = outputPreview ? stripAnsi(outputPreview).replace(/[\r\n]+/g, ' ').trim() : '';
|
|
2822
|
+
const truncatedPreview = cleanPreview.length > maxPreviewWidth
|
|
2823
|
+
? cleanPreview.slice(0, maxPreviewWidth - 3) + '...'
|
|
2824
|
+
: cleanPreview;
|
|
2825
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
2826
|
+
React.createElement(Box, null,
|
|
2827
|
+
React.createElement(Text, { color: isSelected ? '#9966ff' : 'white', bold: isSelected },
|
|
2828
|
+
isSelected ? '> ' : ' ',
|
|
2829
|
+
label),
|
|
2830
|
+
React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
|
|
2831
|
+
" [",
|
|
2832
|
+
isRunning ? 'running' : 'done',
|
|
2833
|
+
"]"),
|
|
2834
|
+
React.createElement(Text, { color: "gray" },
|
|
2835
|
+
" ",
|
|
2836
|
+
duration)),
|
|
2837
|
+
React.createElement(Box, { paddingLeft: 4 },
|
|
2838
|
+
React.createElement(Text, { color: "gray", wrap: "truncate" }, truncatedCwd)),
|
|
2839
|
+
truncatedPreview && (React.createElement(Box, { paddingLeft: 4 },
|
|
2840
|
+
React.createElement(Text, { color: "gray", dimColor: true, wrap: "truncate" },
|
|
2841
|
+
"\u2514\u2500 ",
|
|
2842
|
+
truncatedPreview)))));
|
|
2843
|
+
}, onSelect: (item) => {
|
|
2434
2844
|
setState(prev => ({ ...prev, screen: 'chat', isLoading: false }));
|
|
2435
2845
|
onBackgroundTaskSelection(item.value);
|
|
2436
2846
|
} })),
|
|
2437
2847
|
React.createElement(Box, { marginTop: 1 },
|
|
2438
2848
|
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
|
|
2439
2849
|
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 }, "
|
|
2850
|
+
React.createElement(Text, { color: "#ff6666", bold: true }, "Cancel a Background Task"),
|
|
2441
2851
|
React.createElement(Text, { dimColor: true }, "Select a task to terminate it"),
|
|
2442
2852
|
React.createElement(Box, { marginTop: 1 },
|
|
2443
2853
|
React.createElement(SelectInput, { items: state.backgroundTasks.map((task) => {
|
|
@@ -2449,26 +2859,125 @@ export const App = ({ onMessage, onCancelRequest, initialModel, initialPlanMode,
|
|
|
2449
2859
|
duration: `${durationSec}s`,
|
|
2450
2860
|
isRunning: task.isRunning
|
|
2451
2861
|
};
|
|
2452
|
-
}), itemComponent: ({ isSelected, label, cwd, duration, isRunning }) =>
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
React.createElement(
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2862
|
+
}), itemComponent: ({ isSelected, label, cwd, duration, isRunning }) => {
|
|
2863
|
+
// Calculate max width for wrapping (terminal width - border padding - left padding)
|
|
2864
|
+
const terminalWidth = process.stdout.columns || 80;
|
|
2865
|
+
const maxTextWidth = Math.max(terminalWidth - 10, 40);
|
|
2866
|
+
// Truncate cwd if too long
|
|
2867
|
+
const truncatedCwd = cwd && cwd.length > maxTextWidth
|
|
2868
|
+
? cwd.slice(0, maxTextWidth - 3) + '...'
|
|
2869
|
+
: cwd;
|
|
2870
|
+
return (React.createElement(Box, { flexDirection: "column" },
|
|
2871
|
+
React.createElement(Box, null,
|
|
2872
|
+
React.createElement(Text, { color: isSelected ? '#ff6666' : 'white', bold: isSelected },
|
|
2873
|
+
isSelected ? '> ' : ' ',
|
|
2874
|
+
label),
|
|
2875
|
+
React.createElement(Text, { color: isRunning ? '#00cc66' : '#ffaa00' },
|
|
2876
|
+
" [",
|
|
2877
|
+
isRunning ? 'running' : 'done',
|
|
2878
|
+
"]"),
|
|
2879
|
+
React.createElement(Text, { color: "gray" },
|
|
2880
|
+
" ",
|
|
2881
|
+
duration)),
|
|
2882
|
+
React.createElement(Box, { paddingLeft: 4 },
|
|
2883
|
+
React.createElement(Text, { color: "gray", wrap: "truncate" }, truncatedCwd))));
|
|
2884
|
+
}, onSelect: (item) => {
|
|
2468
2885
|
setState(prev => ({ ...prev, screen: 'chat', isLoading: false }));
|
|
2469
2886
|
onBackgroundTaskCancel(item.value);
|
|
2470
2887
|
} })),
|
|
2471
2888
|
React.createElement(Box, { marginTop: 1 },
|
|
2472
|
-
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat"))))
|
|
2889
|
+
React.createElement(Text, { dimColor: true }, "Press ESC to return to chat")))),
|
|
2890
|
+
state.screen === 'mcp-add' && (React.createElement(MCPAddScreen, { onAdd: (config) => {
|
|
2891
|
+
const result = onMCPAddServer(config);
|
|
2892
|
+
if (result.success) {
|
|
2893
|
+
// Return to chat with success message
|
|
2894
|
+
const successMessage = {
|
|
2895
|
+
id: `mcp-add-${Date.now()}`,
|
|
2896
|
+
role: 'system',
|
|
2897
|
+
content: `✅ MCP server "${config.name}" added successfully!\n\nRun \`/mcp refresh\` to connect to the new server.`,
|
|
2898
|
+
timestamp: new Date()
|
|
2899
|
+
};
|
|
2900
|
+
setState(prev => ({
|
|
2901
|
+
...prev,
|
|
2902
|
+
screen: 'chat',
|
|
2903
|
+
messageHistory: [...prev.messageHistory, successMessage]
|
|
2904
|
+
}));
|
|
2905
|
+
}
|
|
2906
|
+
}, onCancel: () => {
|
|
2907
|
+
setState(prev => ({
|
|
2908
|
+
...prev,
|
|
2909
|
+
screen: 'chat'
|
|
2910
|
+
}));
|
|
2911
|
+
}, validateConfig: onMCPValidateConfig })),
|
|
2912
|
+
state.screen === 'mcp-remove' && state.mcpServers && (React.createElement(MCPServerListScreen, { mode: "remove", servers: state.mcpServers, onSelect: (serverName) => {
|
|
2913
|
+
onMCPRemoveServer(serverName);
|
|
2914
|
+
const successMessage = {
|
|
2915
|
+
id: `mcp-remove-${Date.now()}`,
|
|
2916
|
+
role: 'system',
|
|
2917
|
+
content: `✅ MCP server "${serverName}" removed from configuration.`,
|
|
2918
|
+
timestamp: new Date()
|
|
2919
|
+
};
|
|
2920
|
+
setState(prev => ({
|
|
2921
|
+
...prev,
|
|
2922
|
+
screen: 'chat',
|
|
2923
|
+
mcpServers: undefined,
|
|
2924
|
+
messageHistory: [...prev.messageHistory, successMessage]
|
|
2925
|
+
}));
|
|
2926
|
+
}, onCancel: () => {
|
|
2927
|
+
setState(prev => ({
|
|
2928
|
+
...prev,
|
|
2929
|
+
screen: 'chat',
|
|
2930
|
+
mcpServers: undefined
|
|
2931
|
+
}));
|
|
2932
|
+
} })),
|
|
2933
|
+
state.screen === 'mcp-enable' && state.mcpServers && (React.createElement(MCPServerListScreen, { mode: "enable", servers: state.mcpServers, onSelect: (serverName) => {
|
|
2934
|
+
onMCPEnableServer(serverName);
|
|
2935
|
+
const successMessage = {
|
|
2936
|
+
id: `mcp-enable-${Date.now()}`,
|
|
2937
|
+
role: 'system',
|
|
2938
|
+
content: `✅ MCP server "${serverName}" enabled!\n\nRun \`/mcp refresh\` to connect.`,
|
|
2939
|
+
timestamp: new Date()
|
|
2940
|
+
};
|
|
2941
|
+
setState(prev => ({
|
|
2942
|
+
...prev,
|
|
2943
|
+
screen: 'chat',
|
|
2944
|
+
mcpServers: undefined,
|
|
2945
|
+
messageHistory: [...prev.messageHistory, successMessage]
|
|
2946
|
+
}));
|
|
2947
|
+
}, onCancel: () => {
|
|
2948
|
+
setState(prev => ({
|
|
2949
|
+
...prev,
|
|
2950
|
+
screen: 'chat',
|
|
2951
|
+
mcpServers: undefined
|
|
2952
|
+
}));
|
|
2953
|
+
} })),
|
|
2954
|
+
state.screen === 'mcp-disable' && state.mcpServers && (React.createElement(MCPServerListScreen, { mode: "disable", servers: state.mcpServers, onSelect: (serverName) => {
|
|
2955
|
+
onMCPDisableServer(serverName);
|
|
2956
|
+
const successMessage = {
|
|
2957
|
+
id: `mcp-disable-${Date.now()}`,
|
|
2958
|
+
role: 'system',
|
|
2959
|
+
content: `⏸️ MCP server "${serverName}" disabled. AI will no longer use its tools.`,
|
|
2960
|
+
timestamp: new Date()
|
|
2961
|
+
};
|
|
2962
|
+
setState(prev => ({
|
|
2963
|
+
...prev,
|
|
2964
|
+
screen: 'chat',
|
|
2965
|
+
mcpServers: undefined,
|
|
2966
|
+
messageHistory: [...prev.messageHistory, successMessage]
|
|
2967
|
+
}));
|
|
2968
|
+
}, onCancel: () => {
|
|
2969
|
+
setState(prev => ({
|
|
2970
|
+
...prev,
|
|
2971
|
+
screen: 'chat',
|
|
2972
|
+
mcpServers: undefined
|
|
2973
|
+
}));
|
|
2974
|
+
} })),
|
|
2975
|
+
state.screen === 'mcp-list' && state.mcpListData && (React.createElement(MCPListScreen, { servers: state.mcpListData, onClose: () => {
|
|
2976
|
+
setState(prev => ({
|
|
2977
|
+
...prev,
|
|
2978
|
+
screen: 'chat',
|
|
2979
|
+
mcpListData: undefined
|
|
2980
|
+
}));
|
|
2981
|
+
} }))));
|
|
2473
2982
|
};
|
|
2474
2983
|
//# sourceMappingURL=App.js.map
|