fss-link 1.0.40 → 1.0.45

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 (428) hide show
  1. package/dist/commands/mcp/add.test.ts +122 -0
  2. package/dist/commands/mcp/add.ts +222 -0
  3. package/dist/commands/mcp/list.test.ts +154 -0
  4. package/dist/commands/mcp/list.ts +139 -0
  5. package/dist/commands/mcp/remove.test.ts +69 -0
  6. package/dist/commands/mcp/remove.ts +60 -0
  7. package/dist/commands/mcp.test.ts +55 -0
  8. package/dist/commands/mcp.ts +27 -0
  9. package/dist/config/apiValidation.test.ts +118 -0
  10. package/dist/config/auth.test.ts +79 -0
  11. package/dist/config/auth.ts +100 -0
  12. package/dist/config/config.integration.test.ts +407 -0
  13. package/dist/config/config.test.ts +1952 -0
  14. package/dist/config/config.ts +690 -0
  15. package/dist/config/database.test.ts +96 -0
  16. package/dist/config/database.ts +752 -0
  17. package/dist/config/extension.test.ts +236 -0
  18. package/dist/config/extension.ts +180 -0
  19. package/dist/config/keyBindings.test.ts +62 -0
  20. package/dist/config/keyBindings.ts +184 -0
  21. package/dist/config/modelManager.ts +275 -0
  22. package/dist/config/providerManager.ts +244 -0
  23. package/dist/config/providerPersistence.test.ts +377 -0
  24. package/dist/config/providerPersistence.ts +105 -0
  25. package/dist/config/sandboxConfig.ts +107 -0
  26. package/dist/config/settings.test.ts +1424 -0
  27. package/dist/config/settings.ts +517 -0
  28. package/dist/config/settingsSchema.test.ts +252 -0
  29. package/dist/config/settingsSchema.ts +728 -0
  30. package/dist/config/trustedFolders.test.ts +208 -0
  31. package/dist/config/trustedFolders.ts +167 -0
  32. package/dist/gemini.test.tsx +252 -0
  33. package/dist/gemini.tsx +357 -0
  34. package/dist/generated/git-commit.ts +10 -0
  35. package/dist/index.ts +21 -0
  36. package/dist/nonInteractiveCli.test.ts +276 -0
  37. package/dist/nonInteractiveCli.ts +143 -0
  38. package/dist/package.json +87 -87
  39. package/dist/patches/is-in-ci.ts +17 -0
  40. package/dist/services/BuiltinCommandLoader.test.ts +127 -0
  41. package/dist/services/BuiltinCommandLoader.ts +95 -0
  42. package/dist/services/CommandService.test.ts +352 -0
  43. package/dist/services/CommandService.ts +103 -0
  44. package/dist/services/FileCommandLoader.test.ts +1002 -0
  45. package/dist/services/FileCommandLoader.ts +289 -0
  46. package/dist/services/McpPromptLoader.ts +231 -0
  47. package/dist/services/SearchEngineConfigProvider.ts +100 -0
  48. package/dist/services/prompt-processors/argumentProcessor.test.ts +41 -0
  49. package/dist/services/prompt-processors/argumentProcessor.ts +23 -0
  50. package/dist/services/prompt-processors/shellProcessor.test.ts +709 -0
  51. package/dist/services/prompt-processors/shellProcessor.ts +248 -0
  52. package/dist/services/prompt-processors/types.ts +44 -0
  53. package/dist/services/types.ts +24 -0
  54. package/dist/src/config/apiValidation.test.d.ts +6 -0
  55. package/dist/src/config/apiValidation.test.js +99 -0
  56. package/dist/src/config/apiValidation.test.js.map +1 -0
  57. package/dist/src/config/database.d.ts +32 -0
  58. package/dist/src/config/database.js +281 -2
  59. package/dist/src/config/database.js.map +1 -1
  60. package/dist/src/config/database.test.d.ts +6 -0
  61. package/dist/src/config/database.test.js +80 -0
  62. package/dist/src/config/database.test.js.map +1 -0
  63. package/dist/src/config/providerManager.d.ts +74 -0
  64. package/dist/src/config/providerManager.js +203 -0
  65. package/dist/src/config/providerManager.js.map +1 -0
  66. package/dist/src/config/providerPersistence.d.ts +75 -0
  67. package/dist/src/config/providerPersistence.js +55 -0
  68. package/dist/src/config/providerPersistence.js.map +1 -0
  69. package/dist/src/config/providerPersistence.test.d.ts +6 -0
  70. package/dist/src/config/providerPersistence.test.js +283 -0
  71. package/dist/src/config/providerPersistence.test.js.map +1 -0
  72. package/dist/src/config/settingsSchema.d.ts +9 -0
  73. package/dist/src/config/settingsSchema.js +9 -0
  74. package/dist/src/config/settingsSchema.js.map +1 -1
  75. package/dist/src/generated/git-commit.d.ts +1 -1
  76. package/dist/src/generated/git-commit.js +1 -1
  77. package/dist/src/services/BuiltinCommandLoader.js +2 -0
  78. package/dist/src/services/BuiltinCommandLoader.js.map +1 -1
  79. package/dist/src/ui/App.js +14 -2
  80. package/dist/src/ui/App.js.map +1 -1
  81. package/dist/src/ui/commands/contextCommand.d.ts +7 -0
  82. package/dist/src/ui/commands/contextCommand.js +115 -0
  83. package/dist/src/ui/commands/contextCommand.js.map +1 -0
  84. package/dist/src/ui/components/ContextUsageDisplay.d.ts +3 -1
  85. package/dist/src/ui/components/ContextUsageDisplay.js +43 -3
  86. package/dist/src/ui/components/ContextUsageDisplay.js.map +1 -1
  87. package/dist/src/ui/components/Footer.d.ts +1 -0
  88. package/dist/src/ui/components/Footer.js +2 -2
  89. package/dist/src/ui/components/Footer.js.map +1 -1
  90. package/dist/src/ui/components/GeminiKeyDialog.d.ts +11 -0
  91. package/dist/src/ui/components/GeminiKeyDialog.js +156 -0
  92. package/dist/src/ui/components/GeminiKeyDialog.js.map +1 -0
  93. package/dist/src/ui/components/OpenAIEndpointDialog.d.ts +19 -0
  94. package/dist/src/ui/components/OpenAIEndpointDialog.js +163 -0
  95. package/dist/src/ui/components/OpenAIEndpointDialog.js.map +1 -0
  96. package/dist/src/ui/components/WelcomeBackDialog.d.ts +36 -0
  97. package/dist/src/ui/components/WelcomeBackDialog.js +109 -0
  98. package/dist/src/ui/components/WelcomeBackDialog.js.map +1 -0
  99. package/dist/src/ui/hooks/useWelcomeBack.d.ts +52 -0
  100. package/dist/src/ui/hooks/useWelcomeBack.js +214 -0
  101. package/dist/src/ui/hooks/useWelcomeBack.js.map +1 -0
  102. package/dist/src/zed-integration/schema.d.ts +1516 -1516
  103. package/dist/test-setup.ts +12 -0
  104. package/dist/test-utils/customMatchers.ts +65 -0
  105. package/dist/test-utils/mockCommandContext.test.ts +62 -0
  106. package/dist/test-utils/mockCommandContext.ts +105 -0
  107. package/dist/test-utils/render.tsx +18 -0
  108. package/dist/tsconfig.tsbuildinfo +1 -1
  109. package/dist/ui/App.test.tsx +2181 -0
  110. package/dist/ui/App.tsx +1344 -0
  111. package/dist/ui/IdeIntegrationNudge.tsx +98 -0
  112. package/dist/ui/__snapshots__/App.test.tsx.snap +124 -0
  113. package/dist/ui/colors.ts +56 -0
  114. package/dist/ui/commands/aboutCommand.test.ts +153 -0
  115. package/dist/ui/commands/aboutCommand.ts +49 -0
  116. package/dist/ui/commands/authCommand.test.ts +36 -0
  117. package/dist/ui/commands/authCommand.ts +17 -0
  118. package/dist/ui/commands/bugCommand.test.ts +114 -0
  119. package/dist/ui/commands/bugCommand.ts +92 -0
  120. package/dist/ui/commands/chatCommand.test.ts +414 -0
  121. package/dist/ui/commands/chatCommand.ts +280 -0
  122. package/dist/ui/commands/clearCommand.test.ts +100 -0
  123. package/dist/ui/commands/clearCommand.ts +29 -0
  124. package/dist/ui/commands/compressCommand.test.ts +129 -0
  125. package/dist/ui/commands/compressCommand.ts +78 -0
  126. package/dist/ui/commands/contextCommand.ts +132 -0
  127. package/dist/ui/commands/copyCommand.test.ts +296 -0
  128. package/dist/ui/commands/copyCommand.ts +67 -0
  129. package/dist/ui/commands/corgiCommand.test.ts +34 -0
  130. package/dist/ui/commands/corgiCommand.ts +16 -0
  131. package/dist/ui/commands/directoryCommand.test.tsx +185 -0
  132. package/dist/ui/commands/directoryCommand.tsx +179 -0
  133. package/dist/ui/commands/docsCommand.test.ts +99 -0
  134. package/dist/ui/commands/docsCommand.ts +42 -0
  135. package/dist/ui/commands/editorCommand.test.ts +30 -0
  136. package/dist/ui/commands/editorCommand.ts +21 -0
  137. package/dist/ui/commands/extensionsCommand.test.ts +67 -0
  138. package/dist/ui/commands/extensionsCommand.ts +46 -0
  139. package/dist/ui/commands/helpCommand.test.ts +52 -0
  140. package/dist/ui/commands/helpCommand.ts +23 -0
  141. package/dist/ui/commands/ideCommand.test.ts +255 -0
  142. package/dist/ui/commands/ideCommand.ts +283 -0
  143. package/dist/ui/commands/initCommand.test.ts +127 -0
  144. package/dist/ui/commands/initCommand.ts +117 -0
  145. package/dist/ui/commands/mcpCommand.test.ts +1057 -0
  146. package/dist/ui/commands/mcpCommand.ts +531 -0
  147. package/dist/ui/commands/memoryCommand.test.ts +344 -0
  148. package/dist/ui/commands/memoryCommand.ts +305 -0
  149. package/dist/ui/commands/privacyCommand.test.ts +38 -0
  150. package/dist/ui/commands/privacyCommand.ts +17 -0
  151. package/dist/ui/commands/quitCommand.test.ts +55 -0
  152. package/dist/ui/commands/quitCommand.ts +36 -0
  153. package/dist/ui/commands/restoreCommand.test.ts +250 -0
  154. package/dist/ui/commands/restoreCommand.ts +157 -0
  155. package/dist/ui/commands/searchEngineSetupCommand.ts +18 -0
  156. package/dist/ui/commands/settingsCommand.test.ts +36 -0
  157. package/dist/ui/commands/settingsCommand.ts +17 -0
  158. package/dist/ui/commands/setupGithubCommand.test.ts +238 -0
  159. package/dist/ui/commands/setupGithubCommand.ts +212 -0
  160. package/dist/ui/commands/speakCommand.ts +175 -0
  161. package/dist/ui/commands/statsCommand.test.ts +78 -0
  162. package/dist/ui/commands/statsCommand.ts +70 -0
  163. package/dist/ui/commands/terminalSetupCommand.test.ts +85 -0
  164. package/dist/ui/commands/terminalSetupCommand.ts +45 -0
  165. package/dist/ui/commands/themeCommand.test.ts +38 -0
  166. package/dist/ui/commands/themeCommand.ts +17 -0
  167. package/dist/ui/commands/toolsCommand.test.ts +105 -0
  168. package/dist/ui/commands/toolsCommand.ts +71 -0
  169. package/dist/ui/commands/ttsCommand.ts +143 -0
  170. package/dist/ui/commands/types.ts +204 -0
  171. package/dist/ui/commands/vimCommand.ts +25 -0
  172. package/dist/ui/commands/voiceCommand.ts +125 -0
  173. package/dist/ui/components/AboutBox.tsx +133 -0
  174. package/dist/ui/components/AsciiArt.ts +54 -0
  175. package/dist/ui/components/AuthDialog.test.tsx +334 -0
  176. package/dist/ui/components/AuthDialog.tsx +289 -0
  177. package/dist/ui/components/AuthInProgress.tsx +62 -0
  178. package/dist/ui/components/AutoAcceptIndicator.tsx +47 -0
  179. package/dist/ui/components/ConsoleSummaryDisplay.tsx +35 -0
  180. package/dist/ui/components/ContextSummaryDisplay.test.tsx +85 -0
  181. package/dist/ui/components/ContextSummaryDisplay.tsx +120 -0
  182. package/dist/ui/components/ContextUsageDisplay.tsx +77 -0
  183. package/dist/ui/components/DebugProfiler.tsx +36 -0
  184. package/dist/ui/components/DetailedMessagesDisplay.tsx +82 -0
  185. package/dist/ui/components/EditorSettingsDialog.tsx +172 -0
  186. package/dist/ui/components/FolderTrustDialog.test.tsx +36 -0
  187. package/dist/ui/components/FolderTrustDialog.tsx +74 -0
  188. package/dist/ui/components/Footer.test.tsx +159 -0
  189. package/dist/ui/components/Footer.tsx +158 -0
  190. package/dist/ui/components/GeminiKeyDialog.tsx +252 -0
  191. package/dist/ui/components/GeminiRespondingSpinner.tsx +34 -0
  192. package/dist/ui/components/Header.test.tsx +44 -0
  193. package/dist/ui/components/Header.tsx +70 -0
  194. package/dist/ui/components/Help.tsx +174 -0
  195. package/dist/ui/components/HistoryItemDisplay.test.tsx +125 -0
  196. package/dist/ui/components/HistoryItemDisplay.tsx +98 -0
  197. package/dist/ui/components/InputPrompt.test.tsx +1467 -0
  198. package/dist/ui/components/InputPrompt.tsx +641 -0
  199. package/dist/ui/components/LMStudioModelPrompt.tsx +215 -0
  200. package/dist/ui/components/LoadingIndicator.test.tsx +296 -0
  201. package/dist/ui/components/LoadingIndicator.tsx +82 -0
  202. package/dist/ui/components/MemoryUsageDisplay.tsx +36 -0
  203. package/dist/ui/components/ModelStatsDisplay.test.tsx +252 -0
  204. package/dist/ui/components/ModelStatsDisplay.tsx +197 -0
  205. package/dist/ui/components/OllamaModelPrompt.tsx +206 -0
  206. package/dist/ui/components/OpenAIEndpointDialog.tsx +261 -0
  207. package/dist/ui/components/OpenAIKeyPrompt.test.tsx +64 -0
  208. package/dist/ui/components/OpenAIKeyPrompt.tsx +197 -0
  209. package/dist/ui/components/PrepareLabel.tsx +48 -0
  210. package/dist/ui/components/SearchEngineConfigDialog.tsx +280 -0
  211. package/dist/ui/components/SessionSummaryDisplay.test.tsx +75 -0
  212. package/dist/ui/components/SessionSummaryDisplay.tsx +18 -0
  213. package/dist/ui/components/SettingsDialog.test.tsx +865 -0
  214. package/dist/ui/components/SettingsDialog.tsx +753 -0
  215. package/dist/ui/components/ShellConfirmationDialog.test.tsx +53 -0
  216. package/dist/ui/components/ShellConfirmationDialog.tsx +103 -0
  217. package/dist/ui/components/ShellModeIndicator.tsx +18 -0
  218. package/dist/ui/components/ShowMoreLines.tsx +40 -0
  219. package/dist/ui/components/StatsDisplay.test.tsx +401 -0
  220. package/dist/ui/components/StatsDisplay.tsx +273 -0
  221. package/dist/ui/components/SuggestionsDisplay.tsx +102 -0
  222. package/dist/ui/components/ThemeDialog.tsx +310 -0
  223. package/dist/ui/components/Tips.tsx +45 -0
  224. package/dist/ui/components/TodoDisplay.test.tsx +97 -0
  225. package/dist/ui/components/TodoDisplay.tsx +72 -0
  226. package/dist/ui/components/ToolStatsDisplay.test.tsx +180 -0
  227. package/dist/ui/components/ToolStatsDisplay.tsx +208 -0
  228. package/dist/ui/components/UpdateNotification.tsx +23 -0
  229. package/dist/ui/components/WelcomeBackDialog.tsx +290 -0
  230. package/dist/ui/components/__snapshots__/IDEContextDetailDisplay.test.tsx.snap +24 -0
  231. package/dist/ui/components/__snapshots__/ModelStatsDisplay.test.tsx.snap +121 -0
  232. package/dist/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap +30 -0
  233. package/dist/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap +21 -0
  234. package/dist/ui/components/__snapshots__/StatsDisplay.test.tsx.snap +264 -0
  235. package/dist/ui/components/__snapshots__/ToolStatsDisplay.test.tsx.snap +91 -0
  236. package/dist/ui/components/messages/CompressionMessage.tsx +49 -0
  237. package/dist/ui/components/messages/DiffRenderer.test.tsx +365 -0
  238. package/dist/ui/components/messages/DiffRenderer.tsx +358 -0
  239. package/dist/ui/components/messages/ErrorMessage.tsx +31 -0
  240. package/dist/ui/components/messages/GeminiMessage.tsx +43 -0
  241. package/dist/ui/components/messages/GeminiMessageContent.tsx +43 -0
  242. package/dist/ui/components/messages/InfoMessage.tsx +32 -0
  243. package/dist/ui/components/messages/ToolConfirmationMessage.test.tsx +58 -0
  244. package/dist/ui/components/messages/ToolConfirmationMessage.tsx +297 -0
  245. package/dist/ui/components/messages/ToolGroupMessage.tsx +126 -0
  246. package/dist/ui/components/messages/ToolMessage.test.tsx +183 -0
  247. package/dist/ui/components/messages/ToolMessage.tsx +296 -0
  248. package/dist/ui/components/messages/UserMessage.tsx +43 -0
  249. package/dist/ui/components/messages/UserShellMessage.tsx +25 -0
  250. package/dist/ui/components/shared/MaxSizedBox.test.tsx +425 -0
  251. package/dist/ui/components/shared/MaxSizedBox.tsx +624 -0
  252. package/dist/ui/components/shared/RadioButtonSelect.test.tsx +181 -0
  253. package/dist/ui/components/shared/RadioButtonSelect.tsx +234 -0
  254. package/dist/ui/components/shared/__snapshots__/RadioButtonSelect.test.tsx.snap +47 -0
  255. package/dist/ui/components/shared/text-buffer.test.ts +1728 -0
  256. package/dist/ui/components/shared/text-buffer.ts +2227 -0
  257. package/dist/ui/components/shared/vim-buffer-actions.test.ts +1119 -0
  258. package/dist/ui/components/shared/vim-buffer-actions.ts +814 -0
  259. package/dist/ui/constants.ts +17 -0
  260. package/dist/ui/contexts/KeypressContext.test.tsx +391 -0
  261. package/dist/ui/contexts/KeypressContext.tsx +440 -0
  262. package/dist/ui/contexts/OverflowContext.tsx +87 -0
  263. package/dist/ui/contexts/SessionContext.test.tsx +132 -0
  264. package/dist/ui/contexts/SessionContext.tsx +143 -0
  265. package/dist/ui/contexts/SettingsContext.tsx +20 -0
  266. package/dist/ui/contexts/StreamingContext.tsx +22 -0
  267. package/dist/ui/contexts/VimModeContext.tsx +79 -0
  268. package/dist/ui/editors/editorSettingsManager.ts +66 -0
  269. package/dist/ui/hooks/atCommandProcessor.test.ts +1102 -0
  270. package/dist/ui/hooks/atCommandProcessor.ts +485 -0
  271. package/dist/ui/hooks/shellCommandProcessor.test.ts +481 -0
  272. package/dist/ui/hooks/shellCommandProcessor.ts +314 -0
  273. package/dist/ui/hooks/slashCommandProcessor.test.ts +1044 -0
  274. package/dist/ui/hooks/slashCommandProcessor.ts +595 -0
  275. package/dist/ui/hooks/useAtCompletion.test.ts +497 -0
  276. package/dist/ui/hooks/useAtCompletion.ts +244 -0
  277. package/dist/ui/hooks/useAuthCommand.ts +129 -0
  278. package/dist/ui/hooks/useAutoAcceptIndicator.test.ts +300 -0
  279. package/dist/ui/hooks/useAutoAcceptIndicator.ts +52 -0
  280. package/dist/ui/hooks/useBracketedPaste.ts +37 -0
  281. package/dist/ui/hooks/useCommandCompletion.test.ts +518 -0
  282. package/dist/ui/hooks/useCommandCompletion.tsx +238 -0
  283. package/dist/ui/hooks/useCompletion.ts +128 -0
  284. package/dist/ui/hooks/useConsoleMessages.test.ts +147 -0
  285. package/dist/ui/hooks/useConsoleMessages.ts +110 -0
  286. package/dist/ui/hooks/useEditorSettings.test.ts +283 -0
  287. package/dist/ui/hooks/useEditorSettings.ts +75 -0
  288. package/dist/ui/hooks/useFocus.test.ts +119 -0
  289. package/dist/ui/hooks/useFocus.ts +48 -0
  290. package/dist/ui/hooks/useFolderTrust.test.ts +159 -0
  291. package/dist/ui/hooks/useFolderTrust.ts +72 -0
  292. package/dist/ui/hooks/useGeminiStream.test.tsx +1998 -0
  293. package/dist/ui/hooks/useGeminiStream.ts +1017 -0
  294. package/dist/ui/hooks/useGitBranchName.test.ts +280 -0
  295. package/dist/ui/hooks/useGitBranchName.ts +79 -0
  296. package/dist/ui/hooks/useHistoryManager.test.ts +202 -0
  297. package/dist/ui/hooks/useHistoryManager.ts +111 -0
  298. package/dist/ui/hooks/useInputHistory.test.ts +261 -0
  299. package/dist/ui/hooks/useInputHistory.ts +111 -0
  300. package/dist/ui/hooks/useKeypress.test.ts +280 -0
  301. package/dist/ui/hooks/useKeypress.ts +39 -0
  302. package/dist/ui/hooks/useKittyKeyboardProtocol.ts +31 -0
  303. package/dist/ui/hooks/useLoadingIndicator.test.ts +139 -0
  304. package/dist/ui/hooks/useLoadingIndicator.ts +57 -0
  305. package/dist/ui/hooks/useLogger.ts +32 -0
  306. package/dist/ui/hooks/useMessageQueue.test.ts +226 -0
  307. package/dist/ui/hooks/useMessageQueue.ts +69 -0
  308. package/dist/ui/hooks/usePhraseCycler.test.ts +145 -0
  309. package/dist/ui/hooks/usePhraseCycler.ts +198 -0
  310. package/dist/ui/hooks/usePrivacySettings.test.ts +242 -0
  311. package/dist/ui/hooks/usePrivacySettings.ts +150 -0
  312. package/dist/ui/hooks/useReactToolScheduler.ts +309 -0
  313. package/dist/ui/hooks/useRefreshMemoryCommand.ts +7 -0
  314. package/dist/ui/hooks/useReverseSearchCompletion.test.tsx +260 -0
  315. package/dist/ui/hooks/useReverseSearchCompletion.tsx +95 -0
  316. package/dist/ui/hooks/useSettingsCommand.ts +25 -0
  317. package/dist/ui/hooks/useShellHistory.test.ts +219 -0
  318. package/dist/ui/hooks/useShellHistory.ts +133 -0
  319. package/dist/ui/hooks/useShowMemoryCommand.ts +75 -0
  320. package/dist/ui/hooks/useSlashCompletion.test.ts +434 -0
  321. package/dist/ui/hooks/useSlashCompletion.ts +187 -0
  322. package/dist/ui/hooks/useStateAndRef.ts +36 -0
  323. package/dist/ui/hooks/useTerminalSize.ts +32 -0
  324. package/dist/ui/hooks/useThemeCommand.ts +110 -0
  325. package/dist/ui/hooks/useTimer.test.ts +120 -0
  326. package/dist/ui/hooks/useTimer.ts +65 -0
  327. package/dist/ui/hooks/useToolScheduler.test.ts +1123 -0
  328. package/dist/ui/hooks/useWelcomeBack.ts +253 -0
  329. package/dist/ui/hooks/vim.test.ts +1691 -0
  330. package/dist/ui/hooks/vim.ts +784 -0
  331. package/dist/ui/keyMatchers.test.ts +337 -0
  332. package/dist/ui/keyMatchers.ts +105 -0
  333. package/dist/ui/privacy/CloudFreePrivacyNotice.tsx +117 -0
  334. package/dist/ui/privacy/CloudPaidPrivacyNotice.tsx +59 -0
  335. package/dist/ui/privacy/GeminiPrivacyNotice.tsx +62 -0
  336. package/dist/ui/privacy/PrivacyNotice.tsx +42 -0
  337. package/dist/ui/semantic-colors.ts +26 -0
  338. package/dist/ui/themes/ansi-light.ts +150 -0
  339. package/dist/ui/themes/ansi.ts +159 -0
  340. package/dist/ui/themes/atom-one-dark.ts +147 -0
  341. package/dist/ui/themes/ayu-light.ts +139 -0
  342. package/dist/ui/themes/ayu.ts +113 -0
  343. package/dist/ui/themes/color-utils.test.ts +221 -0
  344. package/dist/ui/themes/color-utils.ts +231 -0
  345. package/dist/ui/themes/default-light.ts +108 -0
  346. package/dist/ui/themes/default.ts +151 -0
  347. package/dist/ui/themes/dracula.ts +124 -0
  348. package/dist/ui/themes/fss-code-dark.ts +156 -0
  349. package/dist/ui/themes/fss-dark.ts +113 -0
  350. package/dist/ui/themes/fss-light.ts +139 -0
  351. package/dist/ui/themes/github-dark.ts +147 -0
  352. package/dist/ui/themes/github-light.ts +149 -0
  353. package/dist/ui/themes/googlecode.ts +146 -0
  354. package/dist/ui/themes/no-color.ts +125 -0
  355. package/dist/ui/themes/qwen-dark.ts +118 -0
  356. package/dist/ui/themes/qwen-light.ts +144 -0
  357. package/dist/ui/themes/semantic-tokens.ts +127 -0
  358. package/dist/ui/themes/shades-of-purple.ts +352 -0
  359. package/dist/ui/themes/theme-manager.test.ts +99 -0
  360. package/dist/ui/themes/theme-manager.ts +257 -0
  361. package/dist/ui/themes/theme.test.ts +97 -0
  362. package/dist/ui/themes/theme.ts +451 -0
  363. package/dist/ui/themes/xcode.ts +154 -0
  364. package/dist/ui/types.ts +255 -0
  365. package/dist/ui/utils/CodeColorizer.tsx +217 -0
  366. package/dist/ui/utils/ConsolePatcher.ts +71 -0
  367. package/dist/ui/utils/InlineMarkdownRenderer.tsx +173 -0
  368. package/dist/ui/utils/MarkdownDisplay.test.tsx +244 -0
  369. package/dist/ui/utils/MarkdownDisplay.tsx +415 -0
  370. package/dist/ui/utils/TableRenderer.tsx +159 -0
  371. package/dist/ui/utils/__snapshots__/MarkdownDisplay.test.tsx.snap +93 -0
  372. package/dist/ui/utils/clipboardUtils.test.ts +76 -0
  373. package/dist/ui/utils/clipboardUtils.ts +149 -0
  374. package/dist/ui/utils/commandUtils.test.ts +384 -0
  375. package/dist/ui/utils/commandUtils.ts +106 -0
  376. package/dist/ui/utils/computeStats.test.ts +292 -0
  377. package/dist/ui/utils/computeStats.ts +86 -0
  378. package/dist/ui/utils/displayUtils.test.ts +58 -0
  379. package/dist/ui/utils/displayUtils.ts +32 -0
  380. package/dist/ui/utils/formatters.test.ts +72 -0
  381. package/dist/ui/utils/formatters.ts +63 -0
  382. package/dist/ui/utils/isNarrowWidth.ts +9 -0
  383. package/dist/ui/utils/kittyProtocolDetector.ts +105 -0
  384. package/dist/ui/utils/markdownUtilities.test.ts +50 -0
  385. package/dist/ui/utils/markdownUtilities.ts +125 -0
  386. package/dist/ui/utils/platformConstants.ts +52 -0
  387. package/dist/ui/utils/terminalSetup.ts +342 -0
  388. package/dist/ui/utils/textUtils.ts +40 -0
  389. package/dist/ui/utils/updateCheck.test.ts +163 -0
  390. package/dist/ui/utils/updateCheck.ts +100 -0
  391. package/dist/utils/checks.ts +28 -0
  392. package/dist/utils/cleanup.test.ts +68 -0
  393. package/dist/utils/cleanup.ts +36 -0
  394. package/dist/utils/dialogScopeUtils.ts +64 -0
  395. package/dist/utils/events.ts +14 -0
  396. package/dist/utils/gitUtils.test.ts +149 -0
  397. package/dist/utils/gitUtils.ts +116 -0
  398. package/dist/utils/handleAutoUpdate.test.ts +272 -0
  399. package/dist/utils/handleAutoUpdate.ts +145 -0
  400. package/dist/utils/installationInfo.test.ts +315 -0
  401. package/dist/utils/installationInfo.ts +176 -0
  402. package/dist/utils/package.ts +38 -0
  403. package/dist/utils/readStdin.ts +51 -0
  404. package/dist/utils/resolvePath.ts +21 -0
  405. package/dist/utils/sandbox-macos-permissive-closed.sb +32 -0
  406. package/dist/utils/sandbox-macos-permissive-open.sb +25 -0
  407. package/dist/utils/sandbox-macos-permissive-proxied.sb +37 -0
  408. package/dist/utils/sandbox-macos-restrictive-closed.sb +93 -0
  409. package/dist/utils/sandbox-macos-restrictive-open.sb +96 -0
  410. package/dist/utils/sandbox-macos-restrictive-proxied.sb +98 -0
  411. package/dist/utils/sandbox.ts +962 -0
  412. package/dist/utils/settingsUtils.test.ts +797 -0
  413. package/dist/utils/settingsUtils.ts +489 -0
  414. package/dist/utils/spawnWrapper.ts +9 -0
  415. package/dist/utils/startupWarnings.test.ts +83 -0
  416. package/dist/utils/startupWarnings.ts +40 -0
  417. package/dist/utils/updateEventEmitter.ts +13 -0
  418. package/dist/utils/userStartupWarnings.test.ts +87 -0
  419. package/dist/utils/userStartupWarnings.ts +69 -0
  420. package/dist/utils/version.ts +12 -0
  421. package/dist/validateNonInterActiveAuth.test.ts +260 -0
  422. package/dist/validateNonInterActiveAuth.ts +51 -0
  423. package/dist/vitest.config.ts +37 -0
  424. package/dist/zed-integration/acp.ts +366 -0
  425. package/dist/zed-integration/fileSystemService.ts +47 -0
  426. package/dist/zed-integration/schema.ts +466 -0
  427. package/dist/zed-integration/zedIntegration.ts +944 -0
  428. package/package.json +2 -2
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { act, renderHook } from '@testing-library/react';
8
+ import { useInputHistory } from './useInputHistory.js';
9
+
10
+ describe('useInputHistory', () => {
11
+ const mockOnSubmit = vi.fn();
12
+ const mockOnChange = vi.fn();
13
+
14
+ beforeEach(() => {
15
+ vi.clearAllMocks();
16
+ });
17
+
18
+ const userMessages = ['message 1', 'message 2', 'message 3'];
19
+
20
+ it('should initialize with historyIndex -1 and empty originalQueryBeforeNav', () => {
21
+ const { result } = renderHook(() =>
22
+ useInputHistory({
23
+ userMessages: [],
24
+ onSubmit: mockOnSubmit,
25
+ isActive: true,
26
+ currentQuery: '',
27
+ onChange: mockOnChange,
28
+ }),
29
+ );
30
+
31
+ // Internal state is not directly testable, but we can infer from behavior.
32
+ // Attempting to navigate down should do nothing if historyIndex is -1.
33
+ act(() => {
34
+ result.current.navigateDown();
35
+ });
36
+ expect(mockOnChange).not.toHaveBeenCalled();
37
+ });
38
+
39
+ describe('handleSubmit', () => {
40
+ it('should call onSubmit with trimmed value and reset history', () => {
41
+ const { result } = renderHook(() =>
42
+ useInputHistory({
43
+ userMessages,
44
+ onSubmit: mockOnSubmit,
45
+ isActive: true,
46
+ currentQuery: ' test query ',
47
+ onChange: mockOnChange,
48
+ }),
49
+ );
50
+
51
+ act(() => {
52
+ result.current.handleSubmit(' submit value ');
53
+ });
54
+
55
+ expect(mockOnSubmit).toHaveBeenCalledWith('submit value');
56
+ // Check if history is reset (e.g., by trying to navigate down)
57
+ act(() => {
58
+ result.current.navigateDown();
59
+ });
60
+ expect(mockOnChange).not.toHaveBeenCalled();
61
+ });
62
+
63
+ it('should not call onSubmit if value is empty after trimming', () => {
64
+ const { result } = renderHook(() =>
65
+ useInputHistory({
66
+ userMessages,
67
+ onSubmit: mockOnSubmit,
68
+ isActive: true,
69
+ currentQuery: '',
70
+ onChange: mockOnChange,
71
+ }),
72
+ );
73
+
74
+ act(() => {
75
+ result.current.handleSubmit(' ');
76
+ });
77
+
78
+ expect(mockOnSubmit).not.toHaveBeenCalled();
79
+ });
80
+ });
81
+
82
+ describe('navigateUp', () => {
83
+ it('should not navigate if isActive is false', () => {
84
+ const { result } = renderHook(() =>
85
+ useInputHistory({
86
+ userMessages,
87
+ onSubmit: mockOnSubmit,
88
+ isActive: false,
89
+ currentQuery: 'current',
90
+ onChange: mockOnChange,
91
+ }),
92
+ );
93
+ act(() => {
94
+ const navigated = result.current.navigateUp();
95
+ expect(navigated).toBe(false);
96
+ });
97
+ expect(mockOnChange).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('should not navigate if userMessages is empty', () => {
101
+ const { result } = renderHook(() =>
102
+ useInputHistory({
103
+ userMessages: [],
104
+ onSubmit: mockOnSubmit,
105
+ isActive: true,
106
+ currentQuery: 'current',
107
+ onChange: mockOnChange,
108
+ }),
109
+ );
110
+ act(() => {
111
+ const navigated = result.current.navigateUp();
112
+ expect(navigated).toBe(false);
113
+ });
114
+ expect(mockOnChange).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('should call onChange with the last message when navigating up from initial state', () => {
118
+ const currentQuery = 'current query';
119
+ const { result } = renderHook(() =>
120
+ useInputHistory({
121
+ userMessages,
122
+ onSubmit: mockOnSubmit,
123
+ isActive: true,
124
+ currentQuery,
125
+ onChange: mockOnChange,
126
+ }),
127
+ );
128
+
129
+ act(() => {
130
+ result.current.navigateUp();
131
+ });
132
+
133
+ expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]); // Last message
134
+ });
135
+
136
+ it('should store currentQuery as originalQueryBeforeNav on first navigateUp', () => {
137
+ const currentQuery = 'original user input';
138
+ const { result } = renderHook(() =>
139
+ useInputHistory({
140
+ userMessages,
141
+ onSubmit: mockOnSubmit,
142
+ isActive: true,
143
+ currentQuery,
144
+ onChange: mockOnChange,
145
+ }),
146
+ );
147
+
148
+ act(() => {
149
+ result.current.navigateUp(); // historyIndex becomes 0
150
+ });
151
+ expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]);
152
+
153
+ // Navigate down to restore original query
154
+ act(() => {
155
+ result.current.navigateDown(); // historyIndex becomes -1
156
+ });
157
+ expect(mockOnChange).toHaveBeenCalledWith(currentQuery);
158
+ });
159
+
160
+ it('should navigate through history messages on subsequent navigateUp calls', () => {
161
+ const { result } = renderHook(() =>
162
+ useInputHistory({
163
+ userMessages,
164
+ onSubmit: mockOnSubmit,
165
+ isActive: true,
166
+ currentQuery: '',
167
+ onChange: mockOnChange,
168
+ }),
169
+ );
170
+
171
+ act(() => {
172
+ result.current.navigateUp(); // Navigates to 'message 3'
173
+ });
174
+ expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]);
175
+
176
+ act(() => {
177
+ result.current.navigateUp(); // Navigates to 'message 2'
178
+ });
179
+ expect(mockOnChange).toHaveBeenCalledWith(userMessages[1]);
180
+
181
+ act(() => {
182
+ result.current.navigateUp(); // Navigates to 'message 1'
183
+ });
184
+ expect(mockOnChange).toHaveBeenCalledWith(userMessages[0]);
185
+ });
186
+ });
187
+
188
+ describe('navigateDown', () => {
189
+ it('should not navigate if isActive is false', () => {
190
+ const initialProps = {
191
+ userMessages,
192
+ onSubmit: mockOnSubmit,
193
+ isActive: true, // Start active to allow setup navigation
194
+ currentQuery: 'current',
195
+ onChange: mockOnChange,
196
+ };
197
+ const { result, rerender } = renderHook(
198
+ (props) => useInputHistory(props),
199
+ {
200
+ initialProps,
201
+ },
202
+ );
203
+
204
+ // First navigate up to have something in history
205
+ act(() => {
206
+ result.current.navigateUp();
207
+ });
208
+ mockOnChange.mockClear(); // Clear calls from setup
209
+
210
+ // Set isActive to false for the actual test
211
+ rerender({ ...initialProps, isActive: false });
212
+
213
+ act(() => {
214
+ const navigated = result.current.navigateDown();
215
+ expect(navigated).toBe(false);
216
+ });
217
+ expect(mockOnChange).not.toHaveBeenCalled();
218
+ });
219
+
220
+ it('should not navigate if historyIndex is -1 (not in history navigation)', () => {
221
+ const { result } = renderHook(() =>
222
+ useInputHistory({
223
+ userMessages,
224
+ onSubmit: mockOnSubmit,
225
+ isActive: true,
226
+ currentQuery: 'current',
227
+ onChange: mockOnChange,
228
+ }),
229
+ );
230
+ act(() => {
231
+ const navigated = result.current.navigateDown();
232
+ expect(navigated).toBe(false);
233
+ });
234
+ expect(mockOnChange).not.toHaveBeenCalled();
235
+ });
236
+
237
+ it('should restore originalQueryBeforeNav when navigating down to initial state', () => {
238
+ const originalQuery = 'my original input';
239
+ const { result } = renderHook(() =>
240
+ useInputHistory({
241
+ userMessages,
242
+ onSubmit: mockOnSubmit,
243
+ isActive: true,
244
+ currentQuery: originalQuery,
245
+ onChange: mockOnChange,
246
+ }),
247
+ );
248
+
249
+ act(() => {
250
+ result.current.navigateUp(); // Navigates to 'message 3', stores 'originalQuery'
251
+ });
252
+ expect(mockOnChange).toHaveBeenCalledWith(userMessages[2]);
253
+ mockOnChange.mockClear();
254
+
255
+ act(() => {
256
+ result.current.navigateDown(); // Navigates back to original query
257
+ });
258
+ expect(mockOnChange).toHaveBeenCalledWith(originalQuery);
259
+ });
260
+ });
261
+ });
@@ -0,0 +1,111 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { useState, useCallback } from 'react';
8
+
9
+ interface UseInputHistoryProps {
10
+ userMessages: readonly string[];
11
+ onSubmit: (value: string) => void;
12
+ isActive: boolean;
13
+ currentQuery: string; // Renamed from query to avoid confusion
14
+ onChange: (value: string) => void;
15
+ }
16
+
17
+ export interface UseInputHistoryReturn {
18
+ handleSubmit: (value: string) => void;
19
+ navigateUp: () => boolean;
20
+ navigateDown: () => boolean;
21
+ }
22
+
23
+ export function useInputHistory({
24
+ userMessages,
25
+ onSubmit,
26
+ isActive,
27
+ currentQuery,
28
+ onChange,
29
+ }: UseInputHistoryProps): UseInputHistoryReturn {
30
+ const [historyIndex, setHistoryIndex] = useState<number>(-1);
31
+ const [originalQueryBeforeNav, setOriginalQueryBeforeNav] =
32
+ useState<string>('');
33
+
34
+ const resetHistoryNav = useCallback(() => {
35
+ setHistoryIndex(-1);
36
+ setOriginalQueryBeforeNav('');
37
+ }, []);
38
+
39
+ const handleSubmit = useCallback(
40
+ (value: string) => {
41
+ const trimmedValue = value.trim();
42
+ if (trimmedValue) {
43
+ onSubmit(trimmedValue); // Parent handles clearing the query
44
+ }
45
+ resetHistoryNav();
46
+ },
47
+ [onSubmit, resetHistoryNav],
48
+ );
49
+
50
+ const navigateUp = useCallback(() => {
51
+ if (!isActive) return false;
52
+ if (userMessages.length === 0) return false;
53
+
54
+ let nextIndex = historyIndex;
55
+ if (historyIndex === -1) {
56
+ // Store the current query from the parent before navigating
57
+ setOriginalQueryBeforeNav(currentQuery);
58
+ nextIndex = 0;
59
+ } else if (historyIndex < userMessages.length - 1) {
60
+ nextIndex = historyIndex + 1;
61
+ } else {
62
+ return false; // Already at the oldest message
63
+ }
64
+
65
+ if (nextIndex !== historyIndex) {
66
+ setHistoryIndex(nextIndex);
67
+ const newValue = userMessages[userMessages.length - 1 - nextIndex];
68
+ onChange(newValue);
69
+ return true;
70
+ }
71
+ return false;
72
+ }, [
73
+ historyIndex,
74
+ setHistoryIndex,
75
+ onChange,
76
+ userMessages,
77
+ isActive,
78
+ currentQuery, // Use currentQuery from props
79
+ setOriginalQueryBeforeNav,
80
+ ]);
81
+
82
+ const navigateDown = useCallback(() => {
83
+ if (!isActive) return false;
84
+ if (historyIndex === -1) return false; // Not currently navigating history
85
+
86
+ const nextIndex = historyIndex - 1;
87
+ setHistoryIndex(nextIndex);
88
+
89
+ if (nextIndex === -1) {
90
+ // Reached the end of history navigation, restore original query
91
+ onChange(originalQueryBeforeNav);
92
+ } else {
93
+ const newValue = userMessages[userMessages.length - 1 - nextIndex];
94
+ onChange(newValue);
95
+ }
96
+ return true;
97
+ }, [
98
+ historyIndex,
99
+ setHistoryIndex,
100
+ originalQueryBeforeNav,
101
+ onChange,
102
+ userMessages,
103
+ isActive,
104
+ ]);
105
+
106
+ return {
107
+ handleSubmit,
108
+ navigateUp,
109
+ navigateDown,
110
+ };
111
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import React from 'react';
8
+ import { renderHook, act } from '@testing-library/react';
9
+ import { useKeypress, Key } from './useKeypress.js';
10
+ import { KeypressProvider } from '../contexts/KeypressContext.js';
11
+ import { useStdin } from 'ink';
12
+ import { EventEmitter } from 'events';
13
+ import { PassThrough } from 'stream';
14
+
15
+ // Mock the 'ink' module to control stdin
16
+ vi.mock('ink', async (importOriginal) => {
17
+ const original = await importOriginal<typeof import('ink')>();
18
+ return {
19
+ ...original,
20
+ useStdin: vi.fn(),
21
+ };
22
+ });
23
+
24
+ // Mock the 'readline' module
25
+ vi.mock('readline', () => {
26
+ const mockedReadline = {
27
+ createInterface: vi.fn().mockReturnValue({ close: vi.fn() }),
28
+ // The paste workaround involves replacing stdin with a PassThrough stream.
29
+ // This mock ensures that when emitKeypressEvents is called on that
30
+ // stream, we simulate the 'keypress' events that the hook expects.
31
+ emitKeypressEvents: vi.fn((stream: EventEmitter) => {
32
+ if (stream instanceof PassThrough) {
33
+ stream.on('data', (data) => {
34
+ const str = data.toString();
35
+ for (const char of str) {
36
+ stream.emit('keypress', null, {
37
+ name: char,
38
+ sequence: char,
39
+ ctrl: false,
40
+ meta: false,
41
+ shift: false,
42
+ });
43
+ }
44
+ });
45
+ }
46
+ }),
47
+ };
48
+ return {
49
+ ...mockedReadline,
50
+ default: mockedReadline,
51
+ };
52
+ });
53
+
54
+ class MockStdin extends EventEmitter {
55
+ isTTY = true;
56
+ setRawMode = vi.fn();
57
+ on = this.addListener;
58
+ removeListener = this.removeListener;
59
+ write = vi.fn();
60
+ resume = vi.fn();
61
+
62
+ private isLegacy = false;
63
+
64
+ setLegacy(isLegacy: boolean) {
65
+ this.isLegacy = isLegacy;
66
+ }
67
+
68
+ // Helper to simulate a full paste event.
69
+ paste(text: string) {
70
+ if (this.isLegacy) {
71
+ const PASTE_START = '\x1B[200~';
72
+ const PASTE_END = '\x1B[201~';
73
+ this.emit('data', Buffer.from(`${PASTE_START}${text}${PASTE_END}`));
74
+ } else {
75
+ this.emit('keypress', null, { name: 'paste-start' });
76
+ this.emit('keypress', null, { sequence: text });
77
+ this.emit('keypress', null, { name: 'paste-end' });
78
+ }
79
+ }
80
+
81
+ // Helper to simulate the start of a paste, without the end.
82
+ startPaste(text: string) {
83
+ if (this.isLegacy) {
84
+ this.emit('data', Buffer.from('\x1B[200~' + text));
85
+ } else {
86
+ this.emit('keypress', null, { name: 'paste-start' });
87
+ this.emit('keypress', null, { sequence: text });
88
+ }
89
+ }
90
+
91
+ // Helper to simulate a single keypress event.
92
+ pressKey(key: Partial<Key>) {
93
+ if (this.isLegacy) {
94
+ this.emit('data', Buffer.from(key.sequence ?? ''));
95
+ } else {
96
+ this.emit('keypress', null, key);
97
+ }
98
+ }
99
+ }
100
+
101
+ describe('useKeypress', () => {
102
+ let stdin: MockStdin;
103
+ const mockSetRawMode = vi.fn();
104
+ const onKeypress = vi.fn();
105
+ let originalNodeVersion: string;
106
+
107
+ const wrapper = ({ children }: { children: React.ReactNode }) =>
108
+ React.createElement(KeypressProvider, null, children);
109
+
110
+ beforeEach(() => {
111
+ vi.clearAllMocks();
112
+ stdin = new MockStdin();
113
+ (useStdin as vi.Mock).mockReturnValue({
114
+ stdin,
115
+ setRawMode: mockSetRawMode,
116
+ });
117
+
118
+ originalNodeVersion = process.versions.node;
119
+ vi.unstubAllEnvs();
120
+ });
121
+
122
+ afterEach(() => {
123
+ Object.defineProperty(process.versions, 'node', {
124
+ value: originalNodeVersion,
125
+ configurable: true,
126
+ });
127
+ });
128
+
129
+ const setNodeVersion = (version: string) => {
130
+ Object.defineProperty(process.versions, 'node', {
131
+ value: version,
132
+ configurable: true,
133
+ });
134
+ };
135
+
136
+ it('should not listen if isActive is false', () => {
137
+ renderHook(() => useKeypress(onKeypress, { isActive: false }), {
138
+ wrapper,
139
+ });
140
+ act(() => stdin.pressKey({ name: 'a' }));
141
+ expect(onKeypress).not.toHaveBeenCalled();
142
+ });
143
+
144
+ it.each([
145
+ { key: { name: 'a', sequence: 'a' } },
146
+ { key: { name: 'left', sequence: '\x1b[D' } },
147
+ { key: { name: 'right', sequence: '\x1b[C' } },
148
+ { key: { name: 'up', sequence: '\x1b[A' } },
149
+ { key: { name: 'down', sequence: '\x1b[B' } },
150
+ ])('should listen for keypress when active for key $key.name', ({ key }) => {
151
+ renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
152
+ act(() => stdin.pressKey(key));
153
+ expect(onKeypress).toHaveBeenCalledWith(expect.objectContaining(key));
154
+ });
155
+
156
+ it('should set and release raw mode', () => {
157
+ const { unmount } = renderHook(
158
+ () => useKeypress(onKeypress, { isActive: true }),
159
+ { wrapper },
160
+ );
161
+ expect(mockSetRawMode).toHaveBeenCalledWith(true);
162
+ unmount();
163
+ expect(mockSetRawMode).toHaveBeenCalledWith(false);
164
+ });
165
+
166
+ it('should stop listening after being unmounted', () => {
167
+ const { unmount } = renderHook(
168
+ () => useKeypress(onKeypress, { isActive: true }),
169
+ { wrapper },
170
+ );
171
+ unmount();
172
+ act(() => stdin.pressKey({ name: 'a' }));
173
+ expect(onKeypress).not.toHaveBeenCalled();
174
+ });
175
+
176
+ it('should correctly identify alt+enter (meta key)', () => {
177
+ renderHook(() => useKeypress(onKeypress, { isActive: true }), { wrapper });
178
+ const key = { name: 'return', sequence: '\x1B\r' };
179
+ act(() => stdin.pressKey(key));
180
+ expect(onKeypress).toHaveBeenCalledWith(
181
+ expect.objectContaining({ ...key, meta: true, paste: false }),
182
+ );
183
+ });
184
+
185
+ describe.each([
186
+ {
187
+ description: 'Modern Node (>= v20)',
188
+ setup: () => setNodeVersion('20.0.0'),
189
+ isLegacy: false,
190
+ },
191
+ {
192
+ description: 'Legacy Node (< v20)',
193
+ setup: () => setNodeVersion('18.0.0'),
194
+ isLegacy: true,
195
+ },
196
+ {
197
+ description: 'Workaround Env Var',
198
+ setup: () => {
199
+ setNodeVersion('20.0.0');
200
+ vi.stubEnv('PASTE_WORKAROUND', 'true');
201
+ },
202
+ isLegacy: true,
203
+ },
204
+ ])('in $description', ({ setup, isLegacy }) => {
205
+ beforeEach(() => {
206
+ setup();
207
+ stdin.setLegacy(isLegacy);
208
+ });
209
+
210
+ it('should process a paste as a single event', () => {
211
+ renderHook(() => useKeypress(onKeypress, { isActive: true }), {
212
+ wrapper,
213
+ });
214
+ const pasteText = 'hello world';
215
+ act(() => stdin.paste(pasteText));
216
+
217
+ expect(onKeypress).toHaveBeenCalledTimes(1);
218
+ expect(onKeypress).toHaveBeenCalledWith({
219
+ name: '',
220
+ ctrl: false,
221
+ meta: false,
222
+ shift: false,
223
+ paste: true,
224
+ sequence: pasteText,
225
+ });
226
+ });
227
+
228
+ it('should handle keypress interspersed with pastes', () => {
229
+ renderHook(() => useKeypress(onKeypress, { isActive: true }), {
230
+ wrapper,
231
+ });
232
+
233
+ const keyA = { name: 'a', sequence: 'a' };
234
+ act(() => stdin.pressKey(keyA));
235
+ expect(onKeypress).toHaveBeenCalledWith(
236
+ expect.objectContaining({ ...keyA, paste: false }),
237
+ );
238
+
239
+ const pasteText = 'pasted';
240
+ act(() => stdin.paste(pasteText));
241
+ expect(onKeypress).toHaveBeenCalledWith(
242
+ expect.objectContaining({ paste: true, sequence: pasteText }),
243
+ );
244
+
245
+ const keyB = { name: 'b', sequence: 'b' };
246
+ act(() => stdin.pressKey(keyB));
247
+ expect(onKeypress).toHaveBeenCalledWith(
248
+ expect.objectContaining({ ...keyB, paste: false }),
249
+ );
250
+
251
+ expect(onKeypress).toHaveBeenCalledTimes(3);
252
+ });
253
+
254
+ it('should emit partial paste content if unmounted mid-paste', () => {
255
+ const { unmount } = renderHook(
256
+ () => useKeypress(onKeypress, { isActive: true }),
257
+ { wrapper },
258
+ );
259
+ const pasteText = 'incomplete paste';
260
+
261
+ act(() => stdin.startPaste(pasteText));
262
+
263
+ // No event should be fired yet.
264
+ expect(onKeypress).not.toHaveBeenCalled();
265
+
266
+ // Unmounting should trigger the flush.
267
+ unmount();
268
+
269
+ expect(onKeypress).toHaveBeenCalledTimes(1);
270
+ expect(onKeypress).toHaveBeenCalledWith({
271
+ name: '',
272
+ ctrl: false,
273
+ meta: false,
274
+ shift: false,
275
+ paste: true,
276
+ sequence: pasteText,
277
+ });
278
+ });
279
+ });
280
+ });
@@ -0,0 +1,39 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { useEffect } from 'react';
8
+ import {
9
+ useKeypressContext,
10
+ KeypressHandler,
11
+ Key,
12
+ } from '../contexts/KeypressContext.js';
13
+
14
+ export { Key };
15
+
16
+ /**
17
+ * A hook that listens for keypress events from stdin.
18
+ *
19
+ * @param onKeypress - The callback function to execute on each keypress.
20
+ * @param options - Options to control the hook's behavior.
21
+ * @param options.isActive - Whether the hook should be actively listening for input.
22
+ */
23
+ export function useKeypress(
24
+ onKeypress: KeypressHandler,
25
+ { isActive }: { isActive: boolean },
26
+ ) {
27
+ const { subscribe, unsubscribe } = useKeypressContext();
28
+
29
+ useEffect(() => {
30
+ if (!isActive) {
31
+ return;
32
+ }
33
+
34
+ subscribe(onKeypress);
35
+ return () => {
36
+ unsubscribe(onKeypress);
37
+ };
38
+ }, [isActive, onKeypress, subscribe, unsubscribe]);
39
+ }