pi-bro 0.19.1 → 0.19.3

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
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to pi-bro are documented here.
4
4
 
5
+ ## [0.19.3] - 2026-09-30
6
+
7
+ ### Fixed
8
+
9
+ - Send large Agy explain, Show, and BTW prompts over stdin instead of exceeding argument-size limits. Older Agy versions report an upgrade hint rather than silently falling back (#73).
10
+ - Retain newest turns and message tails when bounding Show and BTW context, preserving JSON message framing (#73).
11
+
12
+ ## [0.19.2] - 2026-09-30
13
+
14
+ ### Fixed
15
+
16
+ - Preserve Show's diagram controls, captured source, and steering when reopening and rerunning a result. Recreate missing temporary diagrams without a model call and retain the previous diagram after a failed rerun (#71).
17
+
5
18
  ## [0.19.1] - 2026-09-30
6
19
 
7
20
  ### Fixed
package/backend.ts CHANGED
@@ -211,8 +211,8 @@ export function parseBtwAgyLine(line: string): { delta?: string; result?: string
211
211
  return { conversationId };
212
212
  }
213
213
 
214
- // explain/show and btw all print the prompt as a single --print argv value and read a stream-json
215
- // result off stdout; only sandbox flag, cwd, timeout, and (btw only) --conversation differ.
214
+ // Explain/show and BTW use argv for small prompts and stdin NDJSON for large
215
+ // prompts; sandbox, cwd, timeout, and continuation semantics stay unchanged.
216
216
  async function executeArgvPrint(
217
217
  request: BackendRequest,
218
218
  selection: AgySelection,
@@ -221,6 +221,9 @@ async function executeArgvPrint(
221
221
  killEscalationMs: number,
222
222
  deadlineMsOverride: number | undefined,
223
223
  ): Promise<BackendOutcome> {
224
+ // Keep legacy CLI compatibility for small prompts; large inputs use the advisor's
225
+ // stdin protocol rather than risking Linux's per-argument byte limit.
226
+ const stdinPrompt = Buffer.byteLength(request.prompt, "utf8") >= 120_000;
224
227
  const isBtw = request.feature === "btw";
225
228
  const full = isBtw && request.access === "workspace-full";
226
229
  const deadlineMs = deadlineMsOverride ?? (isBtw ? (full ? 610_000 : 130_000) : 125_000);
@@ -251,12 +254,11 @@ async function executeArgvPrint(
251
254
  "--print-timeout",
252
255
  printTimeout,
253
256
  ...(request.continuation ? ["--conversation", request.continuation.id] : []),
254
- "--print",
255
- request.prompt,
257
+ ...(stdinPrompt ? ["--input-format", "stream-json"] : ["--print", request.prompt]),
256
258
  ],
257
259
  {
258
260
  cwd: full ? request.cwd : runDirectory,
259
- stdio: ["ignore", "pipe", "pipe"],
261
+ stdio: [stdinPrompt ? "pipe" : "ignore", "pipe", "pipe"],
260
262
  windowsHide: true,
261
263
  detached: process.platform !== "win32",
262
264
  },
@@ -278,6 +280,11 @@ async function executeArgvPrint(
278
280
  processError = error;
279
281
  });
280
282
 
283
+ if (stdinPrompt) {
284
+ child.stdin?.on("error", () => { /* Report early exits through the close/error path. */ });
285
+ child.stdin?.end(`${JSON.stringify({ event: "user", message: { content: request.prompt } })}\n`);
286
+ }
287
+
281
288
  const lines = createInterface({ input: child.stdout!, crlfDelay: Infinity });
282
289
  try {
283
290
  for await (const line of lines) {
@@ -327,7 +334,9 @@ async function executeArgvPrint(
327
334
  if (exitSignal || code === null) {
328
335
  return { status: "failure", message: unexpectedSignalMessage(exitSignal), partialText: partial || undefined };
329
336
  }
330
- if (code !== 0) return { status: "failure", message: agyFailureMessage(action, { code, killed: false, stderr }) };
337
+ if (code !== 0) return { status: "failure", message: stdinPrompt && advisorFlagErrorHint(stderr)
338
+ ? withDoctor("Large prompts require Agy 1.1.15+ with stdin support. Run `agy update`. " + stderr.trim())
339
+ : agyFailureMessage(action, { code, killed: false, stderr }) };
331
340
 
332
341
  const text = final.trim();
333
342
  if (!text) return { status: "failure", message: withDoctor(stderr.trim() || emptyTextMessage) };
package/bro.ts CHANGED
@@ -1475,7 +1475,8 @@ function serializeShowTurns(turns: readonly ShowTurn[]): string {
1475
1475
  return turns.flatMap((turn) => turn.entries).join("\n\n");
1476
1476
  }
1477
1477
 
1478
- export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequested: number): BroSource | undefined {
1478
+ export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequested: number, maxLength = MAX_TEXT_LENGTH): BroSource | undefined {
1479
+ if (!Number.isSafeInteger(maxLength) || maxLength < 128) throw new Error("Transcript limit must be an integer of at least 128 characters.");
1479
1480
  const turns: ShowTurn[] = [];
1480
1481
  for (const entry of ctx.sessionManager.getBranch()) {
1481
1482
  if (entry.type !== "message") continue;
@@ -1503,7 +1504,7 @@ export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequest
1503
1504
  if (seen === 0) return undefined;
1504
1505
 
1505
1506
  let text = serializeShowTurns(turns.slice(start));
1506
- while (text.length > MAX_TEXT_LENGTH && start < turns.length - 1) {
1507
+ while (text.length > maxLength && start < turns.length - 1) {
1507
1508
  let next = turns.length;
1508
1509
  for (let index = start + 1; index < turns.length; index += 1) {
1509
1510
  if (turns[index]!.startsTurn) {
@@ -1515,7 +1516,22 @@ export function captureShowTranscript(ctx: ExtensionCommandContext, turnsRequest
1515
1516
  start = next;
1516
1517
  text = serializeShowTurns(turns.slice(start));
1517
1518
  }
1518
- if (text.length > MAX_TEXT_LENGTH) text = `${text.slice(0, MAX_TEXT_LENGTH)}\n[… transcript truncated …]`;
1519
+ if (text.length > maxLength) {
1520
+ // Prefer the newest complete messages even within an oversized single turn.
1521
+ const entries = turns.slice(start).flatMap(turn => turn.entries);
1522
+ const notice = "[… earlier messages truncated …]\n";
1523
+ while (entries.length > 1 && entries.join("\n\n").length + notice.length > maxLength) entries.shift();
1524
+ text = entries.join("\n\n");
1525
+ if (text.length + notice.length > maxLength) {
1526
+ const newline = text.indexOf("\n");
1527
+ const header = text.slice(0, newline);
1528
+ const content = JSON.parse(text.slice(newline + 1)) as string;
1529
+ const marker = "[… earlier text truncated …] ";
1530
+ let tail = content.slice(-(maxLength - header.length - marker.length - 8));
1531
+ while (`${header}\n${JSON.stringify(marker + tail)}`.length > maxLength) tail = tail.slice(Math.max(1, Math.floor(tail.length / 10)));
1532
+ text = `${header}\n${JSON.stringify(marker + tail)}`;
1533
+ } else text = notice + text;
1534
+ }
1519
1535
  const kept = turns.slice(start).filter((turn) => turn.startsTurn).length;
1520
1536
  return { text: text.trim(), label: `last ${Math.max(1, kept)} turn${kept > 1 ? "s" : ""} · conversation only` };
1521
1537
  }
@@ -2942,6 +2958,12 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
2942
2958
  options.retryLabel ?? "simplify again",
2943
2959
  );
2944
2960
 
2961
+ const present = (result: ModalResult, notice = "") => {
2962
+ const display = result.htmlPath ? stripShowHtmlFence(result.text) : result.text;
2963
+ modal.setResult(display, options.retryable ?? Boolean(options.run), notice, result.source?.label, result.text, result.model);
2964
+ modal.setHtmlPath(result.htmlPath ?? "");
2965
+ };
2966
+
2945
2967
  execute = (source?: BroSource) => {
2946
2968
  if (!options.run || controller || closed) return;
2947
2969
  const previous = current;
@@ -2958,16 +2980,14 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
2958
2980
  if (closed || nextController.signal.aborted) return;
2959
2981
  current = result;
2960
2982
  options.onResult?.(result);
2961
- const display = result.htmlPath ? stripShowHtmlFence(result.text) : result.text;
2962
- modal.setResult(display, options.retryable ?? true, "", result.source?.label, result.text, result.model);
2963
- if (result.htmlPath) modal.setHtmlPath(result.htmlPath);
2983
+ present(result);
2964
2984
  })
2965
2985
  .catch((error) => {
2966
2986
  if (closed || nextController.signal.aborted) return;
2967
2987
  const message = error instanceof Error ? error.message : String(error);
2968
2988
  if (previous) {
2969
2989
  current = previous;
2970
- modal.setResult(previous.text, options.retryable ?? true, `Retry failed: ${message}`, previous.source?.label, previous.text, previous.model);
2990
+ present(previous, `Retry failed: ${message}`);
2971
2991
  } else {
2972
2992
  modal.setError(message);
2973
2993
  }
@@ -2980,7 +3000,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
2980
3000
  if (options.text !== undefined) {
2981
3001
  modal.setStatic(options.kind ?? "help", options.text, options.copyable ?? false);
2982
3002
  } else if (current) {
2983
- modal.setResult(current.text, options.retryable ?? Boolean(options.run), "", current.source?.label, current.text, current.model);
3003
+ present(current);
2984
3004
  } else {
2985
3005
  execute();
2986
3006
  }
@@ -3318,10 +3338,7 @@ async function openBtwModal(
3318
3338
  }
3319
3339
  if (thread.turns.length === 0) {
3320
3340
  thread.conversationId = undefined;
3321
- let context = captureShowTranscript(ctx, BTW_CONTEXT_TURNS)?.text;
3322
- if (context && context.length > BTW_CONTEXT_MAX) {
3323
- context = `${context.slice(0, BTW_CONTEXT_MAX)}\n[… context truncated …]`;
3324
- }
3341
+ const context = captureShowTranscript(ctx, BTW_CONTEXT_TURNS, BTW_CONTEXT_MAX)?.text;
3325
3342
  thread.context = context;
3326
3343
  }
3327
3344
  // Resume natively when possible; otherwise seed the fresh native session with the main-session
@@ -3479,10 +3496,14 @@ async function openBtwModal(
3479
3496
  }
3480
3497
 
3481
3498
  export default async function bro(pi: ExtensionAPI) {
3482
- let lastResult: BroResult | undefined;
3499
+ let lastResult: ModalResult | undefined;
3500
+ let lastShowSteering: string | undefined;
3483
3501
  let btwThread: BtwThread | undefined;
3484
- const remember = (result: ModalResult) => {
3485
- if (result.source) lastResult = { source: result.source, text: result.text, model: result.model };
3502
+ const remember = (result: ModalResult, showSteering?: string) => {
3503
+ if (result.source) {
3504
+ lastResult = result;
3505
+ lastShowSteering = showSteering;
3506
+ }
3486
3507
  };
3487
3508
 
3488
3509
  pi.on("session_start", async (_event, _ctx) => {
@@ -3659,7 +3680,7 @@ export default async function bro(pi: ExtensionAPI) {
3659
3680
  loadingText: "Drawing what happened…",
3660
3681
  retryLabel: "show again",
3661
3682
  run: runShow,
3662
- onResult: remember,
3683
+ onResult: (result) => remember(result, steering),
3663
3684
  });
3664
3685
  } catch (error) {
3665
3686
  ctx.ui.notify(withDoctor(error), "error");
@@ -3860,10 +3881,29 @@ export default async function bro(pi: ExtensionAPI) {
3860
3881
  return;
3861
3882
  }
3862
3883
 
3884
+ const steering = lastShowSteering;
3885
+ if (steering !== undefined) {
3886
+ const html = extractShowHtml(lastResult.text);
3887
+ // Temp files may have been cleaned by the OS or another Bro session.
3888
+ if (html && ctx.mode === "tui") {
3889
+ try { lastResult.htmlPath = await writeShowHtml(html); }
3890
+ catch (error) {
3891
+ lastResult.htmlPath = undefined;
3892
+ ctx.ui.notify(`Diagram file unavailable: ${errorMessage(error)}`, "warning");
3893
+ }
3894
+ }
3895
+ }
3863
3896
  await showBroModal(ctx, {
3864
3897
  result: lastResult,
3865
- run,
3866
- onResult: remember,
3898
+ loadingText: steering !== undefined ? "Drawing what happened…" : undefined,
3899
+ retryLabel: steering !== undefined ? "show again" : undefined,
3900
+ run: steering === undefined ? run : async (signal, source, onProgress) => {
3901
+ if (!source) throw new Error("No captured Show source.");
3902
+ const result = await runShowExplanation(source.text, steering, signal, await readSettings(), onProgress);
3903
+ const html = extractShowHtml(result.text);
3904
+ return { source, ...result, ...(html ? { htmlPath: await writeShowHtml(html) } : {}) };
3905
+ },
3906
+ onResult: (result) => remember(result, steering),
3867
3907
  });
3868
3908
  return;
3869
3909
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.19.1",
3
+ "version": "0.19.3",
4
4
  "description": "An Earendil Pi extension that explains pasted text, assistant responses, local documents, public webpages, and recent session turns (as shapes) in a context-isolated window, opens a sandboxed side conversation with /bro btw, and provides a second-opinion advisor tool for executor agents.",
5
5
  "type": "module",
6
6
  "license": "MIT",