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.
@@ -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, type ExtensionContext } from "@earendil-works/pi-coding-agent";
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
- if (!ui) return;
172
- if (jobsLine() === "off" || running().length === 0) return ui.setWidget("background-jobs", undefined);
173
- ui.setWidget("background-jobs", (tui, theme) => {
174
- const timer = setInterval(() => tui.requestRender(), 1000);
175
- return {
176
- dispose: () => clearInterval(timer),
177
- invalidate() {},
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 head = theme.fg(d.error ? "error" : "success", "● ") + theme.fg("toolTitle", `Background ${d.headline ?? ""}`);
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" : "success";
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
- "To react when a bg_run job prints something specific while it keeps running (a server is ready, an epoch finished, an error appeared), pass wake_on with a regex instead of polling.",
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; pass wake_on matching the lines you want to see, such as results or Traceback.",
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' or 'epoch \\d+'. Matches within a second are batched; at most 10 wake-ups per job.",
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()) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-background",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "license": "MIT",
5
5
  "author": "Thomas van Dongen",
6
6
  "type": "module",