@ferris1225/pi-subagents 4.3.4 → 4.3.5

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 (37) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +59 -66
  3. package/agents/artisan.md +0 -1
  4. package/agents/steward.md +1 -2
  5. package/{src/index.ts → index.ts} +19 -19
  6. package/package.json +4 -3
  7. package/src/{config.ts → configuration/config.ts} +19 -24
  8. package/src/configuration/setup.ts +375 -0
  9. package/src/configuration/ui.ts +245 -0
  10. package/src/{agents.ts → delegation/agents.ts} +3 -3
  11. package/src/{dispatch.ts → delegation/dispatch.ts} +12 -18
  12. package/src/{prompt.ts → delegation/prompt.ts} +3 -8
  13. package/src/{background.ts → execution/background.ts} +3 -6
  14. package/src/execution/rpc-control.ts +200 -0
  15. package/src/{rpc-run.ts → execution/rpc-run.ts} +11 -199
  16. package/src/{session-fork.ts → execution/session-fork.ts} +1 -1
  17. package/src/{spawn.ts → execution/spawn.ts} +10 -8
  18. package/src/isolation/git-command.ts +147 -0
  19. package/src/{recovery.ts → isolation/recovery.ts} +1 -1
  20. package/src/{worktree.ts → isolation/worktree.ts} +10 -147
  21. package/src/{completion.ts → lifecycle/completion.ts} +2 -2
  22. package/src/{durable.ts → lifecycle/durable.ts} +3 -3
  23. package/src/{runtime.ts → lifecycle/runtime.ts} +7 -7
  24. package/src/{thread-lifecycle.ts → lifecycle/thread-lifecycle.ts} +25 -519
  25. package/src/lifecycle/thread-restore.ts +250 -0
  26. package/src/lifecycle/thread-shared.ts +269 -0
  27. package/src/{tools.ts → lifecycle/tools.ts} +8 -8
  28. package/src/{announcements.ts → presentation/announcements.ts} +4 -4
  29. package/src/{format.ts → presentation/format.ts} +3 -3
  30. package/src/{monitor.ts → presentation/monitor.ts} +2 -2
  31. package/src/{widget.ts → presentation/widget.ts} +1 -1
  32. package/agents/sentinel.md +0 -16
  33. package/src/setup.ts +0 -344
  34. package/src/ui.ts +0 -160
  35. /package/src/{models.ts → configuration/models.ts} +0 -0
  36. /package/src/{temp-hygiene.ts → isolation/temp-hygiene.ts} +0 -0
  37. /package/src/{status.ts → presentation/status.ts} +0 -0
