pi-squire 0.9.4 → 0.9.5
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 +13 -1
- package/README.md +14 -11
- package/package.json +1 -1
- package/src/render.ts +131 -127
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,17 @@ follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.5] - 2026-10-23
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- A delegation is drawn as one tool call that changes state, in the style of
|
|
14
|
+
pi-interactive-subagents, replacing the 0.9.2 to 0.9.4 look: the `delegate`
|
|
15
|
+
row shows `○` with the agent, label and task while the call runs, and one
|
|
16
|
+
header, `● <name> (<model>) — started`, once the delegate starts; the result arrives as a
|
|
17
|
+
finished tool row on Pi's tool success or error background, `✓ <name> (<model>) — done` or `✗` with the status,
|
|
18
|
+
with a 5-line preview. The renderer tests change with it.
|
|
19
|
+
|
|
9
20
|
## [0.9.4] - 2026-10-23
|
|
10
21
|
|
|
11
22
|
### Changed
|
|
@@ -364,7 +375,8 @@ author's dotfiles; the changes below are against that copy.
|
|
|
364
375
|
|
|
365
376
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
366
377
|
|
|
367
|
-
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.
|
|
378
|
+
[Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.5...HEAD
|
|
379
|
+
[0.9.5]: https://github.com/gvanderclay/pi-squire/compare/v0.9.4...v0.9.5
|
|
368
380
|
[0.9.4]: https://github.com/gvanderclay/pi-squire/compare/v0.9.3...v0.9.4
|
|
369
381
|
[0.9.3]: https://github.com/gvanderclay/pi-squire/compare/v0.9.2...v0.9.3
|
|
370
382
|
[0.9.2]: https://github.com/gvanderclay/pi-squire/compare/v0.9.1...v0.9.2
|
package/README.md
CHANGED
|
@@ -448,17 +448,20 @@ Delegate session: <session file or id>
|
|
|
448
448
|
or `/model` in its window), it reads `launched → finished`, for example
|
|
449
449
|
`(anthropic/claude-haiku-5-5 → openai/gpt-6-luna)`. `delegation_status`
|
|
450
450
|
reports the same.
|
|
451
|
-
- That text is what the parent model reads. The user sees
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
451
|
+
- That text is what the parent model reads. The user sees the delegation as
|
|
452
|
+
one tool call that changes state, in the style of
|
|
453
|
+
[pi-interactive-subagents](https://github.com/amosblomqvist/pi-interactive-subagents):
|
|
454
|
+
- The `delegate` row shows `○` with the agent, the label, the task's first
|
|
455
|
+
line and its line count while the call runs. Once the delegate starts,
|
|
456
|
+
that header is replaced by `● <name> (<model>) — started` over the same
|
|
457
|
+
task line, with any skipped fallback model. A refused call keeps its
|
|
458
|
+
`○` header above the plain error text.
|
|
459
|
+
- The result arrives drawn as a finished tool row, on Pi's tool success or
|
|
460
|
+
error background: `✓ <name> (<model>) — done`, or `✗` with `failed`,
|
|
461
|
+
`stopped` or `closed without a result`. Collapsed, it previews the first
|
|
462
|
+
5 non-blank lines with heading marks, backticks and bold stripped;
|
|
463
|
+
expanded (Ctrl+O), it shows the result as Markdown, the quoted task, then
|
|
464
|
+
the ids and files, with the home directory shown as `~`.
|
|
462
465
|
- Stopping a delegate's run (Esc) sends no result: the provider holds the
|
|
463
466
|
task, so the delegation stays running while the user steers it. The next
|
|
464
467
|
run that completes sends the result, `done`, and its body opens with a note
|
package/package.json
CHANGED
package/src/render.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
// How delegations look in the parent
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// How delegations look in the parent. A delegation reads as one tool call that
|
|
2
|
+
// changes state: the `delegate` row shows ○ while its arguments arrive and ●
|
|
3
|
+
// once started, and the result message that arrives later is drawn as a
|
|
4
|
+
// finished tool row (✓ or ✗ on Pi's tool success or error background). The
|
|
5
|
+
// style follows amosblomqvist/pi-interactive-subagents (c3e8b53,
|
|
6
|
+
// pi-extension/subagents/index.ts:1891-1944 and 2353-2460). Everything is
|
|
7
|
+
// drawn from structured details; the text the parent model reads is separate.
|
|
5
8
|
import { homedir } from "node:os";
|
|
6
9
|
|
|
7
10
|
import {
|
|
8
11
|
getMarkdownTheme,
|
|
9
|
-
|
|
12
|
+
keyHint,
|
|
10
13
|
type MessageRenderer,
|
|
11
14
|
type MessageRenderOptions,
|
|
12
15
|
type Theme,
|
|
13
16
|
} from "@earendil-works/pi-coding-agent";
|
|
14
|
-
import { Box, type Component, Markdown, Spacer, Text, TruncatedText } from "@earendil-works/pi-tui";
|
|
17
|
+
import { Box, type Component, Container, Markdown, Spacer, Text, TruncatedText } from "@earendil-works/pi-tui";
|
|
15
18
|
import type { Static } from "typebox";
|
|
16
19
|
|
|
17
20
|
import type { DelegateSchema } from "./index.ts";
|
|
@@ -22,44 +25,20 @@ export function modelLabel(model: string, finished: string | undefined): string
|
|
|
22
25
|
return finished === undefined || finished === model ? model : `${model} → ${finished}`;
|
|
23
26
|
}
|
|
24
27
|
|
|
25
|
-
/** Lines of
|
|
26
|
-
const PREVIEW_LINES =
|
|
28
|
+
/** Lines of a result the collapsed message previews. */
|
|
29
|
+
const PREVIEW_LINES = 5;
|
|
30
|
+
/** Characters of the task's first line the call row shows. */
|
|
31
|
+
const TASK_PREVIEW_CHARS = 100;
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
const STATUS_STYLE: Record<string, { icon: string; color: "accent" | "success" | "error" | "warning" | "muted" }> = {
|
|
30
|
-
started: { icon: "◐", color: "accent" },
|
|
31
|
-
done: { icon: "✓", color: "success" },
|
|
32
|
-
failed: { icon: "✗", color: "error" },
|
|
33
|
-
stopped: { icon: "■", color: "warning" },
|
|
34
|
-
gone: { icon: "⊘", color: "warning" },
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/** The header line: status icon, the delegate's name and the model it ran on. */
|
|
38
|
-
function header(details: MessageDetails, theme: Theme): string {
|
|
39
|
-
const status = details.kind === "gone" ? "gone" : details.status;
|
|
40
|
-
const known = Object.hasOwn(STATUS_STYLE, status);
|
|
41
|
-
const style = known ? STATUS_STYLE[status] : { icon: "•", color: "muted" as const };
|
|
42
|
-
let trailer = "";
|
|
43
|
-
if (details.kind === "gone") trailer = " closed without a result";
|
|
44
|
-
else if (!known) trailer = ` ${details.status}`;
|
|
45
|
-
return headerLine(theme, style, details.name, modelLabel(details.model, details.finishedModel), trailer);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function headerLine(
|
|
49
|
-
theme: Theme,
|
|
50
|
-
style: { icon: string; color: "accent" | "success" | "error" | "warning" | "muted" },
|
|
51
|
-
name: string,
|
|
52
|
-
model: string,
|
|
53
|
-
trailer: string,
|
|
54
|
-
): string {
|
|
55
|
-
return `${theme.fg(style.color, style.icon)} ${theme.fg("accent", name)} ${theme.fg("muted", model)}${
|
|
56
|
-
trailer === "" ? "" : theme.fg(style.color, trailer)
|
|
57
|
-
}`;
|
|
58
|
-
}
|
|
33
|
+
type Color = "success" | "error" | "warning" | "muted" | "dim";
|
|
59
34
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
35
|
+
/** The icon, its color and the status word for each way a delegation ends. */
|
|
36
|
+
const ENDINGS: Record<string, { icon: string; color: Color; word: string; wordColor: Color }> = {
|
|
37
|
+
done: { icon: "✓", color: "success", word: "done", wordColor: "dim" },
|
|
38
|
+
failed: { icon: "✗", color: "error", word: "failed", wordColor: "error" },
|
|
39
|
+
stopped: { icon: "✗", color: "warning", word: "stopped", wordColor: "warning" },
|
|
40
|
+
gone: { icon: "✗", color: "warning", word: "closed without a result", wordColor: "warning" },
|
|
41
|
+
};
|
|
63
42
|
|
|
64
43
|
/** `path` with the home directory shown as `~`. */
|
|
65
44
|
function tilde(path: string): string {
|
|
@@ -67,26 +46,11 @@ function tilde(path: string): string {
|
|
|
67
46
|
return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
68
47
|
}
|
|
69
48
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
else lines.push(`window ${details.windowName}`);
|
|
74
|
-
lines.push(
|
|
75
|
-
`session ${details.sessionPath === undefined ? `${details.id} (no session file found)` : tilde(details.sessionPath)}`,
|
|
76
|
-
);
|
|
77
|
-
return lines;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* The first non-blank lines of a body, with heading marks, backticks and bold
|
|
82
|
-
* markers stripped. Plain text, not Markdown, because a cut-off fragment can
|
|
83
|
-
* break mid-list or mid-code-block.
|
|
84
|
-
*/
|
|
85
|
-
function preview(body: string): string[] {
|
|
86
|
-
return body
|
|
49
|
+
/** Non-blank lines with heading marks, backticks and bold markers stripped, for a plain-text preview. */
|
|
50
|
+
function plainLines(text: string): string[] {
|
|
51
|
+
return text
|
|
87
52
|
.split("\n")
|
|
88
53
|
.filter((line) => line.trim() !== "")
|
|
89
|
-
.slice(0, PREVIEW_LINES)
|
|
90
54
|
.map((line) =>
|
|
91
55
|
line
|
|
92
56
|
.replace(/^#+\s+/, "")
|
|
@@ -95,10 +59,20 @@ function preview(body: string): string[] {
|
|
|
95
59
|
);
|
|
96
60
|
}
|
|
97
61
|
|
|
62
|
+
function idLines(details: MessageDetails): string[] {
|
|
63
|
+
const lines = [`delegation ${details.id}`, `request ${details.requestId}`];
|
|
64
|
+
if (details.kind === "result") lines.push(`envelope ${tilde(details.envelopePath)}`);
|
|
65
|
+
else lines.push(`window ${details.windowName}`);
|
|
66
|
+
lines.push(
|
|
67
|
+
`session ${details.sessionPath === undefined ? `${details.id} (no session file found)` : tilde(details.sessionPath)}`,
|
|
68
|
+
);
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
98
72
|
/**
|
|
99
|
-
* A delegate message drawn
|
|
100
|
-
*
|
|
101
|
-
*
|
|
73
|
+
* A delegate result or gone message, drawn as a finished tool row: a header
|
|
74
|
+
* (icon, name, model, status), then a preview and the expand hint, or,
|
|
75
|
+
* expanded, the result as Markdown, the task and the ids. Ctrl+O toggles it
|
|
102
76
|
* with tool output; the model always gets the full content.
|
|
103
77
|
*/
|
|
104
78
|
export function renderMessage(
|
|
@@ -108,54 +82,90 @@ export function renderMessage(
|
|
|
108
82
|
): Component | undefined {
|
|
109
83
|
const details = message.details as MessageDetails | undefined;
|
|
110
84
|
if (details?.kind !== "result" && details?.kind !== "gone") return undefined;
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
box.addChild(
|
|
118
|
-
|
|
85
|
+
const status = details.kind === "gone" ? "gone" : details.status;
|
|
86
|
+
const background = status === "done" ? "toolSuccessBg" : "toolErrorBg";
|
|
87
|
+
const box = new Box(outputPad, 1, (text) => theme.bg(background, text));
|
|
88
|
+
const header = headerLine(details, status, theme);
|
|
89
|
+
box.addChild(expanded ? new Text(header, 0, 0) : new TruncatedText(header));
|
|
90
|
+
if (details.kind === "result" && details.limit !== undefined)
|
|
91
|
+
box.addChild(new Text(theme.fg("warning", details.limit), 0, 0));
|
|
92
|
+
for (const child of expanded ? expandedLines(details, theme) : collapsedLines(details, theme)) box.addChild(child);
|
|
93
|
+
return box;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** The icon, the delegate's name, the model and the status word. */
|
|
97
|
+
function headerLine(details: MessageDetails, status: string, theme: Theme): string {
|
|
98
|
+
const ending = Object.hasOwn(ENDINGS, status)
|
|
99
|
+
? ENDINGS[status]
|
|
100
|
+
: { icon: "•", color: "muted" as const, word: status, wordColor: "muted" as const };
|
|
101
|
+
const model = modelLabel(details.model, details.finishedModel);
|
|
102
|
+
return `${theme.fg(ending.color, ending.icon)} ${theme.fg("toolTitle", theme.bold(details.name))}${theme.fg(
|
|
103
|
+
"dim",
|
|
104
|
+
` (${model}) —`,
|
|
105
|
+
)} ${theme.fg(ending.wordColor, ending.word)}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** A result's first lines, a count of the rest, and the expand hint. */
|
|
109
|
+
function collapsedLines(details: MessageDetails, theme: Theme): Component[] {
|
|
110
|
+
const children: Component[] = [];
|
|
111
|
+
if (details.kind === "result") {
|
|
112
|
+
const lines = plainLines(details.body);
|
|
113
|
+
for (const line of lines.slice(0, PREVIEW_LINES)) children.push(new TruncatedText(theme.fg("dim", line)));
|
|
114
|
+
if (lines.length > PREVIEW_LINES)
|
|
115
|
+
children.push(new Text(theme.fg("muted", `… ${lines.length - PREVIEW_LINES} more lines`), 0, 0));
|
|
119
116
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
117
|
+
children.push(new Text(keyHint("app.tools.expand", "to expand"), 0, 0));
|
|
118
|
+
return children;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** The result as Markdown and the quoted task, then the ids and files. */
|
|
122
|
+
function expandedLines(details: MessageDetails, theme: Theme): Component[] {
|
|
123
|
+
const dim = (lines: string[]): Text => new Text(lines.map((line) => theme.fg("dim", line)).join("\n"), 0, 0);
|
|
124
|
+
const children: Component[] = [];
|
|
125
|
+
if (details.kind === "result") {
|
|
126
|
+
const { task } = details;
|
|
127
|
+
const quote = "note" in task ? [task.note] : [`Task (${tilde(task.path)}):`, ...task.text.split("\n")];
|
|
128
|
+
children.push(new Spacer(1), new Markdown(details.body, 0, 0, getMarkdownTheme()), new Spacer(1), dim(quote));
|
|
128
129
|
}
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
box.addChild(new Spacer(1));
|
|
142
|
-
box.addChild(metadataText);
|
|
143
|
-
return box;
|
|
130
|
+
const tail = details.kind === "gone" ? ["A reply that arrives later is still delivered."] : [];
|
|
131
|
+
children.push(new Spacer(1), dim([...idLines(details), ...tail]));
|
|
132
|
+
return children;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The task's first non-blank line, cut at 100 characters, and its line count when it has more than one. */
|
|
136
|
+
function taskLine(task: unknown, theme: Theme): string {
|
|
137
|
+
if (typeof task !== "string") return "";
|
|
138
|
+
const first = task.split("\n").find((line) => line.trim() !== "") ?? "";
|
|
139
|
+
const shown = first.length > TASK_PREVIEW_CHARS ? `${first.slice(0, TASK_PREVIEW_CHARS)}…` : first;
|
|
140
|
+
const total = task.split("\n").length;
|
|
141
|
+
return `${shown === "" ? "" : theme.fg("toolOutput", shown)}${total > 1 ? theme.fg("muted", ` (${total} lines)`) : ""}`;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
/**
|
|
145
|
+
* The `delegate` tool's call: ○, the agent, any label and the task line,
|
|
146
|
+
* while arguments stream and the call runs. Pi redraws the call above the
|
|
147
|
+
* result, so once a launch succeeds the call draws nothing and the result's
|
|
148
|
+
* header is the row's only one; a refused call keeps it above the error.
|
|
149
|
+
*/
|
|
150
|
+
export function renderDelegateCall(
|
|
151
|
+
args: { agent?: unknown; label?: unknown; task?: unknown },
|
|
152
|
+
theme: Theme,
|
|
153
|
+
context: { isPartial: boolean; isError: boolean },
|
|
154
|
+
): Component {
|
|
155
|
+
if (!context.isPartial && !context.isError) return new Container();
|
|
156
|
+
const agent = typeof args.agent === "string" && args.agent !== "" ? args.agent : "…";
|
|
157
|
+
let text = `○ ${theme.fg("toolTitle", theme.bold(agent))}`;
|
|
150
158
|
if (typeof args.label === "string" && args.label !== "" && args.label !== "null")
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
text += theme.fg("dim", ` [${args.label}]`);
|
|
160
|
+
const task = taskLine(args.task, theme);
|
|
161
|
+
if (task !== "") text += `\n${task}`;
|
|
162
|
+
return new Text(text, 0, 0);
|
|
153
163
|
}
|
|
154
164
|
|
|
155
165
|
/**
|
|
156
|
-
* The `delegate` tool's result
|
|
157
|
-
*
|
|
158
|
-
*
|
|
166
|
+
* The `delegate` tool's result: ●, the delegate's name, its model and
|
|
167
|
+
* "started", the task line and any skipped fallback models; expanded, the ids too. A
|
|
168
|
+
* refused or blocked call has no details and keeps its plain text.
|
|
159
169
|
*/
|
|
160
170
|
export function renderDelegateResult(
|
|
161
171
|
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
@@ -169,29 +179,23 @@ export function renderDelegateResult(
|
|
|
169
179
|
return new Text(context.isError ? theme.fg("error", text) : text, 0, 0);
|
|
170
180
|
}
|
|
171
181
|
const started = details as Static<typeof DelegateSchema>;
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
for (const line of preview(task)) box.addChild(new TruncatedText(theme.fg("muted", line)));
|
|
178
|
-
for (const line of skipped) box.addChild(new TruncatedText(line));
|
|
179
|
-
box.addChild(expandHint(theme));
|
|
180
|
-
return box;
|
|
181
|
-
}
|
|
182
|
-
const quote = task
|
|
183
|
-
.split("\n")
|
|
184
|
-
.map((line) => theme.fg("muted", `> ${line}`))
|
|
185
|
-
.join("\n");
|
|
186
|
-
box.addChild(new Text(quote, 0, 0));
|
|
187
|
-
if (skipped.length + started.notices.length > 0)
|
|
188
|
-
box.addChild(new Text([...skipped, ...started.notices.map((line) => theme.fg("dim", line))].join("\n"), 0, 0));
|
|
189
|
-
box.addChild(new Spacer(1));
|
|
190
|
-
const ids = [
|
|
191
|
-
`delegation ${started.id}`,
|
|
192
|
-
`window ${started.windowName} (${started.windowId})`,
|
|
193
|
-
`thinking ${started.thinking}, auto-exit ${started.autoExit ? "on" : "off"}`,
|
|
182
|
+
const lines = [
|
|
183
|
+
`${theme.fg("accent", "●")} ${theme.fg("toolTitle", theme.bold(started.windowName))}${theme.fg(
|
|
184
|
+
"dim",
|
|
185
|
+
` (${started.model}) — started`,
|
|
186
|
+
)}`,
|
|
194
187
|
];
|
|
195
|
-
|
|
196
|
-
|
|
188
|
+
const task = taskLine(context.args.task, theme);
|
|
189
|
+
if (task !== "") lines.push(task);
|
|
190
|
+
lines.push(...started.skipped.map(({ model, reason }) => theme.fg("warning", `[Skipped ${model}: ${reason}]`)));
|
|
191
|
+
if (expanded)
|
|
192
|
+
lines.push(
|
|
193
|
+
...[
|
|
194
|
+
...started.notices,
|
|
195
|
+
`delegation ${started.id}`,
|
|
196
|
+
`window ${started.windowName} (${started.windowId})`,
|
|
197
|
+
`thinking ${started.thinking}, auto-exit ${started.autoExit ? "on" : "off"}`,
|
|
198
|
+
].map((line) => theme.fg("dim", line)),
|
|
199
|
+
);
|
|
200
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
197
201
|
}
|