pi-squire 0.9.3 → 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 +24 -1
- package/README.md +14 -6
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/render.ts +153 -79
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,27 @@ 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
|
+
|
|
20
|
+
## [0.9.4] - 2026-10-23
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- The `delegate` tool's row is drawn in the same style as the result message:
|
|
25
|
+
a call line naming the agent and label, then a ◐ header with the delegate's
|
|
26
|
+
name and model, a task preview and any skipped fallback models; expanded,
|
|
27
|
+
the whole task and the delegation id, window, thinking and auto-exit. The
|
|
28
|
+
text the model reads is unchanged.
|
|
29
|
+
|
|
9
30
|
## [0.9.3] - 2026-10-23
|
|
10
31
|
|
|
11
32
|
### Changed
|
|
@@ -354,7 +375,9 @@ author's dotfiles; the changes below are against that copy.
|
|
|
354
375
|
|
|
355
376
|
- The published file list includes `child.ts`, which `index.ts` imports.
|
|
356
377
|
|
|
357
|
-
[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
|
|
380
|
+
[0.9.4]: https://github.com/gvanderclay/pi-squire/compare/v0.9.3...v0.9.4
|
|
358
381
|
[0.9.3]: https://github.com/gvanderclay/pi-squire/compare/v0.9.2...v0.9.3
|
|
359
382
|
[0.9.2]: https://github.com/gvanderclay/pi-squire/compare/v0.9.1...v0.9.2
|
|
360
383
|
[0.9.1]: https://github.com/gvanderclay/pi-squire/compare/v0.9.0...v0.9.1
|
package/README.md
CHANGED
|
@@ -448,12 +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
|
-
|
|
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 `~`.
|
|
457
465
|
- Stopping a delegate's run (Esc) sends no result: the provider holds the
|
|
458
466
|
task, so the delegation stays running while the user steers it. The next
|
|
459
467
|
run that completes sends the result, `done`, and its body opens with a note
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -49,6 +49,7 @@ import { Type } from "typebox";
|
|
|
49
49
|
import { type Agent, isThinking, type Roster, readRoster, THINKING_LEVELS } from "./agents.ts";
|
|
50
50
|
import { AGENT_ENV, AUTO_EXIT_ENV, FALLBACK_ENV, registerChild } from "./child.ts";
|
|
51
51
|
import { activeMark, clearMarks, isFree, readMarks } from "./limits.ts";
|
|
52
|
+
import { renderDelegateCall, renderDelegateResult } from "./render.ts";
|
|
52
53
|
import { createResults, delegateName } from "./results.ts";
|
|
53
54
|
import { createTmuxClient, type TmuxClient } from "./tmux.ts";
|
|
54
55
|
import { createTracking, toolResult } from "./tracking.ts";
|
|
@@ -596,6 +597,8 @@ export default function delegate(
|
|
|
596
597
|
),
|
|
597
598
|
}),
|
|
598
599
|
outputSchema: DelegateSchema,
|
|
600
|
+
renderCall: renderDelegateCall,
|
|
601
|
+
renderResult: renderDelegateResult,
|
|
599
602
|
// Opens a new window each call and removes nothing. Open world: the delegate calls a remote model and runs its agent's tools unattended.
|
|
600
603
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
601
604
|
async execute(
|
package/src/render.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
|
-
// How
|
|
2
|
-
//
|
|
3
|
-
//
|
|
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.
|
|
4
8
|
import { homedir } from "node:os";
|
|
5
9
|
|
|
6
10
|
import {
|
|
7
11
|
getMarkdownTheme,
|
|
8
|
-
|
|
12
|
+
keyHint,
|
|
9
13
|
type MessageRenderer,
|
|
10
14
|
type MessageRenderOptions,
|
|
11
15
|
type Theme,
|
|
12
16
|
} from "@earendil-works/pi-coding-agent";
|
|
13
|
-
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";
|
|
18
|
+
import type { Static } from "typebox";
|
|
14
19
|
|
|
20
|
+
import type { DelegateSchema } from "./index.ts";
|
|
15
21
|
import type { MessageDetails } from "./results.ts";
|
|
16
22
|
|
|
17
23
|
/** The launch model, or `launch → finished` when the delegate finished on another one. */
|
|
@@ -19,30 +25,20 @@ export function modelLabel(model: string, finished: string | undefined): string
|
|
|
19
25
|
return finished === undefined || finished === model ? model : `${model} → ${finished}`;
|
|
20
26
|
}
|
|
21
27
|
|
|
22
|
-
/** Lines of
|
|
23
|
-
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;
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
const STATUS_STYLE: Record<string, { icon: string; color: "success" | "error" | "warning" | "muted" }> = {
|
|
27
|
-
done: { icon: "✓", color: "success" },
|
|
28
|
-
failed: { icon: "✗", color: "error" },
|
|
29
|
-
stopped: { icon: "■", color: "warning" },
|
|
30
|
-
gone: { icon: "⊘", color: "warning" },
|
|
31
|
-
};
|
|
33
|
+
type Color = "success" | "error" | "warning" | "muted" | "dim";
|
|
32
34
|
|
|
33
|
-
/** The
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
else if (!known) trailer = ` ${details.status}`;
|
|
41
|
-
const model = theme.fg("muted", modelLabel(details.model, details.finishedModel));
|
|
42
|
-
return `${theme.fg(style.color, style.icon)} ${theme.fg("accent", details.name)} ${model}${
|
|
43
|
-
trailer === "" ? "" : theme.fg(style.color, trailer)
|
|
44
|
-
}`;
|
|
45
|
-
}
|
|
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
|
+
};
|
|
46
42
|
|
|
47
43
|
/** `path` with the home directory shown as `~`. */
|
|
48
44
|
function tilde(path: string): string {
|
|
@@ -50,26 +46,11 @@ function tilde(path: string): string {
|
|
|
50
46
|
return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
51
47
|
}
|
|
52
48
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
else lines.push(`window ${details.windowName}`);
|
|
57
|
-
lines.push(
|
|
58
|
-
`session ${details.sessionPath === undefined ? `${details.id} (no session file found)` : tilde(details.sessionPath)}`,
|
|
59
|
-
);
|
|
60
|
-
return lines;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* The first non-blank lines of a body, with heading marks, backticks and bold
|
|
65
|
-
* markers stripped. Plain text, not Markdown, because a cut-off fragment can
|
|
66
|
-
* break mid-list or mid-code-block.
|
|
67
|
-
*/
|
|
68
|
-
function preview(body: string): string[] {
|
|
69
|
-
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
|
|
70
52
|
.split("\n")
|
|
71
53
|
.filter((line) => line.trim() !== "")
|
|
72
|
-
.slice(0, PREVIEW_LINES)
|
|
73
54
|
.map((line) =>
|
|
74
55
|
line
|
|
75
56
|
.replace(/^#+\s+/, "")
|
|
@@ -78,10 +59,20 @@ function preview(body: string): string[] {
|
|
|
78
59
|
);
|
|
79
60
|
}
|
|
80
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
|
+
|
|
81
72
|
/**
|
|
82
|
-
* A delegate message drawn
|
|
83
|
-
*
|
|
84
|
-
*
|
|
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
|
|
85
76
|
* with tool output; the model always gets the full content.
|
|
86
77
|
*/
|
|
87
78
|
export function renderMessage(
|
|
@@ -91,37 +82,120 @@ export function renderMessage(
|
|
|
91
82
|
): Component | undefined {
|
|
92
83
|
const details = message.details as MessageDetails | undefined;
|
|
93
84
|
if (details?.kind !== "result" && details?.kind !== "gone") return undefined;
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
box.addChild(new Text(
|
|
101
|
-
|
|
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));
|
|
102
116
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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));
|
|
111
129
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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)`) : ""}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
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))}`;
|
|
158
|
+
if (typeof args.label === "string" && args.label !== "" && args.label !== "null")
|
|
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);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
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.
|
|
169
|
+
*/
|
|
170
|
+
export function renderDelegateResult(
|
|
171
|
+
result: { content: { type: string; text?: string }[]; details?: unknown },
|
|
172
|
+
{ expanded }: { expanded: boolean },
|
|
173
|
+
theme: Theme,
|
|
174
|
+
context: { args: { task?: unknown }; isError: boolean },
|
|
175
|
+
): Component {
|
|
176
|
+
const details = result.details as Partial<Static<typeof DelegateSchema>> | undefined;
|
|
177
|
+
if (context.isError || details?.outcome !== "started") {
|
|
178
|
+
const text = result.content.map((part) => part.text ?? "").join("\n");
|
|
179
|
+
return new Text(context.isError ? theme.fg("error", text) : text, 0, 0);
|
|
180
|
+
}
|
|
181
|
+
const started = details as Static<typeof DelegateSchema>;
|
|
182
|
+
const lines = [
|
|
183
|
+
`${theme.fg("accent", "●")} ${theme.fg("toolTitle", theme.bold(started.windowName))}${theme.fg(
|
|
184
|
+
"dim",
|
|
185
|
+
` (${started.model}) — started`,
|
|
186
|
+
)}`,
|
|
187
|
+
];
|
|
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);
|
|
127
201
|
}
|