@google/gemini-cli 0.12.0-nightly.20251023.a7faa208 → 0.12.0-nightly.20251027.cb0947c5

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 (233) hide show
  1. package/dist/package.json +5 -4
  2. package/dist/src/commands/extensions/disable.js +13 -6
  3. package/dist/src/commands/extensions/disable.js.map +1 -1
  4. package/dist/src/commands/extensions/enable.js +13 -6
  5. package/dist/src/commands/extensions/enable.js.map +1 -1
  6. package/dist/src/commands/extensions/install.js +12 -2
  7. package/dist/src/commands/extensions/install.js.map +1 -1
  8. package/dist/src/commands/extensions/install.test.js +11 -3
  9. package/dist/src/commands/extensions/install.test.js.map +1 -1
  10. package/dist/src/commands/extensions/link.js +12 -2
  11. package/dist/src/commands/extensions/link.js.map +1 -1
  12. package/dist/src/commands/extensions/list.js +13 -4
  13. package/dist/src/commands/extensions/list.js.map +1 -1
  14. package/dist/src/commands/extensions/uninstall.js +12 -2
  15. package/dist/src/commands/extensions/uninstall.js.map +1 -1
  16. package/dist/src/commands/extensions/update.js +17 -13
  17. package/dist/src/commands/extensions/update.js.map +1 -1
  18. package/dist/src/commands/mcp/list.js +10 -3
  19. package/dist/src/commands/mcp/list.js.map +1 -1
  20. package/dist/src/commands/mcp/list.test.js +12 -6
  21. package/dist/src/commands/mcp/list.test.js.map +1 -1
  22. package/dist/src/config/config.d.ts +1 -0
  23. package/dist/src/config/config.js +15 -3
  24. package/dist/src/config/config.js.map +1 -1
  25. package/dist/src/config/config.test.js +23 -15
  26. package/dist/src/config/config.test.js.map +1 -1
  27. package/dist/src/config/extension-manager.d.ts +38 -0
  28. package/dist/src/config/extension-manager.js +411 -0
  29. package/dist/src/config/extension-manager.js.map +1 -0
  30. package/dist/src/config/extension.d.ts +4 -51
  31. package/dist/src/config/extension.js +1 -535
  32. package/dist/src/config/extension.js.map +1 -1
  33. package/dist/src/config/extension.test.js +343 -163
  34. package/dist/src/config/extension.test.js.map +1 -1
  35. package/dist/src/config/extensions/consent.d.ts +38 -0
  36. package/dist/src/config/extensions/consent.js +123 -0
  37. package/dist/src/config/extensions/consent.js.map +1 -0
  38. package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
  39. package/dist/src/config/extensions/extensionEnablement.js +4 -3
  40. package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
  41. package/dist/src/config/extensions/extensionEnablement.test.js +10 -10
  42. package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
  43. package/dist/src/config/extensions/extensionSettings.d.ts +15 -0
  44. package/dist/src/config/extensions/extensionSettings.js +63 -0
  45. package/dist/src/config/extensions/extensionSettings.js.map +1 -0
  46. package/dist/src/config/extensions/extensionSettings.test.d.ts +6 -0
  47. package/dist/src/config/extensions/extensionSettings.test.js +137 -0
  48. package/dist/src/config/extensions/extensionSettings.test.js.map +1 -0
  49. package/dist/src/config/extensions/github.d.ts +2 -2
  50. package/dist/src/config/extensions/github.js +3 -8
  51. package/dist/src/config/extensions/github.js.map +1 -1
  52. package/dist/src/config/extensions/github.test.js +25 -7
  53. package/dist/src/config/extensions/github.test.js.map +1 -1
  54. package/dist/src/config/extensions/github_fetch.d.ts +1 -1
  55. package/dist/src/config/extensions/github_fetch.js +13 -1
  56. package/dist/src/config/extensions/github_fetch.js.map +1 -1
  57. package/dist/src/config/extensions/github_fetch.test.d.ts +6 -0
  58. package/dist/src/config/extensions/github_fetch.test.js +169 -0
  59. package/dist/src/config/extensions/github_fetch.test.js.map +1 -0
  60. package/dist/src/config/extensions/storage.d.ts +14 -0
  61. package/dist/src/config/extensions/storage.js +32 -0
  62. package/dist/src/config/extensions/storage.js.map +1 -0
  63. package/dist/src/config/extensions/update.d.ts +4 -4
  64. package/dist/src/config/extensions/update.js +11 -18
  65. package/dist/src/config/extensions/update.js.map +1 -1
  66. package/dist/src/config/extensions/update.test.js +33 -58
  67. package/dist/src/config/extensions/update.test.js.map +1 -1
  68. package/dist/src/config/extensions/variableSchema.d.ts +0 -6
  69. package/dist/src/config/extensions/variableSchema.js.map +1 -1
  70. package/dist/src/config/extensions/variables.d.ts +4 -0
  71. package/dist/src/config/extensions/variables.js +6 -0
  72. package/dist/src/config/extensions/variables.js.map +1 -1
  73. package/dist/src/config/settings.d.ts +2 -1
  74. package/dist/src/config/settings.js +4 -7
  75. package/dist/src/config/settings.js.map +1 -1
  76. package/dist/src/config/settings.test.js +86 -16
  77. package/dist/src/config/settings.test.js.map +1 -1
  78. package/dist/src/core/initializer.js +2 -1
  79. package/dist/src/core/initializer.js.map +1 -1
  80. package/dist/src/gemini.js +22 -5
  81. package/dist/src/gemini.js.map +1 -1
  82. package/dist/src/gemini.test.js +26 -2
  83. package/dist/src/gemini.test.js.map +1 -1
  84. package/dist/src/generated/git-commit.d.ts +2 -2
  85. package/dist/src/generated/git-commit.js +2 -2
  86. package/dist/src/nonInteractiveCli.js +30 -5
  87. package/dist/src/nonInteractiveCli.js.map +1 -1
  88. package/dist/src/nonInteractiveCli.test.js +151 -14
  89. package/dist/src/nonInteractiveCli.test.js.map +1 -1
  90. package/dist/src/test-utils/createExtension.d.ts +3 -1
  91. package/dist/src/test-utils/createExtension.js +3 -3
  92. package/dist/src/test-utils/createExtension.js.map +1 -1
  93. package/dist/src/ui/AppContainer.js +102 -48
  94. package/dist/src/ui/AppContainer.js.map +1 -1
  95. package/dist/src/ui/AppContainer.test.js +138 -79
  96. package/dist/src/ui/AppContainer.test.js.map +1 -1
  97. package/dist/src/ui/commands/extensionsCommand.js +19 -10
  98. package/dist/src/ui/commands/extensionsCommand.js.map +1 -1
  99. package/dist/src/ui/commands/extensionsCommand.test.js +8 -0
  100. package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -1
  101. package/dist/src/ui/components/FolderTrustDialog.test.js +1 -0
  102. package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
  103. package/dist/src/ui/components/HistoryItemDisplay.js +1 -1
  104. package/dist/src/ui/components/HistoryItemDisplay.js.map +1 -1
  105. package/dist/src/ui/components/InputPrompt.js +5 -6
  106. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  107. package/dist/src/ui/components/InputPrompt.test.js +668 -712
  108. package/dist/src/ui/components/InputPrompt.test.js.map +1 -1
  109. package/dist/src/ui/components/ModelDialog.test.js +1 -0
  110. package/dist/src/ui/components/ModelDialog.test.js.map +1 -1
  111. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +1 -0
  112. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
  113. package/dist/src/ui/components/SettingsDialog.test.js +2 -30
  114. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  115. package/dist/src/ui/components/ThemeDialog.test.js +1 -2
  116. package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
  117. package/dist/src/ui/components/shared/BaseSelectionList.test.js +1 -0
  118. package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
  119. package/dist/src/ui/components/shared/text-buffer.test.js +1 -0
  120. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -1
  121. package/dist/src/ui/components/views/ExtensionsList.d.ts +7 -1
  122. package/dist/src/ui/components/views/ExtensionsList.js +8 -11
  123. package/dist/src/ui/components/views/ExtensionsList.js.map +1 -1
  124. package/dist/src/ui/components/views/ExtensionsList.test.js +34 -21
  125. package/dist/src/ui/components/views/ExtensionsList.test.js.map +1 -1
  126. package/dist/src/ui/contexts/KeypressContext.d.ts +3 -2
  127. package/dist/src/ui/contexts/KeypressContext.js +429 -386
  128. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  129. package/dist/src/ui/contexts/KeypressContext.test.js +162 -479
  130. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  131. package/dist/src/ui/contexts/SessionContext.test.js +1 -0
  132. package/dist/src/ui/contexts/SessionContext.test.js.map +1 -1
  133. package/dist/src/ui/hooks/shellCommandProcessor.test.js +18 -2
  134. package/dist/src/ui/hooks/shellCommandProcessor.test.js.map +1 -1
  135. package/dist/src/ui/hooks/slashCommandProcessor.test.js +74 -80
  136. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -1
  137. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +1 -0
  138. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
  139. package/dist/src/ui/hooks/useCommandCompletion.test.js +79 -78
  140. package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -1
  141. package/dist/src/ui/hooks/useConsoleMessages.test.js +26 -9
  142. package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -1
  143. package/dist/src/ui/hooks/useEditorSettings.test.js +40 -34
  144. package/dist/src/ui/hooks/useEditorSettings.test.js.map +1 -1
  145. package/dist/src/ui/hooks/useExtensionUpdates.d.ts +15 -5
  146. package/dist/src/ui/hooks/useExtensionUpdates.js +16 -18
  147. package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
  148. package/dist/src/ui/hooks/useExtensionUpdates.test.js +46 -36
  149. package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
  150. package/dist/src/ui/hooks/useFlickerDetector.test.js +1 -0
  151. package/dist/src/ui/hooks/useFlickerDetector.test.js.map +1 -1
  152. package/dist/src/ui/hooks/useFocus.test.js +25 -9
  153. package/dist/src/ui/hooks/useFocus.test.js.map +1 -1
  154. package/dist/src/ui/hooks/useFolderTrust.test.js +1 -0
  155. package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -1
  156. package/dist/src/ui/hooks/useGeminiStream.js +49 -14
  157. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  158. package/dist/src/ui/hooks/useGeminiStream.test.js +114 -34
  159. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -1
  160. package/dist/src/ui/hooks/useGitBranchName.js +4 -0
  161. package/dist/src/ui/hooks/useGitBranchName.js.map +1 -1
  162. package/dist/src/ui/hooks/useGitBranchName.test.js +46 -35
  163. package/dist/src/ui/hooks/useGitBranchName.test.js.map +1 -1
  164. package/dist/src/ui/hooks/useHistoryManager.test.js +1 -0
  165. package/dist/src/ui/hooks/useHistoryManager.test.js.map +1 -1
  166. package/dist/src/ui/hooks/useIdeTrustListener.test.js +24 -7
  167. package/dist/src/ui/hooks/useIdeTrustListener.test.js.map +1 -1
  168. package/dist/src/ui/hooks/useInputHistory.test.js +1 -0
  169. package/dist/src/ui/hooks/useInputHistory.test.js.map +1 -1
  170. package/dist/src/ui/hooks/useInputHistoryStore.test.js +1 -0
  171. package/dist/src/ui/hooks/useInputHistoryStore.test.js.map +1 -1
  172. package/dist/src/ui/hooks/useKeypress.test.js +94 -113
  173. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -1
  174. package/dist/src/ui/hooks/useLoadingIndicator.test.js +24 -6
  175. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  176. package/dist/src/ui/hooks/useMemoryMonitor.test.js +10 -5
  177. package/dist/src/ui/hooks/useMemoryMonitor.test.js.map +1 -1
  178. package/dist/src/ui/hooks/useMessageQueue.test.js +61 -45
  179. package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
  180. package/dist/src/ui/hooks/useModelCommand.test.js +18 -11
  181. package/dist/src/ui/hooks/useModelCommand.test.js.map +1 -1
  182. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js +1 -0
  183. package/dist/src/ui/hooks/usePermissionsModifyTrust.test.js.map +1 -1
  184. package/dist/src/ui/hooks/usePhraseCycler.test.js +1 -0
  185. package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -1
  186. package/dist/src/ui/hooks/usePrivacySettings.test.js +26 -10
  187. package/dist/src/ui/hooks/usePrivacySettings.test.js.map +1 -1
  188. package/dist/src/ui/hooks/useQuotaAndFallback.js +13 -14
  189. package/dist/src/ui/hooks/useQuotaAndFallback.js.map +1 -1
  190. package/dist/src/ui/hooks/useQuotaAndFallback.test.js +32 -39
  191. package/dist/src/ui/hooks/useQuotaAndFallback.test.js.map +1 -1
  192. package/dist/src/ui/hooks/useReactToolScheduler.d.ts +8 -1
  193. package/dist/src/ui/hooks/useReactToolScheduler.js +59 -34
  194. package/dist/src/ui/hooks/useReactToolScheduler.js.map +1 -1
  195. package/dist/src/ui/hooks/useReactToolScheduler.test.d.ts +6 -0
  196. package/dist/src/ui/hooks/useReactToolScheduler.test.js +66 -0
  197. package/dist/src/ui/hooks/useReactToolScheduler.test.js.map +1 -0
  198. package/dist/src/ui/hooks/useSelectionList.test.js +193 -132
  199. package/dist/src/ui/hooks/useSelectionList.test.js.map +1 -1
  200. package/dist/src/ui/hooks/useShellHistory.test.js +1 -0
  201. package/dist/src/ui/hooks/useShellHistory.test.js.map +1 -1
  202. package/dist/src/ui/hooks/useTimer.test.js +43 -14
  203. package/dist/src/ui/hooks/useTimer.test.js.map +1 -1
  204. package/dist/src/ui/hooks/useToolScheduler.test.js +122 -39
  205. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  206. package/dist/src/ui/hooks/vim.test.js +251 -356
  207. package/dist/src/ui/hooks/vim.test.js.map +1 -1
  208. package/dist/src/ui/types.d.ts +2 -1
  209. package/dist/src/ui/types.js.map +1 -1
  210. package/dist/src/ui/utils/CodeColorizer.js +2 -1
  211. package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
  212. package/dist/src/ui/utils/textOutput.d.ts +25 -0
  213. package/dist/src/ui/utils/textOutput.js +49 -0
  214. package/dist/src/ui/utils/textOutput.js.map +1 -0
  215. package/dist/src/ui/utils/textOutput.test.d.ts +6 -0
  216. package/dist/src/ui/utils/textOutput.test.js +79 -0
  217. package/dist/src/ui/utils/textOutput.test.js.map +1 -0
  218. package/dist/src/ui/utils/updateCheck.d.ts +7 -1
  219. package/dist/src/ui/utils/updateCheck.js +27 -26
  220. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  221. package/dist/src/ui/utils/updateCheck.test.js +19 -49
  222. package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
  223. package/dist/src/utils/envVarResolver.d.ts +2 -2
  224. package/dist/src/utils/envVarResolver.js +10 -7
  225. package/dist/src/utils/envVarResolver.js.map +1 -1
  226. package/dist/src/utils/handleAutoUpdate.js +9 -3
  227. package/dist/src/utils/handleAutoUpdate.js.map +1 -1
  228. package/dist/src/zed-integration/schema.d.ts +4 -4
  229. package/dist/src/zed-integration/zedIntegration.js +8 -13
  230. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  231. package/dist/tsconfig.tsbuildinfo +1 -1
  232. package/package.json +5 -4
  233. package/dist/google-gemini-cli-0.12.0-nightly.20251022.0542de95.tgz +0 -0
