@jwilger/pi-development-system 0.10.5 → 0.11.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.
Files changed (36) hide show
  1. package/agents/architect.md +66 -0
  2. package/agents/coder.md +57 -0
  3. package/agents/researcher.md +60 -0
  4. package/agents/reviewer.md +53 -0
  5. package/agents/tasker.md +58 -0
  6. package/agents/writer.md +39 -0
  7. package/extensions/development-system.ts +2 -0
  8. package/package.json +5 -3
  9. package/src/core/test-runner.ts +1 -1
  10. package/src/gates/red-first-guard.ts +4 -1
  11. package/src/subagents/VENDORED.md +38 -0
  12. package/src/subagents/index.ts +544 -0
  13. package/src/subagents/orch/inherited-tools.ts +85 -0
  14. package/src/subagents/orch/mailbox.ts +262 -0
  15. package/src/subagents/orch/manager.ts +765 -0
  16. package/src/subagents/orch/paths.ts +74 -0
  17. package/src/subagents/orch/prompt.ts +25 -0
  18. package/src/subagents/orch/runtime.ts +784 -0
  19. package/src/subagents/orch/tools.ts +166 -0
  20. package/src/subagents/orch/transcript.ts +187 -0
  21. package/src/subagents/prefs/agent-import.ts +246 -0
  22. package/src/subagents/prefs/config.ts +1029 -0
  23. package/src/subagents/prefs/import-discovery.ts +644 -0
  24. package/src/subagents/prefs/import-instructions.ts +157 -0
  25. package/src/subagents/prefs/models.ts +78 -0
  26. package/src/subagents/prefs/settings.ts +371 -0
  27. package/src/subagents/types.ts +200 -0
  28. package/src/subagents/ui/agent-navigation-editor.ts +210 -0
  29. package/src/subagents/ui/dialog.ts +651 -0
  30. package/src/subagents/ui/import-picker.ts +232 -0
  31. package/src/subagents/ui/live-agent-view.ts +572 -0
  32. package/src/subagents/ui/model-picker.ts +452 -0
  33. package/src/subagents/ui/settings-ui.ts +329 -0
  34. package/src/subagents/ui/status-ui.ts +545 -0
  35. package/src/subagents/ui/thread-tree.ts +135 -0
  36. package/src/subagents/ui/ui.ts +1507 -0
