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.
- package/README.md +15 -7
- package/bro.ts +238 -42
- 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
|
|
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
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
-
{
|
|
179
|
+
{
|
|
180
|
+
cwd: runDirectory,
|
|
181
|
+
signal,
|
|
182
|
+
timeout: 125_000,
|
|
183
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
184
|
+
windowsHide: true,
|
|
185
|
+
},
|
|
87
186
|
);
|
|
88
187
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
-
|
|
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
|
|
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", `**${
|
|
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
|
|
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 === "
|
|
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
|
-
|
|
261
|
-
|
|
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?:
|
|
306
|
-
run?: (
|
|
307
|
-
|
|
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
|
|
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
|
|
413
|
-
|
|
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 (
|
|
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(
|
|
625
|
+
return { source: target, text: await simplify(target.text, signal, onProgress) };
|
|
426
626
|
};
|
|
427
627
|
|
|
428
|
-
if (
|
|
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:
|
|
441
|
-
lastResult = result;
|
|
442
|
-
},
|
|
640
|
+
onResult: remember,
|
|
443
641
|
});
|
|
444
642
|
return;
|
|
445
643
|
}
|
|
446
644
|
|
|
447
|
-
if (
|
|
448
|
-
ctx.ui.notify(`Unknown action "${
|
|
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:
|
|
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");
|