@google/gemini-cli 0.10.0-preview.3 → 0.11.0-nightly.20251021.e72c00cf

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 (472) hide show
  1. package/README.md +9 -0
  2. package/dist/google-gemini-cli-0.11.0-nightly.20251020.a96f0659.tgz +0 -0
  3. package/dist/index.js +5 -5
  4. package/dist/index.js.map +1 -1
  5. package/dist/package.json +2 -2
  6. package/dist/src/commands/extensions/disable.js +8 -5
  7. package/dist/src/commands/extensions/disable.js.map +1 -1
  8. package/dist/src/commands/extensions/enable.js +5 -3
  9. package/dist/src/commands/extensions/enable.js.map +1 -1
  10. package/dist/src/commands/extensions/examples/mcp-server/example.d.ts +6 -0
  11. package/dist/src/commands/extensions/examples/mcp-server/example.js +46 -0
  12. package/dist/src/commands/extensions/examples/mcp-server/example.js.map +1 -0
  13. package/dist/src/commands/extensions/install.d.ts +1 -0
  14. package/dist/src/commands/extensions/install.js +18 -4
  15. package/dist/src/commands/extensions/install.js.map +1 -1
  16. package/dist/src/commands/extensions/link.js +3 -2
  17. package/dist/src/commands/extensions/link.js.map +1 -1
  18. package/dist/src/commands/extensions/list.js +7 -5
  19. package/dist/src/commands/extensions/list.js.map +1 -1
  20. package/dist/src/commands/extensions/new.js +14 -20
  21. package/dist/src/commands/extensions/new.js.map +1 -1
  22. package/dist/src/commands/extensions/uninstall.js +3 -2
  23. package/dist/src/commands/extensions/uninstall.js.map +1 -1
  24. package/dist/src/commands/extensions/update.js +17 -17
  25. package/dist/src/commands/extensions/update.js.map +1 -1
  26. package/dist/src/commands/mcp/add.js +7 -4
  27. package/dist/src/commands/mcp/add.js.map +1 -1
  28. package/dist/src/commands/mcp/add.test.d.ts +6 -0
  29. package/dist/src/commands/mcp/add.test.js +244 -0
  30. package/dist/src/commands/mcp/add.test.js.map +1 -0
  31. package/dist/src/commands/mcp/list.js +6 -4
  32. package/dist/src/commands/mcp/list.js.map +1 -1
  33. package/dist/src/commands/mcp/list.test.d.ts +6 -0
  34. package/dist/src/commands/mcp/list.test.js +117 -0
  35. package/dist/src/commands/mcp/list.test.js.map +1 -0
  36. package/dist/src/commands/mcp/remove.test.d.ts +6 -0
  37. package/dist/src/commands/mcp/remove.test.js +175 -0
  38. package/dist/src/commands/mcp/remove.test.js.map +1 -0
  39. package/dist/src/commands/mcp.test.d.ts +6 -0
  40. package/dist/src/commands/mcp.test.js +62 -0
  41. package/dist/src/commands/mcp.test.js.map +1 -0
  42. package/dist/src/config/auth.js +3 -1
  43. package/dist/src/config/auth.js.map +1 -1
  44. package/dist/src/config/auth.test.js +3 -1
  45. package/dist/src/config/auth.test.js.map +1 -1
  46. package/dist/src/config/config.d.ts +2 -14
  47. package/dist/src/config/config.integration.test.d.ts +6 -0
  48. package/dist/src/config/config.integration.test.js +321 -0
  49. package/dist/src/config/config.integration.test.js.map +1 -0
  50. package/dist/src/config/config.js +38 -102
  51. package/dist/src/config/config.js.map +1 -1
  52. package/dist/src/config/config.test.d.ts +6 -0
  53. package/dist/src/config/config.test.js +1961 -0
  54. package/dist/src/config/config.test.js.map +1 -0
  55. package/dist/src/config/extension.d.ts +4 -15
  56. package/dist/src/config/extension.js +84 -97
  57. package/dist/src/config/extension.js.map +1 -1
  58. package/dist/src/config/extension.test.d.ts +6 -0
  59. package/dist/src/config/extension.test.js +1076 -0
  60. package/dist/src/config/extension.test.js.map +1 -0
  61. package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
  62. package/dist/src/config/extensions/extensionEnablement.js +4 -3
  63. package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
  64. package/dist/src/config/extensions/extensionEnablement.test.js +21 -18
  65. package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
  66. package/dist/src/config/extensions/github.d.ts +18 -9
  67. package/dist/src/config/extensions/github.js +106 -29
  68. package/dist/src/config/extensions/github.js.map +1 -1
  69. package/dist/src/config/extensions/github.test.js +20 -17
  70. package/dist/src/config/extensions/github.test.js.map +1 -1
  71. package/dist/src/config/extensions/update.d.ts +5 -4
  72. package/dist/src/config/extensions/update.js +10 -6
  73. package/dist/src/config/extensions/update.js.map +1 -1
  74. package/dist/src/config/extensions/update.test.js +57 -57
  75. package/dist/src/config/extensions/update.test.js.map +1 -1
  76. package/dist/src/config/extensions/variableSchema.d.ts +2 -0
  77. package/dist/src/config/extensions/variableSchema.js.map +1 -1
  78. package/dist/src/config/keyBindings.d.ts +2 -1
  79. package/dist/src/config/keyBindings.js +4 -2
  80. package/dist/src/config/keyBindings.js.map +1 -1
  81. package/dist/src/config/policy.d.ts +3 -2
  82. package/dist/src/config/policy.js +32 -23
  83. package/dist/src/config/policy.js.map +1 -1
  84. package/dist/src/config/policy.test.js +60 -36
  85. package/dist/src/config/policy.test.js.map +1 -1
  86. package/dist/src/config/sandboxConfig.d.ts +0 -1
  87. package/dist/src/config/sandboxConfig.js +1 -3
  88. package/dist/src/config/sandboxConfig.js.map +1 -1
  89. package/dist/src/config/settings.js +3 -1
  90. package/dist/src/config/settings.js.map +1 -1
  91. package/dist/src/config/settings.test.d.ts +6 -0
  92. package/dist/src/config/settings.test.js +1937 -0
  93. package/dist/src/config/settings.test.js.map +1 -0
  94. package/dist/src/config/settingsSchema.d.ts +1 -1
  95. package/dist/src/config/settingsSchema.js +1 -1
  96. package/dist/src/config/settingsSchema.js.map +1 -1
  97. package/dist/src/config/settingsSchema.test.js +1 -1
  98. package/dist/src/config/settingsSchema.test.js.map +1 -1
  99. package/dist/src/gemini.js +30 -24
  100. package/dist/src/gemini.js.map +1 -1
  101. package/dist/src/gemini.test.js +42 -11
  102. package/dist/src/gemini.test.js.map +1 -1
  103. package/dist/src/generated/git-commit.d.ts +2 -2
  104. package/dist/src/generated/git-commit.js +2 -2
  105. package/dist/src/generated/git-commit.js.map +1 -1
  106. package/dist/src/nonInteractiveCli.js +92 -4
  107. package/dist/src/nonInteractiveCli.js.map +1 -1
  108. package/dist/src/nonInteractiveCli.test.d.ts +6 -0
  109. package/dist/src/nonInteractiveCli.test.js +711 -0
  110. package/dist/src/nonInteractiveCli.test.js.map +1 -0
  111. package/dist/src/services/FileCommandLoader.test.d.ts +6 -0
  112. package/dist/src/services/FileCommandLoader.test.js +971 -0
  113. package/dist/src/services/FileCommandLoader.test.js.map +1 -0
  114. package/dist/src/services/prompt-processors/argumentProcessor.test.d.ts +6 -0
  115. package/dist/src/services/prompt-processors/argumentProcessor.test.js +40 -0
  116. package/dist/src/services/prompt-processors/argumentProcessor.test.js.map +1 -0
  117. package/dist/src/services/prompt-processors/atFileProcessor.js +3 -2
  118. package/dist/src/services/prompt-processors/atFileProcessor.js.map +1 -1
  119. package/dist/src/services/prompt-processors/shellProcessor.test.d.ts +6 -0
  120. package/dist/src/services/prompt-processors/shellProcessor.test.js +482 -0
  121. package/dist/src/services/prompt-processors/shellProcessor.test.js.map +1 -0
  122. package/dist/src/test-utils/render.d.ts +2 -1
  123. package/dist/src/test-utils/render.js +5 -2
  124. package/dist/src/test-utils/render.js.map +1 -1
  125. package/dist/src/ui/App.test.d.ts +6 -0
  126. package/dist/src/ui/App.test.js +110 -0
  127. package/dist/src/ui/App.test.js.map +1 -0
  128. package/dist/src/ui/AppContainer.js +43 -28
  129. package/dist/src/ui/AppContainer.js.map +1 -1
  130. package/dist/src/ui/AppContainer.test.js +35 -9
  131. package/dist/src/ui/AppContainer.test.js.map +1 -1
  132. package/dist/src/ui/auth/AuthDialog.d.ts +1 -1
  133. package/dist/src/ui/auth/AuthDialog.js +3 -1
  134. package/dist/src/ui/auth/AuthDialog.js.map +1 -1
  135. package/dist/src/ui/auth/useAuth.d.ts +1 -1
  136. package/dist/src/ui/auth/useAuth.js +3 -1
  137. package/dist/src/ui/auth/useAuth.js.map +1 -1
  138. package/dist/src/ui/commands/aboutCommand.js +1 -1
  139. package/dist/src/ui/commands/aboutCommand.test.d.ts +6 -0
  140. package/dist/src/ui/commands/aboutCommand.test.js +130 -0
  141. package/dist/src/ui/commands/aboutCommand.test.js.map +1 -0
  142. package/dist/src/ui/commands/authCommand.js +1 -1
  143. package/dist/src/ui/commands/authCommand.test.d.ts +6 -0
  144. package/dist/src/ui/commands/authCommand.test.js +30 -0
  145. package/dist/src/ui/commands/authCommand.test.js.map +1 -0
  146. package/dist/src/ui/commands/bugCommand.js +1 -1
  147. package/dist/src/ui/commands/bugCommand.test.d.ts +6 -0
  148. package/dist/src/ui/commands/bugCommand.test.js +105 -0
  149. package/dist/src/ui/commands/bugCommand.test.js.map +1 -0
  150. package/dist/src/ui/commands/chatCommand.js +1 -1
  151. package/dist/src/ui/commands/chatCommand.js.map +1 -1
  152. package/dist/src/ui/commands/chatCommand.test.d.ts +6 -0
  153. package/dist/src/ui/commands/chatCommand.test.js +555 -0
  154. package/dist/src/ui/commands/chatCommand.test.js.map +1 -0
  155. package/dist/src/ui/commands/clearCommand.js +1 -1
  156. package/dist/src/ui/commands/clearCommand.test.d.ts +6 -0
  157. package/dist/src/ui/commands/clearCommand.test.js +76 -0
  158. package/dist/src/ui/commands/clearCommand.test.js.map +1 -0
  159. package/dist/src/ui/commands/compressCommand.js +1 -1
  160. package/dist/src/ui/commands/compressCommand.js.map +1 -1
  161. package/dist/src/ui/commands/compressCommand.test.d.ts +6 -0
  162. package/dist/src/ui/commands/compressCommand.test.js +98 -0
  163. package/dist/src/ui/commands/compressCommand.test.js.map +1 -0
  164. package/dist/src/ui/commands/copyCommand.test.d.ts +6 -0
  165. package/dist/src/ui/commands/copyCommand.test.js +242 -0
  166. package/dist/src/ui/commands/copyCommand.test.js.map +1 -0
  167. package/dist/src/ui/commands/corgiCommand.js +1 -1
  168. package/dist/src/ui/commands/corgiCommand.js.map +1 -1
  169. package/dist/src/ui/commands/corgiCommand.test.d.ts +6 -0
  170. package/dist/src/ui/commands/corgiCommand.test.js +28 -0
  171. package/dist/src/ui/commands/corgiCommand.test.js.map +1 -0
  172. package/dist/src/ui/commands/directoryCommand.js +1 -1
  173. package/dist/src/ui/commands/directoryCommand.js.map +1 -1
  174. package/dist/src/ui/commands/directoryCommand.test.d.ts +6 -0
  175. package/dist/src/ui/commands/directoryCommand.test.js +144 -0
  176. package/dist/src/ui/commands/directoryCommand.test.js.map +1 -0
  177. package/dist/src/ui/commands/docsCommand.js +1 -1
  178. package/dist/src/ui/commands/docsCommand.test.d.ts +6 -0
  179. package/dist/src/ui/commands/docsCommand.test.js +72 -0
  180. package/dist/src/ui/commands/docsCommand.test.js.map +1 -0
  181. package/dist/src/ui/commands/editorCommand.js +1 -1
  182. package/dist/src/ui/commands/editorCommand.test.d.ts +6 -0
  183. package/dist/src/ui/commands/editorCommand.test.js +27 -0
  184. package/dist/src/ui/commands/editorCommand.test.js.map +1 -0
  185. package/dist/src/ui/commands/extensionsCommand.test.d.ts +6 -0
  186. package/dist/src/ui/commands/extensionsCommand.test.js +241 -0
  187. package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -0
  188. package/dist/src/ui/commands/helpCommand.js +1 -1
  189. package/dist/src/ui/commands/helpCommand.test.d.ts +6 -0
  190. package/dist/src/ui/commands/helpCommand.test.js +42 -0
  191. package/dist/src/ui/commands/helpCommand.test.js.map +1 -0
  192. package/dist/src/ui/commands/ideCommand.js +6 -6
  193. package/dist/src/ui/commands/ideCommand.test.d.ts +6 -0
  194. package/dist/src/ui/commands/ideCommand.test.js +203 -0
  195. package/dist/src/ui/commands/ideCommand.test.js.map +1 -0
  196. package/dist/src/ui/commands/initCommand.js +1 -1
  197. package/dist/src/ui/commands/initCommand.js.map +1 -1
  198. package/dist/src/ui/commands/initCommand.test.d.ts +6 -0
  199. package/dist/src/ui/commands/initCommand.test.js +80 -0
  200. package/dist/src/ui/commands/initCommand.test.js.map +1 -0
  201. package/dist/src/ui/commands/mcpCommand.js +98 -88
  202. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  203. package/dist/src/ui/commands/mcpCommand.test.d.ts +6 -0
  204. package/dist/src/ui/commands/mcpCommand.test.js +148 -0
  205. package/dist/src/ui/commands/mcpCommand.test.js.map +1 -0
  206. package/dist/src/ui/commands/memoryCommand.js +6 -6
  207. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  208. package/dist/src/ui/commands/memoryCommand.test.d.ts +6 -0
  209. package/dist/src/ui/commands/memoryCommand.test.js +266 -0
  210. package/dist/src/ui/commands/memoryCommand.test.js.map +1 -0
  211. package/dist/src/ui/commands/privacyCommand.js +1 -1
  212. package/dist/src/ui/commands/privacyCommand.test.d.ts +6 -0
  213. package/dist/src/ui/commands/privacyCommand.test.js +32 -0
  214. package/dist/src/ui/commands/privacyCommand.test.js.map +1 -0
  215. package/dist/src/ui/commands/quitCommand.js +1 -1
  216. package/dist/src/ui/commands/quitCommand.test.d.ts +6 -0
  217. package/dist/src/ui/commands/quitCommand.test.js +50 -0
  218. package/dist/src/ui/commands/quitCommand.test.js.map +1 -0
  219. package/dist/src/ui/commands/restoreCommand.test.d.ts +6 -0
  220. package/dist/src/ui/commands/restoreCommand.test.js +190 -0
  221. package/dist/src/ui/commands/restoreCommand.test.js.map +1 -0
  222. package/dist/src/ui/commands/settingsCommand.test.d.ts +6 -0
  223. package/dist/src/ui/commands/settingsCommand.test.js +30 -0
  224. package/dist/src/ui/commands/settingsCommand.test.js.map +1 -0
  225. package/dist/src/ui/commands/setupGithubCommand.test.js +1 -2
  226. package/dist/src/ui/commands/setupGithubCommand.test.js.map +1 -1
  227. package/dist/src/ui/commands/statsCommand.js +3 -3
  228. package/dist/src/ui/commands/statsCommand.js.map +1 -1
  229. package/dist/src/ui/commands/statsCommand.test.d.ts +6 -0
  230. package/dist/src/ui/commands/statsCommand.test.js +53 -0
  231. package/dist/src/ui/commands/statsCommand.test.js.map +1 -0
  232. package/dist/src/ui/commands/terminalSetupCommand.test.d.ts +6 -0
  233. package/dist/src/ui/commands/terminalSetupCommand.test.js +66 -0
  234. package/dist/src/ui/commands/terminalSetupCommand.test.js.map +1 -0
  235. package/dist/src/ui/commands/themeCommand.js +1 -1
  236. package/dist/src/ui/commands/themeCommand.test.d.ts +6 -0
  237. package/dist/src/ui/commands/themeCommand.test.js +32 -0
  238. package/dist/src/ui/commands/themeCommand.test.js.map +1 -0
  239. package/dist/src/ui/commands/toolsCommand.js +1 -1
  240. package/dist/src/ui/commands/toolsCommand.test.d.ts +6 -0
  241. package/dist/src/ui/commands/toolsCommand.test.js +100 -0
  242. package/dist/src/ui/commands/toolsCommand.test.js.map +1 -0
  243. package/dist/src/ui/commands/vimCommand.js +1 -1
  244. package/dist/src/ui/components/Composer.js +5 -3
  245. package/dist/src/ui/components/Composer.js.map +1 -1
  246. package/dist/src/ui/components/Composer.test.js +16 -1
  247. package/dist/src/ui/components/Composer.test.js.map +1 -1
  248. package/dist/src/ui/components/ContextSummaryDisplay.d.ts +0 -1
  249. package/dist/src/ui/components/ContextSummaryDisplay.js +2 -12
  250. package/dist/src/ui/components/ContextSummaryDisplay.js.map +1 -1
  251. package/dist/src/ui/components/ContextSummaryDisplay.test.d.ts +6 -0
  252. package/dist/src/ui/components/ContextSummaryDisplay.test.js +66 -0
  253. package/dist/src/ui/components/ContextSummaryDisplay.test.js.map +1 -0
  254. package/dist/src/ui/components/DialogManager.js +1 -5
  255. package/dist/src/ui/components/DialogManager.js.map +1 -1
  256. package/dist/src/ui/components/EditorSettingsDialog.js +1 -1
  257. package/dist/src/ui/components/EditorSettingsDialog.js.map +1 -1
  258. package/dist/src/ui/components/FolderTrustDialog.test.js +7 -3
  259. package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
  260. package/dist/src/ui/components/Footer.js +1 -1
  261. package/dist/src/ui/components/Footer.js.map +1 -1
  262. package/dist/src/ui/components/Footer.test.d.ts +6 -0
  263. package/dist/src/ui/components/Footer.test.js +231 -0
  264. package/dist/src/ui/components/Footer.test.js.map +1 -0
  265. package/dist/src/ui/components/InputPrompt.d.ts +4 -0
  266. package/dist/src/ui/components/InputPrompt.js +53 -4
  267. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  268. package/dist/src/ui/components/InputPrompt.test.d.ts +6 -0
  269. package/dist/src/ui/components/InputPrompt.test.js +1716 -0
  270. package/dist/src/ui/components/InputPrompt.test.js.map +1 -0
  271. package/dist/src/ui/components/ModelStatsDisplay.test.d.ts +6 -0
  272. package/dist/src/ui/components/ModelStatsDisplay.test.js +285 -0
  273. package/dist/src/ui/components/ModelStatsDisplay.test.js.map +1 -0
  274. package/dist/src/ui/components/PermissionsModifyTrustDialog.js +22 -18
  275. package/dist/src/ui/components/PermissionsModifyTrustDialog.js.map +1 -1
  276. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +10 -2
  277. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
  278. package/dist/src/ui/components/QueuedMessageDisplay.js +3 -3
  279. package/dist/src/ui/components/QueuedMessageDisplay.js.map +1 -1
  280. package/dist/src/ui/components/QueuedMessageDisplay.test.js +4 -0
  281. package/dist/src/ui/components/QueuedMessageDisplay.test.js.map +1 -1
  282. package/dist/src/ui/components/RawMarkdownIndicator.d.ts +7 -0
  283. package/dist/src/ui/components/RawMarkdownIndicator.js +8 -0
  284. package/dist/src/ui/components/RawMarkdownIndicator.js.map +1 -0
  285. package/dist/src/ui/components/SessionSummaryDisplay.test.d.ts +6 -0
  286. package/dist/src/ui/components/SessionSummaryDisplay.test.js +74 -0
  287. package/dist/src/ui/components/SessionSummaryDisplay.test.js.map +1 -0
  288. package/dist/src/ui/components/SettingsDialog.js +8 -8
  289. package/dist/src/ui/components/SettingsDialog.js.map +1 -1
  290. package/dist/src/ui/components/SettingsDialog.test.js +189 -76
  291. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  292. package/dist/src/ui/components/StatsDisplay.test.d.ts +6 -0
  293. package/dist/src/ui/components/StatsDisplay.test.js +351 -0
  294. package/dist/src/ui/components/StatsDisplay.test.js.map +1 -0
  295. package/dist/src/ui/components/ThemeDialog.d.ts +4 -2
  296. package/dist/src/ui/components/ThemeDialog.js +3 -3
  297. package/dist/src/ui/components/ThemeDialog.js.map +1 -1
  298. package/dist/src/ui/components/ThemeDialog.test.js +13 -0
  299. package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
  300. package/dist/src/ui/components/ToolStatsDisplay.test.d.ts +6 -0
  301. package/dist/src/ui/components/ToolStatsDisplay.test.js +227 -0
  302. package/dist/src/ui/components/ToolStatsDisplay.test.js.map +1 -0
  303. package/dist/src/ui/components/messages/GeminiMessage.js +3 -1
  304. package/dist/src/ui/components/messages/GeminiMessage.js.map +1 -1
  305. package/dist/src/ui/components/messages/GeminiMessage.test.d.ts +6 -0
  306. package/dist/src/ui/components/messages/GeminiMessage.test.js +35 -0
  307. package/dist/src/ui/components/messages/GeminiMessage.test.js.map +1 -0
  308. package/dist/src/ui/components/messages/GeminiMessageContent.js +3 -1
  309. package/dist/src/ui/components/messages/GeminiMessageContent.js.map +1 -1
  310. package/dist/src/ui/components/messages/Todo.d.ts +7 -0
  311. package/dist/src/ui/components/messages/Todo.js +59 -0
  312. package/dist/src/ui/components/messages/Todo.js.map +1 -0
  313. package/dist/src/ui/components/messages/Todo.test.d.ts +6 -0
  314. package/dist/src/ui/components/messages/Todo.test.js +113 -0
  315. package/dist/src/ui/components/messages/Todo.test.js.map +1 -0
  316. package/dist/src/ui/components/messages/ToolGroupMessage.js +1 -1
  317. package/dist/src/ui/components/messages/ToolGroupMessage.js.map +1 -1
  318. package/dist/src/ui/components/messages/ToolMessage.js +8 -3
  319. package/dist/src/ui/components/messages/ToolMessage.js.map +1 -1
  320. package/dist/src/ui/components/messages/ToolMessage.test.js +2 -2
  321. package/dist/src/ui/components/messages/ToolMessage.test.js.map +1 -1
  322. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.d.ts +6 -0
  323. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js +30 -0
  324. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js.map +1 -0
  325. package/dist/src/ui/components/messages/UserShellMessage.js +1 -1
  326. package/dist/src/ui/components/messages/UserShellMessage.js.map +1 -1
  327. package/dist/src/ui/components/shared/BaseSelectionList.test.js +1 -1
  328. package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
  329. package/dist/src/ui/components/shared/text-buffer.test.d.ts +6 -0
  330. package/dist/src/ui/components/shared/text-buffer.test.js +1554 -0
  331. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -0
  332. package/dist/src/ui/components/shared/vim-buffer-actions.test.d.ts +6 -0
  333. package/dist/src/ui/components/shared/vim-buffer-actions.test.js +951 -0
  334. package/dist/src/ui/components/shared/vim-buffer-actions.test.js.map +1 -0
  335. package/dist/src/ui/components/views/ExtensionsList.js +3 -4
  336. package/dist/src/ui/components/views/ExtensionsList.js.map +1 -1
  337. package/dist/src/ui/components/views/ExtensionsList.test.js +2 -9
  338. package/dist/src/ui/components/views/ExtensionsList.test.js.map +1 -1
  339. package/dist/src/ui/components/views/McpStatus.d.ts +0 -1
  340. package/dist/src/ui/components/views/McpStatus.js +4 -4
  341. package/dist/src/ui/components/views/McpStatus.js.map +1 -1
  342. package/dist/src/ui/components/views/McpStatus.test.js +0 -5
  343. package/dist/src/ui/components/views/McpStatus.test.js.map +1 -1
  344. package/dist/src/ui/components/views/ToolsList.test.js +4 -4
  345. package/dist/src/ui/components/views/ToolsList.test.js.map +1 -1
  346. package/dist/src/ui/contexts/KeypressContext.d.ts +1 -0
  347. package/dist/src/ui/contexts/KeypressContext.js +176 -50
  348. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  349. package/dist/src/ui/contexts/KeypressContext.test.js +413 -14
  350. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  351. package/dist/src/ui/contexts/SessionContext.test.d.ts +6 -0
  352. package/dist/src/ui/contexts/SessionContext.test.js +177 -0
  353. package/dist/src/ui/contexts/SessionContext.test.js.map +1 -0
  354. package/dist/src/ui/contexts/UIActionsContext.d.ts +5 -4
  355. package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
  356. package/dist/src/ui/contexts/UIStateContext.d.ts +3 -3
  357. package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
  358. package/dist/src/ui/hooks/slashCommandProcessor.test.d.ts +6 -0
  359. package/dist/src/ui/hooks/slashCommandProcessor.test.js +779 -0
  360. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -0
  361. package/dist/src/ui/hooks/useAtCompletion.js +2 -2
  362. package/dist/src/ui/hooks/useAtCompletion.js.map +1 -1
  363. package/dist/src/ui/hooks/useAtCompletion.test.d.ts +6 -0
  364. package/dist/src/ui/hooks/useAtCompletion.test.js +385 -0
  365. package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -0
  366. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +0 -1
  367. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
  368. package/dist/src/ui/hooks/useCommandCompletion.d.ts +1 -1
  369. package/dist/src/ui/hooks/useCommandCompletion.js +5 -3
  370. package/dist/src/ui/hooks/useCommandCompletion.js.map +1 -1
  371. package/dist/src/ui/hooks/useCommandCompletion.test.d.ts +6 -0
  372. package/dist/src/ui/hooks/useCommandCompletion.test.js +375 -0
  373. package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -0
  374. package/dist/src/ui/hooks/useConsoleMessages.test.d.ts +6 -0
  375. package/dist/src/ui/hooks/useConsoleMessages.test.js +110 -0
  376. package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -0
  377. package/dist/src/ui/hooks/useExtensionUpdates.d.ts +2 -1
  378. package/dist/src/ui/hooks/useExtensionUpdates.js +5 -3
  379. package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
  380. package/dist/src/ui/hooks/useExtensionUpdates.test.js +22 -13
  381. package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
  382. package/dist/src/ui/hooks/useFocus.test.d.ts +6 -0
  383. package/dist/src/ui/hooks/useFocus.test.js +115 -0
  384. package/dist/src/ui/hooks/useFocus.test.js.map +1 -0
  385. package/dist/src/ui/hooks/useFolderTrust.test.d.ts +6 -0
  386. package/dist/src/ui/hooks/useFolderTrust.test.js +164 -0
  387. package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -0
  388. package/dist/src/ui/hooks/useGeminiStream.js +33 -31
  389. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  390. package/dist/src/ui/hooks/useGeminiStream.test.d.ts +6 -0
  391. package/dist/src/ui/hooks/useGeminiStream.test.js +1936 -0
  392. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -0
  393. package/dist/src/ui/hooks/useKeypress.test.d.ts +6 -0
  394. package/dist/src/ui/hooks/useKeypress.test.js +234 -0
  395. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -0
  396. package/dist/src/ui/hooks/useLoadingIndicator.test.js +5 -0
  397. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  398. package/dist/src/ui/hooks/useMessageQueue.d.ts +1 -0
  399. package/dist/src/ui/hooks/useMessageQueue.js +14 -0
  400. package/dist/src/ui/hooks/useMessageQueue.js.map +1 -1
  401. package/dist/src/ui/hooks/useMessageQueue.test.js +121 -0
  402. package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
  403. package/dist/src/ui/hooks/usePhraseCycler.d.ts +1 -0
  404. package/dist/src/ui/hooks/usePhraseCycler.js +156 -5
  405. package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
  406. package/dist/src/ui/hooks/usePhraseCycler.test.d.ts +6 -0
  407. package/dist/src/ui/hooks/usePhraseCycler.test.js +155 -0
  408. package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -0
  409. package/dist/src/ui/hooks/useThemeCommand.d.ts +2 -1
  410. package/dist/src/ui/hooks/useThemeCommand.js +6 -0
  411. package/dist/src/ui/hooks/useThemeCommand.js.map +1 -1
  412. package/dist/src/ui/hooks/useToolScheduler.test.js +3 -0
  413. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  414. package/dist/src/ui/hooks/vim.test.d.ts +6 -0
  415. package/dist/src/ui/hooks/vim.test.js +1389 -0
  416. package/dist/src/ui/hooks/vim.test.js.map +1 -0
  417. package/dist/src/ui/keyMatchers.test.js +9 -3
  418. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  419. package/dist/src/ui/themes/theme.test.d.ts +6 -0
  420. package/dist/src/ui/themes/theme.test.js +85 -0
  421. package/dist/src/ui/themes/theme.test.js.map +1 -0
  422. package/dist/src/ui/types.d.ts +0 -1
  423. package/dist/src/ui/types.js.map +1 -1
  424. package/dist/src/ui/utils/CodeColorizer.d.ts +1 -1
  425. package/dist/src/ui/utils/CodeColorizer.js +4 -2
  426. package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
  427. package/dist/src/ui/utils/MarkdownDisplay.d.ts +1 -0
  428. package/dist/src/ui/utils/MarkdownDisplay.js +8 -1
  429. package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
  430. package/dist/src/ui/utils/commandUtils.js +18 -2
  431. package/dist/src/ui/utils/commandUtils.js.map +1 -1
  432. package/dist/src/ui/utils/commandUtils.test.js +61 -6
  433. package/dist/src/ui/utils/commandUtils.test.js.map +1 -1
  434. package/dist/src/ui/utils/computeStats.js +5 -2
  435. package/dist/src/ui/utils/computeStats.js.map +1 -1
  436. package/dist/src/ui/utils/computeStats.test.d.ts +6 -0
  437. package/dist/src/ui/utils/computeStats.test.js +262 -0
  438. package/dist/src/ui/utils/computeStats.test.js.map +1 -0
  439. package/dist/src/ui/utils/updateCheck.d.ts +2 -1
  440. package/dist/src/ui/utils/updateCheck.js +4 -1
  441. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  442. package/dist/src/ui/utils/updateCheck.test.js +25 -10
  443. package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
  444. package/dist/src/utils/cleanup.test.d.ts +6 -0
  445. package/dist/src/utils/cleanup.test.js +49 -0
  446. package/dist/src/utils/cleanup.test.js.map +1 -0
  447. package/dist/src/utils/errors.d.ts +1 -0
  448. package/dist/src/utils/errors.js +66 -5
  449. package/dist/src/utils/errors.js.map +1 -1
  450. package/dist/src/utils/handleAutoUpdate.test.d.ts +6 -0
  451. package/dist/src/utils/handleAutoUpdate.test.js +225 -0
  452. package/dist/src/utils/handleAutoUpdate.test.js.map +1 -0
  453. package/dist/src/utils/sandbox-macos-permissive-open.sb +3 -1
  454. package/dist/src/utils/startupWarnings.test.d.ts +6 -0
  455. package/dist/src/utils/startupWarnings.test.js +61 -0
  456. package/dist/src/utils/startupWarnings.test.js.map +1 -0
  457. package/dist/src/validateNonInterActiveAuth.js +2 -2
  458. package/dist/src/validateNonInterActiveAuth.js.map +1 -1
  459. package/dist/src/validateNonInterActiveAuth.test.d.ts +6 -0
  460. package/dist/src/validateNonInterActiveAuth.test.js +336 -0
  461. package/dist/src/validateNonInterActiveAuth.test.js.map +1 -0
  462. package/dist/src/zed-integration/zedIntegration.js +1 -3
  463. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  464. package/dist/tsconfig.tsbuildinfo +1 -1
  465. package/package.json +3 -3
  466. package/dist/google-gemini-cli-0.10.0-preview.2.tgz +0 -0
  467. package/dist/src/ui/components/WorkspaceMigrationDialog.d.ts +0 -11
  468. package/dist/src/ui/components/WorkspaceMigrationDialog.js +0 -44
  469. package/dist/src/ui/components/WorkspaceMigrationDialog.js.map +0 -1
  470. package/dist/src/ui/hooks/useWorkspaceMigration.d.ts +0 -13
  471. package/dist/src/ui/hooks/useWorkspaceMigration.js +0 -59
  472. package/dist/src/ui/hooks/useWorkspaceMigration.js.map +0 -1
