@abacus-ai/cli 1.106.25008 → 2.0.0-canary.1
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.
- package/.oxlintrc.json +8 -0
- package/dist/index.mjs +12823 -0
- package/package.json +7 -39
- package/resources/abacus.ico +0 -0
- package/resources/entitlements.plist +9 -0
- package/src/__e2e__/README.md +196 -0
- package/src/__e2e__/agent-interactions.e2e.test.tsx +61 -0
- package/src/__e2e__/cli-commands.e2e.test.tsx +77 -0
- package/src/__e2e__/conversation-throttle.e2e.test.ts +453 -0
- package/src/__e2e__/conversation.e2e.test.tsx +56 -0
- package/src/__e2e__/diff-preview.e2e.test.tsx +3399 -0
- package/src/__e2e__/file-creation.e2e.test.tsx +149 -0
- package/src/__e2e__/helpers/test-helpers.ts +449 -0
- package/src/__e2e__/keyboard-navigation.e2e.test.tsx +34 -0
- package/src/__e2e__/llm-models.e2e.test.ts +402 -0
- package/src/__e2e__/mcp/mcp-callback-flow.e2e.test.tsx +71 -0
- package/src/__e2e__/mcp/mcp-full-app-ui.e2e.test.tsx +167 -0
- package/src/__e2e__/mcp/mcp-ui-rendering.e2e.test.tsx +185 -0
- package/src/__e2e__/repl.e2e.test.tsx +78 -0
- package/src/__e2e__/shell-compatibility.e2e.test.tsx +76 -0
- package/src/__e2e__/theme-mcp.e2e.test.tsx +98 -0
- package/src/__e2e__/tool-permissions.e2e.test.tsx +66 -0
- package/src/args.ts +22 -0
- package/src/components/__tests__/react-compiler.test.tsx +78 -0
- package/src/components/__tests__/status-indicator.test.tsx +403 -0
- package/src/components/composer/__tests__/bash-runner.test.tsx +263 -0
- package/src/components/composer/agent-mode-indicator.tsx +63 -0
- package/src/components/composer/bash-runner.tsx +54 -0
- package/src/components/composer/commands/default-commands.tsx +615 -0
- package/src/components/composer/commands/handler.tsx +59 -0
- package/src/components/composer/commands/picker.tsx +273 -0
- package/src/components/composer/commands/registry.ts +233 -0
- package/src/components/composer/commands/types.ts +33 -0
- package/src/components/composer/context.tsx +88 -0
- package/src/components/composer/file-mention-picker.tsx +83 -0
- package/src/components/composer/help.tsx +44 -0
- package/src/components/composer/index.tsx +1007 -0
- package/src/components/composer/mentions.ts +57 -0
- package/src/components/composer/message-queue.tsx +70 -0
- package/src/components/composer/mode-panel.tsx +35 -0
- package/src/components/composer/modes/__tests__/bash-handler.test.tsx +755 -0
- package/src/components/composer/modes/__tests__/bash-renderer.test.tsx +1108 -0
- package/src/components/composer/modes/bash-handler.tsx +132 -0
- package/src/components/composer/modes/bash-renderer.tsx +175 -0
- package/src/components/composer/modes/default-handlers.tsx +33 -0
- package/src/components/composer/modes/index.ts +41 -0
- package/src/components/composer/modes/types.ts +21 -0
- package/src/components/composer/persistent-shell.ts +283 -0
- package/src/components/composer/process.ts +65 -0
- package/src/components/composer/types.ts +9 -0
- package/src/components/composer/use-mention-search.ts +68 -0
- package/src/components/error-boundry.tsx +60 -0
- package/src/components/exit-message.tsx +29 -0
- package/src/components/expanded-view.tsx +74 -0
- package/src/components/file-completion.tsx +127 -0
- package/src/components/header.tsx +47 -0
- package/src/components/logo.tsx +37 -0
- package/src/components/segments.tsx +356 -0
- package/src/components/status-indicator.tsx +306 -0
- package/src/components/tool-group-summary.tsx +263 -0
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +319 -0
- package/src/components/tool-permissions/diff-preview.tsx +359 -0
- package/src/components/tool-permissions/index.ts +5 -0
- package/src/components/tool-permissions/permission-options.tsx +401 -0
- package/src/components/tool-permissions/permission-preview-header.tsx +57 -0
- package/src/components/tool-permissions/tool-permission-ui.tsx +420 -0
- package/src/components/tools/agent/ask-user-question.tsx +107 -0
- package/src/components/tools/agent/enter-plan-mode.tsx +55 -0
- package/src/components/tools/agent/exit-plan-mode.tsx +83 -0
- package/src/components/tools/agent/handoff-to-main.tsx +27 -0
- package/src/components/tools/agent/subagent.tsx +37 -0
- package/src/components/tools/agent/todo-write.tsx +104 -0
- package/src/components/tools/browser/close-tab.tsx +58 -0
- package/src/components/tools/browser/computer.tsx +70 -0
- package/src/components/tools/browser/get-interactive-elements.tsx +54 -0
- package/src/components/tools/browser/get-tab-content.tsx +51 -0
- package/src/components/tools/browser/navigate-to.tsx +59 -0
- package/src/components/tools/browser/new-tab.tsx +60 -0
- package/src/components/tools/browser/perform-action.tsx +63 -0
- package/src/components/tools/browser/refresh-tab.tsx +43 -0
- package/src/components/tools/browser/switch-tab.tsx +58 -0
- package/src/components/tools/filesystem/delete-file.tsx +104 -0
- package/src/components/tools/filesystem/edit.tsx +220 -0
- package/src/components/tools/filesystem/list-dir.tsx +78 -0
- package/src/components/tools/filesystem/read-file.tsx +180 -0
- package/src/components/tools/filesystem/upload-image.tsx +76 -0
- package/src/components/tools/ide/ide-diagnostics.tsx +62 -0
- package/src/components/tools/index.ts +91 -0
- package/src/components/tools/mcp/mcp-tool.tsx +158 -0
- package/src/components/tools/search/fetch-url.tsx +73 -0
- package/src/components/tools/search/file-search.tsx +78 -0
- package/src/components/tools/search/grep.tsx +90 -0
- package/src/components/tools/search/semantic-search.tsx +66 -0
- package/src/components/tools/search/web-search.tsx +71 -0
- package/src/components/tools/shared/index.tsx +48 -0
- package/src/components/tools/shared/zod-coercion.ts +35 -0
- package/src/components/tools/terminal/bash-tool-output.tsx +188 -0
- package/src/components/tools/terminal/get-terminal-output.tsx +91 -0
- package/src/components/tools/terminal/run-in-terminal.tsx +131 -0
- package/src/components/tools/types.ts +16 -0
- package/src/components/tools.tsx +68 -0
- package/src/components/ui/__tests__/divider.test.tsx +61 -0
- package/src/components/ui/__tests__/gradient.test.tsx +125 -0
- package/src/components/ui/__tests__/input.test.tsx +166 -0
- package/src/components/ui/__tests__/select.test.tsx +273 -0
- package/src/components/ui/__tests__/shimmer.test.tsx +99 -0
- package/src/components/ui/blinking-indicator.tsx +27 -0
- package/src/components/ui/divider.tsx +162 -0
- package/src/components/ui/gradient.tsx +56 -0
- package/src/components/ui/input.tsx +228 -0
- package/src/components/ui/select.tsx +151 -0
- package/src/components/ui/shimmer.tsx +76 -0
- package/src/context/agent-mode.tsx +95 -0
- package/src/context/extension-file.tsx +136 -0
- package/src/context/network-activity.tsx +45 -0
- package/src/context/notification.tsx +62 -0
- package/src/context/shell-size.tsx +49 -0
- package/src/context/shell-title.tsx +38 -0
- package/src/entrypoints/print-mode.ts +312 -0
- package/src/entrypoints/repl.tsx +389 -0
- package/src/hooks/use-agent.ts +15 -0
- package/src/hooks/use-api-client.ts +1 -0
- package/src/hooks/use-available-height.ts +8 -0
- package/src/hooks/use-cleanup.ts +29 -0
- package/src/hooks/use-interrupt-manager.ts +242 -0
- package/src/hooks/use-models.ts +22 -0
- package/src/index.ts +217 -0
- package/src/lib/__tests__/ansi.test.ts +255 -0
- package/src/lib/__tests__/cli.test.ts +122 -0
- package/src/lib/__tests__/commands.test.ts +325 -0
- package/src/lib/__tests__/constants.test.ts +15 -0
- package/src/lib/__tests__/focusables.test.ts +25 -0
- package/src/lib/__tests__/fs.test.ts +231 -0
- package/src/lib/__tests__/markdown.test.tsx +348 -0
- package/src/lib/__tests__/mcpCommandHandler.test.ts +173 -0
- package/src/lib/__tests__/mcpManagement.test.ts +38 -0
- package/src/lib/__tests__/path-paste.test.ts +144 -0
- package/src/lib/__tests__/path.test.ts +300 -0
- package/src/lib/__tests__/queries.test.ts +39 -0
- package/src/lib/__tests__/standaloneMcpService.test.ts +71 -0
- package/src/lib/__tests__/text-buffer.test.ts +328 -0
- package/src/lib/__tests__/text-utils.test.ts +32 -0
- package/src/lib/__tests__/timing.test.ts +78 -0
- package/src/lib/__tests__/utils.test.ts +238 -0
- package/src/lib/__tests__/vim-buffer-actions.test.ts +154 -0
- package/src/lib/ansi.ts +150 -0
- package/src/lib/cli-push-server.ts +112 -0
- package/src/lib/cli.ts +44 -0
- package/src/lib/clipboard.ts +226 -0
- package/src/lib/command-utils.ts +93 -0
- package/src/lib/commands.ts +270 -0
- package/src/lib/constants.ts +3 -0
- package/src/lib/extension-connection.ts +181 -0
- package/src/lib/focusables.ts +7 -0
- package/src/lib/fs.ts +533 -0
- package/src/lib/markdown/code-block.tsx +63 -0
- package/src/lib/markdown/index.ts +4 -0
- package/src/lib/markdown/link.tsx +19 -0
- package/src/lib/markdown/markdown.tsx +372 -0
- package/src/lib/markdown/types.ts +15 -0
- package/src/lib/mcpCommandHandler.ts +121 -0
- package/src/lib/mcpManagement.ts +44 -0
- package/src/lib/path-paste.ts +185 -0
- package/src/lib/path.ts +179 -0
- package/src/lib/queries.ts +15 -0
- package/src/lib/standaloneMcpService.ts +688 -0
- package/src/lib/status-utils.ts +237 -0
- package/src/lib/test-utils.tsx +72 -0
- package/src/lib/text-buffer.ts +2415 -0
- package/src/lib/text-utils.ts +272 -0
- package/src/lib/timing.ts +63 -0
- package/src/lib/types.ts +295 -0
- package/src/lib/utils.ts +182 -0
- package/src/lib/vim-buffer-actions.ts +732 -0
- package/src/providers/agent.tsx +1063 -0
- package/src/providers/api-client.tsx +43 -0
- package/src/services/logger.ts +85 -0
- package/src/terminal/detection.ts +187 -0
- package/src/terminal/exit.ts +279 -0
- package/src/terminal/notification.ts +83 -0
- package/src/terminal/progress.ts +201 -0
- package/src/terminal/setup.ts +797 -0
- package/src/terminal/types.ts +51 -0
- package/src/theme/context.tsx +57 -0
- package/src/theme/index.ts +4 -0
- package/src/theme/themed.tsx +35 -0
- package/src/theme/themes.json +546 -0
- package/src/theme/types.ts +110 -0
- package/src/tools/types.ts +59 -0
- package/src/tools/utils/__tests__/zod-coercion.test.ts +33 -0
- package/src/tools/utils/tool-ui-components.tsx +649 -0
- package/src/tools/utils/zod-coercion.ts +35 -0
- package/tsconfig.json +16 -0
- package/tsconfig.node.json +29 -0
- package/tsconfig.test.json +27 -0
- package/tsdown.config.ts +17 -0
- package/vitest.config.ts +76 -0
- package/README.md +0 -28
- package/dist/index.js +0 -26
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
|
|
3
|
+
import stripAnsi from "strip-ansi";
|
|
4
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { NetworkActivityProvider } from "../../context/network-activity.js";
|
|
7
|
+
import { render, logInk, cleanup } from "../../lib/test-utils.js";
|
|
8
|
+
import { AgentStatus } from "../../providers/agent.js";
|
|
9
|
+
import { StatusIndicator } from "../status-indicator.js";
|
|
10
|
+
|
|
11
|
+
// Helper function to wrap StatusIndicator with required providers
|
|
12
|
+
function renderStatusIndicator(status: AgentStatus) {
|
|
13
|
+
return render(
|
|
14
|
+
<NetworkActivityProvider>
|
|
15
|
+
<StatusIndicator status={status} />
|
|
16
|
+
</NetworkActivityProvider>,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe.concurrent("StatusIndicator", () => {
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
cleanup();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should not render when status is Idle", () => {
|
|
26
|
+
const instance = renderStatusIndicator(AgentStatus.Idle);
|
|
27
|
+
|
|
28
|
+
logInk(instance);
|
|
29
|
+
|
|
30
|
+
const output = instance.lastFrame() ?? "";
|
|
31
|
+
// Should be empty or whitespace only when idle
|
|
32
|
+
expect(stripAnsi(output).trim()).toBe("");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("should not render when status is WaitingForToolPermission", () => {
|
|
36
|
+
const instance = renderStatusIndicator(AgentStatus.WaitingForToolPermission);
|
|
37
|
+
|
|
38
|
+
logInk(instance);
|
|
39
|
+
|
|
40
|
+
const output = instance.lastFrame() ?? "";
|
|
41
|
+
expect(stripAnsi(output).trim()).toBe("");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should render shimmer text when status is Submitted", () => {
|
|
45
|
+
const instance = renderStatusIndicator(AgentStatus.Submitted);
|
|
46
|
+
|
|
47
|
+
logInk(instance);
|
|
48
|
+
|
|
49
|
+
const output = instance.lastFrame() ?? "";
|
|
50
|
+
const plainText = stripAnsi(output);
|
|
51
|
+
|
|
52
|
+
expect(output).toBeDefined();
|
|
53
|
+
// Should show some status text with "..." and escape hint
|
|
54
|
+
expect(plainText).toContain("...");
|
|
55
|
+
expect(plainText).toContain("esc to interrupt");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("should render shimmer text when status is Streaming", () => {
|
|
59
|
+
const instance = renderStatusIndicator(AgentStatus.Streaming);
|
|
60
|
+
|
|
61
|
+
logInk(instance);
|
|
62
|
+
|
|
63
|
+
const output = instance.lastFrame() ?? "";
|
|
64
|
+
const plainText = stripAnsi(output);
|
|
65
|
+
|
|
66
|
+
expect(output).toBeDefined();
|
|
67
|
+
expect(plainText).toContain("...");
|
|
68
|
+
expect(plainText).toContain("esc to interrupt");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should render shimmer text when status is ExecutingTool", () => {
|
|
72
|
+
const instance = renderStatusIndicator(AgentStatus.ExecutingTool);
|
|
73
|
+
|
|
74
|
+
logInk(instance);
|
|
75
|
+
|
|
76
|
+
const output = instance.lastFrame() ?? "";
|
|
77
|
+
const plainText = stripAnsi(output);
|
|
78
|
+
|
|
79
|
+
expect(output).toBeDefined();
|
|
80
|
+
expect(plainText).toContain("...");
|
|
81
|
+
expect(plainText).toContain("esc to interrupt");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should render "Resuming" when status is LoadingConversation', () => {
|
|
85
|
+
const instance = renderStatusIndicator(AgentStatus.LoadingConversation);
|
|
86
|
+
|
|
87
|
+
logInk(instance);
|
|
88
|
+
|
|
89
|
+
const output = instance.lastFrame() ?? "";
|
|
90
|
+
const plainText = stripAnsi(output);
|
|
91
|
+
|
|
92
|
+
expect(output).toBeDefined();
|
|
93
|
+
expect(plainText).toContain("Resuming...");
|
|
94
|
+
// Should NOT show escape hint for loading conversation
|
|
95
|
+
expect(plainText).not.toContain("esc to interrupt");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should show indicator when transitioning from Idle to ExecutingTool", () => {
|
|
99
|
+
// This simulates what happens when an MCP tool starts executing
|
|
100
|
+
// Using rerender() to simulate prop changes from parent (AgentProvider)
|
|
101
|
+
const instance = renderStatusIndicator(AgentStatus.Idle);
|
|
102
|
+
|
|
103
|
+
// Initially idle - should be empty
|
|
104
|
+
let output = instance.lastFrame() ?? "";
|
|
105
|
+
expect(stripAnsi(output).trim()).toBe("");
|
|
106
|
+
|
|
107
|
+
// Simulate MCP tool starting - parent updates status prop
|
|
108
|
+
instance.rerender(
|
|
109
|
+
<NetworkActivityProvider>
|
|
110
|
+
<StatusIndicator status={AgentStatus.ExecutingTool} />
|
|
111
|
+
</NetworkActivityProvider>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Re-render should show the indicator
|
|
115
|
+
output = instance.lastFrame() ?? "";
|
|
116
|
+
const plainText = stripAnsi(output);
|
|
117
|
+
|
|
118
|
+
console.log("[UI Test] After status change to ExecutingTool:", plainText);
|
|
119
|
+
|
|
120
|
+
expect(plainText).toContain("...");
|
|
121
|
+
expect(plainText).toContain("esc to interrupt");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should hide indicator when transitioning from ExecutingTool to Idle", () => {
|
|
125
|
+
// This simulates what happens when an MCP tool finishes executing
|
|
126
|
+
const instance = renderStatusIndicator(AgentStatus.ExecutingTool);
|
|
127
|
+
|
|
128
|
+
// Initially executing - should show indicator
|
|
129
|
+
let output = instance.lastFrame() ?? "";
|
|
130
|
+
let plainText = stripAnsi(output);
|
|
131
|
+
expect(plainText).toContain("...");
|
|
132
|
+
|
|
133
|
+
// Simulate MCP tool completing - parent updates status prop
|
|
134
|
+
instance.rerender(
|
|
135
|
+
<NetworkActivityProvider>
|
|
136
|
+
<StatusIndicator status={AgentStatus.Idle} />
|
|
137
|
+
</NetworkActivityProvider>,
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
// Re-render should hide the indicator
|
|
141
|
+
output = instance.lastFrame() ?? "";
|
|
142
|
+
plainText = stripAnsi(output);
|
|
143
|
+
|
|
144
|
+
console.log("[UI Test] After status change to Idle:", `"${plainText}"`);
|
|
145
|
+
|
|
146
|
+
expect(plainText.trim()).toBe("");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("should update immediately when status changes (simulating MCP tool call)", () => {
|
|
150
|
+
// This test verifies that UI updates happen synchronously with status changes
|
|
151
|
+
// If there was a delay bug, this test would catch it
|
|
152
|
+
const instance = renderStatusIndicator(AgentStatus.Idle);
|
|
153
|
+
|
|
154
|
+
const startTime = Date.now();
|
|
155
|
+
|
|
156
|
+
// Simulate rapid status changes like during MCP tool execution
|
|
157
|
+
instance.rerender(
|
|
158
|
+
<NetworkActivityProvider>
|
|
159
|
+
<StatusIndicator status={AgentStatus.Submitted} />
|
|
160
|
+
</NetworkActivityProvider>,
|
|
161
|
+
);
|
|
162
|
+
const afterSubmitted = instance.lastFrame() ?? "";
|
|
163
|
+
|
|
164
|
+
instance.rerender(
|
|
165
|
+
<NetworkActivityProvider>
|
|
166
|
+
<StatusIndicator status={AgentStatus.ExecutingTool} />
|
|
167
|
+
</NetworkActivityProvider>,
|
|
168
|
+
);
|
|
169
|
+
const afterExecuting = instance.lastFrame() ?? "";
|
|
170
|
+
|
|
171
|
+
instance.rerender(
|
|
172
|
+
<NetworkActivityProvider>
|
|
173
|
+
<StatusIndicator status={AgentStatus.Streaming} />
|
|
174
|
+
</NetworkActivityProvider>,
|
|
175
|
+
);
|
|
176
|
+
const afterStreaming = instance.lastFrame() ?? "";
|
|
177
|
+
|
|
178
|
+
instance.rerender(
|
|
179
|
+
<NetworkActivityProvider>
|
|
180
|
+
<StatusIndicator status={AgentStatus.Idle} />
|
|
181
|
+
</NetworkActivityProvider>,
|
|
182
|
+
);
|
|
183
|
+
const afterIdle = instance.lastFrame() ?? "";
|
|
184
|
+
|
|
185
|
+
const totalTime = Date.now() - startTime;
|
|
186
|
+
|
|
187
|
+
console.log("[UI Test] Status transitions:");
|
|
188
|
+
console.log(" - Submitted:", stripAnsi(afterSubmitted).includes("...") ? "visible" : "hidden");
|
|
189
|
+
console.log(
|
|
190
|
+
" - ExecutingTool:",
|
|
191
|
+
stripAnsi(afterExecuting).includes("...") ? "visible" : "hidden",
|
|
192
|
+
);
|
|
193
|
+
console.log(" - Streaming:", stripAnsi(afterStreaming).includes("...") ? "visible" : "hidden");
|
|
194
|
+
console.log(" - Idle:", stripAnsi(afterIdle).trim() === "" ? "hidden" : "visible");
|
|
195
|
+
console.log(` Total time for 4 transitions: ${totalTime}ms`);
|
|
196
|
+
|
|
197
|
+
// Verify each state rendered correctly
|
|
198
|
+
expect(stripAnsi(afterSubmitted)).toContain("...");
|
|
199
|
+
expect(stripAnsi(afterExecuting)).toContain("...");
|
|
200
|
+
expect(stripAnsi(afterStreaming)).toContain("...");
|
|
201
|
+
expect(stripAnsi(afterIdle).trim()).toBe("");
|
|
202
|
+
|
|
203
|
+
// All transitions should be nearly instant (< 50ms total for 4 changes)
|
|
204
|
+
// This catches bugs where UI updates are delayed
|
|
205
|
+
expect(totalTime).toBeLessThan(50);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe.sequential("StatusIndicator - MCP Tool Execution Flow", () => {
|
|
210
|
+
afterEach(() => {
|
|
211
|
+
cleanup();
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* This test simulates the exact flow that happens when an MCP tool is called:
|
|
216
|
+
* 1. User sends message -> Submitted
|
|
217
|
+
* 2. Agent starts processing -> Streaming
|
|
218
|
+
* 3. Agent calls MCP tool -> ExecutingTool
|
|
219
|
+
* 4. Tool completes -> Streaming (continues)
|
|
220
|
+
* 5. Agent done -> Idle
|
|
221
|
+
*
|
|
222
|
+
* The bug your teammate fixed was that step 3 (ExecutingTool) wasn't showing
|
|
223
|
+
* in the UI because status updates weren't being handled correctly.
|
|
224
|
+
*/
|
|
225
|
+
it("should show correct UI through full MCP tool execution lifecycle", async () => {
|
|
226
|
+
const renderTimes: { status: string; frame: string; time: number; visible: boolean }[] = [];
|
|
227
|
+
const startTime = Date.now();
|
|
228
|
+
|
|
229
|
+
const instance = renderStatusIndicator(AgentStatus.Idle);
|
|
230
|
+
|
|
231
|
+
// Step 0: Initial idle state
|
|
232
|
+
let frame = instance.lastFrame() ?? "";
|
|
233
|
+
renderTimes.push({
|
|
234
|
+
status: "Idle (initial)",
|
|
235
|
+
frame: stripAnsi(frame),
|
|
236
|
+
time: Date.now() - startTime,
|
|
237
|
+
visible: stripAnsi(frame).includes("..."),
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// Step 1: User submits message
|
|
241
|
+
instance.rerender(
|
|
242
|
+
<NetworkActivityProvider>
|
|
243
|
+
<StatusIndicator status={AgentStatus.Submitted} />
|
|
244
|
+
</NetworkActivityProvider>,
|
|
245
|
+
);
|
|
246
|
+
frame = instance.lastFrame() ?? "";
|
|
247
|
+
renderTimes.push({
|
|
248
|
+
status: "Submitted",
|
|
249
|
+
frame: stripAnsi(frame),
|
|
250
|
+
time: Date.now() - startTime,
|
|
251
|
+
visible: stripAnsi(frame).includes("..."),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Small delay to simulate async behavior
|
|
255
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
256
|
+
|
|
257
|
+
// Step 2: Agent starts streaming response
|
|
258
|
+
instance.rerender(
|
|
259
|
+
<NetworkActivityProvider>
|
|
260
|
+
<StatusIndicator status={AgentStatus.Streaming} />
|
|
261
|
+
</NetworkActivityProvider>,
|
|
262
|
+
);
|
|
263
|
+
frame = instance.lastFrame() ?? "";
|
|
264
|
+
renderTimes.push({
|
|
265
|
+
status: "Streaming",
|
|
266
|
+
frame: stripAnsi(frame),
|
|
267
|
+
time: Date.now() - startTime,
|
|
268
|
+
visible: stripAnsi(frame).includes("..."),
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
272
|
+
|
|
273
|
+
// Step 3: Agent calls MCP tool - THIS IS THE CRITICAL PART
|
|
274
|
+
// If status update is delayed, user sees no feedback
|
|
275
|
+
instance.rerender(
|
|
276
|
+
<NetworkActivityProvider>
|
|
277
|
+
<StatusIndicator status={AgentStatus.ExecutingTool} />
|
|
278
|
+
</NetworkActivityProvider>,
|
|
279
|
+
);
|
|
280
|
+
frame = instance.lastFrame() ?? "";
|
|
281
|
+
renderTimes.push({
|
|
282
|
+
status: "ExecutingTool",
|
|
283
|
+
frame: stripAnsi(frame),
|
|
284
|
+
time: Date.now() - startTime,
|
|
285
|
+
visible: stripAnsi(frame).includes("..."),
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
289
|
+
|
|
290
|
+
// Step 4: Tool completes, back to streaming
|
|
291
|
+
instance.rerender(
|
|
292
|
+
<NetworkActivityProvider>
|
|
293
|
+
<StatusIndicator status={AgentStatus.Streaming} />
|
|
294
|
+
</NetworkActivityProvider>,
|
|
295
|
+
);
|
|
296
|
+
frame = instance.lastFrame() ?? "";
|
|
297
|
+
renderTimes.push({
|
|
298
|
+
status: "Streaming (after tool)",
|
|
299
|
+
frame: stripAnsi(frame),
|
|
300
|
+
time: Date.now() - startTime,
|
|
301
|
+
visible: stripAnsi(frame).includes("..."),
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
305
|
+
|
|
306
|
+
// Step 5: Agent completes
|
|
307
|
+
instance.rerender(
|
|
308
|
+
<NetworkActivityProvider>
|
|
309
|
+
<StatusIndicator status={AgentStatus.Idle} />
|
|
310
|
+
</NetworkActivityProvider>,
|
|
311
|
+
);
|
|
312
|
+
frame = instance.lastFrame() ?? "";
|
|
313
|
+
renderTimes.push({
|
|
314
|
+
status: "Idle (final)",
|
|
315
|
+
frame: stripAnsi(frame),
|
|
316
|
+
time: Date.now() - startTime,
|
|
317
|
+
visible: stripAnsi(frame).includes("..."),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Log the timeline
|
|
321
|
+
console.log("[MCP Flow Test] Render timeline:");
|
|
322
|
+
renderTimes.forEach((r) => {
|
|
323
|
+
console.log(` ${r.time}ms: ${r.status} -> ${r.visible ? "VISIBLE" : "hidden"}`);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// Verify we got through all states
|
|
327
|
+
expect(renderTimes.length).toBe(6);
|
|
328
|
+
|
|
329
|
+
// Verify the ExecutingTool state was visible (the bug was this not showing)
|
|
330
|
+
const executingToolEntry = renderTimes.find((r) => r.status === "ExecutingTool");
|
|
331
|
+
expect(executingToolEntry).toBeDefined();
|
|
332
|
+
expect(executingToolEntry?.visible).toBe(true);
|
|
333
|
+
|
|
334
|
+
// Verify Submitted was visible
|
|
335
|
+
const submittedEntry = renderTimes.find((r) => r.status === "Submitted");
|
|
336
|
+
expect(submittedEntry?.visible).toBe(true);
|
|
337
|
+
|
|
338
|
+
// Verify Streaming was visible
|
|
339
|
+
const streamingEntry = renderTimes.find((r) => r.status === "Streaming");
|
|
340
|
+
expect(streamingEntry?.visible).toBe(true);
|
|
341
|
+
|
|
342
|
+
// Verify final Idle is hidden
|
|
343
|
+
const idleFinalEntry = renderTimes.find((r) => r.status === "Idle (final)");
|
|
344
|
+
expect(idleFinalEntry?.visible).toBe(false);
|
|
345
|
+
|
|
346
|
+
// Verify timing - each transition should happen quickly
|
|
347
|
+
for (let i = 1; i < renderTimes.length; i++) {
|
|
348
|
+
const timeBetween = renderTimes[i].time - renderTimes[i - 1].time;
|
|
349
|
+
// Each transition should be < 20ms (plus our 10ms simulated delay)
|
|
350
|
+
expect(timeBetween).toBeLessThanOrEqual(40);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Test that verifies UI responds to status changes without artificial delays.
|
|
356
|
+
* This would catch bugs where status updates are batched or delayed.
|
|
357
|
+
*/
|
|
358
|
+
it("should render each status change immediately without batching", () => {
|
|
359
|
+
const instance = renderStatusIndicator(AgentStatus.Idle);
|
|
360
|
+
const frames: string[] = [];
|
|
361
|
+
|
|
362
|
+
// Rapid-fire status changes
|
|
363
|
+
const statuses: AgentStatus[] = [
|
|
364
|
+
AgentStatus.Submitted,
|
|
365
|
+
AgentStatus.Streaming,
|
|
366
|
+
AgentStatus.ExecutingTool,
|
|
367
|
+
AgentStatus.WaitingForToolPermission,
|
|
368
|
+
AgentStatus.ExecutingTool,
|
|
369
|
+
AgentStatus.Streaming,
|
|
370
|
+
AgentStatus.Idle,
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
const startTime = Date.now();
|
|
374
|
+
|
|
375
|
+
for (const status of statuses) {
|
|
376
|
+
instance.rerender(
|
|
377
|
+
<NetworkActivityProvider>
|
|
378
|
+
<StatusIndicator status={status} />
|
|
379
|
+
</NetworkActivityProvider>,
|
|
380
|
+
);
|
|
381
|
+
frames.push(instance.lastFrame() ?? "");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const totalTime = Date.now() - startTime;
|
|
385
|
+
|
|
386
|
+
console.log(`[Batching Test] ${statuses.length} status changes in ${totalTime}ms`);
|
|
387
|
+
console.log("[Batching Test] Frames captured:", frames.length);
|
|
388
|
+
|
|
389
|
+
// Each status change should produce a frame
|
|
390
|
+
expect(frames.length).toBe(statuses.length);
|
|
391
|
+
|
|
392
|
+
// Should be very fast (no delays between renders)
|
|
393
|
+
expect(totalTime).toBeLessThan(50);
|
|
394
|
+
|
|
395
|
+
// Verify specific states rendered correctly
|
|
396
|
+
// Submitted (index 0) should be visible
|
|
397
|
+
expect(stripAnsi(frames[0])).toContain("...");
|
|
398
|
+
// WaitingForToolPermission (index 3) should be hidden
|
|
399
|
+
expect(stripAnsi(frames[3]).trim()).toBe("");
|
|
400
|
+
// Final Idle (index 6) should be hidden
|
|
401
|
+
expect(stripAnsi(frames[6]).trim()).toBe("");
|
|
402
|
+
});
|
|
403
|
+
});
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
import * as pathModule from "../../../lib/path.js";
|
|
4
|
+
import { render, cleanup, logInk } from "../../../lib/test-utils.js";
|
|
5
|
+
import { BashRunner } from "../bash-runner.js";
|
|
6
|
+
|
|
7
|
+
// Mock the path module
|
|
8
|
+
vi.mock("../../../lib/path.js", () => ({
|
|
9
|
+
abbreviateHome: vi.fn((path: string) => path),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe.concurrent("BashRunner", () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.clearAllMocks();
|
|
15
|
+
// Default mock implementation
|
|
16
|
+
vi.mocked(pathModule.abbreviateHome).mockImplementation((p: string) => p);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
cleanup();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("should render command and output", () => {
|
|
24
|
+
const instance = render(<BashRunner command="echo hello" output="hello" />);
|
|
25
|
+
|
|
26
|
+
logInk(instance);
|
|
27
|
+
|
|
28
|
+
const output = instance.frames.join("");
|
|
29
|
+
expect(output).toContain("echo hello");
|
|
30
|
+
expect(output).toContain("hello");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should render empty output", () => {
|
|
34
|
+
const instance = render(<BashRunner command="ls" output="" />);
|
|
35
|
+
|
|
36
|
+
logInk(instance);
|
|
37
|
+
|
|
38
|
+
const output = instance.frames.join("");
|
|
39
|
+
expect(output).toContain("ls");
|
|
40
|
+
// Should not crash with empty output
|
|
41
|
+
expect(output).toBeDefined();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should render multi-line output", () => {
|
|
45
|
+
const multiLineOutput = "line1\nline2\nline3";
|
|
46
|
+
const instance = render(<BashRunner command="cat file.txt" output={multiLineOutput} />);
|
|
47
|
+
|
|
48
|
+
logInk(instance);
|
|
49
|
+
|
|
50
|
+
const output = instance.frames.join("");
|
|
51
|
+
expect(output).toContain("cat file.txt");
|
|
52
|
+
expect(output).toContain("line1");
|
|
53
|
+
expect(output).toContain("line2");
|
|
54
|
+
expect(output).toContain("line3");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should not truncate when truncate is false", () => {
|
|
58
|
+
const longOutput = Array(20)
|
|
59
|
+
.fill("line")
|
|
60
|
+
.map((_, i) => `line ${i}`)
|
|
61
|
+
.join("\n");
|
|
62
|
+
const instance = render(
|
|
63
|
+
<BashRunner command="long-command" output={longOutput} truncate={false} />,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
logInk(instance);
|
|
67
|
+
|
|
68
|
+
const output = instance.frames.join("");
|
|
69
|
+
// Should contain all lines when truncate is false
|
|
70
|
+
expect(output).toContain("line 0");
|
|
71
|
+
expect(output).toContain("line 19");
|
|
72
|
+
expect(output).not.toContain("lines hidden");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should truncate output when truncate is true and output exceeds maxLines", () => {
|
|
76
|
+
const longOutput = Array(20)
|
|
77
|
+
.fill("line")
|
|
78
|
+
.map((_, i) => `line ${i}`)
|
|
79
|
+
.join("\n");
|
|
80
|
+
const instance = render(
|
|
81
|
+
<BashRunner command="long-command" output={longOutput} truncate={true} maxLines={10} />,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
logInk(instance);
|
|
85
|
+
|
|
86
|
+
const output = instance.frames.join("");
|
|
87
|
+
// Should show first and last half of maxLines
|
|
88
|
+
expect(output).toContain("line 0");
|
|
89
|
+
expect(output).toContain("line 4"); // First half (5 lines)
|
|
90
|
+
expect(output).toContain("line 15"); // Last half (5 lines)
|
|
91
|
+
expect(output).toContain("line 19");
|
|
92
|
+
expect(output).toContain("lines hidden");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should not truncate when output is within maxLines", () => {
|
|
96
|
+
const shortOutput = Array(5)
|
|
97
|
+
.fill("line")
|
|
98
|
+
.map((_, i) => `line ${i}`)
|
|
99
|
+
.join("\n");
|
|
100
|
+
const instance = render(
|
|
101
|
+
<BashRunner command="short-command" output={shortOutput} truncate={true} maxLines={10} />,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
logInk(instance);
|
|
105
|
+
|
|
106
|
+
const output = instance.frames.join("");
|
|
107
|
+
expect(output).toContain("line 0");
|
|
108
|
+
expect(output).toContain("line 4");
|
|
109
|
+
expect(output).not.toContain("lines hidden");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("should use default maxLines of 10 when not provided", () => {
|
|
113
|
+
const longOutput = Array(20)
|
|
114
|
+
.fill("line")
|
|
115
|
+
.map((_, i) => `line ${i}`)
|
|
116
|
+
.join("\n");
|
|
117
|
+
const instance = render(<BashRunner command="test" output={longOutput} truncate={true} />);
|
|
118
|
+
|
|
119
|
+
logInk(instance);
|
|
120
|
+
|
|
121
|
+
const output = instance.frames.join("");
|
|
122
|
+
// Should truncate at default 10 lines (5 + 5)
|
|
123
|
+
expect(output).toContain("lines hidden");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should handle truncation with odd maxLines", () => {
|
|
127
|
+
const longOutput = Array(20)
|
|
128
|
+
.fill("line")
|
|
129
|
+
.map((_, i) => `line ${i}`)
|
|
130
|
+
.join("\n");
|
|
131
|
+
const instance = render(
|
|
132
|
+
<BashRunner command="test" output={longOutput} truncate={true} maxLines={9} />,
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
logInk(instance);
|
|
136
|
+
|
|
137
|
+
const output = instance.frames.join("");
|
|
138
|
+
// With maxLines=9, halfLines=4, so should show first 4 and last 4
|
|
139
|
+
expect(output).toContain("line 0");
|
|
140
|
+
expect(output).toContain("line 3");
|
|
141
|
+
expect(output).toContain("line 16");
|
|
142
|
+
expect(output).toContain("line 19");
|
|
143
|
+
expect(output).toContain("lines hidden");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("should display truncation indicator in the middle", () => {
|
|
147
|
+
const longOutput = Array(20)
|
|
148
|
+
.fill("line")
|
|
149
|
+
.map((_, i) => `line ${i}`)
|
|
150
|
+
.join("\n");
|
|
151
|
+
const instance = render(
|
|
152
|
+
<BashRunner command="test" output={longOutput} truncate={true} maxLines={10} />,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
logInk(instance);
|
|
156
|
+
|
|
157
|
+
const output = instance.frames.join("");
|
|
158
|
+
// The truncation indicator should appear between first and last half
|
|
159
|
+
const lines = output.split("\n");
|
|
160
|
+
const truncationIndex = lines.findIndex((line) => line.includes("lines hidden"));
|
|
161
|
+
expect(truncationIndex).toBeGreaterThan(-1);
|
|
162
|
+
// Should be after first half and before last half
|
|
163
|
+
expect(truncationIndex).toBeGreaterThan(0);
|
|
164
|
+
expect(truncationIndex).toBeLessThan(lines.length - 1);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should call abbreviateHome with process.cwd()", () => {
|
|
168
|
+
const instance = render(<BashRunner command="pwd" output="output" />);
|
|
169
|
+
|
|
170
|
+
logInk(instance);
|
|
171
|
+
|
|
172
|
+
expect(pathModule.abbreviateHome).toHaveBeenCalledWith(process.cwd());
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("should handle command with special characters", () => {
|
|
176
|
+
const instance = render(<BashRunner command="echo 'hello world' && ls -la" output="output" />);
|
|
177
|
+
|
|
178
|
+
logInk(instance);
|
|
179
|
+
|
|
180
|
+
const output = instance.frames.join("");
|
|
181
|
+
expect(output).toContain("echo 'hello world' && ls -la");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("should handle output with special characters", () => {
|
|
185
|
+
const specialOutput = "line with \"quotes\" and 'apostrophes' and $variables";
|
|
186
|
+
const instance = render(<BashRunner command="test" output={specialOutput} />);
|
|
187
|
+
|
|
188
|
+
logInk(instance);
|
|
189
|
+
|
|
190
|
+
const output = instance.frames.join("");
|
|
191
|
+
expect(output).toContain("quotes");
|
|
192
|
+
expect(output).toContain("apostrophes");
|
|
193
|
+
expect(output).toContain("$variables");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("should handle very long single line output", () => {
|
|
197
|
+
const veryLongLine = "x".repeat(1000);
|
|
198
|
+
const instance = render(<BashRunner command="test" output={veryLongLine} />);
|
|
199
|
+
|
|
200
|
+
logInk(instance);
|
|
201
|
+
|
|
202
|
+
const output = instance.frames.join("");
|
|
203
|
+
expect(output).toContain("x");
|
|
204
|
+
expect(output).toBeDefined();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should handle edge case with maxLines=1", () => {
|
|
208
|
+
const longOutput = Array(10)
|
|
209
|
+
.fill("line")
|
|
210
|
+
.map((_, i) => `line ${i}`)
|
|
211
|
+
.join("\n");
|
|
212
|
+
const instance = render(
|
|
213
|
+
<BashRunner command="test" output={longOutput} truncate={true} maxLines={1} />,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
logInk(instance);
|
|
217
|
+
|
|
218
|
+
const output = instance.frames.join("");
|
|
219
|
+
// Should show at least one line (the last line) even with maxLines=1
|
|
220
|
+
expect(output).toBeDefined();
|
|
221
|
+
expect(output).toContain("line 9"); // Last line should be shown
|
|
222
|
+
expect(output).toContain("lines hidden");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should handle large number of lines", () => {
|
|
226
|
+
const largeOutput = Array(1000)
|
|
227
|
+
.fill("line")
|
|
228
|
+
.map((_, i) => `line ${i}`)
|
|
229
|
+
.join("\n");
|
|
230
|
+
const instance = render(
|
|
231
|
+
<BashRunner command="test" output={largeOutput} truncate={true} maxLines={20} />,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
logInk(instance);
|
|
235
|
+
|
|
236
|
+
const output = instance.frames.join("");
|
|
237
|
+
expect(output).toContain("lines hidden");
|
|
238
|
+
expect(output).toBeDefined();
|
|
239
|
+
// Should show first and last portions of output
|
|
240
|
+
expect(output).toContain("line 0");
|
|
241
|
+
expect(output).toContain("line 999");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("should not show truncation when lines exactly equal maxLines", () => {
|
|
245
|
+
const exactOutput = Array(10)
|
|
246
|
+
.fill("line")
|
|
247
|
+
.map((_, i) => `line ${i}`)
|
|
248
|
+
.join("\n");
|
|
249
|
+
const instance = render(
|
|
250
|
+
<BashRunner command="test" output={exactOutput} truncate={true} maxLines={10} />,
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
logInk(instance);
|
|
254
|
+
|
|
255
|
+
const output = instance.frames.join("");
|
|
256
|
+
// Should not show truncation indicator when lines.length === maxLines
|
|
257
|
+
expect(output).toBeDefined();
|
|
258
|
+
expect(output).not.toContain("lines hidden");
|
|
259
|
+
// Should show all lines
|
|
260
|
+
expect(output).toContain("line 0");
|
|
261
|
+
expect(output).toContain("line 9");
|
|
262
|
+
});
|
|
263
|
+
});
|