pi-bro 0.23.0 → 0.24.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/CHANGELOG.md +36 -0
- package/README.md +10 -9
- package/docs/guided-review.md +8 -12
- package/docs/user-guide.md +38 -19
- package/package.json +8 -2
- package/src/advisor.ts +573 -0
- package/src/backend.ts +100 -42
- package/src/bro.ts +37 -2623
- package/src/btw.ts +556 -0
- package/src/config-ui.ts +76 -39
- package/src/doctor.ts +538 -0
- package/src/explain-modal.ts +777 -0
- package/src/modal-kit.ts +258 -0
- package/src/review-modal.ts +95 -48
- package/src/review-ui.ts +57 -47
- package/src/settings.ts +45 -3
- package/src/show.ts +243 -0
- package/src/ui-capabilities.ts +32 -0
package/src/config-ui.ts
CHANGED
|
@@ -2,20 +2,13 @@ import {
|
|
|
2
2
|
type Component,
|
|
3
3
|
type SelectItem,
|
|
4
4
|
type SettingItem,
|
|
5
|
-
Container,
|
|
6
5
|
Input,
|
|
7
6
|
SelectList,
|
|
8
7
|
SettingsList,
|
|
9
|
-
Text,
|
|
10
8
|
matchesKey,
|
|
11
9
|
truncateToWidth,
|
|
12
|
-
visibleWidth,
|
|
13
10
|
} from "@earendil-works/pi-tui";
|
|
14
|
-
import {
|
|
15
|
-
type ExtensionCommandContext,
|
|
16
|
-
getSelectListTheme,
|
|
17
|
-
getSettingsListTheme,
|
|
18
|
-
} from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { getSelectListTheme, getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
19
12
|
import {
|
|
20
13
|
type AgyModelFamily,
|
|
21
14
|
type BroSettings,
|
|
@@ -34,15 +27,10 @@ import {
|
|
|
34
27
|
withCapabilityOverride,
|
|
35
28
|
} from "./settings.ts";
|
|
36
29
|
import { BRO_MODES, parseBroMode } from "./prompt.ts";
|
|
30
|
+
import { delegateMouse, formatNotice, modalHeight, renderFrame, type Area, type Theme, type TuiLike } from "./modal-kit.ts";
|
|
31
|
+
import type { BroMouseEvent } from "./ui-capabilities.ts";
|
|
37
32
|
import { errorMessage } from "./util.ts";
|
|
38
33
|
|
|
39
|
-
export type Theme = ExtensionCommandContext["ui"]["theme"];
|
|
40
|
-
export type TuiLike = {
|
|
41
|
-
readonly mode: "regular" | "fullscreen";
|
|
42
|
-
readonly terminal?: { rows?: number; columns?: number; write?: (data: string) => void };
|
|
43
|
-
requestRender(): void;
|
|
44
|
-
};
|
|
45
|
-
|
|
46
34
|
const SHOW_TURNS_PRESETS = [1, 2, 3, 5, 8];
|
|
47
35
|
|
|
48
36
|
function showTurnsValues(current: number): string[] {
|
|
@@ -58,6 +46,9 @@ export function createConfigModal(
|
|
|
58
46
|
persistSettings: (settings: BroSettings) => Promise<void>,
|
|
59
47
|
): (tui: TuiLike, theme: Theme, keybindings: unknown, done: (value?: void) => void) => Component & { dispose?(): void } {
|
|
60
48
|
return (tui, theme, _keybindings, done) => {
|
|
49
|
+
// Size the host list once, then stop interaction if a later resize cannot show the whole
|
|
50
|
+
// component. SettingsList has no public resize API; clipping it would hide selection/actions.
|
|
51
|
+
const listRows = Math.max(1, modalHeight(tui) - 11);
|
|
61
52
|
let settings = initialSettings;
|
|
62
53
|
// The last settings actually confirmed on disk. A failed save reverts `settings` (and the
|
|
63
54
|
// whole displayed row set) back to this, so the screen never shows state that doesn't exist.
|
|
@@ -73,6 +64,12 @@ export function createConfigModal(
|
|
|
73
64
|
// that only actually happens once the in-flight (and any coalesced) save has settled, and
|
|
74
65
|
// only if it succeeded — a failure cancels the pending close so its notice stays visible.
|
|
75
66
|
let closeRequested = false;
|
|
67
|
+
let closed = false;
|
|
68
|
+
const close = () => {
|
|
69
|
+
if (closed) return;
|
|
70
|
+
closed = true;
|
|
71
|
+
done(undefined);
|
|
72
|
+
};
|
|
76
73
|
|
|
77
74
|
const modelPicker = (current: string, pickerDone: (value?: string) => void, capability?: Capability) => {
|
|
78
75
|
const defaultResolved = resolveModelEffort({ model: settings.model, effort: settings.effort }, families);
|
|
@@ -101,7 +98,7 @@ export function createConfigModal(
|
|
|
101
98
|
pickerDone(`${meta.prefix}${value.trim()}`);
|
|
102
99
|
}
|
|
103
100
|
};
|
|
104
|
-
const picker = new SelectList(options, Math.min(options.length, 8), getSelectListTheme());
|
|
101
|
+
const picker = new SelectList(options, Math.min(options.length, 8, listRows), getSelectListTheme());
|
|
105
102
|
const selectedIndex = capability && current === "Default" ? 0 : options.findIndex((option) => option.value === current);
|
|
106
103
|
picker.setSelectedIndex(Math.max(0, selectedIndex));
|
|
107
104
|
picker.onSelect = (item) => {
|
|
@@ -112,8 +109,18 @@ export function createConfigModal(
|
|
|
112
109
|
picker.onCancel = () => pickerDone();
|
|
113
110
|
const customLabel = () => enteringBackend ? EXTERNAL_BACKENDS[enteringBackend].label : "";
|
|
114
111
|
return {
|
|
115
|
-
|
|
112
|
+
// SettingsList draws an open submenu instead of its own list and hints, so the picker brings its own.
|
|
113
|
+
render: (width: number) => enteringBackend
|
|
114
|
+
? [
|
|
115
|
+
`${customLabel()} model ID:`,
|
|
116
|
+
...input.render(width),
|
|
117
|
+
"",
|
|
118
|
+
truncateToWidth(theme.fg("dim", " Enter save · Esc back"), width),
|
|
119
|
+
]
|
|
120
|
+
: [...picker.render(width), "", truncateToWidth(theme.fg("dim", " ↑/↓ choose · Enter select · Esc back"), width)],
|
|
116
121
|
invalidate: () => { picker.invalidate(); input.invalidate(); },
|
|
122
|
+
// SettingsList hands an open submenu its own events unchanged.
|
|
123
|
+
handleMouse: (event: BroMouseEvent) => (enteringBackend ? delegateMouse(input, event, { top: 1, left: 0, width: tui.terminal?.columns ?? 80, height: 1 }) : delegateMouse(picker, event)),
|
|
117
124
|
handleInput: (data: string) => {
|
|
118
125
|
if (enteringBackend && matchesKey(data, "escape")) pickerDone();
|
|
119
126
|
else if (enteringBackend) input.handleInput(data);
|
|
@@ -197,14 +204,17 @@ export function createConfigModal(
|
|
|
197
204
|
...CAPABILITIES.flatMap((capability) => [capabilityItems[capability].model, capabilityItems[capability].effort]),
|
|
198
205
|
];
|
|
199
206
|
|
|
200
|
-
|
|
207
|
+
let notice = "";
|
|
208
|
+
let interactive = true;
|
|
209
|
+
// Where the complete list was last drawn inside the overlay, for click forwarding.
|
|
210
|
+
let listArea: Area = { top: 0, left: 0, width: 0, height: 0 };
|
|
201
211
|
|
|
202
212
|
function runSave(toSave: BroSettings): void {
|
|
203
213
|
saving = true;
|
|
204
214
|
void persistSettings(toSave)
|
|
205
215
|
.then(() => {
|
|
206
216
|
savedSettings = toSave;
|
|
207
|
-
|
|
217
|
+
notice = "";
|
|
208
218
|
})
|
|
209
219
|
.catch((error: unknown) => {
|
|
210
220
|
// Restore the last state that is actually on disk: showing the failed, unsaved
|
|
@@ -213,7 +223,8 @@ export function createConfigModal(
|
|
|
213
223
|
queued = undefined;
|
|
214
224
|
closeRequested = false;
|
|
215
225
|
refresh();
|
|
216
|
-
|
|
226
|
+
// Outcome first: long notices are cut after three lines.
|
|
227
|
+
notice = formatNotice(theme, `Reverted to the last saved settings. Could not save settings: ${errorMessage(error)}`, "error");
|
|
217
228
|
})
|
|
218
229
|
.finally(() => {
|
|
219
230
|
saving = false;
|
|
@@ -224,7 +235,7 @@ export function createConfigModal(
|
|
|
224
235
|
runSave(next);
|
|
225
236
|
} else if (closeRequested) {
|
|
226
237
|
closeRequested = false;
|
|
227
|
-
|
|
238
|
+
close();
|
|
228
239
|
}
|
|
229
240
|
});
|
|
230
241
|
}
|
|
@@ -295,30 +306,56 @@ export function createConfigModal(
|
|
|
295
306
|
closeRequested = true;
|
|
296
307
|
return;
|
|
297
308
|
}
|
|
298
|
-
|
|
309
|
+
close();
|
|
299
310
|
};
|
|
300
311
|
|
|
301
|
-
const settingsList = new SettingsList(items, Math.min(items.length
|
|
302
|
-
const container = new Container();
|
|
303
|
-
container.addChild(new Text(theme.fg("accent", theme.bold("Bro · config"))));
|
|
304
|
-
container.addChild(new Text(theme.fg("dim", "Shared defaults, with optional overrides per capability")));
|
|
305
|
-
container.addChild(settingsList);
|
|
306
|
-
container.addChild(noticeText);
|
|
307
|
-
container.addChild(new Text(theme.fg("dim", "↑/↓ navigate · Enter select/change · Esc back/close")));
|
|
308
|
-
|
|
312
|
+
const settingsList = new SettingsList(items, Math.min(items.length, listRows), getSettingsListTheme(), onChange, requestClose);
|
|
309
313
|
return {
|
|
310
|
-
render: (
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
render: (width: number) => {
|
|
315
|
+
listArea = { top: 0, left: 0, width: 0, height: 0 };
|
|
316
|
+
const height = modalHeight(tui);
|
|
317
|
+
const innerWidth = Math.max(1, width - 2);
|
|
318
|
+
const list = settingsList.render(innerWidth);
|
|
319
|
+
// renderFrame shrinks a notice to one line before taking minBody rows.
|
|
320
|
+
interactive = list.length <= Math.max(0, height - 5 - (notice ? 1 : 0));
|
|
321
|
+
return renderFrame(theme, width, height, {
|
|
322
|
+
header: () => theme.fg(
|
|
323
|
+
interactive ? "accent" : "warning",
|
|
324
|
+
theme.bold(interactive ? "Bro · config" : "Bro · config · enlarge terminal"),
|
|
325
|
+
),
|
|
326
|
+
body: (rows) => {
|
|
327
|
+
if (!interactive) return [theme.fg("warning", "Enlarge the terminal to configure settings safely.")];
|
|
328
|
+
// The subtitle gives way before SettingsList's selection, scroll state and key hints.
|
|
329
|
+
const subtitle = rows > list.length
|
|
330
|
+
? [truncateToWidth(theme.fg("dim", "Shared defaults, with optional overrides per capability"), innerWidth)]
|
|
331
|
+
: [];
|
|
332
|
+
// Clicks only reach list rows that were actually drawn.
|
|
333
|
+
listArea = { top: 3 + subtitle.length, left: 1, width: innerWidth, height: Math.min(list.length, rows - subtitle.length) };
|
|
334
|
+
return [...subtitle, ...list];
|
|
335
|
+
},
|
|
336
|
+
notice,
|
|
337
|
+
hints: interactive ? undefined : "Esc close",
|
|
338
|
+
fill: false,
|
|
339
|
+
minBody: interactive ? list.length : 1,
|
|
340
|
+
});
|
|
317
341
|
},
|
|
318
|
-
invalidate: () =>
|
|
342
|
+
invalidate: () => settingsList.invalidate(),
|
|
343
|
+
handleMouse: (event: BroMouseEvent) => delegateMouse(settingsList, event, listArea),
|
|
319
344
|
handleInput: (data: string) => {
|
|
345
|
+
if (!interactive) {
|
|
346
|
+
// No picker is visible here: Esc closes config, still waiting for pending saves.
|
|
347
|
+
if (matchesKey(data, "escape")) {
|
|
348
|
+
requestClose();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (notice) {
|
|
352
|
+
notice = "";
|
|
353
|
+
tui.requestRender();
|
|
354
|
+
}
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
320
357
|
settingsList.handleInput?.(data);
|
|
321
|
-
tui.requestRender();
|
|
358
|
+
if (!closed) tui.requestRender();
|
|
322
359
|
},
|
|
323
360
|
};
|
|
324
361
|
};
|