package/src/setup.ts DELETED
@@ -1,344 +0,0 @@
1
- /** Single-overlay editor for the UI-configurable pi-subagents settings. */
2
-
3
- import type { Api, Model } from "@earendil-works/pi-ai";
4
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
5
- import {
6
- truncateToWidth,
7
- type Component,
8
- type Focusable,
9
- type KeybindingsManager,
10
- type TUI,
11
- } from "@earendil-works/pi-tui";
12
- import {
13
- BUILTIN_AGENT_NAMES,
14
- agentProfile,
15
- errorMessage,
16
- getConfigPath,
17
- loadConfig,
18
- roleThinkingLevel,
19
- saveConfig,
20
- type SubagentsConfig,
21
- type ThinkingLevel,
22
- } from "./config.ts";
23
- import {
24
- CURRENT_MAIN_MODEL,
25
- applyAgentModelChoice,
26
- availableModelsInScope,
27
- buildModelPickerItems,
28
- findModelByRef,
29
- resolveThinkingLevel,
30
- supportedThinkingLevels,
31
- } from "./models.ts";
32
- import { makePickerStyles, Picker } from "./ui.ts";
33
-
34
- const FIELD_COUNT = 3;
35
- const WIDE_LAYOUT_MIN_WIDTH = 112;
36
-
37
- type SetupResult = SubagentsConfig | undefined;
38
-
39
- export interface SetupOverlayOptions {
40
- config: SubagentsConfig;
41
- models: readonly Model<Api>[];
42
- mainModel: Model<Api> | undefined;
43
- theme: Theme;
44
- tui: TUI;
45
- keybindings: KeybindingsManager;
46
- onDone: (result: SetupResult) => void;
47
- }
48
-
49
- function cloneConfig(config: SubagentsConfig): SubagentsConfig {
50
- return {
51
- ...config,
52
- enabledAgents: [...config.enabledAgents],
53
- knownAgents: [...config.knownAgents],
54
- agentModels: { ...config.agentModels },
55
- agentThinkingLevels: { ...config.agentThinkingLevels },
56
- };
57
- }
58
-
59
- function setupAgentNames(config: SubagentsConfig): string[] {
60
- return [
61
- ...new Set([
62
- ...BUILTIN_AGENT_NAMES,
63
- ...config.knownAgents,
64
- ...config.enabledAgents,
65
- ...Object.keys(config.agentModels),
66
- ...Object.keys(config.agentThinkingLevels),
67
- ]),
68
- ];
69
- }
70
-
71
- /**
72
- * Transactional settings component. It mutates only its private draft and
73
- * returns that draft exclusively from the explicit Save & Exit row.
74
- */
75
- export class SetupOverlay implements Component, Focusable {
76
- private _focused = false;
77
- private readonly state: SubagentsConfig;
78
- private readonly agents: string[];
79
- private row = 0;
80
- private field = 0;
81
- private modelPicker: Picker | undefined;
82
-
83
- constructor(private readonly options: SetupOverlayOptions) {
84
- this.state = cloneConfig(options.config);
85
- this.agents = setupAgentNames(this.state);
86
- this.state.knownAgents = [...new Set([...this.state.knownAgents, ...this.agents])];
87
- }
88
-
89
- get focused(): boolean {
90
- return this._focused;
91
- }
92
-
93
- set focused(value: boolean) {
94
- this._focused = value;
95
- if (this.modelPicker) this.modelPicker.focused = value;
96
- }
97
-
98
- render(width: number): string[] {
99
- if (this.modelPicker) return this.modelPicker.render(width);
100
-
101
- const { theme } = this.options;
102
- const safeWidth = Math.max(0, width);
103
- const fit = (line: string): string => truncateToWidth(line, safeWidth, "", true);
104
- const border = theme.fg("border", "─".repeat(Math.max(1, safeWidth)));
105
- const lines = [
106
- border,
107
- theme.fg("accent", theme.bold("pi-subagents setup")),
108
- theme.fg("dim", "↑/↓ agent or action • ←/→ field • Enter/Space edit • Esc cancels without saving"),
109
- border,
110
- ];
111
-
112
- const maxVisibleAgents = safeWidth >= WIDE_LAYOUT_MIN_WIDTH ? 8 : 3;
113
- const activeAgentRow = Math.min(this.row, this.agents.length - 1);
114
- const start = Math.max(
115
- 0,
116
- Math.min(activeAgentRow - Math.floor(maxVisibleAgents / 2), this.agents.length - maxVisibleAgents),
117
- );
118
- const end = Math.min(start + maxVisibleAgents, this.agents.length);
119
- for (let index = start; index < end; index++) {
120
- const name = this.agents[index]!;
121
- const active = index === this.row;
122
- const cursor = active ? theme.fg("accent", "❯ ") : " ";
123
- const enabled = this.state.enabledAgents.includes(name);
124
- const enabledText = this.cell(enabled ? "[x]" : "[ ]", active && this.field === 0);
125
- const profile = agentProfile(name);
126
- const nameText = profile ? `${name} — ${profile.summary}` : `${name} (custom)`;
127
- const modelText = this.cell(`model: ${this.modelDisplay(name)}`, active && this.field === 1);
128
- const thinkingText = this.cell(`thinking: ${this.thinkingDisplay(name)}`, active && this.field === 2);
129
-
130
- if (safeWidth >= WIDE_LAYOUT_MIN_WIDTH) {
131
- lines.push(`${cursor}${enabledText} ${nameText} │ ${modelText} │ ${thinkingText}`);
132
- } else {
133
- lines.push(`${cursor}${enabledText} ${nameText}`);
134
- lines.push(` ${modelText}`);
135
- lines.push(` ${thinkingText}`);
136
- }
137
- }
138
- const range = start > 0 || end < this.agents.length
139
- ? `agents ${start + 1}-${end} of ${this.agents.length}`
140
- : undefined;
141
- const selectedName = this.agents[this.row];
142
- const selectedProfile = selectedName ? agentProfile(selectedName) : undefined;
143
- const context = [range, selectedProfile?.remark].filter(Boolean).join(" · ");
144
- if (context) lines.push(theme.fg("dim", ` ${context}`));
145
- lines.push(border);
146
- lines.push(this.actionLine(this.agents.length, "Save & Exit", "persist all changes"));
147
- lines.push(this.actionLine(this.agents.length + 1, "Cancel", "discard this session"));
148
- lines.push(border);
149
- return lines.map(fit);
150
- }
151
-
152
- handleInput(data: string): void {
153
- if (this.modelPicker) {
154
- this.modelPicker.handleInput(data);
155
- return;
156
- }
157
-
158
- const { keybindings } = this.options;
159
- const rowCount = this.agents.length + 2;
160
- if (keybindings.matches(data, "tui.select.cancel")) {
161
- this.options.onDone(undefined);
162
- return;
163
- }
164
- if (keybindings.matches(data, "tui.select.up")) {
165
- this.row = (this.row - 1 + rowCount) % rowCount;
166
- } else if (keybindings.matches(data, "tui.select.down")) {
167
- this.row = (this.row + 1) % rowCount;
168
- } else if (this.row < this.agents.length && keybindings.matches(data, "tui.editor.cursorLeft")) {
169
- this.field = (this.field - 1 + FIELD_COUNT) % FIELD_COUNT;
170
- } else if (this.row < this.agents.length && keybindings.matches(data, "tui.editor.cursorRight")) {
171
- this.field = (this.field + 1) % FIELD_COUNT;
172
- } else if (keybindings.matches(data, "tui.select.confirm") || data === " ") {
173
- this.activateCurrent();
174
- }
175
- this.options.tui.requestRender();
176
- }
177
-
178
- invalidate(): void {
179
- this.modelPicker?.invalidate();
180
- }
181
-
182
- private cell(text: string, selected: boolean): string {
183
- if (!selected) return text;
184
- return this.options.theme.bg("selectedBg", this.options.theme.fg("accent", text));
185
- }
186
-
187
- private actionLine(row: number, label: string, description: string): string {
188
- const active = this.row === row;
189
- const cursor = active ? this.options.theme.fg("accent", "❯ ") : " ";
190
- const text = active ? this.cell(label, true) : label;
191
- return `${cursor}${text} ${this.options.theme.fg("dim", `— ${description}`)}`;
192
- }
193
-
194
- private activateCurrent(): void {
195
- if (this.row === this.agents.length) {
196
- this.options.onDone(cloneConfig(this.state));
197
- return;
198
- }
199
- if (this.row === this.agents.length + 1) {
200
- this.options.onDone(undefined);
201
- return;
202
- }
203
-
204
- const name = this.agents[this.row];
205
- if (!name) return;
206
- if (this.field === 0) this.toggleEnabled(name);
207
- else if (this.field === 1) this.openModelPicker(name);
208
- else this.cycleThinking(name);
209
- }
210
-
211
- private toggleEnabled(name: string): void {
212
- if (this.state.enabledAgents.includes(name)) {
213
- this.state.enabledAgents = this.state.enabledAgents.filter((candidate) => candidate !== name);
214
- } else {
215
- this.state.enabledAgents = [...this.state.enabledAgents, name];
216
- }
217
- }
218
-
219
- private openModelPicker(name: string): void {
220
- const explicitRef = this.state.agentModels[name];
221
- const items = buildModelPickerItems({
222
- models: this.options.models,
223
- configuredRef: explicitRef,
224
- mainRef: this.options.mainModel ? `${this.options.mainModel.provider}/${this.options.mainModel.id}` : undefined,
225
- });
226
- if (name === "sentinel") {
227
- const inherited = this.state.agentModels.artisan ?? "current main model";
228
- items[0] = {
229
- value: CURRENT_MAIN_MODEL,
230
- label: "Follow artisan (role default)",
231
- description: `Clear the sentinel override; currently follows ${inherited}`,
232
- };
233
- }
234
-
235
- const styles = makePickerStyles(this.options.theme);
236
- this.modelPicker = new Picker(
237
- items,
238
- styles,
239
- [
240
- styles.title(`Model for ${name}`),
241
- styles.hint("Type to filter • ↑/↓ move • Enter select • Esc return to table"),
242
- ],
243
- this.options.tui,
244
- this.options.keybindings,
245
- {
246
- onSelect: (choice) => {
247
- this.state.agentModels = applyAgentModelChoice(this.state.agentModels, name, choice);
248
- this.clampThinkingOverride(name);
249
- if (name === "artisan" && this.state.agentModels.sentinel === undefined) {
250
- this.clampThinkingOverride("sentinel");
251
- }
252
- this.closeModelPicker();
253
- },
254
- onCancel: () => this.closeModelPicker(),
255
- },
256
- explicitRef ?? CURRENT_MAIN_MODEL,
257
- );
258
- this.modelPicker.focused = this.focused;
259
- }
260
-
261
- private closeModelPicker(): void {
262
- this.modelPicker = undefined;
263
- this.options.tui.requestRender();
264
- }
265
-
266
- private effectiveModel(name: string): Model<Api> | undefined {
267
- const explicitRef = this.state.agentModels[name];
268
- const inheritedRef = name === "sentinel" && explicitRef === undefined ? this.state.agentModels.artisan : undefined;
269
- return findModelByRef(this.options.models, explicitRef ?? inheritedRef) ?? this.options.mainModel;
270
- }
271
-
272
- private clampThinkingOverride(name: string): void {
273
- const current = this.state.agentThinkingLevels[name];
274
- if (current === undefined) return;
275
- this.state.agentThinkingLevels[name] = resolveThinkingLevel(this.effectiveModel(name), current);
276
- }
277
-
278
- private cycleThinking(name: string): void {
279
- const model = this.effectiveModel(name);
280
- const roleDefault = resolveThinkingLevel(model, roleThinkingLevel(name));
281
- const overrides = supportedThinkingLevels(model).filter((level) => level !== roleDefault);
282
- const current = this.state.agentThinkingLevels[name];
283
- let next: ThinkingLevel | undefined;
284
- if (current === undefined) {
285
- next = overrides[0];
286
- } else {
287
- const index = overrides.indexOf(current);
288
- next = index < 0 || index === overrides.length - 1 ? undefined : overrides[index + 1];
289
- }
290
- if (next === undefined) delete this.state.agentThinkingLevels[name];
291
- else this.state.agentThinkingLevels[name] = next;
292
- }
293
-
294
- private modelDisplay(name: string): string {
295
- const explicitRef = this.state.agentModels[name];
296
- if (explicitRef) return explicitRef;
297
- if (name === "sentinel") return `follow artisan → ${this.state.agentModels.artisan ?? "current main"}`;
298
- return "current main (dynamic)";
299
- }
300
-
301
- private thinkingDisplay(name: string): string {
302
- const override = this.state.agentThinkingLevels[name];
303
- if (override !== undefined) return `${resolveThinkingLevel(this.effectiveModel(name), override)} (override)`;
304
- const roleDefault = resolveThinkingLevel(this.effectiveModel(name), roleThinkingLevel(name));
305
- return `${roleDefault} (role default)`;
306
- }
307
- }
308
-
309
- /** Entry point for the /subagents-setup command. */
310
- export async function runSetup(ctx: ExtensionCommandContext, configPath: string = getConfigPath()): Promise<void> {
311
- if (ctx.mode !== "tui") {
312
- ctx.ui.notify("/subagents-setup requires Pi's interactive TUI.", "error");
313
- return;
314
- }
315
-
316
- try {
317
- const config = await loadConfig(configPath, { persistNormalization: false });
318
- const models = availableModelsInScope(ctx);
319
- const result = await ctx.ui.custom<SetupResult>(
320
- (tui, theme, keybindings, done) =>
321
- new SetupOverlay({
322
- config,
323
- models,
324
- mainModel: ctx.model,
325
- theme,
326
- tui,
327
- keybindings,
328
- onDone: done,
329
- }),
330
- {
331
- overlay: true,
332
- overlayOptions: { anchor: "center", width: "90%", minWidth: 36, maxHeight: "90%", margin: 1 },
333
- },
334
- );
335
- if (result === undefined) {
336
- ctx.ui.notify("pi-subagents setup cancelled; no changes saved.", "info");
337
- return;
338
- }
339
- await saveConfig(result, configPath);
340
- ctx.ui.notify(`pi-subagents updated. Saved to ${configPath}`, "info");
341
- } catch (error) {
342
- ctx.ui.notify(`pi-subagents setup failed: ${errorMessage(error)}`, "error");
343
- }
344
- }
package/src/ui.ts DELETED
@@ -1,160 +0,0 @@
1
- /** Searchable, width-safe model picker used inside /subagents-setup. */
2
-
3
- import {
4
- fuzzyFilter,
5
- truncateToWidth,
6
- type Component,
7
- type Focusable,
8
- type KeybindingsManager,
9
- type SelectItem,
10
- type TUI,
11
- } from "@earendil-works/pi-tui";
12
-
13
- /** Rows shown at once; longer lists are reached with PageUp/PageDown. */
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;
25
- }
26
-
27
- type PickerItem = SelectItem;
28
-
29
- function pickerItemSearchText(item: PickerItem): string {
30
- return `${item.value} ${item.label} ${item.description ?? ""}`;
31
- }
32
-
33
- interface PickerCallbacks {
34
- onSelect: (value: string) => void;
35
- onCancel: () => void;
36
- }
37
-
38
- export class Picker implements Component, Focusable {
39
- private _focused = false;
40
- private query = "";
41
- private cursor = 0;
42
- private filtered: PickerItem[];
43
-
44
- constructor(
45
- private readonly items: PickerItem[],
46
- private readonly styles: PickerStyles,
47
- private readonly headerLines: string[],
48
- private readonly tui: TUI,
49
- private readonly keybindings: KeybindingsManager,
50
- private readonly callbacks: PickerCallbacks,
51
- initialValue?: string,
52
- ) {
53
- this.filtered = items;
54
- const initialIndex = initialValue === undefined
55
- ? -1
56
- : items.findIndex((item) => item.value === initialValue);
57
- if (initialIndex >= 0) this.cursor = initialIndex;
58
- }
59
-
60
- get focused(): boolean {
61
- return this._focused;
62
- }
63
-
64
- set focused(value: boolean) {
65
- this._focused = value;
66
- }
67
-
68
- private recompute(): void {
69
- const query = this.query.trim();
70
- this.filtered = query ? fuzzyFilter(this.items, query, pickerItemSearchText) : this.items;
71
- this.cursor = Math.max(0, Math.min(this.cursor, this.filtered.length - 1));
72
- }
73
-
74
- render(width: number): string[] {
75
- const fit = (line: string): string => truncateToWidth(line, width, "");
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)")));
81
- lines.push(border);
82
-
83
- if (this.filtered.length === 0) {
84
- lines.push(fit(this.styles.dim(" (no matches)")));
85
- } else {
86
- const start = Math.max(
87
- 0,
88
- Math.min(this.cursor - Math.floor(PAGE_SIZE / 2), this.filtered.length - PAGE_SIZE),
89
- );
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));
96
- }
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}`)));
99
- }
100
-
101
- lines.push(border);
102
- return lines;
103
- }
104
-
105
- handleInput(data: string): void {
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")) {
116
- this.cursor = Math.max(0, this.cursor - PAGE_SIZE);
117
- } else if (keybindings.matches(data, "tui.select.pageDown")) {
118
- this.cursor = Math.min(Math.max(0, this.filtered.length - 1), this.cursor + PAGE_SIZE);
119
- } else if (keybindings.matches(data, "tui.select.confirm")) {
120
- const item = this.filtered[this.cursor];
121
- if (item) this.callbacks.onSelect(item.value);
122
- return;
123
- } else if (keybindings.matches(data, "tui.select.cancel")) {
124
- this.callbacks.onCancel();
125
- return;
126
- } else if (data === "\x7f" || data === "\b") {
127
- this.query = this.query.slice(0, -1);
128
- this.cursor = 0;
129
- this.recompute();
130
- } else if (isPrintable(data)) {
131
- this.query += data;
132
- this.cursor = 0;
133
- this.recompute();
134
- }
135
- this.tui.requestRender();
136
- }
137
-
138
- invalidate(): void {}
139
- }
140
-
141
- function isPrintable(data: string): boolean {
142
- return data.length > 0 && data.charCodeAt(0) >= 0x20;
143
- }
144
-
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 {
150
- return {
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),
159
- };
160
- }
File without changes
File without changes