@@ -5,7 +5,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
5
5
  * SPDX-License-Identifier: Apache-2.0
6
6
  */
7
7
  import { renderWithProviders } from '../../test-utils/render.js';
8
- import { waitFor, act } from '@testing-library/react';
8
+ import { act } from 'react';
9
9
  import { InputPrompt } from './InputPrompt.js';
10
10
  import { ApprovalMode } from '@google/gemini-cli-core';
11
11
  import * as path from 'node:path';
@@ -207,63 +207,68 @@ describe('InputPrompt', () => {
207
207
  streamingState: StreamingState.Idle,
208
208
  };
209
209
  });
210
- const wait = (ms = 50) => new Promise((resolve) => setTimeout(resolve, ms));
211
210
  it('should call shellHistory.getPreviousCommand on up arrow in shell mode', async () => {
212
211
  props.shellModeActive = true;
213
212
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
214
- await wait();
215
- stdin.write('\u001B[A');
216
- await wait();
217
- expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
213
+ await act(async () => {
214
+ stdin.write('\u001B[A');
215
+ });
216
+ await vi.waitFor(() => expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled());
218
217
  unmount();
219
218
  });
220
219
  it('should call shellHistory.getNextCommand on down arrow in shell mode', async () => {
221
220
  props.shellModeActive = true;
222
221
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
223
- await wait();
224
- stdin.write('\u001B[B');
225
- await wait();
226
- expect(mockShellHistory.getNextCommand).toHaveBeenCalled();
222
+ await act(async () => {
223
+ stdin.write('\u001B[B');
224
+ });
225
+ await vi.waitFor(() => expect(mockShellHistory.getNextCommand).toHaveBeenCalled());
227
226
  unmount();
228
227
  });
229
228
  it('should set the buffer text when a shell history command is retrieved', async () => {
230
229
  props.shellModeActive = true;
231
230
  vi.mocked(mockShellHistory.getPreviousCommand).mockReturnValue('previous command');
232
231
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
233
- await wait();
234
- stdin.write('\u001B[A');
235
- await wait();
236
- expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
237
- expect(props.buffer.setText).toHaveBeenCalledWith('previous command');
232
+ await act(async () => {
233
+ stdin.write('\u001B[A');
234
+ });
235
+ await vi.waitFor(() => {
236
+ expect(mockShellHistory.getPreviousCommand).toHaveBeenCalled();
237
+ expect(props.buffer.setText).toHaveBeenCalledWith('previous command');
238
+ });
238
239
  unmount();
239
240
  });
240
241
  it('should call shellHistory.addCommandToHistory on submit in shell mode', async () => {
241
242
  props.shellModeActive = true;
242
243
  props.buffer.setText('ls -l');
243
244
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
244
- await wait();
245
- stdin.write('\r');
246
- await wait();
247
- expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith('ls -l');
248
- expect(props.onSubmit).toHaveBeenCalledWith('ls -l');
245
+ await act(async () => {
246
+ stdin.write('\r');
247
+ });
248
+ await vi.waitFor(() => {
249
+ expect(mockShellHistory.addCommandToHistory).toHaveBeenCalledWith('ls -l');
250
+ expect(props.onSubmit).toHaveBeenCalledWith('ls -l');
251
+ });
249
252
  unmount();
250
253
  });
251
254
  it('should NOT call shell history methods when not in shell mode', async () => {
252
255
  props.buffer.setText('some text');
253
256
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
254
- await wait();
255
- stdin.write('\u001B[A'); // Up arrow
256
- await wait();
257
- stdin.write('\u001B[B'); // Down arrow
258
- await wait();
259
- stdin.write('\r'); // Enter
260
- await wait();
257
+ await act(async () => {
258
+ stdin.write('\u001B[A'); // Up arrow
259
+ });
260
+ await vi.waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
261
+ await act(async () => {
262
+ stdin.write('\u001B[B'); // Down arrow
263
+ });
264
+ await vi.waitFor(() => expect(mockInputHistory.navigateDown).toHaveBeenCalled());
265
+ await act(async () => {
266
+ stdin.write('\r'); // Enter
267
+ });
268
+ await vi.waitFor(() => expect(props.onSubmit).toHaveBeenCalledWith('some text'));
261
269
  expect(mockShellHistory.getPreviousCommand).not.toHaveBeenCalled();
262
270
  expect(mockShellHistory.getNextCommand).not.toHaveBeenCalled();
263
271
  expect(mockShellHistory.addCommandToHistory).not.toHaveBeenCalled();
264
- expect(mockInputHistory.navigateUp).toHaveBeenCalled();
265
- expect(mockInputHistory.navigateDown).toHaveBeenCalled();
266
- expect(props.onSubmit).toHaveBeenCalledWith('some text');
267
272
  unmount();
268
273
  });
269
274
  it('should call completion.navigateUp for both up arrow and Ctrl+P when suggestions are showing', async () => {
@@ -277,13 +282,15 @@ describe('InputPrompt', () => {
277
282
  });
278
283
  props.buffer.setText('/mem');
279
284
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
280
- await wait();
281
285
  // Test up arrow
282
- stdin.write('\u001B[A'); // Up arrow
283
- await wait();
284
- stdin.write('\u0010'); // Ctrl+P
285
- await wait();
286
- expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(2);
286
+ await act(async () => {
287
+ stdin.write('\u001B[A'); // Up arrow
288
+ });
289
+ await vi.waitFor(() => expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(1));
290
+ await act(async () => {
291
+ stdin.write('\u0010'); // Ctrl+P
292
+ });
293
+ await vi.waitFor(() => expect(mockCommandCompletion.navigateUp).toHaveBeenCalledTimes(2));
287
294
  expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
288
295
  unmount();
289
296
  });
@@ -298,13 +305,15 @@ describe('InputPrompt', () => {
298
305
  });
299
306
  props.buffer.setText('/mem');
300
307
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
301
- await wait();
302
308
  // Test down arrow
303
- stdin.write('\u001B[B'); // Down arrow
304
- await wait();
305
- stdin.write('\u000E'); // Ctrl+N
306
- await wait();
307
- expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(2);
309
+ await act(async () => {
310
+ stdin.write('\u001B[B'); // Down arrow
311
+ });
312
+ await vi.waitFor(() => expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(1));
313
+ await act(async () => {
314
+ stdin.write('\u000E'); // Ctrl+N
315
+ });
316
+ await vi.waitFor(() => expect(mockCommandCompletion.navigateDown).toHaveBeenCalledTimes(2));
308
317
  expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
309
318
  unmount();
310
319
  });
@@ -315,15 +324,22 @@ describe('InputPrompt', () => {
315
324
  });
316
325
  props.buffer.setText('some text');
317
326
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
318
- await wait();
319
- stdin.write('\u001B[A'); // Up arrow
320
- await wait();
321
- stdin.write('\u001B[B'); // Down arrow
322
- await wait();
323
- stdin.write('\u0010'); // Ctrl+P
324
- await wait();
325
- stdin.write('\u000E'); // Ctrl+N
326
- await wait();
327
+ await act(async () => {
328
+ stdin.write('\u001B[A'); // Up arrow
329
+ });
330
+ await vi.waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
331
+ await act(async () => {
332
+ stdin.write('\u001B[B'); // Down arrow
333
+ });
334
+ await vi.waitFor(() => expect(mockInputHistory.navigateDown).toHaveBeenCalled());
335
+ await act(async () => {
336
+ stdin.write('\u0010'); // Ctrl+P
337
+ });
338
+ await vi.waitFor(() => { });
339
+ await act(async () => {
340
+ stdin.write('\u000E'); // Ctrl+N
341
+ });
342
+ await vi.waitFor(() => { });
327
343
  expect(mockCommandCompletion.navigateUp).not.toHaveBeenCalled();
328
344
  expect(mockCommandCompletion.navigateDown).not.toHaveBeenCalled();
329
345
  unmount();
@@ -338,23 +354,27 @@ describe('InputPrompt', () => {
338
354
  vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
339
355
  vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue('/test/.gemini-clipboard/clipboard-123.png');
340
356
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
341
- await wait();
342
357
  // Send Ctrl+V
343
- stdin.write('\x16'); // Ctrl+V
344
- await wait();
345
- expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
346
- expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith(props.config.getTargetDir());
347
- expect(clipboardUtils.cleanupOldClipboardImages).toHaveBeenCalledWith(props.config.getTargetDir());
348
- expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
358
+ await act(async () => {
359
+ stdin.write('\x16'); // Ctrl+V
360
+ });
361
+ await vi.waitFor(() => {
362
+ expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
363
+ expect(clipboardUtils.saveClipboardImage).toHaveBeenCalledWith(props.config.getTargetDir());
364
+ expect(clipboardUtils.cleanupOldClipboardImages).toHaveBeenCalledWith(props.config.getTargetDir());
365
+ expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
366
+ });
349
367
  unmount();
350
368
  });
