@abacus-ai/cli 2.0.0-canary.0 → 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/dist/index.mjs +680 -460
- package/package.json +1 -1
- package/src/__e2e__/helpers/test-helpers.ts +0 -1
- package/src/components/composer/index.tsx +9 -8
- package/src/components/tool-permissions/ask-user-question-permission-ui.tsx +14 -7
- package/src/components/tool-permissions/diff-preview.tsx +4 -0
- package/src/components/tool-permissions/permission-options.tsx +31 -5
- package/src/components/tool-permissions/tool-permission-ui.tsx +36 -14
- package/src/components/tools/agent/ask-user-question.tsx +9 -3
- package/src/components/tools/agent/enter-plan-mode.tsx +9 -3
- package/src/components/tools/agent/exit-plan-mode.tsx +12 -4
- package/src/components/tools/terminal/bash-tool-output.tsx +43 -29
- package/src/components/tools/terminal/get-terminal-output.tsx +8 -2
- package/src/components/tools/terminal/run-in-terminal.tsx +31 -6
- package/src/components/tools.tsx +2 -0
- package/src/components/ui/blinking-indicator.tsx +4 -2
- package/src/components/ui/input.tsx +1 -1
- package/src/components/ui/shimmer.tsx +20 -28
- package/src/entrypoints/repl.tsx +8 -20
- package/src/providers/agent.tsx +48 -60
- package/src/tools/utils/tool-ui-components.tsx +21 -3
- package/tsconfig.json +6 -1
- package/src/terminal/suspend.ts +0 -58
package/package.json
CHANGED
|
@@ -632,6 +632,13 @@ const ComposerInner = memo(
|
|
|
632
632
|
return;
|
|
633
633
|
}
|
|
634
634
|
|
|
635
|
+
if (currentHandler?.shouldPreventDefault) {
|
|
636
|
+
if (currentHandler.shouldPreventDefault(event.input || "", event.key)) {
|
|
637
|
+
event.preventDefault();
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
635
642
|
// Handle prompt history navigation for Prompt/Help/Files modes.
|
|
636
643
|
// Only trigger when the cursor is at the extreme edge of the buffer:
|
|
637
644
|
// Up arrow → only when cursor is on the first visual line (top of buffer)
|
|
@@ -688,13 +695,6 @@ const ComposerInner = memo(
|
|
|
688
695
|
}
|
|
689
696
|
}
|
|
690
697
|
|
|
691
|
-
if (currentHandler?.shouldPreventDefault) {
|
|
692
|
-
if (currentHandler.shouldPreventDefault(event.input || "", event.key)) {
|
|
693
|
-
event.preventDefault();
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
}
|
|
697
|
-
|
|
698
698
|
if (atStart && event.key.backspace) {
|
|
699
699
|
if (isWaitingForYoloConfirm) {
|
|
700
700
|
resetToNormal();
|
|
@@ -911,7 +911,8 @@ const ComposerInner = memo(
|
|
|
911
911
|
|
|
912
912
|
const grammar = useMemo(() => buildMentionGrammar(), []);
|
|
913
913
|
const effectiveActivity = mode === ComposerMode.Bash ? getActivity() : activity;
|
|
914
|
-
const isInputDisabled =
|
|
914
|
+
const isInputDisabled =
|
|
915
|
+
effectiveActivity === "pending" || agentStatus === AgentStatus.WaitingForToolPermission;
|
|
915
916
|
|
|
916
917
|
const pathText = (
|
|
917
918
|
<Text wrap="truncate-middle" color="yellowBright">
|
|
@@ -2,6 +2,7 @@ import type { AskUserQuestionItem, PermissionDecision } from "@codellm/agent";
|
|
|
2
2
|
|
|
3
3
|
import { View, Text } from "@codellm/jar";
|
|
4
4
|
import { type Key, useInput } from "@codellm/jar";
|
|
5
|
+
import process from "node:process";
|
|
5
6
|
import { memo, useCallback, useRef, useState } from "react";
|
|
6
7
|
|
|
7
8
|
import { TextBuffer } from "../../lib/text-buffer.js";
|
|
@@ -270,6 +271,7 @@ export const AskUserQuestionPermissionUI = memo(
|
|
|
270
271
|
<Text dimColor>Note: </Text>
|
|
271
272
|
<Input
|
|
272
273
|
ref={noteBufferRef}
|
|
274
|
+
width={process.stdout.columns - 10}
|
|
273
275
|
placeholder="Add context or instructions…"
|
|
274
276
|
onSubmit={handleNoteSubmit}
|
|
275
277
|
onKeyDown={handleNoteKeyDown}
|
|
@@ -294,16 +296,21 @@ export const AskUserQuestionPermissionUI = memo(
|
|
|
294
296
|
borderRight={false}
|
|
295
297
|
/>
|
|
296
298
|
<View flexDirection="row" paddingLeft={1} paddingBottom={1} gap={2}>
|
|
297
|
-
|
|
298
|
-
<Text dimColor>space select</Text>
|
|
299
|
-
<Text dimColor>enter confirm</Text>
|
|
300
|
-
{questions.length > 1 && (
|
|
299
|
+
{noteMode ? (
|
|
301
300
|
<>
|
|
302
|
-
<Text dimColor
|
|
301
|
+
<Text dimColor>enter save note</Text>
|
|
302
|
+
<Text dimColor>esc discard note</Text>
|
|
303
|
+
</>
|
|
304
|
+
) : (
|
|
305
|
+
<>
|
|
306
|
+
<Text dimColor>↑↓ option</Text>
|
|
307
|
+
<Text dimColor>space select</Text>
|
|
308
|
+
<Text dimColor>enter confirm</Text>
|
|
309
|
+
{questions.length > 1 && <Text dimColor>←→ navigate</Text>}
|
|
310
|
+
<Text dimColor>tab add note</Text>
|
|
311
|
+
<Text dimColor>esc skip</Text>
|
|
303
312
|
</>
|
|
304
313
|
)}
|
|
305
|
-
<Text dimColor>tab note</Text>
|
|
306
|
-
<Text dimColor>esc skip</Text>
|
|
307
314
|
</View>
|
|
308
315
|
</View>
|
|
309
316
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { View, Text } from "@codellm/jar";
|
|
2
2
|
import { type Key, useInput } from "@codellm/jar";
|
|
3
|
+
import process from "node:process";
|
|
3
4
|
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
4
5
|
|
|
5
6
|
import type { ToolPermissionDecision, ToolPermissionResult } from "../../providers/agent.js";
|
|
@@ -50,6 +51,7 @@ const PermissionOptionItem = memo(
|
|
|
50
51
|
<Text>{`${index + 1}. ${inputPrefix || ""}`}</Text>
|
|
51
52
|
<Input
|
|
52
53
|
ref={inputBufferRef}
|
|
54
|
+
width={process.stdout.columns - 10}
|
|
53
55
|
placeholder={inputPlaceholder}
|
|
54
56
|
onSubmit={onInputSubmit}
|
|
55
57
|
onKeyDown={onInputKeyDown}
|
|
@@ -194,6 +196,13 @@ export const PermissionOptions = memo(
|
|
|
194
196
|
event.preventDefault();
|
|
195
197
|
return;
|
|
196
198
|
}
|
|
199
|
+
if (event.key.escape) {
|
|
200
|
+
clearPlaceholders();
|
|
201
|
+
setTabPressedForInput(false);
|
|
202
|
+
onSelectRef.current({ decision: "reject" });
|
|
203
|
+
event.preventDefault();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
197
206
|
},
|
|
198
207
|
[clearPlaceholders],
|
|
199
208
|
);
|
|
@@ -209,6 +218,10 @@ export const PermissionOptions = memo(
|
|
|
209
218
|
setSelectedIndex((prev) => Math.min(optionsLengthRef.current - 1, prev + 1));
|
|
210
219
|
return;
|
|
211
220
|
}
|
|
221
|
+
if (key.escape) {
|
|
222
|
+
onSelectRef.current({ decision: "reject" });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
212
225
|
if (key.tab && key.shift) {
|
|
213
226
|
// Shift+Tab: Find and select the 'allowAlways' option
|
|
214
227
|
const allowAlwaysIndex = optionsRef.current.findIndex((opt) => opt.value === "allowAlways");
|
|
@@ -322,14 +335,13 @@ export const PermissionOptions = memo(
|
|
|
322
335
|
return "";
|
|
323
336
|
};
|
|
324
337
|
|
|
325
|
-
// Show "Tab to add additional instructions" hint when:
|
|
326
|
-
// - Not in input mode
|
|
327
|
-
// - Current option is accept or reject (supports Tab input)
|
|
328
338
|
const showTabHint =
|
|
329
339
|
!tabPressedForInput &&
|
|
330
340
|
currentOption &&
|
|
331
341
|
(currentOption.value === "accept" || currentOption.value === "reject");
|
|
332
342
|
|
|
343
|
+
const hasAllowAlways = filteredOptions.some((opt) => opt.value === "allowAlways");
|
|
344
|
+
|
|
333
345
|
return (
|
|
334
346
|
<View flexDirection="column">
|
|
335
347
|
{filteredOptions.map((option, index) => {
|
|
@@ -365,8 +377,22 @@ export const PermissionOptions = memo(
|
|
|
365
377
|
/>
|
|
366
378
|
);
|
|
367
379
|
})}
|
|
368
|
-
<View marginTop={1}>
|
|
369
|
-
|
|
380
|
+
<View flexDirection="row" marginTop={1} gap={2}>
|
|
381
|
+
{tabPressedForInput ? (
|
|
382
|
+
<>
|
|
383
|
+
<Text dimColor>enter submit</Text>
|
|
384
|
+
<Text dimColor>tab discard note</Text>
|
|
385
|
+
<Text dimColor>esc reject</Text>
|
|
386
|
+
</>
|
|
387
|
+
) : (
|
|
388
|
+
<>
|
|
389
|
+
<Text dimColor>↑↓ navigate</Text>
|
|
390
|
+
<Text dimColor>enter select</Text>
|
|
391
|
+
{showTabHint && <Text dimColor>tab add note</Text>}
|
|
392
|
+
{hasAllowAlways && <Text dimColor>shift+tab don't ask again</Text>}
|
|
393
|
+
<Text dimColor>esc reject</Text>
|
|
394
|
+
</>
|
|
395
|
+
)}
|
|
370
396
|
</View>
|
|
371
397
|
</View>
|
|
372
398
|
);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { PermissionDecision } from "@codellm/agent";
|
|
2
2
|
|
|
3
3
|
import { View, Text } from "@codellm/jar";
|
|
4
|
+
import { highlight, supportsLanguage } from "cli-highlight";
|
|
4
5
|
import { memo } from "react";
|
|
5
6
|
|
|
6
7
|
import type { PendingToolPermission, ToolPermissionResult } from "../../providers/agent.js";
|
|
@@ -47,17 +48,17 @@ const EditFilePermissionUI = memo(
|
|
|
47
48
|
const inPlanMode = agentMode === AgentMode.PlanMode;
|
|
48
49
|
|
|
49
50
|
const options: PermissionOption[] = [
|
|
50
|
-
{ label: "
|
|
51
|
+
{ label: "Yes, apply changes", value: "accept" },
|
|
51
52
|
...(!inPlanMode
|
|
52
53
|
? [
|
|
53
54
|
{
|
|
54
|
-
label: "
|
|
55
|
+
label: "Yes, and auto-accept all edits",
|
|
55
56
|
value: "allowAlways" as const,
|
|
56
57
|
hint: "shift+tab",
|
|
57
58
|
},
|
|
58
59
|
]
|
|
59
60
|
: []),
|
|
60
|
-
{ label: "
|
|
61
|
+
{ label: "No, reject changes", value: "reject" },
|
|
61
62
|
];
|
|
62
63
|
|
|
63
64
|
const handleSelect = (result: ToolPermissionResult) => {
|
|
@@ -116,8 +117,8 @@ const DeleteFilePermissionUI = memo(
|
|
|
116
117
|
onResolve: (decision: PermissionDecision) => void;
|
|
117
118
|
}) => {
|
|
118
119
|
const options: PermissionOption[] = [
|
|
119
|
-
{ label: "Yes", value: "accept" },
|
|
120
|
-
{ label: "No", value: "reject" },
|
|
120
|
+
{ label: "Yes, delete file", value: "accept" },
|
|
121
|
+
{ label: "No, keep file", value: "reject" },
|
|
121
122
|
];
|
|
122
123
|
|
|
123
124
|
const handleSelect = (result: ToolPermissionResult) => {
|
|
@@ -157,8 +158,8 @@ const BrowserActionPermissionUI = memo(
|
|
|
157
158
|
onResolve: (decision: PermissionDecision) => void;
|
|
158
159
|
}) => {
|
|
159
160
|
const options: PermissionOption[] = [
|
|
160
|
-
{ label: "Yes", value: "accept" },
|
|
161
|
-
{ label: "No", value: "reject" },
|
|
161
|
+
{ label: "Yes, allow action", value: "accept" },
|
|
162
|
+
{ label: "No, deny action", value: "reject" },
|
|
162
163
|
];
|
|
163
164
|
|
|
164
165
|
const handleSelect = (result: ToolPermissionResult) => {
|
|
@@ -199,8 +200,9 @@ const RunTerminalPermissionUI = memo(
|
|
|
199
200
|
onResolve: (decision: PermissionDecision) => void;
|
|
200
201
|
}) => {
|
|
201
202
|
const options: PermissionOption[] = [
|
|
202
|
-
{ label: "Yes", value: "accept" },
|
|
203
|
-
{ label: "
|
|
203
|
+
{ label: "Yes, run command", value: "accept" },
|
|
204
|
+
{ label: "Yes, and auto-accept all commands", value: "allowAlways", hint: "shift+tab" },
|
|
205
|
+
{ label: "No, skip command", value: "reject" },
|
|
204
206
|
];
|
|
205
207
|
|
|
206
208
|
const handleSelect = (result: ToolPermissionResult) => {
|
|
@@ -211,7 +213,17 @@ const RunTerminalPermissionUI = memo(
|
|
|
211
213
|
<View flexDirection="column">
|
|
212
214
|
<PermissionPreviewHeader title="Bash command" />
|
|
213
215
|
<View flexDirection="column" paddingLeft={1} paddingY={1}>
|
|
214
|
-
<Text
|
|
216
|
+
<Text>
|
|
217
|
+
{supportsLanguage("bash")
|
|
218
|
+
? (() => {
|
|
219
|
+
try {
|
|
220
|
+
return highlight(command, { language: "bash" });
|
|
221
|
+
} catch {
|
|
222
|
+
return command;
|
|
223
|
+
}
|
|
224
|
+
})()
|
|
225
|
+
: command}
|
|
226
|
+
</Text>
|
|
215
227
|
{background && (
|
|
216
228
|
<View paddingTop={1}>
|
|
217
229
|
<Text dimColor>(Running in background)</Text>
|
|
@@ -249,8 +261,8 @@ const GenericPermissionUI = memo(
|
|
|
249
261
|
onResolve: (decision: PermissionDecision) => void;
|
|
250
262
|
}) => {
|
|
251
263
|
const options: PermissionOption[] = [
|
|
252
|
-
{ label: "Yes", value: "accept" },
|
|
253
|
-
{ label: "No", value: "reject" },
|
|
264
|
+
{ label: "Yes, allow", value: "accept" },
|
|
265
|
+
{ label: "No, deny", value: "reject" },
|
|
254
266
|
];
|
|
255
267
|
|
|
256
268
|
const handleSelect = (result: ToolPermissionResult) => {
|
|
@@ -261,7 +273,17 @@ const GenericPermissionUI = memo(
|
|
|
261
273
|
<View flexDirection="column">
|
|
262
274
|
<PermissionPreviewHeader title={title} />
|
|
263
275
|
<View flexDirection="column" paddingLeft={1} paddingY={1}>
|
|
264
|
-
<Text>
|
|
276
|
+
<Text>
|
|
277
|
+
{supportsLanguage("json")
|
|
278
|
+
? (() => {
|
|
279
|
+
try {
|
|
280
|
+
return highlight(description, { language: "json" });
|
|
281
|
+
} catch {
|
|
282
|
+
return description;
|
|
283
|
+
}
|
|
284
|
+
})()
|
|
285
|
+
: description}
|
|
286
|
+
</Text>
|
|
265
287
|
</View>
|
|
266
288
|
<View
|
|
267
289
|
borderStyle={dashedBorderStyle}
|
|
@@ -290,12 +312,12 @@ const ExitPlanModePermissionUI = memo(
|
|
|
290
312
|
onResolve: (decision: PermissionDecision) => void;
|
|
291
313
|
}) => {
|
|
292
314
|
const options: PermissionOption[] = [
|
|
315
|
+
{ label: "Yes, implement the plan (approve each edit)", value: "accept" },
|
|
293
316
|
{
|
|
294
317
|
label: "Yes, implement the plan (auto-accept all edits)",
|
|
295
318
|
value: "allowAlways",
|
|
296
319
|
hint: "shift+tab",
|
|
297
320
|
},
|
|
298
|
-
{ label: "Yes, implement the plan (approve each edit)", value: "accept" },
|
|
299
321
|
{ label: "No, keep planning", value: "reject" },
|
|
300
322
|
];
|
|
301
323
|
|
|
@@ -60,9 +60,15 @@ export const askUserQuestionTool: ToolDef<AskUserQuestionParsedData> = {
|
|
|
60
60
|
<View flexDirection="column">
|
|
61
61
|
<ToolHeader name="AskUserQuestion" status={hasAnswers ? "success" : "rejected"} />
|
|
62
62
|
<ToolResult color={hasAnswers ? "green" : "yellow"}>
|
|
63
|
-
{hasAnswers
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
{hasAnswers ? (
|
|
64
|
+
`User answered ${Object.keys(answers).length} question${Object.keys(answers).length !== 1 ? "s" : ""}`
|
|
65
|
+
) : parsedResult?.userNote ? (
|
|
66
|
+
<>
|
|
67
|
+
Rejected · <Text italic>"{parsedResult.userNote}"</Text>
|
|
68
|
+
</>
|
|
69
|
+
) : (
|
|
70
|
+
"User cancelled the questions"
|
|
71
|
+
)}
|
|
66
72
|
</ToolResult>
|
|
67
73
|
</View>
|
|
68
74
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ParsedToolResult } from "@codellm/agent";
|
|
2
2
|
|
|
3
|
-
import { View } from "@codellm/jar";
|
|
3
|
+
import { View, Text } from "@codellm/jar";
|
|
4
4
|
import React from "react";
|
|
5
5
|
|
|
6
6
|
import type { ToolDef, ToolStatus } from "../types.js";
|
|
@@ -15,7 +15,7 @@ export const enterPlanModeTool: ToolDef = {
|
|
|
15
15
|
renderUI(
|
|
16
16
|
_rawInput: Record<string, unknown>,
|
|
17
17
|
status: ToolStatus,
|
|
18
|
-
|
|
18
|
+
parsedResult?: ParsedToolResult,
|
|
19
19
|
): React.ReactElement {
|
|
20
20
|
if (status === "interrupted") {
|
|
21
21
|
return (
|
|
@@ -30,7 +30,13 @@ export const enterPlanModeTool: ToolDef = {
|
|
|
30
30
|
return (
|
|
31
31
|
<View flexDirection="column">
|
|
32
32
|
<ToolHeader name="EnterPlanMode" status="rejected" />
|
|
33
|
-
|
|
33
|
+
{parsedResult?.userNote ? (
|
|
34
|
+
<ToolResult color="yellow">
|
|
35
|
+
Rejected · <Text italic>"{parsedResult.userNote}"</Text>
|
|
36
|
+
</ToolResult>
|
|
37
|
+
) : (
|
|
38
|
+
<ToolResult color="yellow">Rejected</ToolResult>
|
|
39
|
+
)}
|
|
34
40
|
</View>
|
|
35
41
|
);
|
|
36
42
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ParsedToolResult } from "@codellm/agent";
|
|
2
2
|
|
|
3
3
|
import { AgentMode } from "@codellm/agent";
|
|
4
|
-
import { View } from "@codellm/jar";
|
|
4
|
+
import { View, Text } from "@codellm/jar";
|
|
5
5
|
import React from "react";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
@@ -16,9 +16,11 @@ const ExitPlanModeParams = z.object({ planFilePath: z.string() });
|
|
|
16
16
|
function ExitPlanModeUI({
|
|
17
17
|
rawInput,
|
|
18
18
|
status,
|
|
19
|
+
parsedResult,
|
|
19
20
|
}: {
|
|
20
21
|
rawInput: Record<string, unknown>;
|
|
21
22
|
status: ToolStatus;
|
|
23
|
+
parsedResult?: ParsedToolResult;
|
|
22
24
|
}) {
|
|
23
25
|
const { agentMode } = useAgentMode();
|
|
24
26
|
const pr = ExitPlanModeParams.safeParse(snakeToCamel(rawInput));
|
|
@@ -38,7 +40,13 @@ function ExitPlanModeUI({
|
|
|
38
40
|
return (
|
|
39
41
|
<View flexDirection="column">
|
|
40
42
|
<ToolHeader name="ExitPlanMode" status="rejected" />
|
|
41
|
-
|
|
43
|
+
{parsedResult?.userNote ? (
|
|
44
|
+
<ToolResult color="yellow">
|
|
45
|
+
Rejected · <Text italic>"{parsedResult.userNote}"</Text>
|
|
46
|
+
</ToolResult>
|
|
47
|
+
) : (
|
|
48
|
+
<ToolResult color="yellow">Rejected</ToolResult>
|
|
49
|
+
)}
|
|
42
50
|
</View>
|
|
43
51
|
);
|
|
44
52
|
}
|
|
@@ -68,8 +76,8 @@ export const exitPlanModeTool: ToolDef = {
|
|
|
68
76
|
renderUI(
|
|
69
77
|
rawInput: Record<string, unknown>,
|
|
70
78
|
status: ToolStatus,
|
|
71
|
-
|
|
79
|
+
parsedResult?: ParsedToolResult,
|
|
72
80
|
): React.ReactElement {
|
|
73
|
-
return <ExitPlanModeUI rawInput={rawInput} status={status} />;
|
|
81
|
+
return <ExitPlanModeUI rawInput={rawInput} status={status} parsedResult={parsedResult} />;
|
|
74
82
|
},
|
|
75
83
|
};
|
|
@@ -102,7 +102,13 @@ export function BashToolOutput({ command, toolCallId, executionResult }: BashToo
|
|
|
102
102
|
if (isExecuting && lines.length === 0) {
|
|
103
103
|
return (
|
|
104
104
|
<View flexDirection="column" gap={0}>
|
|
105
|
-
<ToolHeader
|
|
105
|
+
<ToolHeader
|
|
106
|
+
name="Bash"
|
|
107
|
+
params={displayCommand}
|
|
108
|
+
paramsLanguage="bash"
|
|
109
|
+
status={headerStatus}
|
|
110
|
+
showSpinner
|
|
111
|
+
/>
|
|
106
112
|
<ToolResult color="gray">Running…</ToolResult>
|
|
107
113
|
</View>
|
|
108
114
|
);
|
|
@@ -112,7 +118,13 @@ export function BashToolOutput({ command, toolCallId, executionResult }: BashToo
|
|
|
112
118
|
const elapsedText = elapsedMs > 0 ? ` (${formatElapsedTime(elapsedMs)})` : "";
|
|
113
119
|
return (
|
|
114
120
|
<View flexDirection="column" gap={0}>
|
|
115
|
-
<ToolHeader
|
|
121
|
+
<ToolHeader
|
|
122
|
+
name="Bash"
|
|
123
|
+
params={displayCommand}
|
|
124
|
+
paramsLanguage="bash"
|
|
125
|
+
status={headerStatus}
|
|
126
|
+
showSpinner
|
|
127
|
+
/>
|
|
116
128
|
<PreviewContent>
|
|
117
129
|
{lines.map((line, idx) => (
|
|
118
130
|
<Text key={idx}>{line}</Text>
|
|
@@ -130,37 +142,39 @@ export function BashToolOutput({ command, toolCallId, executionResult }: BashToo
|
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
if (executionResult) {
|
|
145
|
+
const isStaleResult = !finalOutput && (executionResult.duration ?? 0) === 0;
|
|
133
146
|
const duration = formatDuration(executionResult.duration ?? 0);
|
|
134
|
-
const resultText = `exit ${exitCode} (${duration})`;
|
|
147
|
+
const resultText = isStaleResult ? `exit ${exitCode}` : `exit ${exitCode} (${duration})`;
|
|
135
148
|
const hasOutput = lines.length > 0;
|
|
136
149
|
|
|
137
150
|
return (
|
|
138
151
|
<View flexDirection="column" gap={0}>
|
|
139
|
-
<ToolHeader
|
|
140
|
-
|
|
141
|
-
{
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
)}
|
|
164
178
|
<ToolResult color={isError ? "red" : "gray"}>{resultText}</ToolResult>
|
|
165
179
|
</View>
|
|
166
180
|
);
|
|
@@ -168,7 +182,7 @@ export function BashToolOutput({ command, toolCallId, executionResult }: BashToo
|
|
|
168
182
|
|
|
169
183
|
return (
|
|
170
184
|
<View flexDirection="column">
|
|
171
|
-
<ToolHeader name="Bash" params={displayCommand} status="pending" />
|
|
185
|
+
<ToolHeader name="Bash" params={displayCommand} paramsLanguage="bash" status="pending" />
|
|
172
186
|
</View>
|
|
173
187
|
);
|
|
174
188
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ParsedToolResult } from "@codellm/agent";
|
|
2
2
|
|
|
3
|
-
import { View } from "@codellm/jar";
|
|
3
|
+
import { View, Text } from "@codellm/jar";
|
|
4
4
|
import React from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
|
|
@@ -54,7 +54,13 @@ export const getTerminalOutputTool: ToolDef = {
|
|
|
54
54
|
return (
|
|
55
55
|
<View flexDirection="column">
|
|
56
56
|
<ToolHeader name="TerminalOutput" params={terminalIdParam} status="rejected" />
|
|
57
|
-
|
|
57
|
+
{parsedResult?.userNote ? (
|
|
58
|
+
<ToolResult color="yellow">
|
|
59
|
+
Rejected · <Text italic>"{parsedResult.userNote}"</Text>
|
|
60
|
+
</ToolResult>
|
|
61
|
+
) : (
|
|
62
|
+
<ToolResult color="yellow">Rejected</ToolResult>
|
|
63
|
+
)}
|
|
58
64
|
</View>
|
|
59
65
|
);
|
|
60
66
|
}
|
|
@@ -16,6 +16,7 @@ const RunInTerminalParams = z.object({
|
|
|
16
16
|
isBackground: coerceOptionalBoolean,
|
|
17
17
|
id: z.string().optional(),
|
|
18
18
|
interactiveMode: coerceOptionalBoolean,
|
|
19
|
+
timeout: z.coerce.number().optional(),
|
|
19
20
|
});
|
|
20
21
|
|
|
21
22
|
function RunInTerminalUI({
|
|
@@ -31,18 +32,19 @@ function RunInTerminalUI({
|
|
|
31
32
|
if (!parseResult.success) {
|
|
32
33
|
return (
|
|
33
34
|
<View flexDirection="column">
|
|
34
|
-
<ToolHeader name="Bash" params="" status="error" />
|
|
35
|
+
<ToolHeader name="Bash" params="" paramsLanguage="bash" status="error" />
|
|
35
36
|
<ToolResult color="red">Invalid params</ToolResult>
|
|
36
37
|
</View>
|
|
37
38
|
);
|
|
38
39
|
}
|
|
39
40
|
const params = parseResult.data;
|
|
40
41
|
const command = params.command || "";
|
|
42
|
+
const timeoutLabel = params.timeout ? ` (timeout: ${(params.timeout / 1000).toFixed(0)}s)` : "";
|
|
41
43
|
|
|
42
44
|
if (status === "interrupted") {
|
|
43
45
|
return (
|
|
44
46
|
<View flexDirection="column">
|
|
45
|
-
<ToolHeader name="Bash" params={command} status="interrupted" />
|
|
47
|
+
<ToolHeader name="Bash" params={command} paramsLanguage="bash" status="interrupted" />
|
|
46
48
|
<ToolResult color="yellow">Interrupted · What should I do instead?</ToolResult>
|
|
47
49
|
</View>
|
|
48
50
|
);
|
|
@@ -52,7 +54,7 @@ function RunInTerminalUI({
|
|
|
52
54
|
if (parsedResult?.userNote) {
|
|
53
55
|
return (
|
|
54
56
|
<View flexDirection="column">
|
|
55
|
-
<ToolHeader name="Bash" params={command} status="rejected" />
|
|
57
|
+
<ToolHeader name="Bash" params={command} paramsLanguage="bash" status="rejected" />
|
|
56
58
|
<ToolResult color="yellow">
|
|
57
59
|
Rejected · <Text italic>"{parsedResult.userNote}"</Text>
|
|
58
60
|
</ToolResult>
|
|
@@ -61,16 +63,35 @@ function RunInTerminalUI({
|
|
|
61
63
|
}
|
|
62
64
|
return (
|
|
63
65
|
<View flexDirection="column">
|
|
64
|
-
<ToolHeader name="Bash" params={command} status="rejected" />
|
|
66
|
+
<ToolHeader name="Bash" params={command} paramsLanguage="bash" status="rejected" />
|
|
65
67
|
<ToolResult color="yellow">Rejected</ToolResult>
|
|
66
68
|
</View>
|
|
67
69
|
);
|
|
68
70
|
}
|
|
69
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
|
+
|
|
70
86
|
if (parsedResult?.isBackground) {
|
|
71
87
|
return (
|
|
72
88
|
<View flexDirection="column">
|
|
73
|
-
<ToolHeader
|
|
89
|
+
<ToolHeader
|
|
90
|
+
name="Bash"
|
|
91
|
+
params={`${command}${timeoutLabel}`}
|
|
92
|
+
paramsLanguage="bash"
|
|
93
|
+
status="success"
|
|
94
|
+
/>
|
|
74
95
|
<ToolResult>{parsedResult.summary}</ToolResult>
|
|
75
96
|
</View>
|
|
76
97
|
);
|
|
@@ -81,7 +102,11 @@ function RunInTerminalUI({
|
|
|
81
102
|
const executionResult = parsedResult?.data;
|
|
82
103
|
|
|
83
104
|
return (
|
|
84
|
-
<BashToolOutput
|
|
105
|
+
<BashToolOutput
|
|
106
|
+
command={`${command}${timeoutLabel}`}
|
|
107
|
+
toolCallId={toolCallId}
|
|
108
|
+
executionResult={executionResult}
|
|
109
|
+
/>
|
|
85
110
|
);
|
|
86
111
|
}
|
|
87
112
|
|
package/src/components/tools.tsx
CHANGED
|
@@ -39,6 +39,8 @@ export function Tool(tool: ToolProps<ToolUseRequest>) {
|
|
|
39
39
|
status = parsed.reason === "interrupted" ? "interrupted" : "rejected";
|
|
40
40
|
} else if (tool.result) {
|
|
41
41
|
status = "success";
|
|
42
|
+
} else if (tool.parsedResult?.phase === "error") {
|
|
43
|
+
status = "error";
|
|
42
44
|
} else {
|
|
43
45
|
status = "executing";
|
|
44
46
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Text } from "@codellm/jar";
|
|
2
|
-
import { memo, useEffect, useState } from "react";
|
|
2
|
+
import { memo, startTransition, useEffect, useState } from "react";
|
|
3
3
|
|
|
4
4
|
interface BlinkingIndicatorProps {
|
|
5
5
|
char: string;
|
|
@@ -13,7 +13,9 @@ export const BlinkingIndicator = memo(({ char, color }: BlinkingIndicatorProps)
|
|
|
13
13
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
const interval = setInterval(() => {
|
|
16
|
-
|
|
16
|
+
startTransition(() => {
|
|
17
|
+
setVisible((prev) => !prev);
|
|
18
|
+
});
|
|
17
19
|
}, BLINK_INTERVAL);
|
|
18
20
|
|
|
19
21
|
return () => clearInterval(interval);
|