pi-bro 0.3.0 → 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.
- package/README.md +5 -1
- package/bro.ts +99 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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,6 +343,8 @@ 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
|
|
@@ -356,7 +360,7 @@ PI_BRO_MODEL=gemini-3.7-flash-low pi
|
|
|
356
360
|
|
|
357
361
|
## Current limits
|
|
358
362
|
|
|
359
|
-
-
|
|
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
366
|
- Mouse-wheel and trackpad scrolling work in Pi's fullscreen mode
|
package/bro.ts
CHANGED
|
@@ -25,6 +25,7 @@ type TuiLike = { requestRender(): void };
|
|
|
25
25
|
type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
|
|
26
26
|
type AssistantSource = { id: string; text: string };
|
|
27
27
|
type BroResult = { source: AssistantSource; text: string };
|
|
28
|
+
type ModalResult = { source?: AssistantSource; text: string };
|
|
28
29
|
type AgyEvent = {
|
|
29
30
|
event?: string;
|
|
30
31
|
step_update?: { step_type?: string; text_delta?: unknown };
|
|
@@ -42,9 +43,57 @@ export function wheelDelta(data: string): number {
|
|
|
42
43
|
const COMMANDS = [
|
|
43
44
|
{ value: "simplify", label: "simplify", description: "Simplify the latest assistant response" },
|
|
44
45
|
{ value: "open", label: "open", description: "Reopen the last explanation" },
|
|
46
|
+
{ value: "usage", label: "usage", description: "Show current Agy usage" },
|
|
45
47
|
{ value: "help", label: "help", description: "Learn what Bro does and what it can access" },
|
|
46
48
|
];
|
|
47
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
|
+
|
|
48
97
|
function latestAssistant(ctx: ExtensionCommandContext): AssistantSource | undefined {
|
|
49
98
|
const branch = ctx.sessionManager.getBranch();
|
|
50
99
|
|
|
@@ -208,6 +257,7 @@ Bro turns the latest completed assistant response into a clear, plain-language e
|
|
|
208
257
|
|
|
209
258
|
- \`/bro\` or \`/bro simplify\` — create a new explanation
|
|
210
259
|
- \`/bro open\` — reopen the last explanation
|
|
260
|
+
- \`/bro usage\` or \`/bro usage --provider agy\` — show current Agy usage
|
|
211
261
|
- \`/bro help\` — show this guide
|
|
212
262
|
|
|
213
263
|
## Controls
|
|
@@ -228,6 +278,8 @@ Bro does not add explanations to Pi's conversation history, session files, or ma
|
|
|
228
278
|
|
|
229
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.
|
|
230
280
|
|
|
281
|
+
\`/bro usage\` checks your authenticated Agy limits without sending an assistant response or running a model turn.
|
|
282
|
+
|
|
231
283
|
Pressing **C** copies the explanation to your system clipboard, where your operating system or clipboard manager may retain it.
|
|
232
284
|
|
|
233
285
|
## Custom prompt
|
|
@@ -263,8 +315,8 @@ class BroModal implements Focusable {
|
|
|
263
315
|
private readonly onDispose: () => void,
|
|
264
316
|
) {}
|
|
265
317
|
|
|
266
|
-
setLoading(): void {
|
|
267
|
-
this.setContent("loading", `**${
|
|
318
|
+
setLoading(text = LOADING_TEXT): void {
|
|
319
|
+
this.setContent("loading", `**${text}**`, "", false, false);
|
|
268
320
|
}
|
|
269
321
|
|
|
270
322
|
setStreaming(text: string): void {
|
|
@@ -280,7 +332,7 @@ class BroModal implements Focusable {
|
|
|
280
332
|
}
|
|
281
333
|
|
|
282
334
|
setError(message: string): void {
|
|
283
|
-
this.setContent("error", `# Bro
|
|
335
|
+
this.setContent("error", `# Bro ran into a problem\n\n${message}`, "", false, true);
|
|
284
336
|
}
|
|
285
337
|
|
|
286
338
|
private setContent(
|
|
@@ -320,7 +372,9 @@ class BroModal implements Focusable {
|
|
|
320
372
|
private controls(): string {
|
|
321
373
|
if (this.kind === "loading") return "Esc cancel";
|
|
322
374
|
if (this.kind === "streaming") return "Simplifying… · ↑/↓ scroll · Esc cancel";
|
|
323
|
-
if (this.kind === "result")
|
|
375
|
+
if (this.kind === "result") {
|
|
376
|
+
return `↑/↓ scroll · C copy${this.retryable ? " · R simplify again" : ""} · Esc close`;
|
|
377
|
+
}
|
|
324
378
|
if (this.kind === "help") return "↑/↓ scroll · C copy · Esc close";
|
|
325
379
|
if (this.kind === "error") return "R try again · Esc close";
|
|
326
380
|
return "Esc close";
|
|
@@ -411,13 +465,15 @@ interface BroModalOptions {
|
|
|
411
465
|
text?: string;
|
|
412
466
|
kind?: "help" | "empty";
|
|
413
467
|
copyable?: boolean;
|
|
414
|
-
result?:
|
|
468
|
+
result?: ModalResult;
|
|
415
469
|
run?: (
|
|
416
470
|
signal: AbortSignal,
|
|
417
471
|
source?: AssistantSource,
|
|
418
472
|
onProgress?: (text: string) => void,
|
|
419
|
-
) => Promise<
|
|
420
|
-
onResult?: (result:
|
|
473
|
+
) => Promise<ModalResult>;
|
|
474
|
+
onResult?: (result: ModalResult) => void;
|
|
475
|
+
loadingText?: string;
|
|
476
|
+
retryable?: boolean;
|
|
421
477
|
}
|
|
422
478
|
|
|
423
479
|
async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptions): Promise<void> {
|
|
@@ -459,7 +515,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
459
515
|
const previous = current;
|
|
460
516
|
const nextController = new AbortController();
|
|
461
517
|
controller = nextController;
|
|
462
|
-
modal.setLoading();
|
|
518
|
+
modal.setLoading(options.loadingText);
|
|
463
519
|
|
|
464
520
|
void options
|
|
465
521
|
.run(nextController.signal, source, (text) => {
|
|
@@ -470,14 +526,14 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
470
526
|
if (closed || nextController.signal.aborted) return;
|
|
471
527
|
current = result;
|
|
472
528
|
options.onResult?.(result);
|
|
473
|
-
modal.setResult(result.text, true);
|
|
529
|
+
modal.setResult(result.text, options.retryable ?? true);
|
|
474
530
|
})
|
|
475
531
|
.catch((error) => {
|
|
476
532
|
if (closed || nextController.signal.aborted) return;
|
|
477
533
|
const message = error instanceof Error ? error.message : String(error);
|
|
478
534
|
if (previous) {
|
|
479
535
|
current = previous;
|
|
480
|
-
modal.setResult(previous.text, true, `Retry failed: ${message}`);
|
|
536
|
+
modal.setResult(previous.text, options.retryable ?? true, `Retry failed: ${message}`);
|
|
481
537
|
} else {
|
|
482
538
|
modal.setError(message);
|
|
483
539
|
}
|
|
@@ -490,7 +546,7 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
490
546
|
if (options.text !== undefined) {
|
|
491
547
|
modal.setStatic(options.kind ?? "help", options.text, options.copyable ?? false);
|
|
492
548
|
} else if (current) {
|
|
493
|
-
modal.setResult(current.text, Boolean(options.run));
|
|
549
|
+
modal.setResult(current.text, options.retryable ?? Boolean(options.run));
|
|
494
550
|
} else {
|
|
495
551
|
execute();
|
|
496
552
|
}
|
|
@@ -512,21 +568,45 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
512
568
|
|
|
513
569
|
export default function bro(pi: ExtensionAPI) {
|
|
514
570
|
let lastResult: BroResult | undefined;
|
|
571
|
+
const remember = (result: ModalResult) => {
|
|
572
|
+
if (result.source) lastResult = { source: result.source, text: result.text };
|
|
573
|
+
};
|
|
515
574
|
|
|
516
575
|
pi.on("session_start", async () => {
|
|
517
576
|
lastResult = undefined;
|
|
518
577
|
});
|
|
519
578
|
|
|
520
579
|
pi.registerCommand("bro", {
|
|
521
|
-
description: "Simplify, reopen, or
|
|
580
|
+
description: "Simplify responses, reopen explanations, or show Agy usage",
|
|
522
581
|
getArgumentCompletions: (prefix) => {
|
|
523
582
|
const normalized = prefix.trim().toLowerCase();
|
|
524
583
|
const matches = COMMANDS.filter((command) => command.value.startsWith(normalized));
|
|
525
584
|
return matches.length ? matches : null;
|
|
526
585
|
},
|
|
527
586
|
handler: async (args, ctx) => {
|
|
528
|
-
const
|
|
529
|
-
|
|
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") {
|
|
530
610
|
await showBroModal(ctx, { text: helpText(), kind: "help", copyable: true });
|
|
531
611
|
return;
|
|
532
612
|
}
|
|
@@ -545,7 +625,7 @@ export default function bro(pi: ExtensionAPI) {
|
|
|
545
625
|
return { source: target, text: await simplify(target.text, signal, onProgress) };
|
|
546
626
|
};
|
|
547
627
|
|
|
548
|
-
if (
|
|
628
|
+
if (normalized === "open") {
|
|
549
629
|
if (!lastResult) {
|
|
550
630
|
await showBroModal(ctx, {
|
|
551
631
|
text: "# Nothing to open yet\n\nRun `/bro` after an assistant response.",
|
|
@@ -557,24 +637,20 @@ export default function bro(pi: ExtensionAPI) {
|
|
|
557
637
|
await showBroModal(ctx, {
|
|
558
638
|
result: lastResult,
|
|
559
639
|
run,
|
|
560
|
-
onResult:
|
|
561
|
-
lastResult = result;
|
|
562
|
-
},
|
|
640
|
+
onResult: remember,
|
|
563
641
|
});
|
|
564
642
|
return;
|
|
565
643
|
}
|
|
566
644
|
|
|
567
|
-
if (
|
|
568
|
-
ctx.ui.notify(`Unknown action "${
|
|
645
|
+
if (normalized && normalized !== "simplify") {
|
|
646
|
+
ctx.ui.notify(`Unknown action "${normalized}". Use simplify, open, usage, or help.`, "warning");
|
|
569
647
|
return;
|
|
570
648
|
}
|
|
571
649
|
|
|
572
650
|
try {
|
|
573
651
|
await showBroModal(ctx, {
|
|
574
652
|
run,
|
|
575
|
-
onResult:
|
|
576
|
-
lastResult = result;
|
|
577
|
-
},
|
|
653
|
+
onResult: remember,
|
|
578
654
|
});
|
|
579
655
|
} catch (error) {
|
|
580
656
|
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|