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