pi-background 0.3.2 → 0.3.3

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.
@@ -2,8 +2,9 @@ import { createWriteStream, mkdirSync, readFileSync } from "node:fs";
2
2
  import { homedir, tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { createLocalBashOperations, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
- import { Key, matchesKey, stripTerminalSequences, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
5
+ import { Key, matchesKey, stripTerminalSequences, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
6
6
  import { Type } from "typebox";
7
+ import { shortCommand } from "./command.ts";
7
8
 
8
9
  const TAIL_LINES = 20;
9
10
  const TAIL_BYTES = 64 * 1024;
@@ -65,6 +66,8 @@ function shownCommand(command: string): string {
65
66
  const shown = command
66
67
  .split("\n")[0]
67
68
  .replace(/^\s*(cd\s+("[^"]*"|'[^']*'|\S+)\s*(&&|;)\s*)+/, "")
69
+ // Leading settings such as PYTHONPATH=/some/src, which the path shortening below would cut at their last /
70
+ .replace(/^(\w+=("[^"]*"|'[^']*'|\S*)\s+)+/, "")
68
71
  .replace(/^\S*\/(?=[^\s/]+(\s|$))/, "");
69
72
  return shown.length > 50 ? `${shown.slice(0, 49)}…` : shown;
70
73
  }
@@ -204,22 +207,36 @@ export default function (pi: ExtensionAPI) {
204
207
 
205
208
  // Compact, Claude-Code-style rows instead of pi's default boxed tool and message rendering: quiet grey once done,
206
209
  // with a dot only while running or when something failed
210
+ // " └ text" in dim, with long and extra lines wrapped under the text instead of at the left edge
211
+ const branch = (theme: any, text: string, head?: string) => ({
212
+ invalidate() {},
213
+ render: (width: number) => [
214
+ ...(head ? [truncateToWidth(head, width)] : []),
215
+ ...wrapTextWithAnsi(text, Math.max(10, width - 4)).map((line, i) => theme.fg("dim", `${i === 0 ? " └ " : " "}${line}`)),
216
+ ],
217
+ });
207
218
  pi.registerMessageRenderer("background-job", (message, { expanded }, theme) => {
208
219
  const d = (message.details ?? {}) as { headline?: string; last?: string; error?: boolean };
209
220
  const title = `Background ${d.headline ?? ""}`;
210
221
  const head = d.error ? theme.fg("error", "● ") + theme.fg("toolTitle", title) : ` ${theme.fg("dim", title)}`;
211
- const body = expanded ? String(message.content) : (d.last ?? "");
212
- return new Text(`${head}\n${theme.fg("dim", ` └ ${body.replace(/\n/g, "\n ")}`)}`, 0, 0);
222
+ return branch(theme, expanded ? String(message.content) : (d.last ?? ""), head);
223
+ });
224
+ // One line, like pi-polish's shell rows; expanded (ctrl+o) shows the whole text, wrapped under the row
225
+ const row = (theme: any, text: string, context: any, full = text) => ({
226
+ invalidate() {},
227
+ render(width: number) {
228
+ const color = context.isError ? "error" : context.executionStarted && context.isPartial ? "accent" : undefined;
229
+ const mark = color ? theme.fg(color, "● ") : " ";
230
+ const paint = (line: string) => theme.fg(color ? "toolTitle" : "dim", line);
231
+ if (!context.expanded) return [mark + paint(truncateToWidth(text, Math.max(10, width - 2)))];
232
+ return wrapTextWithAnsi(full, Math.max(10, width - 2)).map((line, i) => (i === 0 ? mark : " ") + paint(line));
233
+ },
213
234
  });
214
- const row = (theme: any, text: string, context: any) => {
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);
217
- };
218
235
  const note = (theme: any, result: any, expanded: boolean) => {
219
236
  const text = result.content?.map((c: any) => c.text ?? "").join("\n") ?? "";
220
237
  // Collapsed: first sentence only ("Started bg1"), without the log path
221
238
  const short = text.split("\n")[0].split(";")[0];
222
- return new Text(theme.fg("dim", ` └ ${(expanded ? text : short).replace(/\n/g, "\n ")}`), 0, 0);
239
+ return branch(theme, expanded ? text : short);
223
240
  };
224
241
 
225
242
  pi.registerTool({
@@ -247,7 +264,12 @@ export default function (pi: ExtensionAPI) {
247
264
  // Runs after the edits in the same reply, so the job starts from the code pi just wrote
248
265
  executionMode: "sequential",
249
266
  renderShell: "self",
250
- renderCall: (args: any, theme, context) => row(theme, `Background $ ${String(args?.command ?? "").split("\n")[0]}`, context),
267
+ renderCall: (args: any, theme, context) => {
268
+ const command = String(args?.command ?? "");
269
+ // Collapsed, without leading settings such as PYTHONPATH=/some/src, like the jobs line; ctrl+o shows them
270
+ const short = shortCommand(command, args?.cwd ?? process.cwd()).replace(/^(\w+=("[^"]*"|'[^']*'|\S*)\s+)+/, "");
271
+ return row(theme, `Background $ ${short}`, context, `Background $ ${command.trim()}`);
272
+ },
251
273
  renderResult: (result, { expanded }, theme) => note(theme, result, expanded),
252
274
  parameters: Type.Object({
253
275
  command: Type.String({ description: "Shell command to run" }),
@@ -0,0 +1,22 @@
1
+ import { realpathSync } from "node:fs";
2
+ import { basename, isAbsolute, resolve } from "node:path";
3
+
4
+ // Real paths, so a cd through a symlink such as /tmp still counts as cwd
5
+ const real = (path: string) => {
6
+ try {
7
+ return realpathSync(path);
8
+ } catch {
9
+ return path;
10
+ }
11
+ };
12
+
13
+ /** A shell command's first line, without leading cds into cwd; another folder shows as its last part when the path is absolute. */
14
+ export function shortCommand(command: string, cwd: string): string {
15
+ const rest = command.trim().replace(/^(?:cd\s+(?:"[^"]*"|'[^']*'|\S+)[ \t]*(?:&&|;|\n)\s*)+/, (prefix) => {
16
+ const [, quoted, separator] = [...prefix.matchAll(/cd\s+("[^"]*"|'[^']*'|\S+)[ \t]*(&&|;|\n)/g)].at(-1)!;
17
+ const dir = quoted.replace(/^["']|["']$/g, "");
18
+ if (real(resolve(cwd, dir)) === real(resolve(cwd))) return "";
19
+ return `cd ${isAbsolute(dir) ? `…/${basename(dir)}` : quoted}${separator === ";" ? ";" : " &&"} `;
20
+ });
21
+ return rest.split("\n")[0];
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-background",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "license": "MIT",
5
5
  "author": "Thomas van Dongen",
6
6
  "type": "module",