@bermudi/pi-delegate 0.1.13 → 0.1.15

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/render-result.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Text } from "@earendil-works/pi-tui";
1
+ import { Box, Text } from "@earendil-works/pi-tui";
2
2
  import type { Theme } from "@earendil-works/pi-coding-agent";
3
3
  import {
4
4
  fmtDuration,
@@ -15,6 +15,7 @@ import {
15
15
  type RenderHelpers,
16
16
  } from "./render-branches.ts";
17
17
  import type { DelegateDetails, TaskProgress } from "./types.ts";
18
+ import { sanitizeTerminalLine, sanitizeTerminalText } from "./utils.ts";
18
19
 
19
20
  /** Build the shared helpers bound to a theme, width, and the lines sink.
20
21
  * Both render branches consume one bound set so warning/activity formatting
@@ -25,9 +26,11 @@ function makeRenderHelpers(
25
26
  lines: string[],
26
27
  ): RenderHelpers {
27
28
  const statJoin = (parts: string[]) =>
28
- parts.length ? theme.fg("muted", ` · ${parts.join(" · ")}`) : "";
29
- const modelLabel = (p: TaskProgress) =>
30
- p.model ? ` ${theme.fg("accent", p.model)}` : "";
29
+ parts.length ? theme.fg("dim", ` · ${parts.join(" · ")}`) : "";
30
+ const modelLabel = (p: TaskProgress) => {
31
+ const model = p.model ? sanitizeTerminalLine(p.model) : "";
32
+ return model ? theme.fg("dim", ` · ${model}`) : "";
33
+ };
31
34
 
32
35
  // Push muted warning lines for a task under its status row. Rendered in
33
36
  // both partial and final views so a human watching the TUI sees that tools
@@ -35,11 +38,21 @@ function makeRenderHelpers(
35
38
  const pushWarnings = (p: TaskProgress, ind: string) => {
36
39
  if (!p.warnings?.length) return;
37
40
  for (const wn of p.warnings) {
38
- lines.push(truncLine(`${ind}${theme.fg("warning", `⚠ ${wn}`)}`, w));
41
+ const warning = sanitizeTerminalLine(wn);
42
+ if (warning) {
43
+ lines.push(
44
+ truncLine(`${ind}${theme.fg("warning", `⚠ ${warning}`)}`, w),
45
+ );
46
+ }
39
47
  }
40
48
  };
41
49
 
42
- return { statJoin, modelLabel, compactActivity, pushWarnings };
50
+ return {
51
+ statJoin,
52
+ modelLabel,
53
+ compactActivity: (p) => sanitizeTerminalLine(compactActivity(p)),
54
+ pushWarnings,
55
+ };
43
56
  }
44
57
 
45
58
  /** Minimal structural view of Pi's `ToolRenderContext` used by the delegate
@@ -50,6 +63,12 @@ interface RenderCtx {
50
63
  invalidate: () => void;
51
64
  executionStarted: boolean;
52
65
  isPartial: boolean;
66
+ /** Message renderers own their horizontal padding; tool renderers do not. */
67
+ outputPad?: number;
68
+ /** Width already consumed by an outer shell such as the async result box. */
69
+ widthInset?: number;
70
+ /** Standalone custom messages do not need the tool-result leading spacer. */
71
+ standalone?: boolean;
53
72
  }
54
73
 
