@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.
@@ -165,7 +165,7 @@ export const Input = memo(
165
165
  [buffer, handleSubmit, onKeyDown, disabled],
166
166
  );
167
167
 
168
- useInput(handleKeypress, { isActive: true });
168
+ useInput(handleKeypress, { isActive: !disabled });
169
169
 
170
170
  const linesToRender = buffer.viewportVisualLines;
171
171
  const [cursorVisualRowAbsolute, cursorVisualColAbsolute] = buffer.visualCursor;
@@ -1,6 +1,7 @@
1
1
  import { Text } from "@codellm/jar";
2
+ import chalk from "chalk";
2
3
  import { startTransition } from "react";
3
- import { memo, useCallback, useEffect, useMemo, useState } from "react";
4
+ import { memo, useEffect, useMemo, useState } from "react";
4
5
 
5
6
  interface ShimmerProps {
6
7
  text: string;
@@ -27,31 +28,22 @@ export const Shimmer = memo(({ text, color = "#94a3b8" }: ShimmerProps) => {
27
28
 
28
29
  const baseRgb = useMemo(() => parseHexColor(color), [color]);
29
30
 
30
- const getCharColor = useCallback(
31
- (index: number): string => {
32
- const peakPosition = offset % textLength;
33
-
34
- let distance = Math.abs(index - peakPosition);
31
+ const coloredText = useMemo(() => {
32
+ const peakPosition = offset % textLength;
33
+ const gradientWidth = Math.max(textLength * 0.6, 3);
34
+ let result = "";
35
+ for (let i = 0; i < textLength; i++) {
36
+ let distance = Math.abs(i - peakPosition);
35
37
  const wrapDistance = textLength - distance;
36
38
  const circularDistance = Math.min(distance, wrapDistance);
37
-
38
- const gradientWidth = Math.max(textLength * 0.6, 3);
39
39
  const brightness = Math.max(0, 1 - circularDistance / gradientWidth);
40
+ const rgb = interpolateRgb(baseRgb, brightness);
41
+ result += chalk.rgb(rgb.r, rgb.g, rgb.b)(text[i]);
42
+ }
43
+ return result;
44
+ }, [offset, textLength, baseRgb, text]);
40
45
 
41
- return interpolateColor(baseRgb, brightness);
42
- },
43
- [offset, textLength, baseRgb],
44
- );
45
-
46
- return (
47
- <Text>
48
- {text.split("").map((char, index) => (
49
- <Text key={`${char}-${index}`} color={getCharColor(index)}>
50
- {char}
51
- </Text>
52
- ))}
53
- </Text>
54
- );
46
+ return <Text>{coloredText}</Text>;
55
47
  });
56
48
 
57
49
  Shimmer.displayName = "Shimmer";
@@ -71,14 +63,14 @@ function parseHexColor(hex: string): RGB {
71
63
  };
72
64
  }
73
65
 
