@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abacus-ai/cli",
3
- "version": "2.0.0-canary.0",
3
+ "version": "2.0.0-canary.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "abacusai": "dist/index.mjs"
@@ -302,7 +302,6 @@ export function createMockContext(
302
302
 
303
303
  uploadedDocs: new Set(),
304
304
  allowEditingPaths: [],
305
- terminalTimeoutMs: undefined,
306
305
 
307
306
  addDiff: vi.fn().mockReturnValue(undefined),
308
307
  onOutputUpdate: vi.fn().mockReturnValue(undefined),
@@ -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 = effectiveActivity === "pending";
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
- <Text dimColor>↑↓ option</Text>
298
- <Text dimColor>space select</Text>
299
- <Text dimColor>enter confirm</Text>
300
- {questions.length > 1 && (
299
+ {noteMode ? (
301
300
  <>
302
- <Text dimColor>←→ navigate</Text>
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
  );
@@ -227,6 +227,10 @@ export const DiffPreview = memo(
227
227
  const prefix = line[0];
228
228
  const content = line.slice(1);
229
229
 
230
+ if (prefix === "\\") {
231
+ continue;
232
+ }
233
+
230
234
  if (prefix === "+") {
231
235
  lines.push({
232
236
  type: "added",
@@ -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
- <Text dimColor>{showTabHint ? "Tab to add additional instructions" : ""}</Text>
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: "Accept", value: "accept" },
51
+ { label: "Yes, apply changes", value: "accept" },
51
52
  ...(!inPlanMode
52
53
  ? [
53
54
  {
54
- label: "Always accept all edits",
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: "Reject", value: "reject" },
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: "No", value: "reject" },
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 color="cyan">{command}</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>{description}</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
- ? `User answered ${Object.keys(answers).length} question${Object.keys(answers).length !== 1 ? "s" : ""}`
65
- : "User cancelled the questions"}
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
- _parsedResult?: ParsedToolResult,
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
- <ToolResult color="yellow">Plan mode entry rejected</ToolResult>
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
- <ToolResult color="yellow">Plan rejected</ToolResult>
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
- _parsedResult?: ParsedToolResult,
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 name="Bash" params={displayCommand} status={headerStatus} showSpinner />
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 name="Bash" params={displayCommand} status={headerStatus} showSpinner />
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 name="Bash" params={displayCommand} status={headerStatus} />
140
- <PreviewContent>
141
- {hasOutput ? (
142
- <>
143
- {isError && (
144
- <Text color="red" dimColor>
145
- Error:
146
- </Text>
147
- )}
148
- {lines.map((line, idx) => (
149
- <Text key={idx} color={isError ? "red" : undefined} dimColor>
150
- {line}
151
- </Text>
152
- ))}
153
- {isTruncated && (
154
- <Text dimColor>
155
- … +{hiddenCount} {isExpanded ? "more " : ""}line{hiddenCount !== 1 ? "s" : ""}
156
- {!isExpanded ? " (ctrl+o to expand)" : ""}
157
- </Text>
158
- )}
159
- </>
160
- ) : (
161
- <Text dimColor>(No output)</Text>
162
- )}
163
- </PreviewContent>
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
- <ToolResult color="yellow">Rejected</ToolResult>
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 name="Bash" params={command} status="success" />
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 command={command} toolCallId={toolCallId} executionResult={executionResult} />
105
+ <BashToolOutput
106
+ command={`${command}${timeoutLabel}`}
107
+ toolCallId={toolCallId}
108
+ executionResult={executionResult}
109
+ />
85
110
  );
86
111
  }
87
112
 
@@ -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
- setVisible((prev) => !prev);
16
+ startTransition(() => {
17
+ setVisible((prev) => !prev);
18
+ });
17
19
  }, BLINK_INTERVAL);
18
20
 
19
21
  return () => clearInterval(interval);