snow-ai 0.4.15 → 0.4.17
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/bundle/cli.mjs +477445 -0
- package/bundle/sql-wasm.wasm +0 -0
- package/package.json +31 -26
- package/dist/agents/codebaseIndexAgent.d.ts +0 -102
- package/dist/agents/codebaseIndexAgent.js +0 -641
- package/dist/agents/codebaseReviewAgent.d.ts +0 -61
- package/dist/agents/codebaseReviewAgent.js +0 -301
- package/dist/agents/compactAgent.d.ts +0 -55
- package/dist/agents/compactAgent.js +0 -306
- package/dist/agents/promptOptimizeAgent.d.ts +0 -54
- package/dist/agents/promptOptimizeAgent.js +0 -268
- package/dist/agents/reviewAgent.d.ts +0 -50
- package/dist/agents/reviewAgent.js +0 -265
- package/dist/agents/summaryAgent.d.ts +0 -57
- package/dist/agents/summaryAgent.js +0 -260
- package/dist/api/anthropic.d.ts +0 -44
- package/dist/api/anthropic.js +0 -598
- package/dist/api/chat.d.ts +0 -73
- package/dist/api/chat.js +0 -386
- package/dist/api/embedding.d.ts +0 -34
- package/dist/api/embedding.js +0 -80
- package/dist/api/gemini.d.ts +0 -31
- package/dist/api/gemini.js +0 -445
- package/dist/api/models.d.ts +0 -15
- package/dist/api/models.js +0 -139
- package/dist/api/responses.d.ts +0 -38
- package/dist/api/responses.js +0 -515
- package/dist/api/systemPrompt.d.ts +0 -4
- package/dist/api/systemPrompt.js +0 -408
- package/dist/api/types.d.ts +0 -53
- package/dist/api/types.js +0 -4
- package/dist/app.d.ts +0 -8
- package/dist/app.js +0 -112
- package/dist/cli.d.ts +0 -2
- package/dist/cli.js +0 -199
- package/dist/hooks/useAgentPicker.d.ts +0 -14
- package/dist/hooks/useAgentPicker.js +0 -119
- package/dist/hooks/useClipboard.d.ts +0 -4
- package/dist/hooks/useClipboard.js +0 -175
- package/dist/hooks/useCommandHandler.d.ts +0 -35
- package/dist/hooks/useCommandHandler.js +0 -346
- package/dist/hooks/useCommandPanel.d.ts +0 -17
- package/dist/hooks/useCommandPanel.js +0 -114
- package/dist/hooks/useConversation.d.ts +0 -49
- package/dist/hooks/useConversation.js +0 -1052
- package/dist/hooks/useFilePicker.d.ts +0 -18
- package/dist/hooks/useFilePicker.js +0 -224
- package/dist/hooks/useGlobalExit.d.ts +0 -5
- package/dist/hooks/useGlobalExit.js +0 -34
- package/dist/hooks/useGlobalNavigation.d.ts +0 -6
- package/dist/hooks/useGlobalNavigation.js +0 -17
- package/dist/hooks/useHistoryNavigation.d.ts +0 -35
- package/dist/hooks/useHistoryNavigation.js +0 -133
- package/dist/hooks/useInputBuffer.d.ts +0 -6
- package/dist/hooks/useInputBuffer.js +0 -45
- package/dist/hooks/useKeyboardInput.d.ts +0 -80
- package/dist/hooks/useKeyboardInput.js +0 -608
- package/dist/hooks/useSessionManagement.d.ts +0 -10
- package/dist/hooks/useSessionManagement.js +0 -43
- package/dist/hooks/useSessionSave.d.ts +0 -8
- package/dist/hooks/useSessionSave.js +0 -63
- package/dist/hooks/useSnapshotState.d.ts +0 -26
- package/dist/hooks/useSnapshotState.js +0 -28
- package/dist/hooks/useStreamingState.d.ts +0 -33
- package/dist/hooks/useStreamingState.js +0 -105
- package/dist/hooks/useTerminalFocus.d.ts +0 -28
- package/dist/hooks/useTerminalFocus.js +0 -87
- package/dist/hooks/useTerminalSize.d.ts +0 -4
- package/dist/hooks/useTerminalSize.js +0 -20
- package/dist/hooks/useTodoPicker.d.ts +0 -16
- package/dist/hooks/useTodoPicker.js +0 -94
- package/dist/hooks/useToolConfirmation.d.ts +0 -19
- package/dist/hooks/useToolConfirmation.js +0 -61
- package/dist/hooks/useVSCodeState.d.ts +0 -8
- package/dist/hooks/useVSCodeState.js +0 -81
- package/dist/i18n/I18nContext.d.ts +0 -14
- package/dist/i18n/I18nContext.js +0 -24
- package/dist/i18n/index.d.ts +0 -3
- package/dist/i18n/index.js +0 -2
- package/dist/i18n/lang/en.d.ts +0 -2
- package/dist/i18n/lang/en.js +0 -502
- package/dist/i18n/lang/es.d.ts +0 -2
- package/dist/i18n/lang/es.js +0 -502
- package/dist/i18n/lang/ja.d.ts +0 -2
- package/dist/i18n/lang/ja.js +0 -502
- package/dist/i18n/lang/ko.d.ts +0 -2
- package/dist/i18n/lang/ko.js +0 -502
- package/dist/i18n/lang/zh-TW.d.ts +0 -2
- package/dist/i18n/lang/zh-TW.js +0 -502
- package/dist/i18n/lang/zh.d.ts +0 -2
- package/dist/i18n/lang/zh.js +0 -502
- package/dist/i18n/translations.d.ts +0 -2
- package/dist/i18n/translations.js +0 -14
- package/dist/i18n/types.d.ts +0 -478
- package/dist/i18n/types.js +0 -1
- package/dist/mcp/aceCodeSearch.d.ts +0 -247
- package/dist/mcp/aceCodeSearch.js +0 -1058
- package/dist/mcp/bash.d.ts +0 -50
- package/dist/mcp/bash.js +0 -153
- package/dist/mcp/codebaseSearch.d.ts +0 -44
- package/dist/mcp/codebaseSearch.js +0 -275
- package/dist/mcp/filesystem.d.ts +0 -392
- package/dist/mcp/filesystem.js +0 -1445
- package/dist/mcp/ideDiagnostics.d.ts +0 -36
- package/dist/mcp/ideDiagnostics.js +0 -90
- package/dist/mcp/notebook.d.ts +0 -10
- package/dist/mcp/notebook.js +0 -367
- package/dist/mcp/subagent.d.ts +0 -37
- package/dist/mcp/subagent.js +0 -113
- package/dist/mcp/todo.d.ts +0 -46
- package/dist/mcp/todo.js +0 -511
- package/dist/mcp/types/aceCodeSearch.types.d.ts +0 -92
- package/dist/mcp/types/aceCodeSearch.types.js +0 -4
- package/dist/mcp/types/bash.types.d.ts +0 -13
- package/dist/mcp/types/bash.types.js +0 -4
- package/dist/mcp/types/filesystem.types.d.ts +0 -210
- package/dist/mcp/types/filesystem.types.js +0 -27
- package/dist/mcp/types/todo.types.d.ts +0 -27
- package/dist/mcp/types/todo.types.js +0 -4
- package/dist/mcp/types/websearch.types.d.ts +0 -30
- package/dist/mcp/types/websearch.types.js +0 -4
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +0 -34
- package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +0 -146
- package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +0 -14
- package/dist/mcp/utils/aceCodeSearch/language.utils.js +0 -418
- package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +0 -31
- package/dist/mcp/utils/aceCodeSearch/search.utils.js +0 -136
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +0 -20
- package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +0 -141
- package/dist/mcp/utils/bash/security.utils.d.ts +0 -20
- package/dist/mcp/utils/bash/security.utils.js +0 -34
- package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +0 -39
- package/dist/mcp/utils/filesystem/batch-operations.utils.js +0 -182
- package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +0 -18
- package/dist/mcp/utils/filesystem/code-analysis.utils.js +0 -165
- package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +0 -16
- package/dist/mcp/utils/filesystem/match-finder.utils.js +0 -85
- package/dist/mcp/utils/filesystem/office-parser.utils.d.ts +0 -43
- package/dist/mcp/utils/filesystem/office-parser.utils.js +0 -163
- package/dist/mcp/utils/filesystem/path-fixer.utils.d.ts +0 -7
- package/dist/mcp/utils/filesystem/path-fixer.utils.js +0 -60
- package/dist/mcp/utils/filesystem/similarity.utils.d.ts +0 -22
- package/dist/mcp/utils/filesystem/similarity.utils.js +0 -75
- package/dist/mcp/utils/todo/date.utils.d.ts +0 -9
- package/dist/mcp/utils/todo/date.utils.js +0 -14
- package/dist/mcp/utils/websearch/browser.utils.d.ts +0 -8
- package/dist/mcp/utils/websearch/browser.utils.js +0 -58
- package/dist/mcp/utils/websearch/text.utils.d.ts +0 -16
- package/dist/mcp/utils/websearch/text.utils.js +0 -39
- package/dist/mcp/websearch.d.ts +0 -88
- package/dist/mcp/websearch.js +0 -375
- package/dist/test/logger-test.d.ts +0 -1
- package/dist/test/logger-test.js +0 -7
- package/dist/types/index.d.ts +0 -15
- package/dist/types/index.js +0 -1
- package/dist/ui/components/AgentPickerPanel.d.ts +0 -10
- package/dist/ui/components/AgentPickerPanel.js +0 -74
- package/dist/ui/components/ChatInput.d.ts +0 -46
- package/dist/ui/components/ChatInput.js +0 -379
- package/dist/ui/components/CommandPanel.d.ts +0 -15
- package/dist/ui/components/CommandPanel.js +0 -80
- package/dist/ui/components/DiffViewer.d.ts +0 -11
- package/dist/ui/components/DiffViewer.js +0 -178
- package/dist/ui/components/FileList.d.ts +0 -15
- package/dist/ui/components/FileList.js +0 -360
- package/dist/ui/components/FileRollbackConfirmation.d.ts +0 -8
- package/dist/ui/components/FileRollbackConfirmation.js +0 -108
- package/dist/ui/components/HelpPanel.d.ts +0 -2
- package/dist/ui/components/HelpPanel.js +0 -67
- package/dist/ui/components/MCPInfoPanel.d.ts +0 -2
- package/dist/ui/components/MCPInfoPanel.js +0 -108
- package/dist/ui/components/MCPInfoScreen.d.ts +0 -7
- package/dist/ui/components/MCPInfoScreen.js +0 -115
- package/dist/ui/components/MarkdownRenderer.d.ts +0 -6
- package/dist/ui/components/MarkdownRenderer.js +0 -70
- package/dist/ui/components/Menu.d.ts +0 -17
- package/dist/ui/components/Menu.js +0 -88
- package/dist/ui/components/MessageList.d.ts +0 -56
- package/dist/ui/components/MessageList.js +0 -97
- package/dist/ui/components/PendingMessages.d.ts +0 -13
- package/dist/ui/components/PendingMessages.js +0 -29
- package/dist/ui/components/PendingToolCalls.d.ts +0 -11
- package/dist/ui/components/PendingToolCalls.js +0 -35
- package/dist/ui/components/ScrollableSelectInput.d.ts +0 -29
- package/dist/ui/components/ScrollableSelectInput.js +0 -157
- package/dist/ui/components/SessionListPanel.d.ts +0 -7
- package/dist/ui/components/SessionListPanel.js +0 -175
- package/dist/ui/components/SessionListScreen.d.ts +0 -7
- package/dist/ui/components/SessionListScreen.js +0 -217
- package/dist/ui/components/SessionListScreenWrapper.d.ts +0 -7
- package/dist/ui/components/SessionListScreenWrapper.js +0 -14
- package/dist/ui/components/ShimmerText.d.ts +0 -9
- package/dist/ui/components/ShimmerText.js +0 -30
- package/dist/ui/components/TodoPickerPanel.d.ts +0 -14
- package/dist/ui/components/TodoPickerPanel.js +0 -119
- package/dist/ui/components/TodoTree.d.ts +0 -15
- package/dist/ui/components/TodoTree.js +0 -60
- package/dist/ui/components/ToolConfirmation.d.ts +0 -21
- package/dist/ui/components/ToolConfirmation.js +0 -204
- package/dist/ui/components/ToolResultPreview.d.ts +0 -13
- package/dist/ui/components/ToolResultPreview.js +0 -337
- package/dist/ui/components/UsagePanel.d.ts +0 -2
- package/dist/ui/components/UsagePanel.js +0 -394
- package/dist/ui/contexts/ThemeContext.d.ts +0 -13
- package/dist/ui/contexts/ThemeContext.js +0 -28
- package/dist/ui/pages/ChatScreen.d.ts +0 -6
- package/dist/ui/pages/ChatScreen.js +0 -1495
- package/dist/ui/pages/CodeBaseConfigScreen.d.ts +0 -8
- package/dist/ui/pages/CodeBaseConfigScreen.js +0 -350
- package/dist/ui/pages/ConfigScreen.d.ts +0 -8
- package/dist/ui/pages/ConfigScreen.js +0 -1101
- package/dist/ui/pages/CustomHeadersScreen.d.ts +0 -6
- package/dist/ui/pages/CustomHeadersScreen.js +0 -502
- package/dist/ui/pages/HeadlessModeScreen.d.ts +0 -7
- package/dist/ui/pages/HeadlessModeScreen.js +0 -381
- package/dist/ui/pages/LanguageSettingsScreen.d.ts +0 -7
- package/dist/ui/pages/LanguageSettingsScreen.js +0 -91
- package/dist/ui/pages/MCPConfigScreen.d.ts +0 -6
- package/dist/ui/pages/MCPConfigScreen.js +0 -55
- package/dist/ui/pages/ProxyConfigScreen.d.ts +0 -8
- package/dist/ui/pages/ProxyConfigScreen.js +0 -149
- package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +0 -7
- package/dist/ui/pages/SensitiveCommandConfigScreen.js +0 -271
- package/dist/ui/pages/SubAgentConfigScreen.d.ts +0 -9
- package/dist/ui/pages/SubAgentConfigScreen.js +0 -435
- package/dist/ui/pages/SubAgentListScreen.d.ts +0 -9
- package/dist/ui/pages/SubAgentListScreen.js +0 -131
- package/dist/ui/pages/SystemPromptConfigScreen.d.ts +0 -6
- package/dist/ui/pages/SystemPromptConfigScreen.js +0 -326
- package/dist/ui/pages/ThemeSettingsScreen.d.ts +0 -7
- package/dist/ui/pages/ThemeSettingsScreen.js +0 -106
- package/dist/ui/pages/WelcomeScreen.d.ts +0 -7
- package/dist/ui/pages/WelcomeScreen.js +0 -217
- package/dist/ui/themes/index.d.ts +0 -23
- package/dist/ui/themes/index.js +0 -140
- package/dist/utils/apiConfig.d.ts +0 -126
- package/dist/utils/apiConfig.js +0 -423
- package/dist/utils/autoCompress.d.ts +0 -15
- package/dist/utils/autoCompress.js +0 -24
- package/dist/utils/chatExporter.d.ts +0 -9
- package/dist/utils/chatExporter.js +0 -118
- package/dist/utils/checkpointManager.d.ts +0 -74
- package/dist/utils/checkpointManager.js +0 -181
- package/dist/utils/codebaseConfig.d.ts +0 -16
- package/dist/utils/codebaseConfig.js +0 -67
- package/dist/utils/codebaseDatabase.d.ts +0 -102
- package/dist/utils/codebaseDatabase.js +0 -333
- package/dist/utils/codebaseSearchEvents.d.ts +0 -16
- package/dist/utils/codebaseSearchEvents.js +0 -13
- package/dist/utils/commandExecutor.d.ts +0 -13
- package/dist/utils/commandExecutor.js +0 -26
- package/dist/utils/commands/agent.d.ts +0 -2
- package/dist/utils/commands/agent.js +0 -12
- package/dist/utils/commands/clear.d.ts +0 -2
- package/dist/utils/commands/clear.js +0 -12
- package/dist/utils/commands/compact.d.ts +0 -2
- package/dist/utils/commands/compact.js +0 -12
- package/dist/utils/commands/export.d.ts +0 -2
- package/dist/utils/commands/export.js +0 -12
- package/dist/utils/commands/help.d.ts +0 -2
- package/dist/utils/commands/help.js +0 -11
- package/dist/utils/commands/home.d.ts +0 -2
- package/dist/utils/commands/home.js +0 -34
- package/dist/utils/commands/ide.d.ts +0 -2
- package/dist/utils/commands/ide.js +0 -32
- package/dist/utils/commands/init.d.ts +0 -2
- package/dist/utils/commands/init.js +0 -93
- package/dist/utils/commands/mcp.d.ts +0 -2
- package/dist/utils/commands/mcp.js +0 -12
- package/dist/utils/commands/resume.d.ts +0 -2
- package/dist/utils/commands/resume.js +0 -12
- package/dist/utils/commands/review.d.ts +0 -2
- package/dist/utils/commands/review.js +0 -81
- package/dist/utils/commands/role.d.ts +0 -2
- package/dist/utils/commands/role.js +0 -37
- package/dist/utils/commands/todoPicker.d.ts +0 -2
- package/dist/utils/commands/todoPicker.js +0 -12
- package/dist/utils/commands/usage.d.ts +0 -2
- package/dist/utils/commands/usage.js +0 -12
- package/dist/utils/commands/yolo.d.ts +0 -2
- package/dist/utils/commands/yolo.js +0 -12
- package/dist/utils/configManager.d.ts +0 -45
- package/dist/utils/configManager.js +0 -303
- package/dist/utils/contextCompressor.d.ts +0 -16
- package/dist/utils/contextCompressor.js +0 -334
- package/dist/utils/devMode.d.ts +0 -13
- package/dist/utils/devMode.js +0 -54
- package/dist/utils/escapeHandler.d.ts +0 -79
- package/dist/utils/escapeHandler.js +0 -153
- package/dist/utils/fileDialog.d.ts +0 -9
- package/dist/utils/fileDialog.js +0 -74
- package/dist/utils/fileUtils.d.ts +0 -40
- package/dist/utils/fileUtils.js +0 -185
- package/dist/utils/historyManager.d.ts +0 -45
- package/dist/utils/historyManager.js +0 -159
- package/dist/utils/incrementalSnapshot.d.ts +0 -109
- package/dist/utils/incrementalSnapshot.js +0 -383
- package/dist/utils/index.d.ts +0 -11
- package/dist/utils/index.js +0 -18
- package/dist/utils/languageConfig.d.ts +0 -21
- package/dist/utils/languageConfig.js +0 -61
- package/dist/utils/logger.d.ts +0 -37
- package/dist/utils/logger.js +0 -122
- package/dist/utils/mcpToolsManager.d.ts +0 -52
- package/dist/utils/mcpToolsManager.js +0 -878
- package/dist/utils/messageFormatter.d.ts +0 -12
- package/dist/utils/messageFormatter.js +0 -115
- package/dist/utils/notebookManager.d.ts +0 -59
- package/dist/utils/notebookManager.js +0 -213
- package/dist/utils/patch-highlight.d.ts +0 -5
- package/dist/utils/patch-highlight.js +0 -23
- package/dist/utils/processManager.d.ts +0 -27
- package/dist/utils/processManager.js +0 -75
- package/dist/utils/proxyUtils.d.ts +0 -15
- package/dist/utils/proxyUtils.js +0 -50
- package/dist/utils/resourceMonitor.d.ts +0 -65
- package/dist/utils/resourceMonitor.js +0 -175
- package/dist/utils/retryUtils.d.ts +0 -49
- package/dist/utils/retryUtils.js +0 -303
- package/dist/utils/sensitiveCommandManager.d.ts +0 -53
- package/dist/utils/sensitiveCommandManager.js +0 -308
- package/dist/utils/sessionConverter.d.ts +0 -7
- package/dist/utils/sessionConverter.js +0 -306
- package/dist/utils/sessionManager.d.ts +0 -53
- package/dist/utils/sessionManager.js +0 -371
- package/dist/utils/subAgentConfig.d.ts +0 -50
- package/dist/utils/subAgentConfig.js +0 -221
- package/dist/utils/subAgentExecutor.d.ts +0 -40
- package/dist/utils/subAgentExecutor.js +0 -434
- package/dist/utils/terminal.d.ts +0 -5
- package/dist/utils/terminal.js +0 -13
- package/dist/utils/textBuffer.d.ts +0 -99
- package/dist/utils/textBuffer.js +0 -547
- package/dist/utils/textUtils.d.ts +0 -37
- package/dist/utils/textUtils.js +0 -102
- package/dist/utils/themeConfig.d.ts +0 -21
- package/dist/utils/themeConfig.js +0 -61
- package/dist/utils/todoPreprocessor.d.ts +0 -5
- package/dist/utils/todoPreprocessor.js +0 -18
- package/dist/utils/todoScanner.d.ts +0 -8
- package/dist/utils/todoScanner.js +0 -148
- package/dist/utils/toolDisplayConfig.d.ts +0 -16
- package/dist/utils/toolDisplayConfig.js +0 -47
- package/dist/utils/toolExecutor.d.ts +0 -37
- package/dist/utils/toolExecutor.js +0 -224
- package/dist/utils/usageLogger.d.ts +0 -11
- package/dist/utils/usageLogger.js +0 -114
- package/dist/utils/vscodeConnection.d.ts +0 -76
- package/dist/utils/vscodeConnection.js +0 -430
- package/dist/utils/workspaceSnapshot.d.ts +0 -63
- package/dist/utils/workspaceSnapshot.js +0 -300
|
@@ -1,1495 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect, useRef } from 'react';
|
|
2
|
-
import { Box, Text, useInput, Static, useStdout } from 'ink';
|
|
3
|
-
import Spinner from 'ink-spinner';
|
|
4
|
-
import Gradient from 'ink-gradient';
|
|
5
|
-
import ansiEscapes from 'ansi-escapes';
|
|
6
|
-
import { useI18n } from '../../i18n/I18nContext.js';
|
|
7
|
-
import { useTheme } from '../contexts/ThemeContext.js';
|
|
8
|
-
import ChatInput from '../components/ChatInput.js';
|
|
9
|
-
import PendingMessages from '../components/PendingMessages.js';
|
|
10
|
-
import MCPInfoScreen from '../components/MCPInfoScreen.js';
|
|
11
|
-
import MCPInfoPanel from '../components/MCPInfoPanel.js';
|
|
12
|
-
import SessionListPanel from '../components/SessionListPanel.js';
|
|
13
|
-
import UsagePanel from '../components/UsagePanel.js';
|
|
14
|
-
import HelpPanel from '../components/HelpPanel.js';
|
|
15
|
-
import MarkdownRenderer from '../components/MarkdownRenderer.js';
|
|
16
|
-
import ToolConfirmation from '../components/ToolConfirmation.js';
|
|
17
|
-
import DiffViewer from '../components/DiffViewer.js';
|
|
18
|
-
import ToolResultPreview from '../components/ToolResultPreview.js';
|
|
19
|
-
import FileRollbackConfirmation from '../components/FileRollbackConfirmation.js';
|
|
20
|
-
import ShimmerText from '../components/ShimmerText.js';
|
|
21
|
-
import { getOpenAiConfig } from '../../utils/apiConfig.js';
|
|
22
|
-
import { sessionManager } from '../../utils/sessionManager.js';
|
|
23
|
-
import { useSessionSave } from '../../hooks/useSessionSave.js';
|
|
24
|
-
import { useToolConfirmation } from '../../hooks/useToolConfirmation.js';
|
|
25
|
-
import { handleConversationWithTools } from '../../hooks/useConversation.js';
|
|
26
|
-
import { promptOptimizeAgent } from '../../agents/promptOptimizeAgent.js';
|
|
27
|
-
import { useVSCodeState } from '../../hooks/useVSCodeState.js';
|
|
28
|
-
import { useSnapshotState } from '../../hooks/useSnapshotState.js';
|
|
29
|
-
import { useStreamingState } from '../../hooks/useStreamingState.js';
|
|
30
|
-
import { useCommandHandler } from '../../hooks/useCommandHandler.js';
|
|
31
|
-
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
|
32
|
-
import { parseAndValidateFileReferences, createMessageWithFileInstructions, } from '../../utils/fileUtils.js';
|
|
33
|
-
import { executeCommand } from '../../utils/commandExecutor.js';
|
|
34
|
-
import { convertSessionMessagesToUI } from '../../utils/sessionConverter.js';
|
|
35
|
-
import { incrementalSnapshotManager } from '../../utils/incrementalSnapshot.js';
|
|
36
|
-
import { formatElapsedTime } from '../../utils/textUtils.js';
|
|
37
|
-
import { shouldAutoCompress, performAutoCompression, } from '../../utils/autoCompress.js';
|
|
38
|
-
import { CodebaseIndexAgent } from '../../agents/codebaseIndexAgent.js';
|
|
39
|
-
import { loadCodebaseConfig } from '../../utils/codebaseConfig.js';
|
|
40
|
-
import { codebaseSearchEvents } from '../../utils/codebaseSearchEvents.js';
|
|
41
|
-
import { logger } from '../../utils/logger.js';
|
|
42
|
-
export default function ChatScreen({ skipWelcome }) {
|
|
43
|
-
const { t } = useI18n();
|
|
44
|
-
const { theme } = useTheme();
|
|
45
|
-
const [messages, setMessages] = useState([]);
|
|
46
|
-
const [isSaving] = useState(false);
|
|
47
|
-
const [pendingMessages, setPendingMessages] = useState([]);
|
|
48
|
-
const pendingMessagesRef = useRef([]);
|
|
49
|
-
const hasAttemptedAutoVscodeConnect = useRef(false);
|
|
50
|
-
const userInterruptedRef = useRef(false); // Track if user manually interrupted via ESC
|
|
51
|
-
const [remountKey, setRemountKey] = useState(0);
|
|
52
|
-
const [showMcpInfo, setShowMcpInfo] = useState(false);
|
|
53
|
-
const [mcpPanelKey, setMcpPanelKey] = useState(0);
|
|
54
|
-
const [currentContextPercentage, setCurrentContextPercentage] = useState(0); // Track context percentage from ChatInput
|
|
55
|
-
const currentContextPercentageRef = useRef(0); // Use ref to avoid closure issues
|
|
56
|
-
// Sync state to ref
|
|
57
|
-
useEffect(() => {
|
|
58
|
-
currentContextPercentageRef.current = currentContextPercentage;
|
|
59
|
-
}, [currentContextPercentage]);
|
|
60
|
-
const [yoloMode, setYoloMode] = useState(() => {
|
|
61
|
-
// Load yolo mode from localStorage on initialization
|
|
62
|
-
try {
|
|
63
|
-
const saved = localStorage.getItem('snow-yolo-mode');
|
|
64
|
-
return saved === 'true';
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return false;
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
const [isCompressing, setIsCompressing] = useState(false);
|
|
71
|
-
const [compressionError, setCompressionError] = useState(null);
|
|
72
|
-
const [showSessionPanel, setShowSessionPanel] = useState(false);
|
|
73
|
-
const [showMcpPanel, setShowMcpPanel] = useState(false);
|
|
74
|
-
const [showUsagePanel, setShowUsagePanel] = useState(false);
|
|
75
|
-
const [showHelpPanel, setShowHelpPanel] = useState(false);
|
|
76
|
-
const [restoreInputContent, setRestoreInputContent] = useState(null);
|
|
77
|
-
const { columns: terminalWidth, rows: terminalHeight } = useTerminalSize();
|
|
78
|
-
const { stdout } = useStdout();
|
|
79
|
-
const workingDirectory = process.cwd();
|
|
80
|
-
const isInitialMount = useRef(true);
|
|
81
|
-
// Codebase indexing state
|
|
82
|
-
const [codebaseIndexing, setCodebaseIndexing] = useState(false);
|
|
83
|
-
const [codebaseProgress, setCodebaseProgress] = useState(null);
|
|
84
|
-
const [watcherEnabled, setWatcherEnabled] = useState(false);
|
|
85
|
-
const [fileUpdateNotification, setFileUpdateNotification] = useState(null);
|
|
86
|
-
const codebaseAgentRef = useRef(null);
|
|
87
|
-
// Use custom hooks
|
|
88
|
-
const streamingState = useStreamingState();
|
|
89
|
-
const vscodeState = useVSCodeState();
|
|
90
|
-
const snapshotState = useSnapshotState(messages.length);
|
|
91
|
-
// Use session save hook
|
|
92
|
-
const { saveMessage, clearSavedMessages, initializeFromSession } = useSessionSave();
|
|
93
|
-
// Sync pendingMessages to ref for real-time access in callbacks
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
pendingMessagesRef.current = pendingMessages;
|
|
96
|
-
}, [pendingMessages]);
|
|
97
|
-
// Track if commands are loaded
|
|
98
|
-
const [commandsLoaded, setCommandsLoaded] = useState(false);
|
|
99
|
-
// Load commands dynamically to avoid blocking initial render
|
|
100
|
-
useEffect(() => {
|
|
101
|
-
// Use Promise.all to load all commands in parallel
|
|
102
|
-
Promise.all([
|
|
103
|
-
import('../../utils/commands/clear.js'),
|
|
104
|
-
import('../../utils/commands/resume.js'),
|
|
105
|
-
import('../../utils/commands/mcp.js'),
|
|
106
|
-
import('../../utils/commands/yolo.js'),
|
|
107
|
-
import('../../utils/commands/init.js'),
|
|
108
|
-
import('../../utils/commands/ide.js'),
|
|
109
|
-
import('../../utils/commands/compact.js'),
|
|
110
|
-
import('../../utils/commands/home.js'),
|
|
111
|
-
import('../../utils/commands/review.js'),
|
|
112
|
-
import('../../utils/commands/role.js'),
|
|
113
|
-
import('../../utils/commands/usage.js'),
|
|
114
|
-
import('../../utils/commands/export.js'),
|
|
115
|
-
import('../../utils/commands/agent.js'),
|
|
116
|
-
import('../../utils/commands/todoPicker.js'),
|
|
117
|
-
import('../../utils/commands/help.js'),
|
|
118
|
-
])
|
|
119
|
-
.then(() => {
|
|
120
|
-
setCommandsLoaded(true);
|
|
121
|
-
})
|
|
122
|
-
.catch(error => {
|
|
123
|
-
console.error('Failed to load commands:', error);
|
|
124
|
-
// Still mark as loaded to allow app to continue
|
|
125
|
-
setCommandsLoaded(true);
|
|
126
|
-
});
|
|
127
|
-
}, []);
|
|
128
|
-
// Auto-start codebase indexing on mount if enabled
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
const startCodebaseIndexing = async () => {
|
|
131
|
-
try {
|
|
132
|
-
const config = loadCodebaseConfig();
|
|
133
|
-
// Only start if enabled and not already indexing
|
|
134
|
-
if (!config.enabled || codebaseIndexing) {
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
// Initialize agent
|
|
138
|
-
const agent = new CodebaseIndexAgent(workingDirectory);
|
|
139
|
-
codebaseAgentRef.current = agent;
|
|
140
|
-
// Check if indexing is needed
|
|
141
|
-
const progress = agent.getProgress();
|
|
142
|
-
// If indexing is already completed, start watcher and return early
|
|
143
|
-
if (progress.status === 'completed' && progress.totalChunks > 0) {
|
|
144
|
-
agent.startWatching(progressData => {
|
|
145
|
-
setCodebaseProgress({
|
|
146
|
-
totalFiles: progressData.totalFiles,
|
|
147
|
-
processedFiles: progressData.processedFiles,
|
|
148
|
-
totalChunks: progressData.totalChunks,
|
|
149
|
-
currentFile: progressData.currentFile,
|
|
150
|
-
status: progressData.status,
|
|
151
|
-
});
|
|
152
|
-
// Handle file update notifications
|
|
153
|
-
if (progressData.totalFiles === 0 && progressData.currentFile) {
|
|
154
|
-
setFileUpdateNotification({
|
|
155
|
-
file: progressData.currentFile,
|
|
156
|
-
timestamp: Date.now(),
|
|
157
|
-
});
|
|
158
|
-
// Clear notification after 3 seconds
|
|
159
|
-
setTimeout(() => {
|
|
160
|
-
setFileUpdateNotification(null);
|
|
161
|
-
}, 3000);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
setWatcherEnabled(true);
|
|
165
|
-
setCodebaseIndexing(false); // Ensure loading UI is hidden
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
168
|
-
// If watcher was enabled before but indexing not completed, restore it
|
|
169
|
-
const wasWatcherEnabled = agent.isWatcherEnabled();
|
|
170
|
-
if (wasWatcherEnabled) {
|
|
171
|
-
logger.info('Restoring file watcher from previous session');
|
|
172
|
-
agent.startWatching(progressData => {
|
|
173
|
-
setCodebaseProgress({
|
|
174
|
-
totalFiles: progressData.totalFiles,
|
|
175
|
-
processedFiles: progressData.processedFiles,
|
|
176
|
-
totalChunks: progressData.totalChunks,
|
|
177
|
-
currentFile: progressData.currentFile,
|
|
178
|
-
status: progressData.status,
|
|
179
|
-
});
|
|
180
|
-
// Handle file update notifications
|
|
181
|
-
if (progressData.totalFiles === 0 && progressData.currentFile) {
|
|
182
|
-
setFileUpdateNotification({
|
|
183
|
-
file: progressData.currentFile,
|
|
184
|
-
timestamp: Date.now(),
|
|
185
|
-
});
|
|
186
|
-
// Clear notification after 3 seconds
|
|
187
|
-
setTimeout(() => {
|
|
188
|
-
setFileUpdateNotification(null);
|
|
189
|
-
}, 3000);
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
setWatcherEnabled(true);
|
|
193
|
-
setCodebaseIndexing(false); // Ensure loading UI is hidden when restoring watcher
|
|
194
|
-
}
|
|
195
|
-
// Start or resume indexing in background
|
|
196
|
-
setCodebaseIndexing(true);
|
|
197
|
-
agent.start(progressData => {
|
|
198
|
-
setCodebaseProgress({
|
|
199
|
-
totalFiles: progressData.totalFiles,
|
|
200
|
-
processedFiles: progressData.processedFiles,
|
|
201
|
-
totalChunks: progressData.totalChunks,
|
|
202
|
-
currentFile: progressData.currentFile,
|
|
203
|
-
status: progressData.status,
|
|
204
|
-
});
|
|
205
|
-
// Handle file update notifications (when totalFiles is 0, it's a file update)
|
|
206
|
-
if (progressData.totalFiles === 0 && progressData.currentFile) {
|
|
207
|
-
setFileUpdateNotification({
|
|
208
|
-
file: progressData.currentFile,
|
|
209
|
-
timestamp: Date.now(),
|
|
210
|
-
});
|
|
211
|
-
// Clear notification after 3 seconds
|
|
212
|
-
setTimeout(() => {
|
|
213
|
-
setFileUpdateNotification(null);
|
|
214
|
-
}, 3000);
|
|
215
|
-
}
|
|
216
|
-
// Stop indexing when completed or error
|
|
217
|
-
if (progressData.status === 'completed' ||
|
|
218
|
-
progressData.status === 'error') {
|
|
219
|
-
setCodebaseIndexing(false);
|
|
220
|
-
// Start file watcher after initial indexing is completed
|
|
221
|
-
if (progressData.status === 'completed' && agent) {
|
|
222
|
-
agent.startWatching(watcherProgressData => {
|
|
223
|
-
setCodebaseProgress({
|
|
224
|
-
totalFiles: watcherProgressData.totalFiles,
|
|
225
|
-
processedFiles: watcherProgressData.processedFiles,
|
|
226
|
-
totalChunks: watcherProgressData.totalChunks,
|
|
227
|
-
currentFile: watcherProgressData.currentFile,
|
|
228
|
-
status: watcherProgressData.status,
|
|
229
|
-
});
|
|
230
|
-
// Handle file update notifications
|
|
231
|
-
if (watcherProgressData.totalFiles === 0 &&
|
|
232
|
-
watcherProgressData.currentFile) {
|
|
233
|
-
setFileUpdateNotification({
|
|
234
|
-
file: watcherProgressData.currentFile,
|
|
235
|
-
timestamp: Date.now(),
|
|
236
|
-
});
|
|
237
|
-
// Clear notification after 3 seconds
|
|
238
|
-
setTimeout(() => {
|
|
239
|
-
setFileUpdateNotification(null);
|
|
240
|
-
}, 3000);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
setWatcherEnabled(true);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
console.error('Failed to start codebase indexing:', error);
|
|
250
|
-
setCodebaseIndexing(false);
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
startCodebaseIndexing();
|
|
254
|
-
// Cleanup on unmount - just stop indexing, don't close database
|
|
255
|
-
// This allows resuming when returning to chat screen
|
|
256
|
-
return () => {
|
|
257
|
-
if (codebaseAgentRef.current) {
|
|
258
|
-
codebaseAgentRef.current.stop();
|
|
259
|
-
codebaseAgentRef.current.stopWatching();
|
|
260
|
-
setWatcherEnabled(false);
|
|
261
|
-
// Don't call close() - let it resume when returning
|
|
262
|
-
}
|
|
263
|
-
};
|
|
264
|
-
}, []); // Only run once on mount
|
|
265
|
-
// Export stop function for use in commands (like /home)
|
|
266
|
-
useEffect(() => {
|
|
267
|
-
// Store global reference to stop function for /home command
|
|
268
|
-
global.__stopCodebaseIndexing = async () => {
|
|
269
|
-
if (codebaseAgentRef.current) {
|
|
270
|
-
await codebaseAgentRef.current.stop();
|
|
271
|
-
setCodebaseIndexing(false);
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
return () => {
|
|
275
|
-
delete global.__stopCodebaseIndexing;
|
|
276
|
-
};
|
|
277
|
-
}, []);
|
|
278
|
-
// Persist yolo mode to localStorage
|
|
279
|
-
useEffect(() => {
|
|
280
|
-
try {
|
|
281
|
-
localStorage.setItem('snow-yolo-mode', String(yoloMode));
|
|
282
|
-
}
|
|
283
|
-
catch {
|
|
284
|
-
// Ignore localStorage errors
|
|
285
|
-
}
|
|
286
|
-
}, [yoloMode]);
|
|
287
|
-
// Clear restore input content after it's been used
|
|
288
|
-
useEffect(() => {
|
|
289
|
-
if (restoreInputContent !== null) {
|
|
290
|
-
// Clear after a short delay to ensure ChatInput has processed it
|
|
291
|
-
const timer = setTimeout(() => {
|
|
292
|
-
setRestoreInputContent(null);
|
|
293
|
-
}, 100);
|
|
294
|
-
return () => clearTimeout(timer);
|
|
295
|
-
}
|
|
296
|
-
return undefined;
|
|
297
|
-
}, [restoreInputContent]);
|
|
298
|
-
// Auto-resume last session when skipWelcome is true
|
|
299
|
-
useEffect(() => {
|
|
300
|
-
if (!skipWelcome)
|
|
301
|
-
return;
|
|
302
|
-
const autoResume = async () => {
|
|
303
|
-
try {
|
|
304
|
-
const sessions = await sessionManager.listSessions();
|
|
305
|
-
if (sessions.length > 0) {
|
|
306
|
-
// Get the most recent session (already sorted by updatedAt)
|
|
307
|
-
const latestSession = sessions[0];
|
|
308
|
-
if (latestSession) {
|
|
309
|
-
const session = await sessionManager.loadSession(latestSession.id);
|
|
310
|
-
if (session) {
|
|
311
|
-
// Initialize from session
|
|
312
|
-
const uiMessages = convertSessionMessagesToUI(session.messages);
|
|
313
|
-
setMessages(uiMessages);
|
|
314
|
-
initializeFromSession(session.messages);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
// If no sessions exist, just stay in chat screen with empty state
|
|
319
|
-
}
|
|
320
|
-
catch (error) {
|
|
321
|
-
// Silently fail - just stay in empty chat screen
|
|
322
|
-
console.error('Failed to auto-resume session:', error);
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
autoResume();
|
|
326
|
-
}, [skipWelcome, initializeFromSession]);
|
|
327
|
-
// Clear terminal and remount on terminal width change (like gemini-cli)
|
|
328
|
-
// Use debounce to avoid flickering during continuous resize
|
|
329
|
-
useEffect(() => {
|
|
330
|
-
if (isInitialMount.current) {
|
|
331
|
-
isInitialMount.current = false;
|
|
332
|
-
return;
|
|
333
|
-
}
|
|
334
|
-
const handler = setTimeout(() => {
|
|
335
|
-
stdout.write(ansiEscapes.clearTerminal);
|
|
336
|
-
setRemountKey(prev => prev + 1);
|
|
337
|
-
}, 200); // Wait for resize to stabilize
|
|
338
|
-
return () => {
|
|
339
|
-
clearTimeout(handler);
|
|
340
|
-
};
|
|
341
|
-
}, [terminalWidth]); // stdout 对象可能在每次渲染时变化,移除以避免循环
|
|
342
|
-
// Reload messages from session when remountKey changes (to restore sub-agent messages)
|
|
343
|
-
useEffect(() => {
|
|
344
|
-
if (remountKey === 0)
|
|
345
|
-
return; // Skip initial render
|
|
346
|
-
const reloadMessages = async () => {
|
|
347
|
-
const currentSession = sessionManager.getCurrentSession();
|
|
348
|
-
if (currentSession && currentSession.messages.length > 0) {
|
|
349
|
-
// Convert session messages back to UI format
|
|
350
|
-
const uiMessages = convertSessionMessagesToUI(currentSession.messages);
|
|
351
|
-
setMessages(uiMessages);
|
|
352
|
-
}
|
|
353
|
-
};
|
|
354
|
-
reloadMessages();
|
|
355
|
-
}, [remountKey]);
|
|
356
|
-
// Use tool confirmation hook
|
|
357
|
-
const { pendingToolConfirmation, requestToolConfirmation, isToolAutoApproved, addMultipleToAlwaysApproved, } = useToolConfirmation();
|
|
358
|
-
// Minimum terminal height required for proper rendering
|
|
359
|
-
const MIN_TERMINAL_HEIGHT = 10;
|
|
360
|
-
// Forward reference for processMessage (defined below)
|
|
361
|
-
const processMessageRef = useRef();
|
|
362
|
-
// Use command handler hook
|
|
363
|
-
const { handleCommandExecution } = useCommandHandler({
|
|
364
|
-
messages,
|
|
365
|
-
setMessages,
|
|
366
|
-
setRemountKey,
|
|
367
|
-
clearSavedMessages,
|
|
368
|
-
setIsCompressing,
|
|
369
|
-
setCompressionError,
|
|
370
|
-
setShowSessionPanel,
|
|
371
|
-
setShowMcpInfo,
|
|
372
|
-
setShowMcpPanel,
|
|
373
|
-
setShowUsagePanel,
|
|
374
|
-
setShowHelpPanel,
|
|
375
|
-
setMcpPanelKey,
|
|
376
|
-
setYoloMode,
|
|
377
|
-
setContextUsage: streamingState.setContextUsage,
|
|
378
|
-
setVscodeConnectionStatus: vscodeState.setVscodeConnectionStatus,
|
|
379
|
-
processMessage: (message, images, useBasicModel, hideUserMessage) => processMessageRef.current?.(message, images, useBasicModel, hideUserMessage) || Promise.resolve(),
|
|
380
|
-
});
|
|
381
|
-
useEffect(() => {
|
|
382
|
-
// Wait for commands to be loaded before attempting auto-connect
|
|
383
|
-
if (!commandsLoaded) {
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
if (hasAttemptedAutoVscodeConnect.current) {
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
if (vscodeState.vscodeConnectionStatus !== 'disconnected') {
|
|
390
|
-
hasAttemptedAutoVscodeConnect.current = true;
|
|
391
|
-
return;
|
|
392
|
-
}
|
|
393
|
-
hasAttemptedAutoVscodeConnect.current = true;
|
|
394
|
-
(async () => {
|
|
395
|
-
try {
|
|
396
|
-
const result = await executeCommand('ide');
|
|
397
|
-
await handleCommandExecution('ide', result);
|
|
398
|
-
}
|
|
399
|
-
catch (error) {
|
|
400
|
-
console.error('Failed to auto-connect VSCode:', error);
|
|
401
|
-
await handleCommandExecution('ide', {
|
|
402
|
-
success: false,
|
|
403
|
-
message: error instanceof Error
|
|
404
|
-
? error.message
|
|
405
|
-
: 'Failed to start VSCode connection',
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
})();
|
|
409
|
-
}, [commandsLoaded, handleCommandExecution, vscodeState.vscodeConnectionStatus]);
|
|
410
|
-
// Pending messages are now handled inline during tool execution in useConversation
|
|
411
|
-
// Auto-send pending messages when streaming completely stops (as fallback)
|
|
412
|
-
useEffect(() => {
|
|
413
|
-
if (!streamingState.isStreaming && pendingMessages.length > 0) {
|
|
414
|
-
const timer = setTimeout(() => {
|
|
415
|
-
processPendingMessages();
|
|
416
|
-
}, 100);
|
|
417
|
-
return () => clearTimeout(timer);
|
|
418
|
-
}
|
|
419
|
-
return undefined;
|
|
420
|
-
}, [streamingState.isStreaming, pendingMessages.length]);
|
|
421
|
-
// Listen to codebase search events
|
|
422
|
-
useEffect(() => {
|
|
423
|
-
const handleSearchEvent = (event) => {
|
|
424
|
-
if (event.type === 'search-complete') {
|
|
425
|
-
// Clear status after completion
|
|
426
|
-
streamingState.setCodebaseSearchStatus(null);
|
|
427
|
-
}
|
|
428
|
-
else {
|
|
429
|
-
// Update search status
|
|
430
|
-
streamingState.setCodebaseSearchStatus({
|
|
431
|
-
isSearching: true,
|
|
432
|
-
attempt: event.attempt,
|
|
433
|
-
maxAttempts: event.maxAttempts,
|
|
434
|
-
currentTopN: event.currentTopN,
|
|
435
|
-
message: event.message,
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
codebaseSearchEvents.onSearchEvent(handleSearchEvent);
|
|
440
|
-
return () => {
|
|
441
|
-
codebaseSearchEvents.removeSearchEventListener(handleSearchEvent);
|
|
442
|
-
};
|
|
443
|
-
}, [streamingState]);
|
|
444
|
-
// ESC key handler to interrupt streaming or close overlays
|
|
445
|
-
useInput((_, key) => {
|
|
446
|
-
if (snapshotState.pendingRollback) {
|
|
447
|
-
if (key.escape) {
|
|
448
|
-
snapshotState.setPendingRollback(null);
|
|
449
|
-
}
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
452
|
-
if (showSessionPanel) {
|
|
453
|
-
if (key.escape) {
|
|
454
|
-
setShowSessionPanel(false);
|
|
455
|
-
}
|
|
456
|
-
return;
|
|
457
|
-
}
|
|
458
|
-
if (showMcpPanel) {
|
|
459
|
-
if (key.escape) {
|
|
460
|
-
setShowMcpPanel(false);
|
|
461
|
-
}
|
|
462
|
-
return;
|
|
463
|
-
}
|
|
464
|
-
if (showUsagePanel) {
|
|
465
|
-
if (key.escape) {
|
|
466
|
-
setShowUsagePanel(false);
|
|
467
|
-
}
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
if (showHelpPanel) {
|
|
471
|
-
if (key.escape) {
|
|
472
|
-
setShowHelpPanel(false);
|
|
473
|
-
}
|
|
474
|
-
return;
|
|
475
|
-
}
|
|
476
|
-
if (showMcpInfo) {
|
|
477
|
-
if (key.escape) {
|
|
478
|
-
setShowMcpInfo(false);
|
|
479
|
-
}
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
if (key.escape &&
|
|
483
|
-
streamingState.isStreaming &&
|
|
484
|
-
streamingState.abortController) {
|
|
485
|
-
// Mark that user manually interrupted
|
|
486
|
-
userInterruptedRef.current = true;
|
|
487
|
-
// Abort the controller
|
|
488
|
-
streamingState.abortController.abort();
|
|
489
|
-
// Clear retry status immediately when user cancels
|
|
490
|
-
streamingState.setRetryStatus(null);
|
|
491
|
-
// Remove all pending tool call messages (those with toolPending: true)
|
|
492
|
-
setMessages(prev => prev.filter(msg => !msg.toolPending));
|
|
493
|
-
// Note: discontinued message will be added in processMessage/processPendingMessages finally block
|
|
494
|
-
// Note: session cleanup will be handled in processMessage/processPendingMessages finally block
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
const handleHistorySelect = async (selectedIndex, message, images) => {
|
|
498
|
-
// Clear context percentage and usage when user performs history rollback
|
|
499
|
-
setCurrentContextPercentage(0);
|
|
500
|
-
currentContextPercentageRef.current = 0;
|
|
501
|
-
streamingState.setContextUsage(null);
|
|
502
|
-
// Count total files that will be rolled back (from selectedIndex onwards)
|
|
503
|
-
let totalFileCount = 0;
|
|
504
|
-
for (const [index, count] of snapshotState.snapshotFileCount.entries()) {
|
|
505
|
-
if (index >= selectedIndex) {
|
|
506
|
-
totalFileCount += count;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
// Show confirmation dialog if there are files to rollback
|
|
510
|
-
if (totalFileCount > 0) {
|
|
511
|
-
// Get list of files that will be rolled back
|
|
512
|
-
const currentSession = sessionManager.getCurrentSession();
|
|
513
|
-
const filePaths = currentSession
|
|
514
|
-
? await incrementalSnapshotManager.getFilesToRollback(currentSession.id, selectedIndex)
|
|
515
|
-
: [];
|
|
516
|
-
snapshotState.setPendingRollback({
|
|
517
|
-
messageIndex: selectedIndex,
|
|
518
|
-
fileCount: filePaths.length, // Use actual unique file count
|
|
519
|
-
filePaths,
|
|
520
|
-
message, // Save message for restore after rollback
|
|
521
|
-
images, // Save images for restore after rollback
|
|
522
|
-
});
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
// No files to rollback, just rollback conversation
|
|
526
|
-
// Restore message to input buffer (with or without images)
|
|
527
|
-
setRestoreInputContent({
|
|
528
|
-
text: message,
|
|
529
|
-
images: images,
|
|
530
|
-
});
|
|
531
|
-
await performRollback(selectedIndex, false);
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
const performRollback = async (selectedIndex, rollbackFiles) => {
|
|
535
|
-
const currentSession = sessionManager.getCurrentSession();
|
|
536
|
-
// Rollback workspace to checkpoint if requested
|
|
537
|
-
if (rollbackFiles && currentSession) {
|
|
538
|
-
// Use rollbackToMessageIndex to rollback all snapshots >= selectedIndex
|
|
539
|
-
await incrementalSnapshotManager.rollbackToMessageIndex(currentSession.id, selectedIndex);
|
|
540
|
-
}
|
|
541
|
-
// For session file: find the correct truncation point based on session messages
|
|
542
|
-
// We need to truncate to the same user message in the session file
|
|
543
|
-
if (currentSession) {
|
|
544
|
-
// Count how many user messages we're deleting (from selectedIndex onwards in UI)
|
|
545
|
-
// But exclude any uncommitted user messages that weren't saved to session
|
|
546
|
-
const messagesAfterSelected = messages.slice(selectedIndex);
|
|
547
|
-
const hasDiscontinuedMessage = messagesAfterSelected.some(msg => msg.discontinued);
|
|
548
|
-
let uiUserMessagesToDelete = 0;
|
|
549
|
-
if (hasDiscontinuedMessage) {
|
|
550
|
-
// If there's a discontinued message, it means all messages from selectedIndex onwards
|
|
551
|
-
// (including user messages) were not saved to session
|
|
552
|
-
// So we don't need to delete any user messages from session
|
|
553
|
-
uiUserMessagesToDelete = 0;
|
|
554
|
-
}
|
|
555
|
-
else {
|
|
556
|
-
// Normal case: count all user messages from selectedIndex onwards
|
|
557
|
-
uiUserMessagesToDelete = messagesAfterSelected.filter(msg => msg.role === 'user').length;
|
|
558
|
-
}
|
|
559
|
-
// Check if the selected message is a user message that might not be in session
|
|
560
|
-
// (e.g., interrupted before AI response)
|
|
561
|
-
const selectedMessage = messages[selectedIndex];
|
|
562
|
-
const isUncommittedUserMessage = selectedMessage?.role === 'user' &&
|
|
563
|
-
uiUserMessagesToDelete === 1 &&
|
|
564
|
-
// Check if this is the last or second-to-last message (before discontinued)
|
|
565
|
-
(selectedIndex === messages.length - 1 ||
|
|
566
|
-
(selectedIndex === messages.length - 2 &&
|
|
567
|
-
messages[messages.length - 1]?.discontinued));
|
|
568
|
-
// If this is an uncommitted user message, just truncate UI and skip session modification
|
|
569
|
-
if (isUncommittedUserMessage) {
|
|
570
|
-
// Check if session ends with a complete assistant response
|
|
571
|
-
const lastSessionMsg = currentSession.messages[currentSession.messages.length - 1];
|
|
572
|
-
const sessionEndsWithAssistant = lastSessionMsg?.role === 'assistant' && !lastSessionMsg?.tool_calls;
|
|
573
|
-
if (sessionEndsWithAssistant) {
|
|
574
|
-
// Session is complete, this user message wasn't saved
|
|
575
|
-
// Just truncate UI, don't modify session
|
|
576
|
-
setMessages(prev => prev.slice(0, selectedIndex));
|
|
577
|
-
clearSavedMessages();
|
|
578
|
-
setRemountKey(prev => prev + 1);
|
|
579
|
-
snapshotState.setPendingRollback(null);
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
// Special case: if rolling back to index 0 (first message), always delete entire session
|
|
584
|
-
// This handles the case where user interrupts the first conversation
|
|
585
|
-
let sessionTruncateIndex = currentSession.messages.length;
|
|
586
|
-
if (selectedIndex === 0) {
|
|
587
|
-
// Rolling back to the very first message means deleting entire session
|
|
588
|
-
sessionTruncateIndex = 0;
|
|
589
|
-
}
|
|
590
|
-
else {
|
|
591
|
-
// Find the corresponding user message in session to delete
|
|
592
|
-
// We start from the end and count backwards
|
|
593
|
-
let sessionUserMessageCount = 0;
|
|
594
|
-
for (let i = currentSession.messages.length - 1; i >= 0; i--) {
|
|
595
|
-
const msg = currentSession.messages[i];
|
|
596
|
-
if (msg && msg.role === 'user') {
|
|
597
|
-
sessionUserMessageCount++;
|
|
598
|
-
if (sessionUserMessageCount === uiUserMessagesToDelete) {
|
|
599
|
-
// We want to delete from this user message onwards
|
|
600
|
-
sessionTruncateIndex = i;
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
// Special case: rolling back to index 0 means deleting the entire session
|
|
607
|
-
if (sessionTruncateIndex === 0 && currentSession) {
|
|
608
|
-
// Delete all snapshots for this session
|
|
609
|
-
await incrementalSnapshotManager.clearAllSnapshots(currentSession.id);
|
|
610
|
-
// Delete the session file
|
|
611
|
-
await sessionManager.deleteSession(currentSession.id);
|
|
612
|
-
// Clear current session
|
|
613
|
-
sessionManager.clearCurrentSession();
|
|
614
|
-
// Clear all messages
|
|
615
|
-
setMessages([]);
|
|
616
|
-
// Clear saved messages
|
|
617
|
-
clearSavedMessages();
|
|
618
|
-
// Clear snapshot state
|
|
619
|
-
snapshotState.setSnapshotFileCount(new Map());
|
|
620
|
-
// Clear pending rollback dialog
|
|
621
|
-
snapshotState.setPendingRollback(null);
|
|
622
|
-
// Trigger remount
|
|
623
|
-
setRemountKey(prev => prev + 1);
|
|
624
|
-
return;
|
|
625
|
-
}
|
|
626
|
-
// Delete snapshot files >= selectedIndex (regardless of whether files were rolled back)
|
|
627
|
-
await incrementalSnapshotManager.deleteSnapshotsFromIndex(currentSession.id, selectedIndex);
|
|
628
|
-
// Reload snapshot file counts from disk after deletion
|
|
629
|
-
const snapshots = await incrementalSnapshotManager.listSnapshots(currentSession.id);
|
|
630
|
-
const counts = new Map();
|
|
631
|
-
for (const snapshot of snapshots) {
|
|
632
|
-
counts.set(snapshot.messageIndex, snapshot.fileCount);
|
|
633
|
-
}
|
|
634
|
-
snapshotState.setSnapshotFileCount(counts);
|
|
635
|
-
// Truncate session messages
|
|
636
|
-
await sessionManager.truncateMessages(sessionTruncateIndex);
|
|
637
|
-
}
|
|
638
|
-
// Truncate UI messages array to remove the selected user message and everything after it
|
|
639
|
-
setMessages(prev => prev.slice(0, selectedIndex));
|
|
640
|
-
clearSavedMessages();
|
|
641
|
-
setRemountKey(prev => prev + 1);
|
|
642
|
-
// Clear pending rollback dialog
|
|
643
|
-
snapshotState.setPendingRollback(null);
|
|
644
|
-
};
|
|
645
|
-
const handleRollbackConfirm = async (rollbackFiles) => {
|
|
646
|
-
if (rollbackFiles === null) {
|
|
647
|
-
// User cancelled - just close the dialog without doing anything
|
|
648
|
-
snapshotState.setPendingRollback(null);
|
|
649
|
-
return;
|
|
650
|
-
}
|
|
651
|
-
if (snapshotState.pendingRollback) {
|
|
652
|
-
// Restore message and images to input before rollback
|
|
653
|
-
if (snapshotState.pendingRollback.message) {
|
|
654
|
-
setRestoreInputContent({
|
|
655
|
-
text: snapshotState.pendingRollback.message,
|
|
656
|
-
images: snapshotState.pendingRollback.images,
|
|
657
|
-
});
|
|
658
|
-
}
|
|
659
|
-
await performRollback(snapshotState.pendingRollback.messageIndex, rollbackFiles);
|
|
660
|
-
}
|
|
661
|
-
};
|
|
662
|
-
const handleSessionPanelSelect = async (sessionId) => {
|
|
663
|
-
setShowSessionPanel(false);
|
|
664
|
-
try {
|
|
665
|
-
const session = await sessionManager.loadSession(sessionId);
|
|
666
|
-
if (session) {
|
|
667
|
-
// Convert API format messages to UI format for proper rendering
|
|
668
|
-
const uiMessages = convertSessionMessagesToUI(session.messages);
|
|
669
|
-
initializeFromSession(session.messages);
|
|
670
|
-
setMessages(uiMessages);
|
|
671
|
-
setPendingMessages([]);
|
|
672
|
-
streamingState.setIsStreaming(false);
|
|
673
|
-
setRemountKey(prev => prev + 1);
|
|
674
|
-
// Load snapshot file counts for the loaded session
|
|
675
|
-
const snapshots = await incrementalSnapshotManager.listSnapshots(session.id);
|
|
676
|
-
const counts = new Map();
|
|
677
|
-
for (const snapshot of snapshots) {
|
|
678
|
-
counts.set(snapshot.messageIndex, snapshot.fileCount);
|
|
679
|
-
}
|
|
680
|
-
snapshotState.setSnapshotFileCount(counts);
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
catch (error) {
|
|
684
|
-
console.error('Failed to load session:', error);
|
|
685
|
-
}
|
|
686
|
-
};
|
|
687
|
-
const handleMessageSubmit = async (message, images) => {
|
|
688
|
-
// If streaming, add to pending messages instead of sending immediately
|
|
689
|
-
if (streamingState.isStreaming) {
|
|
690
|
-
setPendingMessages(prev => [...prev, { text: message, images }]);
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
|
-
// Create checkpoint (lightweight, only tracks modifications)
|
|
694
|
-
const currentSession = sessionManager.getCurrentSession();
|
|
695
|
-
if (!currentSession) {
|
|
696
|
-
await sessionManager.createNewSession();
|
|
697
|
-
}
|
|
698
|
-
const session = sessionManager.getCurrentSession();
|
|
699
|
-
if (session) {
|
|
700
|
-
await incrementalSnapshotManager.createSnapshot(session.id, messages.length);
|
|
701
|
-
}
|
|
702
|
-
// Process the message normally
|
|
703
|
-
await processMessage(message, images);
|
|
704
|
-
};
|
|
705
|
-
const processMessage = async (message, images, useBasicModel, hideUserMessage) => {
|
|
706
|
-
// 检查 token 占用,如果 >= 80% 且配置启用了自动压缩,先执行自动压缩
|
|
707
|
-
const autoCompressConfig = getOpenAiConfig();
|
|
708
|
-
if (autoCompressConfig.enableAutoCompress !== false && shouldAutoCompress(currentContextPercentageRef.current)) {
|
|
709
|
-
setIsCompressing(true);
|
|
710
|
-
setCompressionError(null);
|
|
711
|
-
try {
|
|
712
|
-
// 显示压缩提示消息
|
|
713
|
-
const compressingMessage = {
|
|
714
|
-
role: 'assistant',
|
|
715
|
-
content: '✵ Auto-compressing context due to token limit...',
|
|
716
|
-
streaming: false,
|
|
717
|
-
};
|
|
718
|
-
setMessages(prev => [...prev, compressingMessage]);
|
|
719
|
-
const compressionResult = await performAutoCompression();
|
|
720
|
-
if (compressionResult) {
|
|
721
|
-
// 更新UI和token使用情况
|
|
722
|
-
clearSavedMessages();
|
|
723
|
-
setMessages(compressionResult.uiMessages);
|
|
724
|
-
setRemountKey(prev => prev + 1);
|
|
725
|
-
streamingState.setContextUsage(compressionResult.usage);
|
|
726
|
-
}
|
|
727
|
-
else {
|
|
728
|
-
throw new Error('Compression failed');
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
catch (error) {
|
|
732
|
-
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
|
|
733
|
-
setCompressionError(errorMsg);
|
|
734
|
-
const errorMessage = {
|
|
735
|
-
role: 'assistant',
|
|
736
|
-
content: `**Auto-compression Failed**\n\n${errorMsg}`,
|
|
737
|
-
streaming: false,
|
|
738
|
-
};
|
|
739
|
-
setMessages(prev => [...prev, errorMessage]);
|
|
740
|
-
setIsCompressing(false);
|
|
741
|
-
return; // 停止处理,等待用户手动处理
|
|
742
|
-
}
|
|
743
|
-
finally {
|
|
744
|
-
setIsCompressing(false);
|
|
745
|
-
}
|
|
746
|
-
}
|
|
747
|
-
// Clear any previous retry status when starting a new request
|
|
748
|
-
streamingState.setRetryStatus(null);
|
|
749
|
-
// Parse and validate file references (use original message for immediate UI display)
|
|
750
|
-
const { cleanContent, validFiles } = await parseAndValidateFileReferences(message);
|
|
751
|
-
// Separate image files from regular files
|
|
752
|
-
const imageFiles = validFiles.filter(f => f.isImage && f.imageData && f.mimeType);
|
|
753
|
-
const regularFiles = validFiles.filter(f => !f.isImage);
|
|
754
|
-
// Convert image files to image content format
|
|
755
|
-
const imageContents = [
|
|
756
|
-
...(images || []).map(img => ({
|
|
757
|
-
type: 'image',
|
|
758
|
-
data: img.data,
|
|
759
|
-
mimeType: img.mimeType,
|
|
760
|
-
})),
|
|
761
|
-
...imageFiles.map(f => ({
|
|
762
|
-
type: 'image',
|
|
763
|
-
data: f.imageData,
|
|
764
|
-
mimeType: f.mimeType,
|
|
765
|
-
})),
|
|
766
|
-
];
|
|
767
|
-
// Only add user message to UI if not hidden (显示原始用户消息)
|
|
768
|
-
if (!hideUserMessage) {
|
|
769
|
-
const userMessage = {
|
|
770
|
-
role: 'user',
|
|
771
|
-
content: cleanContent,
|
|
772
|
-
files: validFiles.length > 0 ? validFiles : undefined,
|
|
773
|
-
images: imageContents.length > 0 ? imageContents : undefined,
|
|
774
|
-
};
|
|
775
|
-
setMessages(prev => [...prev, userMessage]);
|
|
776
|
-
}
|
|
777
|
-
streamingState.setIsStreaming(true);
|
|
778
|
-
// Create new abort controller for this request
|
|
779
|
-
const controller = new AbortController();
|
|
780
|
-
streamingState.setAbortController(controller);
|
|
781
|
-
// Optimize user prompt in the background (silent execution)
|
|
782
|
-
let originalMessage = message;
|
|
783
|
-
let optimizedMessage = message;
|
|
784
|
-
let optimizedCleanContent = cleanContent;
|
|
785
|
-
// Check if prompt optimization is enabled in config
|
|
786
|
-
const config = getOpenAiConfig();
|
|
787
|
-
const isOptimizationEnabled = config.enablePromptOptimization !== false; // Default to true
|
|
788
|
-
if (isOptimizationEnabled) {
|
|
789
|
-
try {
|
|
790
|
-
// Convert current UI messages to ChatMessage format for context
|
|
791
|
-
const conversationHistory = messages
|
|
792
|
-
.filter(m => m.role === 'user' || m.role === 'assistant')
|
|
793
|
-
.map(m => ({
|
|
794
|
-
role: m.role,
|
|
795
|
-
content: typeof m.content === 'string' ? m.content : '',
|
|
796
|
-
}));
|
|
797
|
-
// Try to optimize the prompt (background execution)
|
|
798
|
-
optimizedMessage = await promptOptimizeAgent.optimizePrompt(message, conversationHistory, controller.signal);
|
|
799
|
-
// Re-parse the optimized message to get clean content for AI
|
|
800
|
-
if (optimizedMessage !== originalMessage) {
|
|
801
|
-
const optimizedParsed = await parseAndValidateFileReferences(optimizedMessage);
|
|
802
|
-
optimizedCleanContent = optimizedParsed.cleanContent;
|
|
803
|
-
}
|
|
804
|
-
}
|
|
805
|
-
catch (error) {
|
|
806
|
-
// If optimization fails, silently fall back to original message
|
|
807
|
-
logger.warn('Prompt optimization failed, using original:', error);
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
try {
|
|
811
|
-
// Create message for AI with file read instructions and editor context (使用优化后的内容)
|
|
812
|
-
const messageForAI = createMessageWithFileInstructions(optimizedCleanContent, regularFiles, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
|
|
813
|
-
// Wrap saveMessage to add originalContent for user messages
|
|
814
|
-
const saveMessageWithOriginal = async (msg) => {
|
|
815
|
-
// If this is a user message and we have an optimized version, add originalContent
|
|
816
|
-
if (msg.role === 'user' && optimizedMessage !== originalMessage) {
|
|
817
|
-
await saveMessage({
|
|
818
|
-
...msg,
|
|
819
|
-
originalContent: originalMessage,
|
|
820
|
-
});
|
|
821
|
-
}
|
|
822
|
-
else {
|
|
823
|
-
await saveMessage(msg);
|
|
824
|
-
}
|
|
825
|
-
};
|
|
826
|
-
// Start conversation with tool support
|
|
827
|
-
await handleConversationWithTools({
|
|
828
|
-
userContent: messageForAI,
|
|
829
|
-
imageContents,
|
|
830
|
-
controller,
|
|
831
|
-
messages,
|
|
832
|
-
saveMessage: saveMessageWithOriginal,
|
|
833
|
-
setMessages,
|
|
834
|
-
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
835
|
-
requestToolConfirmation,
|
|
836
|
-
isToolAutoApproved,
|
|
837
|
-
addMultipleToAlwaysApproved,
|
|
838
|
-
yoloMode,
|
|
839
|
-
setContextUsage: streamingState.setContextUsage,
|
|
840
|
-
useBasicModel,
|
|
841
|
-
getPendingMessages: () => pendingMessagesRef.current,
|
|
842
|
-
clearPendingMessages: () => setPendingMessages([]),
|
|
843
|
-
setIsStreaming: streamingState.setIsStreaming,
|
|
844
|
-
setIsReasoning: streamingState.setIsReasoning,
|
|
845
|
-
setRetryStatus: streamingState.setRetryStatus,
|
|
846
|
-
clearSavedMessages,
|
|
847
|
-
setRemountKey,
|
|
848
|
-
getCurrentContextPercentage: () => currentContextPercentageRef.current,
|
|
849
|
-
});
|
|
850
|
-
}
|
|
851
|
-
catch (error) {
|
|
852
|
-
if (controller.signal.aborted) {
|
|
853
|
-
// Don't return here - let finally block execute
|
|
854
|
-
// Just skip error display for aborted requests
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
858
|
-
const finalMessage = {
|
|
859
|
-
role: 'assistant',
|
|
860
|
-
content: `Error: ${errorMessage}`,
|
|
861
|
-
streaming: false,
|
|
862
|
-
};
|
|
863
|
-
setMessages(prev => [...prev, finalMessage]);
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
finally {
|
|
867
|
-
// Handle user interruption uniformly
|
|
868
|
-
if (userInterruptedRef.current) {
|
|
869
|
-
// Clean up incomplete conversation in session
|
|
870
|
-
const session = sessionManager.getCurrentSession();
|
|
871
|
-
if (session && session.messages.length > 0) {
|
|
872
|
-
(async () => {
|
|
873
|
-
try {
|
|
874
|
-
// Find the last complete conversation round
|
|
875
|
-
const messages = session.messages;
|
|
876
|
-
let truncateIndex = messages.length;
|
|
877
|
-
// Scan from the end to find incomplete round
|
|
878
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
879
|
-
const msg = messages[i];
|
|
880
|
-
if (!msg)
|
|
881
|
-
continue;
|
|
882
|
-
// If last message is user message without assistant response, remove it
|
|
883
|
-
// The user message was saved via await saveMessage() before interruption
|
|
884
|
-
// So it's safe to truncate it from session when incomplete
|
|
885
|
-
if (msg.role === 'user' && i === messages.length - 1) {
|
|
886
|
-
truncateIndex = i;
|
|
887
|
-
break;
|
|
888
|
-
}
|
|
889
|
-
// If assistant message has tool_calls, verify all tool results exist
|
|
890
|
-
if (msg.role === 'assistant' &&
|
|
891
|
-
msg.tool_calls &&
|
|
892
|
-
msg.tool_calls.length > 0) {
|
|
893
|
-
const toolCallIds = new Set(msg.tool_calls.map(tc => tc.id));
|
|
894
|
-
// Check if all tool results exist after this assistant message
|
|
895
|
-
for (let j = i + 1; j < messages.length; j++) {
|
|
896
|
-
const followMsg = messages[j];
|
|
897
|
-
if (followMsg &&
|
|
898
|
-
followMsg.role === 'tool' &&
|
|
899
|
-
followMsg.tool_call_id) {
|
|
900
|
-
toolCallIds.delete(followMsg.tool_call_id);
|
|
901
|
-
}
|
|
902
|
-
}
|
|
903
|
-
// If some tool results are missing, remove from this assistant message onwards
|
|
904
|
-
// But only if this is the last assistant message with tool_calls in the entire conversation
|
|
905
|
-
if (toolCallIds.size > 0) {
|
|
906
|
-
// Additional check: ensure this is the last assistant message with tool_calls
|
|
907
|
-
let hasLaterAssistantWithTools = false;
|
|
908
|
-
for (let k = i + 1; k < messages.length; k++) {
|
|
909
|
-
const laterMsg = messages[k];
|
|
910
|
-
if (laterMsg?.role === 'assistant' &&
|
|
911
|
-
laterMsg?.tool_calls &&
|
|
912
|
-
laterMsg.tool_calls.length > 0) {
|
|
913
|
-
hasLaterAssistantWithTools = true;
|
|
914
|
-
break;
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
// Only truncate if no later assistant messages have tool_calls
|
|
918
|
-
// This preserves complete historical conversations
|
|
919
|
-
if (!hasLaterAssistantWithTools) {
|
|
920
|
-
truncateIndex = i;
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
// If we found a complete assistant response without tool calls, we're done
|
|
926
|
-
if (msg.role === 'assistant' && !msg.tool_calls) {
|
|
927
|
-
break;
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
// Truncate session if needed
|
|
931
|
-
if (truncateIndex < messages.length) {
|
|
932
|
-
await sessionManager.truncateMessages(truncateIndex);
|
|
933
|
-
// Also clear from saved messages tracking
|
|
934
|
-
clearSavedMessages();
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
catch (error) {
|
|
938
|
-
console.error('Failed to clean up incomplete conversation:', error);
|
|
939
|
-
}
|
|
940
|
-
})();
|
|
941
|
-
}
|
|
942
|
-
// Add discontinued message after all processing is done
|
|
943
|
-
setMessages(prev => [
|
|
944
|
-
...prev,
|
|
945
|
-
{
|
|
946
|
-
role: 'assistant',
|
|
947
|
-
content: '',
|
|
948
|
-
streaming: false,
|
|
949
|
-
discontinued: true,
|
|
950
|
-
},
|
|
951
|
-
]);
|
|
952
|
-
// Reset interruption flag
|
|
953
|
-
userInterruptedRef.current = false;
|
|
954
|
-
}
|
|
955
|
-
// End streaming
|
|
956
|
-
streamingState.setIsStreaming(false);
|
|
957
|
-
streamingState.setAbortController(null);
|
|
958
|
-
streamingState.setStreamTokenCount(0);
|
|
959
|
-
}
|
|
960
|
-
};
|
|
961
|
-
// Set the ref to the actual function
|
|
962
|
-
processMessageRef.current = processMessage;
|
|
963
|
-
const processPendingMessages = async () => {
|
|
964
|
-
if (pendingMessages.length === 0)
|
|
965
|
-
return;
|
|
966
|
-
// Clear any previous retry status when starting a new request
|
|
967
|
-
streamingState.setRetryStatus(null);
|
|
968
|
-
// Get current pending messages and clear them immediately
|
|
969
|
-
const messagesToProcess = [...pendingMessages];
|
|
970
|
-
setPendingMessages([]);
|
|
971
|
-
// Combine multiple pending messages into one
|
|
972
|
-
const combinedMessage = messagesToProcess.map(m => m.text).join('\n\n');
|
|
973
|
-
// Parse and validate file references (same as processMessage)
|
|
974
|
-
const { cleanContent, validFiles } = await parseAndValidateFileReferences(combinedMessage);
|
|
975
|
-
// Separate image files from regular files
|
|
976
|
-
const imageFiles = validFiles.filter(f => f.isImage && f.imageData && f.mimeType);
|
|
977
|
-
const regularFiles = validFiles.filter(f => !f.isImage);
|
|
978
|
-
// Collect all images from pending messages
|
|
979
|
-
const allImages = messagesToProcess
|
|
980
|
-
.flatMap(m => m.images || [])
|
|
981
|
-
.concat(imageFiles.map(f => ({
|
|
982
|
-
data: f.imageData,
|
|
983
|
-
mimeType: f.mimeType,
|
|
984
|
-
})));
|
|
985
|
-
// Convert to image content format
|
|
986
|
-
const imageContents = allImages.length > 0
|
|
987
|
-
? allImages.map(img => ({
|
|
988
|
-
type: 'image',
|
|
989
|
-
data: img.data,
|
|
990
|
-
mimeType: img.mimeType,
|
|
991
|
-
}))
|
|
992
|
-
: undefined;
|
|
993
|
-
// Add user message to chat with file references and images
|
|
994
|
-
const userMessage = {
|
|
995
|
-
role: 'user',
|
|
996
|
-
content: cleanContent,
|
|
997
|
-
files: validFiles.length > 0 ? validFiles : undefined,
|
|
998
|
-
images: imageContents,
|
|
999
|
-
};
|
|
1000
|
-
setMessages(prev => [...prev, userMessage]);
|
|
1001
|
-
// Start streaming response
|
|
1002
|
-
streamingState.setIsStreaming(true);
|
|
1003
|
-
// Create new abort controller for this request
|
|
1004
|
-
const controller = new AbortController();
|
|
1005
|
-
streamingState.setAbortController(controller);
|
|
1006
|
-
try {
|
|
1007
|
-
// Create message for AI with file read instructions and editor context
|
|
1008
|
-
const messageForAI = createMessageWithFileInstructions(cleanContent, regularFiles, vscodeState.vscodeConnected ? vscodeState.editorContext : undefined);
|
|
1009
|
-
// Use the same conversation handler
|
|
1010
|
-
await handleConversationWithTools({
|
|
1011
|
-
userContent: messageForAI,
|
|
1012
|
-
imageContents,
|
|
1013
|
-
controller,
|
|
1014
|
-
messages,
|
|
1015
|
-
saveMessage,
|
|
1016
|
-
setMessages,
|
|
1017
|
-
setStreamTokenCount: streamingState.setStreamTokenCount,
|
|
1018
|
-
requestToolConfirmation,
|
|
1019
|
-
isToolAutoApproved,
|
|
1020
|
-
addMultipleToAlwaysApproved,
|
|
1021
|
-
yoloMode,
|
|
1022
|
-
setContextUsage: streamingState.setContextUsage,
|
|
1023
|
-
getPendingMessages: () => pendingMessagesRef.current,
|
|
1024
|
-
clearPendingMessages: () => setPendingMessages([]),
|
|
1025
|
-
setIsStreaming: streamingState.setIsStreaming,
|
|
1026
|
-
setIsReasoning: streamingState.setIsReasoning,
|
|
1027
|
-
setRetryStatus: streamingState.setRetryStatus,
|
|
1028
|
-
clearSavedMessages,
|
|
1029
|
-
setRemountKey,
|
|
1030
|
-
getCurrentContextPercentage: () => currentContextPercentageRef.current,
|
|
1031
|
-
});
|
|
1032
|
-
}
|
|
1033
|
-
catch (error) {
|
|
1034
|
-
if (controller.signal.aborted) {
|
|
1035
|
-
// Don't return here - let finally block execute
|
|
1036
|
-
// Just skip error display for aborted requests
|
|
1037
|
-
}
|
|
1038
|
-
else {
|
|
1039
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1040
|
-
const finalMessage = {
|
|
1041
|
-
role: 'assistant',
|
|
1042
|
-
content: `Error: ${errorMessage}`,
|
|
1043
|
-
streaming: false,
|
|
1044
|
-
};
|
|
1045
|
-
setMessages(prev => [...prev, finalMessage]);
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
finally {
|
|
1049
|
-
// Handle user interruption uniformly
|
|
1050
|
-
if (userInterruptedRef.current) {
|
|
1051
|
-
// Clean up incomplete conversation in session
|
|
1052
|
-
const session = sessionManager.getCurrentSession();
|
|
1053
|
-
if (session && session.messages.length > 0) {
|
|
1054
|
-
(async () => {
|
|
1055
|
-
try {
|
|
1056
|
-
// Find the last complete conversation round
|
|
1057
|
-
const messages = session.messages;
|
|
1058
|
-
let truncateIndex = messages.length;
|
|
1059
|
-
// Scan from the end to find incomplete round
|
|
1060
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1061
|
-
const msg = messages[i];
|
|
1062
|
-
if (!msg)
|
|
1063
|
-
continue;
|
|
1064
|
-
// If last message is user message without assistant response, remove it
|
|
1065
|
-
if (msg.role === 'user' && i === messages.length - 1) {
|
|
1066
|
-
truncateIndex = i;
|
|
1067
|
-
break;
|
|
1068
|
-
}
|
|
1069
|
-
// If assistant message has tool_calls, verify all tool results exist
|
|
1070
|
-
if (msg.role === 'assistant' &&
|
|
1071
|
-
msg.tool_calls &&
|
|
1072
|
-
msg.tool_calls.length > 0) {
|
|
1073
|
-
const toolCallIds = new Set(msg.tool_calls.map(tc => tc.id));
|
|
1074
|
-
// Check if all tool results exist after this assistant message
|
|
1075
|
-
for (let j = i + 1; j < messages.length; j++) {
|
|
1076
|
-
const followMsg = messages[j];
|
|
1077
|
-
if (followMsg &&
|
|
1078
|
-
followMsg.role === 'tool' &&
|
|
1079
|
-
followMsg.tool_call_id) {
|
|
1080
|
-
toolCallIds.delete(followMsg.tool_call_id);
|
|
1081
|
-
}
|
|
1082
|
-
}
|
|
1083
|
-
// If some tool results are missing, remove from this assistant message onwards
|
|
1084
|
-
if (toolCallIds.size > 0) {
|
|
1085
|
-
truncateIndex = i;
|
|
1086
|
-
break;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
// If we found a complete assistant response without tool calls, we're done
|
|
1090
|
-
if (msg.role === 'assistant' && !msg.tool_calls) {
|
|
1091
|
-
break;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
// Truncate session if needed
|
|
1095
|
-
if (truncateIndex < messages.length) {
|
|
1096
|
-
await sessionManager.truncateMessages(truncateIndex);
|
|
1097
|
-
// Also clear from saved messages tracking
|
|
1098
|
-
clearSavedMessages();
|
|
1099
|
-
}
|
|
1100
|
-
}
|
|
1101
|
-
catch (error) {
|
|
1102
|
-
console.error('Failed to clean up incomplete conversation:', error);
|
|
1103
|
-
}
|
|
1104
|
-
})();
|
|
1105
|
-
}
|
|
1106
|
-
// Add discontinued message after all processing is done
|
|
1107
|
-
setMessages(prev => [
|
|
1108
|
-
...prev,
|
|
1109
|
-
{
|
|
1110
|
-
role: 'assistant',
|
|
1111
|
-
content: '',
|
|
1112
|
-
streaming: false,
|
|
1113
|
-
discontinued: true,
|
|
1114
|
-
},
|
|
1115
|
-
]);
|
|
1116
|
-
// Reset interruption flag
|
|
1117
|
-
userInterruptedRef.current = false;
|
|
1118
|
-
}
|
|
1119
|
-
// End streaming
|
|
1120
|
-
streamingState.setIsStreaming(false);
|
|
1121
|
-
streamingState.setAbortController(null);
|
|
1122
|
-
streamingState.setStreamTokenCount(0);
|
|
1123
|
-
}
|
|
1124
|
-
};
|
|
1125
|
-
if (showMcpInfo) {
|
|
1126
|
-
return (React.createElement(MCPInfoScreen, { onClose: () => setShowMcpInfo(false), panelKey: mcpPanelKey }));
|
|
1127
|
-
}
|
|
1128
|
-
// Show warning if terminal is too small
|
|
1129
|
-
if (terminalHeight < MIN_TERMINAL_HEIGHT) {
|
|
1130
|
-
return (React.createElement(Box, { flexDirection: "column", padding: 2 },
|
|
1131
|
-
React.createElement(Box, { borderStyle: "round", borderColor: "red", padding: 1 },
|
|
1132
|
-
React.createElement(Text, { color: "red", bold: true }, t.chatScreen.terminalTooSmall)),
|
|
1133
|
-
React.createElement(Box, { marginTop: 1 },
|
|
1134
|
-
React.createElement(Text, { color: "yellow" }, t.chatScreen.terminalResizePrompt
|
|
1135
|
-
.replace('{current}', terminalHeight.toString())
|
|
1136
|
-
.replace('{required}', MIN_TERMINAL_HEIGHT.toString()))),
|
|
1137
|
-
React.createElement(Box, { marginTop: 1 },
|
|
1138
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.terminalMinHeight))));
|
|
1139
|
-
}
|
|
1140
|
-
return (React.createElement(Box, { flexDirection: "column", height: "100%", width: terminalWidth },
|
|
1141
|
-
React.createElement(Static, { key: remountKey, items: [
|
|
1142
|
-
React.createElement(Box, { key: "header", paddingX: 1, width: terminalWidth },
|
|
1143
|
-
React.createElement(Box, { borderColor: 'cyan', borderStyle: "round", paddingX: 2, paddingY: 1, width: terminalWidth - 2 },
|
|
1144
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
1145
|
-
React.createElement(Text, { color: "white", bold: true },
|
|
1146
|
-
React.createElement(Text, { color: "cyan" }, "\u2746 "),
|
|
1147
|
-
React.createElement(Gradient, { name: "rainbow" }, t.chatScreen.headerTitle),
|
|
1148
|
-
React.createElement(Text, { color: "white" }, " \u26C7")),
|
|
1149
|
-
React.createElement(Text, null,
|
|
1150
|
-
"\u2022 ",
|
|
1151
|
-
t.chatScreen.headerExplanations),
|
|
1152
|
-
React.createElement(Text, null,
|
|
1153
|
-
"\u2022 ",
|
|
1154
|
-
t.chatScreen.headerInterrupt),
|
|
1155
|
-
React.createElement(Text, null,
|
|
1156
|
-
"\u2022 ",
|
|
1157
|
-
t.chatScreen.headerYolo),
|
|
1158
|
-
React.createElement(Text, null, (() => {
|
|
1159
|
-
const pasteKey = process.platform === 'darwin' ? 'Ctrl+V' : 'Alt+V';
|
|
1160
|
-
return `• ${t.chatScreen.headerShortcuts.replace('{pasteKey}', pasteKey)}`;
|
|
1161
|
-
})()),
|
|
1162
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
|
|
1163
|
-
"\u2022",
|
|
1164
|
-
' ',
|
|
1165
|
-
t.chatScreen.headerWorkingDirectory.replace('{directory}', workingDirectory))))),
|
|
1166
|
-
...messages
|
|
1167
|
-
.filter(m => !m.streaming)
|
|
1168
|
-
.map((message, index, filteredMessages) => {
|
|
1169
|
-
// Determine tool message type and color
|
|
1170
|
-
let toolStatusColor = 'cyan';
|
|
1171
|
-
let isToolMessage = false;
|
|
1172
|
-
const isLastMessage = index === filteredMessages.length - 1;
|
|
1173
|
-
// Check if this message is part of a parallel group
|
|
1174
|
-
const isInParallelGroup = message.parallelGroup !== undefined &&
|
|
1175
|
-
message.parallelGroup !== null;
|
|
1176
|
-
// Check if this is a time-consuming tool (has toolPending or starts with ⚡)
|
|
1177
|
-
// Time-consuming tools should not show parallel group indicators
|
|
1178
|
-
const isTimeConsumingTool = message.toolPending ||
|
|
1179
|
-
(message.role === 'assistant' &&
|
|
1180
|
-
(message.content.startsWith('⚡') ||
|
|
1181
|
-
message.content.includes('⚇⚡')));
|
|
1182
|
-
// Only show parallel group indicators for non-time-consuming tools
|
|
1183
|
-
const shouldShowParallelIndicator = isInParallelGroup && !isTimeConsumingTool;
|
|
1184
|
-
const isFirstInGroup = shouldShowParallelIndicator &&
|
|
1185
|
-
(index === 0 ||
|
|
1186
|
-
filteredMessages[index - 1]?.parallelGroup !==
|
|
1187
|
-
message.parallelGroup ||
|
|
1188
|
-
// Previous message is time-consuming tool, so this is the first non-time-consuming one
|
|
1189
|
-
filteredMessages[index - 1]?.toolPending ||
|
|
1190
|
-
filteredMessages[index - 1]?.content.startsWith('⚡'));
|
|
1191
|
-
// Check if this is the last message in the parallel group
|
|
1192
|
-
// Only show end indicator if:
|
|
1193
|
-
// 1. This is truly the last message, OR
|
|
1194
|
-
// 2. Next message has a DIFFERENT non-null parallelGroup (not just undefined)
|
|
1195
|
-
const nextMessage = filteredMessages[index + 1];
|
|
1196
|
-
const nextHasDifferentGroup = nextMessage &&
|
|
1197
|
-
nextMessage.parallelGroup !== undefined &&
|
|
1198
|
-
nextMessage.parallelGroup !== null &&
|
|
1199
|
-
nextMessage.parallelGroup !== message.parallelGroup;
|
|
1200
|
-
const isLastInGroup = shouldShowParallelIndicator &&
|
|
1201
|
-
(!nextMessage || nextHasDifferentGroup);
|
|
1202
|
-
if (message.role === 'assistant' || message.role === 'subagent') {
|
|
1203
|
-
if (message.content.startsWith('⚡') ||
|
|
1204
|
-
message.content.includes('⚇⚡')) {
|
|
1205
|
-
isToolMessage = true;
|
|
1206
|
-
toolStatusColor = 'yellowBright';
|
|
1207
|
-
}
|
|
1208
|
-
else if (message.content.startsWith('✓') ||
|
|
1209
|
-
message.content.includes('⚇✓')) {
|
|
1210
|
-
isToolMessage = true;
|
|
1211
|
-
toolStatusColor = 'green';
|
|
1212
|
-
}
|
|
1213
|
-
else if (message.content.startsWith('✗') ||
|
|
1214
|
-
message.content.includes('⚇✗')) {
|
|
1215
|
-
isToolMessage = true;
|
|
1216
|
-
toolStatusColor = 'red';
|
|
1217
|
-
}
|
|
1218
|
-
else {
|
|
1219
|
-
toolStatusColor =
|
|
1220
|
-
message.role === 'subagent' ? 'magenta' : 'blue';
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
return (React.createElement(Box, { key: `msg-${index}`, marginTop: index > 0 && !shouldShowParallelIndicator ? 1 : 0, marginBottom: isLastMessage ? 1 : 0, paddingX: 1, flexDirection: "column", width: terminalWidth },
|
|
1224
|
-
isFirstInGroup && (React.createElement(Box, { marginBottom: 0 },
|
|
1225
|
-
React.createElement(Text, { color: theme.colors.menuInfo, dimColor: true }, "\u250C\u2500 Parallel execution"))),
|
|
1226
|
-
React.createElement(Box, null,
|
|
1227
|
-
React.createElement(Text, { color: message.role === 'user'
|
|
1228
|
-
? 'green'
|
|
1229
|
-
: message.role === 'command'
|
|
1230
|
-
? theme.colors.menuSecondary
|
|
1231
|
-
: toolStatusColor, bold: true },
|
|
1232
|
-
shouldShowParallelIndicator && !isFirstInGroup
|
|
1233
|
-
? '│'
|
|
1234
|
-
: '',
|
|
1235
|
-
message.role === 'user'
|
|
1236
|
-
? '⛇'
|
|
1237
|
-
: message.role === 'command'
|
|
1238
|
-
? '⌘'
|
|
1239
|
-
: '❆'),
|
|
1240
|
-
React.createElement(Box, { marginLeft: 1, flexDirection: "column" }, message.role === 'command' ? (React.createElement(React.Fragment, null,
|
|
1241
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
|
|
1242
|
-
"\u2514\u2500 ",
|
|
1243
|
-
message.commandName),
|
|
1244
|
-
message.content && (React.createElement(Text, { color: "white" }, message.content)))) : (React.createElement(React.Fragment, null,
|
|
1245
|
-
message.role === 'user' || isToolMessage ? (React.createElement(Text, { color: message.role === 'user'
|
|
1246
|
-
? 'white'
|
|
1247
|
-
: message.content.startsWith('⚡')
|
|
1248
|
-
? 'yellow'
|
|
1249
|
-
: message.content.startsWith('✓')
|
|
1250
|
-
? 'green'
|
|
1251
|
-
: 'red', backgroundColor: message.role === 'user' ? theme.colors.border : undefined }, message.content || ' ')) : (React.createElement(MarkdownRenderer, { content: message.content || ' ' })),
|
|
1252
|
-
message.subAgentUsage &&
|
|
1253
|
-
(() => {
|
|
1254
|
-
const formatTokens = (num) => {
|
|
1255
|
-
if (num >= 1000)
|
|
1256
|
-
return `${(num / 1000).toFixed(1)}K`;
|
|
1257
|
-
return num.toString();
|
|
1258
|
-
};
|
|
1259
|
-
return (React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
|
|
1260
|
-
"\u2514\u2500 Usage: In=",
|
|
1261
|
-
formatTokens(message.subAgentUsage.inputTokens),
|
|
1262
|
-
", Out=",
|
|
1263
|
-
formatTokens(message.subAgentUsage.outputTokens),
|
|
1264
|
-
message.subAgentUsage.cacheReadInputTokens
|
|
1265
|
-
? `, Cache Read=${formatTokens(message.subAgentUsage
|
|
1266
|
-
.cacheReadInputTokens)}`
|
|
1267
|
-
: '',
|
|
1268
|
-
message.subAgentUsage
|
|
1269
|
-
.cacheCreationInputTokens
|
|
1270
|
-
? `, Cache Create=${formatTokens(message.subAgentUsage
|
|
1271
|
-
.cacheCreationInputTokens)}`
|
|
1272
|
-
: ''));
|
|
1273
|
-
})(),
|
|
1274
|
-
message.toolDisplay &&
|
|
1275
|
-
message.toolDisplay.args.length > 0 &&
|
|
1276
|
-
// Hide tool arguments for sub-agent internal tools
|
|
1277
|
-
!message.subAgentInternal && (React.createElement(Box, { flexDirection: "column" }, message.toolDisplay.args.map((arg, argIndex) => (React.createElement(Text, { key: argIndex, color: theme.colors.menuSecondary, dimColor: true },
|
|
1278
|
-
arg.isLast ? '└─' : '├─',
|
|
1279
|
-
" ",
|
|
1280
|
-
arg.key,
|
|
1281
|
-
":",
|
|
1282
|
-
' ',
|
|
1283
|
-
arg.value))))),
|
|
1284
|
-
message.toolCall &&
|
|
1285
|
-
message.toolCall.name === 'filesystem-create' &&
|
|
1286
|
-
message.toolCall.arguments.content && (React.createElement(Box, { marginTop: 1 },
|
|
1287
|
-
React.createElement(DiffViewer, { newContent: message.toolCall.arguments.content, filename: message.toolCall.arguments.path }))),
|
|
1288
|
-
message.toolCall &&
|
|
1289
|
-
message.toolCall.name === 'filesystem-edit' &&
|
|
1290
|
-
message.toolCall.arguments.oldContent &&
|
|
1291
|
-
message.toolCall.arguments.newContent && (React.createElement(Box, { marginTop: 1 },
|
|
1292
|
-
React.createElement(DiffViewer, { oldContent: message.toolCall.arguments.oldContent, newContent: message.toolCall.arguments.newContent, filename: message.toolCall.arguments.filename, completeOldContent: message.toolCall.arguments
|
|
1293
|
-
.completeOldContent, completeNewContent: message.toolCall.arguments
|
|
1294
|
-
.completeNewContent, startLineNumber: message.toolCall.arguments.contextStartLine }))),
|
|
1295
|
-
message.toolCall &&
|
|
1296
|
-
message.toolCall.name ===
|
|
1297
|
-
'filesystem-edit_search' &&
|
|
1298
|
-
message.toolCall.arguments.oldContent &&
|
|
1299
|
-
message.toolCall.arguments.newContent && (React.createElement(Box, { marginTop: 1 },
|
|
1300
|
-
React.createElement(DiffViewer, { oldContent: message.toolCall.arguments.oldContent, newContent: message.toolCall.arguments.newContent, filename: message.toolCall.arguments.filename, completeOldContent: message.toolCall.arguments
|
|
1301
|
-
.completeOldContent, completeNewContent: message.toolCall.arguments
|
|
1302
|
-
.completeNewContent, startLineNumber: message.toolCall.arguments.contextStartLine }))),
|
|
1303
|
-
message.toolCall &&
|
|
1304
|
-
(message.toolCall.name === 'filesystem-edit' ||
|
|
1305
|
-
message.toolCall.name ===
|
|
1306
|
-
'filesystem-edit_search') &&
|
|
1307
|
-
message.toolCall.arguments.isBatch &&
|
|
1308
|
-
message.toolCall.arguments.batchResults &&
|
|
1309
|
-
Array.isArray(message.toolCall.arguments.batchResults) && (React.createElement(Box, { marginTop: 1, flexDirection: "column" }, message.toolCall.arguments.batchResults.map((fileResult, index) => {
|
|
1310
|
-
if (fileResult.success &&
|
|
1311
|
-
fileResult.oldContent &&
|
|
1312
|
-
fileResult.newContent) {
|
|
1313
|
-
return (React.createElement(Box, { key: index, flexDirection: "column", marginBottom: 1 },
|
|
1314
|
-
React.createElement(Text, { bold: true, color: "cyan" }, `File ${index + 1}: ${fileResult.path}`),
|
|
1315
|
-
React.createElement(DiffViewer, { oldContent: fileResult.oldContent, newContent: fileResult.newContent, filename: fileResult.path, completeOldContent: fileResult.completeOldContent, completeNewContent: fileResult.completeNewContent, startLineNumber: fileResult.contextStartLine })));
|
|
1316
|
-
}
|
|
1317
|
-
return null;
|
|
1318
|
-
}))),
|
|
1319
|
-
(message.content.startsWith('✓') ||
|
|
1320
|
-
message.content.includes('⚇✓')) &&
|
|
1321
|
-
message.toolResult &&
|
|
1322
|
-
// 只在没有 diff 数据时显示预览(有 diff 的工具会用 DiffViewer 显示)
|
|
1323
|
-
!(message.toolCall &&
|
|
1324
|
-
(message.toolCall.arguments?.oldContent ||
|
|
1325
|
-
message.toolCall.arguments?.batchResults)) && (React.createElement(ToolResultPreview, { toolName: (message.content || '')
|
|
1326
|
-
.replace(/^✓\s*/, '') // Remove leading ✓
|
|
1327
|
-
.replace(/^⚇✓\s*/, '') // Remove leading ⚇✓
|
|
1328
|
-
.replace(/.*⚇✓\s*/, '') // Remove any prefix before ⚇✓
|
|
1329
|
-
.replace(/\x1b\[[0-9;]*m/g, '') // Remove ANSI color codes
|
|
1330
|
-
.split('\n')[0]
|
|
1331
|
-
?.trim() || '', result: message.toolResult, maxLines: 5, isSubAgentInternal: message.role === 'subagent' ||
|
|
1332
|
-
message.subAgentInternal === true })),
|
|
1333
|
-
(message.content.startsWith('✗') ||
|
|
1334
|
-
message.content.includes('⚇✗')) &&
|
|
1335
|
-
message.content.includes('Tool execution rejected by user:') && (React.createElement(Box, { flexDirection: "column", marginTop: 1 },
|
|
1336
|
-
React.createElement(Text, { color: "yellow", dimColor: true }, "Rejection reason:"),
|
|
1337
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
|
|
1338
|
-
"\u2514\u2500",
|
|
1339
|
-
' ',
|
|
1340
|
-
message.content
|
|
1341
|
-
.split('Tool execution rejected by user:')[1]
|
|
1342
|
-
?.trim() || 'No reason provided'))),
|
|
1343
|
-
message.files && message.files.length > 0 && (React.createElement(Box, { flexDirection: "column" }, message.files.map((file, fileIndex) => (React.createElement(Text, { key: fileIndex, color: theme.colors.menuSecondary, dimColor: true },
|
|
1344
|
-
"\u2514\u2500 ",
|
|
1345
|
-
file.path,
|
|
1346
|
-
file.exists
|
|
1347
|
-
? ` (total line ${file.lineCount})`
|
|
1348
|
-
: ' (file not found)'))))),
|
|
1349
|
-
message.role === 'user' &&
|
|
1350
|
-
message.images &&
|
|
1351
|
-
message.images.length > 0 && (React.createElement(Box, { marginTop: 1, flexDirection: "column" }, message.images.map((_image, imageIndex) => (React.createElement(Text, { key: imageIndex, color: theme.colors.menuSecondary, dimColor: true },
|
|
1352
|
-
"\u2514\u2500 [image #",
|
|
1353
|
-
imageIndex + 1,
|
|
1354
|
-
"]"))))),
|
|
1355
|
-
message.discontinued && (React.createElement(Text, { color: "red", bold: true }, "\u2514\u2500 user discontinue")))))),
|
|
1356
|
-
isLastInGroup && (React.createElement(Box, { marginTop: 0 },
|
|
1357
|
-
React.createElement(Text, { color: theme.colors.menuInfo, dimColor: true }, "\u2514\u2500 End parallel execution")))));
|
|
1358
|
-
}),
|
|
1359
|
-
] }, item => item),
|
|
1360
|
-
(streamingState.isStreaming || isSaving) && !pendingToolConfirmation && (React.createElement(Box, { marginBottom: 1, paddingX: 1, width: terminalWidth },
|
|
1361
|
-
React.createElement(Text, { color: [theme.colors.menuInfo, theme.colors.success, theme.colors.menuSelected, theme.colors.menuInfo, theme.colors.menuSecondary][streamingState.animationFrame], bold: true }, "\u2746"),
|
|
1362
|
-
React.createElement(Box, { marginLeft: 1, marginBottom: 1, flexDirection: "column" }, streamingState.isStreaming ? (React.createElement(React.Fragment, null, streamingState.retryStatus &&
|
|
1363
|
-
streamingState.retryStatus.isRetrying ? (
|
|
1364
|
-
// Retry status display - hide "Thinking" and show retry info
|
|
1365
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
1366
|
-
streamingState.retryStatus.errorMessage && (React.createElement(Text, { color: "red", dimColor: true },
|
|
1367
|
-
"\u2717 Error: ",
|
|
1368
|
-
streamingState.retryStatus.errorMessage)),
|
|
1369
|
-
streamingState.retryStatus.remainingSeconds !==
|
|
1370
|
-
undefined &&
|
|
1371
|
-
streamingState.retryStatus.remainingSeconds > 0 ? (React.createElement(Text, { color: "yellow", dimColor: true },
|
|
1372
|
-
"\u27F3 Retry ",
|
|
1373
|
-
streamingState.retryStatus.attempt,
|
|
1374
|
-
"/5 in",
|
|
1375
|
-
' ',
|
|
1376
|
-
streamingState.retryStatus.remainingSeconds,
|
|
1377
|
-
"s...")) : (React.createElement(Text, { color: "yellow", dimColor: true },
|
|
1378
|
-
"\u27F3 Resending... (Attempt",
|
|
1379
|
-
' ',
|
|
1380
|
-
streamingState.retryStatus.attempt,
|
|
1381
|
-
"/5)")))) : streamingState.codebaseSearchStatus?.isSearching ? (
|
|
1382
|
-
// Codebase search retry status
|
|
1383
|
-
React.createElement(Box, { flexDirection: "column" },
|
|
1384
|
-
React.createElement(Text, { color: "cyan", dimColor: true },
|
|
1385
|
-
"\u23CF Codebase Search (Attempt",
|
|
1386
|
-
' ',
|
|
1387
|
-
streamingState.codebaseSearchStatus.attempt,
|
|
1388
|
-
"/",
|
|
1389
|
-
streamingState.codebaseSearchStatus.maxAttempts,
|
|
1390
|
-
")"),
|
|
1391
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, streamingState.codebaseSearchStatus.message))) : (
|
|
1392
|
-
// Normal thinking status
|
|
1393
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true },
|
|
1394
|
-
React.createElement(ShimmerText, { text: streamingState.isReasoning
|
|
1395
|
-
? t.chatScreen.statusDeepThinking
|
|
1396
|
-
: streamingState.streamTokenCount > 0
|
|
1397
|
-
? t.chatScreen.statusWriting
|
|
1398
|
-
: t.chatScreen.statusThinking }),
|
|
1399
|
-
' ',
|
|
1400
|
-
"(",
|
|
1401
|
-
formatElapsedTime(streamingState.elapsedSeconds),
|
|
1402
|
-
' · ',
|
|
1403
|
-
React.createElement(Text, { color: "cyan" },
|
|
1404
|
-
"\u2193",
|
|
1405
|
-
' ',
|
|
1406
|
-
streamingState.streamTokenCount >= 1000
|
|
1407
|
-
? `${(streamingState.streamTokenCount / 1000).toFixed(1)}k`
|
|
1408
|
-
: streamingState.streamTokenCount,
|
|
1409
|
-
' ',
|
|
1410
|
-
"tokens"),
|
|
1411
|
-
")")))) : (React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.sessionCreating))))),
|
|
1412
|
-
React.createElement(Box, { paddingX: 1, width: terminalWidth },
|
|
1413
|
-
React.createElement(PendingMessages, { pendingMessages: pendingMessages })),
|
|
1414
|
-
pendingToolConfirmation && (React.createElement(ToolConfirmation, { toolName: pendingToolConfirmation.batchToolNames ||
|
|
1415
|
-
pendingToolConfirmation.tool.function.name, toolArguments: !pendingToolConfirmation.allTools
|
|
1416
|
-
? pendingToolConfirmation.tool.function.arguments
|
|
1417
|
-
: undefined, allTools: pendingToolConfirmation.allTools, onConfirm: pendingToolConfirmation.resolve })),
|
|
1418
|
-
showSessionPanel && (React.createElement(Box, { paddingX: 1, width: terminalWidth },
|
|
1419
|
-
React.createElement(SessionListPanel, { onSelectSession: handleSessionPanelSelect, onClose: () => setShowSessionPanel(false) }))),
|
|
1420
|
-
showMcpPanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: terminalWidth },
|
|
1421
|
-
React.createElement(MCPInfoPanel, null),
|
|
1422
|
-
React.createElement(Box, { marginTop: 1 },
|
|
1423
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.pressEscToClose)))),
|
|
1424
|
-
showUsagePanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: terminalWidth },
|
|
1425
|
-
React.createElement(UsagePanel, null),
|
|
1426
|
-
React.createElement(Box, { marginTop: 1 },
|
|
1427
|
-
React.createElement(Text, { color: theme.colors.menuSecondary, dimColor: true }, t.chatScreen.pressEscToClose)))),
|
|
1428
|
-
showHelpPanel && (React.createElement(Box, { paddingX: 1, flexDirection: "column", width: terminalWidth },
|
|
1429
|
-
React.createElement(HelpPanel, null))),
|
|
1430
|
-
snapshotState.pendingRollback && (React.createElement(FileRollbackConfirmation, { fileCount: snapshotState.pendingRollback.fileCount, filePaths: snapshotState.pendingRollback.filePaths || [], onConfirm: handleRollbackConfirm })),
|
|
1431
|
-
!pendingToolConfirmation &&
|
|
1432
|
-
!isCompressing &&
|
|
1433
|
-
!showSessionPanel &&
|
|
1434
|
-
!showMcpPanel &&
|
|
1435
|
-
!showUsagePanel &&
|
|
1436
|
-
!showHelpPanel &&
|
|
1437
|
-
!snapshotState.pendingRollback && (React.createElement(React.Fragment, null,
|
|
1438
|
-
React.createElement(ChatInput, { onSubmit: handleMessageSubmit, onCommand: handleCommandExecution, placeholder: t.chatScreen.inputPlaceholder, disabled: !!pendingToolConfirmation, isProcessing: streamingState.isStreaming || isSaving, chatHistory: messages, onHistorySelect: handleHistorySelect, yoloMode: yoloMode, contextUsage: streamingState.contextUsage
|
|
1439
|
-
? {
|
|
1440
|
-
inputTokens: streamingState.contextUsage.prompt_tokens,
|
|
1441
|
-
maxContextTokens: getOpenAiConfig().maxContextTokens || 4000,
|
|
1442
|
-
cacheCreationTokens: streamingState.contextUsage.cache_creation_input_tokens,
|
|
1443
|
-
cacheReadTokens: streamingState.contextUsage.cache_read_input_tokens,
|
|
1444
|
-
cachedTokens: streamingState.contextUsage.cached_tokens,
|
|
1445
|
-
}
|
|
1446
|
-
: undefined, initialContent: restoreInputContent, onContextPercentageChange: setCurrentContextPercentage }),
|
|
1447
|
-
vscodeState.vscodeConnectionStatus !== 'disconnected' && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
|
|
1448
|
-
React.createElement(Text, { color: vscodeState.vscodeConnectionStatus === 'connecting'
|
|
1449
|
-
? 'yellow'
|
|
1450
|
-
: vscodeState.vscodeConnectionStatus === 'connected'
|
|
1451
|
-
? 'green'
|
|
1452
|
-
: vscodeState.vscodeConnectionStatus === 'error'
|
|
1453
|
-
? 'red'
|
|
1454
|
-
: theme.colors.menuSecondary, dimColor: vscodeState.vscodeConnectionStatus !== 'error' },
|
|
1455
|
-
"\u25CF",
|
|
1456
|
-
' ',
|
|
1457
|
-
vscodeState.vscodeConnectionStatus === 'connecting'
|
|
1458
|
-
? t.chatScreen.ideConnecting
|
|
1459
|
-
: vscodeState.vscodeConnectionStatus === 'connected'
|
|
1460
|
-
? t.chatScreen.ideConnected
|
|
1461
|
-
: vscodeState.vscodeConnectionStatus === 'error'
|
|
1462
|
-
? t.chatScreen.ideError
|
|
1463
|
-
: 'IDE',
|
|
1464
|
-
vscodeState.vscodeConnectionStatus === 'connected' &&
|
|
1465
|
-
vscodeState.editorContext.activeFile &&
|
|
1466
|
-
t.chatScreen.ideActiveFile.replace('{file}', vscodeState.editorContext.activeFile),
|
|
1467
|
-
vscodeState.vscodeConnectionStatus === 'connected' &&
|
|
1468
|
-
vscodeState.editorContext.selectedText &&
|
|
1469
|
-
t.chatScreen.ideSelectedText.replace('{count}', vscodeState.editorContext.selectedText.length.toString())))),
|
|
1470
|
-
codebaseIndexing && codebaseProgress && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
|
|
1471
|
-
React.createElement(Text, { color: "cyan", dimColor: true },
|
|
1472
|
-
React.createElement(Spinner, { type: "dots" }),
|
|
1473
|
-
' ',
|
|
1474
|
-
t.chatScreen.codebaseIndexing
|
|
1475
|
-
.replace('{processed}', codebaseProgress.processedFiles.toString())
|
|
1476
|
-
.replace('{total}', codebaseProgress.totalFiles.toString()),
|
|
1477
|
-
codebaseProgress.totalChunks > 0 &&
|
|
1478
|
-
` (${t.chatScreen.codebaseProgress.replace('{chunks}', codebaseProgress.totalChunks.toString())})`))),
|
|
1479
|
-
!codebaseIndexing && watcherEnabled && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
|
|
1480
|
-
React.createElement(Text, { color: "green", dimColor: true },
|
|
1481
|
-
"\u2609 ",
|
|
1482
|
-
t.chatScreen.statusWatcherActive))),
|
|
1483
|
-
fileUpdateNotification && (React.createElement(Box, { marginTop: 1, paddingX: 1 },
|
|
1484
|
-
React.createElement(Text, { color: "yellow", dimColor: true },
|
|
1485
|
-
"\u26C1",
|
|
1486
|
-
' ',
|
|
1487
|
-
t.chatScreen.statusFileUpdated.replace('{file}', fileUpdateNotification.file)))))),
|
|
1488
|
-
isCompressing && (React.createElement(Box, { marginTop: 1 },
|
|
1489
|
-
React.createElement(Text, { color: "cyan" },
|
|
1490
|
-
React.createElement(Spinner, { type: "dots" }),
|
|
1491
|
-
" ",
|
|
1492
|
-
t.chatScreen.compressionInProgress))),
|
|
1493
|
-
compressionError && (React.createElement(Box, { marginTop: 1 },
|
|
1494
|
-
React.createElement(Text, { color: "red" }, t.chatScreen.compressionFailed.replace('{error}', compressionError))))));
|
|
1495
|
-
}
|