@abacus-ai/cli 2.0.0-canary.1 → 2.0.0-canary.11

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 (198) hide show
  1. package/README.md +25 -0
  2. package/dist/index.mjs +466 -438
  3. package/package.json +4 -1
  4. package/.oxlintrc.json +0 -8
  5. package/resources/abacus.ico +0 -0
  6. package/resources/entitlements.plist +0 -9
  7. package/src/__e2e__/README.md +0 -196
  8. package/src/__e2e__/agent-interactions.e2e.test.tsx +0 -61
  9. package/src/__e2e__/cli-commands.e2e.test.tsx +0 -77
  10. package/src/__e2e__/conversation-throttle.e2e.test.ts +0 -453
  11. package/src/__e2e__/conversation.e2e.test.tsx +0 -56
  12. package/src/__e2e__/diff-preview.e2e.test.tsx +0 -3399
  13. package/src/__e2e__/file-creation.e2e.test.tsx +0 -149
  14. package/src/__e2e__/helpers/test-helpers.ts +0 -449
  15. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +0 -34
  16. package/src/__e2e__/llm-models.e2e.test.ts +0 -402
  17. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +0 -71
  18. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +0 -167
  19. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +0 -185
  20. package/src/__e2e__/repl.e2e.test.tsx +0 -78
  21. package/src/__e2e__/shell-compatibility.e2e.test.tsx +0 -76
  22. package/src/__e2e__/theme-mcp.e2e.test.tsx +0 -98
  23. package/src/__e2e__/tool-permissions.e2e.test.tsx +0 -66
  24. package/src/args.ts +0 -22
  25. package/src/components/__tests__/react-compiler.test.tsx +0 -78
  26. package/src/components/__tests__/status-indicator.test.tsx +0 -403
  27. package/src/components/composer/__tests__/bash-runner.test.tsx +0 -263
  28. package/src/components/composer/agent-mode-indicator.tsx +0 -63
  29. package/src/components/composer/bash-runner.tsx +0 -54
  30. package/src/components/composer/commands/default-commands.tsx +0 -615
  31. package/src/components/composer/commands/handler.tsx +0 -59
  32. package/src/components/composer/commands/picker.tsx +0 -273
  33. package/src/components/composer/commands/registry.ts +0 -233
  34. package/src/components/composer/commands/types.ts +0 -33
  35. package/src/components/composer/context.tsx +0 -88
  36. package/src/components/composer/file-mention-picker.tsx +0 -83
  37. package/src/components/composer/help.tsx +0 -44
  38. package/src/components/composer/index.tsx +0 -1007
  39. package/src/components/composer/mentions.ts +0 -57
  40. package/src/components/composer/message-queue.tsx +0 -70
  41. package/src/components/composer/mode-panel.tsx +0 -35
  42. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +0 -755
  43. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +0 -1108
  44. package/src/components/composer/modes/bash-handler.tsx +0 -132
  45. package/src/components/composer/modes/bash-renderer.tsx +0 -175
  46. package/src/components/composer/modes/default-handlers.tsx +0 -33
  47. package/src/components/composer/modes/index.ts +0 -41
  48. package/src/components/composer/modes/types.ts +0 -21
  49. package/src/components/composer/persistent-shell.ts +0 -283
  50. package/src/components/composer/process.ts +0 -65
  51. package/src/components/composer/types.ts +0 -9
  52. package/src/components/composer/use-mention-search.ts +0 -68
  53. package/src/components/error-boundry.tsx +0 -60
  54. package/src/components/exit-message.tsx +0 -29
  55. package/src/components/expanded-view.tsx +0 -74
  56. package/src/components/file-completion.tsx +0 -127
  57. package/src/components/header.tsx +0 -47
  58. package/src/components/logo.tsx +0 -37
  59. package/src/components/segments.tsx +0 -356
  60. package/src/components/status-indicator.tsx +0 -306
  61. package/src/components/tool-group-summary.tsx +0 -263
  62. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +0 -319
  63. package/src/components/tool-permissions/diff-preview.tsx +0 -359
  64. package/src/components/tool-permissions/index.ts +0 -5
  65. package/src/components/tool-permissions/permission-options.tsx +0 -401
  66. package/src/components/tool-permissions/permission-preview-header.tsx +0 -57
  67. package/src/components/tool-permissions/tool-permission-ui.tsx +0 -420
  68. package/src/components/tools/agent/ask-user-question.tsx +0 -107
  69. package/src/components/tools/agent/enter-plan-mode.tsx +0 -55
  70. package/src/components/tools/agent/exit-plan-mode.tsx +0 -83
  71. package/src/components/tools/agent/handoff-to-main.tsx +0 -27
  72. package/src/components/tools/agent/subagent.tsx +0 -37
  73. package/src/components/tools/agent/todo-write.tsx +0 -104
  74. package/src/components/tools/browser/close-tab.tsx +0 -58
  75. package/src/components/tools/browser/computer.tsx +0 -70
  76. package/src/components/tools/browser/get-interactive-elements.tsx +0 -54
  77. package/src/components/tools/browser/get-tab-content.tsx +0 -51
  78. package/src/components/tools/browser/navigate-to.tsx +0 -59
  79. package/src/components/tools/browser/new-tab.tsx +0 -60
  80. package/src/components/tools/browser/perform-action.tsx +0 -63
  81. package/src/components/tools/browser/refresh-tab.tsx +0 -43
  82. package/src/components/tools/browser/switch-tab.tsx +0 -58
  83. package/src/components/tools/filesystem/delete-file.tsx +0 -104
  84. package/src/components/tools/filesystem/edit.tsx +0 -220
  85. package/src/components/tools/filesystem/list-dir.tsx +0 -78
  86. package/src/components/tools/filesystem/read-file.tsx +0 -180
  87. package/src/components/tools/filesystem/upload-image.tsx +0 -76
  88. package/src/components/tools/ide/ide-diagnostics.tsx +0 -62
  89. package/src/components/tools/index.ts +0 -91
  90. package/src/components/tools/mcp/mcp-tool.tsx +0 -158
  91. package/src/components/tools/search/fetch-url.tsx +0 -73
  92. package/src/components/tools/search/file-search.tsx +0 -78
  93. package/src/components/tools/search/grep.tsx +0 -90
  94. package/src/components/tools/search/semantic-search.tsx +0 -66
  95. package/src/components/tools/search/web-search.tsx +0 -71
  96. package/src/components/tools/shared/index.tsx +0 -48
  97. package/src/components/tools/shared/zod-coercion.ts +0 -35
  98. package/src/components/tools/terminal/bash-tool-output.tsx +0 -188
  99. package/src/components/tools/terminal/get-terminal-output.tsx +0 -91
  100. package/src/components/tools/terminal/run-in-terminal.tsx +0 -131
  101. package/src/components/tools/types.ts +0 -16
  102. package/src/components/tools.tsx +0 -68
  103. package/src/components/ui/__tests__/divider.test.tsx +0 -61
  104. package/src/components/ui/__tests__/gradient.test.tsx +0 -125
  105. package/src/components/ui/__tests__/input.test.tsx +0 -166
  106. package/src/components/ui/__tests__/select.test.tsx +0 -273
  107. package/src/components/ui/__tests__/shimmer.test.tsx +0 -99
  108. package/src/components/ui/blinking-indicator.tsx +0 -27
  109. package/src/components/ui/divider.tsx +0 -162
  110. package/src/components/ui/gradient.tsx +0 -56
  111. package/src/components/ui/input.tsx +0 -228
  112. package/src/components/ui/select.tsx +0 -151
  113. package/src/components/ui/shimmer.tsx +0 -76
  114. package/src/context/agent-mode.tsx +0 -95
  115. package/src/context/extension-file.tsx +0 -136
  116. package/src/context/network-activity.tsx +0 -45
  117. package/src/context/notification.tsx +0 -62
  118. package/src/context/shell-size.tsx +0 -49
  119. package/src/context/shell-title.tsx +0 -38
  120. package/src/entrypoints/print-mode.ts +0 -312
  121. package/src/entrypoints/repl.tsx +0 -389
  122. package/src/hooks/use-agent.ts +0 -15
  123. package/src/hooks/use-api-client.ts +0 -1
  124. package/src/hooks/use-available-height.ts +0 -8
  125. package/src/hooks/use-cleanup.ts +0 -29
  126. package/src/hooks/use-interrupt-manager.ts +0 -242
  127. package/src/hooks/use-models.ts +0 -22
  128. package/src/index.ts +0 -217
  129. package/src/lib/__tests__/ansi.test.ts +0 -255
  130. package/src/lib/__tests__/cli.test.ts +0 -122
  131. package/src/lib/__tests__/commands.test.ts +0 -325
  132. package/src/lib/__tests__/constants.test.ts +0 -15
  133. package/src/lib/__tests__/focusables.test.ts +0 -25
  134. package/src/lib/__tests__/fs.test.ts +0 -231
  135. package/src/lib/__tests__/markdown.test.tsx +0 -348
  136. package/src/lib/__tests__/mcpCommandHandler.test.ts +0 -173
  137. package/src/lib/__tests__/mcpManagement.test.ts +0 -38
  138. package/src/lib/__tests__/path-paste.test.ts +0 -144
  139. package/src/lib/__tests__/path.test.ts +0 -300
  140. package/src/lib/__tests__/queries.test.ts +0 -39
  141. package/src/lib/__tests__/standaloneMcpService.test.ts +0 -71
  142. package/src/lib/__tests__/text-buffer.test.ts +0 -328
  143. package/src/lib/__tests__/text-utils.test.ts +0 -32
  144. package/src/lib/__tests__/timing.test.ts +0 -78
  145. package/src/lib/__tests__/utils.test.ts +0 -238
  146. package/src/lib/__tests__/vim-buffer-actions.test.ts +0 -154
  147. package/src/lib/ansi.ts +0 -150
  148. package/src/lib/cli-push-server.ts +0 -112
  149. package/src/lib/cli.ts +0 -44
  150. package/src/lib/clipboard.ts +0 -226
  151. package/src/lib/command-utils.ts +0 -93
  152. package/src/lib/commands.ts +0 -270
  153. package/src/lib/constants.ts +0 -3
  154. package/src/lib/extension-connection.ts +0 -181
  155. package/src/lib/focusables.ts +0 -7
  156. package/src/lib/fs.ts +0 -533
  157. package/src/lib/markdown/code-block.tsx +0 -63
  158. package/src/lib/markdown/index.ts +0 -4
  159. package/src/lib/markdown/link.tsx +0 -19
  160. package/src/lib/markdown/markdown.tsx +0 -372
  161. package/src/lib/markdown/types.ts +0 -15
  162. package/src/lib/mcpCommandHandler.ts +0 -121
  163. package/src/lib/mcpManagement.ts +0 -44
  164. package/src/lib/path-paste.ts +0 -185
  165. package/src/lib/path.ts +0 -179
  166. package/src/lib/queries.ts +0 -15
  167. package/src/lib/standaloneMcpService.ts +0 -688
  168. package/src/lib/status-utils.ts +0 -237
  169. package/src/lib/test-utils.tsx +0 -72
  170. package/src/lib/text-buffer.ts +0 -2415
  171. package/src/lib/text-utils.ts +0 -272
  172. package/src/lib/timing.ts +0 -63
  173. package/src/lib/types.ts +0 -295
  174. package/src/lib/utils.ts +0 -182
  175. package/src/lib/vim-buffer-actions.ts +0 -732
  176. package/src/providers/agent.tsx +0 -1063
  177. package/src/providers/api-client.tsx +0 -43
  178. package/src/services/logger.ts +0 -85
  179. package/src/terminal/detection.ts +0 -187
  180. package/src/terminal/exit.ts +0 -279
  181. package/src/terminal/notification.ts +0 -83
  182. package/src/terminal/progress.ts +0 -201
  183. package/src/terminal/setup.ts +0 -797
  184. package/src/terminal/types.ts +0 -51
  185. package/src/theme/context.tsx +0 -57
  186. package/src/theme/index.ts +0 -4
  187. package/src/theme/themed.tsx +0 -35
  188. package/src/theme/themes.json +0 -546
  189. package/src/theme/types.ts +0 -110
  190. package/src/tools/types.ts +0 -59
  191. package/src/tools/utils/__tests__/zod-coercion.test.ts +0 -33
  192. package/src/tools/utils/tool-ui-components.tsx +0 -649
  193. package/src/tools/utils/zod-coercion.ts +0 -35
  194. package/tsconfig.json +0 -16
  195. package/tsconfig.node.json +0 -29
  196. package/tsconfig.test.json +0 -27
  197. package/tsdown.config.ts +0 -17
  198. package/vitest.config.ts +0 -76
