@abacus-ai/cli 2.0.0-canary.0 → 2.0.0-canary.2

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/dist/index.mjs +807 -561
  2. package/package.json +4 -1
  3. package/.oxlintrc.json +0 -8
  4. package/resources/abacus.ico +0 -0
  5. package/resources/entitlements.plist +0 -9
  6. package/src/__e2e__/README.md +0 -196
  7. package/src/__e2e__/agent-interactions.e2e.test.tsx +0 -61
  8. package/src/__e2e__/cli-commands.e2e.test.tsx +0 -77
  9. package/src/__e2e__/conversation-throttle.e2e.test.ts +0 -453
  10. package/src/__e2e__/conversation.e2e.test.tsx +0 -56
  11. package/src/__e2e__/diff-preview.e2e.test.tsx +0 -3399
  12. package/src/__e2e__/file-creation.e2e.test.tsx +0 -149
  13. package/src/__e2e__/helpers/test-helpers.ts +0 -450
  14. package/src/__e2e__/keyboard-navigation.e2e.test.tsx +0 -34
  15. package/src/__e2e__/llm-models.e2e.test.ts +0 -402
  16. package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +0 -71
  17. package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +0 -167
  18. package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +0 -185
  19. package/src/__e2e__/repl.e2e.test.tsx +0 -78
  20. package/src/__e2e__/shell-compatibility.e2e.test.tsx +0 -76
  21. package/src/__e2e__/theme-mcp.e2e.test.tsx +0 -98
  22. package/src/__e2e__/tool-permissions.e2e.test.tsx +0 -66
  23. package/src/args.ts +0 -22
  24. package/src/components/__tests__/react-compiler.test.tsx +0 -78
  25. package/src/components/__tests__/status-indicator.test.tsx +0 -403
  26. package/src/components/composer/__tests__/bash-runner.test.tsx +0 -263
  27. package/src/components/composer/agent-mode-indicator.tsx +0 -63
  28. package/src/components/composer/bash-runner.tsx +0 -54
  29. package/src/components/composer/commands/default-commands.tsx +0 -615
  30. package/src/components/composer/commands/handler.tsx +0 -59
  31. package/src/components/composer/commands/picker.tsx +0 -273
  32. package/src/components/composer/commands/registry.ts +0 -233
  33. package/src/components/composer/commands/types.ts +0 -33
  34. package/src/components/composer/context.tsx +0 -88
  35. package/src/components/composer/file-mention-picker.tsx +0 -83
  36. package/src/components/composer/help.tsx +0 -44
  37. package/src/components/composer/index.tsx +0 -1006
  38. package/src/components/composer/mentions.ts +0 -57
  39. package/src/components/composer/message-queue.tsx +0 -70
  40. package/src/components/composer/mode-panel.tsx +0 -35
  41. package/src/components/composer/modes/__tests__/bash-handler.test.tsx +0 -755
  42. package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +0 -1108
  43. package/src/components/composer/modes/bash-handler.tsx +0 -132
  44. package/src/components/composer/modes/bash-renderer.tsx +0 -175
  45. package/src/components/composer/modes/default-handlers.tsx +0 -33
  46. package/src/components/composer/modes/index.ts +0 -41
  47. package/src/components/composer/modes/types.ts +0 -21
  48. package/src/components/composer/persistent-shell.ts +0 -283
  49. package/src/components/composer/process.ts +0 -65
  50. package/src/components/composer/types.ts +0 -9
  51. package/src/components/composer/use-mention-search.ts +0 -68
  52. package/src/components/error-boundry.tsx +0 -60
  53. package/src/components/exit-message.tsx +0 -29
  54. package/src/components/expanded-view.tsx +0 -74
  55. package/src/components/file-completion.tsx +0 -127
  56. package/src/components/header.tsx +0 -47
  57. package/src/components/logo.tsx +0 -37
  58. package/src/components/segments.tsx +0 -356
  59. package/src/components/status-indicator.tsx +0 -306
  60. package/src/components/tool-group-summary.tsx +0 -263
  61. package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +0 -312
  62. package/src/components/tool-permissions/diff-preview.tsx +0 -355
  63. package/src/components/tool-permissions/index.ts +0 -5
  64. package/src/components/tool-permissions/permission-options.tsx +0 -375
  65. package/src/components/tool-permissions/permission-preview-header.tsx +0 -57
  66. package/src/components/tool-permissions/tool-permission-ui.tsx +0 -398
  67. package/src/components/tools/agent/ask-user-question.tsx +0 -101
  68. package/src/components/tools/agent/enter-plan-mode.tsx +0 -49
  69. package/src/components/tools/agent/exit-plan-mode.tsx +0 -75
  70. package/src/components/tools/agent/handoff-to-main.tsx +0 -27
  71. package/src/components/tools/agent/subagent.tsx +0 -37
  72. package/src/components/tools/agent/todo-write.tsx +0 -104
  73. package/src/components/tools/browser/close-tab.tsx +0 -58
  74. package/src/components/tools/browser/computer.tsx +0 -70
  75. package/src/components/tools/browser/get-interactive-elements.tsx +0 -54
  76. package/src/components/tools/browser/get-tab-content.tsx +0 -51
  77. package/src/components/tools/browser/navigate-to.tsx +0 -59
  78. package/src/components/tools/browser/new-tab.tsx +0 -60
  79. package/src/components/tools/browser/perform-action.tsx +0 -63
  80. package/src/components/tools/browser/refresh-tab.tsx +0 -43
  81. package/src/components/tools/browser/switch-tab.tsx +0 -58
  82. package/src/components/tools/filesystem/delete-file.tsx +0 -104
  83. package/src/components/tools/filesystem/edit.tsx +0 -220
  84. package/src/components/tools/filesystem/list-dir.tsx +0 -78
  85. package/src/components/tools/filesystem/read-file.tsx +0 -180
  86. package/src/components/tools/filesystem/upload-image.tsx +0 -76
  87. package/src/components/tools/ide/ide-diagnostics.tsx +0 -62
  88. package/src/components/tools/index.ts +0 -91
  89. package/src/components/tools/mcp/mcp-tool.tsx +0 -158
  90. package/src/components/tools/search/fetch-url.tsx +0 -73
  91. package/src/components/tools/search/file-search.tsx +0 -78
  92. package/src/components/tools/search/grep.tsx +0 -90
  93. package/src/components/tools/search/semantic-search.tsx +0 -66
  94. package/src/components/tools/search/web-search.tsx +0 -71
  95. package/src/components/tools/shared/index.tsx +0 -48
  96. package/src/components/tools/shared/zod-coercion.ts +0 -35
  97. package/src/components/tools/terminal/bash-tool-output.tsx +0 -174
  98. package/src/components/tools/terminal/get-terminal-output.tsx +0 -85
  99. package/src/components/tools/terminal/run-in-terminal.tsx +0 -106
  100. package/src/components/tools/types.ts +0 -16
  101. package/src/components/tools.tsx +0 -66
  102. package/src/components/ui/__tests__/divider.test.tsx +0 -61
  103. package/src/components/ui/__tests__/gradient.test.tsx +0 -125
  104. package/src/components/ui/__tests__/input.test.tsx +0 -166
  105. package/src/components/ui/__tests__/select.test.tsx +0 -273
  106. package/src/components/ui/__tests__/shimmer.test.tsx +0 -99
  107. package/src/components/ui/blinking-indicator.tsx +0 -25
  108. package/src/components/ui/divider.tsx +0 -162
  109. package/src/components/ui/gradient.tsx +0 -56
  110. package/src/components/ui/input.tsx +0 -228
  111. package/src/components/ui/select.tsx +0 -151
  112. package/src/components/ui/shimmer.tsx +0 -84
  113. package/src/context/agent-mode.tsx +0 -95
  114. package/src/context/extension-file.tsx +0 -136
  115. package/src/context/network-activity.tsx +0 -45
  116. package/src/context/notification.tsx +0 -62
  117. package/src/context/shell-size.tsx +0 -49
  118. package/src/context/shell-title.tsx +0 -38
  119. package/src/entrypoints/print-mode.ts +0 -312
  120. package/src/entrypoints/repl.tsx +0 -401
  121. package/src/hooks/use-agent.ts +0 -15
  122. package/src/hooks/use-api-client.ts +0 -1
  123. package/src/hooks/use-available-height.ts +0 -8
  124. package/src/hooks/use-cleanup.ts +0 -29
  125. package/src/hooks/use-interrupt-manager.ts +0 -242
  126. package/src/hooks/use-models.ts +0 -22
  127. package/src/index.ts +0 -217
  128. package/src/lib/__tests__/ansi.test.ts +0 -255
  129. package/src/lib/__tests__/cli.test.ts +0 -122
  130. package/src/lib/__tests__/commands.test.ts +0 -325
  131. package/src/lib/__tests__/constants.test.ts +0 -15
  132. package/src/lib/__tests__/focusables.test.ts +0 -25
  133. package/src/lib/__tests__/fs.test.ts +0 -231
  134. package/src/lib/__tests__/markdown.test.tsx +0 -348
  135. package/src/lib/__tests__/mcpCommandHandler.test.ts +0 -173
  136. package/src/lib/__tests__/mcpManagement.test.ts +0 -38
  137. package/src/lib/__tests__/path-paste.test.ts +0 -144
  138. package/src/lib/__tests__/path.test.ts +0 -300
  139. package/src/lib/__tests__/queries.test.ts +0 -39
  140. package/src/lib/__tests__/standaloneMcpService.test.ts +0 -71
  141. package/src/lib/__tests__/text-buffer.test.ts +0 -328
  142. package/src/lib/__tests__/text-utils.test.ts +0 -32
  143. package/src/lib/__tests__/timing.test.ts +0 -78
  144. package/src/lib/__tests__/utils.test.ts +0 -238
  145. package/src/lib/__tests__/vim-buffer-actions.test.ts +0 -154
  146. package/src/lib/ansi.ts +0 -150
  147. package/src/lib/cli-push-server.ts +0 -112
  148. package/src/lib/cli.ts +0 -44
  149. package/src/lib/clipboard.ts +0 -226
  150. package/src/lib/command-utils.ts +0 -93
  151. package/src/lib/commands.ts +0 -270
  152. package/src/lib/constants.ts +0 -3
  153. package/src/lib/extension-connection.ts +0 -181
  154. package/src/lib/focusables.ts +0 -7
  155. package/src/lib/fs.ts +0 -533
  156. package/src/lib/markdown/code-block.tsx +0 -63
  157. package/src/lib/markdown/index.ts +0 -4
  158. package/src/lib/markdown/link.tsx +0 -19
  159. package/src/lib/markdown/markdown.tsx +0 -372
  160. package/src/lib/markdown/types.ts +0 -15
  161. package/src/lib/mcpCommandHandler.ts +0 -121
  162. package/src/lib/mcpManagement.ts +0 -44
  163. package/src/lib/path-paste.ts +0 -185
  164. package/src/lib/path.ts +0 -179
  165. package/src/lib/queries.ts +0 -15
  166. package/src/lib/standaloneMcpService.ts +0 -688
  167. package/src/lib/status-utils.ts +0 -237
  168. package/src/lib/test-utils.tsx +0 -72
  169. package/src/lib/text-buffer.ts +0 -2415
  170. package/src/lib/text-utils.ts +0 -272
  171. package/src/lib/timing.ts +0 -63
  172. package/src/lib/types.ts +0 -295
  173. package/src/lib/utils.ts +0 -182
  174. package/src/lib/vim-buffer-actions.ts +0 -732
  175. package/src/providers/agent.tsx +0 -1075
  176. package/src/providers/api-client.tsx +0 -43
  177. package/src/services/logger.ts +0 -85
  178. package/src/terminal/detection.ts +0 -187
  179. package/src/terminal/exit.ts +0 -279
  180. package/src/terminal/notification.ts +0 -83
  181. package/src/terminal/progress.ts +0 -201
  182. package/src/terminal/setup.ts +0 -797
  183. package/src/terminal/suspend.ts +0 -58
  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 -631
  193. package/src/tools/utils/zod-coercion.ts +0 -35
  194. package/tsconfig.json +0 -11
  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
- });