@ian-pascoe/pi-advisor 0.6.0 → 0.7.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 +2 -2
- package/package.json +6 -6
- package/src/advisor-command.ts +26 -47
- package/src/advisor-evidence.ts +38 -270
- package/src/advisor-menu.ts +34 -253
- package/src/advisor-rendering.ts +185 -112
- package/src/advisor-settings.ts +26 -114
- package/src/index.ts +23 -11
package/src/advisor-menu.ts
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
|
+
import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { SettingsList, type Component, type SettingItem } from "@earendil-works/pi-tui";
|
|
1
3
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
SettingsList,
|
|
14
|
-
type Component,
|
|
15
|
-
type SelectItem,
|
|
16
|
-
type SettingItem,
|
|
17
|
-
type TUI,
|
|
18
|
-
type TuiMouseEvent,
|
|
19
|
-
type TuiMouseEventResult,
|
|
20
|
-
} from "@earendil-works/pi-tui";
|
|
4
|
+
cycleDisplay,
|
|
5
|
+
EditorChooser,
|
|
6
|
+
effectiveWithSource,
|
|
7
|
+
errorText,
|
|
8
|
+
ModelPicker,
|
|
9
|
+
nextCycleValue,
|
|
10
|
+
scopeRow,
|
|
11
|
+
SettingsMenu,
|
|
12
|
+
ValueInput,
|
|
13
|
+
type SettingsMenuUi,
|
|
14
|
+
} from "@ian-pascoe/pi-utils/settings-menu";
|
|
21
15
|
import { Value } from "typebox/value";
|
|
22
16
|
import { formatAdvisorOption, type AdvisorRenderTheme } from "./advisor-rendering.js";
|
|
23
17
|
import {
|
|
@@ -61,11 +55,8 @@ export interface AdvisorMenuHost {
|
|
|
61
55
|
}
|
|
62
56
|
|
|
63
57
|
/** Native UI collaborators supplied by `ctx.ui.custom`. */
|
|
64
|
-
export interface AdvisorMenuUi {
|
|
65
|
-
tui: TUI;
|
|
66
|
-
keybindings: KeybindingsManager;
|
|
58
|
+
export interface AdvisorMenuUi extends SettingsMenuUi {
|
|
67
59
|
theme: AdvisorRenderTheme;
|
|
68
|
-
externalEditorCommand?: string;
|
|
69
60
|
}
|
|
70
61
|
|
|
71
62
|
const thinkingLevels = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
@@ -119,12 +110,6 @@ const inputHints = {
|
|
|
119
110
|
reviewEvery: "turn, request, a number of turns, or inherit",
|
|
120
111
|
maxSessionTokens: "a token count, auto, or inherit",
|
|
121
112
|
} as const;
|
|
122
|
-
const listActions = [
|
|
123
|
-
"tui.select.up",
|
|
124
|
-
"tui.select.down",
|
|
125
|
-
"tui.select.confirm",
|
|
126
|
-
"tui.select.cancel",
|
|
127
|
-
] as const;
|
|
128
113
|
|
|
129
114
|
/** Convert one typed or selected menu value into a validated change. */
|
|
130
115
|
function parseAdvisorMenuValue(
|
|
@@ -164,188 +149,34 @@ function parseAdvisorMenuValue(
|
|
|
164
149
|
return { action: "set", key, patch: parseAdvisorOptions(patch, scope) };
|
|
165
150
|
}
|
|
166
151
|
|
|
167
|
-
function errorText(cause: unknown): string {
|
|
168
|
-
return cause instanceof Error ? cause.message : String(cause);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Single-line value entry with inline validation. */
|
|
172
|
-
class ValueInput implements Component {
|
|
173
|
-
private readonly input: Input;
|
|
174
|
-
private error: string | undefined;
|
|
175
|
-
|
|
176
|
-
constructor(
|
|
177
|
-
private readonly title: string,
|
|
178
|
-
private readonly hint: string,
|
|
179
|
-
private readonly theme: AdvisorRenderTheme,
|
|
180
|
-
submit: (text: string) => void,
|
|
181
|
-
cancel: () => void,
|
|
182
|
-
) {
|
|
183
|
-
this.input = new Input({ placeholder: hint });
|
|
184
|
-
this.input.focused = true;
|
|
185
|
-
this.input.onSubmit = (text) => {
|
|
186
|
-
try {
|
|
187
|
-
submit(text);
|
|
188
|
-
} catch (cause) {
|
|
189
|
-
this.error = errorText(cause);
|
|
190
|
-
}
|
|
191
|
-
};
|
|
192
|
-
this.input.onEscape = cancel;
|
|
193
|
-
}
|
|
194
|
-
handleInput(data: string): void {
|
|
195
|
-
this.error = undefined;
|
|
196
|
-
this.input.handleInput(data);
|
|
197
|
-
}
|
|
198
|
-
render(width: number): string[] {
|
|
199
|
-
return [
|
|
200
|
-
this.theme.bold(this.title),
|
|
201
|
-
...this.input.render(width),
|
|
202
|
-
this.error ? this.theme.fg("error", `✖ ${this.error}`) : this.theme.fg("dim", this.hint),
|
|
203
|
-
];
|
|
204
|
-
}
|
|
205
|
-
invalidate(): void {
|
|
206
|
-
this.input.invalidate();
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/** Fuzzy-searchable model list with an inherit choice. */
|
|
211
|
-
class ModelPicker implements Component {
|
|
212
|
-
private readonly input = new Input({ placeholder: "type to search" });
|
|
213
|
-
private list: SelectList;
|
|
214
|
-
|
|
215
|
-
constructor(
|
|
216
|
-
private readonly models: readonly string[],
|
|
217
|
-
private readonly choose: (value: string) => void,
|
|
218
|
-
private readonly cancel: () => void,
|
|
219
|
-
) {
|
|
220
|
-
this.input.focused = true;
|
|
221
|
-
this.list = this.createList("");
|
|
222
|
-
}
|
|
223
|
-
private createList(query: string): SelectList {
|
|
224
|
-
const items: SelectItem[] = ["inherit", ...this.models].map((value) => ({
|
|
225
|
-
value,
|
|
226
|
-
label: value,
|
|
227
|
-
}));
|
|
228
|
-
const list = new SelectList(
|
|
229
|
-
query ? fuzzyFilter(items, query, (item) => item.value) : items,
|
|
230
|
-
10,
|
|
231
|
-
getSelectListTheme(),
|
|
232
|
-
);
|
|
233
|
-
list.onSelect = (item) => this.choose(item.value);
|
|
234
|
-
list.onCancel = this.cancel;
|
|
235
|
-
return list;
|
|
236
|
-
}
|
|
237
|
-
handleInput(data: string): void {
|
|
238
|
-
const bindings = getKeybindings();
|
|
239
|
-
if (listActions.some((action) => bindings.matches(data, action))) {
|
|
240
|
-
this.list.handleInput(data);
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
this.input.handleInput(data);
|
|
244
|
-
this.list = this.createList(this.input.getValue());
|
|
245
|
-
}
|
|
246
|
-
render(width: number): string[] {
|
|
247
|
-
return [...this.input.render(width), ...this.list.render(width)];
|
|
248
|
-
}
|
|
249
|
-
invalidate(): void {
|
|
250
|
-
this.input.invalidate();
|
|
251
|
-
this.list.invalidate();
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/** Choose between editing the prompt in Pi's editor component and inheriting it. */
|
|
256
|
-
class PromptChooser implements Component {
|
|
257
|
-
private readonly list: SelectList;
|
|
258
|
-
private editor: ExtensionEditorComponent | undefined;
|
|
259
|
-
private error: string | undefined;
|
|
260
|
-
|
|
261
|
-
/** `submit` throws a user-facing message for an invalid prompt; the editor stays open. */
|
|
262
|
-
constructor(
|
|
263
|
-
ui: AdvisorMenuUi,
|
|
264
|
-
prompt: string,
|
|
265
|
-
submit: (text: string) => void,
|
|
266
|
-
inherit: () => void,
|
|
267
|
-
cancel: () => void,
|
|
268
|
-
) {
|
|
269
|
-
this.list = new SelectList(
|
|
270
|
-
[
|
|
271
|
-
{ value: "edit", label: "Edit…" },
|
|
272
|
-
{ value: "inherit", label: "inherit" },
|
|
273
|
-
],
|
|
274
|
-
2,
|
|
275
|
-
getSelectListTheme(),
|
|
276
|
-
);
|
|
277
|
-
this.list.onCancel = cancel;
|
|
278
|
-
this.list.onSelect = (item) => {
|
|
279
|
-
if (item.value === "inherit") {
|
|
280
|
-
inherit();
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
this.editor = new ExtensionEditorComponent(
|
|
284
|
-
ui.tui,
|
|
285
|
-
ui.keybindings,
|
|
286
|
-
"Advisor Prompt",
|
|
287
|
-
prompt,
|
|
288
|
-
(text) => {
|
|
289
|
-
try {
|
|
290
|
-
submit(text);
|
|
291
|
-
} catch (cause) {
|
|
292
|
-
this.error = ui.theme.fg("error", `✖ ${errorText(cause)}`);
|
|
293
|
-
}
|
|
294
|
-
},
|
|
295
|
-
cancel,
|
|
296
|
-
undefined,
|
|
297
|
-
ui.externalEditorCommand,
|
|
298
|
-
);
|
|
299
|
-
this.editor.focused = true;
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
handleInput(data: string): void {
|
|
303
|
-
this.error = undefined;
|
|
304
|
-
(this.editor ?? this.list).handleInput(data);
|
|
305
|
-
}
|
|
306
|
-
render(width: number): string[] {
|
|
307
|
-
const lines = (this.editor ?? this.list).render(width);
|
|
308
|
-
return this.error ? [...lines, this.error] : lines;
|
|
309
|
-
}
|
|
310
|
-
invalidate(): void {
|
|
311
|
-
(this.editor ?? this.list).invalidate();
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
152
|
/** Menu row ids that are not Advisor options. */
|
|
316
153
|
const actionRows = { resume: "resume", scope: "scope" } as const;
|
|
317
154
|
|
|
318
155
|
/** `/advisor` settings menu built from Pi's native settings list. */
|
|
319
|
-
export class AdvisorSettingsMenu
|
|
156
|
+
export class AdvisorSettingsMenu extends SettingsMenu {
|
|
320
157
|
private scope: AdvisorSettingScope = "session";
|
|
321
158
|
private view: AdvisorMenuView;
|
|
322
159
|
private rows: SettingItem[] = [];
|
|
323
|
-
private list: SettingsList;
|
|
324
|
-
private error: string | undefined;
|
|
325
160
|
private lastRow: string = actionRows.scope;
|
|
326
|
-
/** Edits run one at a time so each reads the result of the previous one. */
|
|
327
|
-
private pending: Promise<void> = Promise.resolve();
|
|
328
161
|
private submenuOpen = false;
|
|
329
162
|
private rebuildPending = false;
|
|
330
|
-
private readonly border: DynamicBorder;
|
|
331
163
|
|
|
332
164
|
constructor(
|
|
333
165
|
private readonly host: AdvisorMenuHost,
|
|
334
|
-
|
|
335
|
-
|
|
166
|
+
protected override readonly ui: AdvisorMenuUi,
|
|
167
|
+
done: () => void,
|
|
336
168
|
) {
|
|
169
|
+
super("Advisor settings", ui, done);
|
|
337
170
|
this.view = host.view();
|
|
338
|
-
this.
|
|
339
|
-
this.list = this.createList();
|
|
171
|
+
this.createList();
|
|
340
172
|
}
|
|
341
173
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
return this.pending;
|
|
174
|
+
protected headline(): readonly string[] {
|
|
175
|
+
return this.view.headline;
|
|
345
176
|
}
|
|
346
177
|
|
|
347
178
|
/** Re-read the host after external state changes, such as a Review starting. */
|
|
348
|
-
refresh(): void {
|
|
179
|
+
override refresh(): void {
|
|
349
180
|
const paused = this.view.paused;
|
|
350
181
|
try {
|
|
351
182
|
this.view = this.host.view();
|
|
@@ -365,8 +196,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
365
196
|
|
|
366
197
|
private rebuild(): void {
|
|
367
198
|
this.rebuildPending = false;
|
|
368
|
-
this.
|
|
369
|
-
this.list.selectItem(this.view.paused ? actionRows.resume : this.lastRow);
|
|
199
|
+
this.createList().selectItem(this.view.paused ? actionRows.resume : this.lastRow);
|
|
370
200
|
}
|
|
371
201
|
|
|
372
202
|
private createList(): SettingsList {
|
|
@@ -376,13 +206,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
376
206
|
...advisorOptionKeys,
|
|
377
207
|
];
|
|
378
208
|
this.rows = ids.map((id) => this.createRow(id));
|
|
379
|
-
return
|
|
380
|
-
this.rows,
|
|
381
|
-
this.rows.length,
|
|
382
|
-
getSettingsListTheme(),
|
|
383
|
-
(id, value) => this.change(id, value),
|
|
384
|
-
this.done,
|
|
385
|
-
);
|
|
209
|
+
return this.setList(this.rows, (id, value) => this.change(id, value));
|
|
386
210
|
}
|
|
387
211
|
|
|
388
212
|
private createRow(id: string): SettingItem {
|
|
@@ -394,14 +218,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
394
218
|
values: ["retry"],
|
|
395
219
|
description: "Retry the Paused Advisor with its current settings",
|
|
396
220
|
};
|
|
397
|
-
if (id === actionRows.scope)
|
|
398
|
-
return {
|
|
399
|
-
id,
|
|
400
|
-
label: "Scope",
|
|
401
|
-
currentValue: this.scope,
|
|
402
|
-
values: [...this.view.scopes],
|
|
403
|
-
description: "Where edits are written",
|
|
404
|
-
};
|
|
221
|
+
if (id === actionRows.scope) return scopeRow(this.scope, this.view.scopes);
|
|
405
222
|
return this.optionRow(advisorOptionKey(id));
|
|
406
223
|
}
|
|
407
224
|
|
|
@@ -470,8 +287,9 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
470
287
|
submenu: this.submenu(
|
|
471
288
|
key,
|
|
472
289
|
(close) =>
|
|
473
|
-
new
|
|
290
|
+
new EditorChooser(
|
|
474
291
|
this.ui,
|
|
292
|
+
"Advisor Prompt",
|
|
475
293
|
settings.prompt ?? "",
|
|
476
294
|
(text) => {
|
|
477
295
|
// Throws for an invalid prompt, keeping the editor open.
|
|
@@ -560,12 +378,14 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
560
378
|
|
|
561
379
|
/** This scope's own value, or what it inherits; notes when another scope overrides it. */
|
|
562
380
|
private cycleDisplay(key: CycleKey): string {
|
|
563
|
-
const own = cycleValue(key, this.view.authored[this.scope] ?? {});
|
|
564
381
|
const effective = cycleValue(key, this.view.settings);
|
|
565
382
|
const source = this.view.sources[key] ?? "default";
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
383
|
+
return cycleDisplay({
|
|
384
|
+
own: cycleValue(key, this.view.authored[this.scope] ?? {}),
|
|
385
|
+
inEffect: effective === undefined ? "observed agent" : effectiveWithSource(effective, source),
|
|
386
|
+
source,
|
|
387
|
+
scope: this.scope,
|
|
388
|
+
});
|
|
569
389
|
}
|
|
570
390
|
|
|
571
391
|
private change(id: string, value: string): void {
|
|
@@ -586,8 +406,7 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
586
406
|
// text is not one of the values, so ignore its suggestion.
|
|
587
407
|
const values = cycleValues[key];
|
|
588
408
|
const own = cycleValue(key, this.view.authored[this.scope] ?? {}) ?? "inherit";
|
|
589
|
-
const next =
|
|
590
|
-
values[(values.findIndex((option) => option === own) + 1) % values.length] ?? "inherit";
|
|
409
|
+
const next = nextCycleValue(values, own) ?? "inherit";
|
|
591
410
|
let change: AdvisorChange;
|
|
592
411
|
try {
|
|
593
412
|
change = parseAdvisorMenuValue(key, next, this.scope);
|
|
@@ -608,42 +427,4 @@ export class AdvisorSettingsMenu implements Component {
|
|
|
608
427
|
const scope = this.scope;
|
|
609
428
|
this.run(() => this.host.apply(scope, change));
|
|
610
429
|
}
|
|
611
|
-
|
|
612
|
-
private run(task: () => Promise<void>): void {
|
|
613
|
-
this.error = undefined;
|
|
614
|
-
this.pending = this.pending
|
|
615
|
-
.then(task)
|
|
616
|
-
.catch((cause: unknown) => {
|
|
617
|
-
this.error = errorText(cause);
|
|
618
|
-
})
|
|
619
|
-
.finally(() => {
|
|
620
|
-
this.refresh();
|
|
621
|
-
this.ui.tui.requestRender();
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
handleInput(data: string): void {
|
|
626
|
-
this.list.handleInput(data);
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
630
|
-
return this.list.handleMouse(event);
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
render(width: number): string[] {
|
|
634
|
-
const { theme } = this.ui;
|
|
635
|
-
return [
|
|
636
|
-
...this.border.render(width),
|
|
637
|
-
` ${theme.bold("Advisor settings")}`,
|
|
638
|
-
...this.view.headline.map((line) => ` ${line}`),
|
|
639
|
-
"",
|
|
640
|
-
...this.list.render(width),
|
|
641
|
-
...(this.error ? [theme.fg("error", ` ✖ ${this.error}`)] : []),
|
|
642
|
-
...this.border.render(width),
|
|
643
|
-
];
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
invalidate(): void {
|
|
647
|
-
this.list.invalidate();
|
|
648
|
-
}
|
|
649
430
|
}
|