@@ -1,1108 +0,0 @@
1
- import { useState, useEffect } from "react";
2
- import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
3
-
4
- import { render, cleanup, logInk } from "../../../../lib/test-utils.js";
5
- import { sleep } from "../../../../lib/utils.js";
6
- import { BashRenderer, type BashRendererProps } from "../../modes/bash-renderer.js";
7
- import { destroyPersistentShell } from "../../persistent-shell.js";
8
-
9
- // Test wrapper component to manage state
10
- function BashRendererWrapper(
11
- props: Omit<BashRendererProps, "state" | "setState"> & {
12
- initialState?: BashRendererProps["state"];
13
- },
14
- ) {
15
- const [state, setState] = useState<BashRendererProps["state"]>(
16
- props.initialState || {
17
- submittedCommand: null,
18
- runId: 0,
19
- output: "",
20
- isRunning: false,
21
- },
22
- );
23
-
24
- return <BashRenderer {...props} state={state} setState={setState} />;
25
- }
26
-
27
- describe.sequential("BashRenderer", () => {
28
- const mockSetState = vi.fn();
29
- const mockOnCommandComplete = vi.fn();
30
-
31
- beforeEach(() => {
32
- vi.clearAllMocks();
33
- // Clean up any existing shell instance before each test
34
- destroyPersistentShell();
35
- });
36
-
37
- afterEach(() => {
38
- cleanup();
39
- // Clean up shell instance after each test
40
- destroyPersistentShell();
41
- });
42
-
43
- it("should render with empty state", () => {
44
- const instance = render(
45
- <BashRenderer
46
- state={{
47
- submittedCommand: null,
48
- runId: 0,
49
- output: "",
50
- isRunning: false,
51
- }}
52
- setState={mockSetState}
53
- onCommandComplete={mockOnCommandComplete}
54
- commandHistory={[]}
55
- currentPrompt=""
56
- availableHeight={20}
57
- />,
58
- );
59
-
60
- logInk(instance);
61
-
62
- const output = instance.frames.join("");
63
- expect(output).toBeDefined();
64
- });
65
-
66
- it("should render command history", () => {
67
- const commandHistory = [
68
- { command: "echo hello", output: "hello" },
69
- { command: "ls", output: "file1.txt\nfile2.txt" },
70
- ];
71
-
72
- const instance = render(
73
- <BashRenderer
74
- state={{
75
- submittedCommand: null,
76
- runId: 0,
77
- output: "",
78
- isRunning: false,
79
- }}
80
- setState={mockSetState}
81
- onCommandComplete={mockOnCommandComplete}
82
- commandHistory={commandHistory}
83
- currentPrompt=""
84
- availableHeight={20}
85
- />,
86
- );
87
-
88
- logInk(instance);
89
-
90
- const output = instance.frames.join("");
91
- expect(output).toContain("echo hello");
92
- expect(output).toContain("ls");
93
- expect(output).toContain("hello");
94
- });
95
-
96
- it("should render submitted command", () => {
97
- const instance = render(
98
- <BashRenderer
99
- state={{
100
- submittedCommand: "echo test",
101
- runId: 1,
102
- output: "",
103
- isRunning: true,
104
- }}
105
- setState={mockSetState}
106
- onCommandComplete={mockOnCommandComplete}
107
- commandHistory={[]}
108
- currentPrompt=""
109
- availableHeight={20}
110
- />,
111
- );
112
-
113
- logInk(instance);
114
-
115
- const output = instance.frames.join("");
116
- expect(output).toContain("echo test");
117
- });
118
-
119
- it("should execute command when submittedCommand is set", async () => {
120
- render(
121
- <BashRendererWrapper
122
- initialState={{
123
- submittedCommand: 'echo "output line 1\noutput line 2"',
124
- runId: 1,
125
- output: "",
126
- isRunning: false,
127
- }}
128
- onCommandComplete={mockOnCommandComplete}
129
- commandHistory={[]}
130
- currentPrompt=""
131
- availableHeight={20}
132
- />,
133
- );
134
-
135
- // Wait for async operations
136
- await sleep(500);
137
-
138
- // Command should have been executed (verified by behavior, not mocks)
139
- expect(mockOnCommandComplete).toHaveBeenCalled();
140
- });
141
-
142
- it("should not execute command if normalized is empty", async () => {
143
- render(
144
- <BashRenderer
145
- state={{
146
- submittedCommand: " ",
147
- runId: 1,
148
- output: "",
149
- isRunning: false,
150
- }}
151
- setState={mockSetState}
152
- onCommandComplete={mockOnCommandComplete}
153
- commandHistory={[]}
154
- currentPrompt=""
155
- availableHeight={20}
156
- />,
157
- );
158
-
159
- await sleep(100);
160
-
161
- // Empty command should not trigger execution
162
- expect(mockOnCommandComplete).not.toHaveBeenCalled();
163
- });
164
-
165
- it("should execute command when runId is set", async () => {
166
- render(
167
- <BashRenderer
168
- state={{
169
- submittedCommand: "echo test",
170
- runId: 1,
171
- output: "",
172
- isRunning: false,
173
- }}
174
- setState={mockSetState}
175
- onCommandComplete={mockOnCommandComplete}
176
- commandHistory={[]}
177
- currentPrompt=""
178
- availableHeight={20}
179
- />,
180
- );
181
-
182
- await sleep(500);
183
-
184
- // The component should execute the command when runId is set
185
- expect(mockOnCommandComplete).toHaveBeenCalled();
186
- });
187
-
188
- it("should not re-execute command with same runId", async () => {
189
- mockOnCommandComplete.mockClear();
190
-
191
- // Use a wrapper component that maintains state internally
192
- // This ensures the same component instance persists, so lastRunIdRef is preserved
193
- const TestWrapper = () => {
194
- const [state, setState] = useState<BashRendererProps["state"]>({
195
- submittedCommand: "echo test1",
196
- runId: 1,
197
- output: "",
198
- isRunning: false,
199
- });
200
-
201
- // After first command completes, update output without changing runId
202
- useEffect(() => {
203
- if (mockOnCommandComplete.mock.calls.length === 1 && state.output === "") {
204
- // Simulate parent updating output after command completes
205
- setTimeout(() => {
206
- setState((prev) => ({ ...prev, output: "some output" }));
207
- }, 100);
208
- }
209
- }, [state.output]);
210
-
211
- return (
212
- <BashRenderer
213
- state={state}
214
- setState={setState}
215
- onCommandComplete={mockOnCommandComplete}
216
- commandHistory={[]}
217
- currentPrompt=""
218
- availableHeight={20}
219
- />
220
- );
221
- };
222
-
223
- render(<TestWrapper />);
224
-
225
- // Wait for first execution to complete
226
- await sleep(600);
227
-
228
- // Verify command was executed once
229
- expect(mockOnCommandComplete).toHaveBeenCalledTimes(1);
230
- const callCountAfterFirstExecution = mockOnCommandComplete.mock.calls.length;
231
-
232
- // Wait for state update to trigger re-render (output changes but runId stays same)
233
- await sleep(200);
234
-
235
- // Wait a bit more to ensure useEffect has run and checked the runId
236
- await sleep(300);
237
-
238
- // onCommandComplete should NOT be called again because runId hasn't changed
239
- // The check `if (runId === lastRunIdRef.current) return` should prevent re-execution
240
- expect(mockOnCommandComplete).toHaveBeenCalledTimes(callCountAfterFirstExecution);
241
- });
242
-
243
- it("should execute command when runId changes", async () => {
244
- const { rerender } = render(
245
- <BashRenderer
246
- state={{
247
- submittedCommand: "echo test1",
248
- runId: 1,
249
- output: "",
250
- isRunning: false,
251
- }}
252
- setState={mockSetState}
253
- onCommandComplete={mockOnCommandComplete}
254
- commandHistory={[]}
255
- currentPrompt=""
256
- availableHeight={20}
257
- />,
258
- );
259
-
260
- await sleep(300);
261
-
262
- // Clear previous calls
263
- mockOnCommandComplete.mockClear();
264
-
265
- // Re-render with different runId
266
- rerender(
267
- <BashRenderer
268
- state={{
269
- submittedCommand: "echo test2",
270
- runId: 2,
271
- output: "",
272
- isRunning: false,
273
- }}
274
- setState={mockSetState}
275
- onCommandComplete={mockOnCommandComplete}
276
- commandHistory={[]}
277
- currentPrompt=""
278
- availableHeight={20}
279
- />,
280
- );
281
-
282
- await sleep(500);
283
-
284
- // Should have called onCommandComplete again with new command
285
- expect(mockOnCommandComplete).toHaveBeenCalled();
286
- });
287
-
288
- it("should update output as stream chunks arrive", async () => {
289
- const instance = render(
290
- <BashRendererWrapper
291
- initialState={{
292
- submittedCommand: 'echo "chunk1\nchunk2"',
293
- runId: 1,
294
- output: "",
295
- isRunning: false,
296
- }}
297
- onCommandComplete={mockOnCommandComplete}
298
- commandHistory={[]}
299
- currentPrompt=""
300
- availableHeight={20}
301
- />,
302
- );
303
-
304
- await sleep(500);
305
-
306
- // Output should be updated as chunks arrive
307
- const output = instance.frames.join("");
308
- expect(output).toBeDefined();
309
- });
310
-
311
- it("should strip dangerous sequences from output", async () => {
312
- // Use a real command - stripDangerousSequences is called internally
313
- render(
314
- <BashRendererWrapper
315
- initialState={{
316
- submittedCommand: "echo test",
317
- runId: 1,
318
- output: "",
319
- isRunning: false,
320
- }}
321
- onCommandComplete={mockOnCommandComplete}
322
- commandHistory={[]}
323
- currentPrompt=""
324
- availableHeight={20}
325
- />,
326
- );
327
-
328
- await sleep(500);
329
-
330
- // stripDangerousSequences is called internally, we verify behavior
331
- expect(mockOnCommandComplete).toHaveBeenCalled();
332
- });
333
-
334
- it("should handle command execution error gracefully", async () => {
335
- // Use a command that will fail (non-existent command)
336
- render(
337
- <BashRendererWrapper
338
- initialState={{
339
- submittedCommand: "nonexistentcommand12345",
340
- runId: 1,
341
- output: "",
342
- isRunning: false,
343
- }}
344
- onCommandComplete={mockOnCommandComplete}
345
- commandHistory={[]}
346
- currentPrompt=""
347
- availableHeight={20}
348
- />,
349
- );
350
-
351
- await sleep(500);
352
-
353
- // Error handling should call onCommandComplete
354
- expect(mockOnCommandComplete).toHaveBeenCalled();
355
- });
356
-
357
- it("should call onCommandComplete with output when command succeeds", async () => {
358
- render(
359
- <BashRendererWrapper
360
- initialState={{
361
- submittedCommand: 'echo "command output"',
362
- runId: 1,
363
- output: "",
364
- isRunning: false,
365
- }}
366
- onCommandComplete={mockOnCommandComplete}
367
- commandHistory={[]}
368
- currentPrompt=""
369
- availableHeight={20}
370
- />,
371
- );
372
-
373
- await sleep(500);
374
-
375
- expect(mockOnCommandComplete).toHaveBeenCalled();
376
- // Verify it was called with output (not empty string)
377
- const calls = mockOnCommandComplete.mock.calls;
378
- expect(calls.length).toBeGreaterThan(0);
379
- if (calls.length > 0 && calls[0][0]) {
380
- expect(calls[0][0].length).toBeGreaterThan(0);
381
- }
382
- });
383
-
384
- it("should set isRunning to true when command starts", async () => {
385
- render(
386
- <BashRenderer
387
- state={{
388
- submittedCommand: "echo test",
389
- runId: 1,
390
- output: "",
391
- isRunning: false,
392
- }}
393
- setState={mockSetState}
394
- onCommandComplete={mockOnCommandComplete}
395
- commandHistory={[]}
396
- currentPrompt=""
397
- availableHeight={20}
398
- />,
399
- );
400
-
401
- await sleep(50);
402
-
403
- // setState is called with a function, so we check if it was called
404
- expect(mockSetState).toHaveBeenCalled();
405
- // Verify it's called with a function that returns isRunning: true
406
- const setStateCall = mockSetState.mock.calls.find((call) => {
407
- if (typeof call[0] === "function") {
408
- const result = call[0]({ isRunning: false });
409
- return result.isRunning === true;
410
- }
411
- return false;
412
- });
413
- expect(setStateCall).toBeDefined();
414
- });
415
-
416
- it("should set isRunning to false when command completes", async () => {
417
- render(
418
- <BashRenderer
419
- state={{
420
- submittedCommand: "echo test",
421
- runId: 1,
422
- output: "",
423
- isRunning: false,
424
- }}
425
- setState={mockSetState}
426
- onCommandComplete={mockOnCommandComplete}
427
- commandHistory={[]}
428
- currentPrompt=""
429
- availableHeight={20}
430
- />,
431
- );
432
-
433
- await sleep(500);
434
-
435
- // setState is called with a function, verify it sets isRunning to false
436
- expect(mockSetState).toHaveBeenCalled();
437
- const setStateCall = mockSetState.mock.calls.find((call) => {
438
- if (typeof call[0] === "function") {
439
- const result = call[0]({ isRunning: true });
440
- return result.isRunning === false;
441
- }
442
- return false;
443
- });
444
- expect(setStateCall).toBeDefined();
445
- });
446
-
447
- it("should limit history display when submittedCommand exists", () => {
448
- const commandHistory = [
449
- { command: "cmd1", output: "out1" },
450
- { command: "cmd2", output: "out2" },
451
- { command: "cmd3", output: "out3" },
452
- { command: "cmd4", output: "out4" },
453
- ];
454
-
455
- const instance = render(
456
- <BashRenderer
457
- state={{
458
- submittedCommand: "current",
459
- runId: 1,
460
- output: "",
461
- isRunning: false,
462
- }}
463
- setState={mockSetState}
464
- onCommandComplete={mockOnCommandComplete}
465
- commandHistory={commandHistory}
466
- currentPrompt=""
467
- availableHeight={20}
468
- />,
469
- );
470
-
471
- logInk(instance);
472
-
473
- const output = instance.frames.join("");
474
- // Should show only last 2 history items when submittedCommand exists
475
- expect(output).toContain("cmd3");
476
- expect(output).toContain("cmd4");
477
- expect(output).not.toContain("cmd1");
478
- expect(output).not.toContain("cmd2");
479
- });
480
-
481
- it("should show more history when no submittedCommand", () => {
482
- const commandHistory = [
483
- { command: "cmd1", output: "out1" },
484
- { command: "cmd2", output: "out2" },
485
- { command: "cmd3", output: "out3" },
486
- ];
487
-
488
- const instance = render(
489
- <BashRenderer
490
- state={{
491
- submittedCommand: null,
492
- runId: 0,
493
- output: "",
494
- isRunning: false,
495
- }}
496
- setState={mockSetState}
497
- onCommandComplete={mockOnCommandComplete}
498
- commandHistory={commandHistory}
499
- currentPrompt=""
500
- availableHeight={20}
501
- />,
502
- );
503
-
504
- logInk(instance);
505
-
506
- const output = instance.frames.join("");
507
- // Should show last 3 history items when no submittedCommand
508
- expect(output).toContain("cmd1");
509
- expect(output).toContain("cmd2");
510
- expect(output).toContain("cmd3");
511
- });
512
-
513
- it("should calculate maxLinesPerCommand based on available height", () => {
514
- const commandHistory = [
515
- { command: "cmd1", output: "out1" },
516
- { command: "cmd2", output: "out2" },
517
- ];
518
-
519
- const instance = render(
520
- <BashRenderer
521
- state={{
522
- submittedCommand: "current",
523
- runId: 1,
524
- output: "",
525
- isRunning: false,
526
- }}
527
- setState={mockSetState}
528
- onCommandComplete={mockOnCommandComplete}
529
- commandHistory={commandHistory}
530
- currentPrompt=""
531
- availableHeight={10}
532
- />,
533
- );
534
-
535
- logInk(instance);
536
-
537
- const output = instance.frames.join("");
538
- expect(output).toBeDefined();
539
- });
540
-
541
- it("should enable truncation when height is limited", () => {
542
- const commandHistory = [{ command: "cmd1", output: "line1\nline2\nline3\nline4\nline5" }];
543
-
544
- const instance = render(
545
- <BashRenderer
546
- state={{
547
- submittedCommand: null,
548
- runId: 0,
549
- output: "",
550
- isRunning: false,
551
- }}
552
- setState={mockSetState}
553
- onCommandComplete={mockOnCommandComplete}
554
- commandHistory={commandHistory}
555
- currentPrompt=""
556
- availableHeight={5}
557
- />,
558
- );
559
-
560
- logInk(instance);
561
-
562
- const output = instance.frames.join("");
563
- expect(output).toBeDefined();
564
- });
565
-
566
- it("should enable truncation when typing", () => {
567
- const commandHistory = [{ command: "cmd1", output: "line1\nline2\nline3\nline4\nline5" }];
568
-
569
- const instance = render(
570
- <BashRenderer
571
- state={{
572
- submittedCommand: null,
573
- runId: 0,
574
- output: "",
575
- isRunning: false,
576
- }}
577
- setState={mockSetState}
578
- onCommandComplete={mockOnCommandComplete}
579
- commandHistory={commandHistory}
580
- currentPrompt="typing..."
581
- availableHeight={20}
582
- />,
583
- );
584
-
585
- logInk(instance);
586
-
587
- const output = instance.frames.join("");
588
- expect(output).toBeDefined();
589
- });
590
-
591
- it("should enable truncation when multiple commands exist", () => {
592
- const commandHistory = [
593
- { command: "cmd1", output: "out1" },
594
- { command: "cmd2", output: "out2" },
595
- ];
596
-
597
- const instance = render(
598
- <BashRenderer
599
- state={{
600
- submittedCommand: "cmd3",
601
- runId: 1,
602
- output: "",
603
- isRunning: false,
604
- }}
605
- setState={mockSetState}
606
- onCommandComplete={mockOnCommandComplete}
607
- commandHistory={commandHistory}
608
- currentPrompt=""
609
- availableHeight={20}
610
- />,
611
- );
612
-
613
- logInk(instance);
614
-
615
- const output = instance.frames.join("");
616
- expect(output).toBeDefined();
617
- });
618
-
619
- it("should handle empty command history", () => {
620
- const instance = render(
621
- <BashRenderer
622
- state={{
623
- submittedCommand: "test",
624
- runId: 1,
625
- output: "",
626
- isRunning: false,
627
- }}
628
- setState={mockSetState}
629
- onCommandComplete={mockOnCommandComplete}
630
- commandHistory={[]}
631
- currentPrompt=""
632
- availableHeight={20}
633
- />,
634
- );
635
-
636
- logInk(instance);
637
-
638
- const output = instance.frames.join("");
639
- expect(output).toContain("test");
640
- expect(output).toBeDefined();
641
- });
642
-
643
- it("should trim submittedCommand before executing", async () => {
644
- render(
645
- <BashRendererWrapper
646
- initialState={{
647
- submittedCommand: " echo trimmed-test ",
648
- runId: 1,
649
- output: "",
650
- isRunning: false,
651
- }}
652
- onCommandComplete={mockOnCommandComplete}
653
- commandHistory={[]}
654
- currentPrompt=""
655
- availableHeight={20}
656
- />,
657
- );
658
-
659
- await sleep(500);
660
-
661
- // Command should be executed with trimmed value
662
- expect(mockOnCommandComplete).toHaveBeenCalled();
663
- });
664
-
665
- it("should handle very small availableHeight", () => {
666
- const instance = render(
667
- <BashRenderer
668
- state={{
669
- submittedCommand: "test",
670
- runId: 1,
671
- output: "",
672
- isRunning: false,
673
- }}
674
- setState={mockSetState}
675
- onCommandComplete={mockOnCommandComplete}
676
- commandHistory={[]}
677
- currentPrompt=""
678
- availableHeight={1}
679
- />,
680
- );
681
-
682
- logInk(instance);
683
-
684
- const output = instance.frames.join("");
685
- expect(output).toBeDefined();
686
- });
687
-
688
- it("should handle multiple output chunks", async () => {
689
- render(
690
- <BashRendererWrapper
691
- initialState={{
692
- submittedCommand: 'echo "chunk1chunk2chunk3"',
693
- runId: 1,
694
- output: "",
695
- isRunning: false,
696
- }}
697
- onCommandComplete={mockOnCommandComplete}
698
- commandHistory={[]}
699
- currentPrompt=""
700
- availableHeight={20}
701
- />,
702
- );
703
-
704
- await sleep(500);
705
-
706
- expect(mockOnCommandComplete).toHaveBeenCalled();
707
- const calls = mockOnCommandComplete.mock.calls;
708
- if (calls.length > 0 && calls[0][0]) {
709
- expect(calls[0][0]).toContain("chunk");
710
- }
711
- });
712
-
713
- it("should handle stderr output", async () => {
714
- // Use a command that produces stderr output
715
- render(
716
- <BashRendererWrapper
717
- initialState={{
718
- submittedCommand: 'echo "error message" >&2',
719
- runId: 1,
720
- output: "",
721
- isRunning: false,
722
- }}
723
- onCommandComplete={mockOnCommandComplete}
724
- commandHistory={[]}
725
- currentPrompt=""
726
- availableHeight={20}
727
- />,
728
- );
729
-
730
- await sleep(500);
731
-
732
- expect(mockOnCommandComplete).toHaveBeenCalled();
733
- });
734
-
735
- it("should handle mixed stdout and stderr", async () => {
736
- // Use a command that produces both stdout and stderr
737
- render(
738
- <BashRendererWrapper
739
- initialState={{
740
- submittedCommand: 'echo "stdout" && echo "stderr" >&2',
741
- runId: 1,
742
- output: "",
743
- isRunning: false,
744
- }}
745
- onCommandComplete={mockOnCommandComplete}
746
- commandHistory={[]}
747
- currentPrompt=""
748
- availableHeight={20}
749
- />,
750
- );
751
-
752
- await sleep(500);
753
-
754
- expect(mockOnCommandComplete).toHaveBeenCalled();
755
- });
756
-
757
- it("should handle component unmount during command execution", async () => {
758
- const instance = render(
759
- <BashRendererWrapper
760
- initialState={{
761
- submittedCommand: "sleep 0.1 && echo test",
762
- runId: 1,
763
- output: "",
764
- isRunning: false,
765
- }}
766
- onCommandComplete={mockOnCommandComplete}
767
- commandHistory={[]}
768
- currentPrompt=""
769
- availableHeight={20}
770
- />,
771
- );
772
-
773
- logInk(instance);
774
-
775
- // Unmount before command completes
776
- cleanup();
777
-
778
- // Should not crash
779
- await sleep(200);
780
- });
781
-
782
- it("should handle new command submission while previous is running", async () => {
783
- const { rerender } = render(
784
- <BashRenderer
785
- state={{
786
- submittedCommand: "echo command1",
787
- runId: 1,
788
- output: "",
789
- isRunning: false,
790
- }}
791
- setState={mockSetState}
792
- onCommandComplete={mockOnCommandComplete}
793
- commandHistory={[]}
794
- currentPrompt=""
795
- availableHeight={20}
796
- />,
797
- );
798
-
799
- await sleep(50);
800
-
801
- // Submit new command before first completes
802
- rerender(
803
- <BashRenderer
804
- state={{
805
- submittedCommand: "echo command2",
806
- runId: 2,
807
- output: "",
808
- isRunning: false,
809
- }}
810
- setState={mockSetState}
811
- onCommandComplete={mockOnCommandComplete}
812
- commandHistory={[]}
813
- currentPrompt=""
814
- availableHeight={20}
815
- />,
816
- );
817
-
818
- await sleep(500);
819
-
820
- // Should handle both commands
821
- expect(mockOnCommandComplete).toHaveBeenCalled();
822
- });
823
-
824
- it("should handle many commands with limited height", () => {
825
- const commandHistory = Array(10)
826
- .fill(0)
827
- .map((_, i) => ({
828
- command: `cmd${i}`,
829
- output: `out${i}`,
830
- }));
831
-
832
- const instance = render(
833
- <BashRenderer
834
- state={{
835
- submittedCommand: "current",
836
- runId: 1,
837
- output: "",
838
- isRunning: false,
839
- }}
840
- setState={mockSetState}
841
- onCommandComplete={mockOnCommandComplete}
842
- commandHistory={commandHistory}
843
- currentPrompt=""
844
- availableHeight={15}
845
- />,
846
- );
847
-
848
- logInk(instance);
849
-
850
- const output = instance.frames.join("");
851
- expect(output).toBeDefined();
852
- });
853
-
854
- it("should call onCommandComplete with empty string on error", async () => {
855
- // Use a command that will fail (non-existent command)
856
- render(
857
- <BashRendererWrapper
858
- initialState={{
859
- submittedCommand: "nonexistentcommand12345",
860
- runId: 1,
861
- output: "",
862
- isRunning: false,
863
- }}
864
- onCommandComplete={mockOnCommandComplete}
865
- commandHistory={[]}
866
- currentPrompt=""
867
- availableHeight={20}
868
- />,
869
- );
870
-
871
- await sleep(500);
872
-
873
- // Error handling should call onCommandComplete
874
- expect(mockOnCommandComplete).toHaveBeenCalled();
875
- });
876
-
877
- it("should reset output when new command starts", async () => {
878
- const { rerender } = render(
879
- <BashRenderer
880
- state={{
881
- submittedCommand: "echo command1",
882
- runId: 1,
883
- output: "old output",
884
- isRunning: false,
885
- }}
886
- setState={mockSetState}
887
- onCommandComplete={mockOnCommandComplete}
888
- commandHistory={[]}
889
- currentPrompt=""
890
- availableHeight={20}
891
- />,
892
- );
893
-
894
- await sleep(300);
895
-
896
- // Clear previous calls
897
- mockSetState.mockClear();
898
-
899
- // Submit new command with different runId
900
- rerender(
901
- <BashRenderer
902
- state={{
903
- submittedCommand: "echo command2",
904
- runId: 2,
905
- output: "",
906
- isRunning: false,
907
- }}
908
- setState={mockSetState}
909
- onCommandComplete={mockOnCommandComplete}
910
- commandHistory={[]}
911
- currentPrompt=""
912
- availableHeight={20}
913
- />,
914
- );
915
-
916
- await sleep(500);
917
-
918
- // Output should be reset for new command (setState is called with isRunning: true first)
919
- // The output reset happens in the useEffect via setOutput('')
920
- expect(mockSetState).toHaveBeenCalled();
921
- });
922
-
923
- it("should handle command with no output", async () => {
924
- // Use a command that produces no output (like true or : command)
925
- render(
926
- <BashRenderer
927
- state={{
928
- submittedCommand: "true",
929
- runId: 1,
930
- output: "",
931
- isRunning: false,
932
- }}
933
- setState={mockSetState}
934
- onCommandComplete={mockOnCommandComplete}
935
- commandHistory={[]}
936
- currentPrompt=""
937
- availableHeight={20}
938
- />,
939
- );
940
-
941
- await sleep(500);
942
-
943
- // When stream has no chunks, commandOutput remains empty string
944
- // onCommandComplete should be called with empty string to signal completion
945
- expect(mockOnCommandComplete).toHaveBeenCalledWith("");
946
- });
947
-
948
- it("should handle very long command output", async () => {
949
- // Use a command that produces long output
950
- render(
951
- <BashRendererWrapper
952
- initialState={{
953
- submittedCommand: "python3 -c \"print('x' * 10000)\"",
954
- runId: 1,
955
- output: "",
956
- isRunning: false,
957
- }}
958
- onCommandComplete={mockOnCommandComplete}
959
- commandHistory={[]}
960
- currentPrompt=""
961
- availableHeight={20}
962
- />,
963
- );
964
-
965
- await sleep(1000);
966
-
967
- expect(mockOnCommandComplete).toHaveBeenCalled();
968
- const calls = mockOnCommandComplete.mock.calls;
969
- if (calls.length > 0 && calls[0][0]) {
970
- expect(calls[0][0].length).toBeGreaterThan(1000);
971
- }
972
- });
973
-
974
- it("should use normalized command in BashRunner", () => {
975
- const instance = render(
976
- <BashRenderer
977
- state={{
978
- submittedCommand: " echo hello ",
979
- runId: 1,
980
- output: "output",
981
- isRunning: false,
982
- }}
983
- setState={mockSetState}
984
- onCommandComplete={mockOnCommandComplete}
985
- commandHistory={[]}
986
- currentPrompt=""
987
- availableHeight={20}
988
- />,
989
- );
990
-
991
- logInk(instance);
992
-
993
- const output = instance.frames.join("");
994
- // Should show trimmed command
995
- expect(output).toContain("echo hello");
996
- expect(output).not.toContain(" echo hello ");
997
- });
998
-
999
- it("should handle currentPrompt affecting truncation", () => {
1000
- const commandHistory = [{ command: "cmd1", output: "line1\nline2\nline3\nline4\nline5" }];
1001
-
1002
- const instance = render(
1003
- <BashRenderer
1004
- state={{
1005
- submittedCommand: null,
1006
- runId: 0,
1007
- output: "",
1008
- isRunning: false,
1009
- }}
1010
- setState={mockSetState}
1011
- onCommandComplete={mockOnCommandComplete}
1012
- commandHistory={commandHistory}
1013
- currentPrompt="typing command..."
1014
- availableHeight={20}
1015
- />,
1016
- );
1017
-
1018
- logInk(instance);
1019
-
1020
- const output = instance.frames.join("");
1021
- expect(output).toBeDefined();
1022
- });
1023
-
1024
- it("should not call onCommandComplete when command is cancelled by new command", async () => {
1025
- mockOnCommandComplete.mockClear();
1026
-
1027
- const firstCommandCallback = vi.fn();
1028
- const secondCommandCallback = vi.fn();
1029
-
1030
- const { rerender } = render(
1031
- <BashRenderer
1032
- state={{
1033
- submittedCommand: "sleep 0.3 && echo command1",
1034
- runId: 1,
1035
- output: "",
1036
- isRunning: false,
1037
- }}
1038
- setState={mockSetState}
1039
- onCommandComplete={firstCommandCallback}
1040
- commandHistory={[]}
1041
- currentPrompt=""
1042
- availableHeight={20}
1043
- />,
1044
- );
1045
-
1046
- // Wait a bit for first command to start
1047
- await sleep(50);
1048
-
1049
- // Submit new command before first completes (this should cancel the first)
1050
- rerender(
1051
- <BashRenderer
1052
- state={{
1053
- submittedCommand: "echo command2",
1054
- runId: 2,
1055
- output: "",
1056
- isRunning: false,
1057
- }}
1058
- setState={mockSetState}
1059
- onCommandComplete={secondCommandCallback}
1060
- commandHistory={[]}
1061
- currentPrompt=""
1062
- availableHeight={20}
1063
- />,
1064
- );
1065
-
1066
- // Wait for both commands to potentially complete
1067
- await sleep(500);
1068
-
1069
- // The first command should have been cancelled and not call onCommandComplete
1070
- expect(firstCommandCallback).not.toHaveBeenCalled();
1071
- // The second command should have completed and called onCommandComplete
1072
- expect(secondCommandCallback).toHaveBeenCalled();
1073
- expect(secondCommandCallback.mock.calls[0][0]).toContain("command2");
1074
- });
1075
-
1076
- it("should not call onCommandComplete when component unmounts during execution", async () => {
1077
- mockOnCommandComplete.mockClear();
1078
-
1079
- const instance = render(
1080
- <BashRendererWrapper
1081
- initialState={{
1082
- submittedCommand: "sleep 0.2 && echo test",
1083
- runId: 1,
1084
- output: "",
1085
- isRunning: false,
1086
- }}
1087
- onCommandComplete={mockOnCommandComplete}
1088
- commandHistory={[]}
1089
- currentPrompt=""
1090
- availableHeight={20}
1091
- />,
1092
- );
1093
-
1094
- logInk(instance);
1095
-
1096
- // Wait a bit for command to start
1097
- await sleep(50);
1098
-
1099
- // Unmount before command completes
1100
- cleanup();
1101
-
1102
- // Wait for command to potentially complete
1103
- await sleep(300);
1104
-
1105
- // onCommandComplete should not be called for cancelled command
1106
- expect(mockOnCommandComplete).not.toHaveBeenCalled();
1107
- });
1108
- });