pi-better-harness 0.13.1 → 0.15.0

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/README.md CHANGED
@@ -63,11 +63,11 @@ need interactive confirmation. Full policy: [pi-better-sandbox](https://github.c
63
63
  ## Minimal Tool Output
64
64
 
65
65
  The bundled harness also provides `/tool-output minimal`, `/tool-output normal`,
66
- and `/tool-output` (toggle). Minimal mode hides every collapsed result body,
67
- including built-in, extension, and MCP text/images, without changing execution,
68
- sandboxing, or agent-facing payloads. Tool headers and error coloring remain;
69
- Ctrl+O reveals results, and fullscreen Pi versions with clickable tool rows
70
- also support clicking headers.
66
+ and `/tool-output` (toggle). Minimal mode leaves the current tool run visible,
67
+ then folds every finished run between model texts into one line. Built-in,
68
+ extension, and MCP calls are included. Execution, sandboxing, and agent-facing
69
+ payloads are unchanged. Ctrl+O unfolds runs and reveals results. Clickable Pi
70
+ builds can also open one folded run by clicking its summary.
71
71
 
72
72
  Normal mode is the default. The preference is saved in the current session,
73
73
  including resume/reload. This is a version-sensitive internal TUI adapter,
@@ -7,9 +7,13 @@ interface ToolComponent {
7
7
  expanded: boolean;
8
8
  showImages: boolean;
9
9
  resultRendererComponent?: unknown;
10
- ui: { requestRender(): void };
10
+ toolName?: string;
11
+ toolCallId?: string;
12
+ result?: { isError?: boolean };
13
+ ui: { requestRender(): void; children?: ChatContainer[] };
11
14
  invalidate(): void;
12
15
  setExpanded(expanded: boolean): void;
16
+ render(width: number): string[];
13
17
  }
14
18
 
15
19
  type Method = (this: ToolComponent, ...args: unknown[]) => unknown;
@@ -21,36 +25,83 @@ interface ToolPrototype {
21
25
  invalidate: Method;
22
26
  [HOOK]?: HookState;
23
27
  }
28
+ interface ChatContainer {
29
+ children: unknown[];
30
+ render: (width: number) => string[];
31
+ mouseLayout?: { width: number; children: Array<{ component: unknown; height: number }> };
32
+ [CHAT]?: { original: (width: number) => string[]; wrapper: (width: number) => string[] };
33
+ }
34
+ interface ContainerPrototype {
35
+ addChild: (child: unknown, ...rest: unknown[]) => unknown;
36
+ render: (width: number) => string[];
37
+ [ADD]?: { original: ContainerPrototype["addChild"]; wrapper: ContainerPrototype["addChild"] };
38
+ }
24
39
  interface HookState {
25
40
  enabled: boolean;
26
41
  owners: number;
42
+ foldsTurns: boolean;
43
+ closeRuns(): void;
27
44
  redraw(collapse?: boolean): void;
28
45
  restore(): void;
46
+ installContainer(prototype: ContainerPrototype): void;
29
47
  }
30
48
  export interface MinimalOutputHook {
31
49
  setEnabled(enabled: boolean): void;
32
50
  dispose(): void;
51
+ readonly foldsTurns: boolean;
52
+ }
53
+ interface FoldSummary {
54
+ render(width: number): string[];
55
+ invalidate(): void;
56
+ handleMouse(event: { type?: string; button?: string }): { handled: true } | undefined;
57
+ setTools(tools: ToolComponent[]): void;
33
58
  }
34
59
 
35
60
  const HOOK = Symbol.for("pi-better-harness.minimal-output-hook");
61
+ const CHAT = Symbol.for("pi-better-harness.minimal-output-chat");
62
+ const ADD = Symbol.for("pi-better-harness.minimal-output-add-child");
36
63
  const emptyResult = () => ({ render: () => [] as string[], invalidate() {} });
37
64
 
38
- /** Resolve the running host's SDK, not a second SDK bundled beside the extension. */
39
- export async function loadToolPrototype(): Promise<ToolPrototype> {
65
+ function hostRequire(): NodeRequire {
40
66
  let require = createRequire(import.meta.url);
41
67
  if (process.argv[1]) {
42
68
  try {
43
- const hostRequire = createRequire(realpathSync(process.argv[1]));
44
- sdkEntry(hostRequire);
45
- require = hostRequire;
69
+ const candidate = createRequire(realpathSync(process.argv[1]));
70
+ sdkEntry(candidate);
71
+ require = candidate;
46
72
  } catch { /* SDK embedding or a test runner: use the extension's SDK. */ }
47
73
  }
48
- const entry = sdkEntry(require);
74
+ return require;
75
+ }
76
+
77
+ /** Resolve the running host's SDK, not a second SDK bundled beside the extension. */
78
+ export async function loadToolPrototype(): Promise<ToolPrototype> {
79
+ const entry = sdkEntry(hostRequire());
49
80
  const path = join(dirname(entry), "modes/interactive/components/tool-execution.js");
50
81
  const module = await import(pathToFileURL(path).href);
51
82
  return module.ToolExecutionComponent.prototype;
52
83
  }
53
84
 
85
+ /** Chat rows are Container children. Patch that host class, not a second copy. */
86
+ export async function loadContainerPrototype(): Promise<ContainerPrototype> {
87
+ const entry = sdkEntry(hostRequire());
88
+ const tuiPath = createRequire(entry).resolve("@earendil-works/pi-tui");
89
+ const tui = await import(pathToFileURL(tuiPath).href);
90
+ const prototype = tui.Container?.prototype;
91
+ if (typeof prototype?.addChild !== "function" || typeof prototype?.render !== "function") {
92
+ throw new Error("Pi's chat container API is incompatible: missing Container.addChild. Turn folding is unavailable.");
93
+ }
94
+ return prototype;
95
+ }
96
+
97
+ export async function loadMutedText(): Promise<(text: string) => string> {
98
+ const entry = sdkEntry(hostRequire());
99
+ const theme = await import(pathToFileURL(join(dirname(entry), "modes/interactive/theme/theme.js")).href);
100
+ return (text) => {
101
+ try { return theme.theme.fg("muted", text); } catch { return text; }
102
+ };
103
+ }
104
+
54
105
  // Pi's ESM-only export cannot be resolved with require.resolve on older SDKs.
55
106
  function sdkEntry(require: ReturnType<typeof createRequire>): string {
56
107
  for (const directory of require.resolve.paths("@earendil-works/pi-coding-agent") ?? []) {
@@ -64,8 +115,37 @@ function sdkEntry(require: ReturnType<typeof createRequire>): string {
64
115
  throw new Error("Could not locate the running Pi SDK. Normal output remains enabled.");
65
116
  }
66
117
 
118
+ function hasModelText(component: unknown): boolean {
119
+ const content = (component as { lastMessage?: { content?: unknown } } | null)?.lastMessage?.content;
120
+ if (!Array.isArray(content)) return false;
121
+ return content.some((block) => block?.type === "text" && typeof block.text === "string" && block.text.trim().length > 0);
122
+ }
123
+
124
+ function formatRun(tools: ToolComponent[], open: boolean): string {
125
+ const counts = new Map<string, number>();
126
+ let errors = 0;
127
+ for (const tool of tools) {
128
+ const name = tool.toolName || "tool";
129
+ counts.set(name, (counts.get(name) ?? 0) + 1);
130
+ if (tool.result?.isError) errors += 1;
131
+ }
132
+ const names = [...counts].map(([name, count]) => count > 1 ? `${name} ×${count}` : name);
133
+ const noun = tools.length === 1 ? "tool" : "tools";
134
+ const error = errors === 0 ? "" : errors === 1 ? " · 1 error" : ` · ${errors} errors`;
135
+ return `${open ? "▾" : "▸"} ${tools.length} ${noun}${error} · ${names.join(", ")}`;
136
+ }
137
+
138
+ function clip(text: string, width: number): string {
139
+ if (width <= 0 || text.length <= width) return text;
140
+ return width === 1 ? "…" : `${text.slice(0, width - 1)}…`;
141
+ }
142
+
67
143
  /** Internal TUI adapter: no tools are replaced and no result content is changed. */
68
- export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutputHook {
144
+ export function installMinimalOutputHook(
145
+ prototype: ToolPrototype,
146
+ containerPrototype?: ContainerPrototype,
147
+ muted: (text: string) => string = (text) => text,
148
+ ): MinimalOutputHook {
69
149
  for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "invalidate"] as const) {
70
150
  if (typeof prototype[name] !== "function") {
71
151
  throw new Error(`Pi's tool display API is incompatible: missing ${name}. Normal output remains enabled.`);
@@ -80,18 +160,131 @@ export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutpu
80
160
  };
81
161
  const seen = new WeakSet<ToolComponent>();
82
162
  const components = new Set<WeakRef<ToolComponent>>();
163
+ const chats = new Set<WeakRef<ChatContainer>>();
164
+ const summaries = new WeakMap<ToolComponent, FoldSummary>();
165
+ const openedAt = new WeakMap<ToolComponent, number>();
166
+ let generation = 0;
167
+ let containerHooked = false;
168
+ let installedContainer: ContainerPrototype | undefined;
169
+ const isOpen = (tool: ToolComponent) => openedAt.get(tool) === generation;
170
+ const laterModelText = (children: unknown[], index: number) => {
171
+ for (let i = index; i < children.length; i++) {
172
+ if (hasModelText(children[i])) return true;
173
+ }
174
+ return false;
175
+ };
176
+ const attachFromTool = (tool: ToolComponent) => {
177
+ for (const child of tool.ui?.children ?? []) {
178
+ if (child?.children?.includes(tool)) wrapChat(child);
179
+ }
180
+ };
181
+ const wrapChat = (container: ChatContainer) => {
182
+ if (!container || seen.has(container as unknown as ToolComponent) || container[CHAT] || typeof container.render !== "function") return;
183
+ const original = container.render;
184
+ const wrapper = function (this: ChatContainer, width: number) {
185
+ if (!current.enabled || !Array.isArray(this.children) || !needsFold(this.children)) return original.call(this, width);
186
+ const lines: string[] = [];
187
+ const mouseChildren: Array<{ component: unknown; height: number }> = [];
188
+ const push = (component: { render(width: number): string[] }) => {
189
+ const childLines = component.render(width) ?? [];
190
+ mouseChildren.push({ component, height: childLines.length });
191
+ lines.push(...childLines);
192
+ };
193
+ let index = 0;
194
+ while (index < this.children.length) {
195
+ const child = this.children[index];
196
+ if (!seen.has(child as ToolComponent)) {
197
+ push(child as { render(width: number): string[] });
198
+ index += 1;
199
+ continue;
200
+ }
201
+ let end = index;
202
+ while (end < this.children.length && seen.has(this.children[end] as ToolComponent)) end += 1;
203
+ const tools = this.children.slice(index, end) as ToolComponent[];
204
+ const closed = laterModelText(this.children, end) && tools.every((tool) => !tool.expanded);
205
+ if (!closed) {
206
+ for (const tool of tools) push(tool);
207
+ } else {
208
+ const open = isOpen(tools[0]!);
209
+ let summary = summaries.get(tools[0]!);
210
+ if (!summary) {
211
+ summary = createSummary(tools[0]!);
212
+ summaries.set(tools[0]!, summary);
213
+ }
214
+ summary.setTools(tools);
215
+ push(summary);
216
+ if (open) for (const tool of tools) push(tool);
217
+ }
218
+ index = end;
219
+ }
220
+ this.mouseLayout = { width, children: mouseChildren };
221
+ return lines;
222
+ };
223
+ container[CHAT] = { original, wrapper };
224
+ container.render = wrapper;
225
+ chats.add(new WeakRef(container));
226
+ };
227
+ const createSummary = (anchor: ToolComponent): FoldSummary => {
228
+ let tools = [anchor];
229
+ return {
230
+ setTools(next) { tools = next; },
231
+ invalidate() {},
232
+ render(width) {
233
+ return ["", muted(clip(formatRun(tools, isOpen(anchor)), width))];
234
+ },
235
+ handleMouse(event) {
236
+ if (event.type !== "click" || (event.button && event.button !== "left")) return undefined;
237
+ if (isOpen(anchor)) openedAt.delete(anchor);
238
+ else openedAt.set(anchor, generation);
239
+ anchor.ui.requestRender();
240
+ return { handled: true };
241
+ },
242
+ };
243
+ };
244
+ const needsFold = (children: unknown[]) => {
245
+ let index = 0;
246
+ while (index < children.length) {
247
+ if (!seen.has(children[index] as ToolComponent)) { index += 1; continue; }
248
+ let end = index;
249
+ while (end < children.length && seen.has(children[end] as ToolComponent)) end += 1;
250
+ const tools = children.slice(index, end) as ToolComponent[];
251
+ if (laterModelText(children, end) && tools.every((tool) => !tool.expanded)) return true;
252
+ index = end;
253
+ }
254
+ return false;
255
+ };
83
256
  state = {
84
257
  enabled: false,
85
258
  owners: 0,
259
+ foldsTurns: false,
260
+ closeRuns() { generation += 1; },
86
261
  redraw(collapse = false) {
87
262
  for (const reference of components) {
88
263
  const component = reference.deref();
89
264
  if (!component) { components.delete(reference); continue; }
90
265
  if (collapse) component.setExpanded(false);
91
266
  else component.invalidate();
267
+ attachFromTool(component);
92
268
  component.ui.requestRender();
93
269
  }
94
270
  },
271
+ installContainer(next) {
272
+ if (containerHooked) return;
273
+ if (typeof next.addChild !== "function") {
274
+ throw new Error("Pi's chat container API is incompatible: missing Container.addChild. Turn folding is unavailable.");
275
+ }
276
+ const original = next[ADD]?.original ?? next.addChild;
277
+ const wrapper: ContainerPrototype["addChild"] = function (this: ChatContainer, child, ...rest) {
278
+ const result = original.call(this, child, ...rest);
279
+ if (seen.has(child as ToolComponent)) wrapChat(this);
280
+ return result;
281
+ };
282
+ next.addChild = wrapper;
283
+ next[ADD] = { original, wrapper };
284
+ containerHooked = true;
285
+ installedContainer = next;
286
+ state!.foldsTurns = true;
287
+ },
95
288
  restore() {
96
289
  for (const name of Object.keys(originals) as Array<keyof typeof originals>) {
97
290
  // Do not remove another extension's subsequently installed wrapper.
@@ -99,12 +292,23 @@ export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutpu
99
292
  }
100
293
  delete prototype[HOOK];
101
294
  components.clear();
295
+ if (installedContainer && installedContainer[ADD]?.wrapper === installedContainer.addChild) {
296
+ installedContainer.addChild = installedContainer[ADD].original;
297
+ delete installedContainer[ADD];
298
+ }
299
+ for (const reference of chats) {
300
+ const container = reference.deref();
301
+ if (container?.[CHAT] && container.render === container[CHAT].wrapper) container.render = container[CHAT].original;
302
+ if (container) delete container[CHAT];
303
+ }
304
+ chats.clear();
102
305
  },
103
306
  };
104
307
  const current = state;
105
308
  const wrappers = {
106
309
  updateDisplay(this: ToolComponent, ...args: unknown[]) {
107
310
  if (!seen.has(this)) { seen.add(this); components.add(new WeakRef(this)); }
311
+ attachFromTool(this);
108
312
  if (!current.enabled || this.expanded) return originals.updateDisplay.apply(this, args);
109
313
  const showImages = this.showImages;
110
314
  const lastRenderer = this.resultRendererComponent;
@@ -128,12 +332,15 @@ export function installMinimalOutputHook(prototype: ToolPrototype): MinimalOutpu
128
332
  prototype[HOOK] = state;
129
333
  }
130
334
  const current = state;
335
+ if (containerPrototype) current.installContainer(containerPrototype);
131
336
  current.owners += 1;
132
337
  let disposed = false;
133
338
  return {
339
+ foldsTurns: current.foldsTurns,
134
340
  setEnabled(enabled) {
135
341
  if (disposed) return;
136
342
  current.enabled = enabled;
343
+ if (enabled) current.closeRuns();
137
344
  current.redraw(enabled);
138
345
  },
139
346
  dispose() {
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { installMinimalOutputHook, loadToolPrototype, type MinimalOutputHook } from "./hook.ts";
2
+ import { installMinimalOutputHook, loadContainerPrototype, loadMutedText, loadToolPrototype, type MinimalOutputHook } from "./hook.ts";
3
3
 
4
4
  const ENTRY = "pi-better-harness-tool-output";
5
5
 
@@ -24,7 +24,12 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
24
24
  }
25
25
  if (hook) return true;
26
26
  try {
27
- hook = installMinimalOutputHook(await loadToolPrototype());
27
+ const [toolPrototype, containerPrototype, muted] = await Promise.all([
28
+ loadToolPrototype(),
29
+ loadContainerPrototype().catch(() => undefined),
30
+ loadMutedText().catch(() => undefined),
31
+ ]);
32
+ hook = installMinimalOutputHook(toolPrototype, containerPrototype, muted);
28
33
  return true;
29
34
  } catch (error) {
30
35
  ctx.ui.notify(`Minimal tool output is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
@@ -56,11 +61,11 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
56
61
  });
57
62
 
58
63
  pi.registerCommand("tool-output", {
59
- description: "Tool output: minimal | normal (no argument toggles). Display only; agent results are unchanged.",
64
+ description: "[minimal|normal] — Fold finished tool runs, or restore normal output. No argument toggles. Display only.",
60
65
  getArgumentCompletions: (argumentPrefix) => {
61
66
  const prefix = argumentPrefix.trimStart().toLowerCase();
62
67
  const matches = [
63
- { value: "minimal", label: "minimal", description: "Hide collapsed tool result bodies" },
68
+ { value: "minimal", label: "minimal", description: "Fold finished tool runs after the next model text" },
64
69
  { value: "normal", label: "normal", description: "Restore ordinary tool output" },
65
70
  ].filter((option) => option.value.startsWith(prefix));
66
71
  return matches.length > 0 ? matches : null;
@@ -78,7 +83,9 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
78
83
  pi.appendEntry(ENTRY, { version: 1, enabled });
79
84
  status(ctx);
80
85
  ctx.ui.notify(enabled
81
- ? "Minimal tool output on. Click a tool in fullscreen mode or use Ctrl+O to reveal results."
86
+ ? hook!.foldsTurns
87
+ ? "Minimal tool output on. Finished tool runs fold after the next model text. Ctrl+O reveals them; click a folded run when the terminal supports it."
88
+ : "Minimal tool output on, but turn folding is unavailable in this Pi version. Ctrl+O reveals results."
82
89
  : "Normal tool output restored.", "info");
83
90
  },
84
91
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-background-tasks",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
4
4
  "description": "Pi extension for durable background shell tasks, watchers, logs, and status inspection.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -66,6 +66,8 @@ export type BackgroundWorkProvider = {
66
66
  detail(id: string, now: number, options?: { logTailLines?: number }): BackgroundWorkDetail | null;
67
67
  armCloseLabel(row: BackgroundWorkRow): string;
68
68
  close(id: string): BackgroundWorkCloseOutcome;
69
+ /** Return true when this detail handled the key. The navigator refreshes the sheet. */
70
+ handleDetailInput?(id: string, data: string): boolean;
69
71
  onVisibleChanged?(notify: () => void): () => void;
70
72
  };
71
73
 
@@ -559,7 +561,7 @@ function rowSummary(row: InternalRow): string {
559
561
  const parts: string[] = [];
560
562
  if (row.statusTone !== "running" && row.kind !== "main agent") parts.push(singleLine(row.status));
561
563
  if (row.model) parts.push(row.effort ? `${singleLine(row.model)} ${singleLine(row.effort)}` : singleLine(row.model));
562
- if (row.tool) parts.push(`tool ${singleLine(row.tool)}`);
564
+ if (row.tool) parts.push(singleLine(row.tool));
563
565
  if (row.tokens) parts.push(singleLine(row.tokens));
564
566
  else if (row.primary) parts.push(singleLine(row.primary));
565
567
  if (parts.length) return parts.join(" · ");
@@ -703,12 +705,7 @@ function handleMainListInput(data: string, deps: HostDeps): boolean {
703
705
  return true;
704
706
  }
705
707
  if (deps.matchKey(data, "enter")) {
706
- const selected = selectedMainListRow();
707
- if (selected?.parentRow) {
708
- unfocusMainList();
709
- dismissOverlay();
710
- }
711
- else openNavigator();
708
+ openNavigator();
712
709
  return true;
713
710
  }
714
711
  if (data === "x" || data === "X" || deps.matchKey(data, "x") || deps.matchKey(data, "X")) {
@@ -851,7 +848,8 @@ function createOverlayComponent(
851
848
  let logTailRows: number = DEFAULT_LOG_TAIL_ROWS;
852
849
  const expandedSections = new Set<string>();
853
850
  let detailId: string = initialDetailId;
854
- let detail: BackgroundWorkDetail | null = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? null;
851
+ const initialRow = initialRows.find((row) => row.navigatorId === detailId);
852
+ let detail: BackgroundWorkDetail | null = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? (initialRow ? fallbackDetail(initialRow) : null);
855
853
  applyDefaultExpandedSections(detail, expandedSections);
856
854
  {
857
855
  const idx = overlayState.rows.findIndex((row) => row.navigatorId === detailId);
@@ -891,8 +889,7 @@ function createOverlayComponent(
891
889
  }
892
890
 
893
891
  function activateSelectedRow(): void {
894
- if (selectedRow()?.parentRow) close();
895
- else openDetail();
892
+ openDetail();
896
893
  }
897
894
 
898
895
  function clearCloseArm(): void {
@@ -922,10 +919,6 @@ function createOverlayComponent(
922
919
  function openDetail(): void {
923
920
  const row = selectedRow();
924
921
  if (!row) return;
925
- if (row.parentRow) {
926
- close();
927
- return;
928
- }
929
922
  clearCloseArm();
930
923
  detailId = row.navigatorId;
931
924
  expandedSections.clear();
@@ -1073,6 +1066,15 @@ function createOverlayComponent(
1073
1066
  requestRender();
1074
1067
  }
1075
1068
  }
1069
+ else if (data === "m" || data === "M" || data === "\x13" || deps.matchKey(data, "ctrl+s")) {
1070
+ const row = selectedRow();
1071
+ const provider = row ? state().providers.get(row.providerId) : undefined;
1072
+ const key = data === "m" || data === "M" ? "m" : "ctrl+s";
1073
+ if (!row || !provider?.handleDetailInput?.(row.id, key)) return;
1074
+ detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
1075
+ refreshMainListWidget();
1076
+ requestRender();
1077
+ }
1076
1078
  else if (data === "l" || data === "L") {
1077
1079
  logTailRows = cycleLogTailRows(logTailRows);
1078
1080
  if (detailId) detail = detailFor(detailId, Date.now(), { logTailLines: logTailRows }) ?? detail;
@@ -1091,8 +1093,7 @@ function createOverlayComponent(
1091
1093
  if (idx < 0) return;
1092
1094
  overlayState.selected = idx;
1093
1095
  state().mainListSelectedId = navigatorId;
1094
- if (overlayState.rows[idx]!.parentRow) close();
1095
- else openDetail();
1096
+ openDetail();
1096
1097
  },
1097
1098
  /** Close the overlay (done), e.g. when the navigator is disposed. */
1098
1099
  dismiss() { close(); },
@@ -1150,7 +1151,11 @@ function buildTranscriptDetailLines(
1150
1151
  ...detail.metadata.map((item) => ` ${item.label.padEnd(8, " ").slice(0, 8)} ${item.value}`),
1151
1152
  ];
1152
1153
  const transcriptHeader = ["", dim(section(`transcript · latest ${tailRows} rows`, width), fg)];
1153
- if (detail.transcriptDiagnostic) transcriptHeader.push(` ${dim(detail.transcriptDiagnostic, fg)}`);
1154
+ if (detail.transcriptDiagnostic) {
1155
+ for (const diagnosticRow of detail.transcriptDiagnostic.split(/\r\n|\r|\n/)) {
1156
+ transcriptHeader.push(` ${dim(diagnosticRow, fg)}`);
1157
+ }
1158
+ }
1154
1159
  // Metadata always renders; the transcript tail takes only the rows left over (the tail size is a cap, not a guarantee).
1155
1160
  lines.push(...optionalMetadata);
1156
1161
  if (options.maxRows !== undefined) {
@@ -17,7 +17,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
17
17
  - Background work that finishes while an `ask_user_question` is pending is handed to the agent right after the answer.
18
18
  - A compact goal widget that does not replace Pi's footer.
19
19
  - Background activity tracking for subagents and other registered providers.
20
- - A progress-aware follow-up loop that holds after repeated identical outcomes.
20
+ - A progress-aware follow-up loop with ten identical no-progress retries and linear backoff (30s through 300s by default), followed by a hold recoverable with `/goal resume`. Delays saturate at Node's timer limit, and observed background drains reset progress before callback turns can cancel the wake. Pi's network retry policy remains independent.
21
21
  - An observable-progress stall state for active goals.
22
22
 
23
23
  ## Skill-Owned Workflows
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-goal",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Pi extension for goal tracking with background-aware continuation.",
5
5
  "license": "MIT",
6
6
  "type": "module",