@@ -0,0 +1,232 @@
1
+ // @ts-nocheck: vendored upstream pi-subagent-manager 0.14.0 compiled under looser options; see src/subagents/VENDORED.md
2
+ import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
+ import {
4
+ CURSOR_MARKER,
5
+ getKeybindings,
6
+ Key,
7
+ matchesKey,
8
+ truncateToWidth,
9
+ visibleWidth,
10
+ } from "@earendil-works/pi-tui";
11
+ import {
12
+ DIALOG_OPTIONS,
13
+ dialogHeight,
14
+ dialogText,
15
+ frameDialog,
16
+ type DialogHost,
17
+ } from "./dialog.ts";
18
+
19
+ export interface ImportPickerItem {
20
+ id: string;
21
+ label: string;
22
+ detail: string;
23
+ }
24
+
25
+ const TITLE = "Import agents";
26
+ const DETAIL_LINE_CAP = 3;
27
+ // Critical controls first so a truncated footer still shows how to finish.
28
+ const FOOTER =
29
+ "Space toggle · Enter import (empty skip) · Esc cancel · Ctrl+A all · ↑↓ navigate · PgUp/PgDn · Home/End";
30
+
31
+ /**
32
+ * Agent labels and paths are untrusted. dialogText strips known sequences;
33
+ * also drop Pi's cursor marker and any leftover ESC so a source file cannot
34
+ * spoof the cursor or inject a title/color sequence.
35
+ */
36
+ function agentText(text: string): string {
37
+ return dialogText(text).split(CURSOR_MARKER).join("").replaceAll("\x1b", " ");
38
+ }
39
+
40
+ /**
41
+ * truncateToWidth appends ESC [ 0 m when it clips. That reset is not a theme
42
+ * color and must not become part of the wrapped agent text.
43
+ */
44
+ function clipVisible(text: string, columns: number): string {
45
+ if (columns <= 0 || !text) return "";
46
+ if (visibleWidth(text) <= columns) return text;
47
+ const clipped = truncateToWidth(text, columns, "").replaceAll("\x1b[0m", "");
48
+ return clipped && text.startsWith(clipped) ? clipped : "";
49
+ }
50
+
51
+ /** Split sanitized text into frame-width lines without dropping a fitting prefix. */
52
+ function wrapText(text: string, columns: number): string[] {
53
+ const clean = agentText(text);
54
+ if (!clean || columns <= 0) return [];
55
+ const lines: string[] = [];
56
+ let rest = clean;
57
+ while (rest.length > 0) {
58
+ const chunk = clipVisible(rest, columns);
59
+ if (!chunk) {
60
+ rest = rest.slice(1);
61
+ continue;
62
+ }
63
+ lines.push(chunk);
64
+ if (chunk.length >= rest.length) break;
65
+ rest = rest.slice(chunk.length);
66
+ }
67
+ return lines;
68
+ }
69
+
70
+ /**
71
+ * Checkbox dialog for choosing external agents to migrate.
72
+ * Selection only — the caller imports; this does not read or write agent files.
73
+ */
74
+ export class ImportPicker {
75
+ private cursor = 0;
76
+ private viewport = 1;
77
+ private readonly checked = new Set<string>();
78
+
79
+ constructor(
80
+ private host: DialogHost,
81
+ private theme: Theme,
82
+ private readonly items: readonly ImportPickerItem[],
83
+ private done: (ids: string[] | undefined) => void,
84
+ ) {}
85
+
86
+ /** Selected ids in source order. Empty until the user toggles a row. */
87
+ getSelectedIds(): string[] {
88
+ const seen = new Set<string>();
89
+ const ids: string[] = [];
90
+ for (const item of this.items) {
91
+ if (!this.checked.has(item.id) || seen.has(item.id)) continue;
92
+ seen.add(item.id);
93
+ ids.push(item.id);
94
+ }
95
+ return ids;
96
+ }
97
+
98
+ getCursorIndex(): number {
99
+ return this.cursor;
100
+ }
101
+
102
+ invalidate(): void {}
103
+
104
+ handleInput(data: string): void {
105
+ const keys = getKeybindings();
106
+ if (keys.matches(data, "tui.select.cancel")) {
107
+ this.done(undefined);
108
+ return;
109
+ }
110
+ if (keys.matches(data, "tui.select.confirm")) {
111
+ this.done(this.getSelectedIds());
112
+ return;
113
+ }
114
+ if (matchesKey(data, Key.space)) {
115
+ this.toggleCursor();
116
+ this.host.requestRender();
117
+ return;
118
+ }
119
+ if (matchesKey(data, Key.ctrl("a"))) {
120
+ this.toggleAll();
121
+ this.host.requestRender();
122
+ return;
123
+ }
124
+ if (this.items.length === 0) return;
125
+ if (keys.matches(data, "tui.select.up")) this.move(this.cursor - 1);
126
+ else if (keys.matches(data, "tui.select.down")) this.move(this.cursor + 1);
127
+ else if (keys.matches(data, "tui.select.pageUp") || matchesKey(data, Key.pageUp))
128
+ this.move(this.cursor - this.viewport);
129
+ else if (keys.matches(data, "tui.select.pageDown") || matchesKey(data, Key.pageDown))
130
+ this.move(this.cursor + this.viewport);
131
+ else if (matchesKey(data, Key.home)) this.move(0);
132
+ else if (matchesKey(data, Key.end)) this.move(this.items.length - 1);
133
+ }
134
+
135
+ render(width: number): string[] {
136
+ const height = dialogHeight(this.host);
137
+ const budget = Math.max(0, height - 4);
138
+ const columns = Math.max(1, width - 4);
139
+ const current = this.items[this.cursor];
140
+ const detail = current ? wrapText(current.detail, columns) : [];
141
+ const headerCount = budget > 0 ? 1 : 0;
142
+ // Keep the checkbox list usable: at most 3 detail lines, and never more than 1/3 of the body.
143
+ let detailCount = Math.min(detail.length, DETAIL_LINE_CAP, Math.floor(budget / 3));
144
+ if (this.items.length > 0)
145
+ detailCount = Math.min(detailCount, Math.max(0, budget - headerCount - 1));
146
+ const room = Math.max(0, budget - headerCount - detailCount);
147
+ const gap = detailCount > 0 && room >= 3 ? 1 : 0;
148
+ const listCount = Math.max(0, room - gap);
149
+ this.viewport = Math.max(1, listCount);
150
+ const start = this.windowStart(listCount);
151
+ const shown = Math.min(listCount, Math.max(0, this.items.length - start));
152
+ const body: string[] = [];
153
+ if (headerCount) body.push(this.countLine(start, shown));
154
+ if (this.items.length === 0) {
155
+ if (listCount > 0) body.push(this.theme.fg("muted", " No external agents to import"));
156
+ } else {
157
+ const rows = this.items.slice(start, start + listCount);
158
+ for (let offset = 0; offset < rows.length; offset++)
159
+ body.push(this.row(rows[offset]!, start + offset === this.cursor));
160
+ }
161
+ if (gap) body.push("");
162
+ for (const line of detail.slice(0, detailCount)) body.push(this.theme.fg("muted", ` ${line}`));
163
+ return frameDialog(this.theme, width, height, TITLE, body, FOOTER);
164
+ }
165
+
166
+ private countLine(start: number, shown: number): string {
167
+ const position = this.items.length ? this.cursor + 1 : 0;
168
+ return this.theme.fg(
169
+ "dim",
170
+ ` ${this.scrollHint(start, shown)}${this.checked.size} selected · ${position}/${this.items.length}`,
171
+ );
172
+ }
173
+
174
+ /** ▲/▼ counts when the checkbox list is scrolled. Omitted when every row fits. */
175
+ private scrollHint(start: number, shown: number): string {
176
+ if (this.items.length === 0) return "";
177
+ const above = shown > 0 ? start : this.cursor;
178
+ const below =
179
+ shown > 0
180
+ ? Math.max(0, this.items.length - (start + shown))
181
+ : Math.max(0, this.items.length - this.cursor - 1);
182
+ if (above <= 0 && below <= 0) return "";
183
+ const parts: string[] = [];
184
+ if (above > 0) parts.push(`▲${above}`);
185
+ if (below > 0) parts.push(`▼${below}`);
186
+ return `${parts.join(" ")} `;
187
+ }
188
+
189
+ private row(item: ImportPickerItem, active: boolean): string {
190
+ const mark = this.checked.has(item.id) ? "x" : " ";
191
+ const text = `${active ? "›" : " "}[${mark}] ${agentText(item.label)}`;
192
+ return active ? this.theme.fg("accent", text) : text;
193
+ }
194
+
195
+ private windowStart(listCount: number): number {
196
+ if (listCount <= 0 || this.items.length === 0) return 0;
197
+ return Math.max(0, Math.min(this.cursor - listCount + 1, this.items.length - listCount));
198
+ }
199
+
200
+ private move(next: number): void {
201
+ if (this.items.length === 0) return;
202
+ this.cursor = Math.max(0, Math.min(this.items.length - 1, next));
203
+ this.host.requestRender();
204
+ }
205
+
206
+ private toggleCursor(): void {
207
+ const item = this.items[this.cursor];
208
+ if (!item) return;
209
+ if (this.checked.has(item.id)) this.checked.delete(item.id);
210
+ else this.checked.add(item.id);
211
+ }
212
+
213
+ private toggleAll(): void {
214
+ const all = this.items.length > 0 && this.items.every((item) => this.checked.has(item.id));
215
+ if (all) {
216
+ this.checked.clear();
217
+ return;
218
+ }
219
+ for (const item of this.items) this.checked.add(item.id);
220
+ }
221
+ }
222
+
223
+ /** Open the import checkbox dialog. Returns ids, [] to skip, or undefined if cancelled. */
224
+ export function selectImportAgents(
225
+ ctx: ExtensionContext,
226
+ items: readonly ImportPickerItem[],
227
+ ): Promise<string[] | undefined> {
228
+ return ctx.ui.custom(
229
+ (host, theme, _keys, done) => new ImportPicker(host, theme, items, done),
230
+ DIALOG_OPTIONS,
231
+ );
232
+ }