pi-better-harness 0.27.0 → 0.27.1

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
@@ -79,6 +79,11 @@ images, boxes, and tool spacers are hidden. Built-in, extension, and MCP calls
79
79
  are included. Tools without a custom call renderer use a generic icon and a
80
80
  compact argument hint. Compaction summaries also collapse to one quiet row with
81
81
  the pre-compaction token count; Ctrl+O reveals their original summary.
82
+ Custom transcript messages from any extension also fold to a quiet disclosure
83
+ row with the message type and first content line. This includes background
84
+ completion batches and subagent health/timing alerts. Ctrl+O or a fullscreen
85
+ click reveals the original content and any custom renderer. Direct UI notices
86
+ are unchanged.
82
87
  Execution, sandboxing, and agent-facing payloads are unchanged.
83
88
  When a foreground run ends, each consecutive block of tool calls folds further
84
89
  into one disclosure row showing the call count and any failures. Only the failure
@@ -105,7 +110,7 @@ Normal mode is the initial default. Changing tool output saves the choice in
105
110
  global `settings.json` under `piBetterHarness.toolOutput`, as well as the current
106
111
  session. New sessions inherit it; resumed branches retain their own saved choice.
107
112
  This is a version-sensitive internal TUI adapter,
108
- tested with Pi 0.82.1, 0.99.1, and the bundled Pi 1.0.0 CLI; incompatible APIs
113
+ tested with Pi 0.82.1, 0.99.1, and the bundled Pi 1.0.4 CLI; incompatible APIs
109
114
  produce a warning and leave ordinary output enabled. Print/RPC output and
110
115
  exported transcripts are unchanged.
111
116
 
