@gotgenes/pi-subagents 6.9.1 → 6.9.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 +19 -0
- package/docs/architecture/architecture.md +196 -94
- package/docs/plans/0116-type-housekeeping.md +351 -0
- package/docs/retro/0115-decompose-agent-tool.md +51 -0
- package/docs/retro/0116-type-housekeeping.md +42 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +2 -1
- package/src/agent-runner.ts +2 -1
- package/src/env.ts +7 -1
- package/src/index.ts +2 -3
- package/src/notification.ts +48 -33
- package/src/parent-snapshot.ts +20 -1
- package/src/prompts.ts +3 -2
- package/src/renderer.ts +1 -1
- package/src/session-config.ts +2 -1
- package/src/types.ts +14 -46
- package/src/ui/agent-menu.ts +1 -1
- package/src/ui/conversation-viewer.ts +27 -10
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [6.9.3](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.2...pi-subagents-v6.9.3) (2026-05-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* add issue numbers to Phase 8 roadmap steps ([77fee40](https://github.com/gotgenes/pi-packages/commit/77fee402ebc445171f3768f2eea1bba242ce9723))
|
|
14
|
+
* add Phase 8 roadmap — testability, display extraction, menu decomposition ([37a0520](https://github.com/gotgenes/pi-packages/commit/37a0520e32a93f4c9bffd5a728882c68e9811024))
|
|
15
|
+
* clean up architecture.md progress tracking ([e006032](https://github.com/gotgenes/pi-packages/commit/e00603209b9c21cb1bef41c34eeb71c1cc338117))
|
|
16
|
+
* **retro:** add retro notes for issue [#116](https://github.com/gotgenes/pi-packages/issues/116) ([701dca8](https://github.com/gotgenes/pi-packages/commit/701dca8075221aa4c36e19e2d54d43c74863ea57))
|
|
17
|
+
|
|
18
|
+
## [6.9.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.1...pi-subagents-v6.9.2) (2026-05-22)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
* mark E2 type housekeeping done in architecture ([#116](https://github.com/gotgenes/pi-packages/issues/116)) ([89f18a8](https://github.com/gotgenes/pi-packages/commit/89f18a82fac39b4a62be8e440355b859afaa6d2f))
|
|
24
|
+
* plan type housekeeping and small structural cleanups ([#116](https://github.com/gotgenes/pi-packages/issues/116)) ([e1cbd26](https://github.com/gotgenes/pi-packages/commit/e1cbd269961a1bff3b11e2e916733a79c39a087d))
|
|
25
|
+
* **retro:** add retro notes for issue [#115](https://github.com/gotgenes/pi-packages/issues/115) ([05b8809](https://github.com/gotgenes/pi-packages/commit/05b88093f7b70ea886b6c7cb5f0cc96161a95df6))
|
|
26
|
+
|
|
8
27
|
## [6.9.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.0...pi-subagents-v6.9.1) (2026-05-22)
|
|
9
28
|
|
|
10
29
|
|
|
@@ -322,10 +322,15 @@ Target: every mutable state bag becomes a class, every dependency bag narrows to
|
|
|
322
322
|
The work is sequenced so each change makes the next change easy.
|
|
323
323
|
See the [Encapsulation roadmap](#encapsulation-roadmap) section for the full breakdown.
|
|
324
324
|
|
|
325
|
+
### Phase 8: Testability, display extraction, and menu decomposition
|
|
326
|
+
|
|
327
|
+
Target: eliminate `vi.mock()` module mocking in the two most fragile test suites by injecting IO-touching collaborators; consolidate shared test fixtures; extract display helpers into a reusable module; decompose the largest UI file.
|
|
328
|
+
|
|
329
|
+
See the [Phase 8 roadmap](#phase-8-roadmap) section for the full breakdown.
|
|
330
|
+
|
|
325
331
|
## Structural refactoring roadmap
|
|
326
332
|
|
|
327
|
-
Phases 1–5 are complete.
|
|
328
|
-
Phase 7 (encapsulation and dependency narrowing) is the active structural track.
|
|
333
|
+
Phases 1–5 and 7 are complete.
|
|
329
334
|
See `git log` for the full history; issue references are preserved below for traceability.
|
|
330
335
|
|
|
331
336
|
| Phase | Issue | Summary |
|
|
@@ -379,137 +384,105 @@ This section describes the Phase 7 targets: encapsulating mutable state into cla
|
|
|
379
384
|
|
|
380
385
|
Each step is sequenced so it makes the next step easier.
|
|
381
386
|
|
|
382
|
-
###
|
|
387
|
+
### Resolved smells
|
|
388
|
+
|
|
389
|
+
All nine smells identified at the start of Phase 7 were resolved:
|
|
383
390
|
|
|
384
|
-
| Smell
|
|
385
|
-
|
|
|
386
|
-
|
|
|
387
|
-
| Closure bag as class
|
|
388
|
-
|
|
|
389
|
-
|
|
|
390
|
-
|
|
|
391
|
-
|
|
|
392
|
-
|
|
|
393
|
-
| Type dumping ground
|
|
394
|
-
|
|
|
391
|
+
| Smell | Resolution |
|
|
392
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
393
|
+
| Global mutable state | `AgentTypeRegistry` class (#108); `reloadCustomAgents` callback removed from dep bags |
|
|
394
|
+
| Closure bag as class | `NotificationManager` class (#116); `pendingNudges` and timer state are private fields |
|
|
395
|
+
| Mutable state bag | `AgentActivityTracker` class (#110); transition methods replace external writes |
|
|
396
|
+
| Settings relay | `SettingsManager` class (#109); 6 callback fields collapsed to one object |
|
|
397
|
+
| Post-construction mutation | `ExecutionState`, `WorktreeState`, `NotificationState` collaborators (#111); stats behind mutation methods |
|
|
398
|
+
| Fire-and-forget callbacks | `AgentManagerObserver` interface (#112); one observer object replaces 3 closure lambdas |
|
|
399
|
+
| Duplicate `SpawnOptions` | Internal type renamed to `AgentSpawnConfig` (#113); public `SpawnOptions` unchanged |
|
|
400
|
+
| Type dumping ground | `NotificationDetails`, `ParentSnapshot`, `EnvInfo` moved to their natural modules (#116); narrow subsets defined |
|
|
401
|
+
| Wide dependency bags | `AgentToolDeps` 9 → 6, `AgentMenuDeps` 8 → 7 (#114); `emitEvent` removed; description text derived from registry; `agentActivity` narrowed |
|
|
395
402
|
|
|
396
403
|
### Step A: Extract state into classes (foundation, parallel)
|
|
397
404
|
|
|
398
405
|
These three extractions are independent and can proceed in any order.
|
|
399
406
|
Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
|
|
400
407
|
|
|
401
|
-
#### A1.
|
|
408
|
+
#### A1. AgentTypeRegistry class (#108)
|
|
402
409
|
|
|
403
|
-
|
|
404
|
-
`reloadCustomAgents`
|
|
405
|
-
`DEFAULT_AGENT_NAMES`
|
|
410
|
+
Wrapped the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
|
|
411
|
+
`reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps`; replaced by `registry.reload()`.
|
|
412
|
+
`DEFAULT_AGENT_NAMES` moved from `types.ts` to the registry.
|
|
406
413
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
#### ~~A2. `SettingsManager` class (#109)~~ — **Done**
|
|
414
|
+
#### A2. SettingsManager class (#109, #118)
|
|
410
415
|
|
|
411
416
|
Encapsulated settings load/save/apply cycle into `SettingsManager` (in `settings.ts`).
|
|
412
417
|
Owns `defaultMaxTurns`, `graceTurns`, `maxConcurrent` with normalizing property accessors.
|
|
413
|
-
|
|
418
|
+
Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` — each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
|
|
414
419
|
The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
|
|
415
|
-
`AgentManager` reads `maxConcurrent` via injected `getMaxConcurrent` function.
|
|
416
|
-
`SubagentRuntime.defaultMaxTurns` and `.graceTurns` removed.
|
|
417
|
-
|
|
418
|
-
Impact: reduced `AgentMenuDeps` from 13 → 8 fields; `AgentToolDeps` from 8 → 7 fields.
|
|
419
|
-
|
|
420
|
-
#### ~~A2b. `SettingsManager` apply methods (#118)~~ — **Done**
|
|
421
|
-
|
|
422
|
-
Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` to `SettingsManager`.
|
|
423
|
-
Each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
|
|
424
|
-
`SettingsManager` accepts an `onMaxConcurrentChanged` callback (wired to `manager.notifyConcurrencyChanged()` at init).
|
|
425
|
-
`notifyConcurrencyChanged` removed from `AgentMenuManager`; `showSettings` now makes a single apply call per setting.
|
|
426
420
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
#### ~~A3. `AgentActivityTracker` class (#110)~~ — **Done**
|
|
421
|
+
#### A3. AgentActivityTracker class (#110)
|
|
430
422
|
|
|
431
423
|
Wrapped the 7-field mutable `AgentActivity` interface in an `AgentActivityTracker` class (`src/ui/agent-activity-tracker.ts`).
|
|
432
|
-
`ui-observer.ts` calls transition methods
|
|
433
|
-
The
|
|
434
|
-
The shared map on `SubagentRuntime` is now `Map<string, AgentActivityTracker>`.
|
|
435
|
-
|
|
436
|
-
Impact: eliminates output-argument writes in `ui-observer.ts`, makes the mutation contract explicit.
|
|
424
|
+
`ui-observer.ts` calls transition methods; consumers use read-only accessors.
|
|
425
|
+
The shared map on `SubagentRuntime` is `Map<string, AgentActivityTracker>`.
|
|
437
426
|
|
|
438
|
-
###
|
|
427
|
+
### Step B: Split AgentRecord lifecycle state (#111)
|
|
439
428
|
|
|
440
429
|
Split post-construction mutation into phase-specific collaborators, each born complete:
|
|
441
430
|
|
|
442
|
-
- **`ExecutionState`** (`session`, `outputFile`) — constructed in `onSessionCreated
|
|
443
|
-
- **`WorktreeState`** (`path`, `branch`, `cleanupResult`) — constructed at worktree setup
|
|
444
|
-
- **`NotificationState`** (`toolCallId`, `resultConsumed`) — constructed by `AgentManager.spawn()` when `toolCallId` is provided
|
|
445
|
-
- **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager
|
|
446
|
-
- Stats
|
|
431
|
+
- **`ExecutionState`** (`session`, `outputFile`) — constructed in `onSessionCreated`.
|
|
432
|
+
- **`WorktreeState`** (`path`, `branch`, `cleanupResult`) — constructed at worktree setup.
|
|
433
|
+
- **`NotificationState`** (`toolCallId`, `resultConsumed`) — constructed by `AgentManager.spawn()` when `toolCallId` is provided.
|
|
434
|
+
- **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager`.
|
|
435
|
+
- Stats encapsulated behind mutation methods with read-only getters.
|
|
447
436
|
- `AgentRecordInit` trimmed from 19 optional fields to 4 construction-time fields.
|
|
448
437
|
|
|
449
|
-
|
|
450
|
-
The record doesn't accumulate half-baked state — it receives fully constructed collaborators.
|
|
438
|
+
### Step C: Replace AgentManager callbacks with observer (#112)
|
|
451
439
|
|
|
452
|
-
### Step C: Replace `AgentManager` callbacks with observer (#112) ✅
|
|
453
|
-
|
|
454
|
-
**Done.**
|
|
455
440
|
`AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
|
|
456
441
|
`index.ts` constructs one observer object instead of 3 closure lambdas.
|
|
457
442
|
`AgentManagerOptions` drops from 9 → 7 fields.
|
|
458
443
|
|
|
459
|
-
### Step D: Disambiguate
|
|
460
|
-
|
|
461
|
-
With the registry class, settings manager, and observer in place, the dependency bags shrink naturally.
|
|
444
|
+
### Step D: Disambiguate SpawnOptions and narrow dependency bags
|
|
462
445
|
|
|
463
|
-
#### D1. Disambiguate
|
|
446
|
+
#### D1. Disambiguate SpawnOptions (#113)
|
|
464
447
|
|
|
465
|
-
**Done.**
|
|
466
448
|
Internal `SpawnOptions` in `agent-manager.ts` renamed to `AgentSpawnConfig`.
|
|
467
|
-
Public `SpawnOptions` in `service.ts`
|
|
449
|
+
Public `SpawnOptions` in `service.ts` unchanged.
|
|
468
450
|
|
|
469
|
-
#### D2. Narrow
|
|
451
|
+
#### D2. Narrow AgentToolDeps and AgentMenuDeps (#114)
|
|
470
452
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
`
|
|
474
|
-
`
|
|
453
|
+
| Bag | Before | After | How |
|
|
454
|
+
| --------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------- |
|
|
455
|
+
| `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
|
|
456
|
+
| `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
|
|
475
457
|
|
|
476
|
-
|
|
477
|
-
| --------------- | -------- | ----- | ------------------------------------------------------------------------------------------------------------- |
|
|
478
|
-
| `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
|
|
479
|
-
| `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
|
|
458
|
+
### Step E: Decompose large files and relocate types
|
|
480
459
|
|
|
481
|
-
|
|
460
|
+
#### E1. Split agent-tool.ts foreground/background (#115)
|
|
482
461
|
|
|
483
|
-
#### E1. Split `agent-tool.ts` foreground/background (#115) ✅
|
|
484
|
-
|
|
485
|
-
**Done.**
|
|
486
|
-
Fixed two upstream API gaps before extracting: `onSessionCreated` now receives `(session, record)` (eliminating a `listAgents()` reverse-search), and `AgentSpawnConfig` accepts `toolCallId` (moving `NotificationState` wiring into `AgentManager.spawn()`).
|
|
487
462
|
Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
|
|
488
|
-
`agent-tool.ts` reduced from 579 → 411 lines
|
|
463
|
+
`agent-tool.ts` reduced from 579 → 411 lines.
|
|
489
464
|
|
|
490
465
|
#### E2. Type housekeeping (#116)
|
|
491
466
|
|
|
492
|
-
-
|
|
493
|
-
-
|
|
494
|
-
-
|
|
495
|
-
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
|
503
|
-
|
|
|
504
|
-
|
|
|
505
|
-
|
|
|
506
|
-
|
|
|
507
|
-
| `
|
|
508
|
-
| `
|
|
509
|
-
|
|
|
510
|
-
| `
|
|
511
|
-
| Callbacks threaded through deps | ~~8~~ 0 remaining settings callbacks (**fixed #109**); `emitEvent` ×3 remain | 0 |
|
|
512
|
-
| Types in `types.ts` without a natural home | 4 | 0 |
|
|
467
|
+
- Moved `NotificationDetails`, `ParentSnapshot`, `EnvInfo` to their natural modules.
|
|
468
|
+
- Converted `createNotificationSystem` closure to `NotificationManager` class.
|
|
469
|
+
- Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
|
|
470
|
+
- Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
|
|
471
|
+
|
|
472
|
+
### Phase 7 results
|
|
473
|
+
|
|
474
|
+
| Metric | Before | After |
|
|
475
|
+
| ------------------------------------------ | ------ | ----- |
|
|
476
|
+
| Module-scoped mutable state | 1 | 0 |
|
|
477
|
+
| Closure-bag "classes" | 2 | 0 |
|
|
478
|
+
| Externally-mutated state bags | 2 | 0 |
|
|
479
|
+
| `AgentManagerOptions` fields | 9 | 7 |
|
|
480
|
+
| `AgentToolDeps` fields | 9 | 6 |
|
|
481
|
+
| `AgentMenuDeps` fields | 13 | 7 |
|
|
482
|
+
| `SpawnOptions` callback fields | 6 | 1 |
|
|
483
|
+
| `RunOptions` callback fields | 6 | 1 |
|
|
484
|
+
| Callbacks threaded through deps | 8 | 0 |
|
|
485
|
+
| Types in `types.ts` without a natural home | 4 | 0 |
|
|
513
486
|
|
|
514
487
|
### Dependency graph
|
|
515
488
|
|
|
@@ -527,6 +500,135 @@ E2 (Type housekeeping) ── can start after A1, runs parallel to later steps
|
|
|
527
500
|
|
|
528
501
|
---
|
|
529
502
|
|
|
503
|
+
## Phase 8 roadmap
|
|
504
|
+
|
|
505
|
+
Phase 7 eliminated all structural smells (mutable state, closure bags, callback threading, wide dependency bags).
|
|
506
|
+
Phase 8 targets the next layer: testability friction, display module cohesion, and menu decomposition.
|
|
507
|
+
|
|
508
|
+
The test suite (690 tests, 1.4:1 test-to-code ratio) is comprehensive but uneven in quality.
|
|
509
|
+
Two files — `session-config.test.ts` and `agent-runner.test.ts` — account for 11 of 12 total `vi.mock()` calls and rely heavily on verifying internal call sequences rather than observable outputs.
|
|
510
|
+
This fragility is a symptom of production code that imports IO-touching collaborators directly instead of receiving them through injection.
|
|
511
|
+
|
|
512
|
+
The display and menu improvements were identified during Phase 7 but deferred because they don't gate encapsulation work.
|
|
513
|
+
They are included here because the display extraction unblocks menu decomposition.
|
|
514
|
+
|
|
515
|
+
### Test pain points
|
|
516
|
+
|
|
517
|
+
| Symptom | Location | Root cause |
|
|
518
|
+
| ----------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
519
|
+
| 7 `vi.mock()` calls | `agent-runner.test.ts` | Runner imports prompts, memory, skills, env, session-dir directly |
|
|
520
|
+
| 4 `vi.mock()` calls | `session-config.test.ts` | Assembler imports prompts, memory, skills directly |
|
|
521
|
+
| 52 `as any` casts | Across test suite | SDK session/context interfaces too wide to construct in tests |
|
|
522
|
+
| 3× duplicated `mockSession()` | agent-manager, record-observer, ui-observer tests | No shared test fixture |
|
|
523
|
+
| 3× duplicated `makeDeps()` | agent-tool, background-spawner, foreground-runner tests | No shared tool-deps fixture |
|
|
524
|
+
| Weak assertions | lifecycle, renderer, session-config tests | `toHaveBeenCalled()` without args, `toContain()` on large strings |
|
|
525
|
+
|
|
526
|
+
Contrast with the well-designed test suites: `agent-manager.test.ts` (1 mock, DI via `AgentRunner` interface), `notification.test.ts` (0 mocks, pure functions + DI), and `agent-tool.test.ts` (0 mocks, tests via deps bag).
|
|
527
|
+
The pattern is clear: modules that accept collaborators through injection produce resilient tests; modules that import collaborators directly produce fragile mock-heavy tests.
|
|
528
|
+
|
|
529
|
+
### Step F: Shared test fixtures (#131)
|
|
530
|
+
|
|
531
|
+
Consolidate duplicated mock factories into `test/helpers/`.
|
|
532
|
+
|
|
533
|
+
1. `createMockSession()` — subscribable event bus with `emit()` helper; replaces 3 hand-rolled copies.
|
|
534
|
+
2. `createToolDeps()` — builds `AgentToolDeps` with sensible defaults and override support; replaces 3 `makeDeps()` copies.
|
|
535
|
+
|
|
536
|
+
Impact: reduces test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
|
|
537
|
+
|
|
538
|
+
### Step G: Inject IO collaborators into session-config (#132)
|
|
539
|
+
|
|
540
|
+
`assembleSessionConfig` is described as a pure assembler, but it directly imports three IO-touching functions: `preloadSkills` (reads `.pi/skills` files), `buildMemoryBlock` (reads `MEMORY.md`), and `buildReadOnlyMemoryBlock` (reads `MEMORY.md`).
|
|
541
|
+
It also imports `buildAgentPrompt`, which is pure but mocked anyway because tests verify call arguments instead of output properties.
|
|
542
|
+
|
|
543
|
+
Inject these as an `AssemblerIO` parameter:
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
export interface AssemblerIO {
|
|
547
|
+
preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
|
|
548
|
+
buildMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
549
|
+
buildReadOnlyMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
550
|
+
buildAgentPrompt: (config: AgentPromptConfig, cwd: string, env: EnvInfo, parentPrompt: string, extras: PromptExtras) => string;
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
The production call site in `agent-runner.ts` passes the real implementations.
|
|
555
|
+
Tests pass stubs or let real implementations run against controlled inputs.
|
|
556
|
+
|
|
557
|
+
Impact: eliminates all 4 `vi.mock()` calls in `session-config.test.ts`; tests verify `SessionConfig` output properties instead of mock call arguments; the assembler becomes truly pure.
|
|
558
|
+
|
|
559
|
+
### Step H: Inject SDK boundary into agent-runner (#133)
|
|
560
|
+
|
|
561
|
+
`agent-runner.ts` has 7 module mocks because it imports `createAgentSession`, `DefaultResourceLoader`, `SessionManager`, and `SettingsManager` from the Pi SDK, plus `detectEnv`, `deriveSubagentSessionDir`, and `assembleSessionConfig` from sibling modules.
|
|
562
|
+
|
|
563
|
+
After Step G, `assembleSessionConfig` no longer needs mocking (its own IO is injected).
|
|
564
|
+
The remaining SDK dependencies can be injected via a narrow `RunnerIO` interface:
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
export interface RunnerIO {
|
|
568
|
+
createSession: (opts: SessionOptions) => AgentSession;
|
|
569
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoader;
|
|
570
|
+
createSessionManager: (cwd: string) => SessionManager;
|
|
571
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
572
|
+
deriveSessionDir: (parentFile: string) => string;
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
The production call site in `agent-manager.ts` passes a `RunnerIO` built from the real SDK imports.
|
|
577
|
+
Tests pass a stub `RunnerIO` without `vi.mock()`.
|
|
578
|
+
|
|
579
|
+
Impact: eliminates 5–7 `vi.mock()` calls in `agent-runner.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected fakes; refactoring internal structure no longer breaks tests.
|
|
580
|
+
|
|
581
|
+
### Step I: Reduce `as any` casts in tests (#134)
|
|
582
|
+
|
|
583
|
+
With Steps G and H, many `as any` casts disappear because tests construct narrow injectable interfaces instead of wide SDK types.
|
|
584
|
+
Remaining casts are addressed by:
|
|
585
|
+
|
|
586
|
+
1. Defining a `TestSession` type in `test/helpers/` that satisfies `SubscribableSession` + the fields tests actually read.
|
|
587
|
+
2. Replacing `const mockCtx = { cwd: "/tmp" } as any` with properly typed `AssemblerContext` or `ParentSnapshot` objects.
|
|
588
|
+
3. Using `satisfies` assertions where possible instead of `as any`.
|
|
589
|
+
|
|
590
|
+
Target: reduce `as any` count from 52 to under 10.
|
|
591
|
+
|
|
592
|
+
### Step J: Extract display helpers (#135)
|
|
593
|
+
|
|
594
|
+
`agent-widget.ts` (600 lines) exports 11 helper functions and constants that are used by both the widget and the menu.
|
|
595
|
+
Extract these into `ui/display.ts`:
|
|
596
|
+
|
|
597
|
+
- Pure formatters: `formatTokens`, `formatSessionTokens`, `formatTurns`, `formatMs`, `formatDuration`.
|
|
598
|
+
- Display helpers: `getDisplayName`, `getPromptModeLabel`, `buildInvocationTags`, `describeActivity`.
|
|
599
|
+
- Constants: `SPINNER`, `ERROR_STATUSES`, `TOOL_DISPLAY`.
|
|
600
|
+
|
|
601
|
+
Impact: `agent-widget.ts` drops from 600 → ~420 lines; shared display logic has a single import point; menu and tool modules stop importing from the widget.
|
|
602
|
+
|
|
603
|
+
### Step K: Decompose agent-menu.ts (#136)
|
|
604
|
+
|
|
605
|
+
`agent-menu.ts` (650 lines) has 8 distinct responsibilities: menu FSM, agent listing, config editing, agent ejection, two creation wizards, running-agent viewer, and settings form.
|
|
606
|
+
Filesystem operations (read/write/delete agent `.md` files) are scattered throughout.
|
|
607
|
+
|
|
608
|
+
1. Extract `AgentFileOps` interface — `read`, `write`, `delete`, `findAgentFile` — abstracting the fs calls.
|
|
609
|
+
2. Extract `ui/agent-config-editor.ts` — `showAgentDetail` with enable/disable/reset/delete transitions.
|
|
610
|
+
3. Extract `ui/agent-creation-wizard.ts` — both AI-generation and manual form paths.
|
|
611
|
+
4. Leave menu orchestration, settings form, and running-agent viewer in `agent-menu.ts` (~200 lines).
|
|
612
|
+
|
|
613
|
+
Impact: `agent-menu.ts` drops from 650 → ~200 lines; extracted modules receive `AgentFileOps` via injection; wizard logic becomes independently testable.
|
|
614
|
+
|
|
615
|
+
### Step dependencies
|
|
616
|
+
|
|
617
|
+
```text
|
|
618
|
+
F (Shared fixtures) ──────────────────────────────┐
|
|
619
|
+
│
|
|
620
|
+
G (session-config IO injection) ──────────────────┤
|
|
621
|
+
└── H (agent-runner SDK injection) ────────────┤
|
|
622
|
+
└── I (Reduce as-any) ────────────────────┘
|
|
623
|
+
|
|
624
|
+
J (Display extraction) ──────────────────────────┐
|
|
625
|
+
└── K (Menu decomposition) ────────────────────┘
|
|
626
|
+
```
|
|
627
|
+
|
|
628
|
+
Steps F through I (testability) and Steps J through K (display/menu) are independent tracks that can proceed in parallel.
|
|
629
|
+
|
|
630
|
+
---
|
|
631
|
+
|
|
530
632
|
## Relationship with upstream
|
|
531
633
|
|
|
532
634
|
This fork (`@gotgenes/pi-subagents` in the [gotgenes/pi-packages] monorepo) is now a hard fork of [tintinweb/pi-subagents].
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 116
|
|
3
|
+
issue_title: "refactor(pi-subagents): type housekeeping and small structural cleanups"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Type housekeeping and small structural cleanups
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`types.ts` is a type dumping ground — it collects types that don't have a natural home because the module that should own them didn't exist yet.
|
|
11
|
+
With foundation extractions (#108 registry, `parent-snapshot.ts`, `env.ts`, `notification.ts`) now in place, most of these types have a natural home.
|
|
12
|
+
Several other small housekeeping items share the same "polish" character: a closure-bag factory that is a class in disguise, a positional constructor that creates test friction, and a 22-field `AgentConfig` interface consumed by modules that touch 2–4 of its fields.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Move `NotificationDetails` to `notification.ts`, `ParentSnapshot` to `parent-snapshot.ts`, and `EnvInfo` to `env.ts`.
|
|
17
|
+
- Convert `createNotificationSystem` closure to a `NotificationManager` class.
|
|
18
|
+
- Convert `ConversationViewer` constructor from 7 positional parameters to an options bag.
|
|
19
|
+
- Define narrow `AgentConfig` subset interfaces for consumers that use only a few fields.
|
|
20
|
+
- Leave `types.ts` containing only genuinely cross-cutting types: `SubagentType`, `MemoryScope`, `IsolationMode`, `AgentConfig`, `AgentInvocation`, `ShellExec`, and re-exports (`AgentRecord`, `ThinkingLevel`).
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Refactoring `AgentConfig` itself (field additions, removals, renames).
|
|
25
|
+
- Changing the `NotificationDeps` interface shape.
|
|
26
|
+
- Modifying `agent-menu.ts` to use narrow subsets — it legitimately reads most `AgentConfig` fields as a config editor/viewer.
|
|
27
|
+
- Touching `invocation-config.ts` — it already receives `AgentConfig | undefined` and only reads invocation-default fields; narrowing its parameter is a separate concern.
|
|
28
|
+
|
|
29
|
+
## Background
|
|
30
|
+
|
|
31
|
+
### Prerequisite status
|
|
32
|
+
|
|
33
|
+
| Issue | Title | Status |
|
|
34
|
+
| ----- | --------------------------------- | ------- |
|
|
35
|
+
| #108 | Extract `AgentTypeRegistry` class | ✅ Done |
|
|
36
|
+
|
|
37
|
+
`DEFAULT_AGENT_NAMES` was already moved to `AgentTypeRegistry.DEFAULT_AGENT_NAMES` as part of #108.
|
|
38
|
+
The remaining work from the issue's checklist is type relocations, two structural conversions, and narrow subset interfaces.
|
|
39
|
+
|
|
40
|
+
### Current `types.ts` exports
|
|
41
|
+
|
|
42
|
+
| Export | Kind | Should stay? |
|
|
43
|
+
| -------------------------------------------------- | --------- | ------------------------------- |
|
|
44
|
+
| `AgentRecord` (re-export) | class | ✅ Cross-cutting |
|
|
45
|
+
| `AgentRecordInit`, `AgentRecordStatus` (re-export) | type | ✅ Cross-cutting |
|
|
46
|
+
| `ThinkingLevel` (re-export) | type | ✅ Cross-cutting |
|
|
47
|
+
| `SubagentType` | type | ✅ Cross-cutting |
|
|
48
|
+
| `MemoryScope` | type | ✅ Cross-cutting |
|
|
49
|
+
| `IsolationMode` | type | ✅ Cross-cutting |
|
|
50
|
+
| `AgentConfig` | interface | ✅ Cross-cutting |
|
|
51
|
+
| `AgentInvocation` | interface | ✅ Cross-cutting |
|
|
52
|
+
| `ShellExec` | type | ✅ Cross-cutting |
|
|
53
|
+
| `NotificationDetails` | interface | ❌ Move to `notification.ts` |
|
|
54
|
+
| `ParentSnapshot` | interface | ❌ Move to `parent-snapshot.ts` |
|
|
55
|
+
| `EnvInfo` | interface | ❌ Move to `env.ts` |
|
|
56
|
+
|
|
57
|
+
### `createNotificationSystem` closure analysis
|
|
58
|
+
|
|
59
|
+
The factory in `notification.ts` shares `pendingNudges` (a `Map`) and timer state across 4 inner functions (`cancelNudge`, `scheduleNudge`, `sendCompletion`, `cleanupCompleted`, `dispose`).
|
|
60
|
+
This is a class in disguise — mutable state + methods that read/write it.
|
|
61
|
+
Converting to a `NotificationManager` class makes the state explicit and lets tests use instance methods directly.
|
|
62
|
+
|
|
63
|
+
### `ConversationViewer` constructor
|
|
64
|
+
|
|
65
|
+
The constructor takes 7 positional parameters:
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
constructor(tui, session, record, activity, theme, done, registry)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Every test must reconstruct all 7 in order.
|
|
72
|
+
An options bag reduces friction and is resilient to new parameters.
|
|
73
|
+
|
|
74
|
+
### `AgentConfig` field usage by consumer
|
|
75
|
+
|
|
76
|
+
| Consumer | Fields used |
|
|
77
|
+
| ---------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
|
|
78
|
+
| `agent-widget.ts` (`getDisplayName`, `getPromptModeLabel`) | `name`, `displayName`, `promptMode` |
|
|
79
|
+
| `tools/helpers.ts` (`formatAgentList`) | `name`, `description`, `model` |
|
|
80
|
+
| `prompts.ts` (`buildAgentPrompt`) | `name`, `promptMode`, `systemPrompt` |
|
|
81
|
+
| `session-config.ts` (`assembleSessionConfig`) | `name`, `model`, `thinking`, `maxTurns`, `extensions`, `skills`, `memory`, `disallowedTools` |
|
|
82
|
+
| `agent-menu.ts` | Nearly all fields (config viewer/editor) |
|
|
83
|
+
|
|
84
|
+
Natural clusters for narrow interfaces:
|
|
85
|
+
|
|
86
|
+
- **`AgentIdentity`**: `name`, `displayName`, `description`, `promptMode` — UI display and agent listing.
|
|
87
|
+
- **`AgentPromptConfig`**: `name`, `promptMode`, `systemPrompt` — prompt assembly.
|
|
88
|
+
|
|
89
|
+
`session-config.ts` uses 8 fields; narrowing it yields limited value vs. complexity.
|
|
90
|
+
`invocation-config.ts` receives `AgentConfig | undefined` and reads invocation-default fields; this is a separate concern.
|
|
91
|
+
|
|
92
|
+
## Design Overview
|
|
93
|
+
|
|
94
|
+
### Type relocations
|
|
95
|
+
|
|
96
|
+
Move each type to its natural home module and re-export from `types.ts` during an interim step.
|
|
97
|
+
After updating all importers to point at the new home, remove the re-export.
|
|
98
|
+
This two-phase approach (move + re-export, then update importers + remove re-export) avoids a big-bang commit.
|
|
99
|
+
|
|
100
|
+
However, since importers are few (2–4 each) and the types are only used internally, a single step per type is simpler: move the type, update all importers in the same commit.
|
|
101
|
+
|
|
102
|
+
### `NotificationManager` class
|
|
103
|
+
|
|
104
|
+
Replace the `createNotificationSystem` factory with a class that owns its state:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
export class NotificationManager {
|
|
108
|
+
private pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
|
|
109
|
+
|
|
110
|
+
constructor(private deps: NotificationDeps) {}
|
|
111
|
+
|
|
112
|
+
cancelNudge(key: string): void { /* ... */ }
|
|
113
|
+
sendCompletion(record: AgentRecord): void { /* ... */ }
|
|
114
|
+
cleanupCompleted(id: string): void { /* ... */ }
|
|
115
|
+
dispose(): void { /* ... */ }
|
|
116
|
+
|
|
117
|
+
// private helpers
|
|
118
|
+
private scheduleNudge(key: string, send: () => void, delay?: number): void { /* ... */ }
|
|
119
|
+
private emitIndividualNudge(record: AgentRecord): void { /* ... */ }
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The `NotificationSystem` interface stays as-is — `NotificationManager implements NotificationSystem`.
|
|
124
|
+
The `NotificationDeps` interface is unchanged.
|
|
125
|
+
Callers in `index.ts` switch from `createNotificationSystem(deps)` to `new NotificationManager(deps)`.
|
|
126
|
+
|
|
127
|
+
Consumer call site (index.ts):
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const notifications = new NotificationManager({
|
|
131
|
+
sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
|
|
132
|
+
agentActivity: runtime.agentActivity,
|
|
133
|
+
markFinished: (id) => runtime.markFinished(id),
|
|
134
|
+
updateWidget: () => runtime.updateWidget(),
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### `ConversationViewer` options bag
|
|
139
|
+
|
|
140
|
+
Replace positional parameters with a named options interface:
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
export interface ConversationViewerOptions {
|
|
144
|
+
tui: TUI;
|
|
145
|
+
session: AgentSession;
|
|
146
|
+
record: AgentRecord;
|
|
147
|
+
activity: AgentActivityTracker | undefined;
|
|
148
|
+
theme: Theme;
|
|
149
|
+
done: (result: undefined) => void;
|
|
150
|
+
registry: AgentConfigLookup;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export class ConversationViewer {
|
|
154
|
+
constructor(private options: ConversationViewerOptions) {
|
|
155
|
+
// destructure into private fields or use options.field access
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The existing private fields (`tui`, `session`, `record`, etc.) become reads from `this.options.*` or are destructured in the constructor to named private fields.
|
|
161
|
+
The constructor body (session subscription) is unchanged.
|
|
162
|
+
|
|
163
|
+
### Narrow `AgentConfig` subset interfaces
|
|
164
|
+
|
|
165
|
+
Define two narrow interfaces in `types.ts` and make `AgentConfig` extend them:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
/** UI display and agent listing — name, display name, description, prompt mode. */
|
|
169
|
+
export interface AgentIdentity {
|
|
170
|
+
name: string;
|
|
171
|
+
displayName?: string;
|
|
172
|
+
description: string;
|
|
173
|
+
promptMode: "replace" | "append";
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Prompt assembly — name, prompt mode, system prompt. */
|
|
177
|
+
export interface AgentPromptConfig {
|
|
178
|
+
name: string;
|
|
179
|
+
promptMode: "replace" | "append";
|
|
180
|
+
systemPrompt: string;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
|
|
184
|
+
// remaining fields unchanged
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Then update consumers to accept the narrow interface:
|
|
189
|
+
|
|
190
|
+
- `agent-widget.ts`: `getDisplayName(type, registry)` and `getPromptModeLabel(type, registry)` — these go through `AgentConfigLookup.resolveAgentConfig()`, which returns `AgentConfig`.
|
|
191
|
+
The narrowing applies to the *return type usage*, not the function signature.
|
|
192
|
+
No change needed here — the function already destructures only `displayName`, `name`, `promptMode`.
|
|
193
|
+
- `tools/helpers.ts`: `formatAgentList` reads `description`, `model` — these are not in `AgentIdentity`.
|
|
194
|
+
`model` is session-config territory.
|
|
195
|
+
Leave as-is.
|
|
196
|
+
- `prompts.ts`: `buildAgentPrompt(config: AgentPromptConfig, ...)` — narrow the parameter type.
|
|
197
|
+
- `AgentConfigLookup.resolveAgentConfig()` return type stays `AgentConfig` — the registry always has the full config.
|
|
198
|
+
|
|
199
|
+
Since `AgentConfig extends AgentIdentity, AgentPromptConfig`, any code passing an `AgentConfig` to a function expecting the narrow type works without casts.
|
|
200
|
+
|
|
201
|
+
## Module-Level Changes
|
|
202
|
+
|
|
203
|
+
### Modified files
|
|
204
|
+
|
|
205
|
+
1. **`src/types.ts`**
|
|
206
|
+
- Remove `NotificationDetails`, `ParentSnapshot`, `EnvInfo` interface definitions.
|
|
207
|
+
- Add `AgentIdentity` and `AgentPromptConfig` interfaces.
|
|
208
|
+
- Make `AgentConfig` extend `AgentIdentity` and `AgentPromptConfig`.
|
|
209
|
+
- Remove fields from `AgentConfig` body that are now inherited (`name`, `displayName`, `description`, `promptMode`, `systemPrompt`).
|
|
210
|
+
|
|
211
|
+
2. **`src/notification.ts`**
|
|
212
|
+
- Add `NotificationDetails` interface (moved from `types.ts`).
|
|
213
|
+
- Replace `createNotificationSystem` factory with `NotificationManager` class implementing `NotificationSystem`.
|
|
214
|
+
- Export `NotificationManager` (named export) and keep `NotificationSystem` interface export.
|
|
215
|
+
- Remove `createNotificationSystem` export.
|
|
216
|
+
|
|
217
|
+
3. **`src/parent-snapshot.ts`**
|
|
218
|
+
- Add `ParentSnapshot` interface (moved from `types.ts`).
|
|
219
|
+
|
|
220
|
+
4. **`src/env.ts`**
|
|
221
|
+
- Add `EnvInfo` interface (moved from `types.ts`).
|
|
222
|
+
|
|
223
|
+
5. **`src/prompts.ts`**
|
|
224
|
+
- Change `config` parameter type from `AgentConfig` to `AgentPromptConfig`.
|
|
225
|
+
- Update import to use `AgentPromptConfig` from `./types.js`.
|
|
226
|
+
|
|
227
|
+
6. **`src/index.ts`**
|
|
228
|
+
- Update `NotificationDetails` import from `./types.js` to `./notification.js`.
|
|
229
|
+
- Replace `createNotificationSystem(deps)` with `new NotificationManager(deps)`.
|
|
230
|
+
- Update import: `NotificationManager` from `./notification.js`.
|
|
231
|
+
|
|
232
|
+
7. **`src/renderer.ts`**
|
|
233
|
+
- Update `NotificationDetails` import from `./types.js` to `./notification.js`.
|
|
234
|
+
|
|
235
|
+
8. **`src/agent-runner.ts`**
|
|
236
|
+
- Update `ParentSnapshot` import from `./types.js` to `./parent-snapshot.js`.
|
|
237
|
+
|
|
238
|
+
9. **`src/agent-manager.ts`**
|
|
239
|
+
- Update `ParentSnapshot` import from `./types.js` to `./parent-snapshot.js`.
|
|
240
|
+
|
|
241
|
+
10. **`src/session-config.ts`**
|
|
242
|
+
- Update `EnvInfo` import from `./types.js` to `./env.js`.
|
|
243
|
+
|
|
244
|
+
11. **`src/ui/conversation-viewer.ts`**
|
|
245
|
+
- Define `ConversationViewerOptions` interface.
|
|
246
|
+
- Replace 7 positional constructor parameters with single `options: ConversationViewerOptions` parameter.
|
|
247
|
+
- Assign private fields from `options` in constructor body.
|
|
248
|
+
|
|
249
|
+
### Unchanged files
|
|
250
|
+
|
|
251
|
+
- `src/agent-types.ts` — `DEFAULT_AGENT_NAMES` already moved in #108.
|
|
252
|
+
- `src/invocation-config.ts` — narrowing is a separate concern.
|
|
253
|
+
- `src/ui/agent-menu.ts` — legitimately uses most `AgentConfig` fields.
|
|
254
|
+
|
|
255
|
+
### Test files
|
|
256
|
+
|
|
257
|
+
12. **`test/notification.test.ts`**
|
|
258
|
+
- Replace `createNotificationSystem(deps)` calls with `new NotificationManager(deps)`.
|
|
259
|
+
- Update import.
|
|
260
|
+
- All existing assertions stay — the public API is identical.
|
|
261
|
+
|
|
262
|
+
13. **`test/conversation-viewer.test.ts`**
|
|
263
|
+
- Replace positional `new ConversationViewer(tui, session, record, activity, theme, done, registry)` calls with `new ConversationViewer({ tui, session, record, activity, theme, done, registry })`.
|
|
264
|
+
- All existing assertions stay.
|
|
265
|
+
|
|
266
|
+
14. **`test/prompts.test.ts`**
|
|
267
|
+
- Verify existing test fixtures satisfy `AgentPromptConfig` (they should — tests already pass `name`, `promptMode`, `systemPrompt`).
|
|
268
|
+
- May need to narrow mock type annotations.
|
|
269
|
+
|
|
270
|
+
15. **Other test files importing relocated types** (`test/parent-snapshot.test.ts`, `test/renderer.test.ts`)
|
|
271
|
+
- Update import paths if they import directly from `types.ts` (most import from the source module already).
|
|
272
|
+
|
|
273
|
+
## Test Impact Analysis
|
|
274
|
+
|
|
275
|
+
1. **New unit tests enabled:**
|
|
276
|
+
- `NotificationManager` as a class can be tested with standard `new` + method calls — no factory indirection.
|
|
277
|
+
However, the existing factory tests are already clean and simply switch to `new NotificationManager(deps)`.
|
|
278
|
+
No fundamentally new test capabilities.
|
|
279
|
+
|
|
280
|
+
2. **Existing tests that simplify:**
|
|
281
|
+
- `conversation-viewer.test.ts` — the options bag makes test construction more readable and resilient to parameter additions.
|
|
282
|
+
Each test only needs to specify the fields it cares about (with a helper providing defaults).
|
|
283
|
+
- `prompts.test.ts` — narrower `AgentPromptConfig` parameter means test fixtures can omit 19 irrelevant `AgentConfig` fields.
|
|
284
|
+
|
|
285
|
+
3. **Existing tests that must stay as-is:**
|
|
286
|
+
- `notification.test.ts` — all factory tests stay, just switch constructor syntax.
|
|
287
|
+
- `conversation-viewer.test.ts` — all render and input tests stay, just switch constructor syntax.
|
|
288
|
+
- `parent-snapshot.test.ts`, `env.test.ts`, `renderer.test.ts` — behavior unchanged.
|
|
289
|
+
|
|
290
|
+
## TDD Order
|
|
291
|
+
|
|
292
|
+
1. **Move `NotificationDetails` from `types.ts` to `notification.ts`.**
|
|
293
|
+
Cut the interface from `types.ts`, paste into `notification.ts`.
|
|
294
|
+
Update importers: `index.ts`, `renderer.ts` (change import from `./types.js` to `./notification.js`).
|
|
295
|
+
`notification.ts` already imports from `./types.js` for `AgentRecord` — no circular dependency.
|
|
296
|
+
Run `pnpm run check`.
|
|
297
|
+
Commit: `refactor: move NotificationDetails to notification.ts`
|
|
298
|
+
|
|
299
|
+
2. **Move `ParentSnapshot` from `types.ts` to `parent-snapshot.ts`.**
|
|
300
|
+
Cut the interface from `types.ts`, paste into `parent-snapshot.ts`.
|
|
301
|
+
Update importers: `agent-manager.ts`, `agent-runner.ts` (change import from `./types.js` to `./parent-snapshot.js`).
|
|
302
|
+
Run `pnpm run check`.
|
|
303
|
+
Commit: `refactor: move ParentSnapshot to parent-snapshot.ts`
|
|
304
|
+
|
|
305
|
+
3. **Move `EnvInfo` from `types.ts` to `env.ts`.**
|
|
306
|
+
Cut the interface from `types.ts`, paste into `env.ts`.
|
|
307
|
+
Update importers: `session-config.ts`, `prompts.ts` (change import from `./types.js` to `./env.js`).
|
|
308
|
+
`env.ts` already imports `ShellExec` from `./types.js` — no circular dependency.
|
|
309
|
+
Run `pnpm run check`.
|
|
310
|
+
Commit: `refactor: move EnvInfo to env.ts`
|
|
311
|
+
|
|
312
|
+
4. **Convert `createNotificationSystem` to `NotificationManager` class.**
|
|
313
|
+
Replace the factory with a class implementing `NotificationSystem`.
|
|
314
|
+
Move `pendingNudges` and timer logic to private instance state.
|
|
315
|
+
Convert inner functions to methods.
|
|
316
|
+
Keep `NotificationSystem` interface, `NotificationDeps` interface, and `NUDGE_HOLD_MS` constant unchanged.
|
|
317
|
+
Remove `createNotificationSystem` export, add `NotificationManager` export.
|
|
318
|
+
Update `index.ts`: `new NotificationManager(deps)` instead of `createNotificationSystem(deps)`.
|
|
319
|
+
Update `test/notification.test.ts`: `new NotificationManager(deps)` instead of `createNotificationSystem(deps)`, update import.
|
|
320
|
+
Run `pnpm vitest run test/notification.test.ts`.
|
|
321
|
+
Commit: `refactor: convert createNotificationSystem to NotificationManager class`
|
|
322
|
+
|
|
323
|
+
5. **Convert `ConversationViewer` constructor to options bag.**
|
|
324
|
+
Define `ConversationViewerOptions` interface.
|
|
325
|
+
Replace 7 positional parameters with `options: ConversationViewerOptions`.
|
|
326
|
+
Assign private fields from options in constructor body.
|
|
327
|
+
Update all call sites in `test/conversation-viewer.test.ts` and `src/index.ts` (or wherever `new ConversationViewer(...)` is called).
|
|
328
|
+
Run `pnpm vitest run test/conversation-viewer.test.ts`.
|
|
329
|
+
Commit: `refactor: convert ConversationViewer to options bag constructor`
|
|
330
|
+
|
|
331
|
+
6. **Define narrow `AgentIdentity` and `AgentPromptConfig` interfaces.**
|
|
332
|
+
Add interfaces to `types.ts`.
|
|
333
|
+
Make `AgentConfig` extend both; remove inherited fields from `AgentConfig` body.
|
|
334
|
+
Narrow `prompts.ts` `buildAgentPrompt` parameter from `AgentConfig` to `AgentPromptConfig`.
|
|
335
|
+
Update `test/prompts.test.ts` type annotations if needed.
|
|
336
|
+
Run `pnpm run check` and `pnpm vitest run`.
|
|
337
|
+
Commit: `refactor: define AgentIdentity and AgentPromptConfig subset interfaces`
|
|
338
|
+
|
|
339
|
+
## Risks and Mitigations
|
|
340
|
+
|
|
341
|
+
| Risk | Mitigation |
|
|
342
|
+
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
343
|
+
| Moving types breaks import paths in files not caught by grep | Run `pnpm run check` (full `tsc --noEmit`) after each type relocation step. |
|
|
344
|
+
| `NotificationManager` class changes test mock patterns (e.g., spreading instances) | Tests already use the factory return as an opaque object — switching to `new` is mechanical. No spread patterns in notification tests. |
|
|
345
|
+
| `ConversationViewer` options bag breaks all test call sites at once | All 314 lines of tests use the same 7-arg pattern. A search-and-replace handles it. The options bag is a single-step change. |
|
|
346
|
+
| `AgentConfig extends AgentIdentity, AgentPromptConfig` changes structural compatibility | `extends` is purely additive — existing code passing `AgentConfig` anywhere still works. Narrowed consumers gain type safety, nothing breaks. |
|
|
347
|
+
| `prompts.ts` parameter narrowing breaks callers passing `AgentConfig` | `AgentConfig extends AgentPromptConfig`, so all existing call sites are compatible without changes. |
|
|
348
|
+
|
|
349
|
+
## Open Questions
|
|
350
|
+
|
|
351
|
+
- Whether to define an `AgentSessionConfig` subset for `session-config.ts` (8 fields) — deferred because the narrowing ratio (8 of 22) yields less clarity benefit than `AgentIdentity` (4 of 22) or `AgentPromptConfig` (3 of 22).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 115
|
|
3
|
+
issue_title: "refactor(pi-subagents): decompose agent-tool.ts into foreground/background modules"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #115 — decompose agent-tool.ts into foreground/background modules
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-21T22:30:00-04:00)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Decomposed the 579-line `tools/agent-tool.ts` into focused modules: `foreground-runner.ts` (spinner, streaming, cleanup) and `background-spawner.ts` (activity setup, notification wiring), with `agent-tool.ts` remaining as the orchestrator (411 lines).
|
|
13
|
+
Before extracting, fixed two upstream API gaps: widened `onSessionCreated` callback to `(session, record)` to eliminate a `listAgents()` reverse-search, and added `toolCallId` to `AgentSpawnConfig` so the manager wires `NotificationState` at spawn time.
|
|
14
|
+
Test count increased from 641 to 690.
|
|
15
|
+
Released as `pi-subagents-v6.9.1`.
|
|
16
|
+
|
|
17
|
+
### Observations
|
|
18
|
+
|
|
19
|
+
#### What went well
|
|
20
|
+
|
|
21
|
+
- The revised plan (after user feedback) was structurally clean — fixing API gaps first made the extraction trivial; each extracted module received only what it needed without workarounds.
|
|
22
|
+
- TDD execution was smooth: 6 steps, all green after each commit, only minor deviations (type annotation issue, `index.ts` call-site fix).
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `wrong-abstraction` (user-caught) — The initial plan was a mechanical code-move that defined 4 new interfaces (`ForegroundRunDeps`, `BackgroundSpawnDeps`, `ForegroundRunParams`, `BackgroundSpawnParams`) to paper over two upstream API gaps: a `listAgents()` reverse-search in the foreground `onSessionCreated` callback, and a post-spawn `record.notification` mutation in the background path.
|
|
27
|
+
The user asked *"What dependencies are still missing for these split tools, that they want, rather than some low level state or collaborators that they have?"*
|
|
28
|
+
— redirecting me to fix the API surface before extracting.
|
|
29
|
+
Impact: entire plan rewritten (~15 minutes), but the revised plan was significantly cleaner and the implementation went smoothly.
|
|
30
|
+
|
|
31
|
+
- `missing-context` (self-identified) — During step 5 (foreground extraction), two tests in `foreground-runner.test.ts` failed because mock sessions were `{}` objects lacking a `subscribe` method required by `subscribeUIObserver`.
|
|
32
|
+
The function's dependency on `SubscribableSession` (requiring `.subscribe()`) wasn't accounted for in the test mock.
|
|
33
|
+
Impact: one test fix cycle (~2 minutes), no rework.
|
|
34
|
+
|
|
35
|
+
- `missing-context` (self-identified) — Annotating `runForeground` with an explicit `Promise<AgentToolResult<any>>` return type widened the content array type from `{ type: "text", text: string }[]` to `(TextContent | ImageContent)[]`, breaking existing `content[0].text` patterns in tests.
|
|
36
|
+
Fixed by removing the explicit annotation and letting TypeScript infer the narrow type.
|
|
37
|
+
Impact: three edit cycles to diagnose and fix (~5 minutes).
|
|
38
|
+
|
|
39
|
+
- `missing-context` (self-identified) — Step 1 removed `listAgents` from `AgentToolManager` but didn't update the construction site in `index.ts`.
|
|
40
|
+
`pnpm run check` caught it in step 4.
|
|
41
|
+
Impact: one-line fix in the same commit, no rework.
|
|
42
|
+
|
|
43
|
+
#### What caused friction (user side)
|
|
44
|
+
|
|
45
|
+
- The initial plan's size and mechanical nature required a user redirect.
|
|
46
|
+
The `/plan-issue` prompt's Design Overview section asks to sketch consumer call sites for *new collaborators*, but the same Tell-Don't-Ask check should apply to *extracted* modules' interactions with their upstream dependencies.
|
|
47
|
+
A prompt tweak could help the agent catch this pattern earlier.
|
|
48
|
+
|
|
49
|
+
### Changes made
|
|
50
|
+
|
|
51
|
+
1. `.pi/prompts/plan-issue.md` — added extraction-specific Tell-Don't-Ask verification step to the Design Overview section: sketch the extracted module's upstream interactions before planning the extraction, fix API gaps first.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 116
|
|
3
|
+
issue_title: "refactor(pi-subagents): type housekeeping and small structural cleanups"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #116 — type housekeeping and small structural cleanups
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-21T23:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and executed 6 refactoring steps for issue #116: relocated 3 misplaced types from `types.ts` to their natural home modules, converted `createNotificationSystem` closure to a `NotificationManager` class, switched `ConversationViewer` to an options-bag constructor, and defined `AgentIdentity`/`AgentPromptConfig` narrow subset interfaces.
|
|
13
|
+
All 690 tests stayed green throughout; no behavioral changes.
|
|
14
|
+
Released as `pi-subagents-v6.9.2`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- The Python script approach for bulk-converting 16 `ConversationViewer` positional constructor calls to options-bag syntax was efficient and correct on the first attempt — all 17 tests passed immediately after the conversion.
|
|
21
|
+
- Proactive `grep -rn 'new ConversationViewer'` before step 5 caught the `agent-menu.ts` call site that the plan's Module-Level Changes section had omitted, avoiding a broken commit.
|
|
22
|
+
- The architecture doc update was clean — 5 targeted edits to mark E2 done, update smells table, and fix metrics.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — In all three type-relocation steps (1–3), I updated source-file imports but did not pre-flight grep for test-file imports of the relocated symbol.
|
|
27
|
+
Each time, `pnpm run check` caught the stale test import, requiring an extra edit-check round trip.
|
|
28
|
+
This happened with `test/renderer.test.ts` (step 1), `test/agent-runner.test.ts` + `test/agent-runner-extension-tools.test.ts` (step 2), and `test/prompts.test.ts` (step 3).
|
|
29
|
+
Impact: 3 unnecessary edit-check cycles; no broken commits since the type checker caught every case before `git commit`.
|
|
30
|
+
|
|
31
|
+
- `missing-context` — In step 4, the first `Edit` call on `src/index.ts` failed because the autoformatter had merged the `NotificationDetails` type import (added in step 1) into the existing notification import line, changing the text I expected.
|
|
32
|
+
Had to re-read the file to find the current import text.
|
|
33
|
+
Impact: one wasted edit call plus a file read; added ~10 seconds of friction.
|
|
34
|
+
|
|
35
|
+
#### What caused friction (user side)
|
|
36
|
+
|
|
37
|
+
- No friction observed — the user's prompts were clear and the `/tdd-plan` and `/ship-issue` templates provided all needed structure.
|
|
38
|
+
|
|
39
|
+
### Takeaway
|
|
40
|
+
|
|
41
|
+
When relocating a type or symbol, always run `grep -rn 'SymbolName' src/ test/` before editing to identify *all* importers upfront — both source and test files.
|
|
42
|
+
This avoids the repeated pattern of "edit source → type-check fails on test → fix test → type-check again."
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -14,10 +14,11 @@ import type { AgentRunner } from "./agent-runner.js";
|
|
|
14
14
|
import { AgentTypeRegistry } from "./agent-types.js";
|
|
15
15
|
import { debugLog } from "./debug.js";
|
|
16
16
|
import { NotificationState } from "./notification-state.js";
|
|
17
|
+
import type { ParentSnapshot } from "./parent-snapshot.js";
|
|
17
18
|
import { buildParentSnapshot } from "./parent-snapshot.js";
|
|
18
19
|
import { subscribeRecordObserver } from "./record-observer.js";
|
|
19
20
|
import type { RunConfig } from "./runtime.js";
|
|
20
|
-
import type { AgentInvocation, IsolationMode,
|
|
21
|
+
import type { AgentInvocation, IsolationMode, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
|
|
21
22
|
import type { WorktreeManager } from "./worktree.js";
|
|
22
23
|
import { WorktreeState } from "./worktree-state.js";
|
|
23
24
|
|
package/src/agent-runner.ts
CHANGED
|
@@ -15,9 +15,10 @@ import {
|
|
|
15
15
|
import type { AgentConfigLookup } from "./agent-types.js";
|
|
16
16
|
import { extractText } from "./context.js";
|
|
17
17
|
import { detectEnv } from "./env.js";
|
|
18
|
+
import type { ParentSnapshot } from "./parent-snapshot.js";
|
|
18
19
|
import { assembleSessionConfig } from "./session-config.js";
|
|
19
20
|
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
20
|
-
import type {
|
|
21
|
+
import type { ShellExec, SubagentType, ThinkingLevel } from "./types.js";
|
|
21
22
|
|
|
22
23
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
23
24
|
const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
|
package/src/env.ts
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { debugLog } from "./debug.js";
|
|
6
|
-
import type {
|
|
6
|
+
import type { ShellExec } from "./types.js";
|
|
7
|
+
|
|
8
|
+
export interface EnvInfo {
|
|
9
|
+
isGitRepo: boolean;
|
|
10
|
+
branch: string;
|
|
11
|
+
platform: string;
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
export async function detectEnv(exec: ShellExec, cwd: string): Promise<EnvInfo> {
|
|
9
15
|
let isGitRepo = false;
|
package/src/index.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { AgentTypeRegistry } from "./agent-types.js";
|
|
|
18
18
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
19
19
|
import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
|
|
20
20
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
21
|
-
import { buildEventData,
|
|
21
|
+
import { buildEventData, type NotificationDetails, NotificationManager } from "./notification.js";
|
|
22
22
|
import { createNotificationRenderer } from "./renderer.js";
|
|
23
23
|
import { createSubagentRuntime } from "./runtime.js";
|
|
24
24
|
import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
|
|
@@ -28,7 +28,6 @@ import { createAgentTool } from "./tools/agent-tool.js";
|
|
|
28
28
|
import { createGetResultTool } from "./tools/get-result-tool.js";
|
|
29
29
|
import { getModelLabelFromConfig } from "./tools/helpers.js";
|
|
30
30
|
import { createSteerTool } from "./tools/steer-tool.js";
|
|
31
|
-
import { type NotificationDetails } from "./types.js";
|
|
32
31
|
import { createAgentsMenuHandler } from "./ui/agent-menu.js";
|
|
33
32
|
import {
|
|
34
33
|
AgentWidget,
|
|
@@ -48,7 +47,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
48
47
|
// ---- Notification system ----
|
|
49
48
|
// runtime.widget is assigned after AgentManager construction; arrow closures
|
|
50
49
|
// capture `runtime` by reference so they always read the current value.
|
|
51
|
-
const notifications =
|
|
50
|
+
const notifications = new NotificationManager({
|
|
52
51
|
sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
|
|
53
52
|
agentActivity: runtime.agentActivity,
|
|
54
53
|
markFinished: (id) => runtime.markFinished(id),
|
package/src/notification.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import { debugLog } from "./debug.js";
|
|
2
|
-
import type { AgentRecord
|
|
2
|
+
import type { AgentRecord } from "./types.js";
|
|
3
3
|
import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
|
|
4
4
|
import { getLifetimeTotal, getSessionContextPercent } from "./usage.js";
|
|
5
5
|
|
|
6
|
+
/** Details attached to custom notification messages for visual rendering. */
|
|
7
|
+
export interface NotificationDetails {
|
|
8
|
+
id: string;
|
|
9
|
+
description: string;
|
|
10
|
+
status: string;
|
|
11
|
+
toolUses: number;
|
|
12
|
+
turnCount: number;
|
|
13
|
+
maxTurns?: number;
|
|
14
|
+
totalTokens: number;
|
|
15
|
+
durationMs: number;
|
|
16
|
+
outputFile?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
resultPreview: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
6
21
|
// ---- Pure helpers (exported for unit testing) ----
|
|
7
22
|
|
|
8
23
|
/** Escape XML special characters to prevent injection in structured notifications. */
|
|
@@ -129,23 +144,43 @@ export interface NotificationSystem {
|
|
|
129
144
|
|
|
130
145
|
const NUDGE_HOLD_MS = 200;
|
|
131
146
|
|
|
132
|
-
export
|
|
133
|
-
|
|
147
|
+
export class NotificationManager implements NotificationSystem {
|
|
148
|
+
private pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
|
|
134
149
|
|
|
135
|
-
|
|
136
|
-
|
|
150
|
+
constructor(private deps: NotificationDeps) {}
|
|
151
|
+
|
|
152
|
+
cancelNudge(key: string): void {
|
|
153
|
+
const timer = this.pendingNudges.get(key);
|
|
137
154
|
if (timer != null) {
|
|
138
155
|
clearTimeout(timer);
|
|
139
|
-
pendingNudges.delete(key);
|
|
156
|
+
this.pendingNudges.delete(key);
|
|
140
157
|
}
|
|
141
158
|
}
|
|
142
159
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
160
|
+
sendCompletion(record: AgentRecord): void {
|
|
161
|
+
this.deps.agentActivity.delete(record.id);
|
|
162
|
+
this.deps.markFinished(record.id);
|
|
163
|
+
this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
|
|
164
|
+
this.deps.updateWidget();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
cleanupCompleted(id: string): void {
|
|
168
|
+
this.deps.agentActivity.delete(id);
|
|
169
|
+
this.deps.markFinished(id);
|
|
170
|
+
this.deps.updateWidget();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
dispose(): void {
|
|
174
|
+
for (const timer of this.pendingNudges.values()) clearTimeout(timer);
|
|
175
|
+
this.pendingNudges.clear();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private scheduleNudge(key: string, send: () => void, delay = NUDGE_HOLD_MS): void {
|
|
179
|
+
this.cancelNudge(key);
|
|
180
|
+
this.pendingNudges.set(
|
|
146
181
|
key,
|
|
147
182
|
setTimeout(() => {
|
|
148
|
-
pendingNudges.delete(key);
|
|
183
|
+
this.pendingNudges.delete(key);
|
|
149
184
|
try {
|
|
150
185
|
send();
|
|
151
186
|
} catch (err) {
|
|
@@ -155,41 +190,21 @@ export function createNotificationSystem(deps: NotificationDeps): NotificationSy
|
|
|
155
190
|
);
|
|
156
191
|
}
|
|
157
192
|
|
|
158
|
-
|
|
193
|
+
private emitIndividualNudge(record: AgentRecord): void {
|
|
159
194
|
if (record.notification?.resultConsumed) return;
|
|
160
195
|
|
|
161
196
|
const notification = formatTaskNotification(record, 500);
|
|
162
197
|
const outputFile = record.execution?.outputFile;
|
|
163
198
|
const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
|
|
164
199
|
|
|
165
|
-
deps.sendMessage(
|
|
200
|
+
this.deps.sendMessage(
|
|
166
201
|
{
|
|
167
202
|
customType: "subagent-notification",
|
|
168
203
|
content: notification + footer,
|
|
169
204
|
display: true,
|
|
170
|
-
details: buildNotificationDetails(record, 500, deps.agentActivity.get(record.id)),
|
|
205
|
+
details: buildNotificationDetails(record, 500, this.deps.agentActivity.get(record.id)),
|
|
171
206
|
},
|
|
172
207
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
173
208
|
);
|
|
174
209
|
}
|
|
175
|
-
|
|
176
|
-
function sendCompletion(record: AgentRecord) {
|
|
177
|
-
deps.agentActivity.delete(record.id);
|
|
178
|
-
deps.markFinished(record.id);
|
|
179
|
-
scheduleNudge(record.id, () => emitIndividualNudge(record));
|
|
180
|
-
deps.updateWidget();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
function cleanupCompleted(id: string) {
|
|
184
|
-
deps.agentActivity.delete(id);
|
|
185
|
-
deps.markFinished(id);
|
|
186
|
-
deps.updateWidget();
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function dispose() {
|
|
190
|
-
for (const timer of pendingNudges.values()) clearTimeout(timer);
|
|
191
|
-
pendingNudges.clear();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return { cancelNudge, sendCompletion, cleanupCompleted, dispose };
|
|
195
210
|
}
|
package/src/parent-snapshot.ts
CHANGED
|
@@ -4,7 +4,26 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { buildParentContext } from "./context.js";
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Plain data snapshot of the parent session state captured at spawn time.
|
|
10
|
+
* Replaces live `ExtensionContext` references so queued agents don't read stale state.
|
|
11
|
+
*/
|
|
12
|
+
export interface ParentSnapshot {
|
|
13
|
+
/** Parent working directory. */
|
|
14
|
+
cwd: string;
|
|
15
|
+
/** Parent's effective system prompt (for append-mode agents). */
|
|
16
|
+
systemPrompt: string;
|
|
17
|
+
/** Parent's current model instance (fallback when agent config has no model). */
|
|
18
|
+
model: unknown;
|
|
19
|
+
/** Model registry for resolving config.model strings and creating sessions. */
|
|
20
|
+
modelRegistry: {
|
|
21
|
+
find(provider: string, modelId: string): unknown;
|
|
22
|
+
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
23
|
+
};
|
|
24
|
+
/** Pre-built parent conversation text (when inheritContext was requested). */
|
|
25
|
+
parentContext?: string;
|
|
26
|
+
}
|
|
8
27
|
|
|
9
28
|
/**
|
|
10
29
|
* Build an immutable snapshot of the parent session state.
|
package/src/prompts.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* prompts.ts — System prompt builder for agents.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type { EnvInfo } from "./env.js";
|
|
6
|
+
import type { AgentPromptConfig } from "./types.js";
|
|
6
7
|
|
|
7
8
|
/** Extra sections to inject into the system prompt (memory, skills, etc.). */
|
|
8
9
|
export interface PromptExtras {
|
|
@@ -27,7 +28,7 @@ export interface PromptExtras {
|
|
|
27
28
|
* @param extras Optional extra sections to inject (memory, preloaded skills).
|
|
28
29
|
*/
|
|
29
30
|
export function buildAgentPrompt(
|
|
30
|
-
config:
|
|
31
|
+
config: AgentPromptConfig,
|
|
31
32
|
cwd: string,
|
|
32
33
|
env: EnvInfo,
|
|
33
34
|
parentSystemPrompt?: string,
|
package/src/renderer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
|
-
import type { NotificationDetails } from "./
|
|
2
|
+
import type { NotificationDetails } from "./notification.js";
|
|
3
3
|
import { formatMs, formatTokens, formatTurns } from "./ui/agent-widget.js";
|
|
4
4
|
|
|
5
5
|
/** Narrow theme interface — only the methods the renderer actually calls. */
|
package/src/session-config.ts
CHANGED
|
@@ -15,10 +15,11 @@ import {
|
|
|
15
15
|
getMemoryToolNames,
|
|
16
16
|
getReadOnlyMemoryToolNames,
|
|
17
17
|
} from "./agent-types.js";
|
|
18
|
+
import type { EnvInfo } from "./env.js";
|
|
18
19
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
19
20
|
import { buildAgentPrompt, type PromptExtras } from "./prompts.js";
|
|
20
21
|
import { preloadSkills } from "./skill-loader.js";
|
|
21
|
-
import type {
|
|
22
|
+
import type { SubagentType, ThinkingLevel } from "./types.js";
|
|
22
23
|
|
|
23
24
|
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
24
25
|
|
package/src/types.ts
CHANGED
|
@@ -18,11 +18,23 @@ export type MemoryScope = "user" | "project" | "local";
|
|
|
18
18
|
/** Isolation mode for agent execution. */
|
|
19
19
|
export type IsolationMode = "worktree";
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
export interface
|
|
21
|
+
/** UI display and agent listing — name, display name, description, prompt mode. */
|
|
22
|
+
export interface AgentIdentity {
|
|
23
23
|
name: string;
|
|
24
24
|
displayName?: string;
|
|
25
25
|
description: string;
|
|
26
|
+
promptMode: "replace" | "append";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Prompt assembly — name, prompt mode, system prompt. */
|
|
30
|
+
export interface AgentPromptConfig {
|
|
31
|
+
name: string;
|
|
32
|
+
promptMode: "replace" | "append";
|
|
33
|
+
systemPrompt: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Unified agent configuration — used for both default and user-defined agents. */
|
|
37
|
+
export interface AgentConfig extends AgentIdentity, AgentPromptConfig {
|
|
26
38
|
builtinToolNames?: string[];
|
|
27
39
|
/** Tool denylist — these tools are removed even if `builtinToolNames` or extensions include them. */
|
|
28
40
|
disallowedTools?: string[];
|
|
@@ -33,8 +45,6 @@ export interface AgentConfig {
|
|
|
33
45
|
model?: string;
|
|
34
46
|
thinking?: ThinkingLevel;
|
|
35
47
|
maxTurns?: number;
|
|
36
|
-
systemPrompt: string;
|
|
37
|
-
promptMode: "replace" | "append";
|
|
38
48
|
/** Default for spawn: fork parent conversation. undefined = caller decides. */
|
|
39
49
|
inheritContext?: boolean;
|
|
40
50
|
/** Default for spawn: run in background. undefined = caller decides. */
|
|
@@ -64,48 +74,6 @@ export interface AgentInvocation {
|
|
|
64
74
|
isolation?: IsolationMode;
|
|
65
75
|
}
|
|
66
76
|
|
|
67
|
-
/** Details attached to custom notification messages for visual rendering. */
|
|
68
|
-
export interface NotificationDetails {
|
|
69
|
-
id: string;
|
|
70
|
-
description: string;
|
|
71
|
-
status: string;
|
|
72
|
-
toolUses: number;
|
|
73
|
-
turnCount: number;
|
|
74
|
-
maxTurns?: number;
|
|
75
|
-
totalTokens: number;
|
|
76
|
-
durationMs: number;
|
|
77
|
-
outputFile?: string;
|
|
78
|
-
error?: string;
|
|
79
|
-
resultPreview: string;
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Plain data snapshot of the parent session state captured at spawn time.
|
|
85
|
-
* Replaces live `ExtensionContext` references so queued agents don't read stale state.
|
|
86
|
-
*/
|
|
87
|
-
export interface ParentSnapshot {
|
|
88
|
-
/** Parent working directory. */
|
|
89
|
-
cwd: string;
|
|
90
|
-
/** Parent's effective system prompt (for append-mode agents). */
|
|
91
|
-
systemPrompt: string;
|
|
92
|
-
/** Parent's current model instance (fallback when agent config has no model). */
|
|
93
|
-
model: unknown;
|
|
94
|
-
/** Model registry for resolving config.model strings and creating sessions. */
|
|
95
|
-
modelRegistry: {
|
|
96
|
-
find(provider: string, modelId: string): unknown;
|
|
97
|
-
getAvailable?(): Array<{ provider: string; id: string }>;
|
|
98
|
-
};
|
|
99
|
-
/** Pre-built parent conversation text (when inheritContext was requested). */
|
|
100
|
-
parentContext?: string;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export interface EnvInfo {
|
|
104
|
-
isGitRepo: boolean;
|
|
105
|
-
branch: string;
|
|
106
|
-
platform: string;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
77
|
/**
|
|
110
78
|
* Narrow shell-exec callback replacing `ExtensionAPI` in `detectEnv()`.
|
|
111
79
|
* Matches the shape of `pi.exec()` without carrying an SDK dependency.
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -220,7 +220,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
220
220
|
|
|
221
221
|
await ctx.ui.custom<undefined>(
|
|
222
222
|
(tui: any, theme: any, _keybindings: any, done: any) => {
|
|
223
|
-
return new ConversationViewer(tui, session, record, activity, theme, done, deps.registry);
|
|
223
|
+
return new ConversationViewer({ tui, session, record, activity, theme, done, registry: deps.registry });
|
|
224
224
|
},
|
|
225
225
|
{
|
|
226
226
|
overlay: true,
|
|
@@ -20,6 +20,16 @@ const MIN_VIEWPORT = 3;
|
|
|
20
20
|
/** Height ceiling shared by the overlay's `maxHeight` and the viewer's internal viewport cap. */
|
|
21
21
|
export const VIEWPORT_HEIGHT_PCT = 70;
|
|
22
22
|
|
|
23
|
+
export interface ConversationViewerOptions {
|
|
24
|
+
tui: TUI;
|
|
25
|
+
session: AgentSession;
|
|
26
|
+
record: AgentRecord;
|
|
27
|
+
activity: AgentActivityTracker | undefined;
|
|
28
|
+
theme: Theme;
|
|
29
|
+
done: (result: undefined) => void;
|
|
30
|
+
registry: AgentConfigLookup;
|
|
31
|
+
}
|
|
32
|
+
|
|
23
33
|
export class ConversationViewer implements Component {
|
|
24
34
|
private scrollOffset = 0;
|
|
25
35
|
private autoScroll = true;
|
|
@@ -27,16 +37,23 @@ export class ConversationViewer implements Component {
|
|
|
27
37
|
private lastInnerW = 0;
|
|
28
38
|
private closed = false;
|
|
29
39
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
) {
|
|
39
|
-
this.
|
|
40
|
+
private tui: TUI;
|
|
41
|
+
private session: AgentSession;
|
|
42
|
+
private record: AgentRecord;
|
|
43
|
+
private activity: AgentActivityTracker | undefined;
|
|
44
|
+
private theme: Theme;
|
|
45
|
+
private done: (result: undefined) => void;
|
|
46
|
+
private registry: AgentConfigLookup;
|
|
47
|
+
|
|
48
|
+
constructor(options: ConversationViewerOptions) {
|
|
49
|
+
this.tui = options.tui;
|
|
50
|
+
this.session = options.session;
|
|
51
|
+
this.record = options.record;
|
|
52
|
+
this.activity = options.activity;
|
|
53
|
+
this.theme = options.theme;
|
|
54
|
+
this.done = options.done;
|
|
55
|
+
this.registry = options.registry;
|
|
56
|
+
this.unsubscribe = options.session.subscribe(() => {
|
|
40
57
|
if (this.closed) return;
|
|
41
58
|
this.tui.requestRender();
|
|
42
59
|
});
|