351
369
  it('should not insert anything when clipboard has no image', async () => {
352
370
  vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(false);
353
371
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
354
- await wait();
355
- stdin.write('\x16'); // Ctrl+V
356
- await wait();
357
- expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
372
+ await act(async () => {
373
+ stdin.write('\x16'); // Ctrl+V
374
+ });
375
+ await vi.waitFor(() => {
376
+ expect(clipboardUtils.clipboardHasImage).toHaveBeenCalled();
377
+ });
358
378
  expect(clipboardUtils.saveClipboardImage).not.toHaveBeenCalled();
359
379
  expect(mockBuffer.setText).not.toHaveBeenCalled();
360
380
  unmount();
@@ -363,10 +383,12 @@ describe('InputPrompt', () => {
363
383
  vi.mocked(clipboardUtils.clipboardHasImage).mockResolvedValue(true);
364
384
  vi.mocked(clipboardUtils.saveClipboardImage).mockResolvedValue(null);
365
385
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
366
- await wait();
367
- stdin.write('\x16'); // Ctrl+V
368
- await wait();
369
- expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled();
386
+ await act(async () => {
387
+ stdin.write('\x16'); // Ctrl+V
388
+ });
389
+ await vi.waitFor(() => {
390
+ expect(clipboardUtils.saveClipboardImage).toHaveBeenCalled();
391
+ });
370
392
  expect(mockBuffer.setText).not.toHaveBeenCalled();
371
393
  unmount();
372
394
  });
@@ -380,11 +402,13 @@ describe('InputPrompt', () => {
380
402
  mockBuffer.lines = ['Hello world'];
381
403
  mockBuffer.replaceRangeByOffset = vi.fn();
382
404
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
383
- await wait();
384
- stdin.write('\x16'); // Ctrl+V
385
- await wait();
386
- // Should insert at cursor position with spaces
387
- expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
405
+ await act(async () => {
406
+ stdin.write('\x16'); // Ctrl+V
407
+ });
408
+ await vi.waitFor(() => {
409
+ // Should insert at cursor position with spaces
410
+ expect(mockBuffer.replaceRangeByOffset).toHaveBeenCalled();
411
+ });
388
412
  // Get the actual call to see what path was used
389
413
  const actualCall = vi.mocked(mockBuffer.replaceRangeByOffset).mock
390
414
  .calls[0];
@@ -399,10 +423,12 @@ describe('InputPrompt', () => {
399
423
  .mockImplementation(() => { });
400
424
  vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue(new Error('Clipboard error'));
401
425
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
402
- await wait();
403
- stdin.write('\x16'); // Ctrl+V
404
- await wait();
405
- expect(consoleErrorSpy).toHaveBeenCalledWith('Error handling clipboard image:', expect.any(Error));
426
+ await act(async () => {
427
+ stdin.write('\x16'); // Ctrl+V
428
+ });
429
+ await vi.waitFor(() => {
430
+ expect(consoleErrorSpy).toHaveBeenCalledWith('Error handling clipboard image:', expect.any(Error));
431
+ });
406
432
  expect(mockBuffer.setText).not.toHaveBeenCalled();
407
433
  consoleErrorSpy.mockRestore();
408
434
  unmount();
@@ -418,10 +444,10 @@ describe('InputPrompt', () => {
418
444
  });
419
445
  props.buffer.setText('/mem');
420
446
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
421
- await wait();
422
- stdin.write('\t'); // Press Tab
423
- await wait();
424
- expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
447
+ await act(async () => {
448
+ stdin.write('\t'); // Press Tab
449
+ });
450
+ await vi.waitFor(() => expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0));
425
451
  unmount();
426
452
  });
427
453
  it('should append a sub-command when the parent command is already complete', async () => {
@@ -437,10 +463,10 @@ describe('InputPrompt', () => {
437
463
  });
438
464
  props.buffer.setText('/memory ');
439
465
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
440
- await wait();
441
- stdin.write('\t'); // Press Tab
442
- await wait();
443
- expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1);
466
+ await act(async () => {
467
+ stdin.write('\t'); // Press Tab
468
+ });
469
+ await vi.waitFor(() => expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(1));
444
470
  unmount();
445
471
  });
446
472
  it('should handle the "backspace" edge case correctly', async () => {
@@ -457,11 +483,12 @@ describe('InputPrompt', () => {
457
483
  // The user has backspaced, so the query is now just '/memory'
458
484
  props.buffer.setText('/memory');
459
485
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
460
- await wait();
461
- stdin.write('\t'); // Press Tab
462
- await wait();
486
+ await act(async () => {
487
+ stdin.write('\t'); // Press Tab
488
+ });
489
+ await vi.waitFor(() =>
463
490
  // It should NOT become '/show'. It should correctly become '/memory show'.
464
- expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
491
+ expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0));
465
492
  unmount();
466
493
  });
467
494
  it('should complete a partial argument for a command', async () => {
@@ -474,10 +501,10 @@ describe('InputPrompt', () => {
474
501
  });
475
502
  props.buffer.setText('/chat resume fi-');
476
503
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
477
- await wait();
478
- stdin.write('\t'); // Press Tab
479
- await wait();
480
- expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
504
+ await act(async () => {
505
+ stdin.write('\t'); // Press Tab
506
+ });
507
+ await vi.waitFor(() => expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0));
481
508
  unmount();
482
509
  });
483
510
  it('should autocomplete on Enter when suggestions are active, without submitting', async () => {
@@ -489,11 +516,13 @@ describe('InputPrompt', () => {
489
516
  });
490
517
  props.buffer.setText('/mem');
491
518
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
492
- await wait();
493
- stdin.write('\r');
494
- await wait();
495
- // The app should autocomplete the text, NOT submit.
496
- expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
519
+ await act(async () => {
520
+ stdin.write('\r');
521
+ });
522
+ await vi.waitFor(() => {
523
+ // The app should autocomplete the text, NOT submit.
524
+ expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
525
+ });
497
526
  expect(props.onSubmit).not.toHaveBeenCalled();
498
527
  unmount();
499
528
  });
@@ -514,18 +543,19 @@ describe('InputPrompt', () => {
514
543
  });
515
544
  props.buffer.setText('/?');
516
545
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
517
- await wait();
518
- stdin.write('\t'); // Press Tab for autocomplete
519
- await wait();
520
- expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
546
+ await act(async () => {
547
+ stdin.write('\t'); // Press Tab for autocomplete
548
+ });
549
+ await vi.waitFor(() => expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0));
521
550
  unmount();
522
551
  });
523
552
  it('should not submit on Enter when the buffer is empty or only contains whitespace', async () => {
524
553
  props.buffer.setText(' '); // Set buffer to whitespace
525
554
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
526
- await wait();
527
- stdin.write('\r'); // Press Enter
528
- await wait();
555
+ await act(async () => {
556
+ stdin.write('\r'); // Press Enter
557
+ });
558
+ await vi.waitFor(() => { });
529
559
  expect(props.onSubmit).not.toHaveBeenCalled();
530
560
  unmount();
531
561
  });
@@ -537,10 +567,10 @@ describe('InputPrompt', () => {
537
567
  });
538
568
  props.buffer.setText('/clear');
539
569
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
540
- await wait();
541
- stdin.write('\r');
542
- await wait();
543
- expect(props.onSubmit).toHaveBeenCalledWith('/clear');
570
+ await act(async () => {
571
+ stdin.write('\r');
572
+ });
573
+ await vi.waitFor(() => expect(props.onSubmit).toHaveBeenCalledWith('/clear'));
544
574
  unmount();
545
575
  });
546
576
  it('should submit directly on Enter when a complete leaf command is typed', async () => {
@@ -551,10 +581,10 @@ describe('InputPrompt', () => {
551
581
  });
552
582
  props.buffer.setText('/clear');
553
583
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
554
- await wait();
555
- stdin.write('\r');
556
- await wait();
557
- expect(props.onSubmit).toHaveBeenCalledWith('/clear');
584
+ await act(async () => {
585
+ stdin.write('\r');
586
+ });
587
+ await vi.waitFor(() => expect(props.onSubmit).toHaveBeenCalledWith('/clear'));
558
588
  unmount();
559
589
  });
560
590
  it('should autocomplete an @-path on Enter without submitting', async () => {
@@ -566,10 +596,10 @@ describe('InputPrompt', () => {
566
596
  });
567
597
  props.buffer.setText('@src/components/');
568
598
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
569
- await wait();
570
- stdin.write('\r');
571
- await wait();
572
- expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0);
599
+ await act(async () => {
600
+ stdin.write('\r');
601
+ });
602
+ await vi.waitFor(() => expect(mockCommandCompletion.handleAutocomplete).toHaveBeenCalledWith(0));
573
603
  expect(props.onSubmit).not.toHaveBeenCalled();
574
604
  unmount();
575
605
  });
@@ -579,243 +609,134 @@ describe('InputPrompt', () => {
579
609
  mockBuffer.cursor = [0, 11];
580
610
  mockBuffer.lines = ['first line\\'];
581
611
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
582
- await wait();
583
- stdin.write('\r');
584
- await wait();
612
+ await act(async () => {
613
+ stdin.write('\r');
614
+ });
615
+ await vi.waitFor(() => {
616
+ expect(props.buffer.backspace).toHaveBeenCalled();
617
+ expect(props.buffer.newline).toHaveBeenCalled();
618
+ });
585
619
  expect(props.onSubmit).not.toHaveBeenCalled();
586
- expect(props.buffer.backspace).toHaveBeenCalled();
587
- expect(props.buffer.newline).toHaveBeenCalled();
588
620
  unmount();
589
621
  });
590
622
  it('should clear the buffer on Ctrl+C if it has text', async () => {
591
623
  props.buffer.setText('some text to clear');
592
624
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
593
- await wait();
594
- stdin.write('\x03'); // Ctrl+C character
595
- await wait();
596
- expect(props.buffer.setText).toHaveBeenCalledWith('');
597
- expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
625
+ await act(async () => {
626
+ stdin.write('\x03'); // Ctrl+C character
627
+ });
628
+ await vi.waitFor(() => {
629
+ expect(props.buffer.setText).toHaveBeenCalledWith('');
630
+ expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
631
+ });
598
632
  expect(props.onSubmit).not.toHaveBeenCalled();
599
633
  unmount();
600
634
  });
601
635
  it('should NOT clear the buffer on Ctrl+C if it is empty', async () => {
602
636
  props.buffer.text = '';
603
637
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
604
- await wait();
605
- stdin.write('\x03'); // Ctrl+C character
606
- await wait();
638
+ await act(async () => {
639
+ stdin.write('\x03'); // Ctrl+C character
640
+ });
641
+ await vi.waitFor(() => { });
607
642
  expect(props.buffer.setText).not.toHaveBeenCalled();
608
643
  unmount();
609
644
  });