@@ -42,6 +42,18 @@ interface CompactionComponent extends Component {
42
42
  interface CompactionPrototype {
43
43
  render: CompactionComponent["render"];
44
44
  }
45
+ interface CustomMessageComponent extends Component {
46
+ _expanded: boolean;
47
+ outputPad?: number;
48
+ message: { customType: string; content: string | { type: string; text?: string }[] };
49
+ setExpanded(expanded: boolean): void;
50
+ }
51
+ interface CustomMessagePrototype {
52
+ render: CustomMessageComponent["render"];
53
+ setExpanded: CustomMessageComponent["setExpanded"];
54
+ invalidate(): void;
55
+ handleMouse?(event: ToolMouseEvent): unknown;
56
+ }
45
57
 
46
58
  type Method = (this: ToolComponent, ...args: unknown[]) => unknown;
47
59
  interface ToolMouseEvent { type: string; button: string; x: number; y: number; screenX: number; screenY: number; width: number; shift?: boolean; alt?: boolean; ctrl?: boolean }
@@ -229,6 +241,17 @@ export async function loadCompactionPrototype(): Promise<CompactionPrototype> {
229
241
  return module.CompactionSummaryMessageComponent.prototype;
230
242
  }
231
243
 
244
+ export async function loadCustomMessagePrototype(): Promise<CustomMessagePrototype> {
245
+ const entry = sdkEntry(hostRequire());
246
+ const path = bundledHostEntry(entry) ?? join(dirname(entry), "modes/interactive/components/custom-message.js");
247
+ const module = await import(pathToFileURL(path).href);
248
+ const prototype = module.CustomMessageComponent?.prototype;
249
+ if (!prototype || ["render", "setExpanded", "invalidate"].some(name => typeof prototype[name] !== "function")) {
250
+ throw new Error("Pi's runtime does not expose its expandable custom-message display class.");
251
+ }
252
+ return prototype;
253
+ }
254
+
232
255
  // Pi's ESM-only export cannot be resolved with require.resolve on older SDKs.
233
256
  function sdkEntry(require: ReturnType<typeof createRequire>): string {
234
257
  for (const directory of require.resolve.paths("@earendil-works/pi-coding-agent") ?? []) {
@@ -243,7 +266,7 @@ function sdkEntry(require: ReturnType<typeof createRequire>): string {
243
266
  }
244
267
 
245
268
  /** Internal TUI adapter: no tools are replaced and no result content is changed. */
246
- export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: () => Theme, compactionPrototype?: CompactionPrototype): MinimalOutputHook {
269
+ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: () => Theme, compactionPrototype?: CompactionPrototype, customMessagePrototype?: CustomMessagePrototype): MinimalOutputHook {
247
270
  for (const name of ["updateDisplay", "getResultRenderer", "getTextOutput", "setExpanded", "invalidate", "render"] as const) {
248
271
  if (typeof prototype[name] !== "function") {
249
272
  throw new Error(`Pi's tool display API is incompatible: missing ${name}. Normal output remains enabled.`);
@@ -409,6 +432,60 @@ export function installMinimalOutputHook(prototype: ToolPrototype, getTheme?: ()
409
432
  restore.call(this);
410
433
  };
411
434
  }
435
+ if (customMessagePrototype) {
436
+ const originalRender = customMessagePrototype.render;
437
+ const originalMouse = customMessagePrototype.handleMouse;
438
+ const renderDescriptor = Object.getOwnPropertyDescriptor(customMessagePrototype, "render");
439
+ const mouseDescriptor = Object.getOwnPropertyDescriptor(customMessagePrototype, "handleMouse");
440
+ const messages = new Set<WeakRef<CustomMessageComponent>>();
441
+ const seenMessages = new WeakSet<CustomMessageComponent>();
442
+ const render = function(this: CustomMessageComponent, width: number): string[] {
443
+ if (!seenMessages.has(this)) { seenMessages.add(this); messages.add(new WeakRef(this)); }
444
+ if (!current.enabled || this._expanded) return originalRender.call(this, width);
445
+ if (width <= 0) return [];
446
+ const label = stripVTControlCharacters(this.message.customType).replace(/[-_\s]+/g, " ").trim().replace(/^./, letter => letter.toUpperCase());
447
+ const content = typeof this.message.content === "string" ? this.message.content
448
+ : this.message.content.filter(block => block.type === "text").map(block => block.text ?? "").join("\n");
449
+ const detail = stripVTControlCharacters(content).split(/\r?\n/).map(line => line.replace(/\s+/g, " ").trim()).find(Boolean) ?? "";
450
+ const theme = getTheme?.();
451
+ const text = theme
452
+ ? ` ${theme.fg("muted", "\u25b8")} ${theme.fg("muted", label)}${detail ? ` ${theme.fg("dim", detail)}` : ""}`
453
+ : ` \u25b8 ${label}${detail ? ` ${detail}` : ""}`;
454
+ const padding = this.outputPad ?? 1;
455
+ const indent = Number.isFinite(padding) ? Math.max(0, Math.floor(padding)) + 2 : 3;
456
+ return [truncateToWidth(`${" ".repeat(Math.min(indent, width - 1))}${text}`, width)];
457
+ };
458
+ const mouse = function(this: CustomMessageComponent, event: ToolMouseEvent): unknown {
459
+ if (current.enabled && !this._expanded && event.type === "click" && event.button === "left"
460
+ && event.y === 0 && !event.shift && !event.alt && !event.ctrl) {
461
+ this.setExpanded(true);
462
+ return { handled: true, render: true };
463
+ }
464
+ return originalMouse?.call(this, event);
465
+ };
466
+ customMessagePrototype.render = render;
467
+ customMessagePrototype.handleMouse = mouse;
468
+ const redraw = current.redraw;
469
+ current.redraw = function(collapse) {
470
+ for (const reference of messages) {
471
+ const component = reference.deref();
472
+ if (!component) { messages.delete(reference); continue; }
473
+ if (collapse) component.setExpanded(false);
474
+ else component.invalidate();
475
+ }
476
+ redraw.call(this, collapse);
477
+ };
478
+ const restore = current.restore;
479
+ current.restore = function() {
480
+ for (const [name, wrapper, descriptor] of [["render", render, renderDescriptor], ["handleMouse", mouse, mouseDescriptor]] as const) {
481
+ if (customMessagePrototype[name] !== wrapper) continue;
482
+ if (descriptor) Object.defineProperty(customMessagePrototype, name, descriptor);
483
+ else Reflect.deleteProperty(customMessagePrototype, name);
484
+ }
485
+ messages.clear();
486
+ restore.call(this);
487
+ };
488
+ }
412
489
  const wrappers = {
413
490
  updateDisplay(this: ToolComponent, ...args: unknown[]) {
414
491
  if (typeof this.ui.handleViewportInput === "function" && !inputWrappers.has(this.ui)) {
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { installMinimalOutputHook, loadToolPrototype, loadCompactionPrototype, type MinimalOutputHook } from "./hook.ts";
2
+ import { installMinimalOutputHook, loadToolPrototype, loadCompactionPrototype, loadCustomMessagePrototype, type MinimalOutputHook } from "./hook.ts";
3
3
  import type { SettingsControl } from "../settings/registry.ts";
4
4
  import { readHarnessSetting, updateHarnessSetting } from "../shared-harness-settings.ts";
5
5
 
@@ -37,7 +37,7 @@ export default function minimalOutputExtension(pi: ExtensionAPI): void {
37
37
  }
38
38
  if (hook) return true;
39
39
  try {
40
- hook = installMinimalOutputHook(await loadToolPrototype(), () => ctx.ui.theme, await loadCompactionPrototype());
40
+ hook = installMinimalOutputHook(await loadToolPrototype(), () => ctx.ui.theme, await loadCompactionPrototype(), await loadCustomMessagePrototype());
41
41
  return true;
42
42
  } catch (error) {
43
43
  ctx.ui.notify(`Minimal tool output is unavailable: ${error instanceof Error ? error.message : String(error)}`, "warning");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-better-harness",
3
- "version": "0.27.0",
3
+ "version": "0.27.1",
4
4
  "description": "Pi extension bundle for a write sandbox, subagents, background tasks, SSH, goals, and structured plans.",
5
5
  "license": "MIT",
6
6
  "type": "module",