@@ -0,0 +1,1936 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { renderHook, act, waitFor } from '@testing-library/react';
8
+ import { useGeminiStream } from './useGeminiStream.js';
9
+ import { useKeypress } from './useKeypress.js';
10
+ import * as atCommandProcessor from './atCommandProcessor.js';
11
+ import { useReactToolScheduler } from './useReactToolScheduler.js';
12
+ import { ApprovalMode, AuthType, GeminiEventType as ServerGeminiEventType, ToolErrorType, ToolConfirmationOutcome, tokenLimit, } from '@google/gemini-cli-core';
13
+ import { MessageType, StreamingState } from '../types.js';
14
+ // --- MOCKS ---
15
+ const mockSendMessageStream = vi
16
+ .fn()
17
+ .mockReturnValue((async function* () { })());
18
+ const mockStartChat = vi.fn();
19
+ const MockedGeminiClientClass = vi.hoisted(() => vi.fn().mockImplementation(function (_config) {
20
+ // _config
21
+ this.startChat = mockStartChat;
22
+ this.sendMessageStream = mockSendMessageStream;
23
+ this.addHistory = vi.fn();
24
+ this.getChat = vi.fn().mockReturnValue({
25
+ recordCompletedToolCalls: vi.fn(),
26
+ });
27
+ this.getChatRecordingService = vi.fn().mockReturnValue({
28
+ recordThought: vi.fn(),
29
+ initialize: vi.fn(),
30
+ recordMessage: vi.fn(),
31
+ recordMessageTokens: vi.fn(),
32
+ recordToolCalls: vi.fn(),
33
+ getConversationFile: vi.fn(),
34
+ });
35
+ }));
36
+ const MockedUserPromptEvent = vi.hoisted(() => vi.fn().mockImplementation(() => { }));
37
+ const mockParseAndFormatApiError = vi.hoisted(() => vi.fn());
38
+ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
39
+ const actualCoreModule = (await importOriginal());
40
+ return {
41
+ ...actualCoreModule,
42
+ GitService: vi.fn(),
43
+ GeminiClient: MockedGeminiClientClass,
44
+ UserPromptEvent: MockedUserPromptEvent,
45
+ parseAndFormatApiError: mockParseAndFormatApiError,
46
+ tokenLimit: vi.fn().mockReturnValue(100), // Mock tokenLimit
47
+ };
48
+ });
49
+ const mockUseReactToolScheduler = useReactToolScheduler;
50
+ vi.mock('./useReactToolScheduler.js', async (importOriginal) => {
51
+ const actualSchedulerModule = (await importOriginal());
52
+ return {
53
+ ...(actualSchedulerModule || {}),
54
+ useReactToolScheduler: vi.fn(),
55
+ };
56
+ });
57
+ vi.mock('./useKeypress.js', () => ({
58
+ useKeypress: vi.fn(),
59
+ }));
60
+ vi.mock('./shellCommandProcessor.js', () => ({
61
+ useShellCommandProcessor: vi.fn().mockReturnValue({
62
+ handleShellCommand: vi.fn(),
63
+ }),
64
+ }));
65
+ vi.mock('./atCommandProcessor.js');
66
+ vi.mock('../utils/markdownUtilities.js', () => ({
67
+ findLastSafeSplitPoint: vi.fn((s) => s.length),
68
+ }));
69
+ vi.mock('./useStateAndRef.js', () => ({
70
+ useStateAndRef: vi.fn((initial) => {
71
+ let val = initial;
72
+ const ref = { current: val };
73
+ const setVal = vi.fn((updater) => {
74
+ if (typeof updater === 'function') {
75
+ val = updater(val);
76
+ }
77
+ else {
78
+ val = updater;
79
+ }
80
+ ref.current = val;
81
+ });
82
+ return [val, ref, setVal];
83
+ }),
84
+ }));
85
+ vi.mock('./useLogger.js', () => ({
86
+ useLogger: vi.fn().mockReturnValue({
87
+ logMessage: vi.fn().mockResolvedValue(undefined),
88
+ }),
89
+ }));
90
+ const mockStartNewPrompt = vi.fn();
91
+ const mockAddUsage = vi.fn();
92
+ vi.mock('../contexts/SessionContext.js', () => ({
93
+ useSessionStats: vi.fn(() => ({
94
+ startNewPrompt: mockStartNewPrompt,
95
+ addUsage: mockAddUsage,
96
+ getPromptCount: vi.fn(() => 5),
97
+ })),
98
+ }));
99
+ vi.mock('./slashCommandProcessor.js', () => ({
100
+ handleSlashCommand: vi.fn().mockReturnValue(false),
101
+ }));
102
+ // --- END MOCKS ---
103
+ // --- Tests for useGeminiStream Hook ---
104
+ describe('useGeminiStream', () => {
105
+ let mockAddItem;
106
+ let mockConfig;
107
+ let mockOnDebugMessage;
108
+ let mockHandleSlashCommand;
109
+ let mockScheduleToolCalls;
110
+ let mockCancelAllToolCalls;
111
+ let mockMarkToolsAsSubmitted;
112
+ let handleAtCommandSpy;
113
+ beforeEach(() => {
114
+ vi.clearAllMocks(); // Clear mocks before each test
115
+ mockAddItem = vi.fn();
116
+ // Define the mock for getGeminiClient
117
+ const mockGetGeminiClient = vi.fn().mockImplementation(() => {
118
+ // MockedGeminiClientClass is defined in the module scope by the previous change.
119
+ // It will use the mockStartChat and mockSendMessageStream that are managed within beforeEach.
120
+ const clientInstance = new MockedGeminiClientClass(mockConfig);
121
+ return clientInstance;
122
+ });
123
+ const contentGeneratorConfig = {
124
+ model: 'test-model',
125
+ apiKey: 'test-key',
126
+ vertexai: false,
127
+ authType: AuthType.USE_GEMINI,
128
+ };
129
+ mockConfig = {
130
+ apiKey: 'test-api-key',
131
+ model: 'gemini-pro',
132
+ sandbox: false,
133
+ targetDir: '/test/dir',
134
+ debugMode: false,
135
+ question: undefined,
136
+ coreTools: [],
137
+ toolDiscoveryCommand: undefined,
138
+ toolCallCommand: undefined,
139
+ mcpServerCommand: undefined,
140
+ mcpServers: undefined,
141
+ userAgent: 'test-agent',
142
+ userMemory: '',
143
+ geminiMdFileCount: 0,
144
+ alwaysSkipModificationConfirmation: false,
145
+ vertexai: false,
146
+ showMemoryUsage: false,
147
+ contextFileName: undefined,
148
+ getToolRegistry: vi.fn(() => ({ getToolSchemaList: vi.fn(() => []) })),
149
+ getProjectRoot: vi.fn(() => '/test/dir'),
150
+ getCheckpointingEnabled: vi.fn(() => false),
151
+ getGeminiClient: mockGetGeminiClient,
152
+ getApprovalMode: () => ApprovalMode.DEFAULT,
153
+ getUsageStatisticsEnabled: () => true,
154
+ getDebugMode: () => false,
155
+ addHistory: vi.fn(),
156
+ getSessionId() {
157
+ return 'test-session-id';
158
+ },
159
+ setQuotaErrorOccurred: vi.fn(),
160
+ getQuotaErrorOccurred: vi.fn(() => false),
161
+ getModel: vi.fn(() => 'gemini-2.5-pro'),
162
+ getContentGeneratorConfig: vi
163
+ .fn()
164
+ .mockReturnValue(contentGeneratorConfig),
165
+ getUseSmartEdit: () => false,
166
+ getUseModelRouter: () => false,
167
+ };
168
+ mockOnDebugMessage = vi.fn();
169
+ mockHandleSlashCommand = vi.fn().mockResolvedValue(false);
170
+ // Mock return value for useReactToolScheduler
171
+ mockScheduleToolCalls = vi.fn();
172
+ mockCancelAllToolCalls = vi.fn();
173
+ mockMarkToolsAsSubmitted = vi.fn();
174
+ // Default mock for useReactToolScheduler to prevent toolCalls being undefined initially
175
+ mockUseReactToolScheduler.mockReturnValue([
176
+ [], // Default to empty array for toolCalls
177
+ mockScheduleToolCalls,
178
+ mockCancelAllToolCalls,
179
+ mockMarkToolsAsSubmitted,
180
+ ]);
181
+ // Reset mocks for GeminiClient instance methods (startChat and sendMessageStream)
182
+ // The GeminiClient constructor itself is mocked at the module level.
183
+ mockStartChat.mockClear().mockResolvedValue({
184
+ sendMessageStream: mockSendMessageStream,
185
+ }); // GeminiChat -> any
186
+ mockSendMessageStream
187
+ .mockClear()
188
+ .mockReturnValue((async function* () { })());
189
+ handleAtCommandSpy = vi.spyOn(atCommandProcessor, 'handleAtCommand');
190
+ });
191
+ const mockLoadedSettings = {
192
+ merged: { preferredEditor: 'vscode' },
193
+ user: { path: '/user/settings.json', settings: {} },
194
+ workspace: { path: '/workspace/.gemini/settings.json', settings: {} },
195
+ errors: [],
196
+ forScope: vi.fn(),
197
+ setValue: vi.fn(),
198
+ };
199
+ const renderTestHook = (initialToolCalls = [], geminiClient) => {
200
+ let currentToolCalls = initialToolCalls;
201
+ const setToolCalls = (newToolCalls) => {
202
+ currentToolCalls = newToolCalls;
203
+ };
204
+ mockUseReactToolScheduler.mockImplementation(() => [
205
+ currentToolCalls,
206
+ mockScheduleToolCalls,
207
+ mockCancelAllToolCalls,
208
+ mockMarkToolsAsSubmitted,
209
+ ]);
210
+ const client = geminiClient || mockConfig.getGeminiClient();
211
+ const { result, rerender } = renderHook((props) => {
212
+ // Update the mock's return value if new toolCalls are passed in props
213
+ if (props.toolCalls) {
214
+ setToolCalls(props.toolCalls);
215
+ }
216
+ return useGeminiStream(props.client, props.history, props.addItem, props.config, props.loadedSettings, props.onDebugMessage, props.handleSlashCommand, props.shellModeActive, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24);
217
+ }, {
218
+ initialProps: {
219
+ client,
220
+ history: [],
221
+ addItem: mockAddItem,
222
+ config: mockConfig,
223
+ onDebugMessage: mockOnDebugMessage,
224
+ handleSlashCommand: mockHandleSlashCommand,
225
+ shellModeActive: false,
226
+ loadedSettings: mockLoadedSettings,
227
+ toolCalls: initialToolCalls,
228
+ },
229
+ });
230
+ return {
231
+ result,
232
+ rerender,
233
+ mockMarkToolsAsSubmitted,
234
+ mockSendMessageStream,
235
+ client,
236
+ };
237
+ };
238
+ it('should not submit tool responses if not all tool calls are completed', () => {
239
+ const toolCalls = [
240
+ {
241
+ request: {
242
+ callId: 'call1',
243
+ name: 'tool1',
244
+ args: {},
245
+ isClientInitiated: false,
246
+ prompt_id: 'prompt-id-1',
247
+ },
248
+ status: 'success',
249
+ responseSubmittedToGemini: false,
250
+ response: {
251
+ callId: 'call1',
252
+ responseParts: [{ text: 'tool 1 response' }],
253
+ error: undefined,
254
+ errorType: undefined, // FIX: Added missing property
255
+ resultDisplay: 'Tool 1 success display',
256
+ },
257
+ tool: {
258
+ name: 'tool1',
259
+ displayName: 'tool1',
260
+ description: 'desc1',
261
+ build: vi.fn(),
262
+ },
263
+ invocation: {
264
+ getDescription: () => `Mock description`,
265
+ },
266
+ startTime: Date.now(),
267
+ endTime: Date.now(),
268
+ },
269
+ {
270
+ request: {
271
+ callId: 'call2',
272
+ name: 'tool2',
273
+ args: {},
274
+ prompt_id: 'prompt-id-1',
275
+ },
276
+ status: 'executing',
277
+ responseSubmittedToGemini: false,
278
+ tool: {
279
+ name: 'tool2',
280
+ displayName: 'tool2',
281
+ description: 'desc2',
282
+ build: vi.fn(),
283
+ },
284
+ invocation: {
285
+ getDescription: () => `Mock description`,
286
+ },
287
+ startTime: Date.now(),
288
+ liveOutput: '...',
289
+ },
290
+ ];
291
+ const { mockMarkToolsAsSubmitted, mockSendMessageStream } = renderTestHook(toolCalls);
292
+ // Effect for submitting tool responses depends on toolCalls and isResponding
293
+ // isResponding is initially false, so the effect should run.
294
+ expect(mockMarkToolsAsSubmitted).not.toHaveBeenCalled();
295
+ expect(mockSendMessageStream).not.toHaveBeenCalled(); // submitQuery uses this
296
+ });
297
+ it('should submit tool responses when all tool calls are completed and ready', async () => {
298
+ const toolCall1ResponseParts = [{ text: 'tool 1 final response' }];
299
+ const toolCall2ResponseParts = [{ text: 'tool 2 final response' }];
300
+ const completedToolCalls = [
301
+ {
302
+ request: {
303
+ callId: 'call1',
304
+ name: 'tool1',
305
+ args: {},
306
+ isClientInitiated: false,
307
+ prompt_id: 'prompt-id-2',
308
+ },
309
+ status: 'success',
310
+ responseSubmittedToGemini: false,
311
+ response: {
312
+ callId: 'call1',
313
+ responseParts: toolCall1ResponseParts,
314
+ errorType: undefined, // FIX: Added missing property
315
+ },
316
+ tool: {
317
+ displayName: 'MockTool',
318
+ },
319
+ invocation: {
320
+ getDescription: () => `Mock description`,
321
+ },
322
+ },
323
+ {
324
+ request: {
325
+ callId: 'call2',
326
+ name: 'tool2',
327
+ args: {},
328
+ isClientInitiated: false,
329
+ prompt_id: 'prompt-id-2',
330
+ },
331
+ status: 'error',
332
+ responseSubmittedToGemini: false,
333
+ response: {
334
+ callId: 'call2',
335
+ responseParts: toolCall2ResponseParts,
336
+ errorType: ToolErrorType.UNHANDLED_EXCEPTION, // FIX: Added missing property
337
+ },
338
+ },
339
+ ];
340
+ // Capture the onComplete callback
341
+ let capturedOnComplete = null;
342
+ mockUseReactToolScheduler.mockImplementation((onComplete) => {
343
+ capturedOnComplete = onComplete;
344
+ return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
345
+ });
346
+ renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
347
+ // Trigger the onComplete callback with completed tools
348
+ await act(async () => {
349
+ if (capturedOnComplete) {
350
+ await capturedOnComplete(completedToolCalls);
351
+ }
352
+ });
353
+ await waitFor(() => {
354
+ expect(mockMarkToolsAsSubmitted).toHaveBeenCalledTimes(1);
355
+ expect(mockSendMessageStream).toHaveBeenCalledTimes(1);
356
+ });
357
+ const expectedMergedResponse = [
358
+ ...toolCall1ResponseParts,
359
+ ...toolCall2ResponseParts,
360
+ ];
361
+ expect(mockSendMessageStream).toHaveBeenCalledWith(expectedMergedResponse, expect.any(AbortSignal), 'prompt-id-2');
362
+ });
363
+ it('should handle all tool calls being cancelled', async () => {
364
+ const cancelledToolCalls = [
365
+ {
366
+ request: {
367
+ callId: '1',
368
+ name: 'testTool',
369
+ args: {},
370
+ isClientInitiated: false,
371
+ prompt_id: 'prompt-id-3',
372
+ },
373
+ status: 'cancelled',
374
+ response: {
375
+ callId: '1',
376
+ responseParts: [{ text: 'cancelled' }],
377
+ errorType: undefined, // FIX: Added missing property
378
+ },
379
+ responseSubmittedToGemini: false,
380
+ tool: {
381
+ displayName: 'mock tool',
382
+ },
383
+ invocation: {
384
+ getDescription: () => `Mock description`,
385
+ },
386
+ },
387
+ ];
388
+ const client = new MockedGeminiClientClass(mockConfig);
389
+ // Capture the onComplete callback
390
+ let capturedOnComplete = null;
391
+ mockUseReactToolScheduler.mockImplementation((onComplete) => {
392
+ capturedOnComplete = onComplete;
393
+ return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
394
+ });
395
+ renderHook(() => useGeminiStream(client, [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
396
+ // Trigger the onComplete callback with cancelled tools
397
+ await act(async () => {
398
+ if (capturedOnComplete) {
399
+ await capturedOnComplete(cancelledToolCalls);
400
+ }
401
+ });
402
+ await waitFor(() => {
403
+ expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith(['1']);
404
+ expect(client.addHistory).toHaveBeenCalledWith({
405
+ role: 'user',
406
+ parts: [{ text: 'cancelled' }],
407
+ });
408
+ // Ensure we do NOT call back to the API
409
+ expect(mockSendMessageStream).not.toHaveBeenCalled();
410
+ });
411
+ });
412
+ it('should group multiple cancelled tool call responses into a single history entry', async () => {
413
+ const cancelledToolCall1 = {
414
+ request: {
415
+ callId: 'cancel-1',
416
+ name: 'toolA',
417
+ args: {},
418
+ isClientInitiated: false,
419
+ prompt_id: 'prompt-id-7',
420
+ },
421
+ tool: {
422
+ name: 'toolA',
423
+ displayName: 'toolA',
424
+ description: 'descA',
425
+ build: vi.fn(),
426
+ },
427
+ invocation: {
428
+ getDescription: () => `Mock description`,
429
+ },
430
+ status: 'cancelled',
431
+ response: {
432
+ callId: 'cancel-1',
433
+ responseParts: [
434
+ { functionResponse: { name: 'toolA', id: 'cancel-1' } },
435
+ ],
436
+ resultDisplay: undefined,
437
+ error: undefined,
438
+ errorType: undefined, // FIX: Added missing property
439
+ },
440
+ responseSubmittedToGemini: false,
441
+ };
442
+ const cancelledToolCall2 = {
443
+ request: {
444
+ callId: 'cancel-2',
445
+ name: 'toolB',
446
+ args: {},
447
+ isClientInitiated: false,
448
+ prompt_id: 'prompt-id-8',
449
+ },
450
+ tool: {
451
+ name: 'toolB',
452
+ displayName: 'toolB',
453
+ description: 'descB',
454
+ build: vi.fn(),
455
+ },
456
+ invocation: {
457
+ getDescription: () => `Mock description`,
458
+ },
459
+ status: 'cancelled',
460
+ response: {
461
+ callId: 'cancel-2',
462
+ responseParts: [
463
+ { functionResponse: { name: 'toolB', id: 'cancel-2' } },
464
+ ],
465
+ resultDisplay: undefined,
466
+ error: undefined,
467
+ errorType: undefined, // FIX: Added missing property
468
+ },
469
+ responseSubmittedToGemini: false,
470
+ };
471
+ const allCancelledTools = [cancelledToolCall1, cancelledToolCall2];
472
+ const client = new MockedGeminiClientClass(mockConfig);
473
+ let capturedOnComplete = null;
474
+ mockUseReactToolScheduler.mockImplementation((onComplete) => {
475
+ capturedOnComplete = onComplete;
476
+ return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
477
+ });
478
+ renderHook(() => useGeminiStream(client, [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
479
+ // Trigger the onComplete callback with multiple cancelled tools
480
+ await act(async () => {
481
+ if (capturedOnComplete) {
482
+ await capturedOnComplete(allCancelledTools);
483
+ }
484
+ });
485
+ await waitFor(() => {
486
+ // The tools should be marked as submitted locally
487
+ expect(mockMarkToolsAsSubmitted).toHaveBeenCalledWith([
488
+ 'cancel-1',
489
+ 'cancel-2',
490
+ ]);
491
+ // Crucially, addHistory should be called only ONCE
492
+ expect(client.addHistory).toHaveBeenCalledTimes(1);
493
+ // And that single call should contain BOTH function responses
494
+ expect(client.addHistory).toHaveBeenCalledWith({
495
+ role: 'user',
496
+ parts: [
497
+ ...cancelledToolCall1.response.responseParts,
498
+ ...cancelledToolCall2.response.responseParts,
499
+ ],
500
+ });
501
+ // No message should be sent back to the API for a turn with only cancellations
502
+ expect(mockSendMessageStream).not.toHaveBeenCalled();
503
+ });
504
+ });
505
+ it('should not flicker streaming state to Idle between tool completion and submission', async () => {
506
+ const toolCallResponseParts = [
507
+ { text: 'tool 1 final response' },
508
+ ];
509
+ const initialToolCalls = [
510
+ {
511
+ request: {
512
+ callId: 'call1',
513
+ name: 'tool1',
514
+ args: {},
515
+ isClientInitiated: false,
516
+ prompt_id: 'prompt-id-4',
517
+ },
518
+ status: 'executing',
519
+ responseSubmittedToGemini: false,
520
+ tool: {
521
+ name: 'tool1',
522
+ displayName: 'tool1',
523
+ description: 'desc',
524
+ build: vi.fn(),
525
+ },
526
+ invocation: {
527
+ getDescription: () => `Mock description`,
528
+ },
529
+ startTime: Date.now(),
530
+ },
531
+ ];
532
+ const completedToolCalls = [
533
+ {
534
+ ...initialToolCalls[0],
535
+ status: 'success',
536
+ response: {
537
+ callId: 'call1',
538
+ responseParts: toolCallResponseParts,
539
+ error: undefined,
540
+ errorType: undefined, // FIX: Added missing property
541
+ resultDisplay: 'Tool 1 success display',
542
+ },
543
+ endTime: Date.now(),
544
+ },
545
+ ];
546
+ // Capture the onComplete callback
547
+ let capturedOnComplete = null;
548
+ let currentToolCalls = initialToolCalls;
549
+ mockUseReactToolScheduler.mockImplementation((onComplete) => {
550
+ capturedOnComplete = onComplete;
551
+ return [
552
+ currentToolCalls,
553
+ mockScheduleToolCalls,
554
+ mockMarkToolsAsSubmitted,
555
+ ];
556
+ });
557
+ const { result, rerender } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
558
+ // 1. Initial state should be Responding because a tool is executing.
559
+ expect(result.current.streamingState).toBe(StreamingState.Responding);
560
+ // 2. Update the tool calls to completed state and rerender
561
+ currentToolCalls = completedToolCalls;
562
+ mockUseReactToolScheduler.mockImplementation((onComplete) => {
563
+ capturedOnComplete = onComplete;
564
+ return [
565
+ completedToolCalls,
566
+ mockScheduleToolCalls,
567
+ mockMarkToolsAsSubmitted,
568
+ ];
569
+ });
570
+ act(() => {
571
+ rerender();
572
+ });
573
+ // 3. The state should *still* be Responding, not Idle.
574
+ // This is because the completed tool's response has not been submitted yet.
575
+ expect(result.current.streamingState).toBe(StreamingState.Responding);
576
+ // 4. Trigger the onComplete callback to simulate tool completion
577
+ await act(async () => {
578
+ if (capturedOnComplete) {
579
+ await capturedOnComplete(completedToolCalls);
580
+ }
581
+ });
582
+ // 5. Wait for submitQuery to be called
583
+ await waitFor(() => {
584
+ expect(mockSendMessageStream).toHaveBeenCalledWith(toolCallResponseParts, expect.any(AbortSignal), 'prompt-id-4');
585
+ });
586
+ // 6. After submission, the state should remain Responding until the stream completes.
587
+ expect(result.current.streamingState).toBe(StreamingState.Responding);
588
+ });
589
+ describe('User Cancellation', () => {
590
+ let keypressCallback;
591
+ const mockUseKeypress = useKeypress;
592
+ beforeEach(() => {
593
+ // Capture the callback passed to useKeypress
594
+ mockUseKeypress.mockImplementation((callback, options) => {
595
+ if (options.isActive) {
596
+ keypressCallback = callback;
597
+ }
598
+ else {
599
+ keypressCallback = () => { };
600
+ }
601
+ });
602
+ });
603
+ const simulateEscapeKeyPress = () => {
604
+ act(() => {
605
+ keypressCallback({ name: 'escape' });
606
+ });
607
+ };
608
+ it('should cancel an in-progress stream when escape is pressed', async () => {
609
+ const mockStream = (async function* () {
610
+ yield { type: 'content', value: 'Part 1' };
611
+ // Keep the stream open
612
+ await new Promise(() => { });
613
+ })();
614
+ mockSendMessageStream.mockReturnValue(mockStream);
615
+ const { result } = renderTestHook();
616
+ // Start a query
617
+ await act(async () => {
618
+ result.current.submitQuery('test query');
619
+ });
620
+ // Wait for the first part of the response
621
+ await waitFor(() => {
622
+ expect(result.current.streamingState).toBe(StreamingState.Responding);
623
+ });
624
+ // Simulate escape key press
625
+ simulateEscapeKeyPress();
626
+ // Verify cancellation message is added
627
+ await waitFor(() => {
628
+ expect(mockAddItem).toHaveBeenCalledWith({
629
+ type: MessageType.INFO,
630
+ text: 'Request cancelled.',
631
+ }, expect.any(Number));
632
+ });
633
+ // Verify state is reset
634
+ expect(result.current.streamingState).toBe(StreamingState.Idle);
635
+ });
636
+ it('should call onCancelSubmit handler when escape is pressed', async () => {
637
+ const cancelSubmitSpy = vi.fn();
638
+ const mockStream = (async function* () {
639
+ yield { type: 'content', value: 'Part 1' };
640
+ // Keep the stream open
641
+ await new Promise(() => { });
642
+ })();
643
+ mockSendMessageStream.mockReturnValue(mockStream);
644
+ const { result } = renderHook(() => useGeminiStream(mockConfig.getGeminiClient(), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, cancelSubmitSpy, () => { }, 80, 24));
645
+ // Start a query
646
+ await act(async () => {
647
+ result.current.submitQuery('test query');
648
+ });
649
+ simulateEscapeKeyPress();
650
+ expect(cancelSubmitSpy).toHaveBeenCalled();
651
+ });
652
+ it('should call setShellInputFocused(false) when escape is pressed', async () => {
653
+ const setShellInputFocusedSpy = vi.fn();
654
+ const mockStream = (async function* () {
655
+ yield { type: 'content', value: 'Part 1' };
656
+ await new Promise(() => { }); // Keep stream open
657
+ })();
658
+ mockSendMessageStream.mockReturnValue(mockStream);
659
+ const { result } = renderHook(() => useGeminiStream(mockConfig.getGeminiClient(), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, vi.fn(), setShellInputFocusedSpy, // Pass the spy here
660
+ 80, 24));
661
+ // Start a query
662
+ await act(async () => {
663
+ result.current.submitQuery('test query');
664
+ });
665
+ simulateEscapeKeyPress();
666
+ expect(setShellInputFocusedSpy).toHaveBeenCalledWith(false);
667
+ });
668
+ it('should not do anything if escape is pressed when not responding', () => {
669
+ const { result } = renderTestHook();
670
+ expect(result.current.streamingState).toBe(StreamingState.Idle);
671
+ // Simulate escape key press
672
+ simulateEscapeKeyPress();
673
+ // No change should happen, no cancellation message
674
+ expect(mockAddItem).not.toHaveBeenCalledWith(expect.objectContaining({
675
+ text: 'Request cancelled.',
676
+ }), expect.any(Number));
677
+ });
678
+ it('should prevent further processing after cancellation', async () => {
679
+ let continueStream;
680
+ const streamPromise = new Promise((resolve) => {
681
+ continueStream = resolve;
682
+ });
683
+ const mockStream = (async function* () {
684
+ yield { type: 'content', value: 'Initial' };
685
+ await streamPromise; // Wait until we manually continue
686
+ yield { type: 'content', value: ' Canceled' };
687
+ })();
688
+ mockSendMessageStream.mockReturnValue(mockStream);
689
+ const { result } = renderTestHook();
690
+ await act(async () => {
691
+ result.current.submitQuery('long running query');
692
+ });
693
+ await waitFor(() => {
694
+ expect(result.current.streamingState).toBe(StreamingState.Responding);
695
+ });
696
+ // Cancel the request
697
+ simulateEscapeKeyPress();
698
+ // Allow the stream to continue
699
+ act(() => {
700
+ continueStream();
701
+ });
702
+ // Wait a bit to see if the second part is processed
703
+ await new Promise((resolve) => setTimeout(resolve, 50));
704
+ // The text should not have been updated with " Canceled"
705
+ const lastCall = mockAddItem.mock.calls.find((call) => call[0].type === 'gemini');
706
+ expect(lastCall?.[0].text).toBe('Initial');
707
+ // The final state should be idle after cancellation
708
+ expect(result.current.streamingState).toBe(StreamingState.Idle);
709
+ });
710
+ it('should not cancel if a tool call is in progress (not just responding)', async () => {
711
+ const toolCalls = [
712
+ {
713
+ request: { callId: 'call1', name: 'tool1', args: {} },
714
+ status: 'executing',
715
+ responseSubmittedToGemini: false,
716
+ tool: {
717
+ name: 'tool1',
718
+ description: 'desc1',
719
+ build: vi.fn().mockImplementation((_) => ({
720
+ getDescription: () => `Mock description`,
721
+ })),
722
+ },
723
+ invocation: {
724
+ getDescription: () => `Mock description`,
725
+ },
726
+ startTime: Date.now(),
727
+ liveOutput: '...',
728
+ },
729
+ ];
730
+ const abortSpy = vi.spyOn(AbortController.prototype, 'abort');
731
+ const { result } = renderTestHook(toolCalls);
732
+ // State is `Responding` because a tool is running
733
+ expect(result.current.streamingState).toBe(StreamingState.Responding);
734
+ // Try to cancel
735
+ simulateEscapeKeyPress();
736
+ // Nothing should happen because the state is not `Responding`
737
+ expect(abortSpy).not.toHaveBeenCalled();
738
+ });
739
+ });
740
+ describe('Slash Command Handling', () => {
741
+ it('should schedule a tool call when the command processor returns a schedule_tool action', async () => {
742
+ const clientToolRequest = {
743
+ type: 'schedule_tool',
744
+ toolName: 'save_memory',
745
+ toolArgs: { fact: 'test fact' },
746
+ };
747
+ mockHandleSlashCommand.mockResolvedValue(clientToolRequest);
748
+ const { result } = renderTestHook();
749
+ await act(async () => {
750
+ await result.current.submitQuery('/memory add "test fact"');
751
+ });
752
+ await waitFor(() => {
753
+ expect(mockScheduleToolCalls).toHaveBeenCalledWith([
754
+ expect.objectContaining({
755
+ name: 'save_memory',
756
+ args: { fact: 'test fact' },
757
+ isClientInitiated: true,
758
+ }),
759
+ ], expect.any(AbortSignal));
760
+ expect(mockSendMessageStream).not.toHaveBeenCalled();
761
+ });
762
+ });
763
+ it('should stop processing and not call Gemini when a command is handled without a tool call', async () => {
764
+ const uiOnlyCommandResult = {
765
+ type: 'handled',
766
+ };
767
+ mockHandleSlashCommand.mockResolvedValue(uiOnlyCommandResult);
768
+ const { result } = renderTestHook();
769
+ await act(async () => {
770
+ await result.current.submitQuery('/help');
771
+ });
772
+ await waitFor(() => {
773
+ expect(mockHandleSlashCommand).toHaveBeenCalledWith('/help');
774
+ expect(mockScheduleToolCalls).not.toHaveBeenCalled();
775
+ expect(mockSendMessageStream).not.toHaveBeenCalled(); // No LLM call made
776
+ });
777
+ });
778
+ it('should call Gemini with prompt content when slash command returns a `submit_prompt` action', async () => {
779
+ const customCommandResult = {
780
+ type: 'submit_prompt',
781
+ content: 'This is the actual prompt from the command file.',
782
+ };
783
+ mockHandleSlashCommand.mockResolvedValue(customCommandResult);
784
+ const { result, mockSendMessageStream: localMockSendMessageStream } = renderTestHook();
785
+ await act(async () => {
786
+ await result.current.submitQuery('/my-custom-command');
787
+ });
788
+ await waitFor(() => {
789
+ expect(mockHandleSlashCommand).toHaveBeenCalledWith('/my-custom-command');
790
+ expect(localMockSendMessageStream).not.toHaveBeenCalledWith('/my-custom-command', expect.anything(), expect.anything());
791
+ expect(localMockSendMessageStream).toHaveBeenCalledWith('This is the actual prompt from the command file.', expect.any(AbortSignal), expect.any(String));
792
+ expect(mockScheduleToolCalls).not.toHaveBeenCalled();
793
+ });
794
+ });
795
+ it('should correctly handle a submit_prompt action with empty content', async () => {
796
+ const emptyPromptResult = {
797
+ type: 'submit_prompt',
798
+ content: '',
799
+ };
800
+ mockHandleSlashCommand.mockResolvedValue(emptyPromptResult);
801
+ const { result, mockSendMessageStream: localMockSendMessageStream } = renderTestHook();
802
+ await act(async () => {
803
+ await result.current.submitQuery('/emptycmd');
804
+ });
805
+ await waitFor(() => {
806
+ expect(mockHandleSlashCommand).toHaveBeenCalledWith('/emptycmd');
807
+ expect(localMockSendMessageStream).toHaveBeenCalledWith('', expect.any(AbortSignal), expect.any(String));
808
+ });
809
+ });
810
+ it('should not call handleSlashCommand for line comments', async () => {
811
+ const { result, mockSendMessageStream: localMockSendMessageStream } = renderTestHook();
812
+ await act(async () => {
813
+ await result.current.submitQuery('// This is a line comment');
814
+ });
815
+ await waitFor(() => {
816
+ expect(mockHandleSlashCommand).not.toHaveBeenCalled();
817
+ expect(localMockSendMessageStream).toHaveBeenCalledWith('// This is a line comment', expect.any(AbortSignal), expect.any(String));
818
+ });
819
+ });
820
+ it('should not call handleSlashCommand for block comments', async () => {
821
+ const { result, mockSendMessageStream: localMockSendMessageStream } = renderTestHook();
822
+ await act(async () => {
823
+ await result.current.submitQuery('/* This is a block comment */');
824
+ });
825
+ await waitFor(() => {
826
+ expect(mockHandleSlashCommand).not.toHaveBeenCalled();
827
+ expect(localMockSendMessageStream).toHaveBeenCalledWith('/* This is a block comment */', expect.any(AbortSignal), expect.any(String));
828
+ });
829
+ });
830
+ it('should not call handleSlashCommand is shell mode is active', async () => {
831
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, () => { }, mockHandleSlashCommand, true, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
832
+ await act(async () => {
833
+ await result.current.submitQuery('/about');
834
+ });
835
+ await waitFor(() => {
836
+ expect(mockHandleSlashCommand).not.toHaveBeenCalled();
837
+ });
838
+ });
839
+ });
840
+ describe('Memory Refresh on save_memory', () => {
841
+ it('should call performMemoryRefresh when a save_memory tool call completes successfully', async () => {
842
+ const mockPerformMemoryRefresh = vi.fn();
843
+ const completedToolCall = {
844
+ request: {
845
+ callId: 'save-mem-call-1',
846
+ name: 'save_memory',
847
+ args: { fact: 'test' },
848
+ isClientInitiated: true,
849
+ prompt_id: 'prompt-id-6',
850
+ },
851
+ status: 'success',
852
+ responseSubmittedToGemini: false,
853
+ response: {
854
+ callId: 'save-mem-call-1',
855
+ responseParts: [{ text: 'Memory saved' }],
856
+ resultDisplay: 'Success: Memory saved',
857
+ error: undefined,
858
+ errorType: undefined, // FIX: Added missing property
859
+ },
860
+ tool: {
861
+ name: 'save_memory',
862
+ displayName: 'save_memory',
863
+ description: 'Saves memory',
864
+ build: vi.fn(),
865
+ },
866
+ invocation: {
867
+ getDescription: () => `Mock description`,
868
+ },
869
+ };
870
+ // Capture the onComplete callback
871
+ let capturedOnComplete = null;
872
+ mockUseReactToolScheduler.mockImplementation((onComplete) => {
873
+ capturedOnComplete = onComplete;
874
+ return [[], mockScheduleToolCalls, mockMarkToolsAsSubmitted];
875
+ });
876
+ renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, mockPerformMemoryRefresh, false, () => { }, () => { }, () => { }, () => { }, 80, 24));
877
+ // Trigger the onComplete callback with the completed save_memory tool
878
+ await act(async () => {
879
+ if (capturedOnComplete) {
880
+ await capturedOnComplete([completedToolCall]);
881
+ }
882
+ });
883
+ await waitFor(() => {
884
+ expect(mockPerformMemoryRefresh).toHaveBeenCalledTimes(1);
885
+ });
886
+ });
887
+ });
888
+ describe('Error Handling', () => {
889
+ it('should call parseAndFormatApiError with the correct authType on stream initialization failure', async () => {
890
+ // 1. Setup
891
+ const mockError = new Error('Rate limit exceeded');
892
+ const mockAuthType = AuthType.LOGIN_WITH_GOOGLE;
893
+ mockParseAndFormatApiError.mockClear();
894
+ mockSendMessageStream.mockReturnValue((async function* () {
895
+ yield { type: 'content', value: '' };
896
+ throw mockError;
897
+ })());
898
+ const testConfig = {
899
+ ...mockConfig,
900
+ getContentGeneratorConfig: vi.fn(() => ({
901
+ authType: mockAuthType,
902
+ })),
903
+ getModel: vi.fn(() => 'gemini-2.5-pro'),
904
+ };
905
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(testConfig), [], mockAddItem, testConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
906
+ // 2. Action
907
+ await act(async () => {
908
+ await result.current.submitQuery('test query');
909
+ });
910
+ // 3. Assertion
911
+ await waitFor(() => {
912
+ expect(mockParseAndFormatApiError).toHaveBeenCalledWith('Rate limit exceeded', mockAuthType, undefined, 'gemini-2.5-pro', 'gemini-2.5-flash');
913
+ });
914
+ });
915
+ });
916
+ describe('handleApprovalModeChange', () => {
917
+ it('should auto-approve all pending tool calls when switching to YOLO mode', async () => {
918
+ const mockOnConfirm = vi.fn().mockResolvedValue(undefined);
919
+ const awaitingApprovalToolCalls = [
920
+ {
921
+ request: {
922
+ callId: 'call1',
923
+ name: 'replace',
924
+ args: { old_string: 'old', new_string: 'new' },
925
+ isClientInitiated: false,
926
+ prompt_id: 'prompt-id-1',
927
+ },
928
+ status: 'awaiting_approval',
929
+ responseSubmittedToGemini: false,
930
+ confirmationDetails: {
931
+ type: 'edit',
932
+ title: 'Confirm Edit',
933
+ onConfirm: mockOnConfirm,
934
+ fileName: 'file.txt',
935
+ filePath: '/test/file.txt',
936
+ fileDiff: 'fake diff',
937
+ originalContent: 'old',
938
+ newContent: 'new',
939
+ },
940
+ tool: {
941
+ name: 'replace',
942
+ displayName: 'replace',
943
+ description: 'Replace text',
944
+ build: vi.fn(),
945
+ },
946
+ invocation: {
947
+ getDescription: () => 'Mock description',
948
+ },
949
+ },
950
+ {
951
+ request: {
952
+ callId: 'call2',
953
+ name: 'read_file',
954
+ args: { path: '/test/file.txt' },
955
+ isClientInitiated: false,
956
+ prompt_id: 'prompt-id-1',
957
+ },
958
+ status: 'awaiting_approval',
959
+ responseSubmittedToGemini: false,
960
+ confirmationDetails: {
961
+ type: 'info',
962
+ title: 'Read File',
963
+ onConfirm: mockOnConfirm,
964
+ prompt: 'Read /test/file.txt?',
965
+ },
966
+ tool: {
967
+ name: 'read_file',
968
+ displayName: 'read_file',
969
+ description: 'Read file',
970
+ build: vi.fn(),
971
+ },
972
+ invocation: {
973
+ getDescription: () => 'Mock description',
974
+ },
975
+ },
976
+ ];
977
+ const { result } = renderTestHook(awaitingApprovalToolCalls);
978
+ await act(async () => {
979
+ await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
980
+ });
981
+ // Both tool calls should be auto-approved
982
+ expect(mockOnConfirm).toHaveBeenCalledTimes(2);
983
+ expect(mockOnConfirm).toHaveBeenNthCalledWith(1, ToolConfirmationOutcome.ProceedOnce);
984
+ expect(mockOnConfirm).toHaveBeenNthCalledWith(2, ToolConfirmationOutcome.ProceedOnce);
985
+ });
986
+ it('should only auto-approve edit tools when switching to AUTO_EDIT mode', async () => {
987
+ const mockOnConfirmReplace = vi.fn().mockResolvedValue(undefined);
988
+ const mockOnConfirmWrite = vi.fn().mockResolvedValue(undefined);
989
+ const mockOnConfirmRead = vi.fn().mockResolvedValue(undefined);
990
+ const awaitingApprovalToolCalls = [
991
+ {
992
+ request: {
993
+ callId: 'call1',
994
+ name: 'replace',
995
+ args: { old_string: 'old', new_string: 'new' },
996
+ isClientInitiated: false,
997
+ prompt_id: 'prompt-id-1',
998
+ },
999
+ status: 'awaiting_approval',
1000
+ responseSubmittedToGemini: false,
1001
+ confirmationDetails: {
1002
+ type: 'edit',
1003
+ title: 'Confirm Edit',
1004
+ onConfirm: mockOnConfirmReplace,
1005
+ fileName: 'file.txt',
1006
+ filePath: '/test/file.txt',
1007
+ fileDiff: 'fake diff',
1008
+ originalContent: 'old',
1009
+ newContent: 'new',
1010
+ },
1011
+ tool: {
1012
+ name: 'replace',
1013
+ displayName: 'replace',
1014
+ description: 'Replace text',
1015
+ build: vi.fn(),
1016
+ },
1017
+ invocation: {
1018
+ getDescription: () => 'Mock description',
1019
+ },
1020
+ },
1021
+ {
1022
+ request: {
1023
+ callId: 'call2',
1024
+ name: 'write_file',
1025
+ args: { path: '/test/new.txt', content: 'content' },
1026
+ isClientInitiated: false,
1027
+ prompt_id: 'prompt-id-1',
1028
+ },
1029
+ status: 'awaiting_approval',
1030
+ responseSubmittedToGemini: false,
1031
+ confirmationDetails: {
1032
+ type: 'edit',
1033
+ title: 'Confirm Edit',
1034
+ onConfirm: mockOnConfirmWrite,
1035
+ fileName: 'new.txt',
1036
+ filePath: '/test/new.txt',
1037
+ fileDiff: 'fake diff',
1038
+ originalContent: null,
1039
+ newContent: 'content',
1040
+ },
1041
+ tool: {
1042
+ name: 'write_file',
1043
+ displayName: 'write_file',
1044
+ description: 'Write file',
1045
+ build: vi.fn(),
1046
+ },
1047
+ invocation: {
1048
+ getDescription: () => 'Mock description',
1049
+ },
1050
+ },
1051
+ {
1052
+ request: {
1053
+ callId: 'call3',
1054
+ name: 'read_file',
1055
+ args: { path: '/test/file.txt' },
1056
+ isClientInitiated: false,
1057
+ prompt_id: 'prompt-id-1',
1058
+ },
1059
+ status: 'awaiting_approval',
1060
+ responseSubmittedToGemini: false,
1061
+ confirmationDetails: {
1062
+ type: 'info',
1063
+ title: 'Read File',
1064
+ onConfirm: mockOnConfirmRead,
1065
+ prompt: 'Read /test/file.txt?',
1066
+ },
1067
+ tool: {
1068
+ name: 'read_file',
1069
+ displayName: 'read_file',
1070
+ description: 'Read file',
1071
+ build: vi.fn(),
1072
+ },
1073
+ invocation: {
1074
+ getDescription: () => 'Mock description',
1075
+ },
1076
+ },
1077
+ ];
1078
+ const { result } = renderTestHook(awaitingApprovalToolCalls);
1079
+ await act(async () => {
1080
+ await result.current.handleApprovalModeChange(ApprovalMode.AUTO_EDIT);
1081
+ });
1082
+ // Only replace and write_file should be auto-approved
1083
+ expect(mockOnConfirmReplace).toHaveBeenCalledTimes(1);
1084
+ expect(mockOnConfirmReplace).toHaveBeenCalledWith(ToolConfirmationOutcome.ProceedOnce);
1085
+ expect(mockOnConfirmWrite).toHaveBeenCalledTimes(1);
1086
+ expect(mockOnConfirmWrite).toHaveBeenCalledWith(ToolConfirmationOutcome.ProceedOnce);
1087
+ // read_file should not be auto-approved
1088
+ expect(mockOnConfirmRead).not.toHaveBeenCalled();
1089
+ });
1090
+ it('should not auto-approve any tools when switching to REQUIRE_CONFIRMATION mode', async () => {
1091
+ const mockOnConfirm = vi.fn().mockResolvedValue(undefined);
1092
+ const awaitingApprovalToolCalls = [
1093
+ {
1094
+ request: {
1095
+ callId: 'call1',
1096
+ name: 'replace',
1097
+ args: { old_string: 'old', new_string: 'new' },
1098
+ isClientInitiated: false,
1099
+ prompt_id: 'prompt-id-1',
1100
+ },
1101
+ status: 'awaiting_approval',
1102
+ responseSubmittedToGemini: false,
1103
+ confirmationDetails: {
1104
+ type: 'edit',
1105
+ title: 'Confirm Edit',
1106
+ onConfirm: mockOnConfirm,
1107
+ fileName: 'file.txt',
1108
+ filePath: '/test/file.txt',
1109
+ fileDiff: 'fake diff',
1110
+ originalContent: 'old',
1111
+ newContent: 'new',
1112
+ },
1113
+ tool: {
1114
+ name: 'replace',
1115
+ displayName: 'replace',
1116
+ description: 'Replace text',
1117
+ build: vi.fn(),
1118
+ },
1119
+ invocation: {
1120
+ getDescription: () => 'Mock description',
1121
+ },
1122
+ },
1123
+ ];
1124
+ const { result } = renderTestHook(awaitingApprovalToolCalls);
1125
+ await act(async () => {
1126
+ await result.current.handleApprovalModeChange(ApprovalMode.DEFAULT);
1127
+ });
1128
+ // No tools should be auto-approved
1129
+ expect(mockOnConfirm).not.toHaveBeenCalled();
1130
+ });
1131
+ it('should handle errors gracefully when auto-approving tool calls', async () => {
1132
+ const consoleSpy = vi
1133
+ .spyOn(console, 'error')
1134
+ .mockImplementation(() => { });
1135
+ const mockOnConfirmSuccess = vi.fn().mockResolvedValue(undefined);
1136
+ const mockOnConfirmError = vi
1137
+ .fn()
1138
+ .mockRejectedValue(new Error('Approval failed'));
1139
+ const awaitingApprovalToolCalls = [
1140
+ {
1141
+ request: {
1142
+ callId: 'call1',
1143
+ name: 'replace',
1144
+ args: { old_string: 'old', new_string: 'new' },
1145
+ isClientInitiated: false,
1146
+ prompt_id: 'prompt-id-1',
1147
+ },
1148
+ status: 'awaiting_approval',
1149
+ responseSubmittedToGemini: false,
1150
+ confirmationDetails: {
1151
+ type: 'edit',
1152
+ title: 'Confirm Edit',
1153
+ onConfirm: mockOnConfirmSuccess,
1154
+ fileName: 'file.txt',
1155
+ filePath: '/test/file.txt',
1156
+ fileDiff: 'fake diff',
1157
+ originalContent: 'old',
1158
+ newContent: 'new',
1159
+ },
1160
+ tool: {
1161
+ name: 'replace',
1162
+ displayName: 'replace',
1163
+ description: 'Replace text',
1164
+ build: vi.fn(),
1165
+ },
1166
+ invocation: {
1167
+ getDescription: () => 'Mock description',
1168
+ },
1169
+ },
1170
+ {
1171
+ request: {
1172
+ callId: 'call2',
1173
+ name: 'write_file',
1174
+ args: { path: '/test/file.txt', content: 'content' },
1175
+ isClientInitiated: false,
1176
+ prompt_id: 'prompt-id-1',
1177
+ },
1178
+ status: 'awaiting_approval',
1179
+ responseSubmittedToGemini: false,
1180
+ confirmationDetails: {
1181
+ type: 'edit',
1182
+ title: 'Confirm Edit',
1183
+ onConfirm: mockOnConfirmError,
1184
+ fileName: 'file.txt',
1185
+ filePath: '/test/file.txt',
1186
+ fileDiff: 'fake diff',
1187
+ originalContent: null,
1188
+ newContent: 'content',
1189
+ },
1190
+ tool: {
1191
+ name: 'write_file',
1192
+ displayName: 'write_file',
1193
+ description: 'Write file',
1194
+ build: vi.fn(),
1195
+ },
1196
+ invocation: {
1197
+ getDescription: () => 'Mock description',
1198
+ },
1199
+ },
1200
+ ];
1201
+ const { result } = renderTestHook(awaitingApprovalToolCalls);
1202
+ await act(async () => {
1203
+ await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
1204
+ });
1205
+ // Both confirmation methods should be called
1206
+ expect(mockOnConfirmSuccess).toHaveBeenCalledTimes(1);
1207
+ expect(mockOnConfirmError).toHaveBeenCalledTimes(1);
1208
+ // Error should be logged
1209
+ expect(consoleSpy).toHaveBeenCalledWith('Failed to auto-approve tool call call2:', expect.any(Error));
1210
+ consoleSpy.mockRestore();
1211
+ });
1212
+ it('should skip tool calls without confirmationDetails', async () => {
1213
+ const awaitingApprovalToolCalls = [
1214
+ {
1215
+ request: {
1216
+ callId: 'call1',
1217
+ name: 'replace',
1218
+ args: { old_string: 'old', new_string: 'new' },
1219
+ isClientInitiated: false,
1220
+ prompt_id: 'prompt-id-1',
1221
+ },
1222
+ status: 'awaiting_approval',
1223
+ responseSubmittedToGemini: false,
1224
+ // No confirmationDetails
1225
+ tool: {
1226
+ name: 'replace',
1227
+ displayName: 'replace',
1228
+ description: 'Replace text',
1229
+ build: vi.fn(),
1230
+ },
1231
+ invocation: {
1232
+ getDescription: () => 'Mock description',
1233
+ },
1234
+ },
1235
+ ];
1236
+ const { result } = renderTestHook(awaitingApprovalToolCalls);
1237
+ // Should not throw an error
1238
+ await act(async () => {
1239
+ await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
1240
+ });
1241
+ });
1242
+ it('should skip tool calls without onConfirm method in confirmationDetails', async () => {
1243
+ const awaitingApprovalToolCalls = [
1244
+ {
1245
+ request: {
1246
+ callId: 'call1',
1247
+ name: 'replace',
1248
+ args: { old_string: 'old', new_string: 'new' },
1249
+ isClientInitiated: false,
1250
+ prompt_id: 'prompt-id-1',
1251
+ },
1252
+ status: 'awaiting_approval',
1253
+ responseSubmittedToGemini: false,
1254
+ confirmationDetails: {
1255
+ type: 'edit',
1256
+ title: 'Confirm Edit',
1257
+ // No onConfirm method
1258
+ fileName: 'file.txt',
1259
+ filePath: '/test/file.txt',
1260
+ fileDiff: 'fake diff',
1261
+ originalContent: 'old',
1262
+ newContent: 'new',
1263
+ },
1264
+ tool: {
1265
+ name: 'replace',
1266
+ displayName: 'replace',
1267
+ description: 'Replace text',
1268
+ build: vi.fn(),
1269
+ },
1270
+ invocation: {
1271
+ getDescription: () => 'Mock description',
1272
+ },
1273
+ },
1274
+ ];
1275
+ const { result } = renderTestHook(awaitingApprovalToolCalls);
1276
+ // Should not throw an error
1277
+ await act(async () => {
1278
+ await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
1279
+ });
1280
+ });
1281
+ it('should only process tool calls with awaiting_approval status', async () => {
1282
+ const mockOnConfirmAwaiting = vi.fn().mockResolvedValue(undefined);
1283
+ const mockOnConfirmExecuting = vi.fn().mockResolvedValue(undefined);
1284
+ const mixedStatusToolCalls = [
1285
+ {
1286
+ request: {
1287
+ callId: 'call1',
1288
+ name: 'replace',
1289
+ args: { old_string: 'old', new_string: 'new' },
1290
+ isClientInitiated: false,
1291
+ prompt_id: 'prompt-id-1',
1292
+ },
1293
+ status: 'awaiting_approval',
1294
+ responseSubmittedToGemini: false,
1295
+ confirmationDetails: {
1296
+ type: 'edit',
1297
+ title: 'Confirm Edit',
1298
+ onConfirm: mockOnConfirmAwaiting,
1299
+ fileName: 'file.txt',
1300
+ filePath: '/test/file.txt',
1301
+ fileDiff: 'fake diff',
1302
+ originalContent: 'old',
1303
+ newContent: 'new',
1304
+ },
1305
+ tool: {
1306
+ name: 'replace',
1307
+ displayName: 'replace',
1308
+ description: 'Replace text',
1309
+ build: vi.fn(),
1310
+ },
1311
+ invocation: {
1312
+ getDescription: () => 'Mock description',
1313
+ },
1314
+ },
1315
+ {
1316
+ request: {
1317
+ callId: 'call2',
1318
+ name: 'write_file',
1319
+ args: { path: '/test/file.txt', content: 'content' },
1320
+ isClientInitiated: false,
1321
+ prompt_id: 'prompt-id-1',
1322
+ },
1323
+ status: 'executing',
1324
+ responseSubmittedToGemini: false,
1325
+ tool: {
1326
+ name: 'write_file',
1327
+ displayName: 'write_file',
1328
+ description: 'Write file',
1329
+ build: vi.fn(),
1330
+ },
1331
+ invocation: {
1332
+ getDescription: () => 'Mock description',
1333
+ },
1334
+ startTime: Date.now(),
1335
+ liveOutput: 'Writing...',
1336
+ },
1337
+ ];
1338
+ const { result } = renderTestHook(mixedStatusToolCalls);
1339
+ await act(async () => {
1340
+ await result.current.handleApprovalModeChange(ApprovalMode.YOLO);
1341
+ });
1342
+ // Only the awaiting_approval tool should be processed
1343
+ expect(mockOnConfirmAwaiting).toHaveBeenCalledTimes(1);
1344
+ expect(mockOnConfirmExecuting).not.toHaveBeenCalled();
1345
+ });
1346
+ });
1347
+ describe('handleFinishedEvent', () => {
1348
+ it('should add info message for MAX_TOKENS finish reason', async () => {
1349
+ // Setup mock to return a stream with MAX_TOKENS finish reason
1350
+ mockSendMessageStream.mockReturnValue((async function* () {
1351
+ yield {
1352
+ type: ServerGeminiEventType.Content,
1353
+ value: 'This is a truncated response...',
1354
+ };
1355
+ yield {
1356
+ type: ServerGeminiEventType.Finished,
1357
+ value: { reason: 'MAX_TOKENS', usageMetadata: undefined },
1358
+ };
1359
+ })());
1360
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1361
+ // Submit a query
1362
+ await act(async () => {
1363
+ await result.current.submitQuery('Generate long text');
1364
+ });
1365
+ // Check that the info message was added
1366
+ await waitFor(() => {
1367
+ expect(mockAddItem).toHaveBeenCalledWith({
1368
+ type: 'info',
1369
+ text: '⚠️ Response truncated due to token limits.',
1370
+ }, expect.any(Number));
1371
+ });
1372
+ });
1373
+ describe('ContextWindowWillOverflow event', () => {
1374
+ beforeEach(() => {
1375
+ vi.mocked(tokenLimit).mockReturnValue(100);
1376
+ });
1377
+ it('should add message without suggestion when remaining tokens are > 75% of limit', async () => {
1378
+ // Setup mock to return a stream with ContextWindowWillOverflow event
1379
+ // Limit is 100, remaining is 80 (> 75)
1380
+ mockSendMessageStream.mockReturnValue((async function* () {
1381
+ yield {
1382
+ type: ServerGeminiEventType.ContextWindowWillOverflow,
1383
+ value: {
1384
+ estimatedRequestTokenCount: 20,
1385
+ remainingTokenCount: 80,
1386
+ },
1387
+ };
1388
+ })());
1389
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1390
+ // Submit a query
1391
+ await act(async () => {
1392
+ await result.current.submitQuery('Test overflow');
1393
+ });
1394
+ // Check that the message was added without suggestion
1395
+ await waitFor(() => {
1396
+ expect(mockAddItem).toHaveBeenCalledWith({
1397
+ type: 'info',
1398
+ text: `Sending this message (20 tokens) might exceed the remaining context window limit (80 tokens).`,
1399
+ }, expect.any(Number));
1400
+ });
1401
+ });
1402
+ it('should add message with suggestion when remaining tokens are < 75% of limit', async () => {
1403
+ // Setup mock to return a stream with ContextWindowWillOverflow event
1404
+ // Limit is 100, remaining is 70 (< 75)
1405
+ mockSendMessageStream.mockReturnValue((async function* () {
1406
+ yield {
1407
+ type: ServerGeminiEventType.ContextWindowWillOverflow,
1408
+ value: {
1409
+ estimatedRequestTokenCount: 30,
1410
+ remainingTokenCount: 70,
1411
+ },
1412
+ };
1413
+ })());
1414
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1415
+ // Submit a query
1416
+ await act(async () => {
1417
+ await result.current.submitQuery('Test overflow');
1418
+ });
1419
+ // Check that the message was added with suggestion
1420
+ await waitFor(() => {
1421
+ expect(mockAddItem).toHaveBeenCalledWith({
1422
+ type: 'info',
1423
+ text: `Sending this message (30 tokens) might exceed the remaining context window limit (70 tokens). Please try reducing the size of your message or use the \`/compress\` command to compress the chat history.`,
1424
+ }, expect.any(Number));
1425
+ });
1426
+ });
1427
+ });
1428
+ it('should call onCancelSubmit when ContextWindowWillOverflow event is received', async () => {
1429
+ const onCancelSubmitSpy = vi.fn();
1430
+ // Setup mock to return a stream with ContextWindowWillOverflow event
1431
+ mockSendMessageStream.mockReturnValue((async function* () {
1432
+ yield {
1433
+ type: ServerGeminiEventType.ContextWindowWillOverflow,
1434
+ value: {
1435
+ estimatedRequestTokenCount: 100,
1436
+ remainingTokenCount: 50,
1437
+ },
1438
+ };
1439
+ })());
1440
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, onCancelSubmitSpy, () => { }, 80, 24));
1441
+ // Submit a query
1442
+ await act(async () => {
1443
+ await result.current.submitQuery('Test overflow');
1444
+ });
1445
+ // Check that onCancelSubmit was called
1446
+ await waitFor(() => {
1447
+ expect(onCancelSubmitSpy).toHaveBeenCalled();
1448
+ });
1449
+ });
1450
+ it('should not add message for STOP finish reason', async () => {
1451
+ // Setup mock to return a stream with STOP finish reason
1452
+ mockSendMessageStream.mockReturnValue((async function* () {
1453
+ yield {
1454
+ type: ServerGeminiEventType.Content,
1455
+ value: 'Complete response',
1456
+ };
1457
+ yield {
1458
+ type: ServerGeminiEventType.Finished,
1459
+ value: { reason: 'STOP', usageMetadata: undefined },
1460
+ };
1461
+ })());
1462
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1463
+ // Submit a query
1464
+ await act(async () => {
1465
+ await result.current.submitQuery('Test normal completion');
1466
+ });
1467
+ // Wait a bit to ensure no message is added
1468
+ await new Promise((resolve) => setTimeout(resolve, 100));
1469
+ // Check that no info message was added for STOP
1470
+ const infoMessages = mockAddItem.mock.calls.filter((call) => call[0].type === 'info');
1471
+ expect(infoMessages).toHaveLength(0);
1472
+ });
1473
+ it('should not add message for FINISH_REASON_UNSPECIFIED', async () => {
1474
+ // Setup mock to return a stream with FINISH_REASON_UNSPECIFIED
1475
+ mockSendMessageStream.mockReturnValue((async function* () {
1476
+ yield {
1477
+ type: ServerGeminiEventType.Content,
1478
+ value: 'Response with unspecified finish',
1479
+ };
1480
+ yield {
1481
+ type: ServerGeminiEventType.Finished,
1482
+ value: {
1483
+ reason: 'FINISH_REASON_UNSPECIFIED',
1484
+ usageMetadata: undefined,
1485
+ },
1486
+ };
1487
+ })());
1488
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1489
+ // Submit a query
1490
+ await act(async () => {
1491
+ await result.current.submitQuery('Test unspecified finish');
1492
+ });
1493
+ // Wait a bit to ensure no message is added
1494
+ await new Promise((resolve) => setTimeout(resolve, 100));
1495
+ // Check that no info message was added
1496
+ const infoMessages = mockAddItem.mock.calls.filter((call) => call[0].type === 'info');
1497
+ expect(infoMessages).toHaveLength(0);
1498
+ });
1499
+ it('should add appropriate messages for other finish reasons', async () => {
1500
+ const testCases = [
1501
+ {
1502
+ reason: 'SAFETY',
1503
+ message: '⚠️ Response stopped due to safety reasons.',
1504
+ },
1505
+ {
1506
+ reason: 'RECITATION',
1507
+ message: '⚠️ Response stopped due to recitation policy.',
1508
+ },
1509
+ {
1510
+ reason: 'LANGUAGE',
1511
+ message: '⚠️ Response stopped due to unsupported language.',
1512
+ },
1513
+ {
1514
+ reason: 'BLOCKLIST',
1515
+ message: '⚠️ Response stopped due to forbidden terms.',
1516
+ },
1517
+ {
1518
+ reason: 'PROHIBITED_CONTENT',
1519
+ message: '⚠️ Response stopped due to prohibited content.',
1520
+ },
1521
+ {
1522
+ reason: 'SPII',
1523
+ message: '⚠️ Response stopped due to sensitive personally identifiable information.',
1524
+ },
1525
+ { reason: 'OTHER', message: '⚠️ Response stopped for other reasons.' },
1526
+ {
1527
+ reason: 'MALFORMED_FUNCTION_CALL',
1528
+ message: '⚠️ Response stopped due to malformed function call.',
1529
+ },
1530
+ {
1531
+ reason: 'IMAGE_SAFETY',
1532
+ message: '⚠️ Response stopped due to image safety violations.',
1533
+ },
1534
+ {
1535
+ reason: 'UNEXPECTED_TOOL_CALL',
1536
+ message: '⚠️ Response stopped due to unexpected tool call.',
1537
+ },
1538
+ ];
1539
+ for (const { reason, message } of testCases) {
1540
+ // Reset mocks for each test case
1541
+ mockAddItem.mockClear();
1542
+ mockSendMessageStream.mockReturnValue((async function* () {
1543
+ yield {
1544
+ type: ServerGeminiEventType.Content,
1545
+ value: `Response for ${reason}`,
1546
+ };
1547
+ yield {
1548
+ type: ServerGeminiEventType.Finished,
1549
+ value: { reason, usageMetadata: undefined },
1550
+ };
1551
+ })());
1552
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, vi.fn(), 80, 24));
1553
+ await act(async () => {
1554
+ await result.current.submitQuery(`Test ${reason}`);
1555
+ });
1556
+ await waitFor(() => {
1557
+ expect(mockAddItem).toHaveBeenCalledWith({
1558
+ type: 'info',
1559
+ text: message,
1560
+ }, expect.any(Number));
1561
+ });
1562
+ }
1563
+ });
1564
+ });
1565
+ it('should process @include commands, adding user turn after processing to prevent race conditions', async () => {
1566
+ const rawQuery = '@include file.txt Summarize this.';
1567
+ const processedQueryParts = [
1568
+ { text: 'Summarize this with content from @file.txt' },
1569
+ { text: 'File content...' },
1570
+ ];
1571
+ const userMessageTimestamp = Date.now();
1572
+ vi.spyOn(Date, 'now').mockReturnValue(userMessageTimestamp);
1573
+ handleAtCommandSpy.mockResolvedValue({
1574
+ processedQuery: processedQueryParts,
1575
+ shouldProceed: true,
1576
+ });
1577
+ const { result } = renderHook(() => useGeminiStream(mockConfig.getGeminiClient(), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, // shellModeActive
1578
+ vi.fn(), // getPreferredEditor
1579
+ vi.fn(), // onAuthError
1580
+ vi.fn(), // performMemoryRefresh
1581
+ false, // modelSwitched
1582
+ vi.fn(), // setModelSwitched
1583
+ vi.fn(), // onEditorClose
1584
+ vi.fn(), // onCancelSubmit
1585
+ vi.fn(), // setShellInputFocused
1586
+ 80, // terminalWidth
1587
+ 24));
1588
+ await act(async () => {
1589
+ await result.current.submitQuery(rawQuery);
1590
+ });
1591
+ expect(handleAtCommandSpy).toHaveBeenCalledWith(expect.objectContaining({
1592
+ query: rawQuery,
1593
+ }));
1594
+ expect(mockAddItem).toHaveBeenCalledWith({
1595
+ type: MessageType.USER,
1596
+ text: rawQuery,
1597
+ }, userMessageTimestamp);
1598
+ // FIX: The expectation now matches the actual call signature.
1599
+ expect(mockSendMessageStream).toHaveBeenCalledWith(processedQueryParts, // Argument 1: The parts array directly
1600
+ expect.any(AbortSignal), // Argument 2: An AbortSignal
1601
+ expect.any(String));
1602
+ });
1603
+ describe('Thought Reset', () => {
1604
+ it('should reset thought to null when starting a new prompt', async () => {
1605
+ // First, simulate a response with a thought
1606
+ mockSendMessageStream.mockReturnValue((async function* () {
1607
+ yield {
1608
+ type: ServerGeminiEventType.Thought,
1609
+ value: {
1610
+ subject: 'Previous thought',
1611
+ description: 'Old description',
1612
+ },
1613
+ };
1614
+ yield {
1615
+ type: ServerGeminiEventType.Content,
1616
+ value: 'Some response content',
1617
+ };
1618
+ yield {
1619
+ type: ServerGeminiEventType.Finished,
1620
+ value: { reason: 'STOP', usageMetadata: undefined },
1621
+ };
1622
+ })());
1623
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1624
+ // Submit first query to set a thought
1625
+ await act(async () => {
1626
+ await result.current.submitQuery('First query');
1627
+ });
1628
+ // Wait for the first response to complete
1629
+ await waitFor(() => {
1630
+ expect(mockAddItem).toHaveBeenCalledWith(expect.objectContaining({
1631
+ type: 'gemini',
1632
+ text: 'Some response content',
1633
+ }), expect.any(Number));
1634
+ });
1635
+ // Now simulate a new response without a thought
1636
+ mockSendMessageStream.mockReturnValue((async function* () {
1637
+ yield {
1638
+ type: ServerGeminiEventType.Content,
1639
+ value: 'New response content',
1640
+ };
1641
+ yield {
1642
+ type: ServerGeminiEventType.Finished,
1643
+ value: { reason: 'STOP', usageMetadata: undefined },
1644
+ };
1645
+ })());
1646
+ // Submit second query - thought should be reset
1647
+ await act(async () => {
1648
+ await result.current.submitQuery('Second query');
1649
+ });
1650
+ // The thought should be reset to null when starting the new prompt
1651
+ // We can verify this by checking that the LoadingIndicator would not show the previous thought
1652
+ // The actual thought state is internal to the hook, but we can verify the behavior
1653
+ // by ensuring the second response doesn't show the previous thought
1654
+ await waitFor(() => {
1655
+ expect(mockAddItem).toHaveBeenCalledWith(expect.objectContaining({
1656
+ type: 'gemini',
1657
+ text: 'New response content',
1658
+ }), expect.any(Number));
1659
+ });
1660
+ });
1661
+ it('should memoize pendingHistoryItems', () => {
1662
+ mockUseReactToolScheduler.mockReturnValue([
1663
+ [],
1664
+ mockScheduleToolCalls,
1665
+ mockCancelAllToolCalls,
1666
+ mockMarkToolsAsSubmitted,
1667
+ ]);
1668
+ const { result, rerender } = renderHook(() => useGeminiStream(mockConfig.getGeminiClient(), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1669
+ const firstResult = result.current.pendingHistoryItems;
1670
+ rerender();
1671
+ const secondResult = result.current.pendingHistoryItems;
1672
+ expect(firstResult).toStrictEqual(secondResult);
1673
+ const newToolCalls = [
1674
+ {
1675
+ request: { callId: 'call1', name: 'tool1', args: {} },
1676
+ status: 'executing',
1677
+ tool: {
1678
+ name: 'tool1',
1679
+ displayName: 'tool1',
1680
+ description: 'desc1',
1681
+ build: vi.fn(),
1682
+ },
1683
+ invocation: {
1684
+ getDescription: () => 'Mock description',
1685
+ },
1686
+ },
1687
+ ];
1688
+ mockUseReactToolScheduler.mockReturnValue([
1689
+ newToolCalls,
1690
+ mockScheduleToolCalls,
1691
+ mockCancelAllToolCalls,
1692
+ mockMarkToolsAsSubmitted,
1693
+ ]);
1694
+ rerender();
1695
+ const thirdResult = result.current.pendingHistoryItems;
1696
+ expect(thirdResult).not.toStrictEqual(secondResult);
1697
+ });
1698
+ it('should reset thought to null when user cancels', async () => {
1699
+ // Mock a stream that yields a thought then gets cancelled
1700
+ mockSendMessageStream.mockReturnValue((async function* () {
1701
+ yield {
1702
+ type: ServerGeminiEventType.Thought,
1703
+ value: { subject: 'Some thought', description: 'Description' },
1704
+ };
1705
+ yield { type: ServerGeminiEventType.UserCancelled };
1706
+ })());
1707
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1708
+ // Submit query
1709
+ await act(async () => {
1710
+ await result.current.submitQuery('Test query');
1711
+ });
1712
+ // Verify cancellation message was added
1713
+ await waitFor(() => {
1714
+ expect(mockAddItem).toHaveBeenCalledWith(expect.objectContaining({
1715
+ type: 'info',
1716
+ text: 'User cancelled the request.',
1717
+ }), expect.any(Number));
1718
+ });
1719
+ // Verify state is reset to idle
1720
+ expect(result.current.streamingState).toBe(StreamingState.Idle);
1721
+ });
1722
+ it('should reset thought to null when there is an error', async () => {
1723
+ // Mock a stream that yields a thought then encounters an error
1724
+ mockSendMessageStream.mockReturnValue((async function* () {
1725
+ yield {
1726
+ type: ServerGeminiEventType.Thought,
1727
+ value: { subject: 'Some thought', description: 'Description' },
1728
+ };
1729
+ yield {
1730
+ type: ServerGeminiEventType.Error,
1731
+ value: { error: { message: 'Test error' } },
1732
+ };
1733
+ })());
1734
+ const { result } = renderHook(() => useGeminiStream(new MockedGeminiClientClass(mockConfig), [], mockAddItem, mockConfig, mockLoadedSettings, mockOnDebugMessage, mockHandleSlashCommand, false, () => 'vscode', () => { }, () => Promise.resolve(), false, () => { }, () => { }, () => { }, () => { }, 80, 24));
1735
+ // Submit query
1736
+ await act(async () => {
1737
+ await result.current.submitQuery('Test query');
1738
+ });
1739
+ // Verify error message was added
1740
+ await waitFor(() => {
1741
+ expect(mockAddItem).toHaveBeenCalledWith(expect.objectContaining({
1742
+ type: 'error',
1743
+ }), expect.any(Number));
1744
+ });
1745
+ // Verify parseAndFormatApiError was called
1746
+ expect(mockParseAndFormatApiError).toHaveBeenCalledWith({ message: 'Test error' }, expect.any(String), undefined, 'gemini-2.5-pro', 'gemini-2.5-flash');
1747
+ });
1748
+ });
1749
+ describe('Loop Detection Confirmation', () => {
1750
+ beforeEach(() => {
1751
+ // Add mock for getLoopDetectionService to the config
1752
+ const mockLoopDetectionService = {
1753
+ disableForSession: vi.fn(),
1754
+ };
1755
+ mockConfig.getGeminiClient = vi.fn().mockReturnValue({
1756
+ ...new MockedGeminiClientClass(mockConfig),
1757
+ getLoopDetectionService: () => mockLoopDetectionService,
1758
+ });
1759
+ });
1760
+ it('should set loopDetectionConfirmationRequest when LoopDetected event is received', async () => {
1761
+ mockSendMessageStream.mockReturnValue((async function* () {
1762
+ yield {
1763
+ type: ServerGeminiEventType.Content,
1764
+ value: 'Some content',
1765
+ };
1766
+ yield {
1767
+ type: ServerGeminiEventType.LoopDetected,
1768
+ };
1769
+ })());
1770
+ const { result } = renderTestHook();
1771
+ await act(async () => {
1772
+ await result.current.submitQuery('test query');
1773
+ });
1774
+ await waitFor(() => {
1775
+ expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
1776
+ expect(typeof result.current.loopDetectionConfirmationRequest?.onComplete).toBe('function');
1777
+ });
1778
+ });
1779
+ it('should disable loop detection and show message when user selects "disable"', async () => {
1780
+ const mockLoopDetectionService = {
1781
+ disableForSession: vi.fn(),
1782
+ };
1783
+ const mockClient = {
1784
+ ...new MockedGeminiClientClass(mockConfig),
1785
+ getLoopDetectionService: () => mockLoopDetectionService,
1786
+ };
1787
+ mockConfig.getGeminiClient = vi.fn().mockReturnValue(mockClient);
1788
+ mockSendMessageStream.mockReturnValue((async function* () {
1789
+ yield {
1790
+ type: ServerGeminiEventType.LoopDetected,
1791
+ };
1792
+ })());
1793
+ const { result } = renderTestHook();
1794
+ await act(async () => {
1795
+ await result.current.submitQuery('test query');
1796
+ });
1797
+ // Wait for confirmation request to be set
1798
+ await waitFor(() => {
1799
+ expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
1800
+ });
1801
+ // Simulate user selecting "disable"
1802
+ await act(async () => {
1803
+ result.current.loopDetectionConfirmationRequest?.onComplete({
1804
+ userSelection: 'disable',
1805
+ });
1806
+ });
1807
+ // Verify loop detection was disabled
1808
+ expect(mockLoopDetectionService.disableForSession).toHaveBeenCalledTimes(1);
1809
+ // Verify confirmation request was cleared
1810
+ expect(result.current.loopDetectionConfirmationRequest).toBeNull();
1811
+ // Verify appropriate message was added
1812
+ expect(mockAddItem).toHaveBeenCalledWith({
1813
+ type: 'info',
1814
+ text: 'Loop detection has been disabled for this session. Please try your request again.',
1815
+ }, expect.any(Number));
1816
+ });
1817
+ it('should keep loop detection enabled and show message when user selects "keep"', async () => {
1818
+ const mockLoopDetectionService = {
1819
+ disableForSession: vi.fn(),
1820
+ };
1821
+ const mockClient = {
1822
+ ...new MockedGeminiClientClass(mockConfig),
1823
+ getLoopDetectionService: () => mockLoopDetectionService,
1824
+ };
1825
+ mockConfig.getGeminiClient = vi.fn().mockReturnValue(mockClient);
1826
+ mockSendMessageStream.mockReturnValue((async function* () {
1827
+ yield {
1828
+ type: ServerGeminiEventType.LoopDetected,
1829
+ };
1830
+ })());
1831
+ const { result } = renderTestHook();
1832
+ await act(async () => {
1833
+ await result.current.submitQuery('test query');
1834
+ });
1835
+ // Wait for confirmation request to be set
1836
+ await waitFor(() => {
1837
+ expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
1838
+ });
1839
+ // Simulate user selecting "keep"
1840
+ await act(async () => {
1841
+ result.current.loopDetectionConfirmationRequest?.onComplete({
1842
+ userSelection: 'keep',
1843
+ });
1844
+ });
1845
+ // Verify loop detection was NOT disabled
1846
+ expect(mockLoopDetectionService.disableForSession).not.toHaveBeenCalled();
1847
+ // Verify confirmation request was cleared
1848
+ expect(result.current.loopDetectionConfirmationRequest).toBeNull();
1849
+ // Verify appropriate message was added
1850
+ expect(mockAddItem).toHaveBeenCalledWith({
1851
+ type: 'info',
1852
+ text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
1853
+ }, expect.any(Number));
1854
+ });
1855
+ it('should handle multiple loop detection events properly', async () => {
1856
+ const { result } = renderTestHook();
1857
+ // First loop detection - set up fresh mock for first call
1858
+ mockSendMessageStream.mockReturnValueOnce((async function* () {
1859
+ yield {
1860
+ type: ServerGeminiEventType.LoopDetected,
1861
+ };
1862
+ })());
1863
+ // First loop detection
1864
+ await act(async () => {
1865
+ await result.current.submitQuery('first query');
1866
+ });
1867
+ await waitFor(() => {
1868
+ expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
1869
+ });
1870
+ // Simulate user selecting "keep" for first request
1871
+ await act(async () => {
1872
+ result.current.loopDetectionConfirmationRequest?.onComplete({
1873
+ userSelection: 'keep',
1874
+ });
1875
+ });
1876
+ expect(result.current.loopDetectionConfirmationRequest).toBeNull();
1877
+ // Verify first message was added
1878
+ expect(mockAddItem).toHaveBeenCalledWith({
1879
+ type: 'info',
1880
+ text: 'A potential loop was detected. This can happen due to repetitive tool calls or other model behavior. The request has been halted.',
1881
+ }, expect.any(Number));
1882
+ // Second loop detection - set up fresh mock for second call
1883
+ mockSendMessageStream.mockReturnValueOnce((async function* () {
1884
+ yield {
1885
+ type: ServerGeminiEventType.LoopDetected,
1886
+ };
1887
+ })());
1888
+ // Second loop detection
1889
+ await act(async () => {
1890
+ await result.current.submitQuery('second query');
1891
+ });
1892
+ await waitFor(() => {
1893
+ expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
1894
+ });
1895
+ // Simulate user selecting "disable" for second request
1896
+ await act(async () => {
1897
+ result.current.loopDetectionConfirmationRequest?.onComplete({
1898
+ userSelection: 'disable',
1899
+ });
1900
+ });
1901
+ expect(result.current.loopDetectionConfirmationRequest).toBeNull();
1902
+ // Verify second message was added
1903
+ expect(mockAddItem).toHaveBeenCalledWith({
1904
+ type: 'info',
1905
+ text: 'Loop detection has been disabled for this session. Please try your request again.',
1906
+ }, expect.any(Number));
1907
+ });
1908
+ it('should process LoopDetected event after moving pending history to history', async () => {
1909
+ mockSendMessageStream.mockReturnValue((async function* () {
1910
+ yield {
1911
+ type: ServerGeminiEventType.Content,
1912
+ value: 'Some response content',
1913
+ };
1914
+ yield {
1915
+ type: ServerGeminiEventType.LoopDetected,
1916
+ };
1917
+ })());
1918
+ const { result } = renderTestHook();
1919
+ await act(async () => {
1920
+ await result.current.submitQuery('test query');
1921
+ });
1922
+ // Verify that the content was added to history before the loop detection dialog
1923
+ await waitFor(() => {
1924
+ expect(mockAddItem).toHaveBeenCalledWith(expect.objectContaining({
1925
+ type: 'gemini',
1926
+ text: 'Some response content',
1927
+ }), expect.any(Number));
1928
+ });
1929
+ // Then verify loop detection confirmation request was set
1930
+ await waitFor(() => {
1931
+ expect(result.current.loopDetectionConfirmationRequest).not.toBeNull();
1932
+ });
1933
+ });
1934
+ });
1935
+ });
1936
+ //# sourceMappingURL=useGeminiStream.test.js.map