@gotgenes/pi-subagents 7.2.5 → 7.2.7
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 +22 -0
- package/docs/architecture/architecture.md +20 -157
- package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
- package/docs/plans/0205-decompose-render-widget-lines.md +140 -0
- package/docs/plans/0206-decompose-show-agent-detail.md +211 -0
- package/docs/retro/0196-convert-runner-menu-to-classes.md +31 -0
- package/docs/retro/0205-decompose-render-widget-lines.md +64 -0
- package/docs/retro/0206-decompose-show-agent-detail.md +36 -0
- package/package.json +1 -1
- package/src/ui/agent-config-editor.ts +92 -79
- package/src/ui/widget-renderer.ts +141 -77
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ 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
|
+
## [7.2.7](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.6...pi-subagents-v7.2.7) (2026-05-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan decompose showAgentDetail ([#206](https://github.com/gotgenes/pi-packages/issues/206)) ([fe575a0](https://github.com/gotgenes/pi-packages/commit/fe575a0f2727bdcdaa4dd977f7b2db4d6cb6e9f3))
|
|
14
|
+
* **retro:** add planning stage notes for issue [#206](https://github.com/gotgenes/pi-packages/issues/206) ([057bbb6](https://github.com/gotgenes/pi-packages/commit/057bbb666b8d1c89d70936a837fc028512368fcc))
|
|
15
|
+
* **retro:** add retro notes for issue [#205](https://github.com/gotgenes/pi-packages/issues/205) ([b9abe3b](https://github.com/gotgenes/pi-packages/commit/b9abe3ba050468d71015eb77262afb4093c8289f))
|
|
16
|
+
* **retro:** add TDD stage notes for issue [#206](https://github.com/gotgenes/pi-packages/issues/206) ([88fdfc2](https://github.com/gotgenes/pi-packages/commit/88fdfc222950ce63e0a8f9273d3bff234ffa0538))
|
|
17
|
+
* update complexity table after showAgentDetail decomposition ([8d8a396](https://github.com/gotgenes/pi-packages/commit/8d8a396bbfbe0e4d86dc37aceb53df613868bd26))
|
|
18
|
+
|
|
19
|
+
## [7.2.6](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.5...pi-subagents-v7.2.6) (2026-05-25)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Documentation
|
|
23
|
+
|
|
24
|
+
* archive Phase 11 to history, add Phase 11 to refactoring table ([2c617f2](https://github.com/gotgenes/pi-packages/commit/2c617f2c752ea935d55878ba58fce997985086b5))
|
|
25
|
+
* plan decompose renderWidgetLines ([#205](https://github.com/gotgenes/pi-packages/issues/205)) ([88d09cd](https://github.com/gotgenes/pi-packages/commit/88d09cdad53bc3f215c069b8d6da6a44e10b5af7))
|
|
26
|
+
* **retro:** add planning stage notes for issue [#205](https://github.com/gotgenes/pi-packages/issues/205) ([14afc1f](https://github.com/gotgenes/pi-packages/commit/14afc1ff82d61828fdac9373f31cb68ebfc1a2e7))
|
|
27
|
+
* **retro:** add retro notes for issue [#196](https://github.com/gotgenes/pi-packages/issues/196) ([cfc7d94](https://github.com/gotgenes/pi-packages/commit/cfc7d94f72b120a4550f73e2d1cf00822db759c2))
|
|
28
|
+
* **retro:** add TDD stage notes for issue [#205](https://github.com/gotgenes/pi-packages/issues/205) ([a676078](https://github.com/gotgenes/pi-packages/commit/a6760789898435e5b552941124de6f32be21407e))
|
|
29
|
+
|
|
8
30
|
## [7.2.5](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.4...pi-subagents-v7.2.5) (2026-05-25)
|
|
9
31
|
|
|
10
32
|
|
|
@@ -466,12 +466,10 @@ Bags with 10+ fields are the highest priority for decomposition.
|
|
|
466
466
|
|
|
467
467
|
Functions with cyclomatic complexity ≥ 21 (critical threshold):
|
|
468
468
|
|
|
469
|
-
| Function | Cyclomatic | Cognitive | File | Concern
|
|
470
|
-
| ------------------- | ---------- | --------- | --------------------------- |
|
|
471
|
-
| `
|
|
472
|
-
| `
|
|
473
|
-
| `ejectAgent` | 21 | 20 | `ui/agent-config-editor.ts` | Eject agent to filesystem |
|
|
474
|
-
| `update` | 21 | 31 | `ui/agent-widget.ts` | Widget lifecycle + polling |
|
|
469
|
+
| Function | Cyclomatic | Cognitive | File | Concern |
|
|
470
|
+
| ------------------- | ---------- | --------- | --------------------------- | ------------------------------- |
|
|
471
|
+
| `renderWidgetLines` | 25 | 44 | `ui/widget-renderer.ts` | Renders widget status lines |
|
|
472
|
+
| `update` | 21 | 31 | `ui/agent-widget.ts` | Widget lifecycle + polling |
|
|
475
473
|
|
|
476
474
|
### Churn hotspots
|
|
477
475
|
|
|
@@ -599,155 +597,18 @@ export type RunnerIO = EnvironmentIO & SessionFactoryIO;
|
|
|
599
597
|
`RunnerIO` is kept as a type alias for the intersection.
|
|
600
598
|
All existing consumers satisfy both sub-interfaces via structural typing with no call-site changes.
|
|
601
599
|
|
|
602
|
-
##
|
|
600
|
+
## Phase 11 (complete)
|
|
603
601
|
|
|
604
|
-
Phase 11
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
> "Make the change that makes the change easy." —Kent Beck
|
|
608
|
-
|
|
609
|
-
### Findings
|
|
610
|
-
|
|
611
|
-
| Metric | Value |
|
|
612
|
-
| ------------------------- | ------------------------------------------ |
|
|
613
|
-
| Health score | 75/100 (B) |
|
|
614
|
-
| #1 hotspot | `index.ts` (128 commits, accelerating) |
|
|
615
|
-
| Dead exports | 0 (down from 1; Layer 2 removed re-export) |
|
|
616
|
-
| Production duplication | 0 |
|
|
617
|
-
| Test duplication | 1,396 lines (69 clone groups, 22 files) |
|
|
618
|
-
| `as any` casts in index | 1 (down from 5; Layer 1 resolved 4) |
|
|
619
|
-
| Adapter closures in index | 40 (down from 44; Layers 1–2 resolved 4) |
|
|
620
|
-
| Index fan-out | 25 imports |
|
|
621
|
-
|
|
622
|
-
### Root cause
|
|
623
|
-
|
|
624
|
-
The 44 adapter closures in `index.ts` exist because the tool factories accept narrow interfaces that don't structurally match the real objects.
|
|
625
|
-
The real objects can't satisfy the interfaces because:
|
|
626
|
-
|
|
627
|
-
1. ~~`SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }` — so every consumer must `as any` cast to read fields.~~
|
|
628
|
-
Resolved by Layer 0 (#192) + Layer 1 (#193).
|
|
629
|
-
2. ~~Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) live as closures in index.ts instead of methods on the state holder.~~
|
|
630
|
-
Resolved by Layer 1 (#193).
|
|
631
|
-
3. ~~`AgentToolManager` mixes fields from `AgentManager` and `SettingsManager` (source mismatch).~~
|
|
632
|
-
Resolved by Layer 2 (#194).
|
|
633
|
-
4. ~~`AgentToolWidget` uses different method names than `SubagentRuntime` (name mismatch).~~
|
|
634
|
-
Resolved by Layer 2 (#194).
|
|
635
|
-
|
|
636
|
-
Fix these structural misalignments and the class conversions become mechanical.
|
|
637
|
-
|
|
638
|
-
### Layer 0: Define `SessionContext` narrow interface ([#192][192])
|
|
639
|
-
|
|
640
|
-
`SubagentRuntime` currently types its context as `unknown` to avoid SDK coupling.
|
|
641
|
-
But `ExtensionContext` is exported by the SDK — the `unknown` is a historical choice, not a constraint.
|
|
642
|
-
Define a narrow `SessionContext` interface capturing the 5 fields runtime actually needs:
|
|
643
|
-
|
|
644
|
-
```typescript
|
|
645
|
-
export interface SessionContext {
|
|
646
|
-
readonly cwd: string;
|
|
647
|
-
readonly model: unknown;
|
|
648
|
-
readonly modelRegistry: ModelRegistry | undefined;
|
|
649
|
-
getSystemPrompt(): string;
|
|
650
|
-
readonly sessionManager: {
|
|
651
|
-
getSessionFile(): string | undefined;
|
|
652
|
-
getSessionId(): string;
|
|
653
|
-
getBranch(): unknown[];
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
```
|
|
657
|
-
|
|
658
|
-
- Target: `src/types.ts`
|
|
659
|
-
- Smell: Category C (platform type threading)
|
|
660
|
-
- Outcome: typed foundation for Layers 1–4; no `as any` needed by consumers of `SubagentRuntime`
|
|
661
|
-
|
|
662
|
-
### Layer 1: `SubagentRuntime` stores typed context, owns its queries ([#193][193]) ✓ done
|
|
663
|
-
|
|
664
|
-
Change `currentCtx` from `{ pi: unknown; ctx: unknown }` to `SessionContext | undefined`.
|
|
665
|
-
The single `as SessionContext` cast moves into `handleSessionStart` — the boundary where the SDK hands us the value.
|
|
666
|
-
Add typed methods: `buildSnapshot(inheritContext)`, `getModelInfo()`, `getSessionInfo()`.
|
|
667
|
-
|
|
668
|
-
- Target: `src/runtime.ts`, `src/handlers/lifecycle.ts`, `src/service/service-adapter.ts`, `src/index.ts`
|
|
669
|
-
- Smell: Category C (closure queries on mutable field → methods on state owner)
|
|
670
|
-
- Outcome: 3 closure queries in index.ts → 0; `SubagentRuntime` is self-sufficient for tool deps
|
|
671
|
-
- Enables: Layer 3 (tools accept `SubagentRuntime` directly)
|
|
672
|
-
|
|
673
|
-
### Layer 2: Align interfaces so real objects satisfy tool deps structurally ([#194][194]) ✓ done
|
|
674
|
-
|
|
675
|
-
Three alignment changes:
|
|
676
|
-
|
|
677
|
-
1. **Move `getMaxConcurrent` off `AgentToolManager`** — it reads from `SettingsManager`, not `AgentManager`.
|
|
678
|
-
The tool already receives `settings`; read it from there.
|
|
679
|
-
2. **Rename widget methods** — align `SubagentRuntime` method names with `AgentToolWidget` (either rename `updateWidget()` → `update()` on runtime, or rename the interface to match).
|
|
680
|
-
3. **Remove dead re-export** — `getToolCallName` in `ui/message-formatters.ts` (fallow finding).
|
|
681
|
-
|
|
682
|
-
After this step, `AgentManager` structurally satisfies `AgentToolManager` and `SubagentRuntime` structurally satisfies `AgentToolWidget`.
|
|
683
|
-
|
|
684
|
-
- Target: `src/tools/agent-tool.ts` (interface), `src/runtime.ts` (method names), `src/ui/message-formatters.ts`
|
|
685
|
-
- Smell: Category C (source mismatch, name mismatch) + Category A (dead export)
|
|
686
|
-
- Outcome: structural typing connects real objects to tool interfaces without adapters; 0 dead exports (fallow clean)
|
|
687
|
-
- Enables: Layer 3 (class constructors accept real objects directly)
|
|
688
|
-
|
|
689
|
-
### Layer 3: Convert closure factories to classes ([#195][195], [#196][196]) ✓ done
|
|
690
|
-
|
|
691
|
-
All closure factories converted to classes. ✓ Tool factories in [#195][195]; runner and menu in [#196][196]:
|
|
692
|
-
|
|
693
|
-
| Factory | Class | Constructor params |
|
|
694
|
-
| -------------------------------- | ----------------------- | -------------------------------------------------------------------- |
|
|
695
|
-
| `createAgentTool({...})` | `AgentTool` ✓ | `manager`, `runtime`, `settings`, `registry` |
|
|
696
|
-
| `createGetResultTool(...)` | `GetResultTool` ✓ | `manager`, `notifications`, `registry` |
|
|
697
|
-
| `createSteerTool(...)` | `SteerTool` ✓ | `manager`, `events` |
|
|
698
|
-
| `createAgentRunner(runnerIO)` | `ConcreteAgentRunner` ✓ | `io: RunnerIO` |
|
|
699
|
-
| `createAgentsMenuHandler({...})` | `AgentsMenuHandler` ✓ | `manager`, `registry`, `agentActivity`, `settings`, `fileOps`, paths |
|
|
700
|
-
|
|
701
|
-
Each class satisfies the existing interface via structural typing.
|
|
702
|
-
The `defineTool()` wrapper moves into a `toToolDefinition()` method on each tool class.
|
|
703
|
-
`getModelLabel` internalized into `AgentsMenuHandler` (was a 7-line closure in `index.ts`).
|
|
704
|
-
|
|
705
|
-
- Target: `src/tools/*.ts`, `src/lifecycle/agent-runner.ts`, `src/ui/agent-menu.ts`
|
|
706
|
-
- Smell: Category C (closure factories masquerading as classes)
|
|
707
|
-
- Outcome: deps are constructor params (inspectable, testable); no captured closures
|
|
708
|
-
- Enables: Layer 4 (index.ts simplification)
|
|
709
|
-
|
|
710
|
-
### Layer 4: Simplify index.ts (included in [#196][196]) ✓ done
|
|
711
|
-
|
|
712
|
-
With all factories converted to classes and `AgentManager` satisfying `AgentMenuManager` structurally:
|
|
713
|
-
|
|
714
|
-
```typescript
|
|
715
|
-
const agentsMenu = new AgentsMenuHandler(
|
|
716
|
-
manager, registry, runtime.agentActivity,
|
|
717
|
-
settings, new FsAgentFileOps(),
|
|
718
|
-
join(getAgentDir(), "agents"),
|
|
719
|
-
join(process.cwd(), ".pi", "agents"),
|
|
720
|
-
);
|
|
721
|
-
```
|
|
722
|
-
|
|
723
|
-
Eliminated: 4 adapter closures (3 manager method adapters + `getModelLabel`), 4 unused imports.
|
|
724
|
-
Remaining ~15 closures are structural (event registrations, SDK factory callbacks).
|
|
725
|
-
|
|
726
|
-
- Target: `src/index.ts`
|
|
727
|
-
- Smell: Category B (god file) + Category C (adapter closure density)
|
|
728
|
-
- Outcome: adapter closure count reduced; `AgentManager` passed directly without wrappers; churn hotspot stabilized
|
|
729
|
-
|
|
730
|
-
### Step dependencies
|
|
731
|
-
|
|
732
|
-
```mermaid
|
|
733
|
-
flowchart LR
|
|
734
|
-
L0["Layer 0: SessionContext interface"] --> L1["Layer 1: Runtime owns queries"]
|
|
735
|
-
L1 --> L3["Layer 3: Classes replace factories"]
|
|
736
|
-
L2["Layer 2: Align interfaces"] --> L3
|
|
737
|
-
L3 --> L4["Layer 4: Simplify index.ts"]
|
|
738
|
-
```
|
|
739
|
-
|
|
740
|
-
Layers 0 and 2 are independent of each other.
|
|
741
|
-
Layer 1 depends on Layer 0.
|
|
742
|
-
Layer 3 depends on both Layer 1 and Layer 2.
|
|
743
|
-
Layer 4 depends on Layer 3.
|
|
602
|
+
Phase 11 converted all closure factories to classes, eliminating adapter closure density in `index.ts`.
|
|
603
|
+
Four layers: SessionContext typing → runtime query methods → interface alignment → class conversions → index.ts simplification.
|
|
604
|
+
See [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) for details.
|
|
744
605
|
|
|
745
606
|
## Improvement roadmap (Phase 12)
|
|
746
607
|
|
|
747
608
|
Phase 12 addresses the remaining fallow refactoring targets and test duplication.
|
|
748
609
|
These are independent of Phase 11 and can proceed in parallel if desired.
|
|
749
610
|
|
|
750
|
-
### Step 1: Decompose `renderWidgetLines` (cognitive 44)
|
|
611
|
+
### Step 1: Decompose `renderWidgetLines` (cognitive 44) — [#205]
|
|
751
612
|
|
|
752
613
|
`renderWidgetLines` in `ui/widget-renderer.ts` handles agent-status formatting, tree connectors, overflow, and empty states.
|
|
753
614
|
Extract per-status renderers and a tree-connector utility.
|
|
@@ -755,7 +616,7 @@ Extract per-status renderers and a tree-connector utility.
|
|
|
755
616
|
- Target: `src/ui/widget-renderer.ts`
|
|
756
617
|
- Outcome: cognitive complexity < 10
|
|
757
618
|
|
|
758
|
-
### Step 2: Decompose `showAgentDetail` (cognitive 33)
|
|
619
|
+
### Step 2: Decompose `showAgentDetail` (cognitive 33) — [#206]
|
|
759
620
|
|
|
760
621
|
`showAgentDetail` in `ui/agent-config-editor.ts` handles display, edit, eject, and delete flows.
|
|
761
622
|
Extract sub-functions per menu action.
|
|
@@ -763,7 +624,7 @@ Extract sub-functions per menu action.
|
|
|
763
624
|
- Target: `src/ui/agent-config-editor.ts`
|
|
764
625
|
- Outcome: cognitive complexity < 10
|
|
765
626
|
|
|
766
|
-
### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31)
|
|
627
|
+
### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31) — [#207]
|
|
767
628
|
|
|
768
629
|
`update` mixes timer lifecycle, agent list assembly, render delegation, and visibility state.
|
|
769
630
|
Extract `assembleWidgetState` (pure) and timer management.
|
|
@@ -771,7 +632,7 @@ Extract `assembleWidgetState` (pure) and timer management.
|
|
|
771
632
|
- Target: `src/ui/agent-widget.ts`
|
|
772
633
|
- Outcome: cognitive complexity < 10
|
|
773
634
|
|
|
774
|
-
### Step 4: Extract shared test fixtures
|
|
635
|
+
### Step 4: Extract shared test fixtures — [#208]
|
|
775
636
|
|
|
776
637
|
The 3 heaviest clone families:
|
|
777
638
|
|
|
@@ -786,7 +647,7 @@ Extract shared factories into `test/fixtures/` modules.
|
|
|
786
647
|
|
|
787
648
|
## Refactoring history
|
|
788
649
|
|
|
789
|
-
Phases 1–5 and 7–
|
|
650
|
+
Phases 1–5 and 7–11 are complete.
|
|
790
651
|
Phase 6 (UI extraction to a separate package) is deferred.
|
|
791
652
|
Detailed records are preserved in per-phase history files:
|
|
792
653
|
|
|
@@ -802,6 +663,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
802
663
|
| 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
|
|
803
664
|
| 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
|
|
804
665
|
| 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
|
|
666
|
+
| 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
|
|
805
667
|
|
|
806
668
|
### Structural refactoring issues
|
|
807
669
|
|
|
@@ -816,6 +678,8 @@ Detailed records are preserved in per-phase history files:
|
|
|
816
678
|
| Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
|
|
817
679
|
| Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
|
|
818
680
|
| Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
|
|
681
|
+
| Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
|
|
682
|
+
| Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
|
|
819
683
|
|
|
820
684
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
821
685
|
|
|
@@ -839,8 +703,7 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
839
703
|
[167]: https://github.com/gotgenes/pi-packages/issues/167
|
|
840
704
|
[168]: https://github.com/gotgenes/pi-packages/issues/168
|
|
841
705
|
[169]: https://github.com/gotgenes/pi-packages/issues/169
|
|
842
|
-
[
|
|
843
|
-
[
|
|
844
|
-
[
|
|
845
|
-
[
|
|
846
|
-
[196]: https://github.com/gotgenes/pi-packages/issues/196
|
|
706
|
+
[#205]: https://github.com/gotgenes/pi-packages/issues/205
|
|
707
|
+
[#206]: https://github.com/gotgenes/pi-packages/issues/206
|
|
708
|
+
[#207]: https://github.com/gotgenes/pi-packages/issues/207
|
|
709
|
+
[#208]: https://github.com/gotgenes/pi-packages/issues/208
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Phase 11: Closure factories to classes
|
|
2
|
+
|
|
3
|
+
Target: convert all closure factories to classes, eliminating adapter closure density in `index.ts` (the package's #1 churn hotspot) and making the composition root a pure construction site.
|
|
4
|
+
|
|
5
|
+
## Root cause
|
|
6
|
+
|
|
7
|
+
The 44 adapter closures in `index.ts` existed because tool factories accepted narrow interfaces that didn't structurally match the real objects.
|
|
8
|
+
Four structural misalignments prevented direct wiring:
|
|
9
|
+
|
|
10
|
+
1. `SubagentRuntime.currentCtx` was typed `{ pi: unknown; ctx: unknown }` — every consumer had to `as any` cast.
|
|
11
|
+
2. Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) lived as closures in `index.ts` instead of methods on the state holder.
|
|
12
|
+
3. `AgentToolManager` mixed fields from `AgentManager` and `SettingsManager` (source mismatch).
|
|
13
|
+
4. `AgentToolWidget` used different method names than `SubagentRuntime` (name mismatch).
|
|
14
|
+
|
|
15
|
+
## Layer 0: Define SessionContext narrow interface (#192)
|
|
16
|
+
|
|
17
|
+
Defined a `SessionContext` interface capturing the 5 fields `SubagentRuntime` actually needs.
|
|
18
|
+
Replaced the `{ pi: unknown; ctx: unknown }` typing with `SessionContext | undefined`.
|
|
19
|
+
|
|
20
|
+
Impact: typed foundation for all subsequent layers; no `as any` needed by consumers.
|
|
21
|
+
|
|
22
|
+
## Layer 1: SubagentRuntime stores typed context, owns its queries (#193)
|
|
23
|
+
|
|
24
|
+
Changed `currentCtx` to `SessionContext | undefined`.
|
|
25
|
+
The single `as SessionContext` cast moved into `handleSessionStart` — the SDK boundary.
|
|
26
|
+
Added typed methods: `buildSnapshot(inheritContext)`, `getModelInfo()`, `getSessionInfo()`.
|
|
27
|
+
|
|
28
|
+
Impact: 3 closure queries in `index.ts` → 0; 4 `as any` casts eliminated; `SubagentRuntime` is self-sufficient for tool deps.
|
|
29
|
+
|
|
30
|
+
## Layer 2: Align interfaces for structural typing (#194)
|
|
31
|
+
|
|
32
|
+
Three alignment changes:
|
|
33
|
+
|
|
34
|
+
1. Moved `getMaxConcurrent` off `AgentToolManager` — it reads from `SettingsManager`, not `AgentManager`.
|
|
35
|
+
2. Renamed widget methods — aligned `SubagentRuntime` method names with `AgentToolWidget`.
|
|
36
|
+
3. Removed dead re-export `getToolCallName` in `ui/message-formatters.ts`.
|
|
37
|
+
|
|
38
|
+
Impact: `AgentManager` structurally satisfies `AgentToolManager`; `SubagentRuntime` structurally satisfies `AgentToolWidget`; 0 dead exports.
|
|
39
|
+
|
|
40
|
+
## Layer 3: Convert closure factories to classes (#195, #196)
|
|
41
|
+
|
|
42
|
+
All five closure factories converted to classes:
|
|
43
|
+
|
|
44
|
+
| Factory | Class | Issue |
|
|
45
|
+
| -------------------------------- | --------------------- | ----- |
|
|
46
|
+
| `createAgentTool({...})` | `AgentTool` | #195 |
|
|
47
|
+
| `createGetResultTool(...)` | `GetResultTool` | #195 |
|
|
48
|
+
| `createSteerTool(...)` | `SteerTool` | #195 |
|
|
49
|
+
| `createAgentRunner(runnerIO)` | `ConcreteAgentRunner` | #196 |
|
|
50
|
+
| `createAgentsMenuHandler({...})` | `AgentsMenuHandler` | #196 |
|
|
51
|
+
|
|
52
|
+
Each class satisfies the existing interface via structural typing.
|
|
53
|
+
Tool classes expose `toToolDefinition()` for Pi tool registration.
|
|
54
|
+
`getModelLabel` internalized into `AgentsMenuHandler` (was a 7-line closure in `index.ts`).
|
|
55
|
+
|
|
56
|
+
Impact: deps are constructor params (inspectable, testable); no captured closures.
|
|
57
|
+
|
|
58
|
+
## Layer 4: Simplify index.ts (#196)
|
|
59
|
+
|
|
60
|
+
With all factories converted and `AgentManager` satisfying `AgentMenuManager` structurally:
|
|
61
|
+
|
|
62
|
+
- 4 adapter closures eliminated (3 manager method adapters + `getModelLabel`).
|
|
63
|
+
- 4 unused imports removed.
|
|
64
|
+
- Remaining ~15 closures are structural (event registrations, SDK factory callbacks).
|
|
65
|
+
|
|
66
|
+
Impact: adapter closure count halved; `AgentManager` passed directly without wrappers; churn hotspot stabilized.
|
|
67
|
+
|
|
68
|
+
## Step dependencies
|
|
69
|
+
|
|
70
|
+
```mermaid
|
|
71
|
+
flowchart LR
|
|
72
|
+
L0["Layer 0: SessionContext #192"] --> L1["Layer 1: Runtime queries #193"]
|
|
73
|
+
L1 --> L3["Layer 3: Classes #195 #196"]
|
|
74
|
+
L2["Layer 2: Align interfaces #194"] --> L3
|
|
75
|
+
L3 --> L4["Layer 4: Simplify index.ts #196"]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Layers 0 and 2 were independent.
|
|
79
|
+
Layer 1 depended on Layer 0.
|
|
80
|
+
Layer 3 depended on both Layer 1 and Layer 2.
|
|
81
|
+
Layer 4 depended on Layer 3.
|
|
82
|
+
|
|
83
|
+
## Impact
|
|
84
|
+
|
|
85
|
+
| Metric | Before | After |
|
|
86
|
+
| ------------------------------ | ---------- | --------------------- |
|
|
87
|
+
| Health score | 75/100 (B) | 76/100 (B) |
|
|
88
|
+
| Adapter closures in `index.ts` | 44 | ~15 (structural only) |
|
|
89
|
+
| `as any` casts in `index.ts` | 5 | 1 (SDK boundary) |
|
|
90
|
+
| Dead exports | 1 | 0 |
|
|
91
|
+
| Closure factories | 5 | 0 |
|
|
92
|
+
| `index.ts` fan-out | 25 imports | 21 imports |
|
|
93
|
+
|
|
94
|
+
## Related issues
|
|
95
|
+
|
|
96
|
+
- #192 — Define SessionContext narrow interface
|
|
97
|
+
- #193 — Runtime owns context queries
|
|
98
|
+
- #194 — Align tool interfaces for structural typing
|
|
99
|
+
- #195 — Convert tool factories to classes
|
|
100
|
+
- #196 — Convert AgentRunner and AgentsMenuHandler to classes, simplify index.ts
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 205
|
|
3
|
+
issue_title: "Decompose renderWidgetLines (cognitive 44)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decompose `renderWidgetLines`
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`renderWidgetLines` in `ui/widget-renderer.ts` has cognitive complexity 44 (CRITICAL per fallow health).
|
|
11
|
+
It handles agent categorization, per-status line building with tree connectors, non-overflow assembly with last-connector fixup, and overflow-budget assembly — all in a single 106-line function.
|
|
12
|
+
This is the highest-complexity function remaining in the codebase (Phase 12, Step 1).
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Extract distinct concerns into separate pure functions, each with cognitive complexity < 10.
|
|
17
|
+
- Preserve all existing behavior — no visual or behavioral changes.
|
|
18
|
+
- Keep all extracted functions in `widget-renderer.ts` (they are private helpers, not a separate module).
|
|
19
|
+
|
|
20
|
+
## Non-Goals
|
|
21
|
+
|
|
22
|
+
- Decomposing `showAgentDetail` (#206), `update` (#207), or shared test fixtures (#208) — those are sibling Phase 12 steps.
|
|
23
|
+
- Changing the widget's visual output or tree-connector style.
|
|
24
|
+
- Modifying `renderFinishedLine` or `renderRunningLines` — those are already single-concern functions.
|
|
25
|
+
|
|
26
|
+
## Background
|
|
27
|
+
|
|
28
|
+
`widget-renderer.ts` was extracted from `AgentWidget` in #148.
|
|
29
|
+
The per-agent renderers (`renderFinishedLine`, `renderRunningLines`) are already clean single-concern functions.
|
|
30
|
+
The remaining complexity lives entirely in `renderWidgetLines`, which orchestrates categorization, section building, and assembly.
|
|
31
|
+
|
|
32
|
+
The function has five interwoven concerns:
|
|
33
|
+
|
|
34
|
+
1. **Agent categorization** — filtering into running/queued/finished buckets.
|
|
35
|
+
2. **Section building** — rendering each bucket into pre-formatted line arrays with `├─` tree connectors.
|
|
36
|
+
3. **Heading construction** — choosing icon/color based on active vs. finished-only.
|
|
37
|
+
4. **Non-overflow assembly** — concatenating sections when under `MAX_WIDGET_LINES`, then fixing the last connector (`├─` → `└─`).
|
|
38
|
+
5. **Overflow assembly** — budget-based prioritized assembly (running > queued > finished) with an overflow indicator line.
|
|
39
|
+
|
|
40
|
+
## Design Overview
|
|
41
|
+
|
|
42
|
+
Extract four helper functions from the body of `renderWidgetLines`:
|
|
43
|
+
|
|
44
|
+
### `categorizeAgents`
|
|
45
|
+
|
|
46
|
+
Accepts the agents array and `shouldShowFinished` callback.
|
|
47
|
+
Returns `{ running, queued, finished }` arrays.
|
|
48
|
+
Pure filter — no rendering.
|
|
49
|
+
|
|
50
|
+
### `buildSections`
|
|
51
|
+
|
|
52
|
+
Accepts categorized agents, `activityMap`, `registry`, `spinnerFrame`, `theme`, and a `truncate` function.
|
|
53
|
+
Returns `{ finishedLines, runningLines, queuedLine }` — the pre-formatted line arrays with `├─` connectors.
|
|
54
|
+
Calls `renderFinishedLine` and `renderRunningLines` internally.
|
|
55
|
+
|
|
56
|
+
### `assembleWithinBudget`
|
|
57
|
+
|
|
58
|
+
Accepts `finishedLines`, `runningLines`, `queuedLine`, and the heading line.
|
|
59
|
+
Handles the non-overflow path: concatenates sections and fixes the last tree connector (`├─` → `└─`, `│` → space).
|
|
60
|
+
Returns the assembled `string[]`.
|
|
61
|
+
|
|
62
|
+
### `assembleOverflow`
|
|
63
|
+
|
|
64
|
+
Accepts `finishedLines`, `runningLines`, `queuedLine`, heading line, `maxBody` budget, `truncate`, and `theme`.
|
|
65
|
+
Handles the overflow path: budget-based prioritized assembly with an overflow indicator.
|
|
66
|
+
Returns the assembled `string[]`.
|
|
67
|
+
|
|
68
|
+
After extraction, `renderWidgetLines` becomes a thin orchestrator:
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
export function renderWidgetLines(params: { ... }): string[] {
|
|
72
|
+
const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
|
|
73
|
+
if (running.length === 0 && queued.length === 0 && finished.length === 0) return [];
|
|
74
|
+
|
|
75
|
+
const truncate = (line: string) => truncateToWidth(line, terminalWidth);
|
|
76
|
+
const heading = buildHeadingLine(running, queued, truncate, theme);
|
|
77
|
+
const sections = buildSections(running, queued, finished, activityMap, registry, spinnerFrame, theme, truncate);
|
|
78
|
+
const totalBody = sections.finishedLines.length + sections.runningLines.length * 2 + (sections.queuedLine ? 1 : 0);
|
|
79
|
+
|
|
80
|
+
if (totalBody <= MAX_WIDGET_LINES - 1) {
|
|
81
|
+
return assembleWithinBudget(heading, sections);
|
|
82
|
+
}
|
|
83
|
+
return assembleOverflow(heading, sections, MAX_WIDGET_LINES - 1, truncate, theme);
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Each helper is a pure function with a single concern and low branching.
|
|
88
|
+
|
|
89
|
+
## Module-Level Changes
|
|
90
|
+
|
|
91
|
+
### Changed: `src/ui/widget-renderer.ts`
|
|
92
|
+
|
|
93
|
+
- Add `categorizeAgents` (private) — extracts the three `agents.filter(...)` calls.
|
|
94
|
+
- Add `buildSections` (private) — extracts the three section-building loops.
|
|
95
|
+
- Add `assembleWithinBudget` (private) — extracts the non-overflow assembly + connector fixup.
|
|
96
|
+
- Add `assembleOverflow` (private) — extracts the overflow-budget assembly + indicator line.
|
|
97
|
+
- Simplify `renderWidgetLines` to a thin orchestrator calling the four helpers.
|
|
98
|
+
|
|
99
|
+
No exports are added, removed, or renamed.
|
|
100
|
+
No other files change.
|
|
101
|
+
|
|
102
|
+
## Test Impact Analysis
|
|
103
|
+
|
|
104
|
+
1. No new unit tests for the extracted helpers are needed — they are private functions tested through `renderWidgetLines`.
|
|
105
|
+
The existing `renderWidgetLines` tests in `test/widget-renderer.test.ts` (8 tests) cover all branches: single running, mixed agents, filtered finished, overflow priority, empty arrays, dim heading.
|
|
106
|
+
2. No existing tests become redundant — they all exercise `renderWidgetLines` end-to-end, which is the correct level for assembly logic.
|
|
107
|
+
3. All existing tests must stay as-is — the extraction is purely internal.
|
|
108
|
+
|
|
109
|
+
## TDD Order
|
|
110
|
+
|
|
111
|
+
1. **Red → Green:** Extract `categorizeAgents` and call it from `renderWidgetLines`.
|
|
112
|
+
All existing tests pass (no behavior change).
|
|
113
|
+
Commit: `refactor: extract categorizeAgents from renderWidgetLines`
|
|
114
|
+
|
|
115
|
+
2. **Red → Green:** Extract `buildSections` and call it from `renderWidgetLines`.
|
|
116
|
+
All existing tests pass.
|
|
117
|
+
Commit: `refactor: extract buildSections from renderWidgetLines`
|
|
118
|
+
|
|
119
|
+
3. **Red → Green:** Extract `assembleWithinBudget` and call it from `renderWidgetLines`.
|
|
120
|
+
All existing tests pass.
|
|
121
|
+
Commit: `refactor: extract assembleWithinBudget from renderWidgetLines`
|
|
122
|
+
|
|
123
|
+
4. **Red → Green:** Extract `assembleOverflow` and call it from `renderWidgetLines`.
|
|
124
|
+
All existing tests pass.
|
|
125
|
+
Commit: `refactor: extract assembleOverflow from renderWidgetLines`
|
|
126
|
+
|
|
127
|
+
5. **Verify:** Run `pnpm run check` and `pnpm vitest run test/widget-renderer.test.ts` to confirm no regressions.
|
|
128
|
+
Commit: n/a (verification only).
|
|
129
|
+
|
|
130
|
+
## Risks and Mitigations
|
|
131
|
+
|
|
132
|
+
| Risk | Mitigation |
|
|
133
|
+
| --------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
134
|
+
| Tree-connector fixup logic is fragile (string replacement on Unicode chars) | Keep the fixup in `assembleWithinBudget` as-is — same logic, just relocated. Existing tests verify exact connector output. |
|
|
135
|
+
| Extracted helpers have many parameters | Accept a `sections` object from `buildSections` to bundle `finishedLines`, `runningLines`, `queuedLine` — avoids long parameter lists. |
|
|
136
|
+
| Intermediate commits break tests | Each extraction step is self-contained — the function body moves into a helper and the call site replaces it in the same commit. |
|
|
137
|
+
|
|
138
|
+
## Open Questions
|
|
139
|
+
|
|
140
|
+
None — the decomposition is purely mechanical extraction of existing code into named helpers.
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 206
|
|
3
|
+
issue_title: "Decompose showAgentDetail (cognitive 33)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Decompose `showAgentDetail`
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`showAgentDetail` in `ui/agent-config-editor.ts` has cognitive complexity 33 (CRITICAL per fallow health).
|
|
11
|
+
It interleaves menu-option computation, user-choice dispatch, and three inline action handlers (edit, delete, reset) in a single 67-line function.
|
|
12
|
+
`ejectAgent` in the same file has cognitive complexity 20 from its 14-branch frontmatter builder.
|
|
13
|
+
Phase 12, Step 2 targets cognitive complexity < 10 per function.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Extract menu-option computation and inline action handlers into separate functions, each with cognitive complexity < 10.
|
|
18
|
+
- Extract frontmatter building from `ejectAgent` into a pure function.
|
|
19
|
+
- Preserve all existing behavior — no user-visible or API changes.
|
|
20
|
+
- Add unit tests for the two new exported pure functions.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Decomposing `renderWidgetLines` (#205), `update` (#207), or shared test fixtures (#208) — sibling Phase 12 steps.
|
|
25
|
+
- Changing the menu structure, option labels, or action semantics.
|
|
26
|
+
- Decomposing `disableAgent` or `enableAgent` — their cognitive complexity is already manageable (< 15).
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
`agent-config-editor.ts` was extracted from `agent-menu.ts` in Phase 8 (#136).
|
|
31
|
+
The file exposes a single factory `createAgentConfigEditor` that returns `{ showAgentDetail }`.
|
|
32
|
+
Internally the factory closes over `fileOps`, `registry`, `personalAgentsDir`, and `projectAgentsDir`.
|
|
33
|
+
|
|
34
|
+
Three action handlers already exist as closure-level functions: `ejectAgent`, `disableAgent`, `enableAgent`.
|
|
35
|
+
The remaining three actions — Edit, Delete, and Reset to default — are inlined in `showAgentDetail`'s if/else dispatch chain.
|
|
36
|
+
|
|
37
|
+
The existing test suite (`test/ui/agent-config-editor.test.ts`, 18 tests) covers all menu-option combinations and action branches through the public `showAgentDetail` entry point.
|
|
38
|
+
|
|
39
|
+
### Complexity sources
|
|
40
|
+
|
|
41
|
+
`showAgentDetail` (cognitive 33):
|
|
42
|
+
|
|
43
|
+
1. Menu-option building — 4-branch if/else chain with 3 boolean conditions (`disabled`, `isDefault`, `file`).
|
|
44
|
+
2. Action dispatch — 6-branch if/else chain based on `choice`.
|
|
45
|
+
3. Inline Edit handler — 3 nested `if` guards (`file`, `content`, `edited !== content`).
|
|
46
|
+
4. Inline Delete handler — nested `if (file)` + `if (confirmed)`.
|
|
47
|
+
5. Inline Reset handler — nested `if (file)` + `if (confirmed)`.
|
|
48
|
+
|
|
49
|
+
`ejectAgent` (cognitive 20):
|
|
50
|
+
|
|
51
|
+
1. Location selection + overwrite check — 2 early returns with nested ifs.
|
|
52
|
+
2. Frontmatter field building — 14 conditional `if`/`else if` branches.
|
|
53
|
+
|
|
54
|
+
## Design Overview
|
|
55
|
+
|
|
56
|
+
### Extracted from `showAgentDetail`
|
|
57
|
+
|
|
58
|
+
#### `buildMenuOptions` (exported, pure)
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
export function buildMenuOptions(
|
|
62
|
+
cfg: { isDefault?: boolean; enabled?: boolean },
|
|
63
|
+
file: string | undefined,
|
|
64
|
+
): string[]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Accepts the minimal config shape and file path.
|
|
68
|
+
Returns the menu option array.
|
|
69
|
+
Pure computation — no IO, no side effects.
|
|
70
|
+
Exported for direct unit testing.
|
|
71
|
+
|
|
72
|
+
#### `handleEdit` (closure-internal)
|
|
73
|
+
|
|
74
|
+
Handles the Edit action: reads the file, opens the editor, writes if changed.
|
|
75
|
+
Signature: `(ui: MenuUI, name: string, file: string) => Promise<void>`.
|
|
76
|
+
Called only when `file` is defined (guaranteed by menu-option construction).
|
|
77
|
+
|
|
78
|
+
#### `handleDelete` (closure-internal)
|
|
79
|
+
|
|
80
|
+
Handles the Delete action: confirms with user, removes file, reloads registry.
|
|
81
|
+
Signature: `(ui: MenuUI, name: string, file: string) => Promise<void>`.
|
|
82
|
+
|
|
83
|
+
#### `handleReset` (closure-internal)
|
|
84
|
+
|
|
85
|
+
Handles the Reset to default action: confirms, removes override file, reloads registry.
|
|
86
|
+
Signature: `(ui: MenuUI, name: string, file: string) => Promise<void>`.
|
|
87
|
+
|
|
88
|
+
### Extracted from `ejectAgent`
|
|
89
|
+
|
|
90
|
+
#### `buildEjectContent` (exported, pure)
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
export function buildEjectContent(cfg: AgentConfig): string
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Builds the full `.md` file content (frontmatter + system prompt) for an ejected agent.
|
|
97
|
+
Pure function — no IO.
|
|
98
|
+
Exported for direct unit testing.
|
|
99
|
+
|
|
100
|
+
### After refactoring
|
|
101
|
+
|
|
102
|
+
`showAgentDetail` becomes a thin orchestrator (~15 lines):
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
async function showAgentDetail(ui: MenuUI, name: string) {
|
|
106
|
+
if (registry.resolveType(name) == null) {
|
|
107
|
+
ui.notify(`Agent config not found for "${name}".`, "warning");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const cfg = registry.resolveAgentConfig(name);
|
|
111
|
+
const file = fileOps.findAgentFile(name, agentDirs());
|
|
112
|
+
|
|
113
|
+
const choice = await ui.select(name, buildMenuOptions(cfg, file));
|
|
114
|
+
if (!choice || choice === "Back") return;
|
|
115
|
+
|
|
116
|
+
if (choice === "Edit" && file) await handleEdit(ui, name, file);
|
|
117
|
+
else if (choice === "Delete" && file) await handleDelete(ui, name, file);
|
|
118
|
+
else if (choice === "Reset to default" && file) await handleReset(ui, name, file);
|
|
119
|
+
else if (choice.startsWith("Eject")) await ejectAgent(ui, name, cfg);
|
|
120
|
+
else if (choice === "Disable") await disableAgent(ui, name);
|
|
121
|
+
else if (choice === "Enable") await enableAgent(ui, name);
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Cognitive complexity: ~5 (one null-check early return + flat dispatch chain with no nesting).
|
|
126
|
+
|
|
127
|
+
`ejectAgent` becomes:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
async function ejectAgent(ui: MenuUI, name: string, cfg: AgentConfig) {
|
|
131
|
+
const location = await ui.select("Choose location", [...]);
|
|
132
|
+
if (!location) return;
|
|
133
|
+
const targetDir = location.startsWith("Project") ? projectAgentsDir : personalAgentsDir;
|
|
134
|
+
const targetPath = join(targetDir, `${name}.md`);
|
|
135
|
+
if (fileOps.exists(targetPath)) {
|
|
136
|
+
const overwrite = await ui.confirm("Overwrite", `${targetPath} already exists. Overwrite?`);
|
|
137
|
+
if (!overwrite) return;
|
|
138
|
+
}
|
|
139
|
+
fileOps.write(targetPath, buildEjectContent(cfg));
|
|
140
|
+
registry.reload();
|
|
141
|
+
ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Cognitive complexity: ~6 (two early returns, one ternary, one nested if).
|
|
146
|
+
|
|
147
|
+
## Module-Level Changes
|
|
148
|
+
|
|
149
|
+
### Changed: `src/ui/agent-config-editor.ts`
|
|
150
|
+
|
|
151
|
+
- Add exported `buildMenuOptions(cfg, file)` — pure function extracted from `showAgentDetail`.
|
|
152
|
+
- Add exported `buildEjectContent(cfg)` — pure function extracted from `ejectAgent`.
|
|
153
|
+
- Add closure-internal `handleEdit(ui, name, file)` — extracted from `showAgentDetail` inline logic.
|
|
154
|
+
- Add closure-internal `handleDelete(ui, name, file)` — extracted from `showAgentDetail` inline logic.
|
|
155
|
+
- Add closure-internal `handleReset(ui, name, file)` — extracted from `showAgentDetail` inline logic.
|
|
156
|
+
- Simplify `showAgentDetail` to orchestrate: resolve → build menu → select → dispatch.
|
|
157
|
+
- Simplify `ejectAgent` to delegate frontmatter building to `buildEjectContent`.
|
|
158
|
+
|
|
159
|
+
No exports are removed or renamed.
|
|
160
|
+
The public API (`createAgentConfigEditor` returning `{ showAgentDetail }`) is unchanged.
|
|
161
|
+
|
|
162
|
+
### Changed: `test/ui/agent-config-editor.test.ts`
|
|
163
|
+
|
|
164
|
+
- Add `describe("buildMenuOptions")` with tests for each menu-option combination (5 cases from existing tests, restructured as direct function calls).
|
|
165
|
+
- Add `describe("buildEjectContent")` with tests for minimal config and config with all optional fields.
|
|
166
|
+
- Existing `showAgentDetail` tests remain unchanged as integration coverage.
|
|
167
|
+
|
|
168
|
+
### Changed: `docs/architecture/architecture.md`
|
|
169
|
+
|
|
170
|
+
- Update the complexity hotspots table: `showAgentDetail` drops from 25/33 to ~5/5; `ejectAgent` drops from 21/20 to ~6/6.
|
|
171
|
+
|
|
172
|
+
## Test Impact Analysis
|
|
173
|
+
|
|
174
|
+
1. **New tests enabled:** Direct unit tests for `buildMenuOptions` (pure function with 5 state combinations) and `buildEjectContent` (pure function with many optional fields).
|
|
175
|
+
These were previously impossible to test in isolation because the logic was embedded in async UI flows.
|
|
176
|
+
2. **Existing tests that stay:** All 18 `showAgentDetail` tests remain as integration coverage — they exercise the full resolve → menu → dispatch → action pipeline.
|
|
177
|
+
3. **No tests become redundant:** The existing menu-option-structure tests (5 tests) overlap with `buildMenuOptions` unit tests, but they remain valuable as integration tests verifying the full flow produces the correct menu.
|
|
178
|
+
|
|
179
|
+
## TDD Order
|
|
180
|
+
|
|
181
|
+
1. **Red → Green:** Add `buildMenuOptions` unit tests (5 cases: default no-file, default with-file, custom with-file, disabled-default with-file, disabled-custom with-file).
|
|
182
|
+
Export `buildMenuOptions` as a pure function.
|
|
183
|
+
Extract the menu-option computation from `showAgentDetail` into it.
|
|
184
|
+
Verify all existing tests pass.
|
|
185
|
+
Commit: `refactor: extract buildMenuOptions from showAgentDetail`
|
|
186
|
+
|
|
187
|
+
2. **Red → Green:** Extract `handleEdit`, `handleDelete`, `handleReset` as closure-internal functions.
|
|
188
|
+
Simplify `showAgentDetail` dispatch to a flat if/else chain calling named handlers.
|
|
189
|
+
Verify all existing tests pass.
|
|
190
|
+
Commit: `refactor: extract inline handlers from showAgentDetail`
|
|
191
|
+
|
|
192
|
+
3. **Red → Green:** Add `buildEjectContent` unit tests (minimal config, config with all optional fields, config with array extensions/skills).
|
|
193
|
+
Export `buildEjectContent` as a pure function.
|
|
194
|
+
Extract the frontmatter-building logic from `ejectAgent` into it.
|
|
195
|
+
Verify all existing tests pass.
|
|
196
|
+
Commit: `refactor: extract buildEjectContent from ejectAgent`
|
|
197
|
+
|
|
198
|
+
4. **Docs:** Update the complexity hotspots table in `docs/architecture/architecture.md`.
|
|
199
|
+
Commit: `docs: update complexity table after showAgentDetail decomposition`
|
|
200
|
+
|
|
201
|
+
## Risks and Mitigations
|
|
202
|
+
|
|
203
|
+
| Risk | Mitigation |
|
|
204
|
+
| ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- |
|
|
205
|
+
| `buildMenuOptions` return order must match existing select mock expectations | Unit tests verify exact array equality; integration tests remain as safety net. |
|
|
206
|
+
| `buildEjectContent` frontmatter field ordering is load-bearing for eject tests | Unit tests verify the full content string; existing eject integration test uses `stringContaining` (flexible). |
|
|
207
|
+
| Closure-internal handlers share mutable `fileOps`/`registry` references | No change from current behavior — they already close over these references. |
|
|
208
|
+
|
|
209
|
+
## Open Questions
|
|
210
|
+
|
|
211
|
+
None — the decomposition is mechanical extraction of existing code into named functions.
|
|
@@ -40,3 +40,34 @@ All type-check, lint, and dead-code gates pass clean.
|
|
|
40
40
|
- `AgentManager` structurally satisfies `AgentMenuManager` with no adapter closures — confirmed by `pnpm run check` passing immediately.
|
|
41
41
|
- The `agent-menu.test.ts` refactor replaced `Partial<AgentMenuDeps>` overrides with a `makeHandler(opts)` helper that returns both the handler and collaborator stubs, which is cleaner for assertion.
|
|
42
42
|
- `rumdl` emitted 3 warnings in `pnpm run lint` — these are pre-existing and unrelated to this change (lint passes for markdown linting, the warnings are from biome/eslint steps that auto-fixed nothing).
|
|
43
|
+
|
|
44
|
+
## Stage: Final Retrospective (2026-05-25T15:04:47Z)
|
|
45
|
+
|
|
46
|
+
### Session summary
|
|
47
|
+
|
|
48
|
+
Completed all three stages (planning, TDD, shipping) in one sitting.
|
|
49
|
+
Issue #196 shipped as `pi-subagents-v7.2.5`.
|
|
50
|
+
All closure factories in pi-subagents are now classes; Phase 11 (Layers 3 + 4) is complete.
|
|
51
|
+
|
|
52
|
+
### Observations
|
|
53
|
+
|
|
54
|
+
#### What went well
|
|
55
|
+
|
|
56
|
+
- The three-session lifecycle (plan → TDD → ship) completed cleanly in a single sitting with no user corrections.
|
|
57
|
+
- Structural typing confirmation during planning paid off — `AgentManager` satisfied `AgentMenuManager` without adapter closures, and `pnpm run check` passed immediately after the wiring change.
|
|
58
|
+
- The `makeHandler(opts)` test helper pattern (returning handler + collaborator stubs) was cleaner than the `Partial<AgentMenuDeps>` spread approach it replaced.
|
|
59
|
+
|
|
60
|
+
#### What caused friction (agent side)
|
|
61
|
+
|
|
62
|
+
- `wrong-abstraction` — The plan separated factory removal (step 3) from call-site update (step 5), even though the testing skill already has a rule: "When a TDD step changes an interface that has a single call site, the step must include updating that call site."
|
|
63
|
+
The planner treated this as a testing concern and didn't apply it during plan authoring.
|
|
64
|
+
Impact: steps 3 and 5 had to be merged at implementation time, producing a commit message explaining the deviation.
|
|
65
|
+
Added a cross-reference to `plan-issue.md`.
|
|
66
|
+
|
|
67
|
+
#### What caused friction (user side)
|
|
68
|
+
|
|
69
|
+
Nothing notable — the user's issue was well-specified and the three `/` commands ran without intervention.
|
|
70
|
+
|
|
71
|
+
### Changes made
|
|
72
|
+
|
|
73
|
+
1. Added single-call-site rule to `.pi/prompts/plan-issue.md` TDD Order section: when a step removes a factory/export with one call site, include the call-site update in the same step.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 205
|
|
3
|
+
issue_title: "Decompose renderWidgetLines (cognitive 44)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #205 — Decompose renderWidgetLines
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-25T15:27:45Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned the decomposition of `renderWidgetLines` (cognitive complexity 44) into four private helper functions: `categorizeAgents`, `buildSections`, `assembleWithinBudget`, and `assembleOverflow`.
|
|
13
|
+
Also updated `architecture.md` Phase 12 steps with issue links (#205–#208) and added a Phase 12 row to the structural refactoring issues table.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The function's complexity comes from five interwoven concerns (categorization, section building, heading, non-overflow assembly with connector fixup, overflow-budget assembly) — but the extraction is mechanical since all logic is already pure and stateless.
|
|
18
|
+
- No new tests are needed — the existing 8 tests in `widget-renderer.test.ts` cover all branches end-to-end and remain the correct test level for assembly logic.
|
|
19
|
+
- The tree-connector fixup (swapping `├─` → `└─` via string replacement on Unicode chars) is the most fragile part; it stays as-is inside `assembleWithinBudget` rather than being further decomposed.
|
|
20
|
+
- A `sections` return object from `buildSections` bundles `finishedLines`, `runningLines`, and `queuedLine` to avoid long parameter lists on the assembly helpers.
|
|
21
|
+
|
|
22
|
+
## Stage: Implementation — TDD (2026-05-25T15:35:10Z)
|
|
23
|
+
|
|
24
|
+
### Session summary
|
|
25
|
+
|
|
26
|
+
Completed all four TDD steps: extracted `categorizeAgents`, `buildSections`, `assembleWithinBudget`, and `assembleOverflow` from `renderWidgetLines`.
|
|
27
|
+
All 23 `widget-renderer.test.ts` tests pass throughout; no new tests were added.
|
|
28
|
+
Full suite (856 tests, 54 files) is green; type check and lint are clean.
|
|
29
|
+
|
|
30
|
+
### Observations
|
|
31
|
+
|
|
32
|
+
- **Stray backtick during `assembleOverflow` extraction:** The Edit tool introduced a double-backtick (`\`\``) at the end of the nested template literal on the overflow indicator line — the inner template literal's closing backtick concatenated with the outer template's closing backtick, creating a parse error.
|
|
33
|
+
Required Python-based line-level surgery to fix since the Edit tool cannot reliably match nested template literals through JSON escaping.
|
|
34
|
+
- The `renderWidgetLines` `else` block removal also required Python because the Edit tool's `oldText` matching is unreliable when the target contains nested template literals with backticks.
|
|
35
|
+
- Aside from the template-literal matching friction, all extractions were purely mechanical; no logic changes were needed.
|
|
36
|
+
- The final `renderWidgetLines` is a clean 12-line orchestrator; each helper is well under complexity 10.
|
|
37
|
+
|
|
38
|
+
## Stage: Final Retrospective (2026-05-25T15:41:48Z)
|
|
39
|
+
|
|
40
|
+
### Session summary
|
|
41
|
+
|
|
42
|
+
All three stages (planning, TDD implementation, shipping) completed in a single session.
|
|
43
|
+
Four refactor commits extracted `categorizeAgents`, `buildSections`, `assembleWithinBudget`, and `assembleOverflow` from `renderWidgetLines`, reducing cognitive complexity from 44 to <10 per function.
|
|
44
|
+
Released as `pi-subagents-v7.2.6`.
|
|
45
|
+
|
|
46
|
+
### Observations
|
|
47
|
+
|
|
48
|
+
#### What went well
|
|
49
|
+
|
|
50
|
+
- The planning-through-shipping pipeline was efficient: plan → 4 TDD steps → ship → release in one session with no rework.
|
|
51
|
+
- The plan correctly identified all four extraction targets and ordered TDD steps to avoid intermediate breakage.
|
|
52
|
+
- All 23 existing `widget-renderer.test.ts` tests passed throughout with zero modifications — the existing test coverage was at the right abstraction level for this refactoring.
|
|
53
|
+
- The `architecture.md` Phase 12 update (issue links, refactoring table row) was a clean opportunistic addition.
|
|
54
|
+
|
|
55
|
+
#### What caused friction (agent side)
|
|
56
|
+
|
|
57
|
+
- `other` (Edit tool limitation) — The Edit tool introduced a stray double-backtick when inserting `assembleOverflow`'s body, caused by JSON escaping colliding with nested template literal backticks.
|
|
58
|
+
The same limitation then prevented matching `oldText` containing nested template literals in the `renderWidgetLines` overflow block.
|
|
59
|
+
Required two Python-based line-level fixes via bash.
|
|
60
|
+
Impact: ~3 extra tool calls; self-identified and self-corrected.
|
|
61
|
+
|
|
62
|
+
#### What caused friction (user side)
|
|
63
|
+
|
|
64
|
+
- No friction observed.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 206
|
|
3
|
+
issue_title: "Decompose showAgentDetail (cognitive 33)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #206 — Decompose showAgentDetail (cognitive 33)
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-25T12:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 4-step plan to decompose `showAgentDetail` (cognitive 33) and `ejectAgent` (cognitive 20) in `ui/agent-config-editor.ts`.
|
|
13
|
+
The plan extracts two exported pure functions (`buildMenuOptions`, `buildEjectContent`) with dedicated unit tests, plus three closure-internal handlers (`handleEdit`, `handleDelete`, `handleReset`).
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- Three of the six action handlers (`ejectAgent`, `disableAgent`, `enableAgent`) were already extracted as closure functions — only Edit, Delete, and Reset were inlined in the dispatch chain.
|
|
18
|
+
- `buildMenuOptions` and `buildEjectContent` are ideal pure-function extractions: complex branching logic with no IO dependencies, previously untestable in isolation.
|
|
19
|
+
- The existing 18 integration tests through `showAgentDetail` provide a strong safety net — no risk of behavior regression during extraction.
|
|
20
|
+
- Chose to scope `ejectAgent` decomposition into this issue since the issue's outcome says "< 10 per function" and `ejectAgent` is at cognitive 20 in the same file.
|
|
21
|
+
- `disableAgent` and `enableAgent` were explicitly deferred — their cognitive complexity is manageable and decomposing them would add scope without meaningful benefit.
|
|
22
|
+
|
|
23
|
+
## Stage: Implementation — TDD (2026-05-25T11:55:00Z)
|
|
24
|
+
|
|
25
|
+
### Session summary
|
|
26
|
+
|
|
27
|
+
Completed all 4 TDD steps. 3 `refactor:` commits extract `buildMenuOptions`, the three inline handlers, and `buildEjectContent`; 1 `docs:` commit updates the architecture table.
|
|
28
|
+
Test count grew from 21 to 33 (+12 new unit tests for the two exported pure functions).
|
|
29
|
+
|
|
30
|
+
### Observations
|
|
31
|
+
|
|
32
|
+
- A `newText: null` bug in the Edit tool corrupted `agent-config-editor.ts` during step 1; recovered immediately by rewriting the file with `Write`.
|
|
33
|
+
- The test used `thinking: "auto"` which is not a valid `ThinkingLevel` — fixed by changing to `"low"` before the final commit; the type error was caught by `pnpm run check` after the TDD step.
|
|
34
|
+
- `buildMenuOptions` extracted cleanly with early-return style (no `let menuOptions` intermediate); the refactored function passes all 5 new unit tests and all 21 existing integration tests.
|
|
35
|
+
- `handleEdit`, `handleDelete`, and `handleReset` are closure-internal; they drop the outer `if (file)` guard since the menu only shows those options when `file` is defined.
|
|
36
|
+
- `buildEjectContent` extracted from `ejectAgent` reduces `ejectAgent` to a thin IO function (~10 lines); no behavior change verified by the existing eject integration tests.
|
package/package.json
CHANGED
|
@@ -12,6 +12,55 @@ import type { AgentConfig } from "#src/types";
|
|
|
12
12
|
import type { AgentFileOps } from "#src/ui/agent-file-ops";
|
|
13
13
|
import type { MenuUI } from "#src/ui/agent-menu";
|
|
14
14
|
|
|
15
|
+
// ---- Pure helpers ----
|
|
16
|
+
|
|
17
|
+
/** Compute the menu option list for the agent detail view. */
|
|
18
|
+
export function buildMenuOptions(
|
|
19
|
+
cfg: { isDefault?: boolean; enabled?: boolean },
|
|
20
|
+
file: string | undefined,
|
|
21
|
+
): string[] {
|
|
22
|
+
const isDefault = cfg.isDefault === true;
|
|
23
|
+
const disabled = cfg.enabled === false;
|
|
24
|
+
|
|
25
|
+
if (disabled && file) {
|
|
26
|
+
return isDefault
|
|
27
|
+
? ["Enable", "Edit", "Reset to default", "Delete", "Back"]
|
|
28
|
+
: ["Enable", "Edit", "Delete", "Back"];
|
|
29
|
+
}
|
|
30
|
+
if (isDefault && !file) {
|
|
31
|
+
return ["Eject (export as .md)", "Disable", "Back"];
|
|
32
|
+
}
|
|
33
|
+
if (isDefault && file) {
|
|
34
|
+
return ["Edit", "Disable", "Reset to default", "Delete", "Back"];
|
|
35
|
+
}
|
|
36
|
+
return ["Edit", "Disable", "Delete", "Back"];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Build the `.md` file content (frontmatter + system prompt) for an ejected agent. */
|
|
40
|
+
export function buildEjectContent(cfg: AgentConfig): string {
|
|
41
|
+
const fmFields: string[] = [];
|
|
42
|
+
fmFields.push(`description: ${cfg.description}`);
|
|
43
|
+
if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
|
|
44
|
+
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") ?? "all"}`);
|
|
45
|
+
if (cfg.model) fmFields.push(`model: ${cfg.model}`);
|
|
46
|
+
if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
|
|
47
|
+
if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
|
|
48
|
+
fmFields.push(`prompt_mode: ${cfg.promptMode}`);
|
|
49
|
+
if (cfg.extensions === false) fmFields.push("extensions: false");
|
|
50
|
+
else if (Array.isArray(cfg.extensions))
|
|
51
|
+
fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
|
|
52
|
+
if (cfg.skills === false) fmFields.push("skills: false");
|
|
53
|
+
else if (Array.isArray(cfg.skills))
|
|
54
|
+
fmFields.push(`skills: ${cfg.skills.join(", ")}`);
|
|
55
|
+
if (cfg.disallowedTools?.length)
|
|
56
|
+
fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
|
|
57
|
+
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
58
|
+
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
59
|
+
if (cfg.isolated) fmFields.push("isolated: true");
|
|
60
|
+
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
|
|
61
|
+
return `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
62
|
+
}
|
|
63
|
+
|
|
15
64
|
// ---- Factory ----
|
|
16
65
|
|
|
17
66
|
export function createAgentConfigEditor(
|
|
@@ -30,65 +79,52 @@ export function createAgentConfigEditor(
|
|
|
30
79
|
return;
|
|
31
80
|
}
|
|
32
81
|
const cfg = registry.resolveAgentConfig(name);
|
|
33
|
-
|
|
34
82
|
const file = fileOps.findAgentFile(name, agentDirs());
|
|
35
|
-
const isDefault = cfg.isDefault === true;
|
|
36
|
-
const disabled = cfg.enabled === false;
|
|
37
|
-
|
|
38
|
-
let menuOptions: string[];
|
|
39
|
-
if (disabled && file) {
|
|
40
|
-
menuOptions = isDefault
|
|
41
|
-
? ["Enable", "Edit", "Reset to default", "Delete", "Back"]
|
|
42
|
-
: ["Enable", "Edit", "Delete", "Back"];
|
|
43
|
-
} else if (isDefault && !file) {
|
|
44
|
-
menuOptions = ["Eject (export as .md)", "Disable", "Back"];
|
|
45
|
-
} else if (isDefault && file) {
|
|
46
|
-
menuOptions = ["Edit", "Disable", "Reset to default", "Delete", "Back"];
|
|
47
|
-
} else {
|
|
48
|
-
menuOptions = ["Edit", "Disable", "Delete", "Back"];
|
|
49
|
-
}
|
|
50
83
|
|
|
51
|
-
const choice = await ui.select(name,
|
|
84
|
+
const choice = await ui.select(name, buildMenuOptions(cfg, file));
|
|
52
85
|
if (!choice || choice === "Back") return;
|
|
53
86
|
|
|
54
|
-
if (choice === "Edit" && file)
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
if (choice === "Edit" && file) await handleEdit(ui, name, file);
|
|
88
|
+
else if (choice === "Delete" && file) await handleDelete(ui, name, file);
|
|
89
|
+
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);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function handleEdit(ui: MenuUI, name: string, file: string) {
|
|
97
|
+
const content = fileOps.read(file);
|
|
98
|
+
if (content === undefined) return;
|
|
99
|
+
const edited = await ui.editor(`Edit ${name}`, content);
|
|
100
|
+
if (edited !== undefined && edited !== content) {
|
|
101
|
+
fileOps.write(file, edited);
|
|
102
|
+
registry.reload();
|
|
103
|
+
ui.notify(`Updated ${file}`, "info");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function handleDelete(ui: MenuUI, name: string, file: string) {
|
|
108
|
+
const confirmed = await ui.confirm(
|
|
109
|
+
"Delete agent",
|
|
110
|
+
`Delete ${name} (${file})?`,
|
|
111
|
+
);
|
|
112
|
+
if (confirmed) {
|
|
113
|
+
fileOps.remove(file);
|
|
114
|
+
registry.reload();
|
|
115
|
+
ui.notify(`Deleted ${file}`, "info");
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function handleReset(ui: MenuUI, name: string, file: string) {
|
|
120
|
+
const confirmed = await ui.confirm(
|
|
121
|
+
"Reset to default",
|
|
122
|
+
`Delete override ${file} and restore embedded default?`,
|
|
123
|
+
);
|
|
124
|
+
if (confirmed) {
|
|
125
|
+
fileOps.remove(file);
|
|
126
|
+
registry.reload();
|
|
127
|
+
ui.notify(`Restored default ${name}`, "info");
|
|
92
128
|
}
|
|
93
129
|
}
|
|
94
130
|
|
|
@@ -112,30 +148,7 @@ export function createAgentConfigEditor(
|
|
|
112
148
|
if (!overwrite) return;
|
|
113
149
|
}
|
|
114
150
|
|
|
115
|
-
|
|
116
|
-
fmFields.push(`description: ${cfg.description}`);
|
|
117
|
-
if (cfg.displayName) fmFields.push(`display_name: ${cfg.displayName}`);
|
|
118
|
-
fmFields.push(`tools: ${cfg.builtinToolNames?.join(", ") ?? "all"}`);
|
|
119
|
-
if (cfg.model) fmFields.push(`model: ${cfg.model}`);
|
|
120
|
-
if (cfg.thinking) fmFields.push(`thinking: ${cfg.thinking}`);
|
|
121
|
-
if (cfg.maxTurns) fmFields.push(`max_turns: ${cfg.maxTurns}`);
|
|
122
|
-
fmFields.push(`prompt_mode: ${cfg.promptMode}`);
|
|
123
|
-
if (cfg.extensions === false) fmFields.push("extensions: false");
|
|
124
|
-
else if (Array.isArray(cfg.extensions))
|
|
125
|
-
fmFields.push(`extensions: ${cfg.extensions.join(", ")}`);
|
|
126
|
-
if (cfg.skills === false) fmFields.push("skills: false");
|
|
127
|
-
else if (Array.isArray(cfg.skills))
|
|
128
|
-
fmFields.push(`skills: ${cfg.skills.join(", ")}`);
|
|
129
|
-
if (cfg.disallowedTools?.length)
|
|
130
|
-
fmFields.push(`disallowed_tools: ${cfg.disallowedTools.join(", ")}`);
|
|
131
|
-
if (cfg.inheritContext) fmFields.push("inherit_context: true");
|
|
132
|
-
if (cfg.runInBackground) fmFields.push("run_in_background: true");
|
|
133
|
-
if (cfg.isolated) fmFields.push("isolated: true");
|
|
134
|
-
if (cfg.isolation) fmFields.push(`isolation: ${cfg.isolation}`);
|
|
135
|
-
|
|
136
|
-
const content = `---\n${fmFields.join("\n")}\n---\n\n${cfg.systemPrompt}\n`;
|
|
137
|
-
|
|
138
|
-
fileOps.write(targetPath, content);
|
|
151
|
+
fileOps.write(targetPath, buildEjectContent(cfg));
|
|
139
152
|
registry.reload();
|
|
140
153
|
ui.notify(`Ejected ${name} to ${targetPath}`, "info");
|
|
141
154
|
}
|
|
@@ -127,6 +127,135 @@ export function renderRunningLines(
|
|
|
127
127
|
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
128
128
|
const MAX_WIDGET_LINES = 12;
|
|
129
129
|
|
|
130
|
+
interface AgentCategories {
|
|
131
|
+
running: WidgetAgent[];
|
|
132
|
+
queued: WidgetAgent[];
|
|
133
|
+
finished: WidgetAgent[];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Partition agents into rendering buckets. */
|
|
137
|
+
function categorizeAgents(
|
|
138
|
+
agents: readonly WidgetAgent[],
|
|
139
|
+
shouldShowFinished: (agentId: string, status: string) => boolean,
|
|
140
|
+
): AgentCategories {
|
|
141
|
+
return {
|
|
142
|
+
running: agents.filter(a => a.status === "running"),
|
|
143
|
+
queued: agents.filter(a => a.status === "queued"),
|
|
144
|
+
finished: agents.filter(
|
|
145
|
+
a => a.status !== "running" && a.status !== "queued" && a.completedAt != null
|
|
146
|
+
&& shouldShowFinished(a.id, a.status),
|
|
147
|
+
),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface WidgetSections {
|
|
152
|
+
finishedLines: string[];
|
|
153
|
+
runningLines: [string, string][];
|
|
154
|
+
queuedLine: string | undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Render each agent bucket into pre-formatted lines with ├─ tree connectors. */
|
|
158
|
+
function buildSections(
|
|
159
|
+
categories: AgentCategories,
|
|
160
|
+
activityMap: ReadonlyMap<string, WidgetActivity>,
|
|
161
|
+
registry: AgentConfigLookup,
|
|
162
|
+
spinnerFrame: number,
|
|
163
|
+
theme: Theme,
|
|
164
|
+
truncate: (line: string) => string,
|
|
165
|
+
): WidgetSections {
|
|
166
|
+
const finishedLines: string[] = [];
|
|
167
|
+
for (const a of categories.finished) {
|
|
168
|
+
finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500") + " " + renderFinishedLine(a, activityMap.get(a.id), registry, theme)));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const runningLines: [string, string][] = [];
|
|
172
|
+
for (const a of categories.running) {
|
|
173
|
+
const [header, act] = renderRunningLines(a, activityMap.get(a.id), registry, spinnerFrame, theme);
|
|
174
|
+
runningLines.push([
|
|
175
|
+
truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
|
|
176
|
+
truncate(theme.fg("dim", "\u2502 ") + act),
|
|
177
|
+
]);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const queuedLine = categories.queued.length > 0
|
|
181
|
+
? truncate(theme.fg("dim", "\u251C\u2500") + ` ${theme.fg("muted", "\u25E6")} ${theme.fg("dim", `${categories.queued.length} queued`)}`)
|
|
182
|
+
: undefined;
|
|
183
|
+
|
|
184
|
+
return { finishedLines, runningLines, queuedLine };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Assemble widget lines when total body fits within MAX_WIDGET_LINES.
|
|
189
|
+
* Fixes the last tree connector: ├─ → └─, and │ → space for the running-agent activity line.
|
|
190
|
+
*/
|
|
191
|
+
function assembleWithinBudget(heading: string, sections: WidgetSections): string[] {
|
|
192
|
+
const { finishedLines, runningLines, queuedLine } = sections;
|
|
193
|
+
const lines: string[] = [heading, ...finishedLines];
|
|
194
|
+
for (const pair of runningLines) lines.push(...pair);
|
|
195
|
+
if (queuedLine) lines.push(queuedLine);
|
|
196
|
+
|
|
197
|
+
// Fix last connector: swap \u251C\u2500 \u2192 \u2514\u2500.
|
|
198
|
+
if (lines.length > 1) {
|
|
199
|
+
const last = lines.length - 1;
|
|
200
|
+
lines[last] = lines[last].replace("\u251C\u2500", "\u2514\u2500");
|
|
201
|
+
if (runningLines.length > 0 && !queuedLine) {
|
|
202
|
+
if (last >= 2) {
|
|
203
|
+
lines[last - 1] = lines[last - 1].replace("\u251C\u2500", "\u2514\u2500");
|
|
204
|
+
lines[last] = lines[last].replace("\u2502 ", " ");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return lines;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Assemble widget lines when total body exceeds MAX_WIDGET_LINES.
|
|
213
|
+
* Prioritizes running > queued > finished and appends an overflow indicator.
|
|
214
|
+
*/
|
|
215
|
+
function assembleOverflow(
|
|
216
|
+
heading: string,
|
|
217
|
+
sections: WidgetSections,
|
|
218
|
+
maxBody: number,
|
|
219
|
+
truncate: (line: string) => string,
|
|
220
|
+
theme: Theme,
|
|
221
|
+
): string[] {
|
|
222
|
+
const { finishedLines, runningLines, queuedLine } = sections;
|
|
223
|
+
const lines: string[] = [heading];
|
|
224
|
+
let budget = maxBody - 1;
|
|
225
|
+
let hiddenRunning = 0;
|
|
226
|
+
let hiddenFinished = 0;
|
|
227
|
+
|
|
228
|
+
for (const pair of runningLines) {
|
|
229
|
+
if (budget >= 2) {
|
|
230
|
+
lines.push(...pair);
|
|
231
|
+
budget -= 2;
|
|
232
|
+
} else {
|
|
233
|
+
hiddenRunning++;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
if (queuedLine && budget >= 1) {
|
|
238
|
+
lines.push(queuedLine);
|
|
239
|
+
budget--;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const fl of finishedLines) {
|
|
243
|
+
if (budget >= 1) {
|
|
244
|
+
lines.push(fl);
|
|
245
|
+
budget--;
|
|
246
|
+
} else {
|
|
247
|
+
hiddenFinished++;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const overflowParts: string[] = [];
|
|
252
|
+
if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
|
|
253
|
+
if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
|
|
254
|
+
const overflowText = overflowParts.join(", ");
|
|
255
|
+
lines.push(truncate(theme.fg("dim", "\u2514\u2500") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`));
|
|
256
|
+
return lines;
|
|
257
|
+
}
|
|
258
|
+
|
|
130
259
|
/** Pure rendering of the widget body. Returns lines to display. */
|
|
131
260
|
export function renderWidgetLines(params: {
|
|
132
261
|
agents: readonly WidgetAgent[];
|
|
@@ -139,12 +268,7 @@ export function renderWidgetLines(params: {
|
|
|
139
268
|
}): string[] {
|
|
140
269
|
const { agents, activityMap, registry, spinnerFrame, terminalWidth, theme, shouldShowFinished } = params;
|
|
141
270
|
|
|
142
|
-
const running
|
|
143
|
-
const queued = agents.filter(a => a.status === "queued");
|
|
144
|
-
const finished = agents.filter(a =>
|
|
145
|
-
a.status !== "running" && a.status !== "queued" && a.completedAt
|
|
146
|
-
&& shouldShowFinished(a.id, a.status),
|
|
147
|
-
);
|
|
271
|
+
const { running, queued, finished } = categorizeAgents(agents, shouldShowFinished);
|
|
148
272
|
|
|
149
273
|
const hasActive = running.length > 0 || queued.length > 0;
|
|
150
274
|
const hasFinished = finished.length > 0;
|
|
@@ -155,82 +279,22 @@ export function renderWidgetLines(params: {
|
|
|
155
279
|
const headingColor = hasActive ? "accent" : "dim";
|
|
156
280
|
const headingIcon = hasActive ? "\u25CF" : "\u25CB";
|
|
157
281
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const [header, act] = renderRunningLines(a, activityMap.get(a.id), registry, spinnerFrame, theme);
|
|
167
|
-
runningLines.push([
|
|
168
|
-
truncate(theme.fg("dim", "\u251C\u2500") + ` ${header}`),
|
|
169
|
-
truncate(theme.fg("dim", "\u2502 ") + act),
|
|
170
|
-
]);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const queuedLine = queued.length > 0
|
|
174
|
-
? truncate(theme.fg("dim", "\u251C\u2500") + ` ${theme.fg("muted", "\u25E6")} ${theme.fg("dim", `${queued.length} queued`)}`)
|
|
175
|
-
: undefined;
|
|
282
|
+
const { finishedLines, runningLines, queuedLine } = buildSections(
|
|
283
|
+
{ running, queued, finished },
|
|
284
|
+
activityMap,
|
|
285
|
+
registry,
|
|
286
|
+
spinnerFrame,
|
|
287
|
+
theme,
|
|
288
|
+
truncate,
|
|
289
|
+
);
|
|
176
290
|
|
|
177
291
|
// Assemble with overflow cap (heading takes 1 line).
|
|
178
292
|
const maxBody = MAX_WIDGET_LINES - 1;
|
|
179
293
|
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
180
|
-
|
|
181
|
-
const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
|
|
294
|
+
const heading = truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"));
|
|
182
295
|
|
|
183
296
|
if (totalBody <= maxBody) {
|
|
184
|
-
|
|
185
|
-
for (const pair of runningLines) lines.push(...pair);
|
|
186
|
-
if (queuedLine) lines.push(queuedLine);
|
|
187
|
-
|
|
188
|
-
// Fix last connector: swap \u251C\u2500 \u2192 \u2514\u2500 and \u2502 \u2192 space for activity lines.
|
|
189
|
-
if (lines.length > 1) {
|
|
190
|
-
const last = lines.length - 1;
|
|
191
|
-
lines[last] = lines[last].replace("\u251C\u2500", "\u2514\u2500");
|
|
192
|
-
if (runningLines.length > 0 && !queuedLine) {
|
|
193
|
-
if (last >= 2) {
|
|
194
|
-
lines[last - 1] = lines[last - 1].replace("\u251C\u2500", "\u2514\u2500");
|
|
195
|
-
lines[last] = lines[last].replace("\u2502 ", " ");
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
} else {
|
|
200
|
-
// Overflow — prioritize: running > queued > finished.
|
|
201
|
-
let budget = maxBody - 1;
|
|
202
|
-
let hiddenRunning = 0;
|
|
203
|
-
let hiddenFinished = 0;
|
|
204
|
-
|
|
205
|
-
for (const pair of runningLines) {
|
|
206
|
-
if (budget >= 2) {
|
|
207
|
-
lines.push(...pair);
|
|
208
|
-
budget -= 2;
|
|
209
|
-
} else {
|
|
210
|
-
hiddenRunning++;
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (queuedLine && budget >= 1) {
|
|
215
|
-
lines.push(queuedLine);
|
|
216
|
-
budget--;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
for (const fl of finishedLines) {
|
|
220
|
-
if (budget >= 1) {
|
|
221
|
-
lines.push(fl);
|
|
222
|
-
budget--;
|
|
223
|
-
} else {
|
|
224
|
-
hiddenFinished++;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const overflowParts: string[] = [];
|
|
229
|
-
if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
|
|
230
|
-
if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
|
|
231
|
-
const overflowText = overflowParts.join(", ");
|
|
232
|
-
lines.push(truncate(theme.fg("dim", "\u2514\u2500") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`));
|
|
297
|
+
return assembleWithinBudget(heading, { finishedLines, runningLines, queuedLine });
|
|
233
298
|
}
|
|
234
|
-
|
|
235
|
-
return lines;
|
|
299
|
+
return assembleOverflow(heading, { finishedLines, runningLines, queuedLine }, maxBody, truncate, theme);
|
|
236
300
|
}
|