@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
|
@@ -165,7 +165,7 @@ export const Input = memo(
|
|
|
165
165
|
[buffer, handleSubmit, onKeyDown, disabled],
|
|
166
166
|
);
|
|
167
167
|
|
|
168
|
-
useInput(handleKeypress, { isActive:
|
|
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,
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
}
|
package/src/entrypoints/repl.tsx
CHANGED
|
@@ -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,
|
|
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
|
|
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:
|
|
196
|
-
prevItems:
|
|
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
|
}
|
package/src/providers/agent.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
412
|
-
|
|
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
|
-
|
|
449
|
+
const decision = await new Promise<PermissionDecision>((resolve) => {
|
|
480
450
|
setPendingToolPermission({
|
|
481
|
-
request:
|
|
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
|
-
|
|
838
|
-
|
|
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
|
-
{
|
|
162
|
-
{isPending && <Text dimColor>...</Text>}
|
|
180
|
+
{paramsContent}
|
|
163
181
|
</View>
|
|
164
182
|
);
|
|
165
183
|
}
|
package/tsconfig.json
CHANGED
package/src/terminal/suspend.ts
DELETED
|
@@ -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
|
-
}
|