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