snow-ai 0.4.16 → 0.4.18

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 (352) hide show
  1. package/bundle/cli.mjs +477445 -0
  2. package/bundle/sql-wasm.wasm +0 -0
  3. package/bundle/tiktoken_bg.wasm +0 -0
  4. package/package.json +31 -26
  5. package/dist/agents/codebaseIndexAgent.d.ts +0 -102
  6. package/dist/agents/codebaseIndexAgent.js +0 -641
  7. package/dist/agents/codebaseReviewAgent.d.ts +0 -61
  8. package/dist/agents/codebaseReviewAgent.js +0 -301
  9. package/dist/agents/compactAgent.d.ts +0 -55
  10. package/dist/agents/compactAgent.js +0 -306
  11. package/dist/agents/promptOptimizeAgent.d.ts +0 -54
  12. package/dist/agents/promptOptimizeAgent.js +0 -268
  13. package/dist/agents/reviewAgent.d.ts +0 -50
  14. package/dist/agents/reviewAgent.js +0 -265
  15. package/dist/agents/summaryAgent.d.ts +0 -57
  16. package/dist/agents/summaryAgent.js +0 -260
  17. package/dist/api/anthropic.d.ts +0 -44
  18. package/dist/api/anthropic.js +0 -598
  19. package/dist/api/chat.d.ts +0 -73
  20. package/dist/api/chat.js +0 -386
  21. package/dist/api/embedding.d.ts +0 -34
  22. package/dist/api/embedding.js +0 -80
  23. package/dist/api/gemini.d.ts +0 -31
  24. package/dist/api/gemini.js +0 -445
  25. package/dist/api/models.d.ts +0 -15
  26. package/dist/api/models.js +0 -139
  27. package/dist/api/responses.d.ts +0 -38
  28. package/dist/api/responses.js +0 -515
  29. package/dist/api/systemPrompt.d.ts +0 -4
  30. package/dist/api/systemPrompt.js +0 -408
  31. package/dist/api/types.d.ts +0 -53
  32. package/dist/api/types.js +0 -4
  33. package/dist/app.d.ts +0 -8
  34. package/dist/app.js +0 -112
  35. package/dist/cli.d.ts +0 -2
  36. package/dist/cli.js +0 -199
  37. package/dist/hooks/useAgentPicker.d.ts +0 -14
  38. package/dist/hooks/useAgentPicker.js +0 -119
  39. package/dist/hooks/useClipboard.d.ts +0 -4
  40. package/dist/hooks/useClipboard.js +0 -175
  41. package/dist/hooks/useCommandHandler.d.ts +0 -35
  42. package/dist/hooks/useCommandHandler.js +0 -346
  43. package/dist/hooks/useCommandPanel.d.ts +0 -17
  44. package/dist/hooks/useCommandPanel.js +0 -114
  45. package/dist/hooks/useConversation.d.ts +0 -49
  46. package/dist/hooks/useConversation.js +0 -1052
  47. package/dist/hooks/useFilePicker.d.ts +0 -18
  48. package/dist/hooks/useFilePicker.js +0 -224
  49. package/dist/hooks/useGlobalExit.d.ts +0 -5
  50. package/dist/hooks/useGlobalExit.js +0 -34
  51. package/dist/hooks/useGlobalNavigation.d.ts +0 -6
  52. package/dist/hooks/useGlobalNavigation.js +0 -17
  53. package/dist/hooks/useHistoryNavigation.d.ts +0 -35
  54. package/dist/hooks/useHistoryNavigation.js +0 -133
  55. package/dist/hooks/useInputBuffer.d.ts +0 -6
  56. package/dist/hooks/useInputBuffer.js +0 -45
  57. package/dist/hooks/useKeyboardInput.d.ts +0 -80
  58. package/dist/hooks/useKeyboardInput.js +0 -608
  59. package/dist/hooks/useSessionManagement.d.ts +0 -10
  60. package/dist/hooks/useSessionManagement.js +0 -43
  61. package/dist/hooks/useSessionSave.d.ts +0 -8
  62. package/dist/hooks/useSessionSave.js +0 -63
  63. package/dist/hooks/useSnapshotState.d.ts +0 -26
  64. package/dist/hooks/useSnapshotState.js +0 -28
  65. package/dist/hooks/useStreamingState.d.ts +0 -33
  66. package/dist/hooks/useStreamingState.js +0 -105
  67. package/dist/hooks/useTerminalFocus.d.ts +0 -28
  68. package/dist/hooks/useTerminalFocus.js +0 -87
  69. package/dist/hooks/useTerminalSize.d.ts +0 -4
  70. package/dist/hooks/useTerminalSize.js +0 -20
  71. package/dist/hooks/useTodoPicker.d.ts +0 -16
  72. package/dist/hooks/useTodoPicker.js +0 -94
  73. package/dist/hooks/useToolConfirmation.d.ts +0 -19
  74. package/dist/hooks/useToolConfirmation.js +0 -61
  75. package/dist/hooks/useVSCodeState.d.ts +0 -8
  76. package/dist/hooks/useVSCodeState.js +0 -81
  77. package/dist/i18n/I18nContext.d.ts +0 -14
  78. package/dist/i18n/I18nContext.js +0 -24
  79. package/dist/i18n/index.d.ts +0 -3
  80. package/dist/i18n/index.js +0 -2
  81. package/dist/i18n/lang/en.d.ts +0 -2
  82. package/dist/i18n/lang/en.js +0 -502
  83. package/dist/i18n/lang/es.d.ts +0 -2
  84. package/dist/i18n/lang/es.js +0 -502
  85. package/dist/i18n/lang/ja.d.ts +0 -2
  86. package/dist/i18n/lang/ja.js +0 -502
  87. package/dist/i18n/lang/ko.d.ts +0 -2
  88. package/dist/i18n/lang/ko.js +0 -502
  89. package/dist/i18n/lang/zh-TW.d.ts +0 -2
  90. package/dist/i18n/lang/zh-TW.js +0 -502
  91. package/dist/i18n/lang/zh.d.ts +0 -2
  92. package/dist/i18n/lang/zh.js +0 -502
  93. package/dist/i18n/translations.d.ts +0 -2
  94. package/dist/i18n/translations.js +0 -14
  95. package/dist/i18n/types.d.ts +0 -478
  96. package/dist/i18n/types.js +0 -1
  97. package/dist/mcp/aceCodeSearch.d.ts +0 -247
  98. package/dist/mcp/aceCodeSearch.js +0 -1058
  99. package/dist/mcp/bash.d.ts +0 -50
  100. package/dist/mcp/bash.js +0 -153
  101. package/dist/mcp/codebaseSearch.d.ts +0 -44
  102. package/dist/mcp/codebaseSearch.js +0 -275
  103. package/dist/mcp/filesystem.d.ts +0 -392
  104. package/dist/mcp/filesystem.js +0 -1445
  105. package/dist/mcp/ideDiagnostics.d.ts +0 -36
  106. package/dist/mcp/ideDiagnostics.js +0 -90
  107. package/dist/mcp/notebook.d.ts +0 -10
  108. package/dist/mcp/notebook.js +0 -367
  109. package/dist/mcp/subagent.d.ts +0 -37
  110. package/dist/mcp/subagent.js +0 -113
  111. package/dist/mcp/todo.d.ts +0 -46
  112. package/dist/mcp/todo.js +0 -511
  113. package/dist/mcp/types/aceCodeSearch.types.d.ts +0 -92
  114. package/dist/mcp/types/aceCodeSearch.types.js +0 -4
  115. package/dist/mcp/types/bash.types.d.ts +0 -13
  116. package/dist/mcp/types/bash.types.js +0 -4
  117. package/dist/mcp/types/filesystem.types.d.ts +0 -210
  118. package/dist/mcp/types/filesystem.types.js +0 -27
  119. package/dist/mcp/types/todo.types.d.ts +0 -27
  120. package/dist/mcp/types/todo.types.js +0 -4
  121. package/dist/mcp/types/websearch.types.d.ts +0 -30
  122. package/dist/mcp/types/websearch.types.js +0 -4
  123. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.d.ts +0 -34
  124. package/dist/mcp/utils/aceCodeSearch/filesystem.utils.js +0 -146
  125. package/dist/mcp/utils/aceCodeSearch/language.utils.d.ts +0 -14
  126. package/dist/mcp/utils/aceCodeSearch/language.utils.js +0 -418
  127. package/dist/mcp/utils/aceCodeSearch/search.utils.d.ts +0 -31
  128. package/dist/mcp/utils/aceCodeSearch/search.utils.js +0 -136
  129. package/dist/mcp/utils/aceCodeSearch/symbol.utils.d.ts +0 -20
  130. package/dist/mcp/utils/aceCodeSearch/symbol.utils.js +0 -141
  131. package/dist/mcp/utils/bash/security.utils.d.ts +0 -20
  132. package/dist/mcp/utils/bash/security.utils.js +0 -34
  133. package/dist/mcp/utils/filesystem/batch-operations.utils.d.ts +0 -39
  134. package/dist/mcp/utils/filesystem/batch-operations.utils.js +0 -182
  135. package/dist/mcp/utils/filesystem/code-analysis.utils.d.ts +0 -18
  136. package/dist/mcp/utils/filesystem/code-analysis.utils.js +0 -165
  137. package/dist/mcp/utils/filesystem/match-finder.utils.d.ts +0 -16
  138. package/dist/mcp/utils/filesystem/match-finder.utils.js +0 -85
  139. package/dist/mcp/utils/filesystem/office-parser.utils.d.ts +0 -43
  140. package/dist/mcp/utils/filesystem/office-parser.utils.js +0 -163
  141. package/dist/mcp/utils/filesystem/path-fixer.utils.d.ts +0 -7
  142. package/dist/mcp/utils/filesystem/path-fixer.utils.js +0 -60
  143. package/dist/mcp/utils/filesystem/similarity.utils.d.ts +0 -22
  144. package/dist/mcp/utils/filesystem/similarity.utils.js +0 -75
  145. package/dist/mcp/utils/todo/date.utils.d.ts +0 -9
  146. package/dist/mcp/utils/todo/date.utils.js +0 -14
  147. package/dist/mcp/utils/websearch/browser.utils.d.ts +0 -8
  148. package/dist/mcp/utils/websearch/browser.utils.js +0 -58
  149. package/dist/mcp/utils/websearch/text.utils.d.ts +0 -16
  150. package/dist/mcp/utils/websearch/text.utils.js +0 -39
  151. package/dist/mcp/websearch.d.ts +0 -88
  152. package/dist/mcp/websearch.js +0 -375
  153. package/dist/test/logger-test.d.ts +0 -1
  154. package/dist/test/logger-test.js +0 -7
  155. package/dist/types/index.d.ts +0 -15
  156. package/dist/types/index.js +0 -1
  157. package/dist/ui/components/AgentPickerPanel.d.ts +0 -10
  158. package/dist/ui/components/AgentPickerPanel.js +0 -74
  159. package/dist/ui/components/ChatInput.d.ts +0 -46
  160. package/dist/ui/components/ChatInput.js +0 -384
  161. package/dist/ui/components/CommandPanel.d.ts +0 -15
  162. package/dist/ui/components/CommandPanel.js +0 -80
  163. package/dist/ui/components/DiffViewer.d.ts +0 -11
  164. package/dist/ui/components/DiffViewer.js +0 -178
  165. package/dist/ui/components/FileList.d.ts +0 -15
  166. package/dist/ui/components/FileList.js +0 -360
  167. package/dist/ui/components/FileRollbackConfirmation.d.ts +0 -8
  168. package/dist/ui/components/FileRollbackConfirmation.js +0 -108
  169. package/dist/ui/components/HelpPanel.d.ts +0 -2
  170. package/dist/ui/components/HelpPanel.js +0 -67
  171. package/dist/ui/components/MCPInfoPanel.d.ts +0 -2
  172. package/dist/ui/components/MCPInfoPanel.js +0 -108
  173. package/dist/ui/components/MCPInfoScreen.d.ts +0 -7
  174. package/dist/ui/components/MCPInfoScreen.js +0 -115
  175. package/dist/ui/components/MarkdownRenderer.d.ts +0 -6
  176. package/dist/ui/components/MarkdownRenderer.js +0 -70
  177. package/dist/ui/components/Menu.d.ts +0 -17
  178. package/dist/ui/components/Menu.js +0 -88
  179. package/dist/ui/components/MessageList.d.ts +0 -56
  180. package/dist/ui/components/MessageList.js +0 -97
  181. package/dist/ui/components/PendingMessages.d.ts +0 -13
  182. package/dist/ui/components/PendingMessages.js +0 -29
  183. package/dist/ui/components/PendingToolCalls.d.ts +0 -11
  184. package/dist/ui/components/PendingToolCalls.js +0 -35
  185. package/dist/ui/components/ScrollableSelectInput.d.ts +0 -29
  186. package/dist/ui/components/ScrollableSelectInput.js +0 -157
  187. package/dist/ui/components/SessionListPanel.d.ts +0 -7
  188. package/dist/ui/components/SessionListPanel.js +0 -175
  189. package/dist/ui/components/SessionListScreen.d.ts +0 -7
  190. package/dist/ui/components/SessionListScreen.js +0 -217
  191. package/dist/ui/components/SessionListScreenWrapper.d.ts +0 -7
  192. package/dist/ui/components/SessionListScreenWrapper.js +0 -14
  193. package/dist/ui/components/ShimmerText.d.ts +0 -9
  194. package/dist/ui/components/ShimmerText.js +0 -30
  195. package/dist/ui/components/TodoPickerPanel.d.ts +0 -14
  196. package/dist/ui/components/TodoPickerPanel.js +0 -119
  197. package/dist/ui/components/TodoTree.d.ts +0 -15
  198. package/dist/ui/components/TodoTree.js +0 -60
  199. package/dist/ui/components/ToolConfirmation.d.ts +0 -21
  200. package/dist/ui/components/ToolConfirmation.js +0 -204
  201. package/dist/ui/components/ToolResultPreview.d.ts +0 -13
  202. package/dist/ui/components/ToolResultPreview.js +0 -337
  203. package/dist/ui/components/UsagePanel.d.ts +0 -2
  204. package/dist/ui/components/UsagePanel.js +0 -394
  205. package/dist/ui/contexts/ThemeContext.d.ts +0 -13
  206. package/dist/ui/contexts/ThemeContext.js +0 -28
  207. package/dist/ui/pages/ChatScreen.d.ts +0 -6
  208. package/dist/ui/pages/ChatScreen.js +0 -1519
  209. package/dist/ui/pages/CodeBaseConfigScreen.d.ts +0 -8
  210. package/dist/ui/pages/CodeBaseConfigScreen.js +0 -350
  211. package/dist/ui/pages/ConfigScreen.d.ts +0 -8
  212. package/dist/ui/pages/ConfigScreen.js +0 -1101
  213. package/dist/ui/pages/CustomHeadersScreen.d.ts +0 -6
  214. package/dist/ui/pages/CustomHeadersScreen.js +0 -502
  215. package/dist/ui/pages/HeadlessModeScreen.d.ts +0 -7
  216. package/dist/ui/pages/HeadlessModeScreen.js +0 -381
  217. package/dist/ui/pages/LanguageSettingsScreen.d.ts +0 -7
  218. package/dist/ui/pages/LanguageSettingsScreen.js +0 -91
  219. package/dist/ui/pages/MCPConfigScreen.d.ts +0 -6
  220. package/dist/ui/pages/MCPConfigScreen.js +0 -55
  221. package/dist/ui/pages/ProxyConfigScreen.d.ts +0 -8
  222. package/dist/ui/pages/ProxyConfigScreen.js +0 -149
  223. package/dist/ui/pages/SensitiveCommandConfigScreen.d.ts +0 -7
  224. package/dist/ui/pages/SensitiveCommandConfigScreen.js +0 -271
  225. package/dist/ui/pages/SubAgentConfigScreen.d.ts +0 -9
  226. package/dist/ui/pages/SubAgentConfigScreen.js +0 -435
  227. package/dist/ui/pages/SubAgentListScreen.d.ts +0 -9
  228. package/dist/ui/pages/SubAgentListScreen.js +0 -131
  229. package/dist/ui/pages/SystemPromptConfigScreen.d.ts +0 -6
  230. package/dist/ui/pages/SystemPromptConfigScreen.js +0 -326
  231. package/dist/ui/pages/ThemeSettingsScreen.d.ts +0 -7
  232. package/dist/ui/pages/ThemeSettingsScreen.js +0 -106
  233. package/dist/ui/pages/WelcomeScreen.d.ts +0 -7
  234. package/dist/ui/pages/WelcomeScreen.js +0 -217
  235. package/dist/ui/themes/index.d.ts +0 -23
  236. package/dist/ui/themes/index.js +0 -140
  237. package/dist/utils/apiConfig.d.ts +0 -126
  238. package/dist/utils/apiConfig.js +0 -423
  239. package/dist/utils/autoCompress.d.ts +0 -15
  240. package/dist/utils/autoCompress.js +0 -24
  241. package/dist/utils/chatExporter.d.ts +0 -9
  242. package/dist/utils/chatExporter.js +0 -118
  243. package/dist/utils/checkpointManager.d.ts +0 -74
  244. package/dist/utils/checkpointManager.js +0 -181
  245. package/dist/utils/codebaseConfig.d.ts +0 -16
  246. package/dist/utils/codebaseConfig.js +0 -67
  247. package/dist/utils/codebaseDatabase.d.ts +0 -102
  248. package/dist/utils/codebaseDatabase.js +0 -333
  249. package/dist/utils/codebaseSearchEvents.d.ts +0 -16
  250. package/dist/utils/codebaseSearchEvents.js +0 -13
  251. package/dist/utils/commandExecutor.d.ts +0 -13
  252. package/dist/utils/commandExecutor.js +0 -26
  253. package/dist/utils/commands/agent.d.ts +0 -2
  254. package/dist/utils/commands/agent.js +0 -12
  255. package/dist/utils/commands/clear.d.ts +0 -2
  256. package/dist/utils/commands/clear.js +0 -12
  257. package/dist/utils/commands/compact.d.ts +0 -2
  258. package/dist/utils/commands/compact.js +0 -12
  259. package/dist/utils/commands/export.d.ts +0 -2
  260. package/dist/utils/commands/export.js +0 -12
  261. package/dist/utils/commands/help.d.ts +0 -2
  262. package/dist/utils/commands/help.js +0 -11
  263. package/dist/utils/commands/home.d.ts +0 -2
  264. package/dist/utils/commands/home.js +0 -34
  265. package/dist/utils/commands/ide.d.ts +0 -2
  266. package/dist/utils/commands/ide.js +0 -32
  267. package/dist/utils/commands/init.d.ts +0 -2
  268. package/dist/utils/commands/init.js +0 -93
  269. package/dist/utils/commands/mcp.d.ts +0 -2
  270. package/dist/utils/commands/mcp.js +0 -12
  271. package/dist/utils/commands/resume.d.ts +0 -2
  272. package/dist/utils/commands/resume.js +0 -12
  273. package/dist/utils/commands/review.d.ts +0 -2
  274. package/dist/utils/commands/review.js +0 -81
  275. package/dist/utils/commands/role.d.ts +0 -2
  276. package/dist/utils/commands/role.js +0 -37
  277. package/dist/utils/commands/todoPicker.d.ts +0 -2
  278. package/dist/utils/commands/todoPicker.js +0 -12
  279. package/dist/utils/commands/usage.d.ts +0 -2
  280. package/dist/utils/commands/usage.js +0 -12
  281. package/dist/utils/commands/yolo.d.ts +0 -2
  282. package/dist/utils/commands/yolo.js +0 -12
  283. package/dist/utils/configManager.d.ts +0 -45
  284. package/dist/utils/configManager.js +0 -303
  285. package/dist/utils/contextCompressor.d.ts +0 -16
  286. package/dist/utils/contextCompressor.js +0 -334
  287. package/dist/utils/devMode.d.ts +0 -13
  288. package/dist/utils/devMode.js +0 -54
  289. package/dist/utils/escapeHandler.d.ts +0 -79
  290. package/dist/utils/escapeHandler.js +0 -153
  291. package/dist/utils/fileDialog.d.ts +0 -9
  292. package/dist/utils/fileDialog.js +0 -74
  293. package/dist/utils/fileUtils.d.ts +0 -40
  294. package/dist/utils/fileUtils.js +0 -185
  295. package/dist/utils/historyManager.d.ts +0 -45
  296. package/dist/utils/historyManager.js +0 -159
  297. package/dist/utils/incrementalSnapshot.d.ts +0 -109
  298. package/dist/utils/incrementalSnapshot.js +0 -383
  299. package/dist/utils/index.d.ts +0 -11
  300. package/dist/utils/index.js +0 -18
  301. package/dist/utils/languageConfig.d.ts +0 -21
  302. package/dist/utils/languageConfig.js +0 -61
  303. package/dist/utils/logger.d.ts +0 -37
  304. package/dist/utils/logger.js +0 -122
  305. package/dist/utils/mcpToolsManager.d.ts +0 -52
  306. package/dist/utils/mcpToolsManager.js +0 -878
  307. package/dist/utils/messageFormatter.d.ts +0 -12
  308. package/dist/utils/messageFormatter.js +0 -115
  309. package/dist/utils/notebookManager.d.ts +0 -59
  310. package/dist/utils/notebookManager.js +0 -213
  311. package/dist/utils/patch-highlight.d.ts +0 -5
  312. package/dist/utils/patch-highlight.js +0 -23
  313. package/dist/utils/processManager.d.ts +0 -27
  314. package/dist/utils/processManager.js +0 -75
  315. package/dist/utils/proxyUtils.d.ts +0 -15
  316. package/dist/utils/proxyUtils.js +0 -50
  317. package/dist/utils/resourceMonitor.d.ts +0 -65
  318. package/dist/utils/resourceMonitor.js +0 -175
  319. package/dist/utils/retryUtils.d.ts +0 -49
  320. package/dist/utils/retryUtils.js +0 -303
  321. package/dist/utils/sensitiveCommandManager.d.ts +0 -53
  322. package/dist/utils/sensitiveCommandManager.js +0 -308
  323. package/dist/utils/sessionConverter.d.ts +0 -7
  324. package/dist/utils/sessionConverter.js +0 -306
  325. package/dist/utils/sessionManager.d.ts +0 -53
  326. package/dist/utils/sessionManager.js +0 -371
  327. package/dist/utils/subAgentConfig.d.ts +0 -50
  328. package/dist/utils/subAgentConfig.js +0 -221
  329. package/dist/utils/subAgentExecutor.d.ts +0 -40
  330. package/dist/utils/subAgentExecutor.js +0 -434
  331. package/dist/utils/terminal.d.ts +0 -5
  332. package/dist/utils/terminal.js +0 -13
  333. package/dist/utils/textBuffer.d.ts +0 -99
  334. package/dist/utils/textBuffer.js +0 -547
  335. package/dist/utils/textUtils.d.ts +0 -37
  336. package/dist/utils/textUtils.js +0 -102
  337. package/dist/utils/themeConfig.d.ts +0 -21
  338. package/dist/utils/themeConfig.js +0 -61
  339. package/dist/utils/todoPreprocessor.d.ts +0 -5
  340. package/dist/utils/todoPreprocessor.js +0 -18
  341. package/dist/utils/todoScanner.d.ts +0 -8
  342. package/dist/utils/todoScanner.js +0 -148
  343. package/dist/utils/toolDisplayConfig.d.ts +0 -16
  344. package/dist/utils/toolDisplayConfig.js +0 -47
  345. package/dist/utils/toolExecutor.d.ts +0 -37
  346. package/dist/utils/toolExecutor.js +0 -224
  347. package/dist/utils/usageLogger.d.ts +0 -11
  348. package/dist/utils/usageLogger.js +0 -114
  349. package/dist/utils/vscodeConnection.d.ts +0 -76
  350. package/dist/utils/vscodeConnection.js +0 -430
  351. package/dist/utils/workspaceSnapshot.d.ts +0 -63
  352. 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
- }