610
645
  describe('cursor-based completion trigger', () => {
611
- it('should trigger completion when cursor is after @ without spaces', async () => {
612
- // Set up buffer state
613
- mockBuffer.text = '@src/components';
614
- mockBuffer.lines = ['@src/components'];
615
- mockBuffer.cursor = [0, 15];
616
- mockedUseCommandCompletion.mockReturnValue({
617
- ...mockCommandCompletion,
646
+ it.each([
647
+ {
648
+ name: 'should trigger completion when cursor is after @ without spaces',
649
+ text: '@src/components',
650
+ cursor: [0, 15],
618
651
  showSuggestions: true,
619
- suggestions: [{ label: 'Button.tsx', value: 'Button.tsx' }],
620
- });
621
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
622
- await wait();
623
- // Verify useCompletion was called with correct signature
624
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
625
- unmount();
626
- });
627
- it('should trigger completion when cursor is after / without spaces', async () => {
628
- mockBuffer.text = '/memory';
629
- mockBuffer.lines = ['/memory'];
630
- mockBuffer.cursor = [0, 7];
631
- mockedUseCommandCompletion.mockReturnValue({
632
- ...mockCommandCompletion,
652
+ },
653
+ {
654
+ name: 'should trigger completion when cursor is after / without spaces',
655
+ text: '/memory',
656
+ cursor: [0, 7],
633
657
  showSuggestions: true,
634
- suggestions: [{ label: 'show', value: 'show' }],
635
- });
636
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
637
- await wait();
638
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
639
- unmount();
640
- });
641
- it('should NOT trigger completion when cursor is after space following @', async () => {
642
- mockBuffer.text = '@src/file.ts hello';
643
- mockBuffer.lines = ['@src/file.ts hello'];
644
- mockBuffer.cursor = [0, 18];
645
- mockedUseCommandCompletion.mockReturnValue({
646
- ...mockCommandCompletion,
658
+ },
659
+ {
660
+ name: 'should NOT trigger completion when cursor is after space following @',
661
+ text: '@src/file.ts hello',
662
+ cursor: [0, 18],
647
663
  showSuggestions: false,
648
- suggestions: [],
649
- });
650
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
651
- await wait();
652
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
653
- unmount();
654
- });
655
- it('should NOT trigger completion when cursor is after space following /', async () => {
656
- mockBuffer.text = '/memory add';
657
- mockBuffer.lines = ['/memory add'];
658
- mockBuffer.cursor = [0, 11];
659
- mockedUseCommandCompletion.mockReturnValue({
660
- ...mockCommandCompletion,
664
+ },
665
+ {
666
+ name: 'should NOT trigger completion when cursor is after space following /',
667
+ text: '/memory add',
668
+ cursor: [0, 11],
661
669
  showSuggestions: false,
662
- suggestions: [],
663
- });
664
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
665
- await wait();
666
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
667
- unmount();
668
- });
669
- it('should NOT trigger completion when cursor is not after @ or /', async () => {
670
- mockBuffer.text = 'hello world';
671
- mockBuffer.lines = ['hello world'];
672
- mockBuffer.cursor = [0, 5];
673
- mockedUseCommandCompletion.mockReturnValue({
674
- ...mockCommandCompletion,
670
+ },
671
+ {
672
+ name: 'should NOT trigger completion when cursor is not after @ or /',
673
+ text: 'hello world',
674
+ cursor: [0, 5],
675
675
  showSuggestions: false,
676
- suggestions: [],
677
- });
678
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
679
- await wait();
680
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
681
- unmount();
682
- });
683
- it('should handle multiline text correctly', async () => {
684
- mockBuffer.text = 'first line\n/memory';
685
- mockBuffer.lines = ['first line', '/memory'];
686
- mockBuffer.cursor = [1, 7];
687
- mockedUseCommandCompletion.mockReturnValue({
688
- ...mockCommandCompletion,
676
+ },
677
+ {
678
+ name: 'should handle multiline text correctly',
679
+ text: 'first line\n/memory',
680
+ cursor: [1, 7],
689
681
  showSuggestions: false,
690
- suggestions: [],
691
- });
692
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
693
- await wait();
694
- // Verify useCompletion was called with the buffer
695
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
696
- unmount();
697
- });
698
- it('should handle single line slash command correctly', async () => {
699
- mockBuffer.text = '/memory';
700
- mockBuffer.lines = ['/memory'];
701
- mockBuffer.cursor = [0, 7];
702
- mockedUseCommandCompletion.mockReturnValue({
703
- ...mockCommandCompletion,
704
- showSuggestions: true,
705
- suggestions: [{ label: 'show', value: 'show' }],
706
- });
707
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
708
- await wait();
709
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
710
- unmount();
711
- });
712
- it('should handle Unicode characters (emojis) correctly in paths', async () => {
713
- // Test with emoji in path after @
714
- mockBuffer.text = '@src/file👍.txt';
715
- mockBuffer.lines = ['@src/file👍.txt'];
716
- mockBuffer.cursor = [0, 14]; // After the emoji character
717
- mockedUseCommandCompletion.mockReturnValue({
718
- ...mockCommandCompletion,
682
+ },
683
+ {
684
+ name: 'should handle Unicode characters (emojis) correctly in paths',
685
+ text: '@src/file👍.txt',
686
+ cursor: [0, 14],
719
687
  showSuggestions: true,
720
- suggestions: [{ label: 'file👍.txt', value: 'file👍.txt' }],
721
- });
722
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
723
- await wait();
724
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
725
- unmount();
726
- });
727
- it('should handle Unicode characters with spaces after them', async () => {
728
- // Test with emoji followed by space - should NOT trigger completion
729
- mockBuffer.text = '@src/file👍.txt hello';
730
- mockBuffer.lines = ['@src/file👍.txt hello'];
731
- mockBuffer.cursor = [0, 20]; // After the space
732
- mockedUseCommandCompletion.mockReturnValue({
733
- ...mockCommandCompletion,
688
+ },
689
+ {
690
+ name: 'should handle Unicode characters with spaces after them',
691
+ text: '@src/file👍.txt hello',
692
+ cursor: [0, 20],
734
693
  showSuggestions: false,
735
- suggestions: [],
736
- });
737
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
738
- await wait();
739
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
740
- unmount();
741
- });
742
- it('should handle escaped spaces in paths correctly', async () => {
743
- // Test with escaped space in path - should trigger completion
744
- mockBuffer.text = '@src/my\\ file.txt';
745
- mockBuffer.lines = ['@src/my\\ file.txt'];
746
- mockBuffer.cursor = [0, 16]; // After the escaped space and filename
747
- mockedUseCommandCompletion.mockReturnValue({
748
- ...mockCommandCompletion,
694
+ },
695
+ {
696
+ name: 'should handle escaped spaces in paths correctly',
697
+ text: '@src/my\\ file.txt',
698
+ cursor: [0, 16],
749
699
  showSuggestions: true,
750
- suggestions: [{ label: 'my file.txt', value: 'my file.txt' }],
751
- });
752
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
753
- await wait();
754
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
755
- unmount();
756
- });
757
- it('should NOT trigger completion after unescaped space following escaped space', async () => {
758
- // Test: @path/my\ file.txt hello (unescaped space after escaped space)
759
- mockBuffer.text = '@path/my\\ file.txt hello';
760
- mockBuffer.lines = ['@path/my\\ file.txt hello'];
761
- mockBuffer.cursor = [0, 24]; // After "hello"
762
- mockedUseCommandCompletion.mockReturnValue({
763
- ...mockCommandCompletion,
700
+ },
701
+ {
702
+ name: 'should NOT trigger completion after unescaped space following escaped space',
703
+ text: '@path/my\\ file.txt hello',
704
+ cursor: [0, 24],
764
705
  showSuggestions: false,
765
- suggestions: [],
766
- });
767
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
768
- await wait();
769
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
770
- unmount();
771
- });
772
- it('should handle multiple escaped spaces in paths', async () => {
773
- // Test with multiple escaped spaces
774
- mockBuffer.text = '@docs/my\\ long\\ file\\ name.md';
775
- mockBuffer.lines = ['@docs/my\\ long\\ file\\ name.md'];
776
- mockBuffer.cursor = [0, 29]; // At the end
777
- mockedUseCommandCompletion.mockReturnValue({
778
- ...mockCommandCompletion,
706
+ },
707
+ {
708
+ name: 'should handle multiple escaped spaces in paths',
709
+ text: '@docs/my\\ long\\ file\\ name.md',
710
+ cursor: [0, 29],
779
711
  showSuggestions: true,
780
- suggestions: [
781
- { label: 'my long file name.md', value: 'my long file name.md' },
782
- ],
783
- });
784
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
785
- await wait();
786
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
787
- unmount();
788
- });
789
- it('should handle escaped spaces in slash commands', async () => {
790
- // Test escaped spaces with slash commands (though less common)
791
- mockBuffer.text = '/memory\\ test';
792
- mockBuffer.lines = ['/memory\\ test'];
793
- mockBuffer.cursor = [0, 13]; // At the end
794
- mockedUseCommandCompletion.mockReturnValue({
795
- ...mockCommandCompletion,
712
+ },
713
+ {
714
+ name: 'should handle escaped spaces in slash commands',
715
+ text: '/memory\\ test',
716
+ cursor: [0, 13],
796
717
  showSuggestions: true,
797
- suggestions: [{ label: 'test-command', value: 'test-command' }],
798
- });
799
- const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
800
- await wait();
801
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
802
- unmount();
803
- });
804
- it('should handle Unicode characters with escaped spaces', async () => {
805
- // Test combining Unicode and escaped spaces
806
- mockBuffer.text = '@' + path.join('files', 'emoji\\ 👍\\ test.txt');
807
- mockBuffer.lines = ['@' + path.join('files', 'emoji\\ 👍\\ test.txt')];
808
- mockBuffer.cursor = [0, 25]; // After the escaped space and emoji
718
+ },
719
+ {
720
+ name: 'should handle Unicode characters with escaped spaces',
721
+ text: `@${path.join('files', 'emoji\\ 👍\\ test.txt')}`,
722
+ cursor: [0, 25],
723
+ showSuggestions: true,
724
+ },
725
+ ])('$name', async ({ text, cursor, showSuggestions }) => {
726
+ mockBuffer.text = text;
727
+ mockBuffer.lines = text.split('\n');
728
+ mockBuffer.cursor = cursor;
809
729
  mockedUseCommandCompletion.mockReturnValue({
810
730
  ...mockCommandCompletion,
811
- showSuggestions: true,
812
- suggestions: [
813
- { label: 'emoji 👍 test.txt', value: 'emoji 👍 test.txt' },
814
- ],
731
+ showSuggestions,
732
+ suggestions: showSuggestions
733
+ ? [{ label: 'suggestion', value: 'suggestion' }]
734
+ : [],
815
735
  });
816
736
  const { unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
817
- await wait();
818
- expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
737
+ await vi.waitFor(() => {
738
+ expect(mockedUseCommandCompletion).toHaveBeenCalledWith(mockBuffer, ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, false, false, expect.any(Object));
739
+ });
819
740
  unmount();
820
741
  });
821
742
  });
@@ -823,32 +744,38 @@ describe('InputPrompt', () => {
823
744
  it('should not call buffer.handleInput when vim mode is enabled and vim handles the input', async () => {
824
745
  props.vimHandleInput = vi.fn().mockReturnValue(true); // Mock that vim handled it.
825
746
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
826
- await wait();
827
- stdin.write('i');
828
- await wait();
829
- expect(props.vimHandleInput).toHaveBeenCalled();
747
+ await act(async () => {
748
+ stdin.write('i');
749
+ });
750
+ await vi.waitFor(() => {
751
+ expect(props.vimHandleInput).toHaveBeenCalled();
752
+ });
830
753
  expect(mockBuffer.handleInput).not.toHaveBeenCalled();
831
754
  unmount();
832
755
  });
833
756
  it('should call buffer.handleInput when vim mode is enabled but vim does not handle the input', async () => {
834
757
  props.vimHandleInput = vi.fn().mockReturnValue(false); // Mock that vim did NOT handle it.
835
758
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
836
- await wait();
837
- stdin.write('i');
838
- await wait();
839
- expect(props.vimHandleInput).toHaveBeenCalled();
840
- expect(mockBuffer.handleInput).toHaveBeenCalled();
759
+ await act(async () => {
760
+ stdin.write('i');
761
+ });
762
+ await vi.waitFor(() => {
763
+ expect(props.vimHandleInput).toHaveBeenCalled();
764
+ expect(mockBuffer.handleInput).toHaveBeenCalled();
765
+ });
841
766
  unmount();
842
767
  });
843
768
  it('should call handleInput when vim mode is disabled', async () => {
844
769
  // Mock vimHandleInput to return false (vim didn't handle the input)
845
770
  props.vimHandleInput = vi.fn().mockReturnValue(false);
846
771
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
847
- await wait();
848
- stdin.write('i');
849
- await wait();
850
- expect(props.vimHandleInput).toHaveBeenCalled();
851
- expect(mockBuffer.handleInput).toHaveBeenCalled();
772
+ await act(async () => {
773
+ stdin.write('i');
774
+ });
775
+ await vi.waitFor(() => {
776
+ expect(props.vimHandleInput).toHaveBeenCalled();
777
+ expect(mockBuffer.handleInput).toHaveBeenCalled();
778
+ });
852
779
  unmount();
853
780
  });
854
781
  });
@@ -856,185 +783,158 @@ describe('InputPrompt', () => {
856
783
  it('should handle bracketed paste when not focused', async () => {
857
784
  props.focus = false;
858
785
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
859
- await wait();
860
- stdin.write('\x1B[200~pasted text\x1B[201~');
861
- await wait();
862
- expect(mockBuffer.handleInput).toHaveBeenCalledWith(expect.objectContaining({
863
- paste: true,
864
- sequence: 'pasted text',
865
- }));
786
+ await act(async () => {
787
+ stdin.write('\x1B[200~pasted text\x1B[201~');
788
+ });
789
+ await vi.waitFor(() => {
790
+ expect(mockBuffer.handleInput).toHaveBeenCalledWith(expect.objectContaining({
791
+ paste: true,
792
+ sequence: 'pasted text',
793
+ }));
794
+ });
866
795
  unmount();
867
796
  });
868
797
  it('should ignore regular keypresses when not focused', async () => {
869
798
  props.focus = false;
870
799
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
871
- await wait();
872
- stdin.write('a');
873
- await wait();
800
+ await act(async () => {
801
+ stdin.write('a');
802
+ });
803
+ await vi.waitFor(() => { });
874
804
  expect(mockBuffer.handleInput).not.toHaveBeenCalled();
875
805
  unmount();
876
806
  });
877
807
  });
878
808
  describe('Highlighting and Cursor Display', () => {
879
- it('should display cursor mid-word by highlighting the character', async () => {
880
- mockBuffer.text = 'hello world';
881
- mockBuffer.lines = ['hello world'];
882
- mockBuffer.viewportVisualLines = ['hello world'];
883
- mockBuffer.visualCursor = [0, 3]; // cursor on the second 'l'
884
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
885
- await wait();
886
- const frame = stdout.lastFrame();
887
- // The component will render the text with the character at the cursor inverted.
888
- expect(frame).toContain(`hel${chalk.inverse('l')}o world`);
889
- unmount();
890
- });
891
- it('should display cursor at the beginning of the line', async () => {
892
- mockBuffer.text = 'hello';
893
- mockBuffer.lines = ['hello'];
894
- mockBuffer.viewportVisualLines = ['hello'];
895
- mockBuffer.visualCursor = [0, 0]; // cursor on 'h'
896
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
897
- await wait();
898
- const frame = stdout.lastFrame();
899
- expect(frame).toContain(`${chalk.inverse('h')}ello`);
900
- unmount();
901
- });
902
- it('should display cursor at the end of the line as an inverted space', async () => {
903
- mockBuffer.text = 'hello';
904
- mockBuffer.lines = ['hello'];
905
- mockBuffer.viewportVisualLines = ['hello'];
906
- mockBuffer.visualCursor = [0, 5]; // cursor after 'o'
907
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
908
- await wait();
909
- const frame = stdout.lastFrame();
910
- expect(frame).toContain(`hello${chalk.inverse(' ')}`);
911
- unmount();
912
- });
913
- it('should display cursor correctly on a highlighted token', async () => {
914
- mockBuffer.text = 'run @path/to/file';
915
- mockBuffer.lines = ['run @path/to/file'];
916
- mockBuffer.viewportVisualLines = ['run @path/to/file'];
917
- mockBuffer.visualCursor = [0, 9]; // cursor on 't' in 'to'
918
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
919
- await wait();
920
- const frame = stdout.lastFrame();
921
- // The token '@path/to/file' is colored, and the cursor highlights one char inside it.
922
- expect(frame).toContain(`@path/${chalk.inverse('t')}o/file`);
923
- unmount();
924
- });
925
- it('should display cursor correctly for multi-byte unicode characters', async () => {
926
- const text = 'hello 👍 world';
927
- mockBuffer.text = text;
928
- mockBuffer.lines = [text];
929
- mockBuffer.viewportVisualLines = [text];
930
- mockBuffer.visualCursor = [0, 6]; // cursor on '👍'
931
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
932
- await wait();
933
- const frame = stdout.lastFrame();
934
- expect(frame).toContain(`hello ${chalk.inverse('👍')} world`);
935
- unmount();
936
- });
937
- it('should display cursor at the end of a line with unicode characters', async () => {
938
- const text = 'hello 👍';
939
- mockBuffer.text = text;
940
- mockBuffer.lines = [text];
941
- mockBuffer.viewportVisualLines = [text];
942
- mockBuffer.visualCursor = [0, 8]; // cursor after '👍' (length is 6 + 2 for emoji)
943
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
944
- await wait();
945
- const frame = stdout.lastFrame();
946
- expect(frame).toContain(`hello 👍${chalk.inverse(' ')}`);
947
- unmount();
948
- });
949
- it('should display cursor on an empty line', async () => {
950
- mockBuffer.text = '';
951
- mockBuffer.lines = [''];
952
- mockBuffer.viewportVisualLines = [''];
953
- mockBuffer.visualCursor = [0, 0];
954
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
955
- await wait();
956
- const frame = stdout.lastFrame();
957
- expect(frame).toContain(chalk.inverse(' '));
958
- unmount();
959
- });
960
- it('should display cursor on a space between words', async () => {
961
- mockBuffer.text = 'hello world';
962
- mockBuffer.lines = ['hello world'];
963
- mockBuffer.viewportVisualLines = ['hello world'];
964
- mockBuffer.visualCursor = [0, 5]; // cursor on the space
965
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
966
- await wait();
967
- const frame = stdout.lastFrame();
968
- expect(frame).toContain(`hello${chalk.inverse(' ')}world`);
969
- unmount();
970
- });
971
- it('should display cursor in the middle of a line in a multiline block', async () => {
972
- const text = 'first line\nsecond line\nthird line';
973
- mockBuffer.text = text;
974
- mockBuffer.lines = text.split('\n');
975
- mockBuffer.viewportVisualLines = text.split('\n');
976
- mockBuffer.visualCursor = [1, 3]; // cursor on 'o' in 'second'
977
- mockBuffer.visualToLogicalMap = [
978
- [0, 0],
979
- [1, 0],
980
- [2, 0],
981
- ];
982
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
983
- await wait();
984
- const frame = stdout.lastFrame();
985
- expect(frame).toContain(`sec${chalk.inverse('o')}nd line`);
986
- unmount();
987
- });
988
- it('should display cursor at the beginning of a line in a multiline block', async () => {
989
- const text = 'first line\nsecond line';
990
- mockBuffer.text = text;
991
- mockBuffer.lines = text.split('\n');
992
- mockBuffer.viewportVisualLines = text.split('\n');
993
- mockBuffer.visualCursor = [1, 0]; // cursor on 's' in 'second'
994
- mockBuffer.visualToLogicalMap = [
995
- [0, 0],
996
- [1, 0],
997
- ];
998
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
999
- await wait();
1000
- const frame = stdout.lastFrame();
1001
- expect(frame).toContain(`${chalk.inverse('s')}econd line`);
1002
- unmount();
1003
- });
1004
- it('should display cursor at the end of a line in a multiline block', async () => {
1005
- const text = 'first line\nsecond line';
1006
- mockBuffer.text = text;
1007
- mockBuffer.lines = text.split('\n');
1008
- mockBuffer.viewportVisualLines = text.split('\n');
1009
- mockBuffer.visualCursor = [0, 10]; // cursor after 'first line'
1010
- mockBuffer.visualToLogicalMap = [
1011
- [0, 0],
1012
- [1, 0],
1013
- ];
1014
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1015
- await wait();
1016
- const frame = stdout.lastFrame();
1017
- expect(frame).toContain(`first line${chalk.inverse(' ')}`);
1018
- unmount();
809
+ describe('single-line scenarios', () => {
810
+ it.each([
811
+ {
812
+ name: 'mid-word',
813
+ text: 'hello world',
814
+ visualCursor: [0, 3],
815
+ expected: `hel${chalk.inverse('l')}o world`,
816
+ },
817
+ {
818
+ name: 'at the beginning of the line',
819
+ text: 'hello',
820
+ visualCursor: [0, 0],
821
+ expected: `${chalk.inverse('h')}ello`,
822
+ },
823
+ {
824
+ name: 'at the end of the line',
825
+ text: 'hello',
826
+ visualCursor: [0, 5],
827
+ expected: `hello${chalk.inverse(' ')}`,
828
+ },
829
+ {
830
+ name: 'on a highlighted token',
831
+ text: 'run @path/to/file',
832
+ visualCursor: [0, 9],
833
+ expected: `@path/${chalk.inverse('t')}o/file`,
834
+ },
835
+ {
836
+ name: 'for multi-byte unicode characters',
837
+ text: 'hello 👍 world',
838
+ visualCursor: [0, 6],
839
+ expected: `hello ${chalk.inverse('👍')} world`,
840
+ },
841
+ {
842
+ name: 'at the end of a line with unicode characters',
843
+ text: 'hello 👍',
844
+ visualCursor: [0, 8],
845
+ expected: `hello 👍${chalk.inverse(' ')}`,
846
+ },
847
+ {
848
+ name: 'on an empty line',
849
+ text: '',
850
+ visualCursor: [0, 0],
851
+ expected: chalk.inverse(' '),
852
+ },
853
+ {
854
+ name: 'on a space between words',
855
+ text: 'hello world',
856
+ visualCursor: [0, 5],
857
+ expected: `hello${chalk.inverse(' ')}world`,
858
+ },
859
+ ])('should display cursor correctly $name', async ({ text, visualCursor, expected }) => {
860
+ mockBuffer.text = text;
861
+ mockBuffer.lines = [text];
862
+ mockBuffer.viewportVisualLines = [text];
863
+ mockBuffer.visualCursor = visualCursor;
864
+ const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
865
+ await vi.waitFor(() => {
866
+ const frame = stdout.lastFrame();
867
+ expect(frame).toContain(expected);
868
+ });
869
+ unmount();
870
+ });
1019
871
  });
1020
- it('should display cursor on a blank line in a multiline block', async () => {
1021
- const text = 'first line\n\nthird line';
1022
- mockBuffer.text = text;
1023
- mockBuffer.lines = text.split('\n');
1024
- mockBuffer.viewportVisualLines = text.split('\n');
1025
- mockBuffer.visualCursor = [1, 0]; // cursor on the blank line
1026
- mockBuffer.visualToLogicalMap = [
1027
- [0, 0],
1028
- [1, 0],
1029
- [2, 0],
1030
- ];
1031
- const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1032
- await wait();
1033
- const frame = stdout.lastFrame();
1034
- const lines = frame.split('\n');
1035
- // The line with the cursor should just be an inverted space inside the box border
1036
- expect(lines.find((l) => l.includes(chalk.inverse(' ')))).not.toBeUndefined();
1037
- unmount();
872
+ describe('multi-line scenarios', () => {
873
+ it.each([
874
+ {
875
+ name: 'in the middle of a line',
876
+ text: 'first line\nsecond line\nthird line',
877
+ visualCursor: [1, 3],
878
+ visualToLogicalMap: [
879
+ [0, 0],
880
+ [1, 0],
881
+ [2, 0],
882
+ ],
883
+ expected: `sec${chalk.inverse('o')}nd line`,
884
+ },
885
+ {
886
+ name: 'at the beginning of a line',
887
+ text: 'first line\nsecond line',
888
+ visualCursor: [1, 0],
889
+ visualToLogicalMap: [
890
+ [0, 0],
891
+ [1, 0],
892
+ ],
893
+ expected: `${chalk.inverse('s')}econd line`,
894
+ },
895
+ {
896
+ name: 'at the end of a line',
897
+ text: 'first line\nsecond line',
898
+ visualCursor: [0, 10],
899
+ visualToLogicalMap: [
900
+ [0, 0],
901
+ [1, 0],
902
+ ],
903
+ expected: `first line${chalk.inverse(' ')}`,
904
+ },
905
+ ])('should display cursor correctly $name in a multiline block', async ({ text, visualCursor, expected, visualToLogicalMap }) => {
906
+ mockBuffer.text = text;
907
+ mockBuffer.lines = text.split('\n');
908
+ mockBuffer.viewportVisualLines = text.split('\n');
909
+ mockBuffer.visualCursor = visualCursor;
910
+ mockBuffer.visualToLogicalMap = visualToLogicalMap;
911
+ const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
912
+ await vi.waitFor(() => {
913
+ const frame = stdout.lastFrame();
914
+ expect(frame).toContain(expected);
915
+ });
916
+ unmount();
917
+ });
918
+ it('should display cursor on a blank line in a multiline block', async () => {
919
+ const text = 'first line\n\nthird line';
920
+ mockBuffer.text = text;
921
+ mockBuffer.lines = text.split('\n');
922
+ mockBuffer.viewportVisualLines = text.split('\n');
923
+ mockBuffer.visualCursor = [1, 0]; // cursor on the blank line
924
+ mockBuffer.visualToLogicalMap = [
925
+ [0, 0],
926
+ [1, 0],
927
+ [2, 0],
928
+ ];
929
+ const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
930
+ await vi.waitFor(() => {
931
+ const frame = stdout.lastFrame();
932
+ const lines = frame.split('\n');
933
+ // The line with the cursor should just be an inverted space inside the box border
934
+ expect(lines.find((l) => l.includes(chalk.inverse(' ')))).not.toBeUndefined();
935
+ });
936
+ unmount();
937
+ });
1038
938
  });
1039
939
  });
1040
940
  describe('multiline rendering', () => {
@@ -1052,15 +952,16 @@ describe('InputPrompt', () => {
1052
952
  [2, 0], // 'world' is logical line 2, col 0
1053
953
  ];
1054
954
  const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1055
- await wait();
1056
- const frame = stdout.lastFrame();
1057
- // Check that all lines, including the empty one, are rendered.
1058
- // This implicitly tests that the Box wrapper provides height for the empty line.
1059
- expect(frame).toContain('hello');
1060
- expect(frame).toContain(`world${chalk.inverse(' ')}`);
1061
- const outputLines = frame.split('\n');
1062
- // The number of lines should be 2 for the border plus 3 for the content.
1063
- expect(outputLines.length).toBe(5);
955
+ await vi.waitFor(() => {
956
+ const frame = stdout.lastFrame();
957
+ // Check that all lines, including the empty one, are rendered.
958
+ // This implicitly tests that the Box wrapper provides height for the empty line.
959
+ expect(frame).toContain('hello');
960
+ expect(frame).toContain(`world${chalk.inverse(' ')}`);
961
+ const outputLines = frame.split('\n');
962
+ // The number of lines should be 2 for the border plus 3 for the content.
963
+ expect(outputLines.length).toBe(5);
964
+ });
1064
965
  unmount();
1065
966
  });
1066
967
  });
@@ -1080,16 +981,18 @@ describe('InputPrompt', () => {
1080
981
  },
1081
982
  ])('should handle multiline paste $description', async ({ pastedText }) => {
1082
983
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1083
- await wait();
1084
984
  // Simulate a bracketed paste event from the terminal
1085
- stdin.write(`\x1b[200~${pastedText}\x1b[201~`);
1086
- await wait();
1087
- // Verify that the buffer's handleInput was called once with the full text
1088
- expect(props.buffer.handleInput).toHaveBeenCalledTimes(1);
1089
- expect(props.buffer.handleInput).toHaveBeenCalledWith(expect.objectContaining({
1090
- paste: true,
1091
- sequence: pastedText,
1092
- }));
985
+ await act(async () => {
986
+ stdin.write(`\x1b[200~${pastedText}\x1b[201~`);
987
+ });
988
+ await vi.waitFor(() => {
989
+ // Verify that the buffer's handleInput was called once with the full text
990
+ expect(props.buffer.handleInput).toHaveBeenCalledTimes(1);
991
+ expect(props.buffer.handleInput).toHaveBeenCalledWith(expect.objectContaining({
992
+ paste: true,
993
+ sequence: pastedText,
994
+ }));
995
+ });
1093
996
  unmount();
1094
997
  });
1095
998
  });
@@ -1111,10 +1014,13 @@ describe('InputPrompt', () => {
1111
1014
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1112
1015
  await vi.runAllTimersAsync();
1113
1016
  // Simulate a paste operation (this should set the paste protection)
1114
- stdin.write(`\x1b[200~pasted content\x1b[201~`);
1115
- await vi.runAllTimersAsync();
1017
+ await act(async () => {
1018
+ stdin.write(`\x1b[200~pasted content\x1b[201~`);
1019
+ });
1116
1020
  // Simulate an Enter key press immediately after paste
1117
- stdin.write('\r');
1021
+ await act(async () => {
1022
+ stdin.write('\r');
1023
+ });
1118
1024
  await vi.runAllTimersAsync();
1119
1025
  // Verify that onSubmit was NOT called due to recent paste protection
1120
1026
  expect(props.onSubmit).not.toHaveBeenCalled();
@@ -1128,7 +1034,7 @@ describe('InputPrompt', () => {
1128
1034
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1129
1035
  await vi.runAllTimersAsync();
1130
1036
  // Simulate a paste operation (this sets the protection)
1131
- act(() => {
1037
+ await act(async () => {
1132
1038
  stdin.write('\x1b[200~pasted text\x1b[201~');
1133
1039
  });
1134
1040
  await vi.runAllTimersAsync();
@@ -1137,7 +1043,9 @@ describe('InputPrompt', () => {
1137
1043
  await vi.advanceTimersByTimeAsync(50);
1138
1044
  });
1139
1045
  // Now Enter should work normally
1140
- stdin.write('\r');
1046
+ await act(async () => {
1047
+ stdin.write('\r');
1048
+ });
1141
1049
  await vi.runAllTimersAsync();
1142
1050
  expect(props.onSubmit).toHaveBeenCalledWith('pasted text');
1143
1051
  expect(props.buffer.newline).not.toHaveBeenCalled();
@@ -1158,10 +1066,14 @@ describe('InputPrompt', () => {
1158
1066
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), { kittyProtocolEnabled: true });
1159
1067
  await vi.runAllTimersAsync();
1160
1068
  // Simulate a paste operation
1161
- stdin.write('\x1b[200~some pasted stuff\x1b[201~');
1069
+ await act(async () => {
1070
+ stdin.write('\x1b[200~some pasted stuff\x1b[201~');
1071
+ });
1162
1072
  await vi.runAllTimersAsync();
1163
1073
  // Simulate an Enter key press immediately after paste
1164
- stdin.write('\r');
1074
+ await act(async () => {
1075
+ stdin.write('\r');
1076
+ });
1165
1077
  await vi.runAllTimersAsync();
1166
1078
  // Verify that onSubmit was called
1167
1079
  expect(props.onSubmit).toHaveBeenCalledWith('pasted command');
@@ -1173,7 +1085,9 @@ describe('InputPrompt', () => {
1173
1085
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1174
1086
  await vi.runAllTimersAsync();
1175
1087
  // Press Enter without any recent paste
1176
- stdin.write('\r');
1088
+ await act(async () => {
1089
+ stdin.write('\r');
1090
+ });
1177
1091
  await vi.runAllTimersAsync();
1178
1092
  // Verify that onSubmit was called normally
1179
1093
  expect(props.onSubmit).toHaveBeenCalledWith('normal command');
@@ -1186,13 +1100,19 @@ describe('InputPrompt', () => {
1186
1100
  props.onEscapePromptChange = onEscapePromptChange;
1187
1101
  props.buffer.setText('text to clear');
1188
1102
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), { kittyProtocolEnabled: false });
1189
- await wait();
1190
- stdin.write('\x1B');
1191
- await wait();
1192
- stdin.write('\x1B');
1193
- await wait();
1194
- expect(props.buffer.setText).toHaveBeenCalledWith('');
1195
- expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
1103
+ await act(async () => {
1104
+ stdin.write('\x1B');
1105
+ });
1106
+ await vi.waitFor(() => {
1107
+ expect(onEscapePromptChange).toHaveBeenCalledWith(true);
1108
+ });
1109
+ await act(async () => {
1110
+ stdin.write('\x1B');
1111
+ });
1112
+ await vi.waitFor(() => {
1113
+ expect(props.buffer.setText).toHaveBeenCalledWith('');
1114
+ expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
1115
+ });
1196
1116
  unmount();
1197
1117
  });
1198
1118
  it('should reset escape state on any non-ESC key', async () => {
@@ -1200,12 +1120,16 @@ describe('InputPrompt', () => {
1200
1120
  props.onEscapePromptChange = onEscapePromptChange;
1201
1121
  props.buffer.setText('some text');
1202
1122
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), { kittyProtocolEnabled: false });
1203
- stdin.write('\x1B');
1204
- await waitFor(() => {
1123
+ await act(async () => {
1124
+ stdin.write('\x1B');
1125
+ });
1126
+ await vi.waitFor(() => {
1205
1127
  expect(onEscapePromptChange).toHaveBeenCalledWith(true);
1206
1128
  });
1207
- stdin.write('a');
1208
- await waitFor(() => {
1129
+ await act(async () => {
1130
+ stdin.write('a');
1131
+ });
1132
+ await vi.waitFor(() => {
1209
1133
  expect(onEscapePromptChange).toHaveBeenCalledWith(false);
1210
1134
  });
1211
1135
  unmount();
@@ -1213,10 +1137,10 @@ describe('InputPrompt', () => {
1213
1137
  it('should handle ESC in shell mode by disabling shell mode', async () => {
1214
1138
  props.shellModeActive = true;
1215
1139
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), { kittyProtocolEnabled: false });
1216
- await wait();
1217
- stdin.write('\x1B');
1218
- await wait();
1219
- expect(props.setShellModeActive).toHaveBeenCalledWith(false);
1140
+ await act(async () => {
1141
+ stdin.write('\x1B');
1142
+ });
1143
+ await vi.waitFor(() => expect(props.setShellModeActive).toHaveBeenCalledWith(false));
1220
1144
  unmount();
1221
1145
  });
1222
1146
  it('should handle ESC when completion suggestions are showing', async () => {
@@ -1226,10 +1150,10 @@ describe('InputPrompt', () => {
1226
1150
  suggestions: [{ label: 'suggestion', value: 'suggestion' }],
1227
1151
  });
1228
1152
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), { kittyProtocolEnabled: false });
1229
- await wait();
1230
- stdin.write('\x1B');
1231
- await wait();
1232
- expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled();
1153
+ await act(async () => {
1154
+ stdin.write('\x1B');
1155
+ });
1156
+ await vi.waitFor(() => expect(mockCommandCompletion.resetCompletionState).toHaveBeenCalled());
1233
1157
  unmount();
1234
1158
  });
1235
1159
  it('should not call onEscapePromptChange when not provided', async () => {
@@ -1238,20 +1162,23 @@ describe('InputPrompt', () => {
1238
1162
  props.buffer.setText('some text');
1239
1163
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), { kittyProtocolEnabled: false });
1240
1164
  await vi.runAllTimersAsync();
1241
- stdin.write('\x1B');
1165
+ await act(async () => {
1166
+ stdin.write('\x1B');
1167
+ });
1242
1168
  await vi.runAllTimersAsync();
1243
1169
  vi.useRealTimers();
1244
1170
  unmount();
1245
1171
  });
1246
1172
  it('should not interfere with existing keyboard shortcuts', async () => {
1247
1173
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), { kittyProtocolEnabled: false });
1248
- await wait();
1249
- stdin.write('\x0C');
1250
- await wait();
1251
- expect(props.onClearScreen).toHaveBeenCalled();
1252
- stdin.write('\x01');
1253
- await wait();
1254
- expect(props.buffer.move).toHaveBeenCalledWith('home');
1174
+ await act(async () => {
1175
+ stdin.write('\x0C');
1176
+ });
1177
+ await vi.waitFor(() => expect(props.onClearScreen).toHaveBeenCalled());
1178
+ await act(async () => {
1179
+ stdin.write('\x01');
1180
+ });
1181
+ await vi.waitFor(() => expect(props.buffer.move).toHaveBeenCalledWith('home'));
1255
1182
  unmount();
1256
1183
  });
1257
1184
  });
@@ -1279,12 +1206,11 @@ describe('InputPrompt', () => {
1279
1206
  activeSuggestionIndex: 0,
1280
1207
  });
1281
1208
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1282
- await wait();
1283
1209
  // Trigger reverse search with Ctrl+R
1284
- act(() => {
1210
+ await act(async () => {
1285
1211
  stdin.write('\x12');
1286
1212
  });
1287
- await waitFor(() => {
1213
+ await vi.waitFor(() => {
1288
1214
  const frame = stdout.lastFrame();
1289
1215
  expect(frame).toContain('(r:)');
1290
1216
  expect(frame).toContain('echo hello');
@@ -1295,12 +1221,17 @@ describe('InputPrompt', () => {
1295
1221
  });
1296
1222
  it('resets reverse search state on Escape', async () => {
1297
1223
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1298
- await wait();
1299
- stdin.write('\x12');
1300
- await wait();
1301
- stdin.write('\x1B');
1302
- stdin.write('\u001b[27u'); // Press kitty escape key
1303
- await waitFor(() => {
1224
+ await act(async () => {
1225
+ stdin.write('\x12');
1226
+ });
1227
+ await vi.waitFor(() => { });
1228
+ await act(async () => {
1229
+ stdin.write('\x1B');
1230
+ });
1231
+ await act(async () => {
1232
+ stdin.write('\u001b[27u'); // Press kitty escape key
1233
+ });
1234
+ await vi.waitFor(() => {
1304
1235
  expect(stdout.lastFrame()).not.toContain('(r:)');
1305
1236
  });
1306
1237
  expect(stdout.lastFrame()).not.toContain('echo hello');
@@ -1326,20 +1257,21 @@ describe('InputPrompt', () => {
1326
1257
  }));
1327
1258
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1328
1259
  // Enter reverse search mode with Ctrl+R
1329
- act(() => {
1260
+ await act(async () => {
1330
1261
  stdin.write('\x12');
1331
1262
  });
1332
1263
  // Verify reverse search is active
1333
- await waitFor(() => {
1264
+ await vi.waitFor(() => {
1334
1265
  expect(stdout.lastFrame()).toContain('(r:)');
1335
1266
  });
1336
1267
  // Press Tab to complete the highlighted entry
1337
- act(() => {
1268
+ await act(async () => {
1338
1269
  stdin.write('\t');
1339
1270
  });
1340
- await wait();
1341
- expect(mockHandleAutocomplete).toHaveBeenCalledWith(0);
1342
- expect(props.buffer.setText).toHaveBeenCalledWith('echo hello');
1271
+ await vi.waitFor(() => {
1272
+ expect(mockHandleAutocomplete).toHaveBeenCalledWith(0);
1273
+ expect(props.buffer.setText).toHaveBeenCalledWith('echo hello');
1274
+ });
1343
1275
  unmount();
1344
1276
  }, 15000);
1345
1277
  it('submits the highlighted entry on Enter and exits reverse-search', async () => {
@@ -1355,16 +1287,16 @@ describe('InputPrompt', () => {
1355
1287
  activeSuggestionIndex: 0,
1356
1288
  });
1357
1289
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1358
- act(() => {
1290
+ await act(async () => {
1359
1291
  stdin.write('\x12');
1360
1292
  });
1361
- await waitFor(() => {
1293
+ await vi.waitFor(() => {
1362
1294
  expect(stdout.lastFrame()).toContain('(r:)');
1363
1295
  });
1364
- act(() => {
1296
+ await act(async () => {
1365
1297
  stdin.write('\r');
1366
1298
  });
1367
- await waitFor(() => {
1299
+ await vi.waitFor(() => {
1368
1300
  expect(stdout.lastFrame()).not.toContain('(r:)');
1369
1301
  });
1370
1302
  expect(props.onSubmit).toHaveBeenCalledWith('echo hello');
@@ -1384,19 +1316,18 @@ describe('InputPrompt', () => {
1384
1316
  showSuggestions: reverseSearchActiveFromInputPrompt,
1385
1317
  }));
1386
1318
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1387
- await wait();
1388
1319
  // reverse search with Ctrl+R
1389
- act(() => {
1320
+ await act(async () => {
1390
1321
  stdin.write('\x12');
1391
1322
  });
1392
- await waitFor(() => {
1323
+ await vi.waitFor(() => {
1393
1324
  expect(stdout.lastFrame()).toContain('(r:)');
1394
1325
  });
1395
1326
  // Press kitty escape key
1396
- act(() => {
1327
+ await act(async () => {
1397
1328
  stdin.write('\u001b[27u');
1398
1329
  });
1399
- await waitFor(() => {
1330
+ await vi.waitFor(() => {
1400
1331
  expect(stdout.lastFrame()).not.toContain('(r:)');
1401
1332
  expect(props.buffer.text).toBe(initialText);
1402
1333
  expect(props.buffer.cursor).toEqual(initialCursor);
@@ -1410,10 +1341,12 @@ describe('InputPrompt', () => {
1410
1341
  props.buffer.cursor = [1, 2];
1411
1342
  props.buffer.lines = ['line 1', 'line 2', 'line 3'];
1412
1343
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1413
- await wait();
1414
- stdin.write('\x05'); // Ctrl+E
1415
- await wait();
1416
- expect(props.buffer.move).toHaveBeenCalledWith('end');
1344
+ await act(async () => {
1345
+ stdin.write('\x05'); // Ctrl+E
1346
+ });
1347
+ await vi.waitFor(() => {
1348
+ expect(props.buffer.move).toHaveBeenCalledWith('end');
1349
+ });
1417
1350
  expect(props.buffer.moveToOffset).not.toHaveBeenCalled();
1418
1351
  unmount();
1419
1352
  });
@@ -1422,10 +1355,12 @@ describe('InputPrompt', () => {
1422
1355
  props.buffer.cursor = [0, 5];
1423
1356
  props.buffer.lines = ['single line text'];
1424
1357
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1425
- await wait();
1426
- stdin.write('\x05'); // Ctrl+E
1427
- await wait();
1428
- expect(props.buffer.move).toHaveBeenCalledWith('end');
1358
+ await act(async () => {
1359
+ stdin.write('\x05'); // Ctrl+E
1360
+ });
1361
+ await vi.waitFor(() => {
1362
+ expect(props.buffer.move).toHaveBeenCalledWith('end');
1363
+ });
1429
1364
  expect(props.buffer.moveToOffset).not.toHaveBeenCalled();
1430
1365
  unmount();
1431
1366
  });
@@ -1445,18 +1380,18 @@ describe('InputPrompt', () => {
1445
1380
  activeSuggestionIndex: isActive ? 0 : -1,
1446
1381
  }));
1447
1382
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1448
- await wait();
1449
- act(() => {
1383
+ await act(async () => {
1450
1384
  stdin.write('\x12'); // Ctrl+R
1451
1385
  });
1452
- await wait();
1453
- const frame = stdout.lastFrame() ?? '';
1454
- expect(frame).toContain('(r:)');
1455
- expect(frame).toContain('git commit');
1456
- expect(frame).toContain('git push');
1386
+ await vi.waitFor(() => {
1387
+ const frame = stdout.lastFrame() ?? '';
1388
+ expect(frame).toContain('(r:)');
1389
+ expect(frame).toContain('git commit');
1390
+ expect(frame).toContain('git push');
1391
+ });
1457
1392
  unmount();
1458
1393
  });
1459
- it.skip('expands and collapses long suggestion via Right/Left arrows', async () => {
1394
+ it('expands and collapses long suggestion via Right/Left arrows', async () => {
1460
1395
  props.shellModeActive = false;
1461
1396
  const longValue = 'l'.repeat(200);
1462
1397
  vi.mocked(useReverseSearchCompletion).mockReturnValue({
@@ -1468,18 +1403,26 @@ describe('InputPrompt', () => {
1468
1403
  isLoadingSuggestions: false,
1469
1404
  });
1470
1405
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1471
- await wait();
1472
- stdin.write('\x12');
1473
- await wait();
1474
- expect(clean(stdout.lastFrame())).toContain('→');
1475
- stdin.write('\u001B[C');
1476
- await wait(200);
1477
- expect(clean(stdout.lastFrame())).toContain('←');
1478
- expect(stdout.lastFrame()).toMatchSnapshot('command-search-expanded-match');
1479
- stdin.write('\u001B[D');
1480
- await wait();
1481
- expect(clean(stdout.lastFrame())).toContain('');
1482
- expect(stdout.lastFrame()).toMatchSnapshot('command-search-collapsed-match');
1406
+ await act(async () => {
1407
+ stdin.write('\x12');
1408
+ });
1409
+ await vi.waitFor(() => {
1410
+ expect(clean(stdout.lastFrame())).toContain('');
1411
+ });
1412
+ await act(async () => {
1413
+ stdin.write('\u001B[C');
1414
+ });
1415
+ await vi.waitFor(() => {
1416
+ expect(clean(stdout.lastFrame())).toContain('');
1417
+ });
1418
+ expect(stdout.lastFrame()).toMatchSnapshot('command-search-render-expanded-match');
1419
+ await act(async () => {
1420
+ stdin.write('\u001B[D');
1421
+ });
1422
+ await vi.waitFor(() => {
1423
+ expect(clean(stdout.lastFrame())).toContain('→');
1424
+ });
1425
+ expect(stdout.lastFrame()).toMatchSnapshot('command-search-render-collapsed-match');
1483
1426
  unmount();
1484
1427
  });
1485
1428
  it('renders match window and expanded view (snapshots)', async () => {
@@ -1496,13 +1439,18 @@ describe('InputPrompt', () => {
1496
1439
  isLoadingSuggestions: false,
1497
1440
  });
1498
1441
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1499
- await wait();
1500
- stdin.write('\x12');
1501
- await wait();
1502
- expect(stdout.lastFrame()).toMatchSnapshot('command-search-collapsed-match');
1503
- stdin.write('\u001B[C');
1504
- await wait();
1505
- expect(stdout.lastFrame()).toMatchSnapshot('command-search-expanded-match');
1442
+ await act(async () => {
1443
+ stdin.write('\x12');
1444
+ });
1445
+ await vi.waitFor(() => {
1446
+ expect(stdout.lastFrame()).toMatchSnapshot('command-search-render-collapsed-match');
1447
+ });
1448
+ await act(async () => {
1449
+ stdin.write('\u001B[C');
1450
+ });
1451
+ await vi.waitFor(() => {
1452
+ expect(stdout.lastFrame()).toMatchSnapshot('command-search-render-expanded-match');
1453
+ });
1506
1454
  unmount();
1507
1455
  });
1508
1456
  it('does not show expand/collapse indicator for short suggestions', async () => {
@@ -1517,12 +1465,16 @@ describe('InputPrompt', () => {
1517
1465
  isLoadingSuggestions: false,
1518
1466
  });
1519
1467
  const { stdin, stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1520
- await wait();
1521
- stdin.write('\x12');
1522
- await wait();
1523
- const frame = clean(stdout.lastFrame());
1524
- expect(frame).not.toContain('→');
1525
- expect(frame).not.toContain('←');
1468
+ await act(async () => {
1469
+ stdin.write('\x12');
1470
+ });
1471
+ await vi.waitFor(() => {
1472
+ const frame = clean(stdout.lastFrame());
1473
+ // Ensure it rendered the search mode
1474
+ expect(frame).toContain('(r:)');
1475
+ expect(frame).not.toContain('→');
1476
+ expect(frame).not.toContain('←');
1477
+ });
1526
1478
  unmount();
1527
1479
  });
1528
1480
  });
@@ -1532,12 +1484,12 @@ describe('InputPrompt', () => {
1532
1484
  props.popAllMessages = mockPopAllMessages;
1533
1485
  props.buffer.text = '';
1534
1486
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1535
- await wait();
1536
- stdin.write('\u001B[A');
1537
- await wait();
1538
- expect(mockPopAllMessages).toHaveBeenCalled();
1487
+ await act(async () => {
1488
+ stdin.write('\u001B[A');
1489
+ });
1490
+ await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
1539
1491
  const callback = mockPopAllMessages.mock.calls[0][0];
1540
- act(() => {
1492
+ await act(async () => {
1541
1493
  callback('Message 1\n\nMessage 2\n\nMessage 3');
1542
1494
  });
1543
1495
  expect(props.buffer.setText).toHaveBeenCalledWith('Message 1\n\nMessage 2\n\nMessage 3');
@@ -1548,11 +1500,11 @@ describe('InputPrompt', () => {
1548
1500
  props.popAllMessages = mockPopAllMessages;
1549
1501
  props.buffer.text = 'some text';
1550
1502
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1551
- await wait();
1552
- stdin.write('\u001B[A');
1553
- await wait();
1503
+ await act(async () => {
1504
+ stdin.write('\u001B[A');
1505
+ });
1506
+ await vi.waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
1554
1507
  expect(mockPopAllMessages).not.toHaveBeenCalled();
1555
- expect(mockInputHistory.navigateUp).toHaveBeenCalled();
1556
1508
  unmount();
1557
1509
  });
1558
1510
  it('should handle undefined messages from popAllMessages', async () => {
@@ -1560,12 +1512,12 @@ describe('InputPrompt', () => {
1560
1512
  props.popAllMessages = mockPopAllMessages;
1561
1513
  props.buffer.text = '';
1562
1514
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1563
- await wait();
1564
- stdin.write('\u001B[A');
1565
- await wait();
1566
- expect(mockPopAllMessages).toHaveBeenCalled();
1515
+ await act(async () => {
1516
+ stdin.write('\u001B[A');
1517
+ });
1518
+ await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
1567
1519
  const callback = mockPopAllMessages.mock.calls[0][0];
1568
- act(() => {
1520
+ await act(async () => {
1569
1521
  callback(undefined);
1570
1522
  });
1571
1523
  expect(props.buffer.setText).not.toHaveBeenCalled();
@@ -1580,10 +1532,10 @@ describe('InputPrompt', () => {
1580
1532
  props.buffer.visualCursor = [0, 0];
1581
1533
  props.buffer.visualScrollRow = 0;
1582
1534
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1583
- await wait();
1584
- stdin.write('\u001B[A');
1585
- await wait();
1586
- expect(mockPopAllMessages).toHaveBeenCalled();
1535
+ await act(async () => {
1536
+ stdin.write('\u001B[A');
1537
+ });
1538
+ await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
1587
1539
  unmount();
1588
1540
  });
1589
1541
  it('should handle single queued message', async () => {
@@ -1591,11 +1543,12 @@ describe('InputPrompt', () => {
1591
1543
  props.popAllMessages = mockPopAllMessages;
1592
1544
  props.buffer.text = '';
1593
1545
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1594
- await wait();
1595
- stdin.write('\u001B[A');
1596
- await wait();
1546
+ await act(async () => {
1547
+ stdin.write('\u001B[A');
1548
+ });
1549
+ await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
1597
1550
  const callback = mockPopAllMessages.mock.calls[0][0];
1598
- act(() => {
1551
+ await act(async () => {
1599
1552
  callback('Single message');
1600
1553
  });
1601
1554
  expect(props.buffer.setText).toHaveBeenCalledWith('Single message');
@@ -1606,20 +1559,20 @@ describe('InputPrompt', () => {
1606
1559
  props.popAllMessages = mockPopAllMessages;
1607
1560
  props.buffer.text = ' '; // Whitespace only
1608
1561
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1609
- await wait();
1610
- stdin.write('\u001B[A');
1611
- await wait();
1612
- expect(mockPopAllMessages).toHaveBeenCalled();
1562
+ await act(async () => {
1563
+ stdin.write('\u001B[A');
1564
+ });
1565
+ await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
1613
1566
  unmount();
1614
1567
  });
1615
1568
  it('should not call popAllMessages if it is not provided', async () => {
1616
1569
  props.popAllMessages = undefined;
1617
1570
  props.buffer.text = '';
1618
1571
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1619
- await wait();
1620
- stdin.write('\u001B[A');
1621
- await wait();
1622
- expect(mockInputHistory.navigateUp).toHaveBeenCalled();
1572
+ await act(async () => {
1573
+ stdin.write('\u001B[A');
1574
+ });
1575
+ await vi.waitFor(() => expect(mockInputHistory.navigateUp).toHaveBeenCalled());
1623
1576
  unmount();
1624
1577
  });
1625
1578
  it('should navigate input history on fresh start when no queued messages exist', async () => {
@@ -1627,12 +1580,12 @@ describe('InputPrompt', () => {
1627
1580
  props.popAllMessages = mockPopAllMessages;
1628
1581
  props.buffer.text = '';
1629
1582
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1630
- await wait();
1631
- stdin.write('\u001B[A');
1632
- await wait();
1633
- expect(mockPopAllMessages).toHaveBeenCalled();
1583
+ await act(async () => {
1584
+ stdin.write('\u001B[A');
1585
+ });
1586
+ await vi.waitFor(() => expect(mockPopAllMessages).toHaveBeenCalled());
1634
1587
  const callback = mockPopAllMessages.mock.calls[0][0];
1635
- act(() => {
1588
+ await act(async () => {
1636
1589
  callback(undefined);
1637
1590
  });
1638
1591
  expect(mockInputHistory.navigateUp).toHaveBeenCalled();
@@ -1644,33 +1597,31 @@ describe('InputPrompt', () => {
1644
1597
  it('should render correctly in shell mode', async () => {
1645
1598
  props.shellModeActive = true;
1646
1599
  const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1647
- await wait();
1648
- expect(stdout.lastFrame()).toMatchSnapshot();
1600
+ await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
1649
1601
  unmount();
1650
1602
  });
1651
1603
  it('should render correctly when accepting edits', async () => {
1652
1604
  props.approvalMode = ApprovalMode.AUTO_EDIT;
1653
1605
  const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1654
- await wait();
1655
- expect(stdout.lastFrame()).toMatchSnapshot();
1606
+ await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
1656
1607
  unmount();
1657
1608
  });
1658
1609
  it('should render correctly in yolo mode', async () => {
1659
1610
  props.approvalMode = ApprovalMode.YOLO;
1660
1611
  const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1661
- await wait();
1662
- expect(stdout.lastFrame()).toMatchSnapshot();
1612
+ await vi.waitFor(() => expect(stdout.lastFrame()).toMatchSnapshot());
1663
1613
  unmount();
1664
1614
  });
1665
1615
  it('should not show inverted cursor when shell is focused', async () => {
1666
1616
  props.isEmbeddedShellFocused = true;
1667
1617
  props.focus = false;
1668
1618
  const { stdout, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1669
- await wait();
1670
- expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
1671
- // This snapshot is good to make sure there was an input prompt but does
1672
- // not show the inverted cursor because snapshots do not show colors.
1673
- expect(stdout.lastFrame()).toMatchSnapshot();
1619
+ await vi.waitFor(() => {
1620
+ expect(stdout.lastFrame()).not.toContain(`{chalk.inverse(' ')}`);
1621
+ // This snapshot is good to make sure there was an input prompt but does
1622
+ // not show the inverted cursor because snapshots do not show colors.
1623
+ expect(stdout.lastFrame()).toMatchSnapshot();
1624
+ });
1674
1625
  unmount();
1675
1626
  });
1676
1627
  });
@@ -1678,54 +1629,59 @@ describe('InputPrompt', () => {
1678
1629
  const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }), {
1679
1630
  shellFocus: false,
1680
1631
  });
1681
- await wait();
1682
- stdin.write('a');
1683
- await wait();
1684
- expect(mockBuffer.handleInput).toHaveBeenCalled();
1685
- unmount();
1686
- });
1687
- it('should prevent slash commands from being queued while streaming', async () => {
1688
- props.onSubmit = vi.fn();
1689
- props.buffer.text = '/help';
1690
- props.setQueueErrorMessage = vi.fn();
1691
- props.streamingState = StreamingState.Responding;
1692
- const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1693
- await wait();
1694
- stdin.write('/help');
1695
- stdin.write('\r');
1696
- await wait();
1697
- expect(props.onSubmit).not.toHaveBeenCalled();
1698
- expect(props.setQueueErrorMessage).toHaveBeenCalledWith('Slash commands cannot be queued');
1699
- unmount();
1700
- });
1701
- it('should prevent shell commands from being queued while streaming', async () => {
1702
- props.onSubmit = vi.fn();
1703
- props.buffer.text = 'ls';
1704
- props.setQueueErrorMessage = vi.fn();
1705
- props.streamingState = StreamingState.Responding;
1706
- props.shellModeActive = true;
1707
- const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1708
- await wait();
1709
- stdin.write('ls');
1710
- stdin.write('\r');
1711
- await wait();
1712
- expect(props.onSubmit).not.toHaveBeenCalled();
1713
- expect(props.setQueueErrorMessage).toHaveBeenCalledWith('Shell commands cannot be queued');
1632
+ await act(async () => {
1633
+ stdin.write('a');
1634
+ });
1635
+ await vi.waitFor(() => expect(mockBuffer.handleInput).toHaveBeenCalled());
1714
1636
  unmount();
1715
1637
  });
1716
- it('should allow regular messages to be queued while streaming', async () => {
1717
- props.onSubmit = vi.fn();
1718
- props.buffer.text = 'regular message';
1719
- props.setQueueErrorMessage = vi.fn();
1720
- props.streamingState = StreamingState.Responding;
1721
- const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1722
- await wait();
1723
- stdin.write('regular message');
1724
- stdin.write('\r');
1725
- await wait();
1726
- expect(props.onSubmit).toHaveBeenCalledWith('regular message');
1727
- expect(props.setQueueErrorMessage).not.toHaveBeenCalled();
1728
- unmount();
1638
+ describe('command queuing while streaming', () => {
1639
+ beforeEach(() => {
1640
+ props.streamingState = StreamingState.Responding;
1641
+ props.setQueueErrorMessage = vi.fn();
1642
+ props.onSubmit = vi.fn();
1643
+ });
1644
+ it.each([
1645
+ {
1646
+ name: 'should prevent slash commands',
1647
+ bufferText: '/help',
1648
+ shellMode: false,
1649
+ shouldSubmit: false,
1650
+ errorMessage: 'Slash commands cannot be queued',
1651
+ },
1652
+ {
1653
+ name: 'should prevent shell commands',
1654
+ bufferText: 'ls',
1655
+ shellMode: true,
1656
+ shouldSubmit: false,
1657
+ errorMessage: 'Shell commands cannot be queued',
1658
+ },
1659
+ {
1660
+ name: 'should allow regular messages',
1661
+ bufferText: 'regular message',
1662
+ shellMode: false,
1663
+ shouldSubmit: true,
1664
+ errorMessage: null,
1665
+ },
1666
+ ])('$name', async ({ bufferText, shellMode, shouldSubmit, errorMessage }) => {
1667
+ props.buffer.text = bufferText;
1668
+ props.shellModeActive = shellMode;
1669
+ const { stdin, unmount } = renderWithProviders(_jsx(InputPrompt, { ...props }));
1670
+ await act(async () => {
1671
+ stdin.write('\r');
1672
+ });
1673
+ await vi.waitFor(() => {
1674
+ if (shouldSubmit) {
1675
+ expect(props.onSubmit).toHaveBeenCalledWith(bufferText);
1676
+ expect(props.setQueueErrorMessage).not.toHaveBeenCalled();
1677
+ }
1678
+ else {
1679
+ expect(props.onSubmit).not.toHaveBeenCalled();
1680
+ expect(props.setQueueErrorMessage).toHaveBeenCalledWith(errorMessage);
1681
+ }
1682
+ });
1683
+ unmount();
1684
+ });
1729
1685
  });
1730
1686
  });
1731
1687
  function clean(str) {