pi-squire 0.9.3 → 0.9.4

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 CHANGED
@@ -6,6 +6,16 @@ follows [Semantic Versioning](https://semver.org/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.9.4] - 2026-10-23
10
+
11
+ ### Changed
12
+
13
+ - The `delegate` tool's row is drawn in the same style as the result message:
14
+ a call line naming the agent and label, then a ◐ header with the delegate's
15
+ name and model, a task preview and any skipped fallback models; expanded,
16
+ the whole task and the delegation id, window, thinking and auto-exit. The
17
+ text the model reads is unchanged.
18
+
9
19
  ## [0.9.3] - 2026-10-23
10
20
 
11
21
  ### Changed
@@ -354,7 +364,8 @@ author's dotfiles; the changes below are against that copy.
354
364
 
355
365
  - The published file list includes `child.ts`, which `index.ts` imports.
356
366
 
357
- [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.3...HEAD
367
+ [Unreleased]: https://github.com/gvanderclay/pi-squire/compare/v0.9.4...HEAD
368
+ [0.9.4]: https://github.com/gvanderclay/pi-squire/compare/v0.9.3...v0.9.4
358
369
  [0.9.3]: https://github.com/gvanderclay/pi-squire/compare/v0.9.2...v0.9.3
359
370
  [0.9.2]: https://github.com/gvanderclay/pi-squire/compare/v0.9.1...v0.9.2
360
371
  [0.9.1]: https://github.com/gvanderclay/pi-squire/compare/v0.9.0...v0.9.1
package/README.md CHANGED
@@ -453,7 +453,12 @@ Delegate session: <session file or id>
453
453
  delegate's name and the model, then the first 3 non-blank lines of the
454
454
  result with heading marks, backticks and bold stripped; expanded (Ctrl+O),
455
455
  the quoted task, the result as Markdown, then the ids and files, with the
456
- home directory shown as `~`. The closed-without-a-result message gets the same header with ⊘.
456
+ home directory shown as `~`.
457
+ - The `delegate` tool's own row is drawn the same way, so a launch and its
458
+ result match: a call line naming the agent and label, then a header with
459
+ a ◐ started icon, the delegate's name and the model it started on, a preview of the task and any skipped fallback models;
460
+ expanded, the whole task, then the delegation id, window, thinking and
461
+ auto-exit. A refused call keeps its plain error text. The closed-without-a-result message gets the same header with ⊘.
457
462
  - Stopping a delegate's run (Esc) sends no result: the provider holds the
458
463
  task, so the delegation stays running while the user steers it. The next
459
464
  run that completes sends the result, `done`, and its body opens with a note
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-squire",
3
- "version": "0.9.3",
3
+ "version": "0.9.4",
4
4
  "description": "Starts a delegate Pi session in a background tmux window, sends it the task over message:*, and shows the parent its result",
5
5
  "keywords": [
6
6
  "pi-package",
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,6 +1,7 @@
1
- // How a delegate message looks in the parent: drawn from the structured
2
- // details `results.ts` attaches, while the message's text stays what the
3
- // parent model reads.
1
+ // How delegations look in the parent: the `delegate` tool's row and the
2
+ // result or gone message that arrives later share one header and preview
3
+ // style. Both are drawn from structured details, while their text stays what
4
+ // the parent model reads.
4
5
  import { homedir } from "node:os";
5
6
 
6
7
  import {
@@ -11,7 +12,9 @@ import {
11
12
  type Theme,
12
13
  } from "@earendil-works/pi-coding-agent";
13
14
  import { Box, type Component, Markdown, Spacer, Text, TruncatedText } from "@earendil-works/pi-tui";
15
+ import type { Static } from "typebox";
14
16
 
17
+ import type { DelegateSchema } from "./index.ts";
15
18
  import type { MessageDetails } from "./results.ts";
16
19
 
17
20
  /** The launch model, or `launch → finished` when the delegate finished on another one. */
@@ -23,7 +26,8 @@ export function modelLabel(model: string, finished: string | undefined): string
23
26
  const PREVIEW_LINES = 3;
24
27
 
25
28
  /** The icon and theme color for each status; an unknown status gets a neutral icon. */
26
- const STATUS_STYLE: Record<string, { icon: string; color: "success" | "error" | "warning" | "muted" }> = {
29
+ const STATUS_STYLE: Record<string, { icon: string; color: "accent" | "success" | "error" | "warning" | "muted" }> = {
30
+ started: { icon: "◐", color: "accent" },
27
31
  done: { icon: "✓", color: "success" },
28
32
  failed: { icon: "✗", color: "error" },
29
33
  stopped: { icon: "■", color: "warning" },
@@ -38,12 +42,25 @@ function header(details: MessageDetails, theme: Theme): string {
38
42
  let trailer = "";
39
43
  if (details.kind === "gone") trailer = " closed without a result";
40
44
  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}${
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)}${
43
56
  trailer === "" ? "" : theme.fg(style.color, trailer)
44
57
  }`;
45
58
  }
46
59
 
60
+ function expandHint(theme: Theme): Text {
61
+ return new Text(`${theme.fg("dim", keyText("app.tools.expand"))}${theme.fg("muted", " to expand")}`, 0, 0);
62
+ }
63
+
47
64
  /** `path` with the home directory shown as `~`. */
48
65
  function tilde(path: string): string {
49
66
  const home = homedir();
@@ -97,7 +114,7 @@ export function renderMessage(
97
114
  if (details.kind === "result") {
98
115
  for (const line of preview(details.body)) box.addChild(new TruncatedText(theme.fg("muted", line)));
99
116
  }
100
- box.addChild(new Text(`${theme.fg("dim", keyText("app.tools.expand"))}${theme.fg("muted", " to expand")}`, 0, 0));
117
+ box.addChild(expandHint(theme));
101
118
  return box;
102
119
  }
103
120
  box.addChild(new Text(header(details, theme), 0, 0));
@@ -125,3 +142,56 @@ export function renderMessage(
125
142
  box.addChild(metadataText);
126
143
  return box;
127
144
  }
145
+
146
+ /** The `delegate` tool's call line: the tool, the agent and any label. Arguments may still be streaming. */
147
+ export function renderDelegateCall(args: { agent?: unknown; label?: unknown }, theme: Theme): Component {
148
+ let line = theme.bold("delegate");
149
+ if (typeof args.agent === "string") line += ` ${theme.fg("accent", args.agent)}`;
150
+ if (typeof args.label === "string" && args.label !== "" && args.label !== "null")
151
+ line += ` ${theme.fg("muted", `· ${args.label}`)}`;
152
+ return new Text(line, 0, 0);
153
+ }
154
+
155
+ /**
156
+ * The `delegate` tool's result, in the result message's style: a started
157
+ * header, then a task preview and the hint, or, expanded, the whole task and
158
+ * the ids. A refused or blocked call has no details and keeps its plain text.
159
+ */
160
+ export function renderDelegateResult(
161
+ result: { content: { type: string; text?: string }[]; details?: unknown },
162
+ { expanded }: { expanded: boolean },
163
+ theme: Theme,
164
+ context: { args: { task?: unknown }; isError: boolean },
165
+ ): Component {
166
+ const details = result.details as Partial<Static<typeof DelegateSchema>> | undefined;
167
+ if (context.isError || details?.outcome !== "started") {
168
+ const text = result.content.map((part) => part.text ?? "").join("\n");
169
+ return new Text(context.isError ? theme.fg("error", text) : text, 0, 0);
170
+ }
171
+ const started = details as Static<typeof DelegateSchema>;
172
+ const task = typeof context.args.task === "string" ? context.args.task : "";
173
+ const box = new Box(0, 0);
174
+ box.addChild(new TruncatedText(headerLine(theme, STATUS_STYLE.started, started.windowName, started.model, "")));
175
+ const skipped = started.skipped.map(({ model, reason }) => theme.fg("warning", `skipped ${model}: ${reason}`));
176
+ if (!expanded) {
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"}`,
194
+ ];
195
+ box.addChild(new Text(ids.map((line) => theme.fg("dim", line)).join("\n"), 0, 0));
196
+ return box;
197
+ }