pi-background 0.3.1 → 0.3.2
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/extensions/background.ts +44 -31
- package/package.json +1 -1
package/extensions/background.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createWriteStream, mkdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { homedir, tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { createLocalBashOperations, type ExtensionAPI
|
|
4
|
+
import { createLocalBashOperations, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { Key, matchesKey, stripTerminalSequences, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
6
|
import { Type } from "typebox";
|
|
7
7
|
|
|
@@ -161,35 +161,40 @@ export default function (pi: ExtensionAPI) {
|
|
|
161
161
|
};
|
|
162
162
|
|
|
163
163
|
// One line per running job above the input, with its elapsed time and latest output
|
|
164
|
-
let ui: ExtensionContext["ui"] | undefined;
|
|
165
164
|
const running = () => [...jobs.values()].filter((job) => job.status === "running");
|
|
166
165
|
// The job open in /bg, left out of the jobs line meanwhile so it isn't shown twice
|
|
167
166
|
let viewing: Job | undefined;
|
|
168
167
|
// "full" (the default), "compact" without the output, or "off", set with "background": { "jobs": ... } in pi's settings.json
|
|
169
168
|
const jobsLine = () => (pi.getSettings() as { background?: { jobs?: string } }).background?.jobs ?? "full";
|
|
169
|
+
// The widget is set once per session and redrawn in place: setting it again would move it below the widgets other
|
|
170
|
+
// extensions added since, while running jobs belong at the top, next to what Pi is doing now
|
|
171
|
+
let redrawJobs = () => {};
|
|
172
|
+
let ticker: NodeJS.Timeout | undefined;
|
|
170
173
|
const showJobs = () => {
|
|
171
|
-
|
|
172
|
-
if (
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
render(width: number) {
|
|
179
|
-
const live = running().filter((job) => job !== viewing);
|
|
180
|
-
const lines = live.slice(0, MAX_SHOWN_JOBS).map((job) => {
|
|
181
|
-
const text =
|
|
182
|
-
theme.fg("accent", " ⏵ ") +
|
|
183
|
-
theme.fg("toolTitle", `${job.id} ${shownCommand(job.command)}`) +
|
|
184
|
-
theme.fg("dim", ` · ${elapsed(job)}${jobsLine() === "full" && job.lastLine ? ` · ${job.lastLine}` : ""}`);
|
|
185
|
-
return truncateToWidth(text, width);
|
|
186
|
-
});
|
|
187
|
-
if (live.length > MAX_SHOWN_JOBS) lines.push(theme.fg("dim", ` and ${live.length - MAX_SHOWN_JOBS} more`));
|
|
188
|
-
return lines;
|
|
189
|
-
},
|
|
190
|
-
};
|
|
191
|
-
});
|
|
174
|
+
// A running job's elapsed time and latest output change every second
|
|
175
|
+
if (running().length > 0) ticker ??= setInterval(() => redrawJobs(), 1000);
|
|
176
|
+
else {
|
|
177
|
+
clearInterval(ticker);
|
|
178
|
+
ticker = undefined;
|
|
179
|
+
}
|
|
180
|
+
redrawJobs();
|
|
192
181
|
};
|
|
182
|
+
const jobsWidget = (theme: any) => ({
|
|
183
|
+
invalidate() {},
|
|
184
|
+
render(width: number) {
|
|
185
|
+
if (jobsLine() === "off") return [];
|
|
186
|
+
const live = running().filter((job) => job !== viewing);
|
|
187
|
+
const lines = live.slice(0, MAX_SHOWN_JOBS).map((job) => {
|
|
188
|
+
const text =
|
|
189
|
+
theme.fg("accent", "⏵ ") +
|
|
190
|
+
theme.fg("toolTitle", `${job.id} ${shownCommand(job.command)}`) +
|
|
191
|
+
theme.fg("dim", ` · ${elapsed(job)}${jobsLine() === "full" && job.lastLine ? ` · ${job.lastLine}` : ""}`);
|
|
192
|
+
return truncateToWidth(text, width);
|
|
193
|
+
});
|
|
194
|
+
if (live.length > MAX_SHOWN_JOBS) lines.push(theme.fg("dim", ` and ${live.length - MAX_SHOWN_JOBS} more`));
|
|
195
|
+
return lines;
|
|
196
|
+
},
|
|
197
|
+
});
|
|
193
198
|
|
|
194
199
|
const find = (id: string): Job => {
|
|
195
200
|
const job = jobs.get(id);
|
|
@@ -197,16 +202,18 @@ export default function (pi: ExtensionAPI) {
|
|
|
197
202
|
return job;
|
|
198
203
|
};
|
|
199
204
|
|
|
200
|
-
// Compact, Claude-Code-style rows instead of pi's default boxed tool and message rendering
|
|
205
|
+
// Compact, Claude-Code-style rows instead of pi's default boxed tool and message rendering: quiet grey once done,
|
|
206
|
+
// with a dot only while running or when something failed
|
|
201
207
|
pi.registerMessageRenderer("background-job", (message, { expanded }, theme) => {
|
|
202
208
|
const d = (message.details ?? {}) as { headline?: string; last?: string; error?: boolean };
|
|
203
|
-
const
|
|
209
|
+
const title = `Background ${d.headline ?? ""}`;
|
|
210
|
+
const head = d.error ? theme.fg("error", "● ") + theme.fg("toolTitle", title) : ` ${theme.fg("dim", title)}`;
|
|
204
211
|
const body = expanded ? String(message.content) : (d.last ?? "");
|
|
205
212
|
return new Text(`${head}\n${theme.fg("dim", ` └ ${body.replace(/\n/g, "\n ")}`)}`, 0, 0);
|
|
206
213
|
});
|
|
207
214
|
const row = (theme: any, text: string, context: any) => {
|
|
208
|
-
const color = context.isError ? "error" : context.executionStarted && context.isPartial ? "accent" :
|
|
209
|
-
return new Text(theme.fg(color, "● ") + theme.fg("toolTitle", text), 0, 0);
|
|
215
|
+
const color = context.isError ? "error" : context.executionStarted && context.isPartial ? "accent" : undefined;
|
|
216
|
+
return new Text(color ? theme.fg(color, "● ") + theme.fg("toolTitle", text) : ` ${theme.fg("dim", text)}`, 0, 0);
|
|
210
217
|
};
|
|
211
218
|
const note = (theme: any, result: any, expanded: boolean) => {
|
|
212
219
|
const text = result.content?.map((c: any) => c.text ?? "").join("\n") ?? "";
|
|
@@ -229,13 +236,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
229
236
|
"When unsure whether a command is quick, prefer bg_run: a quick job just reports back sooner, while a long bash command blocks the user until it finishes.",
|
|
230
237
|
"After bg_run, continue with independent work or end your turn; a message with the exit code and log tail arrives automatically when the job exits, so never sleep, poll or tail the log to wait for it.",
|
|
231
238
|
"Keep using bash for quick commands whose output you need right away, such as git, ls, cat, grep, find, file inspection and short checks.",
|
|
232
|
-
"
|
|
239
|
+
"Pass wake_on only when you must act before a bg_run job ends, such as a server becoming ready to test against, or when the user asked to follow its progress. To report a job's result, leave wake_on out: the exit message already brings the exit code and last lines, so waking on every epoch or step only adds turns.",
|
|
233
240
|
"Don't redirect a bg_run command's output to a file (> file, 2>&1, nohup): bg_run already saves all of it to the job's log, and redirecting hides the job's progress from the user and from wake_on. If the output should be kept, tell the user where the job's log is.",
|
|
234
241
|
"bg_run starts only after the other tool calls in the same reply have finished, so editing a file and starting a job that uses it in one reply is safe.",
|
|
235
242
|
"Use bg_status to check progress of a bg_run job and bg_kill to stop one.",
|
|
236
243
|
`bash commands time out after ${BASH_DEFAULT_TIMEOUT / 60} minutes unless you pass timeout (at most ${BASH_MAX_TIMEOUT / 60} minutes); run anything that may take longer with bg_run instead.`,
|
|
237
244
|
"Unless the user explicitly asks for that bash command, never wait, poll or sleep in bash (while/until loops with sleep, watch, repeated status checks), not even as part of a larger command; start the wait with bg_run and pass wake_on to be woken when the awaited output appears.",
|
|
238
|
-
"Unless the user explicitly asks otherwise, start long-running processes (training or evaluation runs, servers, watchers) with bg_run, not with & or nohup in bash followed by sleep
|
|
245
|
+
"Unless the user explicitly asks otherwise, start long-running processes (training or evaluation runs, servers, watchers) with bg_run, not with & or nohup in bash followed by sleep.",
|
|
239
246
|
],
|
|
240
247
|
// Runs after the edits in the same reply, so the job starts from the code pi just wrote
|
|
241
248
|
executionMode: "sequential",
|
|
@@ -247,7 +254,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
247
254
|
cwd: Type.Optional(Type.String({ description: "Working directory (defaults to the session cwd)" })),
|
|
248
255
|
wake_on: Type.Optional(
|
|
249
256
|
Type.String({
|
|
250
|
-
description: "Regex; also wake you while the job runs whenever an output line matches, e.g. 'listening on|ready'
|
|
257
|
+
description: "Regex; also wake you while the job runs whenever an output line matches, e.g. 'listening on|ready'. Matches within a second are batched; at most 10 wake-ups per job.",
|
|
251
258
|
}),
|
|
252
259
|
),
|
|
253
260
|
}),
|
|
@@ -293,7 +300,6 @@ export default function (pi: ExtensionAPI) {
|
|
|
293
300
|
});
|
|
294
301
|
});
|
|
295
302
|
|
|
296
|
-
if (ctx.mode === "tui") ui = ctx.ui;
|
|
297
303
|
showJobs();
|
|
298
304
|
return { content: [{ type: "text", text: `Started ${id}; log: ${log}. You will be notified when it exits.` }], details: { id, log } };
|
|
299
305
|
},
|
|
@@ -395,6 +401,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
395
401
|
|
|
396
402
|
// After /reload or a resume, carry on after the ids already in the conversation so "bg3" keeps meaning one job
|
|
397
403
|
pi.on("session_start", async (_event, ctx) => {
|
|
404
|
+
if (ctx.mode === "tui") {
|
|
405
|
+
ctx.ui.setWidget("background-jobs", (tui, theme) => {
|
|
406
|
+
redrawJobs = () => tui.requestRender();
|
|
407
|
+
return jobsWidget(theme);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
398
410
|
for (const entry of ctx.sessionManager.getBranch()) {
|
|
399
411
|
const msg = entry.type === "message" ? (entry.message as any) : undefined;
|
|
400
412
|
const n = Number(/^bg(\d+)$/.exec(msg?.role === "toolResult" && msg.toolName === "bg_run" ? String(msg.details?.id) : "")?.[1]);
|
|
@@ -403,6 +415,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
403
415
|
});
|
|
404
416
|
|
|
405
417
|
pi.on("session_shutdown", async () => {
|
|
418
|
+
clearInterval(ticker);
|
|
406
419
|
for (const timer of holds.values()) clearTimeout(timer);
|
|
407
420
|
holds.clear();
|
|
408
421
|
for (const job of jobs.values()) {
|