@jwilger/pi-development-system 0.50.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/package.json +2 -2
- package/skills/strict-lints/SKILL.md +2 -1
- package/src/core/spawn-overrides.ts +2 -2
- package/src/core/test-runner.ts +8 -2
- package/src/review/digest.ts +7 -1
- 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 +7 -1
- 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 +22 -19
- 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,7 @@
|
|
|
1
|
-
//
|
|
1
|
+
// biome-ignore-all lint/complexity/noExcessiveCognitiveComplexity: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
|
+
// biome-ignore-all lint/complexity/noVoid: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
3
|
+
// biome-ignore-all lint/suspicious/noShadow: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
4
|
+
// biome-ignore-all lint/suspicious/noUnnecessaryConditions: vendored upstream TUI/config code with no tests; rewriting it risks behaviour (see src/subagents/VENDORED.md)
|
|
2
5
|
import type {
|
|
3
6
|
ExtensionCommandContext,
|
|
4
7
|
ExtensionContext,
|
|
@@ -7,30 +10,34 @@ import type {
|
|
|
7
10
|
} from "@earendil-works/pi-coding-agent";
|
|
8
11
|
import {
|
|
9
12
|
getKeybindings,
|
|
10
|
-
setKeybindings,
|
|
11
13
|
Key,
|
|
12
|
-
matchesKey,
|
|
13
14
|
type Keybinding,
|
|
15
|
+
matchesKey,
|
|
16
|
+
setKeybindings,
|
|
14
17
|
} from "@earendil-works/pi-tui";
|
|
18
|
+
import type { ThreadService } from "../types.ts";
|
|
15
19
|
import {
|
|
16
20
|
canOpenDialog,
|
|
17
21
|
DIALOG_OPTIONS,
|
|
18
22
|
type DialogHost,
|
|
19
23
|
DialogSession,
|
|
20
|
-
scopeDialogContext,
|
|
21
|
-
dialogMenu,
|
|
22
24
|
dialogEditor,
|
|
23
25
|
dialogHeight,
|
|
26
|
+
dialogMenu,
|
|
24
27
|
dialogText,
|
|
25
28
|
frameDialog,
|
|
29
|
+
scopeDialogContext,
|
|
26
30
|
} from "./dialog.ts";
|
|
27
|
-
import type
|
|
31
|
+
import { type AgentViewportState, fillViewport, LiveAgentView } from "./live-agent-view.ts";
|
|
28
32
|
import { buildStatusTree, type StatusRow } from "./thread-tree.ts";
|
|
29
33
|
import {
|
|
30
|
-
AGENT_PROGRESS_INTERVAL,
|
|
34
|
+
AGENT_PROGRESS_INTERVAL,
|
|
35
|
+
agentProgressIcon,
|
|
36
|
+
agentTypeBadge,
|
|
37
|
+
agentTypeLabel,
|
|
38
|
+
showThreads,
|
|
39
|
+
threadMetrics,
|
|
31
40
|
} from "./ui.ts";
|
|
32
|
-
import { LiveAgentView, fillViewport, type AgentViewportState } from "./live-agent-view.ts";
|
|
33
|
-
|
|
34
41
|
|
|
35
42
|
const ROOT = "/root";
|
|
36
43
|
const INACTIVE_STATES = new Set(["paused", "stopped", "failed", "completed"]);
|
|
@@ -55,7 +62,7 @@ function lexicalParent(path: string): string | null {
|
|
|
55
62
|
function treeParent(rows: StatusRow[], index: number): StatusRow | undefined {
|
|
56
63
|
const depth = rows[index]?.prefix.length ?? 0;
|
|
57
64
|
for (let cursor = index - 1; cursor >= 0; cursor--) {
|
|
58
|
-
if (rows[cursor]
|
|
65
|
+
if ((rows[cursor]?.prefix.length ?? 0) < depth) return rows[cursor];
|
|
59
66
|
}
|
|
60
67
|
return undefined;
|
|
61
68
|
}
|
|
@@ -96,24 +103,59 @@ class StatusDialog {
|
|
|
96
103
|
private refreshDelay = 0;
|
|
97
104
|
private refreshing = false;
|
|
98
105
|
|
|
106
|
+
private host: DialogHost;
|
|
107
|
+
|
|
108
|
+
private theme: Theme;
|
|
109
|
+
|
|
110
|
+
private service: ThreadService;
|
|
111
|
+
|
|
112
|
+
private done: (path: string | undefined) => void;
|
|
113
|
+
|
|
114
|
+
private title: string;
|
|
115
|
+
|
|
116
|
+
private navigation:
|
|
117
|
+
| {
|
|
118
|
+
state: AgentTreeState;
|
|
119
|
+
actions(path: string): void;
|
|
120
|
+
fullscreen?: boolean;
|
|
121
|
+
}
|
|
122
|
+
| undefined;
|
|
123
|
+
|
|
124
|
+
private nerdFontIcons: boolean;
|
|
125
|
+
|
|
99
126
|
constructor(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
host: DialogHost,
|
|
128
|
+
theme: Theme,
|
|
129
|
+
service: ThreadService,
|
|
130
|
+
done: (path: string | undefined) => void,
|
|
104
131
|
selected?: string,
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
title: string = TITLE,
|
|
133
|
+
navigation?: {
|
|
107
134
|
state: AgentTreeState;
|
|
108
135
|
actions(path: string): void;
|
|
109
136
|
fullscreen?: boolean;
|
|
110
137
|
},
|
|
111
|
-
|
|
138
|
+
nerdFontIcons: boolean = false,
|
|
112
139
|
) {
|
|
140
|
+
this.host = host;
|
|
141
|
+
|
|
142
|
+
this.theme = theme;
|
|
143
|
+
|
|
144
|
+
this.service = service;
|
|
145
|
+
|
|
146
|
+
this.done = done;
|
|
147
|
+
|
|
148
|
+
this.title = title;
|
|
149
|
+
|
|
150
|
+
this.navigation = navigation;
|
|
151
|
+
|
|
152
|
+
this.nerdFontIcons = nerdFontIcons;
|
|
113
153
|
this.state = navigation?.state ?? { selectedPath: selected || ROOT, collapsed: new Set() };
|
|
114
154
|
}
|
|
115
155
|
|
|
116
|
-
invalidate(): void {
|
|
156
|
+
invalidate(): void {
|
|
157
|
+
// Nothing is cached, so there is nothing to invalidate.
|
|
158
|
+
}
|
|
117
159
|
|
|
118
160
|
/** Poll retained status, accelerating the existing timer only for animated live agents. */
|
|
119
161
|
startRefresh(): void {
|
|
@@ -130,9 +172,15 @@ class StatusDialog {
|
|
|
130
172
|
|
|
131
173
|
private syncRefresh(): void {
|
|
132
174
|
if (!this.refreshing) return;
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
175
|
+
const animated =
|
|
176
|
+
this.nerdFontIcons &&
|
|
177
|
+
this.service
|
|
178
|
+
.list()
|
|
179
|
+
.some(
|
|
180
|
+
(thread) =>
|
|
181
|
+
thread.path !== ROOT && (thread.state === "starting" || thread.state === "running"),
|
|
182
|
+
);
|
|
183
|
+
const delay = animated ? AGENT_PROGRESS_INTERVAL : 1000;
|
|
136
184
|
if (this.timer !== undefined && this.refreshDelay === delay) return;
|
|
137
185
|
this.stopRefresh();
|
|
138
186
|
this.refreshDelay = delay;
|
|
@@ -154,14 +202,18 @@ class StatusDialog {
|
|
|
154
202
|
private stopAll(): void {
|
|
155
203
|
const live = this.service
|
|
156
204
|
.list()
|
|
157
|
-
.filter(
|
|
205
|
+
.filter(
|
|
206
|
+
(thread) =>
|
|
207
|
+
thread.path !== ROOT && (thread.state === "starting" || thread.state === "running"),
|
|
208
|
+
)
|
|
158
209
|
.map((thread) => thread.path);
|
|
159
210
|
const tops = live.filter((path) => !live.some((other) => path.startsWith(`${other}/`)));
|
|
160
211
|
for (const path of tops) {
|
|
161
|
-
|
|
212
|
+
this.service
|
|
162
213
|
.stop(path)
|
|
163
|
-
.catch(() =>
|
|
164
|
-
.finally(() => this.host.requestRender())
|
|
214
|
+
.catch(() => undefined)
|
|
215
|
+
.finally(() => this.host.requestRender())
|
|
216
|
+
.catch(() => undefined);
|
|
165
217
|
}
|
|
166
218
|
this.host.requestRender();
|
|
167
219
|
}
|
|
@@ -241,8 +293,18 @@ class StatusDialog {
|
|
|
241
293
|
this.host.requestRender();
|
|
242
294
|
}
|
|
243
295
|
|
|
296
|
+
private markerFor(row: StatusRow): string {
|
|
297
|
+
if (!row.hasChildren) return "";
|
|
298
|
+
return this.collapsed.has(row.path) ? "▸ " : "▾ ";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private missingThreadText(row: StatusRow | undefined): string {
|
|
302
|
+
if (row?.path !== ROOT) return "Missing parent";
|
|
303
|
+
return agentCount(this.service.list()) ? "Main Pi session" : "Main Pi session — No agents yet";
|
|
304
|
+
}
|
|
305
|
+
|
|
244
306
|
private line(row: StatusRow, selected: boolean): string {
|
|
245
|
-
const marker =
|
|
307
|
+
const marker = this.markerFor(row);
|
|
246
308
|
const head = ` ${row.prefix}${selected ? "›" : " "} ${marker}`;
|
|
247
309
|
const thread = row.thread;
|
|
248
310
|
if (!thread) {
|
|
@@ -253,14 +315,16 @@ class StatusDialog {
|
|
|
253
315
|
if (inactive) {
|
|
254
316
|
// Completed fades furthest (dim); the state word keeps a theme hint for done/failed.
|
|
255
317
|
const base = thread.state === "completed" ? "dim" : "muted";
|
|
256
|
-
const hint =
|
|
257
|
-
thread.state === "completed" ? "success" : thread.state === "failed" ? "error" : base;
|
|
318
|
+
const hint = hintFor(thread.state, base);
|
|
258
319
|
const state = dialogText(thread.state);
|
|
259
320
|
const path = dialogText(thread.path);
|
|
260
321
|
const status = dialogText(thread.status);
|
|
261
322
|
return [
|
|
262
323
|
this.theme.fg(selected ? "accent" : base, head),
|
|
263
|
-
this.theme.fg(
|
|
324
|
+
this.theme.fg(
|
|
325
|
+
base,
|
|
326
|
+
`${agentTypeLabel(thread.type, thread.icon, this.nerdFontIcons)} ${path} `,
|
|
327
|
+
),
|
|
264
328
|
this.theme.fg(hint, state),
|
|
265
329
|
this.theme.fg(base, ` ${status}`),
|
|
266
330
|
].join("");
|
|
@@ -268,8 +332,13 @@ class StatusDialog {
|
|
|
268
332
|
const rest = [thread.path, thread.state, thread.status]
|
|
269
333
|
.map((part) => dialogText(part))
|
|
270
334
|
.join(" ");
|
|
271
|
-
const badge = agentTypeBadge(
|
|
272
|
-
|
|
335
|
+
const badge = agentTypeBadge(
|
|
336
|
+
thread.type,
|
|
337
|
+
thread.color,
|
|
338
|
+
this.theme,
|
|
339
|
+
agentProgressIcon(thread, this.nerdFontIcons),
|
|
340
|
+
this.nerdFontIcons,
|
|
341
|
+
);
|
|
273
342
|
if (selected)
|
|
274
343
|
return `${this.theme.fg("accent", head)}${badge} ${this.theme.fg("accent", rest)}`;
|
|
275
344
|
return `${head}${badge} ${rest}`;
|
|
@@ -277,10 +346,7 @@ class StatusDialog {
|
|
|
277
346
|
|
|
278
347
|
private detail(row: StatusRow | undefined): string[] {
|
|
279
348
|
if (!row?.thread) {
|
|
280
|
-
return [
|
|
281
|
-
` ${row?.path === ROOT ? (agentCount(this.service.list()) ? "Main Pi session" : "Main Pi session — No agents yet") : "Missing parent"}`,
|
|
282
|
-
"",
|
|
283
|
-
];
|
|
349
|
+
return [` ${this.missingThreadText(row)}`, ""];
|
|
284
350
|
}
|
|
285
351
|
return [` task ${dialogText(row.thread.task)}`, ` ${threadMetrics(row.thread)}`];
|
|
286
352
|
}
|
|
@@ -312,9 +378,7 @@ class StatusDialog {
|
|
|
312
378
|
height,
|
|
313
379
|
this.heading(rows, start, treeCount),
|
|
314
380
|
body,
|
|
315
|
-
this.navigation
|
|
316
|
-
? "Esc main · ↑↓ select · ←→ fold · i Actions · Ctrl+C stop all"
|
|
317
|
-
: FOOTER,
|
|
381
|
+
this.navigation ? "Esc main · ↑↓ select · ←→ fold · i Actions · Ctrl+C stop all" : FOOTER,
|
|
318
382
|
);
|
|
319
383
|
}
|
|
320
384
|
|
|
@@ -333,10 +397,10 @@ async function showAgentDialog(
|
|
|
333
397
|
ctx: ExtensionCommandContext,
|
|
334
398
|
service: ThreadService,
|
|
335
399
|
options: {
|
|
336
|
-
title?: string;
|
|
337
|
-
selected?: string;
|
|
338
|
-
inspect?: (path: string) => Promise<void
|
|
339
|
-
nerdFontIcons?: boolean;
|
|
400
|
+
title?: string | undefined;
|
|
401
|
+
selected?: string | undefined;
|
|
402
|
+
inspect?: ((path: string) => Promise<void>) | undefined;
|
|
403
|
+
nerdFontIcons?: boolean | undefined;
|
|
340
404
|
} = {},
|
|
341
405
|
): Promise<void> {
|
|
342
406
|
const open = options.inspect ?? ((path: string) => showThreads(ctx, service, path));
|
|
@@ -347,7 +411,16 @@ async function showAgentDialog(
|
|
|
347
411
|
let dialog: StatusDialog | undefined;
|
|
348
412
|
try {
|
|
349
413
|
const chosen = await ctx.ui.custom<string | undefined>((host, theme, _keys, done) => {
|
|
350
|
-
dialog = new StatusDialog(
|
|
414
|
+
dialog = new StatusDialog(
|
|
415
|
+
host,
|
|
416
|
+
theme,
|
|
417
|
+
service,
|
|
418
|
+
done,
|
|
419
|
+
selected,
|
|
420
|
+
options.title,
|
|
421
|
+
undefined,
|
|
422
|
+
options.nerdFontIcons,
|
|
423
|
+
);
|
|
351
424
|
dialog.startRefresh();
|
|
352
425
|
return dialog;
|
|
353
426
|
}, DIALOG_OPTIONS);
|
|
@@ -390,7 +463,12 @@ export class AgentNavigationController {
|
|
|
390
463
|
this.session = undefined;
|
|
391
464
|
}
|
|
392
465
|
|
|
393
|
-
open(
|
|
466
|
+
open(
|
|
467
|
+
ctx: ExtensionContext,
|
|
468
|
+
service: ThreadService,
|
|
469
|
+
selectedPath?: string,
|
|
470
|
+
nerdFontIcons = false,
|
|
471
|
+
): Promise<void> {
|
|
394
472
|
if (this.opening) return this.opening;
|
|
395
473
|
if (!canOpenDialog(ctx)) return Promise.resolve();
|
|
396
474
|
if (selectedPath) this.tree.selectedPath = selectedPath;
|
|
@@ -481,14 +559,20 @@ export class AgentNavigationController {
|
|
|
481
559
|
await showThreads(scoped, service, chosen.path);
|
|
482
560
|
continue;
|
|
483
561
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
this.viewports.set(chosen.path, viewport);
|
|
488
|
-
}
|
|
562
|
+
const existing = this.viewports.get(chosen.path);
|
|
563
|
+
const viewport: AgentViewportState = existing ?? { scrollTop: 0, follow: true };
|
|
564
|
+
if (existing === undefined) this.viewports.set(chosen.path, viewport);
|
|
489
565
|
const result = await session.mount<"back" | "main">(
|
|
490
566
|
(tui, activeTheme, _keys, finish) =>
|
|
491
|
-
new LiveAgentView(
|
|
567
|
+
new LiveAgentView(
|
|
568
|
+
tui,
|
|
569
|
+
activeTheme,
|
|
570
|
+
service,
|
|
571
|
+
chosen.path,
|
|
572
|
+
viewport,
|
|
573
|
+
finish,
|
|
574
|
+
nerdFontIcons,
|
|
575
|
+
),
|
|
492
576
|
);
|
|
493
577
|
if (result !== "back") break;
|
|
494
578
|
}
|
|
@@ -529,7 +613,7 @@ export class AgentNavigationController {
|
|
|
529
613
|
|
|
530
614
|
const navigationByService = new WeakMap<ThreadService, AgentNavigationController>();
|
|
531
615
|
|
|
532
|
-
async function
|
|
616
|
+
async function _showAgentTree(
|
|
533
617
|
ctx: ExtensionContext,
|
|
534
618
|
service: ThreadService,
|
|
535
619
|
selectedPath?: string,
|
|
@@ -542,3 +626,8 @@ async function showAgentTree(
|
|
|
542
626
|
}
|
|
543
627
|
await navigation.open(ctx, service, selectedPath, nerdFontIcons);
|
|
544
628
|
}
|
|
629
|
+
|
|
630
|
+
function hintFor<T extends string>(state: string, base: T): T | "success" | "error" {
|
|
631
|
+
if (state === "completed") return "success";
|
|
632
|
+
return state === "failed" ? "error" : base;
|
|
633
|
+
}
|
|
@@ -25,11 +25,7 @@ function comparePath(a: string, b: string): number {
|
|
|
25
25
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function chainHas(
|
|
29
|
-
parentOf: Map<string, string | null>,
|
|
30
|
-
start: string,
|
|
31
|
-
target: string,
|
|
32
|
-
): boolean {
|
|
28
|
+
function chainHas(parentOf: Map<string, string | null>, start: string, target: string): boolean {
|
|
33
29
|
const seen = new Set<string>();
|
|
34
30
|
let current: string | null = start;
|
|
35
31
|
while (current) {
|
|
@@ -50,8 +46,7 @@ export function buildStatusTree(
|
|
|
50
46
|
): StatusRow[] {
|
|
51
47
|
const threadsByPath = new Map<string, ThreadView>();
|
|
52
48
|
for (const thread of threads) {
|
|
53
|
-
if (thread?.path && !threadsByPath.has(thread.path))
|
|
54
|
-
threadsByPath.set(thread.path, thread);
|
|
49
|
+
if (thread?.path && !threadsByPath.has(thread.path)) threadsByPath.set(thread.path, thread);
|
|
55
50
|
}
|
|
56
51
|
const nodes = new Map<string, StatusNode>();
|
|
57
52
|
const parentOf = new Map<string, string | null>();
|
|
@@ -66,8 +61,7 @@ export function buildStatusTree(
|
|
|
66
61
|
const declaredParent = (path: string): string | null => {
|
|
67
62
|
if (path === ROOT) return null;
|
|
68
63
|
const thread = threadsByPath.get(path);
|
|
69
|
-
if (thread)
|
|
70
|
-
return thread.parent && thread.parent !== path ? thread.parent : null;
|
|
64
|
+
if (thread) return thread.parent && thread.parent !== path ? thread.parent : null;
|
|
71
65
|
return lexicalParent(path);
|
|
72
66
|
};
|
|
73
67
|
const ensure = (path: string, seen = new Set<string>()): void => {
|