pi-bro 0.4.0 → 0.5.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 +47 -15
- package/bro.ts +282 -15
- 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
|
|
9
|
+
and your selected Agy 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 >=1.1.
|
|
268
|
+
- `agy >=1.1.11` 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.
|
|
@@ -301,15 +301,52 @@ pi -e npm:pi-bro
|
|
|
301
301
|
| `/bro open` | Reopen the latest explanation without calling the simplifier again. |
|
|
302
302
|
| `/bro usage` | Show current Agy resource limits. |
|
|
303
303
|
| `/bro usage --provider agy` | Same as `/bro usage`, with the provider stated explicitly. |
|
|
304
|
+
| `/bro model` | Choose from the models currently available through Agy. |
|
|
305
|
+
| `/bro model <id>` | Set an available Agy model directly. |
|
|
306
|
+
| `/bro effort` | Choose an effort supported by the current model. |
|
|
307
|
+
| `/bro effort <low\|medium\|high>` | Set a supported reasoning effort directly. |
|
|
304
308
|
| `/bro help` | Open the built-in guide. |
|
|
305
309
|
|
|
306
310
|
### Modal controls
|
|
307
311
|
|
|
312
|
+
- **Mouse wheel / trackpad**: Scroll in Pi's fullscreen mode
|
|
308
313
|
- **↑ / ↓**: Scroll up or down
|
|
309
314
|
- **C**: Copy the full explanation to your clipboard
|
|
310
315
|
- **R**: Run the simplifier again on the same response
|
|
311
316
|
- **Esc**: Close the window, or cancel while Bro is running
|
|
312
317
|
|
|
318
|
+
In Pi's regular terminal mode, the Bro title warns that mouse-wheel scrolling
|
|
319
|
+
needs fullscreen mode. Arrow-key scrolling still works.
|
|
320
|
+
|
|
321
|
+
## Settings
|
|
322
|
+
|
|
323
|
+
Bro creates this user-editable settings file when the extension loads:
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
~/.pi/agent/bro-settings.json
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"model": "gemini-3.7-flash",
|
|
332
|
+
"effort": "low"
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
Use `/bro model` and `/bro effort` to update it from Pi, or edit it directly.
|
|
337
|
+
Bro reads the file again before each explanation, so manual changes apply to
|
|
338
|
+
the next `/bro`. Use a model ID shown by `/bro model`; `effort` must be
|
|
339
|
+
one of the levels shown by `/bro effort`. Models without adjustable effort use
|
|
340
|
+
`default`. The choices remain active across Pi restarts until you change them.
|
|
341
|
+
`/bro help` shows the active settings and the exact file path.
|
|
342
|
+
|
|
343
|
+
If `PI_CODING_AGENT_DIR` is set, the file lives there instead. `PI_BRO_MODEL`
|
|
344
|
+
chooses the initial model only when Bro creates a missing settings file:
|
|
345
|
+
|
|
346
|
+
```sh
|
|
347
|
+
PI_BRO_MODEL=gemini-3.7-flash-low pi
|
|
348
|
+
```
|
|
349
|
+
|
|
313
350
|
## Custom prompt
|
|
314
351
|
|
|
315
352
|
Bro uses a built-in prompt by default. To use your own, create:
|
|
@@ -331,14 +368,6 @@ Assistant response:
|
|
|
331
368
|
Bro re-reads this file every time you simplify, so your edits take effect
|
|
332
369
|
immediately without reloading Pi. Bro never creates or modifies this file.
|
|
333
370
|
|
|
334
|
-
### Custom model
|
|
335
|
-
|
|
336
|
-
Set `PI_BRO_MODEL` before starting Pi to use a different Agy model:
|
|
337
|
-
|
|
338
|
-
```sh
|
|
339
|
-
PI_BRO_MODEL=gemini-3.7-flash-low pi
|
|
340
|
-
```
|
|
341
|
-
|
|
342
371
|
## Privacy and files
|
|
343
372
|
|
|
344
373
|
- **External requests**: Bro sends the latest completed assistant response to
|
|
@@ -352,7 +381,8 @@ PI_BRO_MODEL=gemini-3.7-flash-low pi
|
|
|
352
381
|
Pi.
|
|
353
382
|
- **File safety**: Bro does not modify project files. It runs Agy in sandbox
|
|
354
383
|
mode inside a temporary empty folder. This reduces project access, but it is
|
|
355
|
-
not a security boundary.
|
|
384
|
+
not a security boundary. Bro only writes its own user settings file described
|
|
385
|
+
above.
|
|
356
386
|
- **Provider data**: Agy and your model provider may retain logs and request data
|
|
357
387
|
according to their own settings and privacy policies.
|
|
358
388
|
- **Clipboard**: Pressing **C** copies the text to your system clipboard, where
|
|
@@ -364,8 +394,9 @@ PI_BRO_MODEL=gemini-3.7-flash-low pi
|
|
|
364
394
|
- Keeps only the latest explanation in memory.
|
|
365
395
|
- Does not store history or export directly to files.
|
|
366
396
|
- Mouse-wheel and trackpad scrolling work in Pi's fullscreen mode
|
|
367
|
-
(`pi --tui-mode fullscreen`). In regular mode,
|
|
368
|
-
not interfere with your terminal's
|
|
397
|
+
(`pi --tui-mode fullscreen`). In regular mode, Bro shows a warning in its
|
|
398
|
+
title; use the arrow keys so Bro does not interfere with your terminal's
|
|
399
|
+
native text selection.
|
|
369
400
|
- In fullscreen mode, mouse text selection may visually extend outside the Bro
|
|
370
401
|
window. Press **C** to copy the full explanation instead.
|
|
371
402
|
|
|
@@ -374,11 +405,12 @@ PI_BRO_MODEL=gemini-3.7-flash-low pi
|
|
|
374
405
|
```sh
|
|
375
406
|
npm install
|
|
376
407
|
npm test
|
|
377
|
-
pi -e ./bro.ts
|
|
408
|
+
pi --tui-mode fullscreen -e ./bro.ts
|
|
378
409
|
```
|
|
379
410
|
|
|
380
411
|
The smoke test uses a fake `agy`, so it does not call an external model. It
|
|
381
|
-
verifies command routing, custom prompt handling, and context
|
|
412
|
+
verifies command routing, settings, custom prompt handling, and context
|
|
413
|
+
isolation.
|
|
382
414
|
|
|
383
415
|
## License
|
|
384
416
|
|
package/bro.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
@@ -7,8 +7,11 @@ import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-c
|
|
|
7
7
|
import { copyToClipboard, getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { Markdown, matchesKey, truncateToWidth, visibleWidth, type Focusable } from "@earendil-works/pi-tui";
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const AGENT_DIR = process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
11
|
+
const ENV_MODEL = process.env.PI_BRO_MODEL?.trim();
|
|
12
|
+
const DEFAULT_MODEL = ENV_MODEL || "gemini-3.7-flash";
|
|
13
|
+
const PROMPT_FILE = join(AGENT_DIR, "bro-prompt.md");
|
|
14
|
+
const SETTINGS_FILE = join(AGENT_DIR, "bro-settings.json");
|
|
12
15
|
const LOADING_TEXT = "Simplifying for my bro…";
|
|
13
16
|
const DEFAULT_TEMPLATE = `Rewrite the quoted response for a non-expert.
|
|
14
17
|
Use plain English and short sentences. Explain jargon briefly.
|
|
@@ -21,11 +24,21 @@ Quoted response as a JSON string:
|
|
|
21
24
|
{{response}}`;
|
|
22
25
|
|
|
23
26
|
type Theme = ExtensionCommandContext["ui"]["theme"];
|
|
24
|
-
type TuiLike = { requestRender(): void };
|
|
27
|
+
type TuiLike = { readonly mode: "regular" | "fullscreen"; requestRender(): void };
|
|
25
28
|
type ModalKind = "loading" | "streaming" | "result" | "help" | "empty" | "error";
|
|
26
29
|
type AssistantSource = { id: string; text: string };
|
|
27
30
|
type BroResult = { source: AssistantSource; text: string };
|
|
28
31
|
type ModalResult = { source?: AssistantSource; text: string };
|
|
32
|
+
const EFFORTS = ["default", "low", "medium", "high"] as const;
|
|
33
|
+
type BroEffort = (typeof EFFORTS)[number];
|
|
34
|
+
type AgyEffort = Exclude<BroEffort, "default">;
|
|
35
|
+
type BroSettings = { model: string; effort: BroEffort };
|
|
36
|
+
type AgyModelFamily = {
|
|
37
|
+
id: string;
|
|
38
|
+
label: string;
|
|
39
|
+
efforts: AgyEffort[];
|
|
40
|
+
variants: Array<{ id: string; effort?: AgyEffort }>;
|
|
41
|
+
};
|
|
29
42
|
type AgyEvent = {
|
|
30
43
|
event?: string;
|
|
31
44
|
step_update?: { step_type?: string; text_delta?: unknown };
|
|
@@ -44,6 +57,8 @@ const COMMANDS = [
|
|
|
44
57
|
{ value: "simplify", label: "simplify", description: "Simplify the latest assistant response" },
|
|
45
58
|
{ value: "open", label: "open", description: "Reopen the last explanation" },
|
|
46
59
|
{ value: "usage", label: "usage", description: "Show current Agy usage" },
|
|
60
|
+
{ value: "model", label: "model", description: "Choose the Agy model" },
|
|
61
|
+
{ value: "effort", label: "effort", description: "Choose the Agy reasoning effort" },
|
|
47
62
|
{ value: "help", label: "help", description: "Learn what Bro does and what it can access" },
|
|
48
63
|
];
|
|
49
64
|
|
|
@@ -51,6 +66,47 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
51
66
|
return typeof value === "object" && value !== null;
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
export function parseBroSettings(value: unknown): BroSettings {
|
|
70
|
+
if (
|
|
71
|
+
!isRecord(value) ||
|
|
72
|
+
typeof value.model !== "string" ||
|
|
73
|
+
!value.model.trim() ||
|
|
74
|
+
!EFFORTS.some((effort) => effort === value.effort)
|
|
75
|
+
) {
|
|
76
|
+
throw new Error('Settings must contain a model and effort set to "default", "low", "medium", or "high".');
|
|
77
|
+
}
|
|
78
|
+
return { model: value.model.trim(), effort: value.effort as BroSettings["effort"] };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function ensureSettingsFile(): Promise<void> {
|
|
82
|
+
await mkdir(AGENT_DIR, { recursive: true });
|
|
83
|
+
try {
|
|
84
|
+
await writeFile(
|
|
85
|
+
SETTINGS_FILE,
|
|
86
|
+
`${JSON.stringify({ model: DEFAULT_MODEL, effort: ENV_MODEL ? "default" : "low" }, null, 2)}\n`,
|
|
87
|
+
{ encoding: "utf8", flag: "wx", mode: 0o600 },
|
|
88
|
+
);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function readSettings(): Promise<BroSettings> {
|
|
95
|
+
await ensureSettingsFile();
|
|
96
|
+
try {
|
|
97
|
+
return parseBroSettings(JSON.parse(await readFile(SETTINGS_FILE, "utf8")));
|
|
98
|
+
} catch (error) {
|
|
99
|
+
if (error instanceof SyntaxError) throw new Error(`${SETTINGS_FILE} is not valid JSON.`);
|
|
100
|
+
if (error instanceof Error) throw new Error(`${SETTINGS_FILE}: ${error.message}`);
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function writeSettings(settings: BroSettings): Promise<void> {
|
|
106
|
+
// ponytail: last writer wins across concurrent Pi processes; add locking only if that becomes a common workflow.
|
|
107
|
+
await writeFile(SETTINGS_FILE, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
108
|
+
}
|
|
109
|
+
|
|
54
110
|
export function formatAgyUsage(value: unknown): string {
|
|
55
111
|
if (!isRecord(value) || value.status !== "SUCCESS" || typeof value.response !== "string") {
|
|
56
112
|
throw new Error("Agy returned invalid usage data.");
|
|
@@ -72,6 +128,78 @@ export function formatAgyUsage(value: unknown): string {
|
|
|
72
128
|
return `# Agy usage\n\n${sections.join("\n\n")}`;
|
|
73
129
|
}
|
|
74
130
|
|
|
131
|
+
export function parseAgyModels(output: string): AgyModelFamily[] {
|
|
132
|
+
// ponytail: Agy 1.1.13 exposes a tab-separated variant list; use structured catalog data when available here.
|
|
133
|
+
const families = new Map<string, AgyModelFamily>();
|
|
134
|
+
for (const line of output.split(/\r?\n/)) {
|
|
135
|
+
const [rawId, ...rawLabel] = line.split("\t");
|
|
136
|
+
if (!rawId?.trim() || !rawLabel.length) continue;
|
|
137
|
+
const id = rawId.trim();
|
|
138
|
+
const label = rawLabel.join(" ").trim();
|
|
139
|
+
const effort = (["low", "medium", "high"] as const).find(
|
|
140
|
+
(value) => id.endsWith(`-${value}`) && label.endsWith(`(${value[0].toUpperCase()}${value.slice(1)})`),
|
|
141
|
+
);
|
|
142
|
+
const familyId = effort ? id.slice(0, -effort.length - 1) : id;
|
|
143
|
+
const family = families.get(familyId) ?? {
|
|
144
|
+
id: familyId,
|
|
145
|
+
label: effort ? label.replace(/\s+\((Low|Medium|High)\)$/, "") : label,
|
|
146
|
+
efforts: [],
|
|
147
|
+
variants: [],
|
|
148
|
+
};
|
|
149
|
+
if (effort && !family.efforts.includes(effort)) family.efforts.push(effort);
|
|
150
|
+
family.variants.push({ id, effort });
|
|
151
|
+
families.set(familyId, family);
|
|
152
|
+
}
|
|
153
|
+
if (!families.size) throw new Error("Agy returned no available models.");
|
|
154
|
+
for (const family of families.values()) {
|
|
155
|
+
family.efforts.sort((a, b) => EFFORTS.indexOf(a) - EFFORTS.indexOf(b));
|
|
156
|
+
}
|
|
157
|
+
return [...families.values()];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function listAgyModels(pi: ExtensionAPI): Promise<AgyModelFamily[]> {
|
|
161
|
+
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
162
|
+
try {
|
|
163
|
+
const result = await pi.exec("agy", ["models"], { cwd: runDirectory, timeout: 30_000 });
|
|
164
|
+
if (result.killed) throw new Error("Agy model check timed out.");
|
|
165
|
+
if (result.code !== 0) throw new Error(result.stderr.trim() || `Agy exited with code ${result.code}.`);
|
|
166
|
+
return parseAgyModels(result.stdout);
|
|
167
|
+
} finally {
|
|
168
|
+
await rm(runDirectory, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function resolveCatalogSettings(
|
|
173
|
+
settings: BroSettings,
|
|
174
|
+
families: AgyModelFamily[],
|
|
175
|
+
): { settings: BroSettings; family?: AgyModelFamily } {
|
|
176
|
+
const family = families.find(
|
|
177
|
+
(item) => item.id === settings.model || item.variants.some((variant) => variant.id === settings.model),
|
|
178
|
+
);
|
|
179
|
+
if (!family) return { settings };
|
|
180
|
+
const variant = family.variants.find((item) => item.id === settings.model);
|
|
181
|
+
return {
|
|
182
|
+
family,
|
|
183
|
+
settings: {
|
|
184
|
+
model: family.id,
|
|
185
|
+
effort: settings.effort === "default" && variant?.effort ? variant.effort : settings.effort,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function preferredEffort(family: AgyModelFamily): BroEffort {
|
|
191
|
+
return family.efforts.includes("low") ? "low" : (family.efforts[0] ?? "default");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function agySelection(settings: BroSettings): { model: string; effort?: AgyEffort } {
|
|
195
|
+
if (settings.effort === "default") return { model: settings.model };
|
|
196
|
+
const suffix = (["low", "medium", "high"] as const).find((effort) => settings.model.endsWith(`-${effort}`));
|
|
197
|
+
return {
|
|
198
|
+
model: suffix ? settings.model.slice(0, -suffix.length - 1) : settings.model,
|
|
199
|
+
effort: settings.effort,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
75
203
|
async function checkAgyUsage(pi: ExtensionAPI, signal: AbortSignal): Promise<string> {
|
|
76
204
|
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
77
205
|
try {
|
|
@@ -155,9 +283,11 @@ function parseAgyLine(line: string): { delta?: string; result?: string } {
|
|
|
155
283
|
async function simplify(
|
|
156
284
|
response: string,
|
|
157
285
|
signal: AbortSignal,
|
|
286
|
+
settings: BroSettings,
|
|
158
287
|
onProgress?: (text: string) => void,
|
|
159
288
|
): Promise<string> {
|
|
160
289
|
const prompt = await promptFor(response);
|
|
290
|
+
const selection = agySelection(settings);
|
|
161
291
|
const runDirectory = await mkdtemp(join(tmpdir(), "pi-bro-"));
|
|
162
292
|
let updateTimer: ReturnType<typeof setTimeout> | undefined;
|
|
163
293
|
|
|
@@ -170,7 +300,8 @@ async function simplify(
|
|
|
170
300
|
"--output-format",
|
|
171
301
|
"stream-json",
|
|
172
302
|
"--model",
|
|
173
|
-
|
|
303
|
+
selection.model,
|
|
304
|
+
...(selection.effort ? ["--effort", selection.effort] : []),
|
|
174
305
|
"--print-timeout",
|
|
175
306
|
"2m",
|
|
176
307
|
"--print",
|
|
@@ -248,7 +379,7 @@ async function simplify(
|
|
|
248
379
|
}
|
|
249
380
|
}
|
|
250
381
|
|
|
251
|
-
function helpText(): string {
|
|
382
|
+
function helpText(settings: BroSettings): string {
|
|
252
383
|
return `# Bro
|
|
253
384
|
|
|
254
385
|
Bro turns the latest completed assistant response into a clear, plain-language explanation.
|
|
@@ -258,8 +389,21 @@ Bro turns the latest completed assistant response into a clear, plain-language e
|
|
|
258
389
|
- \`/bro\` or \`/bro simplify\` — create a new explanation
|
|
259
390
|
- \`/bro open\` — reopen the last explanation
|
|
260
391
|
- \`/bro usage\` or \`/bro usage --provider agy\` — show current Agy usage
|
|
392
|
+
- \`/bro model\` — choose the Agy model
|
|
393
|
+
- \`/bro effort\` — choose the Agy reasoning effort
|
|
261
394
|
- \`/bro help\` — show this guide
|
|
262
395
|
|
|
396
|
+
## Current simplifier settings
|
|
397
|
+
|
|
398
|
+
- **Model:** \`${settings.model}\`
|
|
399
|
+
- **Reasoning effort:** ${settings.effort === "default" ? "built into the selected model" : settings.effort}
|
|
400
|
+
|
|
401
|
+
These choices are saved in:
|
|
402
|
+
|
|
403
|
+
\`${SETTINGS_FILE}\`
|
|
404
|
+
|
|
405
|
+
Use the slash commands or edit that file directly. Changes apply to future explanations and remain active across Pi restarts until you change them. Use a model ID shown by \`/bro model\`. Use an effort shown by \`/bro effort\`; fixed-effort models use \`default\`.
|
|
406
|
+
|
|
263
407
|
## Controls
|
|
264
408
|
|
|
265
409
|
- **Mouse wheel / trackpad** — scroll in Pi's fullscreen mode
|
|
@@ -268,15 +412,17 @@ Bro turns the latest completed assistant response into a clear, plain-language e
|
|
|
268
412
|
- **R** — simplify the same response again
|
|
269
413
|
- **Esc** — close the window, or cancel while Bro is working
|
|
270
414
|
|
|
415
|
+
In Pi's regular terminal mode, the Bro title warns that mouse-wheel scrolling needs fullscreen mode. Arrow-key scrolling still works.
|
|
416
|
+
|
|
271
417
|
Mouse text selection may extend outside the Bro window. Press **C** to copy the complete explanation instead.
|
|
272
418
|
|
|
273
419
|
## Privacy and file safety
|
|
274
420
|
|
|
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.
|
|
421
|
+
Bro does not modify your project files. It creates and updates only its user settings file shown above. It runs the simplifier in sandbox mode inside a temporary empty folder. This reduces project access, but it is not a security boundary.
|
|
276
422
|
|
|
277
423
|
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.
|
|
278
424
|
|
|
279
|
-
Bro sends the assistant response to an external simplifier (currently Agy with
|
|
425
|
+
Bro sends the assistant response to an external simplifier (currently Agy with your selected model). Agy and the model provider may retain request data or logs under their own policies.
|
|
280
426
|
|
|
281
427
|
\`/bro usage\` checks your authenticated Agy limits without sending an assistant response or running a model turn.
|
|
282
428
|
|
|
@@ -290,7 +436,7 @@ You can create or edit:
|
|
|
290
436
|
|
|
291
437
|
Bro reads this file when running but never creates or edits it. Include \`{{response}}\` exactly once in your template. Changes take effect on the next simplification.
|
|
292
438
|
|
|
293
|
-
|
|
439
|
+
When the settings file does not exist yet, \`PI_BRO_MODEL\` can choose its initial model.`;
|
|
294
440
|
}
|
|
295
441
|
|
|
296
442
|
// The overlay framing pattern is adapted from pi-btw (MIT); see THIRD_PARTY_NOTICES.md.
|
|
@@ -392,12 +538,13 @@ class BroModal implements Focusable {
|
|
|
392
538
|
this.offset = Math.max(0, Math.min(this.offset, this.maxOffset));
|
|
393
539
|
const visible = rendered.slice(this.offset, this.offset + this.bodyHeight);
|
|
394
540
|
const hiddenBelow = Math.max(0, this.maxOffset - this.offset);
|
|
541
|
+
const modeHint = this.tui.mode === "regular" ? " · mouse wheel needs fullscreen" : "";
|
|
395
542
|
const scroll = this.maxOffset > 0 ? ` · ↑${this.offset} ↓${hiddenBelow}` : "";
|
|
396
543
|
const controls = this.notice ? `${this.notice} · ${this.controls()}` : this.controls();
|
|
397
544
|
|
|
398
545
|
const lines = [
|
|
399
546
|
this.borderLine(innerWidth, "top"),
|
|
400
|
-
this.frameLine(this.theme.fg("accent", this.theme.bold(`Bro${scroll}`)), innerWidth),
|
|
547
|
+
this.frameLine(this.theme.fg("accent", this.theme.bold(`Bro${modeHint}${scroll}`)), innerWidth),
|
|
401
548
|
this.ruleLine(innerWidth),
|
|
402
549
|
];
|
|
403
550
|
|
|
@@ -566,7 +713,8 @@ async function showBroModal(ctx: ExtensionCommandContext, options: BroModalOptio
|
|
|
566
713
|
);
|
|
567
714
|
}
|
|
568
715
|
|
|
569
|
-
export default function bro(pi: ExtensionAPI) {
|
|
716
|
+
export default async function bro(pi: ExtensionAPI) {
|
|
717
|
+
await ensureSettingsFile();
|
|
570
718
|
let lastResult: BroResult | undefined;
|
|
571
719
|
const remember = (result: ModalResult) => {
|
|
572
720
|
if (result.source) lastResult = { source: result.source, text: result.text };
|
|
@@ -577,7 +725,7 @@ export default function bro(pi: ExtensionAPI) {
|
|
|
577
725
|
});
|
|
578
726
|
|
|
579
727
|
pi.registerCommand("bro", {
|
|
580
|
-
description: "Simplify responses
|
|
728
|
+
description: "Simplify responses and manage Bro",
|
|
581
729
|
getArgumentCompletions: (prefix) => {
|
|
582
730
|
const normalized = prefix.trim().toLowerCase();
|
|
583
731
|
const matches = COMMANDS.filter((command) => command.value.startsWith(normalized));
|
|
@@ -606,8 +754,123 @@ export default function bro(pi: ExtensionAPI) {
|
|
|
606
754
|
return;
|
|
607
755
|
}
|
|
608
756
|
|
|
757
|
+
if (action === "model") {
|
|
758
|
+
if (parts.length > 2) {
|
|
759
|
+
ctx.ui.notify("Use /bro model or /bro model <id>.", "warning");
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
try {
|
|
763
|
+
const settings = await readSettings();
|
|
764
|
+
const models = await listAgyModels(pi);
|
|
765
|
+
const current = resolveCatalogSettings(settings, models);
|
|
766
|
+
const requested = parts[1];
|
|
767
|
+
let selected: AgyModelFamily | undefined;
|
|
768
|
+
let selectedEffort: BroEffort | undefined;
|
|
769
|
+
if (requested) {
|
|
770
|
+
selected = models.find((item) => item.id.toLowerCase() === requested);
|
|
771
|
+
if (!selected) {
|
|
772
|
+
for (const family of models) {
|
|
773
|
+
const variant = family.variants.find((item) => item.id.toLowerCase() === requested);
|
|
774
|
+
if (variant) {
|
|
775
|
+
selected = family;
|
|
776
|
+
selectedEffort = variant.effort ?? "default";
|
|
777
|
+
break;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
if (!selected) {
|
|
782
|
+
ctx.ui.notify(`Unknown Agy model "${requested}". Run /bro model to see available choices.`, "warning");
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
if (ctx.mode !== "tui") {
|
|
787
|
+
ctx.ui.notify("Use /bro model <id> outside Pi's interactive UI.", "warning");
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const ordered = [...models].sort((a, b) => Number(b.id === current.family?.id) - Number(a.id === current.family?.id));
|
|
791
|
+
const choices = ordered.map(
|
|
792
|
+
(item) =>
|
|
793
|
+
`${item.id} — ${item.label} · ${item.efforts.length ? item.efforts.join("/") : "fixed effort"}${item.id === current.family?.id ? " (current)" : ""}`,
|
|
794
|
+
);
|
|
795
|
+
const choice = await ctx.ui.select(`Agy model (current: ${current.settings.model})`, choices);
|
|
796
|
+
if (!choice) return;
|
|
797
|
+
selected = ordered[choices.indexOf(choice)];
|
|
798
|
+
}
|
|
799
|
+
if (!selectedEffort) {
|
|
800
|
+
const currentEffort = current.settings.effort;
|
|
801
|
+
const canKeepCurrent =
|
|
802
|
+
current.family?.id === selected.id &&
|
|
803
|
+
(currentEffort === "default" ? !selected.efforts.length : selected.efforts.includes(currentEffort));
|
|
804
|
+
selectedEffort = canKeepCurrent ? currentEffort : preferredEffort(selected);
|
|
805
|
+
}
|
|
806
|
+
await writeSettings({ model: selected.id, effort: selectedEffort });
|
|
807
|
+
ctx.ui.notify(
|
|
808
|
+
`Bro model: ${selected.id}${selectedEffort === "default" ? "" : ` (${selectedEffort})`}`,
|
|
809
|
+
"info",
|
|
810
|
+
);
|
|
811
|
+
} catch (error) {
|
|
812
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
813
|
+
}
|
|
814
|
+
return;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
if (action === "effort") {
|
|
818
|
+
const requested = parts[1];
|
|
819
|
+
if (parts.length > 2 || (requested && !EFFORTS.some((effort) => effort === requested))) {
|
|
820
|
+
ctx.ui.notify("Use /bro effort, or choose low, medium, or high.", "warning");
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const settings = await readSettings();
|
|
825
|
+
const current = resolveCatalogSettings(settings, await listAgyModels(pi));
|
|
826
|
+
if (!current.family) {
|
|
827
|
+
ctx.ui.notify(`Model "${settings.model}" is not in Agy's current model list. Run /bro model first.`, "warning");
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
if (!current.family.efforts.length) {
|
|
831
|
+
if (requested && requested !== "default") {
|
|
832
|
+
ctx.ui.notify(`${current.family.label} uses a fixed effort level.`, "warning");
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
await writeSettings({ model: current.family.id, effort: "default" });
|
|
836
|
+
ctx.ui.notify(`${current.family.label} uses its built-in effort level.`, "info");
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
if (requested === "default" || (requested && !current.family.efforts.includes(requested as AgyEffort))) {
|
|
840
|
+
ctx.ui.notify(
|
|
841
|
+
`${current.family.label} supports ${current.family.efforts.join(" or ")} effort.`,
|
|
842
|
+
"warning",
|
|
843
|
+
);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
let selected = requested as AgyEffort | undefined;
|
|
847
|
+
if (!selected) {
|
|
848
|
+
if (ctx.mode !== "tui") {
|
|
849
|
+
ctx.ui.notify("Use /bro effort <low|medium|high> outside Pi's interactive UI.", "warning");
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
const efforts = [...current.family.efforts].sort(
|
|
853
|
+
(a, b) => Number(b === current.settings.effort) - Number(a === current.settings.effort),
|
|
854
|
+
);
|
|
855
|
+
const choices = efforts.map((effort) => `${effort}${effort === current.settings.effort ? " (current)" : ""}`);
|
|
856
|
+
const choice = await ctx.ui.select(`Agy reasoning effort (current: ${current.settings.effort})`, choices);
|
|
857
|
+
if (!choice) return;
|
|
858
|
+
selected = efforts[choices.indexOf(choice)];
|
|
859
|
+
}
|
|
860
|
+
await writeSettings({ model: current.family.id, effort: selected });
|
|
861
|
+
ctx.ui.notify(`Bro reasoning effort: ${selected}`, "info");
|
|
862
|
+
} catch (error) {
|
|
863
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
864
|
+
}
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
609
868
|
if (normalized === "help") {
|
|
610
|
-
|
|
869
|
+
try {
|
|
870
|
+
await showBroModal(ctx, { text: helpText(await readSettings()), kind: "help", copyable: true });
|
|
871
|
+
} catch (error) {
|
|
872
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
873
|
+
}
|
|
611
874
|
return;
|
|
612
875
|
}
|
|
613
876
|
|
|
@@ -622,7 +885,11 @@ export default function bro(pi: ExtensionAPI) {
|
|
|
622
885
|
target = latestAssistant(ctx);
|
|
623
886
|
}
|
|
624
887
|
if (!target) throw new Error("No completed assistant response found.");
|
|
625
|
-
|
|
888
|
+
const settings = await readSettings();
|
|
889
|
+
return {
|
|
890
|
+
source: target,
|
|
891
|
+
text: await simplify(target.text, signal, settings, onProgress),
|
|
892
|
+
};
|
|
626
893
|
};
|
|
627
894
|
|
|
628
895
|
if (normalized === "open") {
|
|
@@ -643,7 +910,7 @@ export default function bro(pi: ExtensionAPI) {
|
|
|
643
910
|
}
|
|
644
911
|
|
|
645
912
|
if (normalized && normalized !== "simplify") {
|
|
646
|
-
ctx.ui.notify(`Unknown action "${normalized}". Use simplify, open, usage, or help.`, "warning");
|
|
913
|
+
ctx.ui.notify(`Unknown action "${normalized}". Use simplify, open, usage, model, effort, or help.`, "warning");
|
|
647
914
|
return;
|
|
648
915
|
}
|
|
649
916
|
|