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.
- package/extensions/background.ts +31 -9
- package/extensions/command.ts +22 -0
- package/package.json +1 -1
package/extensions/background.ts
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
212
|
-
|
|
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
|
|
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) =>
|
|
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
|
+
}
|