@jwilger/pi-development-system 0.48.0 → 0.51.1
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/extensions/development-system.ts +1 -1
- package/package.json +2 -2
- package/skills/strict-lints/SKILL.md +2 -1
- package/src/core/profile.ts +3 -27
- package/src/core/spawn-overrides.ts +2 -2
- package/src/core/test-runner.ts +10 -3
- package/src/review/digest.ts +7 -1
- package/src/state/profile-detect.ts +27 -0
- package/src/subagents/VENDORED.md +1 -1
- package/src/subagents/index.ts +144 -60
- package/src/subagents/orch/inherited-tools.ts +3 -3
- package/src/subagents/orch/mailbox.ts +11 -7
- package/src/subagents/orch/manager.ts +90 -57
- package/src/subagents/orch/runtime.ts +38 -33
- package/src/subagents/orch/tools.ts +12 -10
- package/src/subagents/prefs/agent-import.ts +13 -13
- package/src/subagents/prefs/config.ts +85 -185
- package/src/subagents/prefs/import-discovery.ts +6 -6
- package/src/subagents/prefs/models.ts +6 -10
- package/src/subagents/prefs/settings.ts +38 -38
- package/src/subagents/types.ts +37 -34
- package/src/subagents/ui/agent-navigation-editor.ts +20 -13
- package/src/subagents/ui/dialog.ts +99 -84
- package/src/subagents/ui/import-picker.ts +28 -11
- package/src/subagents/ui/live-agent-view.ts +17 -5
- package/src/subagents/ui/model-picker.ts +26 -17
- package/src/subagents/ui/settings-ui.ts +16 -34
- package/src/subagents/ui/status-ui.ts +140 -51
- package/src/subagents/ui/thread-tree.ts +3 -9
- package/src/subagents/ui/ui.ts +162 -265
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/complexity/noVoid: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
// biome-ignore-all lint/correctness/noVoidTypeReturn: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
3
|
+
// biome-ignore-all lint/suspicious/noControlCharactersInRegex: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
4
|
+
// biome-ignore-all lint/suspicious/noEmptyBlockStatements: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
5
|
+
// biome-ignore-all lint/suspicious/noUnnecessaryConditions: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
6
|
+
// biome-ignore-all lint/suspicious/useAwait: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
7
|
import type {
|
|
3
8
|
ExtensionContext,
|
|
4
9
|
ExtensionUIContext,
|
|
@@ -6,6 +11,7 @@ import type {
|
|
|
6
11
|
Theme,
|
|
7
12
|
} from "@earendil-works/pi-coding-agent";
|
|
8
13
|
import {
|
|
14
|
+
type Component,
|
|
9
15
|
CURSOR_MARKER,
|
|
10
16
|
Editor,
|
|
11
17
|
getKeybindings,
|
|
@@ -13,11 +19,10 @@ import {
|
|
|
13
19
|
Key,
|
|
14
20
|
matchesKey,
|
|
15
21
|
stripTerminalSequences,
|
|
22
|
+
type TUI,
|
|
16
23
|
truncateToWidth,
|
|
17
24
|
visibleWidth,
|
|
18
25
|
wrapTextWithAnsi,
|
|
19
|
-
type Component,
|
|
20
|
-
type TUI,
|
|
21
26
|
} from "@earendil-works/pi-tui";
|
|
22
27
|
|
|
23
28
|
export interface DialogHost {
|
|
@@ -31,10 +36,10 @@ export interface DialogRow {
|
|
|
31
36
|
id: string;
|
|
32
37
|
label: string;
|
|
33
38
|
/** Trusted formatter receives sanitized text; generated ANSI styling is kept when clipping. */
|
|
34
|
-
renderLabel?: (label: string, theme: Theme) => string;
|
|
35
|
-
value?: string;
|
|
36
|
-
valueColor?: Parameters<Theme["fg"]>[0];
|
|
37
|
-
help?: string;
|
|
39
|
+
renderLabel?: ((label: string, theme: Theme) => string) | undefined;
|
|
40
|
+
value?: string | undefined;
|
|
41
|
+
valueColor?: Parameters<Theme["fg"]>[0] | undefined;
|
|
42
|
+
help?: string | undefined;
|
|
38
43
|
}
|
|
39
44
|
export const DIALOG_OPTIONS = {
|
|
40
45
|
overlay: true,
|
|
@@ -69,18 +74,14 @@ export function frameDialog(
|
|
|
69
74
|
const clipped = truncateToWidth(text, Math.max(0, columns), "", true);
|
|
70
75
|
return clipped + " ".repeat(Math.max(0, columns - visibleWidth(clipped)));
|
|
71
76
|
};
|
|
72
|
-
if (width < 4 || height < 4)
|
|
73
|
-
return [fit(theme.fg("muted", dialogText(title)), width)];
|
|
77
|
+
if (width < 4 || height < 4) return [fit(theme.fg("muted", dialogText(title)), width)];
|
|
74
78
|
const inner = width - 2;
|
|
75
79
|
const line = (text: string) =>
|
|
76
80
|
theme.fg("border", "│") + fit(text, inner) + theme.fg("border", "│");
|
|
77
81
|
const heading = truncateToWidth(` ${dialogText(title)} `, inner, "");
|
|
78
82
|
const top = theme.fg(
|
|
79
83
|
"border",
|
|
80
|
-
"
|
|
81
|
-
heading +
|
|
82
|
-
"─".repeat(Math.max(0, inner - visibleWidth(heading))) +
|
|
83
|
-
"╮",
|
|
84
|
+
`╭${heading}${"─".repeat(Math.max(0, inner - visibleWidth(heading)))}╮`,
|
|
84
85
|
);
|
|
85
86
|
// Pad to the full frame so centered overlays keep the same bounds between views.
|
|
86
87
|
const budget = height - 4;
|
|
@@ -90,9 +91,9 @@ export function frameDialog(
|
|
|
90
91
|
return [
|
|
91
92
|
top,
|
|
92
93
|
...content,
|
|
93
|
-
theme.fg("border", "
|
|
94
|
+
theme.fg("border", `├${"─".repeat(inner)}┤`),
|
|
94
95
|
line(theme.fg("dim", dialogText(footer))),
|
|
95
|
-
theme.fg("border", "
|
|
96
|
+
theme.fg("border", `╰${"─".repeat(inner)}╯`),
|
|
96
97
|
];
|
|
97
98
|
}
|
|
98
99
|
|
|
@@ -100,13 +101,24 @@ export function frameDialog(
|
|
|
100
101
|
class DialogEditor {
|
|
101
102
|
private editor: Editor;
|
|
102
103
|
private initialText: string;
|
|
104
|
+
private host: DialogEditorHost;
|
|
105
|
+
private theme: Theme;
|
|
106
|
+
readonly title: string;
|
|
107
|
+
readonly prefill: string;
|
|
108
|
+
private done: (value: string | undefined) => void;
|
|
109
|
+
|
|
103
110
|
constructor(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
111
|
+
host: DialogEditorHost,
|
|
112
|
+
theme: Theme,
|
|
113
|
+
title: string,
|
|
114
|
+
prefill: string,
|
|
115
|
+
done: (value: string | undefined) => void,
|
|
109
116
|
) {
|
|
117
|
+
this.host = host;
|
|
118
|
+
this.theme = theme;
|
|
119
|
+
this.title = title;
|
|
120
|
+
this.prefill = prefill;
|
|
121
|
+
this.done = done;
|
|
110
122
|
this.editor = new Editor(
|
|
111
123
|
host as ConstructorParameters<typeof Editor>[0],
|
|
112
124
|
{
|
|
@@ -173,8 +185,7 @@ export function dialogEditor(
|
|
|
173
185
|
prefill: string,
|
|
174
186
|
): Promise<string | undefined> {
|
|
175
187
|
return ctx.ui.custom(
|
|
176
|
-
(host, theme, _keys, done) =>
|
|
177
|
-
new DialogEditor(host, theme, title, prefill, done),
|
|
188
|
+
(host, theme, _keys, done) => new DialogEditor(host, theme, title, prefill, done),
|
|
178
189
|
DIALOG_OPTIONS,
|
|
179
190
|
);
|
|
180
191
|
}
|
|
@@ -183,16 +194,31 @@ export function dialogEditor(
|
|
|
183
194
|
class DialogMenu {
|
|
184
195
|
private selected = 0;
|
|
185
196
|
private viewport = 1;
|
|
197
|
+
private host: DialogHost;
|
|
198
|
+
private theme: Theme;
|
|
199
|
+
readonly title: string;
|
|
200
|
+
readonly rows: DialogRow[];
|
|
201
|
+
private done: (id: string | undefined) => void;
|
|
202
|
+
readonly footer: string;
|
|
203
|
+
private saveId: string | undefined;
|
|
204
|
+
|
|
186
205
|
constructor(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
206
|
+
host: DialogHost,
|
|
207
|
+
theme: Theme,
|
|
208
|
+
title: string,
|
|
209
|
+
rows: DialogRow[],
|
|
210
|
+
done: (id: string | undefined) => void,
|
|
192
211
|
selectedId?: string,
|
|
193
|
-
|
|
194
|
-
|
|
212
|
+
footer: string = "↑↓ navigate · Enter select · Esc close",
|
|
213
|
+
saveId?: string,
|
|
195
214
|
) {
|
|
215
|
+
this.host = host;
|
|
216
|
+
this.theme = theme;
|
|
217
|
+
this.title = title;
|
|
218
|
+
this.rows = rows;
|
|
219
|
+
this.done = done;
|
|
220
|
+
this.footer = footer;
|
|
221
|
+
this.saveId = saveId;
|
|
196
222
|
const index = rows.findIndex((row) => row.id === selectedId);
|
|
197
223
|
if (index >= 0) this.selected = index;
|
|
198
224
|
}
|
|
@@ -202,23 +228,16 @@ class DialogMenu {
|
|
|
202
228
|
handleInput(data: string): void {
|
|
203
229
|
const keys = getKeybindings();
|
|
204
230
|
if (keys.matches(data, "tui.select.cancel")) return this.done(undefined);
|
|
205
|
-
if (keys.matches(data, "tui.select.confirm"))
|
|
206
|
-
|
|
207
|
-
if (this.saveId && matchesKey(data, Key.ctrl("s")))
|
|
208
|
-
return this.done(this.saveId);
|
|
231
|
+
if (keys.matches(data, "tui.select.confirm")) return this.done(this.getSelectedId());
|
|
232
|
+
if (this.saveId && matchesKey(data, Key.ctrl("s"))) return this.done(this.saveId);
|
|
209
233
|
let offset = 0;
|
|
210
234
|
if (keys.matches(data, "tui.select.up")) offset = -1;
|
|
211
|
-
else if (keys.matches(data, "tui.select.down") || matchesKey(data, Key.tab))
|
|
212
|
-
offset = 1;
|
|
235
|
+
else if (keys.matches(data, "tui.select.down") || matchesKey(data, Key.tab)) offset = 1;
|
|
213
236
|
else if (matchesKey(data, Key.pageUp)) offset = -this.viewport;
|
|
214
237
|
else if (matchesKey(data, Key.pageDown)) offset = this.viewport;
|
|
215
238
|
else if (matchesKey(data, Key.home)) this.selected = 0;
|
|
216
|
-
else if (matchesKey(data, Key.end))
|
|
217
|
-
|
|
218
|
-
this.selected = Math.max(
|
|
219
|
-
0,
|
|
220
|
-
Math.min(this.rows.length - 1, this.selected + offset),
|
|
221
|
-
);
|
|
239
|
+
else if (matchesKey(data, Key.end)) this.selected = Math.max(0, this.rows.length - 1);
|
|
240
|
+
this.selected = Math.max(0, Math.min(this.rows.length - 1, this.selected + offset));
|
|
222
241
|
this.host.requestRender();
|
|
223
242
|
}
|
|
224
243
|
invalidate(): void {}
|
|
@@ -232,20 +251,17 @@ class DialogMenu {
|
|
|
232
251
|
this.viewport = Math.max(1, height - 6 - help.length);
|
|
233
252
|
const start = Math.max(
|
|
234
253
|
0,
|
|
235
|
-
Math.min(
|
|
236
|
-
this.selected - this.viewport + 1,
|
|
237
|
-
this.rows.length - this.viewport,
|
|
238
|
-
),
|
|
254
|
+
Math.min(this.selected - this.viewport + 1, this.rows.length - this.viewport),
|
|
239
255
|
);
|
|
240
256
|
const labelWidth = Math.min(28, Math.max(8, Math.floor(inner * 0.35)));
|
|
241
257
|
const body = [
|
|
242
258
|
this.theme.fg(
|
|
243
259
|
"dim",
|
|
244
|
-
` ${this.rows.length ? this.selected + 1 : 0}/${this.rows.length} · Field / Value`,
|
|
260
|
+
` ${this.rows.length > 0 ? this.selected + 1 : 0}/${this.rows.length} · Field / Value`,
|
|
245
261
|
),
|
|
246
262
|
...this.rows.slice(start, start + this.viewport).map((row, i) => {
|
|
247
263
|
const selected = start + i === this.selected;
|
|
248
|
-
const color = (text: string) => selected ? this.theme.fg("accent", text) : text;
|
|
264
|
+
const color = (text: string) => (selected ? this.theme.fg("accent", text) : text);
|
|
249
265
|
const plainLabel = dialogText(row.label);
|
|
250
266
|
const label = truncateToWidth(
|
|
251
267
|
row.renderLabel ? row.renderLabel(plainLabel, this.theme) : plainLabel,
|
|
@@ -255,22 +271,17 @@ class DialogMenu {
|
|
|
255
271
|
);
|
|
256
272
|
const padding = " ".repeat(Math.max(0, labelWidth - visibleWidth(label)));
|
|
257
273
|
const value = dialogText(row.value ?? "");
|
|
258
|
-
return
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
274
|
+
return (
|
|
275
|
+
color(`${selected ? "›" : " "} `) +
|
|
276
|
+
(row.renderLabel ? label : color(label)) +
|
|
277
|
+
color(`${padding} │ `) +
|
|
278
|
+
(row.valueColor ? this.theme.fg(row.valueColor, value) : color(value))
|
|
279
|
+
);
|
|
262
280
|
}),
|
|
263
281
|
"",
|
|
264
282
|
...help.map((line) => this.theme.fg("muted", ` ${line}`)),
|
|
265
283
|
];
|
|
266
|
-
return frameDialog(
|
|
267
|
-
this.theme,
|
|
268
|
-
width,
|
|
269
|
-
height,
|
|
270
|
-
this.title,
|
|
271
|
-
body,
|
|
272
|
-
this.footer,
|
|
273
|
-
);
|
|
284
|
+
return frameDialog(this.theme, width, height, this.title, body, this.footer);
|
|
274
285
|
}
|
|
275
286
|
}
|
|
276
287
|
|
|
@@ -287,7 +298,11 @@ export async function dialogMenu(
|
|
|
287
298
|
ctx: ExtensionContext,
|
|
288
299
|
title: string,
|
|
289
300
|
rows: DialogRow[],
|
|
290
|
-
options: {
|
|
301
|
+
options: {
|
|
302
|
+
selectedId?: string | undefined;
|
|
303
|
+
footer?: string | undefined;
|
|
304
|
+
saveId?: string | undefined;
|
|
305
|
+
} = {},
|
|
291
306
|
): Promise<string | undefined> {
|
|
292
307
|
return ctx.ui.custom(
|
|
293
308
|
(host, theme, _keys, done) =>
|
|
@@ -371,9 +386,7 @@ const dialogScopes = new WeakSet<ExtensionContext>();
|
|
|
371
386
|
|
|
372
387
|
function isPromise<T>(value: T | Promise<T>): value is Promise<T> {
|
|
373
388
|
return (
|
|
374
|
-
typeof value === "object" &&
|
|
375
|
-
value !== null &&
|
|
376
|
-
typeof (value as Promise<T>).then === "function"
|
|
389
|
+
typeof value === "object" && value !== null && typeof (value as Promise<T>).then === "function"
|
|
377
390
|
);
|
|
378
391
|
}
|
|
379
392
|
|
|
@@ -395,14 +408,25 @@ export class DialogSession {
|
|
|
395
408
|
private disposed = false;
|
|
396
409
|
private generation = 0;
|
|
397
410
|
private pending: PendingView | undefined;
|
|
398
|
-
private outerDone: ((result?:
|
|
411
|
+
private outerDone: ((result?: undefined) => void) | undefined;
|
|
412
|
+
|
|
413
|
+
private host: TUI;
|
|
414
|
+
|
|
415
|
+
private theme: Theme;
|
|
416
|
+
|
|
417
|
+
private keys: KeybindingsManager;
|
|
399
418
|
|
|
400
419
|
constructor(
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
done: (result?:
|
|
420
|
+
host: TUI,
|
|
421
|
+
theme: Theme,
|
|
422
|
+
keys: KeybindingsManager,
|
|
423
|
+
done: (result?: undefined) => void,
|
|
405
424
|
) {
|
|
425
|
+
this.host = host;
|
|
426
|
+
|
|
427
|
+
this.theme = theme;
|
|
428
|
+
|
|
429
|
+
this.keys = keys;
|
|
406
430
|
this.outerDone = done;
|
|
407
431
|
}
|
|
408
432
|
|
|
@@ -442,16 +466,14 @@ export class DialogSession {
|
|
|
442
466
|
const resolveOnce = (value: T) => {
|
|
443
467
|
if (settled) return;
|
|
444
468
|
settled = true;
|
|
445
|
-
if (this.pending?.generation === generation)
|
|
446
|
-
this.pending.settled = true;
|
|
469
|
+
if (this.pending?.generation === generation) this.pending.settled = true;
|
|
447
470
|
resolve(value);
|
|
448
471
|
};
|
|
449
472
|
const rejectOnce = (error: unknown) => {
|
|
450
473
|
if (settled) return;
|
|
451
474
|
settled = true;
|
|
452
475
|
this.inputLocked = true;
|
|
453
|
-
if (this.pending?.generation === generation)
|
|
454
|
-
this.pending.settled = true;
|
|
476
|
+
if (this.pending?.generation === generation) this.pending.settled = true;
|
|
455
477
|
reject(error);
|
|
456
478
|
};
|
|
457
479
|
this.pending = {
|
|
@@ -480,8 +502,7 @@ export class DialogSession {
|
|
|
480
502
|
void produced.then(
|
|
481
503
|
(component) => adopt(component),
|
|
482
504
|
(error: unknown) => {
|
|
483
|
-
if (settled || this.closed || generation !== this.generation)
|
|
484
|
-
return;
|
|
505
|
+
if (settled || this.closed || generation !== this.generation) return;
|
|
485
506
|
rejectOnce(error);
|
|
486
507
|
},
|
|
487
508
|
);
|
|
@@ -540,8 +561,7 @@ export class DialogSession {
|
|
|
540
561
|
return;
|
|
541
562
|
}
|
|
542
563
|
this.replaceChild(component);
|
|
543
|
-
this.inputLocked =
|
|
544
|
-
this.pending?.generation === generation ? this.pending.settled : true;
|
|
564
|
+
this.inputLocked = this.pending?.generation === generation ? this.pending.settled : true;
|
|
545
565
|
this.host.requestRender();
|
|
546
566
|
}
|
|
547
567
|
|
|
@@ -556,7 +576,7 @@ export class DialogSession {
|
|
|
556
576
|
|
|
557
577
|
private forwardFocus(): void {
|
|
558
578
|
const child = this.child;
|
|
559
|
-
if (!child
|
|
579
|
+
if (!(child && "focused" in child)) return;
|
|
560
580
|
// Read the setter's latest value; the first view can mount before showOverlay focuses us.
|
|
561
581
|
child.focused = this.focused;
|
|
562
582
|
}
|
|
@@ -569,18 +589,13 @@ export class DialogSession {
|
|
|
569
589
|
}
|
|
570
590
|
}
|
|
571
591
|
|
|
572
|
-
export function scopeDialogContext<T extends ExtensionContext>(
|
|
573
|
-
ctx: T,
|
|
574
|
-
session: DialogSession,
|
|
575
|
-
): T {
|
|
592
|
+
export function scopeDialogContext<T extends ExtensionContext>(ctx: T, session: DialogSession): T {
|
|
576
593
|
const source = ctx.ui;
|
|
577
594
|
const originalCustom = source.custom.bind(source);
|
|
578
595
|
const ui = Object.create(source) as ExtensionUIContext;
|
|
579
596
|
ui.custom = ((factory, options) => {
|
|
580
597
|
if (options?.overlay) return session.mount(factory);
|
|
581
|
-
return originalCustom(factory, options).finally(() =>
|
|
582
|
-
session.restoreFocus(),
|
|
583
|
-
);
|
|
598
|
+
return originalCustom(factory, options).finally(() => session.restoreFocus());
|
|
584
599
|
}) as ExtensionUIContext["custom"];
|
|
585
600
|
// Pi exposes ctx.ui as a getter; assignment cannot shadow an inherited accessor.
|
|
586
601
|
// Define an own property while keeping the remaining context getters live.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
|
|
2
1
|
import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import {
|
|
4
3
|
CURSOR_MARKER,
|
|
@@ -10,10 +9,10 @@ import {
|
|
|
10
9
|
} from "@earendil-works/pi-tui";
|
|
11
10
|
import {
|
|
12
11
|
DIALOG_OPTIONS,
|
|
12
|
+
type DialogHost,
|
|
13
13
|
dialogHeight,
|
|
14
14
|
dialogText,
|
|
15
15
|
frameDialog,
|
|
16
|
-
type DialogHost,
|
|
17
16
|
} from "./dialog.ts";
|
|
18
17
|
|
|
19
18
|
export interface ImportPickerItem {
|
|
@@ -76,12 +75,28 @@ class ImportPicker {
|
|
|
76
75
|
private viewport = 1;
|
|
77
76
|
private readonly checked = new Set<string>();
|
|
78
77
|
|
|
78
|
+
private host: DialogHost;
|
|
79
|
+
|
|
80
|
+
private theme: Theme;
|
|
81
|
+
|
|
82
|
+
private readonly items: readonly ImportPickerItem[];
|
|
83
|
+
|
|
84
|
+
private done: (ids: string[] | undefined) => void;
|
|
85
|
+
|
|
79
86
|
constructor(
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
) {
|
|
87
|
+
host: DialogHost,
|
|
88
|
+
theme: Theme,
|
|
89
|
+
items: readonly ImportPickerItem[],
|
|
90
|
+
done: (ids: string[] | undefined) => void,
|
|
91
|
+
) {
|
|
92
|
+
this.host = host;
|
|
93
|
+
|
|
94
|
+
this.theme = theme;
|
|
95
|
+
|
|
96
|
+
this.items = items;
|
|
97
|
+
|
|
98
|
+
this.done = done;
|
|
99
|
+
}
|
|
85
100
|
|
|
86
101
|
/** Selected ids in source order. Empty until the user toggles a row. */
|
|
87
102
|
getSelectedIds(): string[] {
|
|
@@ -99,7 +114,9 @@ class ImportPicker {
|
|
|
99
114
|
return this.cursor;
|
|
100
115
|
}
|
|
101
116
|
|
|
102
|
-
invalidate(): void {
|
|
117
|
+
invalidate(): void {
|
|
118
|
+
// Nothing is cached, so there is nothing to invalidate.
|
|
119
|
+
}
|
|
103
120
|
|
|
104
121
|
handleInput(data: string): void {
|
|
105
122
|
const keys = getKeybindings();
|
|
@@ -155,8 +172,8 @@ class ImportPicker {
|
|
|
155
172
|
if (listCount > 0) body.push(this.theme.fg("muted", " No external agents to import"));
|
|
156
173
|
} else {
|
|
157
174
|
const rows = this.items.slice(start, start + listCount);
|
|
158
|
-
for (
|
|
159
|
-
body.push(this.row(
|
|
175
|
+
for (const [offset, row] of rows.entries())
|
|
176
|
+
body.push(this.row(row, start + offset === this.cursor));
|
|
160
177
|
}
|
|
161
178
|
if (gap) body.push("");
|
|
162
179
|
for (const line of detail.slice(0, detailCount)) body.push(this.theme.fg("muted", ` ${line}`));
|
|
@@ -164,7 +181,7 @@ class ImportPicker {
|
|
|
164
181
|
}
|
|
165
182
|
|
|
166
183
|
private countLine(start: number, shown: number): string {
|
|
167
|
-
const position = this.items.length ? this.cursor + 1 : 0;
|
|
184
|
+
const position = this.items.length > 0 ? this.cursor + 1 : 0;
|
|
168
185
|
return this.theme.fg(
|
|
169
186
|
"dim",
|
|
170
187
|
` ${this.scrollHint(start, shown)}${this.checked.size} selected · ${position}/${this.items.length}`,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
wrapTextWithAnsi,
|
|
14
14
|
} from "@earendil-works/pi-tui";
|
|
15
15
|
import type { ThreadService, TranscriptSnapshot } from "../types.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { type DialogHost, dialogText } from "./dialog.ts";
|
|
17
17
|
import { AGENT_PROGRESS_INTERVAL, agentProgressIcon, agentTypeLabel } from "./ui.ts";
|
|
18
18
|
|
|
19
19
|
/** Transcript detail level cycled by the t key: full → preview → compact. */
|
|
@@ -66,7 +66,10 @@ export class LiveAgentView {
|
|
|
66
66
|
private unsubscribe: (() => void) | undefined;
|
|
67
67
|
private renderTimer: ReturnType<typeof setTimeout> | undefined;
|
|
68
68
|
private progressTimer: ReturnType<typeof setInterval> | undefined;
|
|
69
|
-
private cache = new WeakMap<
|
|
69
|
+
private cache = new WeakMap<
|
|
70
|
+
object,
|
|
71
|
+
{ width: number; detail: TranscriptDetail; lines: string[] }
|
|
72
|
+
>();
|
|
70
73
|
private scroll: ScrollView;
|
|
71
74
|
private content: string[] = [];
|
|
72
75
|
private restoreViewport = true;
|
|
@@ -188,7 +191,10 @@ export class LiveAgentView {
|
|
|
188
191
|
|
|
189
192
|
private syncProgress(): void {
|
|
190
193
|
const state = this.snapshot?.thread?.state;
|
|
191
|
-
const active =
|
|
194
|
+
const active =
|
|
195
|
+
this.nerdFontIcons &&
|
|
196
|
+
!this.disposed &&
|
|
197
|
+
!this.error &&
|
|
192
198
|
(state === "starting" || state === "running");
|
|
193
199
|
if (!active) {
|
|
194
200
|
if (this.progressTimer !== undefined) clearInterval(this.progressTimer);
|
|
@@ -333,7 +339,11 @@ export class LiveAgentView {
|
|
|
333
339
|
if (this.detail === "compact")
|
|
334
340
|
return [this.theme.fg("dim", "[Thinking hidden · t cycles view]")];
|
|
335
341
|
// Thinking is model-authored Markdown; preview truncates the rendered lines, not the source.
|
|
336
|
-
const rendered = this.markdown(
|
|
342
|
+
const rendered = this.markdown(
|
|
343
|
+
String(block.thinking ?? "[redacted thinking]"),
|
|
344
|
+
width,
|
|
345
|
+
true,
|
|
346
|
+
);
|
|
337
347
|
if (this.detail === "full" || rendered.length <= 3) return rendered;
|
|
338
348
|
return [...rendered.slice(0, 3), this.theme.fg("dim", "...")];
|
|
339
349
|
}
|
|
@@ -449,7 +459,9 @@ export class LiveAgentView {
|
|
|
449
459
|
}
|
|
450
460
|
case "ls": {
|
|
451
461
|
const limit = num("limit");
|
|
452
|
-
lines = [
|
|
462
|
+
lines = [
|
|
463
|
+
`${title("ls")} ${path ?? "."}${limit != null ? detail(` (limit ${limit})`) : ""}`,
|
|
464
|
+
];
|
|
453
465
|
break;
|
|
454
466
|
}
|
|
455
467
|
default:
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
1
|
+
// biome-ignore-all lint/suspicious/noControlCharactersInRegex: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
// biome-ignore-all lint/suspicious/useAwait: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
3
3
|
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
4
4
|
import {
|
|
5
|
-
getSelectListTheme,
|
|
6
5
|
type ExtensionCommandContext,
|
|
6
|
+
getSelectListTheme,
|
|
7
7
|
type ScopedModel,
|
|
8
8
|
type Theme,
|
|
9
9
|
} from "@earendil-works/pi-coding-agent";
|
|
@@ -13,14 +13,15 @@ import {
|
|
|
13
13
|
Input,
|
|
14
14
|
Key,
|
|
15
15
|
matchesKey,
|
|
16
|
+
type SelectItem,
|
|
16
17
|
SelectList,
|
|
17
18
|
stripTerminalSequences,
|
|
18
|
-
truncateToWidth,
|
|
19
19
|
Text,
|
|
20
|
-
|
|
20
|
+
truncateToWidth,
|
|
21
21
|
} from "@earendil-works/pi-tui";
|
|
22
|
+
import fuzzysort, { type SnapshotKeys } from "fuzzysort";
|
|
22
23
|
import { modelIdentity } from "../prefs/models.ts";
|
|
23
|
-
import { dialogHeight, frameDialog
|
|
24
|
+
import { DIALOG_OPTIONS, dialogHeight, frameDialog } from "./dialog.ts";
|
|
24
25
|
|
|
25
26
|
const MODEL_EDITOR_LAYOUT = {
|
|
26
27
|
minPrimaryColumnWidth: 24,
|
|
@@ -44,8 +45,8 @@ interface OrderedModelEditorComponentOptions {
|
|
|
44
45
|
theme: Theme;
|
|
45
46
|
availableModels: readonly AvailableModel[];
|
|
46
47
|
scopedModels: readonly ScopedModel[];
|
|
47
|
-
initialModels?: readonly string[];
|
|
48
|
-
modelSuggestions?: readonly string[];
|
|
48
|
+
initialModels?: readonly string[] | undefined;
|
|
49
|
+
modelSuggestions?: readonly string[] | undefined;
|
|
49
50
|
onDone(models: string[]): void;
|
|
50
51
|
onCancel(): void;
|
|
51
52
|
}
|
|
@@ -133,7 +134,7 @@ class OrderedModelEditorComponent extends Container {
|
|
|
133
134
|
this.theme.fg(
|
|
134
135
|
"muted",
|
|
135
136
|
this.withScopeWarning(
|
|
136
|
-
this.suggestedScores.size
|
|
137
|
+
this.suggestedScores.size > 0
|
|
137
138
|
? "Selected first; suggested matches scoped first, then other models; the rest A–Z. Type to search."
|
|
138
139
|
: "Selected first. Search by provider/id or model name; other models A–Z.",
|
|
139
140
|
),
|
|
@@ -244,7 +245,12 @@ class OrderedModelEditorComponent extends Container {
|
|
|
244
245
|
const next = index + (earlier ? -1 : 1);
|
|
245
246
|
// Consume reorder keys even on unselected models or at a list boundary.
|
|
246
247
|
if (index >= 0 && next >= 0 && next < this.draft.length) {
|
|
247
|
-
|
|
248
|
+
const moved = this.draft[index];
|
|
249
|
+
const other = this.draft[next];
|
|
250
|
+
if (moved !== undefined && other !== undefined) {
|
|
251
|
+
this.draft[index] = other;
|
|
252
|
+
this.draft[next] = moved;
|
|
253
|
+
}
|
|
248
254
|
this.refreshPickerList(selected);
|
|
249
255
|
this.tui.requestRender();
|
|
250
256
|
}
|
|
@@ -271,6 +277,11 @@ class OrderedModelEditorComponent extends Container {
|
|
|
271
277
|
this.tui.requestRender();
|
|
272
278
|
}
|
|
273
279
|
|
|
280
|
+
private fallbackIndex(focusSearchResult: boolean, previousIndex: number): number {
|
|
281
|
+
if (focusSearchResult) return 0;
|
|
282
|
+
return Math.max(0, Math.min(previousIndex, this.currentItems.length - 1));
|
|
283
|
+
}
|
|
284
|
+
|
|
274
285
|
private refreshPickerList(selectedValue?: string, focusSearchResult = false): void {
|
|
275
286
|
const previousIndex = this.currentItems.findIndex(
|
|
276
287
|
(item) => item.value === this.selectList.getSelectedItem()?.value,
|
|
@@ -314,11 +325,7 @@ class OrderedModelEditorComponent extends Container {
|
|
|
314
325
|
)
|
|
315
326
|
: this.currentItems.findIndex((item) => item.value === selectedValue);
|
|
316
327
|
list.setSelectedIndex(
|
|
317
|
-
preferredIndex >= 0
|
|
318
|
-
? preferredIndex
|
|
319
|
-
: focusSearchResult
|
|
320
|
-
? 0
|
|
321
|
-
: Math.max(0, Math.min(previousIndex, this.currentItems.length - 1)),
|
|
328
|
+
preferredIndex >= 0 ? preferredIndex : this.fallbackIndex(focusSearchResult, previousIndex),
|
|
322
329
|
);
|
|
323
330
|
list.onSelect = (item) => {
|
|
324
331
|
if (this.currentItems.find((row) => row.value === item.value)?.group === ACTIONS_GROUP) {
|
|
@@ -380,7 +387,8 @@ class OrderedModelEditorComponent extends Container {
|
|
|
380
387
|
}
|
|
381
388
|
}
|
|
382
389
|
const bySuggestionScore = (a: AvailableModel, b: AvailableModel) =>
|
|
383
|
-
this.suggestedScores.get(modelIdentity(b))
|
|
390
|
+
(this.suggestedScores.get(modelIdentity(b)) ?? 0) -
|
|
391
|
+
(this.suggestedScores.get(modelIdentity(a)) ?? 0);
|
|
384
392
|
// Stable score sorting preserves alphabetical order for ties. The last
|
|
385
393
|
// tier retains the registry's alphabetical order, irrespective of scope.
|
|
386
394
|
scoped.sort(bySuggestionScore);
|
|
@@ -397,7 +405,8 @@ class OrderedModelEditorComponent extends Container {
|
|
|
397
405
|
...items(other, "Suggested matches · other models"),
|
|
398
406
|
...items(
|
|
399
407
|
remaining,
|
|
400
|
-
|
|
408
|
+
|
|
409
|
+
scoped.length > 0 || other.length > 0 ? "All other models · A–Z" : "All models · A–Z",
|
|
401
410
|
),
|
|
402
411
|
];
|
|
403
412
|
}
|