@ferris1225/pi-subagents 4.3.2 → 4.3.3
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 +13 -0
- package/README.md +88 -49
- package/agents/artisan.md +7 -5
- package/agents/scout.md +5 -3
- package/agents/sentinel.md +16 -0
- package/agents/steward.md +9 -8
- package/package.json +2 -2
- package/src/agents.ts +1 -1
- package/src/announcements.ts +1 -1
- package/src/config.ts +63 -14
- package/src/dispatch.ts +9 -8
- package/src/prompt.ts +15 -9
- package/src/setup.ts +277 -280
- package/src/thread-lifecycle.ts +4 -2
- package/src/ui.ts +66 -154
- package/src/widget.ts +14 -13
package/src/ui.ts
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI pickers for /subagents-setup, built on @earendil-works/pi-tui.
|
|
3
|
-
*
|
|
4
|
-
* A single self-contained `Picker` component powers both selectors:
|
|
5
|
-
* - single-select (model picker): type to fuzzy-filter, arrows to move,
|
|
6
|
-
* PageUp/PageDown to page, Enter to choose, Esc to cancel.
|
|
7
|
-
* - multi-select (module picker): same navigation, Space toggles a checkbox,
|
|
8
|
-
* Enter confirms the selection set.
|
|
9
|
-
*
|
|
10
|
-
* pi-tui's built-in SelectList only handles up/down/confirm/cancel (no paging),
|
|
11
|
-
* so we render the list ourselves and drive it with getKeybindings(). Every line
|
|
12
|
-
* is passed through truncateToWidth() — pi hard-crashes if a rendered line is
|
|
13
|
-
* wider than the terminal.
|
|
14
|
-
*/
|
|
1
|
+
/** Searchable, width-safe model picker used inside /subagents-setup. */
|
|
15
2
|
|
|
16
3
|
import {
|
|
17
4
|
fuzzyFilter,
|
|
@@ -22,40 +9,29 @@ import {
|
|
|
22
9
|
type SelectItem,
|
|
23
10
|
type TUI,
|
|
24
11
|
} from "@earendil-works/pi-tui";
|
|
25
|
-
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
26
|
-
|
|
27
|
-
/** The slice of the extension context the pickers need (mode + ui), so both
|
|
28
|
-
* command handlers (ExtensionCommandContext) and tool execute handlers
|
|
29
|
-
* (ExtensionContext) can use them. */
|
|
30
|
-
export type PickerContext = Pick<ExtensionCommandContext, "mode" | "ui">;
|
|
31
12
|
|
|
32
13
|
/** Rows shown at once; longer lists are reached with PageUp/PageDown. */
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
border: (
|
|
37
|
-
title: (
|
|
38
|
-
hint: (
|
|
39
|
-
cursorMark: (
|
|
40
|
-
selectedLabel: (
|
|
41
|
-
label: (
|
|
42
|
-
dim: (
|
|
43
|
-
|
|
44
|
-
unchecked: (t: string) => string;
|
|
45
|
-
filterEcho: (t: string) => string;
|
|
14
|
+
const PAGE_SIZE = 8;
|
|
15
|
+
|
|
16
|
+
interface PickerStyles {
|
|
17
|
+
border: (text: string) => string;
|
|
18
|
+
title: (text: string) => string;
|
|
19
|
+
hint: (text: string) => string;
|
|
20
|
+
cursorMark: (text: string) => string;
|
|
21
|
+
selectedLabel: (text: string) => string;
|
|
22
|
+
label: (text: string) => string;
|
|
23
|
+
dim: (text: string) => string;
|
|
24
|
+
filterEcho: (text: string) => string;
|
|
46
25
|
}
|
|
47
26
|
|
|
48
|
-
|
|
27
|
+
type PickerItem = SelectItem;
|
|
49
28
|
|
|
50
|
-
|
|
29
|
+
function pickerItemSearchText(item: PickerItem): string {
|
|
51
30
|
return `${item.value} ${item.label} ${item.description ?? ""}`;
|
|
52
31
|
}
|
|
53
32
|
|
|
54
33
|
interface PickerCallbacks {
|
|
55
|
-
|
|
56
|
-
onSelect?: (value: string) => void;
|
|
57
|
-
/** multi-select: fired with the full chosen set on Enter. */
|
|
58
|
-
onConfirm?: (values: string[]) => void;
|
|
34
|
+
onSelect: (value: string) => void;
|
|
59
35
|
onCancel: () => void;
|
|
60
36
|
}
|
|
61
37
|
|
|
@@ -67,64 +43,59 @@ export class Picker implements Component, Focusable {
|
|
|
67
43
|
|
|
68
44
|
constructor(
|
|
69
45
|
private readonly items: PickerItem[],
|
|
70
|
-
private readonly multi: boolean,
|
|
71
|
-
private readonly selected: Set<string>,
|
|
72
46
|
private readonly styles: PickerStyles,
|
|
73
47
|
private readonly headerLines: string[],
|
|
74
48
|
private readonly tui: TUI,
|
|
75
49
|
private readonly keybindings: KeybindingsManager,
|
|
76
|
-
private readonly
|
|
50
|
+
private readonly callbacks: PickerCallbacks,
|
|
77
51
|
initialValue?: string,
|
|
78
52
|
) {
|
|
79
53
|
this.filtered = items;
|
|
80
|
-
const initialIndex = initialValue === undefined
|
|
54
|
+
const initialIndex = initialValue === undefined
|
|
55
|
+
? -1
|
|
56
|
+
: items.findIndex((item) => item.value === initialValue);
|
|
81
57
|
if (initialIndex >= 0) this.cursor = initialIndex;
|
|
82
58
|
}
|
|
83
59
|
|
|
84
60
|
get focused(): boolean {
|
|
85
61
|
return this._focused;
|
|
86
62
|
}
|
|
63
|
+
|
|
87
64
|
set focused(value: boolean) {
|
|
88
65
|
this._focused = value;
|
|
89
66
|
}
|
|
90
67
|
|
|
91
68
|
private recompute(): void {
|
|
92
|
-
const
|
|
93
|
-
this.filtered =
|
|
69
|
+
const query = this.query.trim();
|
|
70
|
+
this.filtered = query ? fuzzyFilter(this.items, query, pickerItemSearchText) : this.items;
|
|
94
71
|
this.cursor = Math.max(0, Math.min(this.cursor, this.filtered.length - 1));
|
|
95
72
|
}
|
|
96
73
|
|
|
97
74
|
render(width: number): string[] {
|
|
98
|
-
const s = this.styles;
|
|
99
75
|
const fit = (line: string): string => truncateToWidth(line, width, "");
|
|
100
|
-
const border = fit(
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
76
|
+
const border = fit(this.styles.border("─".repeat(Math.max(1, width))));
|
|
77
|
+
const lines = [border, ...this.headerLines.map(fit)];
|
|
78
|
+
lines.push(fit(this.query
|
|
79
|
+
? this.styles.filterEcho(`filter: ${this.query}`)
|
|
80
|
+
: this.styles.dim("filter: (type to narrow)")));
|
|
105
81
|
lines.push(border);
|
|
106
82
|
|
|
107
83
|
if (this.filtered.length === 0) {
|
|
108
|
-
lines.push(fit(
|
|
84
|
+
lines.push(fit(this.styles.dim(" (no matches)")));
|
|
109
85
|
} else {
|
|
110
86
|
const start = Math.max(
|
|
111
87
|
0,
|
|
112
88
|
Math.min(this.cursor - Math.floor(PAGE_SIZE / 2), this.filtered.length - PAGE_SIZE),
|
|
113
89
|
);
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
const description = item.description ? s.dim(` — ${item.description}`) : "";
|
|
121
|
-
const line = this.multi
|
|
122
|
-
? mark + (this.selected.has(item.value) ? s.checked("[x] ") : s.unchecked("[ ] ")) + label + description
|
|
123
|
-
: mark + label + description;
|
|
124
|
-
lines.push(fit(line));
|
|
90
|
+
for (const [index, item] of this.filtered.slice(start, start + PAGE_SIZE).entries()) {
|
|
91
|
+
const isCursor = start + index === this.cursor;
|
|
92
|
+
const mark = isCursor ? this.styles.cursorMark("❯ ") : " ";
|
|
93
|
+
const label = isCursor ? this.styles.selectedLabel(item.label) : this.styles.label(item.label);
|
|
94
|
+
const description = item.description ? this.styles.dim(` — ${item.description}`) : "";
|
|
95
|
+
lines.push(fit(mark + label + description));
|
|
125
96
|
}
|
|
126
|
-
const
|
|
127
|
-
lines.push(fit(
|
|
97
|
+
const paging = this.filtered.length > PAGE_SIZE ? " ↑/↓ move • PgUp/PgDn page" : "";
|
|
98
|
+
lines.push(fit(this.styles.dim(` (${this.cursor + 1}/${this.filtered.length})${paging}`)));
|
|
128
99
|
}
|
|
129
100
|
|
|
130
101
|
lines.push(border);
|
|
@@ -132,41 +103,30 @@ export class Picker implements Component, Focusable {
|
|
|
132
103
|
}
|
|
133
104
|
|
|
134
105
|
handleInput(data: string): void {
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
137
|
-
if (this.filtered.length > 0)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
} else if (
|
|
106
|
+
const keybindings = this.keybindings;
|
|
107
|
+
if (keybindings.matches(data, "tui.select.up")) {
|
|
108
|
+
if (this.filtered.length > 0) {
|
|
109
|
+
this.cursor = this.cursor === 0 ? this.filtered.length - 1 : this.cursor - 1;
|
|
110
|
+
}
|
|
111
|
+
} else if (keybindings.matches(data, "tui.select.down")) {
|
|
112
|
+
if (this.filtered.length > 0) {
|
|
113
|
+
this.cursor = this.cursor === this.filtered.length - 1 ? 0 : this.cursor + 1;
|
|
114
|
+
}
|
|
115
|
+
} else if (keybindings.matches(data, "tui.select.pageUp")) {
|
|
141
116
|
this.cursor = Math.max(0, this.cursor - PAGE_SIZE);
|
|
142
|
-
} else if (
|
|
117
|
+
} else if (keybindings.matches(data, "tui.select.pageDown")) {
|
|
143
118
|
this.cursor = Math.min(Math.max(0, this.filtered.length - 1), this.cursor + PAGE_SIZE);
|
|
144
|
-
} else if (
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const item = this.filtered[this.cursor];
|
|
148
|
-
if (item) this.cb.onSelect?.(item.value);
|
|
149
|
-
}
|
|
119
|
+
} else if (keybindings.matches(data, "tui.select.confirm")) {
|
|
120
|
+
const item = this.filtered[this.cursor];
|
|
121
|
+
if (item) this.callbacks.onSelect(item.value);
|
|
150
122
|
return;
|
|
151
|
-
} else if (
|
|
152
|
-
this.
|
|
123
|
+
} else if (keybindings.matches(data, "tui.select.cancel")) {
|
|
124
|
+
this.callbacks.onCancel();
|
|
153
125
|
return;
|
|
154
126
|
} else if (data === "\x7f" || data === "\b") {
|
|
155
127
|
this.query = this.query.slice(0, -1);
|
|
156
128
|
this.cursor = 0;
|
|
157
129
|
this.recompute();
|
|
158
|
-
} else if (data === " ") {
|
|
159
|
-
if (this.multi) {
|
|
160
|
-
const item = this.filtered[this.cursor];
|
|
161
|
-
if (item) {
|
|
162
|
-
if (this.selected.has(item.value)) this.selected.delete(item.value);
|
|
163
|
-
else this.selected.add(item.value);
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
this.query += data;
|
|
167
|
-
this.cursor = 0;
|
|
168
|
-
this.recompute();
|
|
169
|
-
}
|
|
170
130
|
} else if (isPrintable(data)) {
|
|
171
131
|
this.query += data;
|
|
172
132
|
this.cursor = 0;
|
|
@@ -179,70 +139,22 @@ export class Picker implements Component, Focusable {
|
|
|
179
139
|
}
|
|
180
140
|
|
|
181
141
|
function isPrintable(data: string): boolean {
|
|
182
|
-
|
|
183
|
-
// Reject ESC-led escape sequences and other control characters.
|
|
184
|
-
return data.charCodeAt(0) >= 0x20;
|
|
142
|
+
return data.length > 0 && data.charCodeAt(0) >= 0x20;
|
|
185
143
|
}
|
|
186
144
|
|
|
187
|
-
/** Build
|
|
188
|
-
|
|
189
|
-
|
|
145
|
+
/** Build picker styles from the pi theme. */
|
|
146
|
+
export function makePickerStyles(theme: {
|
|
147
|
+
fg: (color: any, text: string) => string;
|
|
148
|
+
bold: (text: string) => string;
|
|
149
|
+
}): PickerStyles {
|
|
190
150
|
return {
|
|
191
|
-
border: (
|
|
192
|
-
title: (
|
|
193
|
-
hint: (
|
|
194
|
-
cursorMark: (
|
|
195
|
-
selectedLabel: (
|
|
196
|
-
label: (
|
|
197
|
-
dim: (
|
|
198
|
-
|
|
199
|
-
unchecked: (t) => theme.fg("dim", t),
|
|
200
|
-
filterEcho: (t) => theme.fg("accent", t),
|
|
151
|
+
border: (text) => theme.fg("accent", text),
|
|
152
|
+
title: (text) => theme.fg("accent", theme.bold(text)),
|
|
153
|
+
hint: (text) => theme.fg("dim", text),
|
|
154
|
+
cursorMark: (text) => theme.fg("accent", text),
|
|
155
|
+
selectedLabel: (text) => theme.fg("accent", theme.bold(text)),
|
|
156
|
+
label: (text) => text,
|
|
157
|
+
dim: (text) => theme.fg("dim", text),
|
|
158
|
+
filterEcho: (text) => theme.fg("accent", text),
|
|
201
159
|
};
|
|
202
160
|
}
|
|
203
|
-
|
|
204
|
-
function requireTui(ctx: PickerContext): boolean {
|
|
205
|
-
if (ctx.mode !== "tui") {
|
|
206
|
-
ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error");
|
|
207
|
-
return false;
|
|
208
|
-
}
|
|
209
|
-
return true;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/** Single-select with fuzzy filter + paging. Resolves undefined on Esc. */
|
|
213
|
-
export function promptSelectOne(
|
|
214
|
-
ctx: PickerContext,
|
|
215
|
-
title: string,
|
|
216
|
-
hint: string,
|
|
217
|
-
items: PickerItem[],
|
|
218
|
-
initialValue?: string,
|
|
219
|
-
): Promise<string | undefined> {
|
|
220
|
-
if (!requireTui(ctx)) return Promise.resolve(undefined);
|
|
221
|
-
return ctx.ui.custom<string | undefined>((tui, theme, keybindings, done) => {
|
|
222
|
-
const styles = makeStyles(theme);
|
|
223
|
-
const header = [styles.title(title), styles.hint(hint)];
|
|
224
|
-
return new Picker(items, false, new Set<string>(), styles, header, tui, keybindings, {
|
|
225
|
-
onSelect: (value) => done(value),
|
|
226
|
-
onCancel: () => done(undefined),
|
|
227
|
-
}, initialValue);
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/** Multi-select with fuzzy filter + paging. Resolves undefined on Esc. */
|
|
232
|
-
export function promptSelectMany(
|
|
233
|
-
ctx: PickerContext,
|
|
234
|
-
title: string,
|
|
235
|
-
hint: string,
|
|
236
|
-
items: PickerItem[],
|
|
237
|
-
initialSelected: readonly string[],
|
|
238
|
-
): Promise<string[] | undefined> {
|
|
239
|
-
if (!requireTui(ctx)) return Promise.resolve(undefined);
|
|
240
|
-
return ctx.ui.custom<string[] | undefined>((tui, theme, keybindings, done) => {
|
|
241
|
-
const styles = makeStyles(theme);
|
|
242
|
-
const header = [styles.title(title), styles.hint(hint)];
|
|
243
|
-
return new Picker(items, true, new Set<string>(initialSelected), styles, header, tui, keybindings, {
|
|
244
|
-
onConfirm: (values) => done(values),
|
|
245
|
-
onCancel: () => done(undefined),
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
}
|
package/src/widget.ts
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
* id and agent so every label starts at the same column; a resumed thread
|
|
7
7
|
* carries a dim `↻` inside the agent column.
|
|
8
8
|
* - A live run owns two lines. Line 1 is what it is: identity, task label,
|
|
9
|
-
* then the telemetry flow (
|
|
10
|
-
* vocabulary `↑in ↓out R/W cache`, cost,
|
|
11
|
-
* elapsed). Line 2 is
|
|
12
|
-
* indented under the label column behind a `↳` marker.
|
|
9
|
+
* then the telemetry flow (worktree, wait state, token flow in the pi-footer
|
|
10
|
+
* vocabulary `↑in ↓out R/W cache`, cost, `provider/model`, effective
|
|
11
|
+
* `think:<level>`, seconds-precision elapsed). Line 2 is the current activity,
|
|
12
|
+
* dim and indented under the label column behind a `↳` marker.
|
|
13
13
|
* - Telemetry drops leftmost-first under width pressure (badge, wait, usage,
|
|
14
|
-
* model); the elapsed survives every width.
|
|
14
|
+
* model, thinking); the elapsed survives every width.
|
|
15
15
|
* - Queued rows say what they actually wait for ("queued" for a process slot,
|
|
16
16
|
* "repo lane" for shared-writer serialization, "starting" while the child
|
|
17
17
|
* process launches) instead of one catch-all "queued".
|
|
@@ -129,24 +129,25 @@ function usagePart(usage: UsageStats | undefined): string | undefined {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
/** Telemetry tail parts of a run row: badge and wait word first (dropped first
|
|
132
|
-
* under pressure), then
|
|
133
|
-
* elapsed. */
|
|
132
|
+
* under pressure), then usage, model, effective thinking strength, and the
|
|
133
|
+
* always-surviving elapsed time. */
|
|
134
134
|
function telemetryTailParts(run: RunView, now: number): Array<string | undefined> {
|
|
135
|
-
// Queued rows omit
|
|
135
|
+
// Queued rows omit model and thinking because the route is re-resolved at actual start.
|
|
136
136
|
// The full provider/model ref is kept — "which provider served this run" is
|
|
137
137
|
// exactly what a multi-provider session needs to see.
|
|
138
138
|
const modelPart = run.status === "queued" || !run.model ? undefined : run.model;
|
|
139
|
+
const thinkingPart = run.status === "queued" || !run.thinking ? undefined : `think:${run.thinking}`;
|
|
139
140
|
const badge = run.isolation === "worktree" ? worktreeBadge(run) : undefined;
|
|
140
141
|
const wait = run.status === "queued" ? waitWord(run) : undefined;
|
|
141
|
-
// Drop order under pressure: badge, wait word, usage, model; elapsed
|
|
142
|
+
// Drop order under pressure: badge, wait word, usage, model, thinking; elapsed
|
|
142
143
|
// survives every width the identity leaves room for.
|
|
143
|
-
return [badge, wait, usagePart(run.usage), modelPart, formatElapsed(run, now) || undefined];
|
|
144
|
+
return [badge, wait, usagePart(run.usage), modelPart, thinkingPart, formatElapsed(run, now) || undefined];
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
/** Two lines for a live run. Line 1 is what the run is: identity, task label,
|
|
147
|
-
* then
|
|
148
|
-
*
|
|
149
|
-
*
|
|
148
|
+
* then telemetry (worktree badge, wait state, token flow, cost, provider/model,
|
|
149
|
+
* effective thinking, elapsed). Line 2 is the current activity, dim and indented
|
|
150
|
+
* under the label column behind a `↳` marker. The
|
|
150
151
|
* label takes the full content budget on line 1; the identity and the elapsed
|
|
151
152
|
* survive every width. */
|
|
152
153
|
function primaryLine(
|