pi-bro 0.1.1 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +15 -7
  2. package/bro.ts +238 -42
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -6,7 +6,7 @@ adding extra messages to your conversation context.
6
6
  `pi-bro` is a small extension for
7
7
  [Earendil Pi](https://github.com/earendil-works/pi). It uses the
8
8
  [Google Antigravity CLI](https://antigravity.google/docs/cli-install) (`agy`)
9
- and a Gemini model to create plain-language explanations.
9
+ and a Gemini model to stream plain-language explanations.
10
10
 
11
11
  ## Bro in action
12
12
 
@@ -265,7 +265,7 @@ cached files, not your source code or dependencies.
265
265
 
266
266
  - Earendil Pi `>=0.78.1 <1` (tested on `0.84.2`)
267
267
  - Node.js `>=22.19.0`
268
- - `agy` installed, authenticated, and on your `PATH` (tested on `1.1.13`)
268
+ - `agy >=1.1.8` installed, authenticated, and on your `PATH` (tested on `1.1.13`)
269
269
  - Pi's interactive terminal UI
270
270
 
271
271
  Run `agy` once in your terminal to complete sign-in before using Bro.
@@ -299,6 +299,8 @@ pi -e npm:pi-bro
299
299
  | `/bro` | Create a new plain-language explanation of the latest completed assistant response. |
300
300
  | `/bro simplify` | Same as `/bro`. |
301
301
  | `/bro open` | Reopen the latest explanation without calling the simplifier again. |
302
+ | `/bro usage` | Show current Agy resource limits. |
303
+ | `/bro usage --provider agy` | Same as `/bro usage`, with the provider stated explicitly. |
302
304
  | `/bro help` | Open the built-in guide. |
303
305
 
304
306
  ### Modal controls
@@ -341,14 +343,16 @@ PI_BRO_MODEL=gemini-3.7-flash-low pi
341
343
 
342
344
  - **External requests**: Bro sends the latest completed assistant response to
343
345
  Agy and its configured model provider.
346
+ - **Usage checks**: `/bro usage` checks your authenticated Agy limits without
347
+ sending an assistant response or running a model turn.
344
348
  - **Context isolation**: Bro does not add explanations to Pi's conversation
345
349
  history, session files, or main-agent context.
346
350
  - **Memory cache**: The latest explanation is stored only in process memory for
347
351
  `/bro open`. It clears when you switch Pi sessions, reload extensions, or quit
348
352
  Pi.
349
- - **File safety**: Bro does not modify project files. It runs Agy in plan and
350
- sandbox modes inside a temporary empty folder. This reduces project access,
351
- but it is not a security boundary.
353
+ - **File safety**: Bro does not modify project files. It runs Agy in sandbox
354
+ mode inside a temporary empty folder. This reduces project access, but it is
355
+ not a security boundary.
352
356
  - **Provider data**: Agy and your model provider may retain logs and request data
353
357
  according to their own settings and privacy policies.
354
358
  - **Clipboard**: Pressing **C** copies the text to your system clipboard, where
@@ -356,10 +360,14 @@ PI_BRO_MODEL=gemini-3.7-flash-low pi
356
360
 
357
361
  ## Current limits
358
362
 
359
- - Supports only Agy/Gemini in v0.1.
363
+ - Uses Agy as its only provider.
360
364
  - Keeps only the latest explanation in memory.
361
365
  - Does not store history or export directly to files.
362
- - Mouse scrolling is disabled; use the arrow keys to scroll and **C** to copy.
366
+ - Mouse-wheel and trackpad scrolling work in Pi's fullscreen mode
367
+ (`pi --tui-mode fullscreen`). In regular mode, use the arrow keys so Bro does
368
+ not interfere with your terminal's native text selection.
369
+ - In fullscreen mode, mouse text selection may visually extend outside the Bro
370
+ window. Press **C** to copy the full explanation instead.
363
371
 
364
372
  ## Development
365
373
 
package/bro.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { spawn } from "node:child_process";
1
2
  import { mkdtemp, readFile, rm } from "node:fs/promises";
2
3
  import { homedir, tmpdir } from "node:os";
3
4
  import { join } from "node:path";
5
+ import { createInterface } from "node:readline";
4
6
  import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
5
7
  import { copyToClipboard, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
6
8
  import { Markdown, matchesKey, truncateToWidth, visibleWidth, type Focusable } from "@earendil-works/pi-tui";
@@ -20,16 +22,78 @@ Quoted response as a JSON string:
20
22
 
21
23
  type Theme = ExtensionCommandContext["ui"]["theme"];
22
24
  type TuiLike = { requestRender(): void };
23
- type ModalKind = "loading" | "result" | "help" | "empty" | "error";
25
+ type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
24
26
  type AssistantSource = { id: string; text: string };
25
27
  type BroResult = { source: AssistantSource; text: string };
28
+ type ModalResult = { source?: AssistantSource; text: string };
29
+ type AgyEvent = {
30
+ event?: string;
31
+ step_update?: { step_type?: string; text_delta?: unknown };
32
+ result?: { status?: string; response?: unknown };
33
+ };
34
+
35
+ export function wheelDelta(data: string): number {
36
+ const match = /^\x1b\[<(\d+);\d+;\d+[Mm]$/.exec(data);
37
+ if (!match) return 0;
38
+ const button = Number.parseInt(match[1], 10);
39
+ if ((button & 64) === 0) return 0;
40
+ return (button & 3) === 0 ? -3 : (button & 3) === 1 ? 3 : 0;
41
+ }
26
42
 
27
43
  const COMMANDS = [
28
44
  { value: "simplify", label: "simplify", description: "Simplify the latest assistant response" },
29
45
  { value: "open", label: "open", description: "Reopen the last explanation" },
46
+ { value: "usage", label: "usage", description: "Show current Agy usage" },
30
47
  { value: "help", label: "help", description: "Learn what Bro does and what it can access" },
31
48
  ];
32
49
 
50
+ function isRecord(value: unknown): value is Record<string, unknown> {
51
+ return typeof value === "object" && value !== null;
52
+ }
53
+
54
+ export function formatAgyUsage(value: unknown): string {
55
+ if (!isRecord(value) || value.status !== "SUCCESS" || typeof value.response !== "string") {
56
+ throw new Error("Agy returned invalid usage data.");
57
+ }
58
+
59
+ const groups = new Map<string, string[]>();
60
+ for (const line of value.response.trim().split("\n")) {
61
+ const [group, limit, remaining, resetTime] = line.split("\t");
62
+ if (!group || !limit || !remaining) throw new Error("Agy returned invalid usage data.");
63
+ const reset = resetTime ? new Date(resetTime) : undefined;
64
+ const resetText = reset && !Number.isNaN(reset.getTime()) ? ` — resets ${reset.toLocaleString()}` : "";
65
+ const items = groups.get(group) ?? [];
66
+ items.push(`- **${limit}:** ${remaining}${resetText}`);
67
+ groups.set(group, items);
68
+ }
69
+ if (!groups.size) throw new Error("Agy returned no usage information.");
70
+
71
+ const sections = [...groups].map(([group, items]) => `## ${group}\n\n${items.join("\n")}`);
72
+ return `# Agy usage\n\n${sections.join("\n\n")}`;
73
+ }
74
+
75
+ async function checkAgyUsage(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
76
+ const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
77
+ try {
78
+ const result = await pi.exec(
79
+ "agy",
80
+ ["-p", "/usage", "--output-format", "json", "--print-timeout", "30s", "--sandbox"],
81
+ { cwd: runDirectory, signal, timeout: 35_000 },
82
+ );
83
+ if (signal.aborted) throw new Error("Canceled.");
84
+ if (result.killed) throw new Error("Agy usage check timed out.");
85
+ if (result.code !== 0) throw new Error(result.stderr.trim() || `Agy exited with code ${result.code}.`);
86
+ try {
87
+ return formatAgyUsage(JSON.parse(result.stdout));
88
+ } catch (error) {
89
+ if (error instanceof SyntaxError) throw new Error("Agy returned invalid usage data.");
90
+ throw error;
91
+ }
92
+ } finally {
93
+ await rm(runDirectory, { recursive: true, force: true });
94
+ }
95
+ }
96
+
33
97
  function latestAssistant(ctx: ExtensionCommandContext): AssistantSource | undefined {
34
98
  const branch = ctx.sessionManager.getBranch();
35
99
 
@@ -62,20 +126,49 @@ async function promptFor(response: string): Promise<string> {
62
126
  return parts.join(JSON.stringify(response));
63
127
  }
64
128
 
65
- async function simplify(pi: ExtensionAPI, response: string, signal: AbortSignal): Promise<string> {
129
+ function parseAgyLine(line: string): { delta?: string; result?: string } {
130
+ let event: AgyEvent;
131
+ try {
132
+ event = JSON.parse(line) as AgyEvent;
133
+ } catch {
134
+ throw new Error("Agy returned invalid streaming data.");
135
+ }
136
+
137
+ if (
138
+ event.event === "step_update" &&
139
+ event.step_update?.step_type === "agent_response" &&
140
+ typeof event.step_update.text_delta === "string"
141
+ ) {
142
+ return { delta: event.step_update.text_delta };
143
+ }
144
+
145
+ if (event.event === "result") {
146
+ if (event.result?.status !== "SUCCESS" || typeof event.result.response !== "string") {
147
+ throw new Error("Agy did not complete the explanation successfully.");
148
+ }
149
+ return { result: event.result.response };
150
+ }
151
+
152
+ return {};
153
+ }
154
+
155
+ async function simplify(
156
+ response: string,
157
+ signal: AbortSignal,
158
+ onProgress?: (text: string) => void,
159
+ ): Promise<string> {
66
160
  const prompt = await promptFor(response);
67
161
  const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
162
+ let updateTimer: ReturnType<typeof setTimeout> | undefined;
68
163
 
69
164
  try {
70
- const result = await pi.exec(
165
+ const child = spawn(
71
166
  "agy",
72
167
  [
73
- "--mode",
74
- "plan",
75
168
  "--sandbox",
76
169
  "--disable-slash-commands",
77
170
  "--output-format",
78
- "text",
171
+ "stream-json",
79
172
  "--model",
80
173
  MODEL,
81
174
  "--print-timeout",
@@ -83,17 +176,74 @@ async function simplify(pi: ExtensionAPI, response: string, signal: AbortSignal)
83
176
  "--print",
84
177
  prompt,
85
178
  ],
86
- { cwd: runDirectory, signal, timeout: 125_000 },
179
+ {
180
+ cwd: runDirectory,
181
+ signal,
182
+ timeout: 125_000,
183
+ stdio: ["ignore", "pipe", "pipe"],
184
+ windowsHide: true,
185
+ },
87
186
  );
88
187
 
89
- if (result.killed) throw new Error(signal.aborted ? "Canceled." : "Simplification timed out.");
90
- const text = result.stdout.trim();
91
- if (result.code !== 0 || !text) {
92
- throw new Error(result.stderr.trim() || "No explanation was generated.");
188
+ let processError: Error | undefined;
189
+ let stderr = "";
190
+ let partial = "";
191
+ let final = "";
192
+ let parseError: Error | undefined;
193
+
194
+ child.stderr.setEncoding("utf8");
195
+ child.stderr.on("data", (chunk: string) => {
196
+ stderr += chunk;
197
+ });
198
+ child.once("error", (error) => {
199
+ processError = error;
200
+ });
201
+
202
+ const closed = new Promise<{ code: number | null; exitSignal: NodeJS.Signals | null }>((resolve) => {
203
+ child.once("close", (code, exitSignal) => resolve({ code, exitSignal }));
204
+ });
205
+
206
+ const lines = createInterface({ input: child.stdout, crlfDelay: Infinity });
207
+ try {
208
+ for await (const line of lines) {
209
+ if (!line.trim()) continue;
210
+ try {
211
+ const event = parseAgyLine(line);
212
+ if (event.delta) {
213
+ partial += event.delta;
214
+ if (onProgress && !updateTimer) {
215
+ updateTimer = setTimeout(() => {
216
+ updateTimer = undefined;
217
+ if (!signal.aborted) onProgress(partial);
218
+ }, 75);
219
+ }
220
+ }
221
+ if (event.result !== undefined) final = event.result;
222
+ } catch (error) {
223
+ parseError = error instanceof Error ? error : new Error(String(error));
224
+ child.kill();
225
+ break;
226
+ }
227
+ }
228
+ } finally {
229
+ lines.close();
230
+ }
231
+
232
+ const { code, exitSignal } = await closed;
233
+ if (signal.aborted) throw new Error("Canceled.");
234
+ if (parseError) throw parseError;
235
+ if (processError) throw processError;
236
+ if (exitSignal || code === null) throw new Error("Simplification timed out.");
237
+ if (code !== 0) throw new Error(stderr.trim() || `Agy exited with code ${code}.`);
238
+
239
+ const text = final.trim();
240
+ if (!text) {
241
+ throw new Error(stderr.trim() || "Agy returned no final explanation.");
93
242
  }
94
243
 
95
244
  return text;
96
245
  } finally {
246
+ if (updateTimer) clearTimeout(updateTimer);
97
247
  await rm(runDirectory, { recursive: true, force: true });
98
248
  }
99
249
  }
@@ -107,23 +257,29 @@ Bro turns the latest completed assistant response into a clear, plain-language e
107
257
 
108
258
  - \`/bro\` or \`/bro simplify\` — create a new explanation
109
259
  - \`/bro open\` — reopen the last explanation
260
+ - \`/bro usage\` or \`/bro usage --provider agy\` — show current Agy usage
110
261
  - \`/bro help\` — show this guide
111
262
 
112
263
  ## Controls
113
264
 
114
- - **↑ / ↓** — scroll
265
+ - **Mouse wheel / trackpad** — scroll in Pi's fullscreen mode
266
+ - **↑ / ↓** — scroll in any mode
115
267
  - **C** — copy the full explanation
116
268
  - **R** — simplify the same response again
117
269
  - **Esc** — close the window, or cancel while Bro is working
118
270
 
271
+ Mouse text selection may extend outside the Bro window. Press **C** to copy the complete explanation instead.
272
+
119
273
  ## Privacy and file safety
120
274
 
121
- Bro does not modify your project files. It runs the simplifier in plan and sandbox modes inside a temporary empty folder. This reduces project access, but it is not a security boundary.
275
+ Bro does not modify your project files. It runs the simplifier in sandbox mode inside a temporary empty folder. This reduces project access, but it is not a security boundary.
122
276
 
123
277
  Bro does not add explanations to Pi's conversation history, session files, or main-agent context. The latest explanation is kept in process memory only so \`/bro open\` can reopen it. It is cleared when you change sessions, reload extensions, or exit Pi.
124
278
 
125
279
  Bro sends the assistant response to an external simplifier (currently Agy with a Gemini model). Agy and the model provider may retain request data or logs under their own policies.
126
280
 
281
+ \`/bro usage\` checks your authenticated Agy limits without sending an assistant response or running a model turn.
282
+
127
283
  Pressing **C** copies the explanation to your system clipboard, where your operating system or clipboard manager may retain it.
128
284
 
129
285
  ## Custom prompt
@@ -159,8 +315,12 @@ class BroModal implements Focusable {
159
315
  private readonly onDispose: () => void,
160
316
  ) {}
161
317
 
162
- setLoading(): void {
163
- this.setContent("loading", `**${LOADING_TEXT}**`, "", false, false);
318
+ setLoading(text = LOADING_TEXT): void {
319
+ this.setContent("loading", `**${text}**`, "", false, false);
320
+ }
321
+
322
+ setStreaming(text: string): void {
323
+ this.setContent("streaming", text, "", false, false);
164
324
  }
165
325
 
166
326
  setResult(text: string, retryable: boolean, notice = ""): void {
@@ -172,7 +332,7 @@ class BroModal implements Focusable {
172
332
  }
173
333
 
174
334
  setError(message: string): void {
175
- this.setContent("error", `# Bro could not simplify this\n\n${message}`, "", false, true);
335
+ this.setContent("error", `# Bro ran into a problem\n\n${message}`, "", false, true);
176
336
  }
177
337
 
178
338
  private setContent(
@@ -188,7 +348,7 @@ class BroModal implements Focusable {
188
348
  this.copyable = copyable;
189
349
  this.retryable = retryable;
190
350
  this.notice = notice;
191
- this.offset = 0;
351
+ if (kind !== "streaming") this.offset = 0;
192
352
  this.markdown.setText(text);
193
353
  this.tui.requestRender();
194
354
  }
@@ -211,7 +371,10 @@ class BroModal implements Focusable {
211
371
 
212
372
  private controls(): string {
213
373
  if (this.kind === "loading") return "Esc cancel";
214
- if (this.kind === "result") return "↑/↓ scroll · C copy · R simplify again · Esc close";
374
+ if (this.kind === "streaming") return "Simplifying… · ↑/↓ scroll · Esc cancel";
375
+ if (this.kind === "result") {
376
+ return `↑/↓ scroll · C copy${this.retryable ? " · R simplify again" : ""} · Esc close`;
377
+ }
215
378
  if (this.kind === "help") return "↑/↓ scroll · C copy · Esc close";
216
379
  if (this.kind === "error") return "R try again · Esc close";
217
380
  return "Esc close";
@@ -257,8 +420,8 @@ class BroModal implements Focusable {
257
420
  return;
258
421
  }
259
422
 
260
- if (matchesKey(data, "up") || matchesKey(data, "down")) {
261
- const delta = matchesKey(data, "up") ? -1 : 1;
423
+ const delta = wheelDelta(data) || (matchesKey(data, "up") ? -1 : matchesKey(data, "down") ? 1 : 0);
424
+ if (delta) {
262
425
  this.offset = Math.max(0, Math.min(this.offset + delta, this.maxOffset));
263
426
  this.notice = "";
264
427
  this.tui.requestRender();
@@ -302,9 +465,15 @@ interface BroModalOptions {
302
465
  text?: string;
303
466
  kind?: "help" | "empty";
304
467
  copyable?: boolean;
305
- result?: BroResult;
306
- run?: (signal: AbortSignal, source?: AssistantSource) => Promise<BroResult>;
307
- onResult?: (result: BroResult) => void;
468
+ result?: ModalResult;
469
+ run?: (
470
+ signal: AbortSignal,
471
+ source?: AssistantSource,
472
+ onProgress?: (text: string) => void,
473
+ ) => Promise<ModalResult>;
474
+ onResult?: (result: ModalResult) => void;
475
+ loadingText?: string;
476
+ retryable?: boolean;
308
477
  }
309
478
 
310
479
  async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptions): Promise<void> {
@@ -346,22 +515,25 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
346
515
  const previous = current;
347
516
  const nextController = new AbortController();
348
517
  controller = nextController;
349
- modal.setLoading();
518
+ modal.setLoading(options.loadingText);
350
519
 
351
520
  void options
352
- .run(nextController.signal, source)
521
+ .run(nextController.signal, source, (text) => {
522
+ if (closed || nextController.signal.aborted || controller !== nextController) return;
523
+ modal.setStreaming(text);
524
+ })
353
525
  .then((result) => {
354
526
  if (closed || nextController.signal.aborted) return;
355
527
  current = result;
356
528
  options.onResult?.(result);
357
- modal.setResult(result.text, true);
529
+ modal.setResult(result.text, options.retryable ?? true);
358
530
  })
359
531
  .catch((error) => {
360
532
  if (closed || nextController.signal.aborted) return;
361
533
  const message = error instanceof Error ? error.message : String(error);
362
534
  if (previous) {
363
535
  current = previous;
364
- modal.setResult(previous.text, true, `Retry failed: ${message}`);
536
+ modal.setResult(previous.text, options.retryable ?? true, `Retry failed: ${message}`);
365
537
  } else {
366
538
  modal.setError(message);
367
539
  }
@@ -374,7 +546,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
374
546
  if (options.text !== undefined) {
375
547
  modal.setStatic(options.kind ?? "help", options.text, options.copyable ?? false);
376
548
  } else if (current) {
377
- modal.setResult(current.text, Boolean(options.run));
549
+ modal.setResult(current.text, options.retryable ?? Boolean(options.run));
378
550
  } else {
379
551
  execute();
380
552
  }
@@ -396,36 +568,64 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
396
568
 
397
569
  export default function bro(pi: ExtensionAPI) {
398
570
  let lastResult: BroResult | undefined;
571
+ const remember = (result: ModalResult) => {
572
+ if (result.source) lastResult = { source: result.source, text: result.text };
573
+ };
399
574
 
400
575
  pi.on("session_start", async () => {
401
576
  lastResult = undefined;
402
577
  });
403
578
 
404
579
  pi.registerCommand("bro", {
405
- description: "Simplify, reopen, or learn about Bro explanations",
580
+ description: "Simplify responses, reopen explanations, or show Agy usage",
406
581
  getArgumentCompletions: (prefix) => {
407
582
  const normalized = prefix.trim().toLowerCase();
408
583
  const matches = COMMANDS.filter((command) => command.value.startsWith(normalized));
409
584
  return matches.length ? matches : null;
410
585
  },
411
586
  handler: async (args, ctx) => {
412
- const action = args.trim().toLowerCase();
413
- if (action === "help") {
587
+ const normalized = args.trim().toLowerCase();
588
+ const parts = normalized ? normalized.split(/\s+/) : [];
589
+ const action = parts[0] ?? "";
590
+
591
+ if (action === "usage") {
592
+ const valid = parts.length === 1 || (parts.length === 3 && parts[1] === "--provider" && parts[2] === "agy");
593
+ if (!valid) {
594
+ ctx.ui.notify("Use /bro usage or /bro usage --provider agy.", "warning");
595
+ return;
596
+ }
597
+ try {
598
+ await showBroModal(ctx, {
599
+ loadingText: "Checking Agy usage…",
600
+ retryable: false,
601
+ run: async (signal) => ({ text: await checkAgyUsage(pi, signal) }),
602
+ });
603
+ } catch (error) {
604
+ ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
605
+ }
606
+ return;
607
+ }
608
+
609
+ if (normalized === "help") {
414
610
  await showBroModal(ctx, { text: helpText(), kind: "help", copyable: true });
415
611
  return;
416
612
  }
417
613
 
418
- const run = async (signal: AbortSignal, source?: AssistantSource): Promise<BroResult> => {
614
+ const run = async (
615
+ signal: AbortSignal,
616
+ source?: AssistantSource,
617
+ onProgress?: (text: string) => void,
618
+ ): Promise<BroResult> => {
419
619
  let target = source;
420
620
  if (!target) {
421
621
  await ctx.waitForIdle();
422
622
  target = latestAssistant(ctx);
423
623
  }
424
624
  if (!target) throw new Error("No completed assistant response found.");
425
- return { source: target, text: await simplify(pi, target.text, signal) };
625
+ return { source: target, text: await simplify(target.text, signal, onProgress) };
426
626
  };
427
627
 
428
- if (action === "open") {
628
+ if (normalized === "open") {
429
629
  if (!lastResult) {
430
630
  await showBroModal(ctx, {
431
631
  text: "# Nothing to open yet\n\nRun `/bro` after an assistant response.",
@@ -437,24 +637,20 @@ export default function bro(pi: ExtensionAPI) {
437
637
  await showBroModal(ctx, {
438
638
  result: lastResult,
439
639
  run,
440
- onResult: (result) => {
441
- lastResult = result;
442
- },
640
+ onResult: remember,
443
641
  });
444
642
  return;
445
643
  }
446
644
 
447
- if (action && action !== "simplify") {
448
- ctx.ui.notify(`Unknown action "${action}". Use simplify, open, or help.`, "warning");
645
+ if (normalized && normalized !== "simplify") {
646
+ ctx.ui.notify(`Unknown action "${normalized}". Use simplify, open, usage, or help.`, "warning");
449
647
  return;
450
648
  }
451
649
 
452
650
  try {
453
651
  await showBroModal(ctx, {
454
652
  run,
455
- onResult: (result) => {
456
- lastResult = result;
457
- },
653
+ onResult: remember,
458
654
  });
459
655
  } catch (error) {
460
656
  ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-bro",
3
- "version": "0.1.1",
3
+ "version": "0.4.0",
4
4
  "description": "An Earendil Pi extension that simplifies the latest assistant response in a separate, context-isolated window.",
5
5
  "type": "module",
6
6
  "license": "MIT",