74
- function interpolateColor(base: RGB, brightness: number): string {
66
+ function interpolateRgb(base: RGB, brightness: number): RGB {
75
67
  const minR = Math.floor(base.r * DIM_FACTOR);
76
68
  const minG = Math.floor(base.g * DIM_FACTOR);
77
69
  const minB = Math.floor(base.b * DIM_FACTOR);
78
70
 
79
- const newR = Math.floor(minR + (base.r - minR) * brightness);
80
- const newG = Math.floor(minG + (base.g - minG) * brightness);
81
- const newB = Math.floor(minB + (base.b - minB) * brightness);
82
-
83
- return `#${newR.toString(16).padStart(2, "0")}${newG.toString(16).padStart(2, "0")}${newB.toString(16).padStart(2, "0")}`;
71
+ return {
72
+ r: Math.floor(minR + (base.r - minR) * brightness),
73
+ g: Math.floor(minG + (base.g - minG) * brightness),
74
+ b: Math.floor(minB + (base.b - minB) * brightness),
75
+ };
84
76
  }
@@ -1,7 +1,7 @@
1
1
  import type { AbacusClient } from "@codellm/api";
2
2
 
3
3
  import { AuthManager } from "@codellm/auth";
4
- import { View, render, Text, useInput } from "@codellm/jar";
4
+ import { View, render, useInput } from "@codellm/jar";
5
5
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
6
  import {
7
7
  Activity,
@@ -40,7 +40,6 @@ import { Timing } from "../lib/timing.js";
40
40
  import { AgentProvider, AgentStatus } from "../providers/agent.js";
41
41
  import { ApiClientProvider } from "../providers/api-client.js";
42
42
  import { onExit, gracefulExit } from "../terminal/exit.js";
43
- import { setupSuspendHandler } from "../terminal/suspend.js";
44
43
  import { ThemeProvider } from "../theme/index.js";
45
44
 
46
45
  /** Group consecutive "groupable" tool call segments together for compact rendering. */
@@ -186,15 +185,12 @@ export function App({ timing }: { timing: Timing }) {
186
185
  { isActive: expandedSnapshot === null },
187
186
  );
188
187
 
189
- const { lastItem, prevItems, isTruncated } = useMemo(() => {
188
+ const { lastItem, prevItems } = useMemo(() => {
190
189
  const filtered = rawSegments.filter((s) => !s.temp && !s.isSpinny);
191
190
  const items = groupSegments(filtered);
192
- const truncated = items.length > 100;
193
- const visible = truncated ? items.slice(-100) : items;
194
191
  return {
195
- lastItem: visible.at(-1) ?? null,
196
- prevItems: visible.slice(0, -1),
197
- isTruncated: truncated,
192
+ lastItem: items.at(-1) ?? null,
193
+ prevItems: items.slice(0, -1),
198
194
  };
199
195
  }, [rawSegments]);
200
196
 
@@ -268,12 +264,6 @@ export function App({ timing }: { timing: Timing }) {
268
264
  <>
269
265
  <View flexDirection="column" gap={1} paddingBottom={1} width="95%">
270
266
  <Header />
271
- {isTruncated && (
272
- <Text dimColor>
273
- — Output limited to the last 100 items for performance. Use /new to start a fresh
274
- conversation. —
275
- </Text>
276
- )}
277
267
  <PendingToolCallIdContext.Provider value={pendingToolCallId}>
278
268
  {deferredPrevItems.map((item) =>
279
269
  item.kind === "segment" ? (
@@ -347,11 +337,6 @@ export async function runRepl({ apiClient, authManager, agentArgs }: ReplConfig)
347
337
  const showCursor = () => write(ANSI.SHOW_CURSOR);
348
338
 
349
339
  hideCursor();
350
- const cleanupSuspend = setupSuspendHandler({
351
- onAfterResume: () => {
352
- // Ink will re-render automatically when stdin resumes
353
- },
354
- });
355
340
  const app = render(
356
341
  <ThemeProvider>
357
342
  <ApiClientProvider client={apiClient} authManager={authManager}>
@@ -376,6 +361,10 @@ export async function runRepl({ apiClient, authManager, agentArgs }: ReplConfig)
376
361
  </ThemeProvider>,
377
362
  {
378
363
  exitOnCtrlC: false,
364
+ suspendable: true,
365
+ onSuspended: () => {
366
+ process.stderr.write("\nAbacus.AI has been suspended. Run `fg` to bring Abacus back.\n");
367
+ },
379
368
  },
380
369
  );
381
370
 
@@ -396,6 +385,5 @@ export async function runRepl({ apiClient, authManager, agentArgs }: ReplConfig)
396
385
 
397
386
  await app.waitUntilExit();
398
387
  process.off("exit", handleProcessExit);
399
- cleanupSuspend();
400
388
  cleanup();
401
389
  }
@@ -142,7 +142,23 @@ function segmentsReducer(prev: HistorySegment[], action: SegmentAction): History
142
142
 
143
143
  case "TEXT_DELTA": {
144
144
  const { segId, content } = action;
145
- const withoutSpinny = prev.filter((s) => !s.isSpinny);
145
+ const base = prev.map((s) =>
146
+ s.type === "code_llm_tool_call" && s.status === "transient" && !s.toolUseResult
147
+ ? {
148
+ ...s,
149
+ status: "completed" as const,
150
+ parsedResult: {
151
+ phase: "error" as const,
152
+ toolType: Array.isArray(s.toolUseRequest)
153
+ ? (s.toolUseRequest[0]?.name ?? "")
154
+ : (s.toolUseRequest?.name ?? ""),
155
+ data: {},
156
+ summary: "",
157
+ },
158
+ }
159
+ : s,
160
+ );
161
+ const withoutSpinny = base.filter((s) => !s.isSpinny);
146
162
  const existing = withoutSpinny.find((s) => s.id === segId);
147
163
  if (existing) {
148
164
  return withoutSpinny.map((s) =>
@@ -389,8 +405,10 @@ export const AgentProvider = memo(({ children, apiClient, agentArgs }: AgentProv
389
405
  return new CallbackPermissionProvider(
390
406
  async (req: PermissionRequest): Promise<PermissionDecision> => {
391
407
  const conn = connectionRef.current;
408
+ let permissionReq: PermissionRequest = req;
409
+ let editorDiffFilePath: string | null = null;
392
410
 
393
- // For edit tool with VS Code connection: show diff in VS Code, show options in CLI
411
+ // For edit tool with VS Code connection: show diff in VS Code
394
412
  if (conn && pushServerRef.current && req.type === "edit_file") {
395
413
  const toolId = req.tool.id;
396
414
  const displayData = displayDataMapRef.current.get(toolId);
@@ -408,56 +426,8 @@ export const AgentProvider = memo(({ children, apiClient, agentArgs }: AgentProv
408
426
  toolId,
409
427
  });
410
428
  if (shown) {
411
- const pushServer = pushServerRef.current;
412
- const editReq: PermissionRequest = {
413
- ...req,
414
- diffShownInEditor: true,
415
- };
416
-
417
- const cliPromise = new Promise<PermissionDecision>((resolve) => {
418
- setPendingToolPermission({
419
- request: editReq,
420
- resolve: (decision) => {
421
- setPendingToolPermission(null);
422
- const isAccept =
423
- decision === "accept" ||
424
- decision === "allowAlways" ||
425
- decision === "background" ||
426
- (typeof decision === "object" && decision.type === "accept_with_message") ||
427
- (typeof decision === "object" && decision.type === "question_answers");
428
- setAgentStatus(isAccept ? AgentStatus.ExecutingTool : AgentStatus.Submitted);
429
- resolve(decision);
430
- },
431
- });
432
- setAgentStatus(AgentStatus.WaitingForToolPermission);
433
- void notifyActionRequired("Abacus.AI is waiting for your input");
434
- });
435
-
436
- const pushPromise = pushServer.waitForDecision(toolId).then((d) => {
437
- setPendingToolPermission(null);
438
- return (d === "allowAlways" ? "allowAlways" : d) as PermissionDecision;
439
- });
440
-
441
- const decision = await Promise.race([cliPromise, pushPromise]);
442
-
443
- const simpleDecision =
444
- decision === "accept" || decision === "reject" || decision === "allowAlways"
445
- ? decision
446
- : decision === "background"
447
- ? "accept"
448
- : typeof decision === "object" && decision.type === "accept_with_message"
449
- ? "accept"
450
- : typeof decision === "object" && decision.type === "reject_with_message"
451
- ? "reject"
452
- : "accept";
453
- conn
454
- .notifyDiffDecision({
455
- targetFile: filePath,
456
- decision: simpleDecision === "allowAlways" ? "accept" : simpleDecision,
457
- })
458
- .catch(() => {});
459
-
460
- return decision;
429
+ permissionReq = { ...req, diffShownInEditor: true };
430
+ editorDiffFilePath = filePath;
461
431
  }
462
432
  }
463
433
  }
@@ -476,17 +446,14 @@ export const AgentProvider = memo(({ children, apiClient, agentArgs }: AgentProv
476
446
  }
477
447
 
478
448
  // Fallback: show CLI permission UI
479
- return new Promise((resolve) => {
449
+ const decision = await new Promise<PermissionDecision>((resolve) => {
480
450
  setPendingToolPermission({
481
- request: req,
451
+ request: permissionReq,
482
452
  resolve: (decision) => {
483
453
  setPendingToolPermission(null);
484
- // Immediately reflect the decision in the status indicator so the running
485
- // spinner appears without waiting for the event buffer to drain
486
454
  const isAccept =
487
455
  decision === "accept" ||
488
456
  decision === "allowAlways" ||
489
- decision === "background" ||
490
457
  (typeof decision === "object" && decision.type === "accept_with_message") ||
491
458
  (typeof decision === "object" && decision.type === "question_answers");
492
459
  setAgentStatus(isAccept ? AgentStatus.ExecutingTool : AgentStatus.Submitted);
@@ -496,6 +463,20 @@ export const AgentProvider = memo(({ children, apiClient, agentArgs }: AgentProv
496
463
  setAgentStatus(AgentStatus.WaitingForToolPermission);
497
464
  void notifyActionRequired("Abacus.AI is waiting for your input");
498
465
  });
466
+
467
+ if (editorDiffFilePath && conn) {
468
+ const isReject =
469
+ decision === "reject" ||
470
+ (typeof decision === "object" && decision.type === "reject_with_message");
471
+ conn
472
+ .notifyDiffDecision({
473
+ targetFile: editorDiffFilePath,
474
+ decision: isReject ? "reject" : "accept",
475
+ })
476
+ .catch(() => {});
477
+ }
478
+
479
+ return decision;
499
480
  },
500
481
  );
501
482
  }, [notifyActionRequired]);
@@ -834,9 +815,16 @@ export const AgentProvider = memo(({ children, apiClient, agentArgs }: AgentProv
834
815
  }
835
816
 
836
817
  case "notification": {
837
- if (event.severity === "error" || event.severity === "warning") {
838
- addNotification(event.message, event.severity, false);
839
- }
818
+ const notifSeg: HistorySegment = {
819
+ id: crypto.randomUUID(),
820
+ status: "completed",
821
+ source: "bot",
822
+ type: "code_llm_notification",
823
+ severity: event.severity,
824
+ message: event.message,
825
+ ...(event.actions !== undefined && { actions: event.actions }),
826
+ };
827
+ dispatchSegments({ type: "ADD_NOTIFICATION", seg: notifSeg });
840
828
  break;
841
829
  }
842
830
 
@@ -122,6 +122,8 @@ export interface ToolHeaderProps {
122
122
  isAwaitingPermission?: boolean;
123
123
  /** Tool call ID — used to check against PendingToolCallIdContext for auto awaiting-permission detection */
124
124
  toolCallId?: string;
125
+ /** When set, syntax-highlight params using this language via cli-highlight */
126
+ paramsLanguage?: string;
125
127
  }
126
128
 
127
129
  export function ToolHeader({
@@ -131,9 +133,9 @@ export function ToolHeader({
131
133
  showSpinner,
132
134
  isAwaitingPermission,
133
135
  toolCallId,
136
+ paramsLanguage,
134
137
  }: ToolHeaderProps) {
135
138
  const color = getStatusColor(status);
136
- const isPending = status === "pending" || status === "executing";
137
139
 
138
140
  // Auto-detect awaiting permission via context:
139
141
  // When a tool call ID matches the pending permission, or when showSpinner is true
@@ -154,12 +156,28 @@ export function ToolHeader({
154
156
  indicator = <Text color={color}>⏺</Text>;
155
157
  }
156
158
 
159
+ let paramsContent: React.ReactNode = null;
160
+ if (params) {
161
+ let highlighted = params;
162
+ if (paramsLanguage && supportsLanguage(paramsLanguage)) {
163
+ try {
164
+ highlighted = highlight(params, { language: paramsLanguage });
165
+ } catch {
166
+ // fallback to plain text
167
+ }
168
+ }
169
+ paramsContent = paramsLanguage ? (
170
+ <Text>({highlighted})</Text>
171
+ ) : (
172
+ <Text dimColor>({params})</Text>
173
+ );
174
+ }
175
+
157
176
  return (
158
177
  <View flexDirection="row" alignItems="center" gap={1}>
159
178
  {indicator}
160
179
  <Text color="cyan">{name}</Text>
161
- {params && <Text dimColor>({params})</Text>}
162
- {isPending && <Text dimColor>...</Text>}
180
+ {paramsContent}
163
181
  </View>
164
182
  );
165
183
  }
package/tsconfig.json CHANGED
@@ -7,5 +7,10 @@
7
7
  {
8
8
  "path": "./tsconfig.test.json"
9
9
  }
10
- ]
10
+ ],
11
+ "compilerOptions": {
12
+ "paths": {
13
+ "@/package.json": ["./package.json"]
14
+ }
15
+ }
11
16
  }
@@ -1,58 +0,0 @@
1
- /**
2
- * Platform-aware SIGTSTP/SIGCONT handling for Ctrl+Z suspend support.
3
- *
4
- * In raw mode, Ctrl+Z sends a `\x1a` byte rather than SIGTSTP.
5
- * The interrupt manager detects this byte and calls `process.kill(pid, "SIGTSTP")`.
6
- * This module handles the signal-level suspend/resume lifecycle.
7
- */
8
-
9
- export function setupSuspendHandler(options: {
10
- onBeforeSuspend?: () => void;
11
- onAfterResume?: () => void;
12
- }): () => void {
13
- if (process.platform === "win32") {
14
- // SIGTSTP doesn't exist on Windows
15
- return () => {};
16
- }
17
-
18
- const handleSIGTSTP = () => {
19
- options.onBeforeSuspend?.();
20
-
21
- // Restore terminal defaults before suspending
22
- if (process.stdout.isTTY) {
23
- process.stdout.write("\x1b[?25h"); // show cursor
24
- }
25
- if (process.stdin.isTTY && process.stdin.isRaw) {
26
- process.stdin.setRawMode(false);
27
- }
28
-
29
- process.stderr.write("\nAbacus.AI has been suspended. Run `fg` to bring Abacus back.\n");
30
-
31
- // Re-raise SIGTSTP with default handler to actually suspend
32
- process.removeListener("SIGTSTP", handleSIGTSTP);
33
- process.kill(process.pid, "SIGTSTP");
34
- };
35
-
36
- const handleSIGCONT = () => {
37
- // Re-enter raw mode and hide cursor after resume
38
- if (process.stdin.isTTY) {
39
- process.stdin.setRawMode(true);
40
- }
41
- if (process.stdout.isTTY) {
42
- process.stdout.write("\x1b[?25l"); // hide cursor
43
- }
44
-
45
- // Re-register SIGTSTP handler (was removed before suspend)
46
- process.on("SIGTSTP", handleSIGTSTP);
47
-
48
- options.onAfterResume?.();
49
- };
50
-
51
- process.on("SIGTSTP", handleSIGTSTP);
52
- process.on("SIGCONT", handleSIGCONT);
53
-
54
- return () => {
55
- process.removeListener("SIGTSTP", handleSIGTSTP);
56
- process.removeListener("SIGCONT", handleSIGCONT);
57
- };
58
- }