@gotgenes/pi-subagents 7.2.5 → 7.2.6
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 +11 -0
- package/docs/architecture/architecture.md +16 -151
- 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/retro/0196-convert-runner-menu-to-classes.md +31 -0
- package/docs/retro/0205-decompose-render-widget-lines.md +36 -0
- package/package.json +1 -1
- package/src/ui/widget-renderer.ts +141 -77
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ 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.6](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.5...pi-subagents-v7.2.6) (2026-05-25)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* archive Phase 11 to history, add Phase 11 to refactoring table ([2c617f2](https://github.com/gotgenes/pi-packages/commit/2c617f2c752ea935d55878ba58fce997985086b5))
|
|
14
|
+
* plan decompose renderWidgetLines ([#205](https://github.com/gotgenes/pi-packages/issues/205)) ([88d09cd](https://github.com/gotgenes/pi-packages/commit/88d09cdad53bc3f215c069b8d6da6a44e10b5af7))
|
|
15
|
+
* **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))
|
|
16
|
+
* **retro:** add retro notes for issue [#196](https://github.com/gotgenes/pi-packages/issues/196) ([cfc7d94](https://github.com/gotgenes/pi-packages/commit/cfc7d94f72b120a4550f73e2d1cf00822db759c2))
|
|
17
|
+
* **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))
|
|
18
|
+
|
|
8
19
|
## [7.2.5](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.4...pi-subagents-v7.2.5) (2026-05-25)
|
|
9
20
|
|
|
10
21
|
|
|
@@ -599,155 +599,18 @@ export type RunnerIO = EnvironmentIO & SessionFactoryIO;
|
|
|
599
599
|
`RunnerIO` is kept as a type alias for the intersection.
|
|
600
600
|
All existing consumers satisfy both sub-interfaces via structural typing with no call-site changes.
|
|
601
601
|
|
|
602
|
-
##
|
|
602
|
+
## Phase 11 (complete)
|
|
603
603
|
|
|
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.
|
|
604
|
+
Phase 11 converted all closure factories to classes, eliminating adapter closure density in `index.ts`.
|
|
605
|
+
Four layers: SessionContext typing → runtime query methods → interface alignment → class conversions → index.ts simplification.
|
|
606
|
+
See [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) for details.
|
|
744
607
|
|
|
745
608
|
## Improvement roadmap (Phase 12)
|
|
746
609
|
|
|
747
610
|
Phase 12 addresses the remaining fallow refactoring targets and test duplication.
|
|
748
611
|
These are independent of Phase 11 and can proceed in parallel if desired.
|
|
749
612
|
|
|
750
|
-
### Step 1: Decompose `renderWidgetLines` (cognitive 44)
|
|
613
|
+
### Step 1: Decompose `renderWidgetLines` (cognitive 44) — [#205]
|
|
751
614
|
|
|
752
615
|
`renderWidgetLines` in `ui/widget-renderer.ts` handles agent-status formatting, tree connectors, overflow, and empty states.
|
|
753
616
|
Extract per-status renderers and a tree-connector utility.
|
|
@@ -755,7 +618,7 @@ Extract per-status renderers and a tree-connector utility.
|
|
|
755
618
|
- Target: `src/ui/widget-renderer.ts`
|
|
756
619
|
- Outcome: cognitive complexity < 10
|
|
757
620
|
|
|
758
|
-
### Step 2: Decompose `showAgentDetail` (cognitive 33)
|
|
621
|
+
### Step 2: Decompose `showAgentDetail` (cognitive 33) — [#206]
|
|
759
622
|
|
|
760
623
|
`showAgentDetail` in `ui/agent-config-editor.ts` handles display, edit, eject, and delete flows.
|
|
761
624
|
Extract sub-functions per menu action.
|
|
@@ -763,7 +626,7 @@ Extract sub-functions per menu action.
|
|
|
763
626
|
- Target: `src/ui/agent-config-editor.ts`
|
|
764
627
|
- Outcome: cognitive complexity < 10
|
|
765
628
|
|
|
766
|
-
### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31)
|
|
629
|
+
### Step 3: Decompose `update` in `agent-widget.ts` (cognitive 31) — [#207]
|
|
767
630
|
|
|
768
631
|
`update` mixes timer lifecycle, agent list assembly, render delegation, and visibility state.
|
|
769
632
|
Extract `assembleWidgetState` (pure) and timer management.
|
|
@@ -771,7 +634,7 @@ Extract `assembleWidgetState` (pure) and timer management.
|
|
|
771
634
|
- Target: `src/ui/agent-widget.ts`
|
|
772
635
|
- Outcome: cognitive complexity < 10
|
|
773
636
|
|
|
774
|
-
### Step 4: Extract shared test fixtures
|
|
637
|
+
### Step 4: Extract shared test fixtures — [#208]
|
|
775
638
|
|
|
776
639
|
The 3 heaviest clone families:
|
|
777
640
|
|
|
@@ -786,7 +649,7 @@ Extract shared factories into `test/fixtures/` modules.
|
|
|
786
649
|
|
|
787
650
|
## Refactoring history
|
|
788
651
|
|
|
789
|
-
Phases 1–5 and 7–
|
|
652
|
+
Phases 1–5 and 7–11 are complete.
|
|
790
653
|
Phase 6 (UI extraction to a separate package) is deferred.
|
|
791
654
|
Detailed records are preserved in per-phase history files:
|
|
792
655
|
|
|
@@ -802,6 +665,7 @@ Detailed records are preserved in per-phase history files:
|
|
|
802
665
|
| 8 | Testability, display extraction, menu decomposition | Complete | [phase-8-testability.md](history/phase-8-testability.md) |
|
|
803
666
|
| 9 | Observation consolidation, ctx elimination | Complete | [phase-9-observation-ctx.md](history/phase-9-observation-ctx.md) |
|
|
804
667
|
| 10 | Domain organization, bag decomposition, complexity | Complete | [phase-10-structural-decomposition.md](history/phase-10-structural-decomposition.md) |
|
|
668
|
+
| 11 | Closure factories to classes | Complete | [phase-11-closure-to-class.md](history/phase-11-closure-to-class.md) |
|
|
805
669
|
|
|
806
670
|
### Structural refactoring issues
|
|
807
671
|
|
|
@@ -816,6 +680,8 @@ Detailed records are preserved in per-phase history files:
|
|
|
816
680
|
| Testability | #131, #132, #133, #134, #135, #136 | Shared fixtures, session-config IO, runner SDK boundary, as-any reduction, display extraction, menu decomposition |
|
|
817
681
|
| Observation/ctx | #144, #145, #146, #147, #148 | Observation consolidation, execute decomposition, UI context, text wrapping injection, widget rendering split |
|
|
818
682
|
| Phase 10 | #164, #165, #166, #167, #168, #169, #170, #171, #172 | Domain directories, ResolvedSpawnConfig, ParentSessionInfo, RunnerIO split, ToolFilterConfig, RunContext, buildContentLines, renderResult, content-items |
|
|
683
|
+
| Phase 11 | #192, #193, #194, #195, #196 | SessionContext, runtime queries, interface alignment, tool classes, runner/menu classes, index.ts simplification |
|
|
684
|
+
| Phase 12 | #205, #206, #207, #208 | renderWidgetLines, showAgentDetail, widget update, shared test fixtures |
|
|
819
685
|
|
|
820
686
|
The remaining open issue is #22 (parent-session resolution), a cross-extension track that does not gate the structural work.
|
|
821
687
|
|
|
@@ -839,8 +705,7 @@ The upstream test suite is run periodically as a regression canary for the agent
|
|
|
839
705
|
[167]: https://github.com/gotgenes/pi-packages/issues/167
|
|
840
706
|
[168]: https://github.com/gotgenes/pi-packages/issues/168
|
|
841
707
|
[169]: https://github.com/gotgenes/pi-packages/issues/169
|
|
842
|
-
[
|
|
843
|
-
[
|
|
844
|
-
[
|
|
845
|
-
[
|
|
846
|
-
[196]: https://github.com/gotgenes/pi-packages/issues/196
|
|
708
|
+
[#205]: https://github.com/gotgenes/pi-packages/issues/205
|
|
709
|
+
[#206]: https://github.com/gotgenes/pi-packages/issues/206
|
|
710
|
+
[#207]: https://github.com/gotgenes/pi-packages/issues/207
|
|
711
|
+
[#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.
|
|
@@ -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,36 @@
|
|
|
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.
|
package/package.json
CHANGED
|
@@ -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
|
}
|