55
74
  interface RenderResultOptions {
@@ -65,6 +84,15 @@ interface RenderResult {
65
84
  details?: DelegateDetails;
66
85
  }
67
86
 
87
+ interface AsyncDelegateMessage {
88
+ content: string | Array<{ type: string; text?: string }>;
89
+ details?: DelegateDetails;
90
+ }
91
+
92
+ interface AsyncMessageRenderOptions {
93
+ expanded: boolean;
94
+ }
95
+
68
96
  /** Custom rendering for the tool *call* display — minimal by design; the result
69
97
  * renderer shows all detail. Only animates a spinner while still running. */
70
98
  export function renderDelegateCall(
@@ -75,7 +103,9 @@ export function renderDelegateCall(
75
103
  const state = ctx.state;
76
104
  const rawTasks = args.tasks;
77
105
  const tasks = Array.isArray(rawTasks) ? rawTasks : [];
78
- const text = (ctx.lastComponent as Text | undefined) ?? new Text("", 0, 0);
106
+ const text =
107
+ (ctx.lastComponent as Text | undefined) ??
108
+ new Text("", ctx.outputPad ?? 0, 0);
79
109
  if (typeof rawTasks === "string") {
80
110
  text.setText(theme.fg("toolTitle", theme.bold("delegate invalid tasks")));
81
111
  return text;
@@ -129,7 +159,9 @@ export function renderDelegateResult(
129
159
  clearInterval(state.interval);
130
160
  state.interval = undefined;
131
161
  }
132
- const text = (ctx.lastComponent as Text | undefined) ?? new Text("", 0, 0);
162
+ const text =
163
+ (ctx.lastComponent as Text | undefined) ??
164
+ new Text("", ctx.outputPad ?? 0, 0);
133
165
 
134
166
  const details = result.details;
135
167
  if (!details?.progress?.length) {
@@ -138,7 +170,10 @@ export function renderDelegateResult(
138
170
  ?.filter((c) => c.type === "text")
139
171
  .map((c) => c.text)
140
172
  .join("\n") ?? "";
141
- text.setText(content ? `\n${content}` : "");
173
+ const safeContent = sanitizeTerminalText(content);
174
+ text.setText(
175
+ safeContent ? `${ctx.standalone ? "" : "\n"}${safeContent}` : "",
176
+ );
142
177
  return text;
143
178
  }
144
179
 
@@ -149,8 +184,11 @@ export function renderDelegateResult(
149
184
  status: ticketStatus,
150
185
  } = details;
151
186
  const total = progress.length;
152
- const w = getTermWidth() - 4;
153
- const lines: string[] = [""];
187
+ const widthInset =
188
+ ctx.widthInset ??
189
+ (ctx.outputPad === undefined ? 4 : Math.max(0, ctx.outputPad * 2));
190
+ const w = getTermWidth() - widthInset;
191
+ const lines: string[] = ctx.standalone ? [] : [""];
154
192
  const helpers = makeRenderHelpers(theme, w, lines);
155
193
 
156
194
  const branchCtx = {
@@ -164,19 +202,32 @@ export function renderDelegateResult(
164
202
  lines,
165
203
  ticketId,
166
204
  ticketStatus,
205
+ elapsedMs: details.elapsedMs,
167
206
  };
168
207
 
169
208
  // Surface batch-level warnings at the top of the TUI. The same text already
170
209
  // lives in textual content, but the progress renderer ignores content.
171
210
  if (details?.dispatchWarning) {
172
- lines.push(
173
- truncLine(theme.fg("warning", `⚠ ${details.dispatchWarning}`), w),
174
- "",
175
- );
211
+ const warning = sanitizeTerminalLine(details.dispatchWarning);
212
+ if (warning) {
213
+ lines.push(truncLine(theme.fg("warning", `⚠ ${warning}`), w), "");
214
+ }
176
215
  }
177
216
  if (details?.overlapWarning) {
217
+ const warning = sanitizeTerminalLine(details.overlapWarning);
218
+ if (warning) {
219
+ lines.push(truncLine(theme.fg("warning", `⚠ ${warning}`), w), "");
220
+ }
221
+ }
222
+ if (details?.crossLeafDelivery) {
178
223
  lines.push(
179
- truncLine(theme.fg("warning", `⚠ ${details.overlapWarning}`), w),
224
+ truncLine(
225
+ theme.fg(
226
+ "warning",
227
+ "↳ Result delivered from another session-tree branch",
228
+ ),
229
+ w,
230
+ ),
180
231
  "",
181
232
  );
182
233
  }
@@ -195,3 +246,52 @@ export function renderDelegateResult(
195
246
  text.setText(budgeted.join("\n"));
196
247
  return text;
197
248
  }
249
+
250
+ /**
251
+ * Render an automatically delivered async result with the same compact /
252
+ * expanded presentation as a synchronous tool result. The full message
253
+ * content remains untouched in model context; only its TUI presentation is
254
+ * replaced.
255
+ */
256
+ export function renderAsyncDelegateMessage(
257
+ message: AsyncDelegateMessage,
258
+ options: AsyncMessageRenderOptions,
259
+ theme: Theme,
260
+ ): Box {
261
+ const content =
262
+ typeof message.content === "string"
263
+ ? [{ type: "text", text: message.content }]
264
+ : message.content;
265
+ const result = renderDelegateResult(
266
+ { content, details: message.details },
267
+ { isPartial: false, expanded: options.expanded },
268
+ theme,
269
+ {
270
+ state: {},
271
+ lastComponent: undefined,
272
+ invalidate: () => undefined,
273
+ executionStarted: false,
274
+ isPartial: false,
275
+ // The Box below owns the same one-column padding as Pi's tool shell.
276
+ outputPad: 0,
277
+ widthInset: 2,
278
+ standalone: true,
279
+ },
280
+ );
281
+ const status = message.details?.status;
282
+ const failed =
283
+ status === "failed" ||
284
+ status === "cancelled" ||
285
+ message.details?.progress?.some((task) => task.status === "failed");
286
+ const pending = status === "running" || status === "cancelling";
287
+ let background: "toolPendingBg" | "toolErrorBg" | "toolSuccessBg" =
288
+ "toolSuccessBg";
289
+ if (pending) {
290
+ background = "toolPendingBg";
291
+ } else if (failed) {
292
+ background = "toolErrorBg";
293
+ }
294
+ const box = new Box(1, 1, (text) => theme.bg(background, text));
295
+ box.addChild(result);
296
+ return box;
297
+ }