@gotgenes/pi-subagents 7.3.1 → 7.4.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.
- package/CHANGELOG.md +26 -0
- package/docs/architecture/architecture.md +148 -60
- package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
- package/docs/plans/0214-convert-remaining-closure-factories-to-classes.md +261 -0
- package/docs/retro/0208-extract-shared-test-fixtures.md +57 -0
- package/docs/retro/0214-convert-remaining-closure-factories-to-classes.md +66 -0
- package/package.json +1 -1
- package/src/index.ts +2 -2
- package/src/lifecycle/agent-runner.ts +10 -1
- package/src/service/service-adapter.ts +76 -76
- package/src/ui/agent-config-editor.ts +56 -56
- package/src/ui/agent-creation-wizard.ts +28 -36
- package/src/ui/agent-menu.ts +7 -7
- package/src/ui/message-formatters.ts +26 -1
|
@@ -5,6 +5,63 @@ issue_title: "Extract shared test fixtures to reduce test duplication"
|
|
|
5
5
|
|
|
6
6
|
# Retro: #208 — Extract shared test fixtures to reduce test duplication
|
|
7
7
|
|
|
8
|
+
## Stage: Final Retrospective (2026-05-25T22:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Reviewed the planning, TDD, and shipping sessions for issue #208.
|
|
13
|
+
The work completed Phase 12 of the pi-subagents improvement roadmap.
|
|
14
|
+
Identified Vitest v4 type compatibility as the dominant friction pattern, and the user surfaced a strategic insight: shared test factories are treating symptoms of ISP violations in production interfaces.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- The planning session's duplication diff (comparing default values across all copies before writing the shared factory) prevented cascading assertion failures during migration — the testing skill rule paid off.
|
|
21
|
+
- Using a Python script for the `agent-manager.test.ts` spawn consolidation (step 9) handled 42 multi-line replacements with 7 distinct patterns cleanly in one pass.
|
|
22
|
+
- `pnpm run check` caught 4 separate type errors that Vitest's esbuild would have silently ignored at runtime: wrong `findAgentFile` parameter type, dropped `AgentConfig` import, unused `@ts-expect-error` directives, and `Mock<Procedure>` incompatibility.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
1. `missing-context` — Wrote `vi.fn().mockReturnValue([])` in the shared factory without checking whether `Mock<Procedure>` is structurally assignable to the production function signatures (`RunnerIO`, `AgentFileOps`).
|
|
27
|
+
The testing skill warns about `ReturnType<typeof vi.fn>` at the field level, but I didn't connect this to the structural-compatibility scenario where a factory's return value flows to a typed production parameter.
|
|
28
|
+
Impact: 37 type errors after step 2, requiring a fixup commit (2953adc) and removal of the `assemblerOverrides` parameter.
|
|
29
|
+
2. `missing-context` — Assumed `findAgentFile` took `(dir: string, name: string)` based on test usage patterns rather than checking the `AgentFileOps` interface.
|
|
30
|
+
The actual signature is `(name: string, dirs: string[])` — the second parameter is `string[]`.
|
|
31
|
+
Impact: caught by `pnpm run check`, fixed in the same commit, no rework.
|
|
32
|
+
3. `scope-drift` — Removed `import type { AgentConfig }` from `agent-config-editor.test.ts` because the symbols it was needed for (`testDefaultConfig`, `testCustomConfig`) were being removed.
|
|
33
|
+
Didn't check that `buildEjectContent` tests 400 lines later also used `AgentConfig`.
|
|
34
|
+
Impact: caught by `pnpm run check`, one-line fix, no rework.
|
|
35
|
+
4. `premature-convergence` — Added `@ts-expect-error` directives for `.mock` property access in the factory tests, assuming TypeScript wouldn't know about Mock's `.mock` property.
|
|
36
|
+
Once the `vi.fn()` stubs were typed with implementations, TypeScript recognized the Mock type and `.mock` was accessible, making all 5 directives unused.
|
|
37
|
+
Impact: added friction but no rework — eslint caught them.
|
|
38
|
+
|
|
39
|
+
#### What caused friction (user side)
|
|
40
|
+
|
|
41
|
+
- No friction caused by the user.
|
|
42
|
+
The user's post-ship reflection ("we need to eliminate complex setup, right?") was a strategic insight that elevated the conversation from mechanical duplication reduction to ISP-driven interface narrowing.
|
|
43
|
+
This kind of mid-session reframing is valuable and came at exactly the right time — after the work was done, so it informs future phases rather than disrupting the current one.
|
|
44
|
+
|
|
45
|
+
#### Phase 13 guidance: ISP narrowing targets
|
|
46
|
+
|
|
47
|
+
The `test/helpers/` factory inventory is a concrete map of production interfaces worth narrowing:
|
|
48
|
+
|
|
49
|
+
| Factory | Production interface | Methods | Consumer usage |
|
|
50
|
+
| ------------------- | ------------------------------------------------------------- | ----------------------------- | -------------------------------- |
|
|
51
|
+
| `createRunnerIO` | `RunnerIO` (`EnvironmentIO & SessionFactoryIO`) | 8 (7 functions + assemblerIO) | Most tests use 2–3 |
|
|
52
|
+
| `makeFileOps` | `AgentFileOps` | 6 | Most tests use `exists` + `read` |
|
|
53
|
+
| `createToolDeps` | `AgentToolManager` + `AgentToolRuntime` + `AgentToolSettings` | 12+ across 3 interfaces | Spawners use 2–3 each |
|
|
54
|
+
| `createMockSession` | `AgentSession` (SDK class) | 5 stubs | Observers use `subscribe` only |
|
|
55
|
+
| `createAgentLookup` | `AgentConfigLookup` | 2 | Already narrow ✓ |
|
|
56
|
+
|
|
57
|
+
When a factory needs its own unit tests, the interface it stubs is too wide for its consumers.
|
|
58
|
+
The fix is ISP narrowing of the production interface, not more test infrastructure.
|
|
59
|
+
|
|
60
|
+
### Changes made
|
|
61
|
+
|
|
62
|
+
1. Added rule to `testing` skill: typed implementations in shared factories when return value satisfies a production interface.
|
|
63
|
+
2. Added ISP signal to `improvement-discovery` skill Category D table: shared factory complexity → narrow the production interface.
|
|
64
|
+
|
|
8
65
|
## Stage: Implementation — TDD (2026-05-25T21:00:00Z)
|
|
9
66
|
|
|
10
67
|
### Session summary
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 214
|
|
3
|
+
issue_title: "Convert remaining closure factories to classes (Phase 13, Step 1)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #214 — Convert remaining closure factories to classes
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-25T20:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 4-step TDD plan to convert the three remaining closure factories (`createAgentConfigEditor`, `createAgentCreationWizard`, `createSubagentsService`) to classes.
|
|
13
|
+
Each conversion is one commit covering source, test, and consumer updates together.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The conversions are entirely mechanical — same pattern as Phase 11 (#195, #196).
|
|
18
|
+
No design ambiguity requiring user input.
|
|
19
|
+
- `AgentCreationWizardDeps` is only used within its own file, so removing it is safe.
|
|
20
|
+
The class dissolves the deps bag into positional constructor params for consistency with `AgentConfigEditor`.
|
|
21
|
+
- The `agent-creation-wizard.test.ts` has ~18 inline `createAgentCreationWizard(deps)` calls; the plan suggests adding a `makeWizard(deps)` helper to centralize construction and reduce the diff size.
|
|
22
|
+
- `SubagentsServiceAdapter` uses `implements SubagentsService` for compile-time verification, unlike the factory which relied on structural typing of the returned object literal.
|
|
23
|
+
- Pure helper functions (`buildMenuOptions`, `buildEjectContent`, `toSubagentRecord`) and narrow interfaces (`AgentManagerLike`, `ServiceRuntimeLike`, `WizardManager`, `WizardRegistry`) remain unchanged.
|
|
24
|
+
|
|
25
|
+
## Stage: Implementation — TDD (2026-05-25T21:00:00Z)
|
|
26
|
+
|
|
27
|
+
### Session summary
|
|
28
|
+
|
|
29
|
+
All 4 TDD steps completed in 4 commits.
|
|
30
|
+
Three closure factories converted to classes (`AgentConfigEditor`, `AgentCreationWizard`, `SubagentsServiceAdapter`) with tests and consumers updated in the same commit as each production change.
|
|
31
|
+
Test count held at 913 (57 files) — no new tests needed, no tests removed.
|
|
32
|
+
|
|
33
|
+
### Observations
|
|
34
|
+
|
|
35
|
+
- All three conversions were mechanical find-and-replace with no behavioral surprises.
|
|
36
|
+
- The `makeWizard(deps)` helper in `agent-creation-wizard.test.ts` centralized 14 inline `createAgentCreationWizard(deps)` calls, keeping the diff readable.
|
|
37
|
+
- `SubagentsServiceAdapter` uses `implements SubagentsService` — the TypeScript compiler confirmed the contract at compile time with no gaps.
|
|
38
|
+
- Adding the `SpawnOptions` import to `service-adapter.ts` was required for the `spawn` method signature; the plan anticipated this correctly.
|
|
39
|
+
- The `sed -i` command required the macOS `-i ''` form (no in-place backup extension) rather than the GNU `sed -i` form.
|
|
40
|
+
- Dead-code gate (`pnpm fallow dead-code`) passed cleanly from the repo root — no suppression needed.
|
|
41
|
+
|
|
42
|
+
## Stage: Final Retrospective (2026-05-25T22:00:00Z)
|
|
43
|
+
|
|
44
|
+
### Session summary
|
|
45
|
+
|
|
46
|
+
Shipped `pi-subagents-v7.3.2` with 3 refactor commits converting all remaining closure factories to classes.
|
|
47
|
+
All 4 lifecycle stages (plan → TDD → ship → retro) completed in a single day with zero rework and zero deviations from the plan.
|
|
48
|
+
|
|
49
|
+
### Observations
|
|
50
|
+
|
|
51
|
+
#### What went well
|
|
52
|
+
|
|
53
|
+
- Strong precedent from Phase 11 (#195, #196) made this issue zero-friction — the plan, implementation, and test updates all followed an established template.
|
|
54
|
+
- The plan's prediction of a `makeWizard(deps)` helper for `agent-creation-wizard.test.ts` kept the step-2 diff readable by centralizing 14 inline constructor calls.
|
|
55
|
+
- `SubagentsServiceAdapter implements SubagentsService` gave compile-time contract verification, catching any interface drift immediately via `pnpm run check`.
|
|
56
|
+
- The plan correctly anticipated the `SpawnOptions` import need in `service-adapter.ts`.
|
|
57
|
+
|
|
58
|
+
#### What caused friction (agent side)
|
|
59
|
+
|
|
60
|
+
- None.
|
|
61
|
+
This was a textbook mechanical refactoring with no behavioral changes, no edge cases, and no test rework.
|
|
62
|
+
|
|
63
|
+
#### What caused friction (user side)
|
|
64
|
+
|
|
65
|
+
- None.
|
|
66
|
+
The issue was well-scoped with explicit target files and a clear precedent to follow.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { buildEventData, type NotificationDetails, NotificationManager } from "#
|
|
|
31
31
|
import { createNotificationRenderer } from "#src/observation/renderer";
|
|
32
32
|
import { createSubagentRuntime } from "#src/runtime";
|
|
33
33
|
import { publishSubagentsService, unpublishSubagentsService } from "#src/service/service";
|
|
34
|
-
import {
|
|
34
|
+
import { SubagentsServiceAdapter } from "#src/service/service-adapter";
|
|
35
35
|
import { detectEnv } from "#src/session/env";
|
|
36
36
|
|
|
37
37
|
import { resolveModel } from "#src/session/model-resolver";
|
|
@@ -157,7 +157,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
157
157
|
|
|
158
158
|
// Typed service published via Symbol.for() for cross-extension access.
|
|
159
159
|
// Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
|
|
160
|
-
const service =
|
|
160
|
+
const service = new SubagentsServiceAdapter(manager, resolveModel, runtime);
|
|
161
161
|
publishSubagentsService(service);
|
|
162
162
|
|
|
163
163
|
const lifecycle = new SessionLifecycleHandler(
|
|
@@ -455,8 +455,9 @@ export function getAgentConversation(session: AgentSession): string {
|
|
|
455
455
|
if (text.trim()) parts.push(`[User]: ${text.trim()}`);
|
|
456
456
|
} else if (msg.role === "assistant") {
|
|
457
457
|
const { textParts, toolNames } = extractAssistantContent(msg.content);
|
|
458
|
+
const attribution = formatAttribution(msg);
|
|
458
459
|
if (textParts.length > 0)
|
|
459
|
-
parts.push(`[Assistant]: ${textParts.join("\n")}`);
|
|
460
|
+
parts.push(`[Assistant${attribution}]: ${textParts.join("\n")}`);
|
|
460
461
|
if (toolNames.length > 0)
|
|
461
462
|
parts.push(`[Tool Calls]:\n${toolNames.map((n) => ` Tool: ${n}`).join("\n")}`);
|
|
462
463
|
} else if (msg.role === "toolResult") {
|
|
@@ -468,3 +469,11 @@ export function getAgentConversation(session: AgentSession): string {
|
|
|
468
469
|
|
|
469
470
|
return parts.join("\n\n");
|
|
470
471
|
}
|
|
472
|
+
|
|
473
|
+
/** Build a `(provider/model)` attribution suffix for assistant messages. */
|
|
474
|
+
function formatAttribution(msg: { provider?: string; model?: string }): string {
|
|
475
|
+
const { provider, model } = msg;
|
|
476
|
+
if (!provider && !model) return "";
|
|
477
|
+
if (provider && model) return ` (${provider}/${model})`;
|
|
478
|
+
return ` (${provider ?? model})`;
|
|
479
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
9
|
-
import type { SubagentRecord, SubagentsService } from "#src/service/service";
|
|
9
|
+
import type { SpawnOptions, SubagentRecord, SubagentsService } from "#src/service/service";
|
|
10
10
|
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
11
11
|
import type { AgentRecord, SessionContext } from "#src/types";
|
|
12
12
|
|
|
@@ -30,83 +30,83 @@ export interface ServiceRuntimeLike {
|
|
|
30
30
|
buildSnapshot(inheritContext: boolean): ParentSnapshot;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/**
|
|
34
|
-
export
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
33
|
+
/** Adapter that wraps AgentManager to satisfy SubagentsService. */
|
|
34
|
+
export class SubagentsServiceAdapter implements SubagentsService {
|
|
35
|
+
constructor(
|
|
36
|
+
private readonly manager: AgentManagerLike,
|
|
37
|
+
private readonly resolveModel: (input: string, registry: ModelRegistry) => unknown,
|
|
38
|
+
private readonly runtime: ServiceRuntimeLike,
|
|
39
|
+
) {}
|
|
40
|
+
|
|
41
|
+
spawn(type: string, prompt: string, options?: SpawnOptions): string {
|
|
42
|
+
if (!this.runtime.currentCtx) {
|
|
43
|
+
throw new Error("No active session — cannot spawn agents outside a session.");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let model: unknown;
|
|
47
|
+
if (options?.model) {
|
|
48
|
+
const registry = this.runtime.currentCtx.modelRegistry;
|
|
49
|
+
if (!registry) {
|
|
50
|
+
throw new Error("No model registry available.");
|
|
43
51
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const registry = runtime.currentCtx.modelRegistry;
|
|
48
|
-
if (!registry) {
|
|
49
|
-
throw new Error("No model registry available.");
|
|
50
|
-
}
|
|
51
|
-
const resolved = resolveModel(options.model, registry);
|
|
52
|
-
if (typeof resolved === "string") {
|
|
53
|
-
throw new Error(resolved);
|
|
54
|
-
}
|
|
55
|
-
model = resolved;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const description = options?.description ?? prompt.slice(0, 80);
|
|
59
|
-
const isBackground = !(options?.foreground ?? false);
|
|
60
|
-
|
|
61
|
-
const snapshot = runtime.buildSnapshot(options?.inheritContext ?? false);
|
|
62
|
-
return manager.spawn(snapshot, type, prompt, {
|
|
63
|
-
description,
|
|
64
|
-
model,
|
|
65
|
-
maxTurns: options?.maxTurns,
|
|
66
|
-
thinkingLevel: options?.thinkingLevel,
|
|
67
|
-
isolated: options?.isolated,
|
|
68
|
-
inheritContext: options?.inheritContext,
|
|
69
|
-
bypassQueue: options?.bypassQueue,
|
|
70
|
-
isolation: options?.isolation,
|
|
71
|
-
isBackground,
|
|
72
|
-
});
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
getRecord(id: string): SubagentRecord | undefined {
|
|
76
|
-
const record = manager.getRecord(id);
|
|
77
|
-
return record ? toSubagentRecord(record) : undefined;
|
|
78
|
-
},
|
|
79
|
-
|
|
80
|
-
listAgents(): SubagentRecord[] {
|
|
81
|
-
return manager.listAgents().map(toSubagentRecord);
|
|
82
|
-
},
|
|
83
|
-
|
|
84
|
-
abort(id: string): boolean {
|
|
85
|
-
return manager.abort(id);
|
|
86
|
-
},
|
|
87
|
-
|
|
88
|
-
async steer(id: string, message: string): Promise<boolean> {
|
|
89
|
-
const record = manager.getRecord(id);
|
|
90
|
-
if (record?.status !== "running") {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
const session = record.session;
|
|
94
|
-
if (!session) {
|
|
95
|
-
// Session not ready yet — queue via manager for delivery once initialized
|
|
96
|
-
return manager.queueSteer(id, message);
|
|
52
|
+
const resolved = this.resolveModel(options.model, registry);
|
|
53
|
+
if (typeof resolved === "string") {
|
|
54
|
+
throw new Error(resolved);
|
|
97
55
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
56
|
+
model = resolved;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const description = options?.description ?? prompt.slice(0, 80);
|
|
60
|
+
const isBackground = !(options?.foreground ?? false);
|
|
61
|
+
|
|
62
|
+
const snapshot = this.runtime.buildSnapshot(options?.inheritContext ?? false);
|
|
63
|
+
return this.manager.spawn(snapshot, type, prompt, {
|
|
64
|
+
description,
|
|
65
|
+
model,
|
|
66
|
+
maxTurns: options?.maxTurns,
|
|
67
|
+
thinkingLevel: options?.thinkingLevel,
|
|
68
|
+
isolated: options?.isolated,
|
|
69
|
+
inheritContext: options?.inheritContext,
|
|
70
|
+
bypassQueue: options?.bypassQueue,
|
|
71
|
+
isolation: options?.isolation,
|
|
72
|
+
isBackground,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
getRecord(id: string): SubagentRecord | undefined {
|
|
77
|
+
const record = this.manager.getRecord(id);
|
|
78
|
+
return record ? toSubagentRecord(record) : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
listAgents(): SubagentRecord[] {
|
|
82
|
+
return this.manager.listAgents().map(toSubagentRecord);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
abort(id: string): boolean {
|
|
86
|
+
return this.manager.abort(id);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async steer(id: string, message: string): Promise<boolean> {
|
|
90
|
+
const record = this.manager.getRecord(id);
|
|
91
|
+
if (record?.status !== "running") {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const session = record.session;
|
|
95
|
+
if (!session) {
|
|
96
|
+
// Session not ready yet — queue via manager for delivery once initialized
|
|
97
|
+
return this.manager.queueSteer(id, message);
|
|
98
|
+
}
|
|
99
|
+
await session.steer(message);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async waitForAll(): Promise<void> {
|
|
104
|
+
return this.manager.waitForAll();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
hasRunning(): boolean {
|
|
108
|
+
return this.manager.hasRunning();
|
|
109
|
+
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
/**
|
|
@@ -61,86 +61,88 @@ export function buildEjectContent(cfg: AgentConfig): string {
|
|
|
61
61
|
return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
// ----
|
|
65
|
-
|
|
66
|
-
export
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
64
|
+
// ---- Class ----
|
|
65
|
+
|
|
66
|
+
export class AgentConfigEditor {
|
|
67
|
+
constructor(
|
|
68
|
+
private readonly fileOps: AgentFileOps,
|
|
69
|
+
private readonly registry: AgentTypeRegistry,
|
|
70
|
+
private readonly personalAgentsDir: string,
|
|
71
|
+
private readonly projectAgentsDir: string,
|
|
72
|
+
) {}
|
|
73
|
+
|
|
74
|
+
private agentDirs(): string[] {
|
|
75
|
+
return [this.projectAgentsDir, this.personalAgentsDir];
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
async
|
|
77
|
-
if (registry.resolveType(name) == null) {
|
|
78
|
+
async showAgentDetail(ui: MenuUI, name: string): Promise<void> {
|
|
79
|
+
if (this.registry.resolveType(name) == null) {
|
|
78
80
|
ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
79
81
|
return;
|
|
80
82
|
}
|
|
81
|
-
const cfg = registry.resolveAgentConfig(name);
|
|
82
|
-
const file = fileOps.findAgentFile(name, agentDirs());
|
|
83
|
+
const cfg = this.registry.resolveAgentConfig(name);
|
|
84
|
+
const file = this.fileOps.findAgentFile(name, this.agentDirs());
|
|
83
85
|
|
|
84
86
|
const choice = await ui.select(name, buildMenuOptions(cfg, file));
|
|
85
87
|
if (!choice || choice === "Back") return;
|
|
86
88
|
|
|
87
|
-
if (choice === "Edit" && file) await handleEdit(ui, name, file);
|
|
88
|
-
else if (choice === "Delete" && file) await handleDelete(ui, name, file);
|
|
89
|
+
if (choice === "Edit" && file) await this.handleEdit(ui, name, file);
|
|
90
|
+
else if (choice === "Delete" && file) await this.handleDelete(ui, name, file);
|
|
89
91
|
else if (choice === "Reset to default" && file)
|
|
90
|
-
await handleReset(ui, name, file);
|
|
91
|
-
else if (choice.startsWith("Eject")) await ejectAgent(ui, name, cfg);
|
|
92
|
-
else if (choice === "Disable") await disableAgent(ui, name);
|
|
93
|
-
else if (choice === "Enable") await enableAgent(ui, name);
|
|
92
|
+
await this.handleReset(ui, name, file);
|
|
93
|
+
else if (choice.startsWith("Eject")) await this.ejectAgent(ui, name, cfg);
|
|
94
|
+
else if (choice === "Disable") await this.disableAgent(ui, name);
|
|
95
|
+
else if (choice === "Enable") await this.enableAgent(ui, name);
|
|
94
96
|
}
|
|
95
97
|
|
|
96
|
-
async
|
|
97
|
-
const content = fileOps.read(file);
|
|
98
|
+
private async handleEdit(ui: MenuUI, name: string, file: string): Promise<void> {
|
|
99
|
+
const content = this.fileOps.read(file);
|
|
98
100
|
if (content === undefined) return;
|
|
99
101
|
const edited = await ui.editor(`Edit ${name}`, content);
|
|
100
102
|
if (edited !== undefined && edited !== content) {
|
|
101
|
-
fileOps.write(file, edited);
|
|
102
|
-
registry.reload();
|
|
103
|
+
this.fileOps.write(file, edited);
|
|
104
|
+
this.registry.reload();
|
|
103
105
|
ui.notify(`Updated ${file}`, "info");
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
108
|
|
|
107
|
-
async
|
|
109
|
+
private async handleDelete(ui: MenuUI, name: string, file: string): Promise<void> {
|
|
108
110
|
const confirmed = await ui.confirm(
|
|
109
111
|
"Delete agent",
|
|
110
112
|
`Delete ${name} (${file})?`,
|
|
111
113
|
);
|
|
112
114
|
if (confirmed) {
|
|
113
|
-
fileOps.remove(file);
|
|
114
|
-
registry.reload();
|
|
115
|
+
this.fileOps.remove(file);
|
|
116
|
+
this.registry.reload();
|
|
115
117
|
ui.notify(`Deleted ${file}`, "info");
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
|
|
119
|
-
async
|
|
121
|
+
private async handleReset(ui: MenuUI, name: string, file: string): Promise<void> {
|
|
120
122
|
const confirmed = await ui.confirm(
|
|
121
123
|
"Reset to default",
|
|
122
124
|
`Delete override ${file} and restore embedded default?`,
|
|
123
125
|
);
|
|
124
126
|
if (confirmed) {
|
|
125
|
-
fileOps.remove(file);
|
|
126
|
-
registry.reload();
|
|
127
|
+
this.fileOps.remove(file);
|
|
128
|
+
this.registry.reload();
|
|
127
129
|
ui.notify(`Restored default ${name}`, "info");
|
|
128
130
|
}
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
async
|
|
133
|
+
private async ejectAgent(ui: MenuUI, name: string, cfg: AgentConfig): Promise<void> {
|
|
132
134
|
const location = await ui.select("Choose location", [
|
|
133
135
|
"Project (.pi/agents/)",
|
|
134
|
-
`Personal (${personalAgentsDir})`,
|
|
136
|
+
`Personal (${this.personalAgentsDir})`,
|
|
135
137
|
]);
|
|
136
138
|
if (!location) return;
|
|
137
139
|
|
|
138
140
|
const targetDir = location.startsWith("Project")
|
|
139
|
-
? projectAgentsDir
|
|
140
|
-
: personalAgentsDir;
|
|
141
|
+
? this.projectAgentsDir
|
|
142
|
+
: this.personalAgentsDir;
|
|
141
143
|
|
|
142
144
|
const targetPath = join(targetDir, `${name}.md`);
|
|
143
|
-
if (fileOps.exists(targetPath)) {
|
|
145
|
+
if (this.fileOps.exists(targetPath)) {
|
|
144
146
|
const overwrite = await ui.confirm(
|
|
145
147
|
"Overwrite",
|
|
146
148
|
`${targetPath} already exists. Overwrite?`,
|
|
@@ -148,23 +150,23 @@ export function createAgentConfigEditor(
|
|
|
148
150
|
if (!overwrite) return;
|
|
149
151
|
}
|
|
150
152
|
|
|
151
|
-
fileOps.write(targetPath, buildEjectContent(cfg));
|
|
152
|
-
registry.reload();
|
|
153
|
+
this.fileOps.write(targetPath, buildEjectContent(cfg));
|
|
154
|
+
this.registry.reload();
|
|
153
155
|
ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
154
156
|
}
|
|
155
157
|
|
|
156
|
-
async
|
|
157
|
-
const file = fileOps.findAgentFile(name, agentDirs());
|
|
158
|
+
private async disableAgent(ui: MenuUI, name: string): Promise<void> {
|
|
159
|
+
const file = this.fileOps.findAgentFile(name, this.agentDirs());
|
|
158
160
|
if (file) {
|
|
159
|
-
const content = fileOps.read(file);
|
|
161
|
+
const content = this.fileOps.read(file);
|
|
160
162
|
if (content?.includes("\nenabled: false\n")) {
|
|
161
163
|
ui.notify(`${name} is already disabled.`, "info");
|
|
162
164
|
return;
|
|
163
165
|
}
|
|
164
166
|
if (content) {
|
|
165
167
|
const updated = content.replace(/^---\n/, "---\nenabled: false\n");
|
|
166
|
-
fileOps.write(file, updated);
|
|
167
|
-
registry.reload();
|
|
168
|
+
this.fileOps.write(file, updated);
|
|
169
|
+
this.registry.reload();
|
|
168
170
|
ui.notify(`Disabled ${name} (${file})`, "info");
|
|
169
171
|
}
|
|
170
172
|
return;
|
|
@@ -172,40 +174,38 @@ export function createAgentConfigEditor(
|
|
|
172
174
|
|
|
173
175
|
const location = await ui.select("Choose location", [
|
|
174
176
|
"Project (.pi/agents/)",
|
|
175
|
-
`Personal (${personalAgentsDir})`,
|
|
177
|
+
`Personal (${this.personalAgentsDir})`,
|
|
176
178
|
]);
|
|
177
179
|
if (!location) return;
|
|
178
180
|
|
|
179
181
|
const targetDir = location.startsWith("Project")
|
|
180
|
-
? projectAgentsDir
|
|
181
|
-
: personalAgentsDir;
|
|
182
|
+
? this.projectAgentsDir
|
|
183
|
+
: this.personalAgentsDir;
|
|
182
184
|
|
|
183
185
|
const targetPath = join(targetDir, `${name}.md`);
|
|
184
|
-
fileOps.write(targetPath, "---\nenabled: false\n---\n");
|
|
185
|
-
registry.reload();
|
|
186
|
+
this.fileOps.write(targetPath, "---\nenabled: false\n---\n");
|
|
187
|
+
this.registry.reload();
|
|
186
188
|
ui.notify(`Disabled ${name} (${targetPath})`, "info");
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
190
|
-
async
|
|
191
|
-
const file = fileOps.findAgentFile(name, agentDirs());
|
|
192
|
+
private async enableAgent(ui: MenuUI, name: string): Promise<void> {
|
|
193
|
+
const file = this.fileOps.findAgentFile(name, this.agentDirs());
|
|
192
194
|
if (!file) return;
|
|
193
195
|
|
|
194
|
-
const content = fileOps.read(file);
|
|
196
|
+
const content = this.fileOps.read(file);
|
|
195
197
|
if (!content) return;
|
|
196
198
|
|
|
197
199
|
const updated = content.replace(/^(---\n)enabled: false\n/, "$1");
|
|
198
200
|
|
|
199
201
|
if (updated.trim() === "---\n---" || updated.trim() === "---\n---\n") {
|
|
200
|
-
fileOps.remove(file);
|
|
201
|
-
registry.reload();
|
|
202
|
+
this.fileOps.remove(file);
|
|
203
|
+
this.registry.reload();
|
|
202
204
|
ui.notify(`Enabled ${name} (removed ${file})`, "info");
|
|
203
205
|
} else {
|
|
204
|
-
fileOps.write(file, updated);
|
|
205
|
-
registry.reload();
|
|
206
|
+
this.fileOps.write(file, updated);
|
|
207
|
+
this.registry.reload();
|
|
206
208
|
ui.notify(`Enabled ${name} (${file})`, "info");
|
|
207
209
|
}
|
|
208
210
|
}
|
|
209
|
-
|
|
210
|
-
return { showAgentDetail };
|
|
211
211
|
}
|