@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,48 @@
|
|
|
1
|
+
// components/tools/shared/index.tsx
|
|
2
|
+
// Re-exports all shared UI components and utilities for tools
|
|
3
|
+
|
|
4
|
+
export {
|
|
5
|
+
formatBytes,
|
|
6
|
+
formatDuration,
|
|
7
|
+
truncatePath,
|
|
8
|
+
getStatusColor,
|
|
9
|
+
BlinkingDot,
|
|
10
|
+
ToolHeader,
|
|
11
|
+
ToolResult,
|
|
12
|
+
PreviewContent,
|
|
13
|
+
parseToolResult,
|
|
14
|
+
computeExpectedFinalContentForDiff,
|
|
15
|
+
wrapLine,
|
|
16
|
+
CodePreview,
|
|
17
|
+
CondensedDiffPreview,
|
|
18
|
+
PendingToolCallIdContext,
|
|
19
|
+
} from "../../../tools/utils/tool-ui-components.js";
|
|
20
|
+
|
|
21
|
+
export type {
|
|
22
|
+
ToolHeaderProps,
|
|
23
|
+
ToolResultProps,
|
|
24
|
+
PreviewContentProps,
|
|
25
|
+
CodePreviewProps,
|
|
26
|
+
CondensedDiffLine,
|
|
27
|
+
CondensedDiffPreviewProps,
|
|
28
|
+
} from "../../../tools/utils/tool-ui-components.js";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Convert snake_case keys to camelCase recursively
|
|
32
|
+
*/
|
|
33
|
+
export function snakeToCamel(obj: unknown): unknown {
|
|
34
|
+
if (obj === null || obj === undefined || typeof obj !== "object") {
|
|
35
|
+
return obj;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(obj)) {
|
|
39
|
+
return obj.map((item) => snakeToCamel(item));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result: Record<string, unknown> = {};
|
|
43
|
+
for (const [key, value] of Object.entries(obj as Record<string, unknown>)) {
|
|
44
|
+
const camelKey = key.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase());
|
|
45
|
+
result[camelKey] = snakeToCamel(value);
|
|
46
|
+
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
function normalizeNumberInput(value: unknown): unknown {
|
|
4
|
+
if (typeof value !== "string") {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
if (!trimmed) {
|
|
10
|
+
return Number.NaN;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Number(trimmed);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeBooleanInput(value: unknown): unknown {
|
|
17
|
+
if (typeof value !== "string") {
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const normalized = value.trim().toLowerCase();
|
|
22
|
+
if (["true", "1", "yes", "y"].includes(normalized)) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (["false", "0", "no", "n"].includes(normalized)) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const coerceNumber = z.preprocess(normalizeNumberInput, z.number());
|
|
33
|
+
export const coerceOptionalNumber = z.preprocess(normalizeNumberInput, z.number().optional());
|
|
34
|
+
export const coerceBoolean = z.preprocess(normalizeBooleanInput, z.boolean());
|
|
35
|
+
export const coerceOptionalBoolean = z.preprocess(normalizeBooleanInput, z.boolean().optional());
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import type { RunInTerminalParsedData } from "@codellm/agent";
|
|
2
|
+
|
|
3
|
+
import { View, Text, useFocus, useInput } from "@codellm/jar";
|
|
4
|
+
import { useState, useEffect, useMemo, useDeferredValue } from "react";
|
|
5
|
+
|
|
6
|
+
import { useAgent } from "../../../hooks/use-agent.js";
|
|
7
|
+
import { AgentStatus } from "../../../providers/agent.js";
|
|
8
|
+
import { ToolHeader, ToolResult, PreviewContent, formatDuration } from "../shared/index.js";
|
|
9
|
+
|
|
10
|
+
const MAX_LINES_PREVIEW = 7;
|
|
11
|
+
const MAX_LINES_EXPANDED = 100;
|
|
12
|
+
|
|
13
|
+
interface BashToolOutputProps {
|
|
14
|
+
command: string;
|
|
15
|
+
toolCallId: string;
|
|
16
|
+
executionResult?: RunInTerminalParsedData;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface TruncatedOutput {
|
|
20
|
+
lines: string[];
|
|
21
|
+
isTruncated: boolean;
|
|
22
|
+
hiddenCount: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getTruncatedOutput(
|
|
26
|
+
output: string,
|
|
27
|
+
maxLines: number,
|
|
28
|
+
isExpanded: boolean,
|
|
29
|
+
): TruncatedOutput {
|
|
30
|
+
const limit = isExpanded ? MAX_LINES_EXPANDED : maxLines;
|
|
31
|
+
const lines = output.split("\n").filter(Boolean);
|
|
32
|
+
if (lines.length <= limit) {
|
|
33
|
+
return { lines, isTruncated: false, hiddenCount: 0 };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
lines: lines.slice(0, limit),
|
|
38
|
+
isTruncated: true,
|
|
39
|
+
hiddenCount: lines.length - limit,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatElapsedTime(ms: number): string {
|
|
44
|
+
const seconds = ms / 1000;
|
|
45
|
+
return `${seconds.toFixed(1)}s`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function BashToolOutput({ command, toolCallId, executionResult }: BashToolOutputProps) {
|
|
49
|
+
const { agentStatus, activeToolOutputs } = useAgent();
|
|
50
|
+
const [startTime] = useState(Date.now());
|
|
51
|
+
const [elapsedMs, setElapsedMs] = useState(0);
|
|
52
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
53
|
+
|
|
54
|
+
const isExecuting =
|
|
55
|
+
!executionResult &&
|
|
56
|
+
(agentStatus === AgentStatus.Streaming || agentStatus === AgentStatus.ExecutingTool);
|
|
57
|
+
|
|
58
|
+
const streamingOutput = activeToolOutputs[toolCallId] || "";
|
|
59
|
+
const finalOutput = executionResult?.output || "";
|
|
60
|
+
const currentOutput = isExecuting ? streamingOutput : finalOutput;
|
|
61
|
+
|
|
62
|
+
const deferredOutput = useDeferredValue(currentOutput);
|
|
63
|
+
const isProcessing = currentOutput !== deferredOutput;
|
|
64
|
+
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
if (isExecuting) {
|
|
67
|
+
const interval = setInterval(() => {
|
|
68
|
+
setElapsedMs(Date.now() - startTime);
|
|
69
|
+
}, 1000);
|
|
70
|
+
return () => clearInterval(interval);
|
|
71
|
+
} else if (executionResult) {
|
|
72
|
+
setElapsedMs(executionResult.duration ?? 0);
|
|
73
|
+
}
|
|
74
|
+
return () => {};
|
|
75
|
+
}, [isExecuting, startTime, executionResult]);
|
|
76
|
+
|
|
77
|
+
const { lines, isTruncated, hiddenCount } = useMemo(
|
|
78
|
+
() => getTruncatedOutput(deferredOutput, MAX_LINES_PREVIEW, isExpanded),
|
|
79
|
+
[deferredOutput, isExpanded],
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const canExpand = !isExecuting && executionResult !== undefined && (isTruncated || isExpanded);
|
|
83
|
+
const { isFocused } = useFocus({
|
|
84
|
+
id: `bash-tool-${toolCallId}`,
|
|
85
|
+
isActive: canExpand,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
useInput(
|
|
89
|
+
(input, key) => {
|
|
90
|
+
if (isFocused && key.ctrl && input === "o") {
|
|
91
|
+
setIsExpanded((prev) => !prev);
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{ isActive: isFocused && canExpand },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const displayCommand = command;
|
|
98
|
+
const exitCode = executionResult?.exitCode ?? 0;
|
|
99
|
+
const isError = executionResult?.status === "error" || exitCode !== 0;
|
|
100
|
+
const headerStatus = isExecuting ? "pending" : isError ? "error" : "success";
|
|
101
|
+
|
|
102
|
+
if (isExecuting && lines.length === 0) {
|
|
103
|
+
return (
|
|
104
|
+
<View flexDirection="column" gap={0}>
|
|
105
|
+
<ToolHeader
|
|
106
|
+
name="Bash"
|
|
107
|
+
params={displayCommand}
|
|
108
|
+
paramsLanguage="bash"
|
|
109
|
+
status={headerStatus}
|
|
110
|
+
showSpinner
|
|
111
|
+
/>
|
|
112
|
+
<ToolResult color="gray">Running…</ToolResult>
|
|
113
|
+
</View>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (isExecuting && lines.length > 0) {
|
|
118
|
+
const elapsedText = elapsedMs > 0 ? ` (${formatElapsedTime(elapsedMs)})` : "";
|
|
119
|
+
return (
|
|
120
|
+
<View flexDirection="column" gap={0}>
|
|
121
|
+
<ToolHeader
|
|
122
|
+
name="Bash"
|
|
123
|
+
params={displayCommand}
|
|
124
|
+
paramsLanguage="bash"
|
|
125
|
+
status={headerStatus}
|
|
126
|
+
showSpinner
|
|
127
|
+
/>
|
|
128
|
+
<PreviewContent>
|
|
129
|
+
{lines.map((line, idx) => (
|
|
130
|
+
<Text key={idx}>{line}</Text>
|
|
131
|
+
))}
|
|
132
|
+
{isTruncated && (
|
|
133
|
+
<Text dimColor>
|
|
134
|
+
… +{hiddenCount} more line{hiddenCount !== 1 ? "s" : ""}
|
|
135
|
+
{elapsedText}
|
|
136
|
+
</Text>
|
|
137
|
+
)}
|
|
138
|
+
{isProcessing && <Text dimColor>Processing output...</Text>}
|
|
139
|
+
</PreviewContent>
|
|
140
|
+
</View>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (executionResult) {
|
|
145
|
+
const isStaleResult = !finalOutput && (executionResult.duration ?? 0) === 0;
|
|
146
|
+
const duration = formatDuration(executionResult.duration ?? 0);
|
|
147
|
+
const resultText = isStaleResult ? `exit ${exitCode}` : `exit ${exitCode} (${duration})`;
|
|
148
|
+
const hasOutput = lines.length > 0;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<View flexDirection="column" gap={0}>
|
|
152
|
+
<ToolHeader
|
|
153
|
+
name="Bash"
|
|
154
|
+
params={displayCommand}
|
|
155
|
+
paramsLanguage="bash"
|
|
156
|
+
status={headerStatus}
|
|
157
|
+
/>
|
|
158
|
+
{hasOutput && (
|
|
159
|
+
<PreviewContent>
|
|
160
|
+
{isError && (
|
|
161
|
+
<Text color="red" dimColor>
|
|
162
|
+
Error:
|
|
163
|
+
</Text>
|
|
164
|
+
)}
|
|
165
|
+
{lines.map((line, idx) => (
|
|
166
|
+
<Text key={idx} color={isError ? "red" : undefined} dimColor>
|
|
167
|
+
{line}
|
|
168
|
+
</Text>
|
|
169
|
+
))}
|
|
170
|
+
{isTruncated && (
|
|
171
|
+
<Text dimColor>
|
|
172
|
+
… +{hiddenCount} {isExpanded ? "more " : ""}line{hiddenCount !== 1 ? "s" : ""}
|
|
173
|
+
{!isExpanded ? " (ctrl+o to expand)" : ""}
|
|
174
|
+
</Text>
|
|
175
|
+
)}
|
|
176
|
+
</PreviewContent>
|
|
177
|
+
)}
|
|
178
|
+
<ToolResult color={isError ? "red" : "gray"}>{resultText}</ToolResult>
|
|
179
|
+
</View>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return (
|
|
184
|
+
<View flexDirection="column">
|
|
185
|
+
<ToolHeader name="Bash" params={displayCommand} paramsLanguage="bash" status="pending" />
|
|
186
|
+
</View>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { ParsedToolResult } from "@codellm/agent";
|
|
2
|
+
|
|
3
|
+
import { View, Text } from "@codellm/jar";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import type { ToolDef, ToolStatus } from "../types.js";
|
|
8
|
+
|
|
9
|
+
import { ToolHeader, ToolResult } from "../shared/index.js";
|
|
10
|
+
import { snakeToCamel } from "../shared/index.js";
|
|
11
|
+
|
|
12
|
+
const GetTerminalOutputParams = z.object({
|
|
13
|
+
id: z.string(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const getTerminalOutputTool: ToolDef = {
|
|
17
|
+
displayName: "TerminalOutput",
|
|
18
|
+
|
|
19
|
+
getHeaderParams(input: Record<string, unknown>): string {
|
|
20
|
+
try {
|
|
21
|
+
return `id: ${input?.id || ""}`;
|
|
22
|
+
} catch {
|
|
23
|
+
return "[unknown]";
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
renderUI(
|
|
28
|
+
rawInput: Record<string, unknown>,
|
|
29
|
+
status: ToolStatus,
|
|
30
|
+
parsedResult?: ParsedToolResult,
|
|
31
|
+
): React.ReactElement {
|
|
32
|
+
const parseResult = GetTerminalOutputParams.safeParse(snakeToCamel(rawInput));
|
|
33
|
+
if (!parseResult.success) {
|
|
34
|
+
return (
|
|
35
|
+
<View flexDirection="column">
|
|
36
|
+
<ToolHeader name="TerminalOutput" params="" status="error" />
|
|
37
|
+
<ToolResult color="red">Invalid params</ToolResult>
|
|
38
|
+
</View>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const params = parseResult.data;
|
|
42
|
+
const terminalIdParam = `id: ${params.id}`;
|
|
43
|
+
|
|
44
|
+
if (status === "interrupted") {
|
|
45
|
+
return (
|
|
46
|
+
<View flexDirection="column">
|
|
47
|
+
<ToolHeader name="TerminalOutput" params={terminalIdParam} status="interrupted" />
|
|
48
|
+
<ToolResult color="yellow">Interrupted · What should I do instead?</ToolResult>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (status === "rejected") {
|
|
54
|
+
return (
|
|
55
|
+
<View flexDirection="column">
|
|
56
|
+
<ToolHeader name="TerminalOutput" params={terminalIdParam} status="rejected" />
|
|
57
|
+
{parsedResult?.userNote ? (
|
|
58
|
+
<ToolResult color="yellow">
|
|
59
|
+
Rejected · <Text italic>"{parsedResult.userNote}"</Text>
|
|
60
|
+
</ToolResult>
|
|
61
|
+
) : (
|
|
62
|
+
<ToolResult color="yellow">Rejected</ToolResult>
|
|
63
|
+
)}
|
|
64
|
+
</View>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const isExecuting = !parsedResult && (status === "executing" || status === "pending");
|
|
69
|
+
|
|
70
|
+
if (isExecuting) {
|
|
71
|
+
return (
|
|
72
|
+
<ToolHeader name="TerminalOutput" params={terminalIdParam} status="pending" showSpinner />
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!parsedResult) {
|
|
77
|
+
return (
|
|
78
|
+
<View flexDirection="column">
|
|
79
|
+
<ToolHeader name="TerminalOutput" params={terminalIdParam} status="pending" />
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View flexDirection="column">
|
|
86
|
+
<ToolHeader name="TerminalOutput" params={terminalIdParam} status="success" />
|
|
87
|
+
<ToolResult>{parsedResult.summary}</ToolResult>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { ParsedToolResult, RunInTerminalParsedData } from "@codellm/agent";
|
|
2
|
+
|
|
3
|
+
import { View, Text } from "@codellm/jar";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
import type { ToolDef, ToolStatus } from "../types.js";
|
|
8
|
+
|
|
9
|
+
import { ToolHeader, ToolResult, snakeToCamel } from "../shared/index.js";
|
|
10
|
+
import { coerceOptionalBoolean } from "../shared/zod-coercion.js";
|
|
11
|
+
import { BashToolOutput } from "./bash-tool-output.js";
|
|
12
|
+
|
|
13
|
+
const RunInTerminalParams = z.object({
|
|
14
|
+
command: z.string(),
|
|
15
|
+
explanation: z.string().optional(),
|
|
16
|
+
isBackground: coerceOptionalBoolean,
|
|
17
|
+
id: z.string().optional(),
|
|
18
|
+
interactiveMode: coerceOptionalBoolean,
|
|
19
|
+
timeout: z.coerce.number().optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function RunInTerminalUI({
|
|
23
|
+
rawInput,
|
|
24
|
+
status,
|
|
25
|
+
parsedResult,
|
|
26
|
+
}: {
|
|
27
|
+
rawInput: Record<string, unknown>;
|
|
28
|
+
status: ToolStatus;
|
|
29
|
+
parsedResult?: ParsedToolResult<RunInTerminalParsedData>;
|
|
30
|
+
}) {
|
|
31
|
+
const parseResult = RunInTerminalParams.safeParse(snakeToCamel(rawInput));
|
|
32
|
+
if (!parseResult.success) {
|
|
33
|
+
return (
|
|
34
|
+
<View flexDirection="column">
|
|
35
|
+
<ToolHeader name="Bash" params="" paramsLanguage="bash" status="error" />
|
|
36
|
+
<ToolResult color="red">Invalid params</ToolResult>
|
|
37
|
+
</View>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const params = parseResult.data;
|
|
41
|
+
const command = params.command || "";
|
|
42
|
+
const timeoutLabel = params.timeout ? ` (timeout: ${(params.timeout / 1000).toFixed(0)}s)` : "";
|
|
43
|
+
|
|
44
|
+
if (status === "interrupted") {
|
|
45
|
+
return (
|
|
46
|
+
<View flexDirection="column">
|
|
47
|
+
<ToolHeader name="Bash" params={command} paramsLanguage="bash" status="interrupted" />
|
|
48
|
+
<ToolResult color="yellow">Interrupted · What should I do instead?</ToolResult>
|
|
49
|
+
</View>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (status === "rejected") {
|
|
54
|
+
if (parsedResult?.userNote) {
|
|
55
|
+
return (
|
|
56
|
+
<View flexDirection="column">
|
|
57
|
+
<ToolHeader name="Bash" params={command} paramsLanguage="bash" status="rejected" />
|
|
58
|
+
<ToolResult color="yellow">
|
|
59
|
+
Rejected · <Text italic>"{parsedResult.userNote}"</Text>
|
|
60
|
+
</ToolResult>
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return (
|
|
65
|
+
<View flexDirection="column">
|
|
66
|
+
<ToolHeader name="Bash" params={command} paramsLanguage="bash" status="rejected" />
|
|
67
|
+
<ToolResult color="yellow">Rejected</ToolResult>
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (parsedResult?.data?.timedOut) {
|
|
73
|
+
return (
|
|
74
|
+
<View flexDirection="column">
|
|
75
|
+
<ToolHeader
|
|
76
|
+
name="Bash"
|
|
77
|
+
params={`${command}${timeoutLabel}`}
|
|
78
|
+
paramsLanguage="bash"
|
|
79
|
+
status="success"
|
|
80
|
+
/>
|
|
81
|
+
<ToolResult>Running in the background</ToolResult>
|
|
82
|
+
</View>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (parsedResult?.isBackground) {
|
|
87
|
+
return (
|
|
88
|
+
<View flexDirection="column">
|
|
89
|
+
<ToolHeader
|
|
90
|
+
name="Bash"
|
|
91
|
+
params={`${command}${timeoutLabel}`}
|
|
92
|
+
paramsLanguage="bash"
|
|
93
|
+
status="success"
|
|
94
|
+
/>
|
|
95
|
+
<ToolResult>{parsedResult.summary}</ToolResult>
|
|
96
|
+
</View>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// We need toolCallId here; use a best-effort id from rawInput
|
|
101
|
+
const toolCallId = (rawInput?.id as string) || "unknown";
|
|
102
|
+
const executionResult = parsedResult?.data;
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<BashToolOutput
|
|
106
|
+
command={`${command}${timeoutLabel}`}
|
|
107
|
+
toolCallId={toolCallId}
|
|
108
|
+
executionResult={executionResult}
|
|
109
|
+
/>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const runInTerminalTool: ToolDef<RunInTerminalParsedData> = {
|
|
114
|
+
displayName: "Bash",
|
|
115
|
+
|
|
116
|
+
getHeaderParams(input: Record<string, unknown>): string {
|
|
117
|
+
try {
|
|
118
|
+
return (input?.command || "[no command]") as string;
|
|
119
|
+
} catch {
|
|
120
|
+
return "[unknown]";
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
renderUI(
|
|
125
|
+
rawInput: Record<string, unknown>,
|
|
126
|
+
status: ToolStatus,
|
|
127
|
+
parsedResult,
|
|
128
|
+
): React.ReactElement {
|
|
129
|
+
return <RunInTerminalUI rawInput={rawInput} status={status} parsedResult={parsedResult} />;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// components/tools/types.ts
|
|
2
|
+
import type { ParsedToolResult, ToolDisplayData } from "@codellm/agent";
|
|
3
|
+
import type { ReactElement } from "react";
|
|
4
|
+
|
|
5
|
+
export type ToolStatus = "pending" | "executing" | "success" | "error" | "rejected" | "interrupted";
|
|
6
|
+
|
|
7
|
+
export interface ToolDef<TData = Record<string, unknown>> {
|
|
8
|
+
displayName: string;
|
|
9
|
+
getHeaderParams(input: Record<string, unknown>): string;
|
|
10
|
+
renderUI(
|
|
11
|
+
input: Record<string, unknown>,
|
|
12
|
+
status: ToolStatus,
|
|
13
|
+
parsedResult?: ParsedToolResult<TData>,
|
|
14
|
+
displayData?: ToolDisplayData,
|
|
15
|
+
): ReactElement;
|
|
16
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ParsedToolResult, ToolDisplayData } from "@codellm/agent";
|
|
2
|
+
import type { ToolUseRequest, ToolUseResult } from "@codellm/api";
|
|
3
|
+
|
|
4
|
+
import { View, Text } from "@codellm/jar";
|
|
5
|
+
|
|
6
|
+
import { toolMap } from "./tools/index.js";
|
|
7
|
+
import { ToolHeader, ToolResult } from "./tools/shared/index.js";
|
|
8
|
+
|
|
9
|
+
type ToolProps<T extends ToolUseRequest> = T & {
|
|
10
|
+
result?: ToolUseResult;
|
|
11
|
+
displayData?: ToolDisplayData;
|
|
12
|
+
parsedResult?: ParsedToolResult;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generic Tool component that uses the new toolMap.
|
|
17
|
+
* Prefers parsedResult.data (from SDK parse()) over re-parsing toolUseResult.content.
|
|
18
|
+
*/
|
|
19
|
+
export function Tool(tool: ToolProps<ToolUseRequest>) {
|
|
20
|
+
const def = toolMap[tool.name];
|
|
21
|
+
|
|
22
|
+
if (!def) {
|
|
23
|
+
return (
|
|
24
|
+
<View flexDirection="column">
|
|
25
|
+
<Text color="red">Unknown tool: {tool.name}</Text>
|
|
26
|
+
</View>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let status: import("./tools/types.js").ToolStatus;
|
|
31
|
+
if (tool.result?.rejected) {
|
|
32
|
+
const parsed = (() => {
|
|
33
|
+
try {
|
|
34
|
+
return JSON.parse(tool.result.content) as { reason?: string };
|
|
35
|
+
} catch {
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
})();
|
|
39
|
+
status = parsed.reason === "interrupted" ? "interrupted" : "rejected";
|
|
40
|
+
} else if (tool.result) {
|
|
41
|
+
status = "success";
|
|
42
|
+
} else if (tool.parsedResult?.phase === "error") {
|
|
43
|
+
status = "error";
|
|
44
|
+
} else {
|
|
45
|
+
status = "executing";
|
|
46
|
+
}
|
|
47
|
+
const input = (tool.input ?? {}) as Record<string, unknown>;
|
|
48
|
+
|
|
49
|
+
let headerParams = "";
|
|
50
|
+
try {
|
|
51
|
+
headerParams = def.getHeaderParams(input);
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
return def.renderUI(input, status, tool.parsedResult, tool.displayData);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
60
|
+
console.error(`Error rendering tool ${tool.name}:`, errorMessage);
|
|
61
|
+
return (
|
|
62
|
+
<View flexDirection="column">
|
|
63
|
+
<ToolHeader name={def.displayName} params={headerParams} status="error" />
|
|
64
|
+
<ToolResult color="red">Error rendering tool</ToolResult>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Text } from "@codellm/jar";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { stripAnsi } from "../../../lib/ansi.js";
|
|
5
|
+
import { render, logInk } from "../../../lib/test-utils.js";
|
|
6
|
+
import { Divider } from "../divider.js";
|
|
7
|
+
|
|
8
|
+
describe.concurrent("Divider", () => {
|
|
9
|
+
it("should render a basic divider", () => {
|
|
10
|
+
const instance = render(<Divider />);
|
|
11
|
+
|
|
12
|
+
logInk(instance);
|
|
13
|
+
|
|
14
|
+
const output = stripAnsi(instance.frames.join(""));
|
|
15
|
+
|
|
16
|
+
expect(output).toBeDefined();
|
|
17
|
+
expect(typeof output).toBe("string");
|
|
18
|
+
// The divider should contain the default divider character
|
|
19
|
+
expect(output).toContain("─");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should render divider with custom character", () => {
|
|
23
|
+
const instance = render(<Divider dividerChar="=" />);
|
|
24
|
+
|
|
25
|
+
logInk(instance);
|
|
26
|
+
|
|
27
|
+
const output = stripAnsi(instance.frames.join(""));
|
|
28
|
+
|
|
29
|
+
expect(output).toBeDefined();
|
|
30
|
+
expect(output).toContain("=");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("should render divider with children", () => {
|
|
34
|
+
const instance = render(
|
|
35
|
+
<Divider>
|
|
36
|
+
<Text>Title</Text>
|
|
37
|
+
</Divider>,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
logInk(instance);
|
|
41
|
+
|
|
42
|
+
const output = stripAnsi(instance.frames.join(""));
|
|
43
|
+
|
|
44
|
+
expect(output).toBeDefined();
|
|
45
|
+
expect(output).toContain("Title");
|
|
46
|
+
// Should still have divider characters
|
|
47
|
+
expect(output).toContain("─");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("should render divider with padding", () => {
|
|
51
|
+
const instance = render(<Divider padding={2} />);
|
|
52
|
+
|
|
53
|
+
logInk(instance);
|
|
54
|
+
|
|
55
|
+
const output = stripAnsi(instance.frames.join(""));
|
|
56
|
+
|
|
57
|
+
expect(output).toBeDefined();
|
|
58
|
+
// Padding should add spaces
|
|
59
|
+
expect(output).toContain("─");
|
|
60
|
+
});
|
|
61
|
+
});
|