@astralyn/pi 0.84.5 → 0.84.6
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/CHANGELOG.md +27 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +4 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/index.d.ts +1 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +1 -0
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +6 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +11 -0
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/tools/bash.d.ts +5 -0
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +1 -1
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/extensions/background/index.d.ts +95 -20
- package/dist/extensions/background/index.d.ts.map +1 -1
- package/dist/extensions/background/index.js +470 -136
- package/dist/extensions/background/index.js.map +1 -1
- package/dist/extensions/background/manager.d.ts +14 -0
- package/dist/extensions/background/manager.d.ts.map +1 -1
- package/dist/extensions/background/manager.js +125 -24
- package/dist/extensions/background/manager.js.map +1 -1
- package/dist/extensions/background/registry.d.ts +82 -0
- package/dist/extensions/background/registry.d.ts.map +1 -1
- package/dist/extensions/background/registry.js +237 -6
- package/dist/extensions/background/registry.js.map +1 -1
- package/dist/extensions/background/render.d.ts +35 -11
- package/dist/extensions/background/render.d.ts.map +1 -1
- package/dist/extensions/background/render.js +172 -30
- package/dist/extensions/background/render.js.map +1 -1
- package/dist/extensions/subagent/activity.d.ts +3 -5
- package/dist/extensions/subagent/activity.d.ts.map +1 -1
- package/dist/extensions/subagent/activity.js +8 -89
- package/dist/extensions/subagent/activity.js.map +1 -1
- package/dist/extensions/subagent/agents-command.d.ts.map +1 -1
- package/dist/extensions/subagent/agents-command.js +40 -94
- package/dist/extensions/subagent/agents-command.js.map +1 -1
- package/dist/extensions/subagent/agents.d.ts +2 -0
- package/dist/extensions/subagent/agents.d.ts.map +1 -1
- package/dist/extensions/subagent/agents.js +7 -1
- package/dist/extensions/subagent/agents.js.map +1 -1
- package/dist/extensions/subagent/budget.d.ts +15 -0
- package/dist/extensions/subagent/budget.d.ts.map +1 -0
- package/dist/extensions/subagent/budget.js +96 -0
- package/dist/extensions/subagent/budget.js.map +1 -0
- package/dist/extensions/subagent/cancellation.d.ts +29 -0
- package/dist/extensions/subagent/cancellation.d.ts.map +1 -0
- package/dist/extensions/subagent/cancellation.js +73 -0
- package/dist/extensions/subagent/cancellation.js.map +1 -0
- package/dist/extensions/subagent/index.d.ts.map +1 -1
- package/dist/extensions/subagent/index.js +5 -22
- package/dist/extensions/subagent/index.js.map +1 -1
- package/dist/extensions/subagent/model-selection.d.ts +16 -0
- package/dist/extensions/subagent/model-selection.d.ts.map +1 -0
- package/dist/extensions/subagent/model-selection.js +51 -0
- package/dist/extensions/subagent/model-selection.js.map +1 -0
- package/dist/extensions/subagent/render.d.ts +6 -1
- package/dist/extensions/subagent/render.d.ts.map +1 -1
- package/dist/extensions/subagent/render.js +23 -12
- package/dist/extensions/subagent/render.js.map +1 -1
- package/dist/extensions/subagent/resolve.d.ts.map +1 -1
- package/dist/extensions/subagent/resolve.js +2 -21
- package/dist/extensions/subagent/resolve.js.map +1 -1
- package/dist/extensions/subagent/runner.d.ts +4 -5
- package/dist/extensions/subagent/runner.d.ts.map +1 -1
- package/dist/extensions/subagent/runner.js +71 -264
- package/dist/extensions/subagent/runner.js.map +1 -1
- package/dist/extensions/subagent/sdk-runner.d.ts +13 -11
- package/dist/extensions/subagent/sdk-runner.d.ts.map +1 -1
- package/dist/extensions/subagent/sdk-runner.js +167 -148
- package/dist/extensions/subagent/sdk-runner.js.map +1 -1
- package/dist/extensions/subagent/state.d.ts +89 -0
- package/dist/extensions/subagent/state.d.ts.map +1 -0
- package/dist/extensions/subagent/state.js +294 -0
- package/dist/extensions/subagent/state.js.map +1 -0
- package/dist/extensions/subagent/text.d.ts +4 -0
- package/dist/extensions/subagent/text.d.ts.map +1 -1
- package/dist/extensions/subagent/text.js +75 -0
- package/dist/extensions/subagent/text.js.map +1 -1
- package/dist/extensions/subagent/types.d.ts +3 -8
- package/dist/extensions/subagent/types.d.ts.map +1 -1
- package/dist/extensions/subagent/types.js.map +1 -1
- package/dist/extensions/subagent/ui/choices.d.ts +36 -0
- package/dist/extensions/subagent/ui/choices.d.ts.map +1 -0
- package/dist/extensions/subagent/ui/choices.js +54 -0
- package/dist/extensions/subagent/ui/choices.js.map +1 -0
- package/dist/extensions/subagent/ui/model-list.d.ts.map +1 -1
- package/dist/extensions/subagent/ui/model-list.js +10 -18
- package/dist/extensions/subagent/ui/model-list.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/docs/bundled/README.md +1 -1
- package/docs/bundled/extensions/background.md +55 -20
- package/docs/extensions.md +13 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/extensions/subagent/retry.d.ts +0 -17
- package/dist/extensions/subagent/retry.d.ts.map +0 -1
- package/dist/extensions/subagent/retry.js +0 -41
- package/dist/extensions/subagent/retry.js.map +0 -1
|
@@ -1,41 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* background — run bash commands in the background.
|
|
3
3
|
*
|
|
4
|
-
* Tools:
|
|
5
|
-
* a <background-task> notification
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* task
|
|
4
|
+
* Tools: a single `bg` tool with five actions. create starts a task and
|
|
5
|
+
* returns immediately; the result arrives as a <background-task> notification
|
|
6
|
+
* (queued behind the current run while streaming, waking the agent when
|
|
7
|
+
* idle). read returns a bounded slice of a task's output file; wait blocks
|
|
8
|
+
* (bounded) for a task's completion and delivers it inline — the followUp
|
|
9
|
+
* notification is suppressed so the completion is delivered exactly once;
|
|
10
|
+
* kill stops a single task; list enumerates known tasks. /bg opens the
|
|
11
|
+
* interactive task manager. Running counts surface in the footer via
|
|
12
|
+
* ctx.ui.setStatus.
|
|
9
13
|
*/
|
|
10
14
|
import { Type } from "typebox";
|
|
11
15
|
import { getAgentDir } from "../../config.js";
|
|
12
16
|
import { SettingsManager } from "../../core/settings-manager.js";
|
|
13
|
-
import { createLocalBashOperations, resolveSpawnContext } from "../../core/tools/bash.js";
|
|
17
|
+
import { createLocalBashOperations, MAX_TIMEOUT_SECONDS, resolveSpawnContext, } from "../../core/tools/bash.js";
|
|
14
18
|
import { DEFAULT_MAX_BYTES, formatSize } from "../../core/tools/truncate.js";
|
|
15
19
|
import { sanitizeBinaryOutput } from "../../utils/shell.js";
|
|
16
20
|
import { BackgroundTasksMenu } from "./manager.js";
|
|
17
|
-
import { BackgroundTaskRegistry, formatDuration, readOutputSlice, } from "./registry.js";
|
|
18
|
-
import { commandLabel, renderBackgroundNotification,
|
|
21
|
+
import { BackgroundTaskRegistry, BG_WAIT_DEFAULT_MS, BG_WAIT_MAX_MS, BG_WAIT_MIN_MS, firstCommandLine, formatDuration, readOutputSince, readOutputSlice, } from "./registry.js";
|
|
22
|
+
import { commandLabel, renderBackgroundNotification, renderBgCall, renderBgResult, } from "./render.js";
|
|
19
23
|
export const BG_NOTIFICATION_TYPE = "background-task";
|
|
20
24
|
const BG_LOGS_DEFAULT_BYTES = 8 * 1024;
|
|
21
25
|
const BG_LOGS_MIN_BYTES = 256;
|
|
22
|
-
const
|
|
23
|
-
|
|
26
|
+
const BG_LIST_FINISHED_SHOWN = 5;
|
|
27
|
+
/** Bounded output delta returned by a successful wait. */
|
|
28
|
+
const BG_WAIT_DELTA_BYTES = 32 * 1024;
|
|
29
|
+
/** Tail peek included in a wait timeout result so progress stays visible. */
|
|
30
|
+
const BG_WAIT_PEEK_BYTES = 2 * 1024;
|
|
31
|
+
const bgSchema = Type.Object({
|
|
32
|
+
action: Type.Union([Type.Literal("create"), Type.Literal("read"), Type.Literal("wait"), Type.Literal("kill"), Type.Literal("list")], { description: "Which background-task operation to perform" }),
|
|
33
|
+
// — create —
|
|
34
|
+
command: Type.Optional(Type.String({ description: "Bash command to run in the background (create). Do not append '&'" })),
|
|
35
|
+
description: Type.Optional(Type.String({
|
|
36
|
+
description: "Short label for the task, shown in /bg and notifications, e.g. 'dev server' (create)",
|
|
37
|
+
})),
|
|
24
38
|
timeout: Type.Optional(Type.Number({
|
|
25
|
-
description: "
|
|
39
|
+
description: "Kill the task after this many seconds; on expiry it is reported as timeout (create)",
|
|
26
40
|
})),
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
41
|
+
// — read / wait / kill —
|
|
42
|
+
taskId: Type.Optional(Type.String({ description: "Task id; a unique prefix is accepted, e.g. 'bg-3f' or '3f' (read/wait/kill)" })),
|
|
43
|
+
// — read —
|
|
30
44
|
mode: Type.Optional(Type.Union([Type.Literal("tail"), Type.Literal("head")], {
|
|
31
|
-
description: "Read from the end (default) or the start of the output",
|
|
45
|
+
description: "Read from the end (default) or the start of the output (read)",
|
|
32
46
|
})),
|
|
33
47
|
bytes: Type.Optional(Type.Number({
|
|
34
|
-
description: `Max bytes to return (default ${BG_LOGS_DEFAULT_BYTES}, max ${DEFAULT_MAX_BYTES})`,
|
|
48
|
+
description: `Max bytes to return (read, default ${formatSize(BG_LOGS_DEFAULT_BYTES)}, max ${formatSize(DEFAULT_MAX_BYTES)})`,
|
|
49
|
+
})),
|
|
50
|
+
// — wait —
|
|
51
|
+
waitMs: Type.Optional(Type.Number({
|
|
52
|
+
description: `Max time to wait in ms, default ${BG_WAIT_DEFAULT_MS}, max ${BG_WAIT_MAX_MS} (wait)`,
|
|
53
|
+
})),
|
|
54
|
+
sinceBytes: Type.Optional(Type.Number({
|
|
55
|
+
description: "Only return output written after this byte offset — take it from a previous read/wait result (wait)",
|
|
35
56
|
})),
|
|
36
|
-
});
|
|
37
|
-
const bgKillSchema = Type.Object({
|
|
38
|
-
taskId: Type.String({ description: "Task id; a unique prefix is accepted" }),
|
|
39
57
|
});
|
|
40
58
|
export function escapeXml(text) {
|
|
41
59
|
return text
|
|
@@ -45,20 +63,47 @@ export function escapeXml(text) {
|
|
|
45
63
|
.replaceAll('"', """)
|
|
46
64
|
.replaceAll("'", "'");
|
|
47
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Strip characters XML 1.0 forbids: C0 controls (except \t \n \r), lone
|
|
68
|
+
* surrogates (U+D800–U+DFFF), and the non-characters U+FFFE/U+FFFF.
|
|
69
|
+
* sanitizeBinaryOutput does not remove the latter two classes, so this filter
|
|
70
|
+
* is the authority for every text field in the notification XML.
|
|
71
|
+
*/
|
|
72
|
+
export function filterXmlCharacters(text) {
|
|
73
|
+
let result = "";
|
|
74
|
+
for (const char of text) {
|
|
75
|
+
const code = char.codePointAt(0) ?? 0;
|
|
76
|
+
if (code === 0x09 ||
|
|
77
|
+
code === 0x0a ||
|
|
78
|
+
code === 0x0d ||
|
|
79
|
+
(code >= 0x20 && code <= 0xd7ff) ||
|
|
80
|
+
(code >= 0xe000 && code <= 0xfffd) ||
|
|
81
|
+
code >= 0x10000) {
|
|
82
|
+
result += char;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
48
87
|
export function buildNotificationContent(notification) {
|
|
49
88
|
const task = notification.task;
|
|
50
89
|
const runtime = formatDuration((task.endedAt ?? task.startedAt) - task.startedAt);
|
|
51
90
|
const exitCode = task.exitCode === undefined || task.exitCode === null ? "" : ` exitCode="${task.exitCode}"`;
|
|
91
|
+
// filterXmlCharacters keeps \t \n \r and every other XML-legal codepoint,
|
|
92
|
+
// so each field below is valid XML 1.0 after escaping.
|
|
93
|
+
const xmlText = (text) => escapeXml(filterXmlCharacters(text));
|
|
52
94
|
const lines = [
|
|
53
95
|
`<background-task id="${task.id}" status="${task.status}"${exitCode} runtime="${runtime}">`,
|
|
54
|
-
`<command>${
|
|
55
|
-
`<output-file>${escapeXml(task.outputPath)}</output-file>`,
|
|
96
|
+
`<command>${xmlText(task.command)}</command>`,
|
|
56
97
|
];
|
|
98
|
+
if (task.description) {
|
|
99
|
+
lines.push(`<description>${xmlText(task.description)}</description>`);
|
|
100
|
+
}
|
|
101
|
+
lines.push(`<output-file>${xmlText(task.outputPath)}</output-file>`);
|
|
57
102
|
if (task.error) {
|
|
58
|
-
lines.push(`<error>${
|
|
103
|
+
lines.push(`<error>${xmlText(task.error)}</error>`);
|
|
59
104
|
}
|
|
60
105
|
if (notification.tailError) {
|
|
61
|
-
lines.push(`<output-tail unavailable="${
|
|
106
|
+
lines.push(`<output-tail unavailable="${xmlText(notification.tailError)}"/>`);
|
|
62
107
|
}
|
|
63
108
|
else {
|
|
64
109
|
const attrs = [
|
|
@@ -69,7 +114,7 @@ export function buildNotificationContent(notification) {
|
|
|
69
114
|
]
|
|
70
115
|
.filter(Boolean)
|
|
71
116
|
.join(" ");
|
|
72
|
-
const tail = notification.tailText.length > 0 ?
|
|
117
|
+
const tail = notification.tailText.length > 0 ? xmlText(notification.tailText) : "(no output)";
|
|
73
118
|
lines.push(`<output-tail ${attrs}>`, tail.trimEnd(), "</output-tail>");
|
|
74
119
|
}
|
|
75
120
|
lines.push("</background-task>");
|
|
@@ -78,31 +123,92 @@ export function buildNotificationContent(notification) {
|
|
|
78
123
|
export function formatStatusline(counts) {
|
|
79
124
|
if (counts.total === 0)
|
|
80
125
|
return undefined;
|
|
126
|
+
// Stalled tasks are running tasks: report them separately so the counts add up.
|
|
127
|
+
const waiting = counts.stalled ?? 0;
|
|
128
|
+
const running = counts.running - waiting;
|
|
81
129
|
const ended = counts.total - counts.running;
|
|
82
130
|
const parts = [];
|
|
83
|
-
if (
|
|
84
|
-
parts.push(`${
|
|
131
|
+
if (running > 0)
|
|
132
|
+
parts.push(`${running} running`);
|
|
133
|
+
if (waiting > 0)
|
|
134
|
+
parts.push(`${waiting} waiting for input`);
|
|
85
135
|
if (ended > 0)
|
|
86
136
|
parts.push(`${ended} done`);
|
|
87
137
|
return `bg ${parts.join(" · ")}`;
|
|
88
138
|
}
|
|
139
|
+
export function buildStallContent(notification) {
|
|
140
|
+
const task = notification.task;
|
|
141
|
+
const runtime = formatDuration((task.endedAt ?? Date.now()) - task.startedAt);
|
|
142
|
+
const xmlText = (text) => escapeXml(filterXmlCharacters(text));
|
|
143
|
+
const lines = [
|
|
144
|
+
`<background-task id="${task.id}" status="running" waiting-for-input="true" runtime="${runtime}">`,
|
|
145
|
+
`<command>${xmlText(task.command)}</command>`,
|
|
146
|
+
];
|
|
147
|
+
if (task.description) {
|
|
148
|
+
lines.push(`<description>${xmlText(task.description)}</description>`);
|
|
149
|
+
}
|
|
150
|
+
lines.push(`<output-file>${xmlText(task.outputPath)}</output-file>`);
|
|
151
|
+
if (notification.tailError) {
|
|
152
|
+
lines.push(`<output-tail unavailable="${xmlText(notification.tailError)}"/>`);
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const attrs = [
|
|
156
|
+
`bytes="${notification.tailBytes}"`,
|
|
157
|
+
`totalBytes="${notification.totalBytes}"`,
|
|
158
|
+
notification.tailTruncated ? 'truncated="true"' : "",
|
|
159
|
+
notification.tailStartsMidLine ? 'startsMidLine="true"' : "",
|
|
160
|
+
]
|
|
161
|
+
.filter(Boolean)
|
|
162
|
+
.join(" ");
|
|
163
|
+
const tail = notification.tailText.length > 0 ? xmlText(notification.tailText) : "(no output)";
|
|
164
|
+
lines.push(`<output-tail ${attrs}>`, tail.trimEnd(), "</output-tail>");
|
|
165
|
+
}
|
|
166
|
+
lines.push("<advice>The command appears blocked waiting for interactive input. Kill it (bg action kill) and " +
|
|
167
|
+
"re-run with piped input (e.g. echo y | command) or a non-interactive flag if one exists (e.g. --yes). " +
|
|
168
|
+
"The task keeps running until then.</advice>");
|
|
169
|
+
lines.push("</background-task>");
|
|
170
|
+
return lines.join("\n");
|
|
171
|
+
}
|
|
172
|
+
function toStallDetails(notification) {
|
|
173
|
+
const task = notification.task;
|
|
174
|
+
return {
|
|
175
|
+
taskId: task.id,
|
|
176
|
+
command: task.command,
|
|
177
|
+
description: task.description,
|
|
178
|
+
status: "running",
|
|
179
|
+
stalled: true,
|
|
180
|
+
exitCode: undefined,
|
|
181
|
+
runtimeMs: Date.now() - task.startedAt,
|
|
182
|
+
outputPath: task.outputPath,
|
|
183
|
+
totalBytes: notification.totalBytes,
|
|
184
|
+
tailText: notification.tailText,
|
|
185
|
+
tailTruncated: notification.tailTruncated,
|
|
186
|
+
tailError: notification.tailError,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
89
189
|
function describeResolveFailure(input, result) {
|
|
90
190
|
if (result.reason === "ambiguous") {
|
|
91
|
-
const lines = result.candidates.map((task) => ` ${task.id} (${task.status}) ${commandLabel(task
|
|
191
|
+
const lines = result.candidates.map((task) => ` ${task.id} (${task.status}) ${commandLabel(taskLabel(task), 60)}`);
|
|
92
192
|
return `Task id "${input}" is ambiguous. Candidates:\n${lines.join("\n")}`;
|
|
93
193
|
}
|
|
94
|
-
return `No background task matches "${input}". Check the id returned by
|
|
194
|
+
return `No background task matches "${input}". Check the id returned by bg (action create), or run action list.`;
|
|
95
195
|
}
|
|
196
|
+
/** One row of a bounded task listing: the model-facing list and /bg summaries. */
|
|
96
197
|
function describeTaskLine(task, now) {
|
|
97
198
|
const duration = formatDuration((task.endedAt ?? now) - task.startedAt);
|
|
98
199
|
const exit = task.exitCode !== undefined && task.exitCode !== null ? ` exit ${task.exitCode}` : "";
|
|
99
|
-
return ` ${task.id} ${task.status}${exit} ${duration} ${commandLabel(task
|
|
200
|
+
return ` ${task.id} ${task.status}${exit} ${duration} ${commandLabel(taskLabel(task), 60)}`;
|
|
201
|
+
}
|
|
202
|
+
/** Label for listings: the model-provided description over the first command line. */
|
|
203
|
+
function taskLabel(task) {
|
|
204
|
+
return task.description ? `${task.description} — ${firstCommandLine(task.command)}` : firstCommandLine(task.command);
|
|
100
205
|
}
|
|
101
206
|
function toNotificationDetails(notification) {
|
|
102
207
|
const task = notification.task;
|
|
103
208
|
return {
|
|
104
209
|
taskId: task.id,
|
|
105
210
|
command: task.command,
|
|
211
|
+
description: task.description,
|
|
106
212
|
status: task.status,
|
|
107
213
|
exitCode: task.exitCode,
|
|
108
214
|
runtimeMs: (task.endedAt ?? task.startedAt) - task.startedAt,
|
|
@@ -114,12 +220,52 @@ function toNotificationDetails(notification) {
|
|
|
114
220
|
tailError: notification.tailError,
|
|
115
221
|
};
|
|
116
222
|
}
|
|
117
|
-
/**
|
|
223
|
+
/**
|
|
224
|
+
* Wrap operations so every executed command carries the prefix, mirroring the
|
|
225
|
+
* built-in bash tool. The prefix never reaches task labels or notifications:
|
|
226
|
+
* `BgTask.command` stays the user's original input.
|
|
227
|
+
*/
|
|
228
|
+
export function prependCommandPrefix(operations, prefix) {
|
|
229
|
+
if (!prefix)
|
|
230
|
+
return operations;
|
|
231
|
+
return {
|
|
232
|
+
exec: (command, cwd, options) => operations.exec(`${prefix}\n${command}`, cwd, options),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** Shell configuration for one bg session: the session's settings win, disk settings are the fallback. */
|
|
236
|
+
export function resolveSessionShell(ctx, readDiskSettings) {
|
|
237
|
+
return ctx.getShellSettings?.() ?? readDiskSettings?.() ?? {};
|
|
238
|
+
}
|
|
239
|
+
/** Local shell operations honoring the session's shell settings (shellPath, commandPrefix). */
|
|
118
240
|
function createSessionBashOperations(ctx) {
|
|
119
|
-
const
|
|
120
|
-
|
|
241
|
+
const shell = resolveSessionShell(ctx, () => {
|
|
242
|
+
// Hosts without getShellSettings (older SDK hosts): fall back to reading
|
|
243
|
+
// settings from disk, matching what the session was configured with.
|
|
244
|
+
const settings = SettingsManager.create(ctx.cwd, getAgentDir(), {
|
|
245
|
+
projectTrusted: ctx.isProjectTrusted(),
|
|
246
|
+
});
|
|
247
|
+
return { shellPath: settings.getShellPath(), commandPrefix: settings.getShellCommandPrefix() };
|
|
121
248
|
});
|
|
122
|
-
return createLocalBashOperations({ shellPath:
|
|
249
|
+
return prependCommandPrefix(createLocalBashOperations({ shellPath: shell.shellPath }), shell.commandPrefix);
|
|
250
|
+
}
|
|
251
|
+
/** Validate per-action required fields; every message names the next step. */
|
|
252
|
+
function validateAction(input) {
|
|
253
|
+
switch (input.action) {
|
|
254
|
+
case "create":
|
|
255
|
+
if (!input.command || input.command.trim().length === 0) {
|
|
256
|
+
throw new Error("action 'create' requires 'command' — pass the bash command to run in the background.");
|
|
257
|
+
}
|
|
258
|
+
break;
|
|
259
|
+
case "read":
|
|
260
|
+
case "wait":
|
|
261
|
+
case "kill":
|
|
262
|
+
if (!input.taskId || input.taskId.trim().length === 0) {
|
|
263
|
+
throw new Error(`action '${input.action}' requires 'taskId' — use the id returned by action create, or action list to find it.`);
|
|
264
|
+
}
|
|
265
|
+
break;
|
|
266
|
+
case "list":
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
123
269
|
}
|
|
124
270
|
/** Factory with injectable seams for tests; the default export uses production wiring. */
|
|
125
271
|
export function createBackgroundExtension(overrides) {
|
|
@@ -137,6 +283,13 @@ export function createBackgroundExtension(overrides) {
|
|
|
137
283
|
throw new Error(describeResolveFailure(taskId, resolved));
|
|
138
284
|
return resolved.task;
|
|
139
285
|
};
|
|
286
|
+
/** Read-only live peek for the pending wait renderer; never throws. */
|
|
287
|
+
const waitLiveProbe = (taskId) => {
|
|
288
|
+
if (!registry)
|
|
289
|
+
return undefined;
|
|
290
|
+
const resolved = registry.resolveTask(taskId);
|
|
291
|
+
return resolved.ok ? { status: resolved.task.status, outputBytes: resolved.task.outputBytes } : undefined;
|
|
292
|
+
};
|
|
140
293
|
const updateStatus = () => {
|
|
141
294
|
if (!registry || registry.isShuttingDown)
|
|
142
295
|
return;
|
|
@@ -151,6 +304,7 @@ export function createBackgroundExtension(overrides) {
|
|
|
151
304
|
operations: overrides?.operations ?? createSessionBashOperations(ctx),
|
|
152
305
|
outputDir: overrides?.outputDir,
|
|
153
306
|
maxOutputBytes: overrides?.maxOutputBytes,
|
|
307
|
+
stall: overrides?.stall,
|
|
154
308
|
onNotify: (notification) => {
|
|
155
309
|
pi.sendMessage({
|
|
156
310
|
customType: BG_NOTIFICATION_TYPE,
|
|
@@ -159,6 +313,17 @@ export function createBackgroundExtension(overrides) {
|
|
|
159
313
|
details: toNotificationDetails(notification),
|
|
160
314
|
}, { deliverAs: "followUp", triggerTurn: true });
|
|
161
315
|
},
|
|
316
|
+
onStall: (notification) => {
|
|
317
|
+
// Informational one-shot signal: not a terminal state, so it bypasses
|
|
318
|
+
// the claim protocol (a waiting model needs to know it is blocked on
|
|
319
|
+
// input, not merely slow).
|
|
320
|
+
pi.sendMessage({
|
|
321
|
+
customType: BG_NOTIFICATION_TYPE,
|
|
322
|
+
content: buildStallContent(notification),
|
|
323
|
+
display: true,
|
|
324
|
+
details: toStallDetails(notification),
|
|
325
|
+
}, { deliverAs: "followUp", triggerTurn: true });
|
|
326
|
+
},
|
|
162
327
|
onChange: updateStatus,
|
|
163
328
|
});
|
|
164
329
|
updateStatus();
|
|
@@ -170,125 +335,294 @@ export function createBackgroundExtension(overrides) {
|
|
|
170
335
|
registry = undefined;
|
|
171
336
|
await active?.shutdown();
|
|
172
337
|
});
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
338
|
+
const runCreate = async (params, ctx) => {
|
|
339
|
+
if (params.timeout !== undefined && (!Number.isFinite(params.timeout) || params.timeout <= 0)) {
|
|
340
|
+
throw new Error("Invalid timeout: must be a positive number of seconds.");
|
|
341
|
+
}
|
|
342
|
+
if (params.timeout !== undefined && params.timeout > MAX_TIMEOUT_SECONDS) {
|
|
343
|
+
throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
|
|
344
|
+
}
|
|
345
|
+
// Same PI_* session variables as the built-in bash tool, snapshotted at start.
|
|
346
|
+
const spawnContext = resolveSpawnContext(params.command, ctx.cwd, undefined, true, ctx);
|
|
347
|
+
// Deliberately ignore the turn signal: aborting the current turn must
|
|
348
|
+
// not kill the background task (that is what action kill is for).
|
|
349
|
+
const task = requireRegistry().startTask({
|
|
350
|
+
command: params.command,
|
|
351
|
+
cwd: ctx.cwd,
|
|
352
|
+
description: params.description,
|
|
353
|
+
timeoutSeconds: params.timeout,
|
|
354
|
+
env: spawnContext.env,
|
|
355
|
+
});
|
|
356
|
+
const label = task.description ? ` (${task.description})` : "";
|
|
357
|
+
const text = [
|
|
358
|
+
`Started background task ${task.id}${label}.`,
|
|
359
|
+
`Output file: ${task.outputPath}`,
|
|
360
|
+
"",
|
|
361
|
+
"A <background-task> notification with the result and output tail arrives automatically when the task " +
|
|
362
|
+
"ends — do NOT poll (no sleep loops, no repeated reads). Use action read for an early peek, " +
|
|
363
|
+
"action wait when the next step depends on the result, action kill to stop it.",
|
|
364
|
+
].join("\n");
|
|
365
|
+
return {
|
|
366
|
+
content: [{ type: "text", text }],
|
|
367
|
+
details: {
|
|
368
|
+
action: "create",
|
|
369
|
+
taskId: task.id,
|
|
370
|
+
outputPath: task.outputPath,
|
|
194
371
|
command: params.command,
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
372
|
+
description: params.description,
|
|
373
|
+
},
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
const runRead = async (params) => {
|
|
377
|
+
const task = resolveTaskOrThrow(params.taskId);
|
|
378
|
+
const mode = params.mode ?? "tail";
|
|
379
|
+
const maxBytes = Math.min(DEFAULT_MAX_BYTES, Math.max(BG_LOGS_MIN_BYTES, Math.floor(params.bytes ?? BG_LOGS_DEFAULT_BYTES)));
|
|
380
|
+
let slice;
|
|
381
|
+
try {
|
|
382
|
+
slice = await readOutputSlice(task.outputPath, { mode, maxBytes });
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
386
|
+
throw new Error(`Could not read output for ${task.id} (${task.status}): ${message}\nOutput file: ${task.outputPath}`);
|
|
387
|
+
}
|
|
388
|
+
const text = sanitizeBinaryOutput(slice.text);
|
|
389
|
+
const running = task.status === "running" ? ["still running"] : [];
|
|
390
|
+
const noticeParts = slice.truncated
|
|
391
|
+
? [
|
|
392
|
+
`${mode} ${formatSize(slice.sliceBytes)} of ${formatSize(slice.totalBytes)}`,
|
|
393
|
+
`task ${task.id} ${task.status}`,
|
|
394
|
+
...running,
|
|
395
|
+
...(slice.startsMidLine ? ["first line may be partial"] : []),
|
|
396
|
+
`full output: ${task.outputPath}`,
|
|
397
|
+
]
|
|
398
|
+
: [
|
|
399
|
+
`task ${task.id} ${task.status}`,
|
|
400
|
+
...running,
|
|
401
|
+
`${formatSize(slice.totalBytes)} total`,
|
|
402
|
+
`output: ${task.outputPath}`,
|
|
403
|
+
];
|
|
404
|
+
const body = text.length > 0 ? text : "(no output yet)";
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: `[${noticeParts.join(" · ")}]\n${body}` }],
|
|
407
|
+
details: {
|
|
408
|
+
action: "read",
|
|
409
|
+
taskId: task.id,
|
|
410
|
+
mode,
|
|
411
|
+
sliceBytes: slice.sliceBytes,
|
|
412
|
+
totalBytes: slice.totalBytes,
|
|
413
|
+
outputPath: task.outputPath,
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
const runWait = async (params) => {
|
|
418
|
+
const task = resolveTaskOrThrow(params.taskId);
|
|
419
|
+
const waitMs = Math.min(BG_WAIT_MAX_MS, Math.max(BG_WAIT_MIN_MS, Math.floor(params.waitMs ?? BG_WAIT_DEFAULT_MS)));
|
|
420
|
+
const startedWait = Date.now();
|
|
421
|
+
const result = await requireRegistry().waitForResult(task.id, waitMs);
|
|
422
|
+
const waitedMs = Date.now() - startedWait;
|
|
423
|
+
const finalTask = result.task;
|
|
424
|
+
const exitSuffix = finalTask.exitCode !== undefined && finalTask.exitCode !== null ? ` · exit ${finalTask.exitCode}` : "";
|
|
425
|
+
const ran = formatDuration((finalTask.endedAt ?? Date.now()) - finalTask.startedAt);
|
|
426
|
+
if (result.outcome === "timeout") {
|
|
427
|
+
// Still running: keep progress visible with a bounded tail peek.
|
|
428
|
+
let peekText = "";
|
|
429
|
+
let peekNote = "";
|
|
430
|
+
let totalBytes = finalTask.outputBytes;
|
|
231
431
|
try {
|
|
232
|
-
|
|
432
|
+
const peek = await readOutputSlice(finalTask.outputPath, { mode: "tail", maxBytes: BG_WAIT_PEEK_BYTES });
|
|
433
|
+
peekText = sanitizeBinaryOutput(peek.text);
|
|
434
|
+
totalBytes = peek.totalBytes;
|
|
435
|
+
peekNote = peek.truncated ? ` (last ${formatSize(peek.sliceBytes)})` : "";
|
|
233
436
|
}
|
|
234
|
-
catch
|
|
235
|
-
|
|
236
|
-
throw new Error(`Could not read output for ${task.id} (${task.status}): ${message}\nOutput file: ${task.outputPath}`);
|
|
437
|
+
catch {
|
|
438
|
+
peekNote = " (output unavailable)";
|
|
237
439
|
}
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
: [
|
|
247
|
-
`task ${task.id} ${task.status}`,
|
|
248
|
-
`${formatSize(slice.totalBytes)} total`,
|
|
249
|
-
`output: ${task.outputPath}`,
|
|
250
|
-
];
|
|
251
|
-
const body = text.length > 0 ? text : "(no output yet)";
|
|
440
|
+
const lines = [
|
|
441
|
+
`[${finalTask.id} still running · waited ${formatDuration(waitedMs)} · ${formatSize(totalBytes)} total]`,
|
|
442
|
+
];
|
|
443
|
+
const trimmed = peekText.trimEnd();
|
|
444
|
+
if (trimmed.length > 0)
|
|
445
|
+
lines.push(`Last output${peekNote}:\n${trimmed}`);
|
|
446
|
+
lines.push("The task keeps running. Wait again (action wait), peek with action read, or stop it with action kill; " +
|
|
447
|
+
"the <background-task> notification still arrives when it ends. Do not sleep-poll.");
|
|
252
448
|
return {
|
|
253
|
-
content: [{ type: "text", text:
|
|
449
|
+
content: [{ type: "text", text: lines.join("\n\n") }],
|
|
254
450
|
details: {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
451
|
+
action: "wait",
|
|
452
|
+
taskId: finalTask.id,
|
|
453
|
+
timedOut: true,
|
|
454
|
+
status: finalTask.status,
|
|
455
|
+
exitCode: finalTask.exitCode,
|
|
456
|
+
waitedMs,
|
|
457
|
+
deltaBytes: 0,
|
|
458
|
+
totalBytes,
|
|
459
|
+
deltaTruncated: false,
|
|
460
|
+
outputPath: finalTask.outputPath,
|
|
260
461
|
},
|
|
261
462
|
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
463
|
+
}
|
|
464
|
+
// Terminal: deliver the bounded delta since the requested offset.
|
|
465
|
+
// The followUp notification was suppressed by the claim protocol —
|
|
466
|
+
// this result is the single delivery of the completion.
|
|
467
|
+
const since = params.sinceBytes !== undefined ? Math.max(0, Math.floor(params.sinceBytes)) : undefined;
|
|
468
|
+
let delta;
|
|
469
|
+
let offsetNote;
|
|
470
|
+
try {
|
|
471
|
+
delta = await readOutputSince(finalTask.outputPath, since ?? 0, BG_WAIT_DELTA_BYTES);
|
|
472
|
+
if (since !== undefined && since > delta.totalBytes) {
|
|
473
|
+
// A stale offset past EOF (the output was truncated at the cap, or
|
|
474
|
+
// the model is reusing an id from an earlier read): fall back to
|
|
475
|
+
// the tail and say so. Never an error — the offset is not a bug.
|
|
476
|
+
delta = await readOutputSince(finalTask.outputPath, 0, BG_WAIT_DELTA_BYTES);
|
|
477
|
+
offsetNote = finalTask.outputTruncated
|
|
478
|
+
? "the given offset is past EOF — output was truncated at the limit; showing the tail"
|
|
479
|
+
: "the given offset is past EOF; showing the tail";
|
|
279
480
|
}
|
|
481
|
+
}
|
|
482
|
+
catch (error) {
|
|
483
|
+
// Deliver the terminal status even when the output file cannot be read.
|
|
484
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
280
485
|
return {
|
|
281
486
|
content: [
|
|
282
487
|
{
|
|
283
488
|
type: "text",
|
|
284
|
-
text: `
|
|
489
|
+
text: `[${finalTask.id} ${finalTask.status}${exitSuffix} · ran ${ran} · waited ${formatDuration(waitedMs)}]\nerror reading output: ${message}`,
|
|
285
490
|
},
|
|
286
491
|
],
|
|
287
|
-
details: {
|
|
492
|
+
details: {
|
|
493
|
+
action: "wait",
|
|
494
|
+
taskId: finalTask.id,
|
|
495
|
+
timedOut: false,
|
|
496
|
+
status: finalTask.status,
|
|
497
|
+
exitCode: finalTask.exitCode,
|
|
498
|
+
waitedMs,
|
|
499
|
+
deltaBytes: 0,
|
|
500
|
+
totalBytes: finalTask.outputBytes,
|
|
501
|
+
deltaTruncated: false,
|
|
502
|
+
outputPath: finalTask.outputPath,
|
|
503
|
+
},
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const lines = [
|
|
507
|
+
`[${finalTask.id} ${finalTask.status}${exitSuffix} · ran ${ran} · waited ${formatDuration(waitedMs)} · +${formatSize(delta.sliceBytes)} new output · total ${formatSize(delta.totalBytes)}]`,
|
|
508
|
+
];
|
|
509
|
+
if (offsetNote)
|
|
510
|
+
lines.push(`[${offsetNote}]`);
|
|
511
|
+
if (finalTask.error)
|
|
512
|
+
lines.push(`error: ${finalTask.error}`);
|
|
513
|
+
if (delta.truncated) {
|
|
514
|
+
lines.push(`[showing ${formatSize(delta.sliceBytes)} of ${formatSize(delta.totalBytes)}; full output: ${finalTask.outputPath}]`);
|
|
515
|
+
}
|
|
516
|
+
const body = sanitizeBinaryOutput(delta.text).trimEnd();
|
|
517
|
+
lines.push(body.length > 0 ? body : "(no new output)");
|
|
518
|
+
return {
|
|
519
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
520
|
+
details: {
|
|
521
|
+
action: "wait",
|
|
522
|
+
taskId: finalTask.id,
|
|
523
|
+
timedOut: false,
|
|
524
|
+
status: finalTask.status,
|
|
525
|
+
exitCode: finalTask.exitCode,
|
|
526
|
+
waitedMs,
|
|
527
|
+
deltaBytes: delta.sliceBytes,
|
|
528
|
+
totalBytes: delta.totalBytes,
|
|
529
|
+
deltaTruncated: delta.truncated,
|
|
530
|
+
outputPath: finalTask.outputPath,
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
};
|
|
534
|
+
const runKill = (params) => {
|
|
535
|
+
const task = resolveTaskOrThrow(params.taskId);
|
|
536
|
+
const result = requireRegistry().killTask(task.id);
|
|
537
|
+
if (!result.killed) {
|
|
538
|
+
const exit = task.exitCode !== undefined && task.exitCode !== null ? `, exit ${task.exitCode}` : "";
|
|
539
|
+
throw new Error(`Task ${task.id} is not running (status: ${task.status}${exit}).`);
|
|
540
|
+
}
|
|
541
|
+
return {
|
|
542
|
+
content: [
|
|
543
|
+
{
|
|
544
|
+
type: "text",
|
|
545
|
+
text: `Killed task ${task.id}. If you are waiting on it (action wait) the killed status is delivered there; otherwise a completion notification follows once the process tree is reaped.`,
|
|
546
|
+
},
|
|
547
|
+
],
|
|
548
|
+
details: { action: "kill", taskId: task.id, command: task.command },
|
|
549
|
+
};
|
|
550
|
+
};
|
|
551
|
+
const runList = () => {
|
|
552
|
+
const tasks = requireRegistry().listTasks();
|
|
553
|
+
if (tasks.length === 0) {
|
|
554
|
+
return {
|
|
555
|
+
content: [{ type: "text", text: "No background tasks this session. Start one with action create." }],
|
|
556
|
+
details: { action: "list", running: 0, finished: 0, shown: 0, hidden: 0 },
|
|
288
557
|
};
|
|
558
|
+
}
|
|
559
|
+
const now = Date.now();
|
|
560
|
+
const running = tasks.filter((task) => task.status === "running");
|
|
561
|
+
const finished = tasks.filter((task) => task.status !== "running");
|
|
562
|
+
const shownFinished = finished.slice(0, BG_LIST_FINISHED_SHOWN);
|
|
563
|
+
const hidden = finished.length - shownFinished.length;
|
|
564
|
+
const lines = [`${running.length} running · ${finished.length} finished`];
|
|
565
|
+
for (const task of [...running, ...shownFinished]) {
|
|
566
|
+
lines.push(describeTaskLine(task, now));
|
|
567
|
+
}
|
|
568
|
+
if (hidden > 0) {
|
|
569
|
+
lines.push(`(+${hidden} more finished, not shown — action read shows any task's output)`);
|
|
570
|
+
}
|
|
571
|
+
return {
|
|
572
|
+
content: [{ type: "text", text: lines.join("\n") }],
|
|
573
|
+
details: {
|
|
574
|
+
action: "list",
|
|
575
|
+
running: running.length,
|
|
576
|
+
finished: finished.length,
|
|
577
|
+
shown: running.length + shownFinished.length,
|
|
578
|
+
hidden,
|
|
579
|
+
},
|
|
580
|
+
};
|
|
581
|
+
};
|
|
582
|
+
pi.registerTool({
|
|
583
|
+
name: "bg",
|
|
584
|
+
label: "bg",
|
|
585
|
+
description: "Run and manage background bash tasks.\n\n" +
|
|
586
|
+
"create: start a command in the background and return immediately with a task id and an " +
|
|
587
|
+
"output file path. Only use this when you don't need the result immediately and are OK being " +
|
|
588
|
+
"notified when the command completes later — a <background-task> notification with status, exit " +
|
|
589
|
+
"code, and output tail arrives automatically when it ends. Do NOT append '&' to the command; bg " +
|
|
590
|
+
"already runs it detached. An optional description labels the task in the UI; an optional timeout " +
|
|
591
|
+
"in seconds kills the task on expiry.\n\n" +
|
|
592
|
+
"read: bounded slice of a task's output (default: last 8KB); works while the task runs. The " +
|
|
593
|
+
"output file is a plain file — the read tool also works on it for line-based paging.\n\n" +
|
|
594
|
+
"wait: block until a task finishes (default 20s, max 60s) and return its status plus output " +
|
|
595
|
+
"written since a given offset. Use when the next step depends on the result; on timeout the task " +
|
|
596
|
+
"is still running and you may wait again. Never emulate waiting with sleep.\n\n" +
|
|
597
|
+
"kill: stop a running task (whole process tree).\n\n" +
|
|
598
|
+
"list: currently known tasks with status.\n\n" +
|
|
599
|
+
"Task ids accept a unique prefix ('3f' for 'bg-3f').",
|
|
600
|
+
promptSnippet: "Manage background shell tasks (bg: create/read/wait/kill/list)",
|
|
601
|
+
promptGuidelines: [
|
|
602
|
+
"Use bg (action create) for long-running commands (dev servers, watch builds, slow tests) instead of regular bash; results arrive as an automatic <background-task> notification, so continue independent work or end the turn.",
|
|
603
|
+
"Never wait on background tasks with sleep loops or repeated reads; use bg action wait (bounded) when a step depends on a task's result, or rely on the completion notification.",
|
|
604
|
+
],
|
|
605
|
+
parameters: bgSchema,
|
|
606
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
607
|
+
validateAction(params);
|
|
608
|
+
switch (params.action) {
|
|
609
|
+
case "create":
|
|
610
|
+
return runCreate(params, ctx);
|
|
611
|
+
case "read":
|
|
612
|
+
return runRead(params);
|
|
613
|
+
case "wait":
|
|
614
|
+
return runWait(params);
|
|
615
|
+
case "kill":
|
|
616
|
+
return runKill(params);
|
|
617
|
+
case "list":
|
|
618
|
+
return runList();
|
|
619
|
+
}
|
|
289
620
|
},
|
|
290
|
-
renderCall(args, theme) {
|
|
291
|
-
return
|
|
621
|
+
renderCall(args, theme, context) {
|
|
622
|
+
return renderBgCall(args, theme, context, waitLiveProbe);
|
|
623
|
+
},
|
|
624
|
+
renderResult(result, options, theme, context) {
|
|
625
|
+
return renderBgResult(result, options, theme, context);
|
|
292
626
|
},
|
|
293
627
|
});
|
|
294
628
|
pi.registerMessageRenderer(BG_NOTIFICATION_TYPE, renderBackgroundNotification);
|
|
@@ -306,7 +640,7 @@ export function createBackgroundExtension(overrides) {
|
|
|
306
640
|
.listTasks()
|
|
307
641
|
.slice(0, 10)
|
|
308
642
|
.map((task) => describeTaskLine(task, Date.now()));
|
|
309
|
-
ctx.ui.notify(`Background tasks:\n${lines.join("\n")}\nUse
|
|
643
|
+
ctx.ui.notify(`Background tasks:\n${lines.join("\n")}\nUse the bg tool (action kill) to stop a task.`, "info");
|
|
310
644
|
return;
|
|
311
645
|
}
|
|
312
646
|
await showBackgroundManager(ctx, active);
|