snow-ai 0.4.15 → 0.4.17

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