@gotgenes/pi-subagents 6.12.0 → 6.12.1
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 +9 -0
- package/docs/architecture/architecture.md +6 -9
- package/docs/plans/0135-extract-display-helpers.md +182 -0
- package/docs/retro/0134-reduce-as-any-casts.md +56 -0
- package/package.json +1 -1
- package/src/renderer.ts +1 -1
- package/src/tools/agent-tool.ts +2 -2
- package/src/tools/background-spawner.ts +1 -1
- package/src/tools/foreground-runner.ts +1 -1
- package/src/tools/get-result-tool.ts +1 -1
- package/src/tools/helpers.ts +1 -1
- package/src/ui/agent-menu.ts +1 -1
- package/src/ui/agent-widget.ts +13 -165
- package/src/ui/conversation-viewer.ts +1 -1
- package/src/ui/display.ts +178 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [6.12.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.12.0...pi-subagents-v6.12.1) (2026-05-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* mark Step J done, add ui/display.ts to module listing ([#135](https://github.com/gotgenes/pi-packages/issues/135)) ([37ced45](https://github.com/gotgenes/pi-packages/commit/37ced45e1a0287aa78e588cb8bc7905c0f969637))
|
|
14
|
+
* plan display helper extraction from agent-widget ([#135](https://github.com/gotgenes/pi-packages/issues/135)) ([9e65e7d](https://github.com/gotgenes/pi-packages/commit/9e65e7d93bf47d4c4582d367d2f31a2386a5cc8c))
|
|
15
|
+
* **retro:** add retro notes for issue [#134](https://github.com/gotgenes/pi-packages/issues/134) ([775ce99](https://github.com/gotgenes/pi-packages/commit/775ce99710153d4ebcf40f773eae21553c7f8a82))
|
|
16
|
+
|
|
8
17
|
## [6.12.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.11.0...pi-subagents-v6.12.0) (2026-05-22)
|
|
9
18
|
|
|
10
19
|
|
|
@@ -68,6 +68,7 @@ notification.ts — completion nudges, custom message renderer
|
|
|
68
68
|
renderer.ts — notification TUI component
|
|
69
69
|
record-observer.ts — session-event observer for record statistics
|
|
70
70
|
|
|
71
|
+
ui/display.ts — pure formatters, display helpers, and shared types (Theme, AgentDetails)
|
|
71
72
|
ui/agent-widget.ts — above-editor live status widget
|
|
72
73
|
ui/agent-menu.ts — /agents slash command menu
|
|
73
74
|
ui/conversation-viewer.ts — scrollable session overlay
|
|
@@ -564,16 +565,12 @@ Production changes:
|
|
|
564
565
|
|
|
565
566
|
Remaining 15 `as any` casts are: 8 menu-handler `ctx as any` (deferred — requires `AgentManager.spawn` to accept `ParentSnapshot` directly), 2 `print-mode.test.ts` (same ExtensionContext/API pattern), 2 private-field test access, 1 `createSession` SDK bridge in `index.ts`, 1 `foreground-runner.ts` `AgentToolResult<any>` detail, 1 `stub-ctx.ts` comment.
|
|
566
567
|
|
|
567
|
-
### Step J: Extract display helpers (#135)
|
|
568
|
+
### Step J: Extract display helpers (#135) ✓ done
|
|
568
569
|
|
|
569
|
-
`
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
-
|
|
573
|
-
- Display helpers: `getDisplayName`, `getPromptModeLabel`, `buildInvocationTags`, `describeActivity`.
|
|
574
|
-
- Constants: `SPINNER`, `ERROR_STATUSES`, `TOOL_DISPLAY`.
|
|
575
|
-
|
|
576
|
-
Impact: `agent-widget.ts` drops from 600 → ~420 lines; shared display logic has a single import point; menu and tool modules stop importing from the widget.
|
|
570
|
+
`ui/display.ts` now contains all pure formatters, display helpers, constants, and shared types (`Theme`, `AgentDetails`).
|
|
571
|
+
`agent-widget.ts` dropped from 522 → ~340 lines.
|
|
572
|
+
All consumer modules (menu, tools, renderer, conversation viewer) import from `ui/display.ts` directly.
|
|
573
|
+
`test/agent-widget.test.ts` renamed to `test/display.test.ts`.
|
|
577
574
|
|
|
578
575
|
### Step K: Decompose agent-menu.ts (#136)
|
|
579
576
|
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 135
|
|
3
|
+
issue_title: "Extract display helpers from `agent-widget.ts`"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extract display helpers from agent-widget.ts
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`agent-widget.ts` (522 lines) exports 11 helper functions and constants that are general-purpose display utilities with no dependency on the widget's lifecycle or state.
|
|
11
|
+
Six other source modules (`agent-menu.ts`, `conversation-viewer.ts`, `renderer.ts`, `agent-tool.ts`, `foreground-runner.ts`, `get-result-tool.ts`) and two tool support modules (`helpers.ts`, `background-spawner.ts`) import formatting functions or display types from the widget — creating a false dependency on a lifecycle-heavy UI module.
|
|
12
|
+
|
|
13
|
+
This is Phase 8, Step J of the architecture plan.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Extract pure formatters, display helpers, constants, and associated types into `ui/display.ts`.
|
|
18
|
+
- Update all import sites to import from `ui/display.ts` instead of `ui/agent-widget.ts`.
|
|
19
|
+
- Reduce `agent-widget.ts` to only the `AgentWidget` class and its immediate dependencies (`UICtx`, private helpers).
|
|
20
|
+
- Unblock Step K (menu decomposition, #136) — extracted menu sub-modules will import display helpers without pulling in the widget.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Decomposing `agent-menu.ts` — deferred to #136 (Step K).
|
|
25
|
+
- Changing any runtime behavior or public API.
|
|
26
|
+
- Extracting `UICtx` — it is a widget-lifecycle type used only by `AgentWidget`, `runtime.ts`, and `index.ts`.
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
The architecture doc (Phase 8 roadmap, Step J) prescribes exactly which symbols to extract.
|
|
31
|
+
The `code-design` skill's "Helpers stay in the file" rule applies: these helpers have accumulated to the point where they warrant their own module and tests.
|
|
32
|
+
AGENTS.md's "one concern per file" constraint also supports the extraction.
|
|
33
|
+
|
|
34
|
+
### Symbols to extract
|
|
35
|
+
|
|
36
|
+
#### Pure formatters (zero runtime dependencies)
|
|
37
|
+
|
|
38
|
+
1. `formatTokens(count)` — compact token count ("33.8k token").
|
|
39
|
+
2. `formatSessionTokens(tokens, percent, theme, compactions)` — annotated token string with threshold colors.
|
|
40
|
+
3. `formatTurns(turnCount, maxTurns)` — turn counter with optional limit.
|
|
41
|
+
4. `formatMs(ms)` — milliseconds → "1.2s".
|
|
42
|
+
5. `formatDuration(startedAt, completedAt)` — timestamp pair → human duration.
|
|
43
|
+
|
|
44
|
+
#### Display helpers (registry lookup only)
|
|
45
|
+
|
|
46
|
+
6. `getDisplayName(type, registry)` — resolved display name for an agent type.
|
|
47
|
+
7. `getPromptModeLabel(type, registry)` — "twin" for append mode, undefined otherwise.
|
|
48
|
+
8. `buildInvocationTags(invocation)` — config tags array from invocation options.
|
|
49
|
+
9. `describeActivity(activeTools, responseText)` — human-readable activity string.
|
|
50
|
+
|
|
51
|
+
#### Constants
|
|
52
|
+
|
|
53
|
+
10. `SPINNER` — braille spinner frames.
|
|
54
|
+
11. `ERROR_STATUSES` — set of error/non-success status strings.
|
|
55
|
+
12. `TOOL_DISPLAY` — tool name → action verb mapping (private, moves with `describeActivity`).
|
|
56
|
+
|
|
57
|
+
#### Types
|
|
58
|
+
|
|
59
|
+
13. `Theme` — used in `formatSessionTokens` signature; must co-locate.
|
|
60
|
+
14. `AgentDetails` — display metadata interface used by tools; no widget dependency.
|
|
61
|
+
|
|
62
|
+
`UICtx` stays in `agent-widget.ts` — it defines the widget's host contract and is only consumed by the widget class, `runtime.ts`, and `index.ts`.
|
|
63
|
+
|
|
64
|
+
### Current import graph (agent-widget.ts consumers)
|
|
65
|
+
|
|
66
|
+
| Consumer | Symbols imported |
|
|
67
|
+
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `ui/conversation-viewer.ts` | `buildInvocationTags`, `describeActivity`, `formatDuration`, `formatSessionTokens`, `getDisplayName`, `getPromptModeLabel`, `Theme` |
|
|
69
|
+
| `ui/agent-menu.ts` | `formatDuration`, `getDisplayName` |
|
|
70
|
+
| `tools/agent-tool.ts` | `AgentDetails`, `buildInvocationTags`, `formatMs`, `formatTurns`, `getDisplayName`, `getPromptModeLabel`, `SPINNER`, `UICtx` |
|
|
71
|
+
| `tools/foreground-runner.ts` | `AgentDetails`, `describeActivity`, `formatMs`, `SPINNER` |
|
|
72
|
+
| `tools/get-result-tool.ts` | `formatDuration`, `getDisplayName` |
|
|
73
|
+
| `tools/helpers.ts` | `AgentDetails`, `formatTokens` |
|
|
74
|
+
| `tools/background-spawner.ts` | `AgentDetails` |
|
|
75
|
+
| `renderer.ts` | `formatMs`, `formatTokens`, `formatTurns` |
|
|
76
|
+
| `runtime.ts` | `UICtx` |
|
|
77
|
+
| `index.ts` | `AgentWidget`, `UICtx` |
|
|
78
|
+
| `test/agent-widget.test.ts` | `formatSessionTokens`, `getDisplayName`, `getPromptModeLabel` |
|
|
79
|
+
| `test/conversation-viewer.test.ts` | `Theme` |
|
|
80
|
+
|
|
81
|
+
### Post-extraction import graph
|
|
82
|
+
|
|
83
|
+
After extraction, `ui/agent-widget.ts` imports `display.ts` for the symbols it still uses internally (e.g., `getDisplayName`, `formatMs`, `formatTurns`, `formatSessionTokens`, `ERROR_STATUSES`, `SPINNER`, `describeActivity`).
|
|
84
|
+
All other consumers switch their imports from `./agent-widget.js` to `./display.js` (or `../ui/display.js` for `tools/` and `renderer.ts`).
|
|
85
|
+
|
|
86
|
+
Only `index.ts` and `runtime.ts` continue to import from `agent-widget.ts` (for `AgentWidget` class and `UICtx` type).
|
|
87
|
+
`tools/agent-tool.ts` splits its import: `UICtx` from `agent-widget.ts`, everything else from `display.ts`.
|
|
88
|
+
|
|
89
|
+
## Design Overview
|
|
90
|
+
|
|
91
|
+
This is a pure code-motion refactoring — no behavior changes.
|
|
92
|
+
|
|
93
|
+
### New module: `ui/display.ts`
|
|
94
|
+
|
|
95
|
+
Contains all 12 exported symbols (5 formatters, 4 display helpers, 3 constants) plus 2 types (`Theme`, `AgentDetails`) and 1 private helper (`truncateLine`, used by `describeActivity`).
|
|
96
|
+
|
|
97
|
+
The module's only imports are:
|
|
98
|
+
|
|
99
|
+
- `AgentConfigLookup` from `../agent-types.js` (type-only, for `getDisplayName`/`getPromptModeLabel`).
|
|
100
|
+
- `SubagentType`, `AgentInvocation` from `../types.js` (type-only).
|
|
101
|
+
|
|
102
|
+
No SDK imports, no runtime dependencies — exactly the kind of pure utility module the code-design skill prescribes.
|
|
103
|
+
|
|
104
|
+
### Residual `agent-widget.ts`
|
|
105
|
+
|
|
106
|
+
After extraction, `agent-widget.ts` contains:
|
|
107
|
+
|
|
108
|
+
- `UICtx` type (widget host contract).
|
|
109
|
+
- `AgentWidget` class (~340 lines) with its private helpers.
|
|
110
|
+
- Imports from `./display.js` for the format/display functions used in rendering.
|
|
111
|
+
|
|
112
|
+
## Module-Level Changes
|
|
113
|
+
|
|
114
|
+
### New files
|
|
115
|
+
|
|
116
|
+
| File | Contents |
|
|
117
|
+
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
118
|
+
| `src/ui/display.ts` | All extracted symbols: `Theme`, `AgentDetails`, `SPINNER`, `ERROR_STATUSES`, `TOOL_DISPLAY`, `formatTokens`, `formatSessionTokens`, `formatTurns`, `formatMs`, `formatDuration`, `getDisplayName`, `getPromptModeLabel`, `buildInvocationTags`, `describeActivity`, private `truncateLine`. |
|
|
119
|
+
|
|
120
|
+
### Modified files
|
|
121
|
+
|
|
122
|
+
| File | Change |
|
|
123
|
+
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `src/ui/agent-widget.ts` | Remove all extracted symbols. Add `import` from `./display.js` for symbols `AgentWidget` still uses internally. Keep `UICtx`, `AgentWidget` class, private widget helpers. |
|
|
125
|
+
| `src/ui/conversation-viewer.ts` | Change import path from `./agent-widget.js` to `./display.js`. |
|
|
126
|
+
| `src/ui/agent-menu.ts` | Change import path from `./agent-widget.js` to `./display.js`. |
|
|
127
|
+
| `src/tools/agent-tool.ts` | Split import: `UICtx` from `../ui/agent-widget.js`; all others from `../ui/display.js`. |
|
|
128
|
+
| `src/tools/foreground-runner.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
|
|
129
|
+
| `src/tools/get-result-tool.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
|
|
130
|
+
| `src/tools/helpers.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
|
|
131
|
+
| `src/tools/background-spawner.ts` | Change import path from `../ui/agent-widget.js` to `../ui/display.js`. |
|
|
132
|
+
| `src/renderer.ts` | Change import path from `./ui/agent-widget.js` to `./ui/display.js`. |
|
|
133
|
+
| `test/agent-widget.test.ts` | Change import path to `../src/ui/display.js`. Rename file to `test/display.test.ts` since it tests extracted functions. |
|
|
134
|
+
| `test/conversation-viewer.test.ts` | Change `Theme` import from `../src/ui/agent-widget.js` to `../src/ui/display.js`. |
|
|
135
|
+
|
|
136
|
+
### Unchanged files
|
|
137
|
+
|
|
138
|
+
| File | Reason |
|
|
139
|
+
| ---------------- | ------------------------------------------------------------------- |
|
|
140
|
+
| `src/runtime.ts` | Imports only `UICtx` — stays in `agent-widget.ts`. |
|
|
141
|
+
| `src/index.ts` | Imports `AgentWidget` and `UICtx` — both stay in `agent-widget.ts`. |
|
|
142
|
+
|
|
143
|
+
## Test Impact Analysis
|
|
144
|
+
|
|
145
|
+
1. The extraction enables dedicated `display.test.ts` that tests formatting functions in isolation without any widget class ceremony.
|
|
146
|
+
The existing `agent-widget.test.ts` already tests only extracted functions (`formatSessionTokens`, `getDisplayName`, `getPromptModeLabel`) — it becomes `display.test.ts` with no assertion changes, just a file rename and import path update.
|
|
147
|
+
2. No existing tests become redundant — the current test file already exercises the extracted layer exclusively.
|
|
148
|
+
3. No existing tests need assertion changes — this is a pure code-motion refactoring with no behavior change.
|
|
149
|
+
|
|
150
|
+
## TDD Order
|
|
151
|
+
|
|
152
|
+
1. **Create `ui/display.ts` with all extracted symbols; update `agent-widget.ts` to import from it.**
|
|
153
|
+
Move the 12 exported symbols, 2 types, and 1 private helper to `ui/display.ts`.
|
|
154
|
+
Remove them from `agent-widget.ts` and add imports from `./display.js` for symbols the `AgentWidget` class still references.
|
|
155
|
+
Commit: `refactor: extract display helpers into ui/display.ts (#135)`
|
|
156
|
+
|
|
157
|
+
2. **Update all consumer imports to point at `ui/display.ts`.**
|
|
158
|
+
Update the 8 source files (`conversation-viewer.ts`, `agent-menu.ts`, `agent-tool.ts`, `foreground-runner.ts`, `get-result-tool.ts`, `helpers.ts`, `background-spawner.ts`, `renderer.ts`) to import from the new module.
|
|
159
|
+
Commit: `refactor: update imports to use ui/display.ts (#135)`
|
|
160
|
+
|
|
161
|
+
3. **Rename test file and update test imports.**
|
|
162
|
+
Rename `test/agent-widget.test.ts` → `test/display.test.ts`.
|
|
163
|
+
Update import path to `../src/ui/display.js`.
|
|
164
|
+
Update `Theme` import in `test/conversation-viewer.test.ts`.
|
|
165
|
+
Commit: `test: rename agent-widget test to display test (#135)`
|
|
166
|
+
|
|
167
|
+
4. **Verify: run `pnpm run check` and `pnpm vitest run`.**
|
|
168
|
+
Confirm type-checking and all tests pass.
|
|
169
|
+
No commit needed — validation step.
|
|
170
|
+
|
|
171
|
+
## Risks and Mitigations
|
|
172
|
+
|
|
173
|
+
| Risk | Mitigation |
|
|
174
|
+
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
175
|
+
| Missed import site causes runtime `undefined` import | Grep confirmed all 10 source consumers and 2 test consumers above. Step 4 validates with type-check + full test suite. |
|
|
176
|
+
| `TOOL_DISPLAY` made public unintentionally | Keep it non-exported in `display.ts` (only `describeActivity` uses it). |
|
|
177
|
+
| Circular dependency `display.ts` ↔ `agent-widget.ts` | `display.ts` has no imports from `agent-widget.ts`. `agent-widget.ts` imports from `display.ts` — one-directional. |
|
|
178
|
+
| Re-export churn for downstream consumers | No downstream consumers — these are all internal module imports, not public API. |
|
|
179
|
+
|
|
180
|
+
## Open Questions
|
|
181
|
+
|
|
182
|
+
None — the architecture doc and issue specify the exact extraction set and target module.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 134
|
|
3
|
+
issue_title: "Reduce `as any` casts in test suite"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #134 — Reduce as-any casts in test suite
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-22T17:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Reduced `as any` casts from 93 to 15 across the pi-subagents package.
|
|
13
|
+
Production changes added type guards (`getToolCallName`, `isBashExecution`), narrowed `SubagentRuntime.widget` to `WidgetLike`, typed `CreateSessionOptions.settingsManager` as `SettingsManager`, and fixed `ResourceLoaderOptions.appendSystemPromptOverride` to match the SDK.
|
|
14
|
+
Test improvements introduced `toAgentSession()`, `STUB_CTX`, and `makeRegistry()` helpers to centralise unavoidable bridge casts.
|
|
15
|
+
Also fixed 3 pre-existing lint issues.
|
|
16
|
+
|
|
17
|
+
This session also included #133 (plan + implement + ship) which preceded the #134 work.
|
|
18
|
+
|
|
19
|
+
### Observations
|
|
20
|
+
|
|
21
|
+
#### What went well
|
|
22
|
+
|
|
23
|
+
- The user's Kent Beck prompt ("make the change that makes the change easy") redirected the plan from test-only fixes to targeted production changes, yielding cleaner results — `WidgetLike`, type guards, and SDK-typed options eliminated casts that would have been impossible to remove from tests alone.
|
|
24
|
+
- The user's TDA observation on the `MenuCtx` step caught a deep architectural issue (context threaded 4 layers just to relay to `buildParentSnapshot`).
|
|
25
|
+
Skipping step 4 was the right call — attempting it would have added a production cast or cascaded changes through 6+ files.
|
|
26
|
+
|
|
27
|
+
#### What caused friction (agent side)
|
|
28
|
+
|
|
29
|
+
1. `rabbit-hole` — Step 1 planning involved extensive analysis of whether `CreateSessionOptions` could use full SDK types.
|
|
30
|
+
Spent significant reasoning tracing `ModelRegistry` / `SessionManager` private fields, structural compatibility, `ParentSnapshot` constructibility, and `ResourceLoader` interface width before concluding that only `settingsManager` and the callback signature could be fixed.
|
|
31
|
+
The plan's claim "widen to SDK types" was optimistic about class-with-private-fields constraints.
|
|
32
|
+
Impact: added friction but no rework — the conclusion was correct, just slow to reach.
|
|
33
|
+
|
|
34
|
+
2. `wrong-abstraction` — Step 4 (`MenuCtx`) attempted to mechanically narrow the handler parameter without questioning why `ctx` was threaded 4 layers deep.
|
|
35
|
+
Partially implemented the `MenuCtx` interface before the user's "What am I misunderstanding?"
|
|
36
|
+
prompt exposed the real issue: a TDA violation where intermediate functions carry `ExtensionContext` only to relay it.
|
|
37
|
+
Impact: partial implementation reverted via `git checkout src/ui/agent-menu.ts`; ~10 minutes of wasted edits.
|
|
38
|
+
|
|
39
|
+
3. `instruction-violation` (user-caught) — Used `pnpm exec biome check --write --unsafe` instead of `pnpm run lint:fix` to fix pre-existing lint issues.
|
|
40
|
+
The `/tdd-plan` prompt explicitly says "run `pnpm run lint:fix`."
|
|
41
|
+
Impact: no functional difference (same result), but violated the established convention and used an unnecessary `--unsafe` flag.
|
|
42
|
+
|
|
43
|
+
4. `instruction-violation` (user-caught) — Dismissed pre-existing lint issues as "not from our changes" without fixing them.
|
|
44
|
+
The user asked "Why do we have pre-existing lint issues?"
|
|
45
|
+
— the correct action was to fix them immediately rather than noting and ignoring them.
|
|
46
|
+
Impact: required an extra commit (`style: fix pre-existing lint issues`) that should have been folded into an earlier step.
|
|
47
|
+
|
|
48
|
+
#### What caused friction (user side)
|
|
49
|
+
|
|
50
|
+
- The user's interventions on the `MenuCtx` step and lint issues were both valuable redirections that improved the outcome.
|
|
51
|
+
No mechanical overhead identified — each intervention was strategic judgment that the agent couldn't have reached alone.
|
|
52
|
+
|
|
53
|
+
### Changes made
|
|
54
|
+
|
|
55
|
+
1. `.pi/prompts/tdd-plan.md` — added "Verify green baseline" section (check + lint + test before starting TDD); added "Fix all failures — including pre-existing ones" to the end-of-cycle lint step.
|
|
56
|
+
2. `.pi/prompts/build-plan.md` — same baseline check and fix-all rule for non-TDD plans.
|
package/package.json
CHANGED
package/src/renderer.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import type { NotificationDetails } from "./notification.js";
|
|
3
|
-
import { formatMs, formatTokens, formatTurns } from "./ui/
|
|
3
|
+
import { formatMs, formatTokens, formatTurns } from "./ui/display.js";
|
|
4
4
|
|
|
5
5
|
/** Narrow theme interface — only the methods the renderer actually calls. */
|
|
6
6
|
interface RendererTheme {
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { resolveInvocationModel } from "../model-resolver.js";
|
|
|
9
9
|
|
|
10
10
|
import type { AgentInvocation, AgentRecord, SubagentType } from "../types.js";
|
|
11
11
|
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
12
|
+
import { type UICtx } from "../ui/agent-widget.js";
|
|
12
13
|
import {
|
|
13
14
|
type AgentDetails,
|
|
14
15
|
buildInvocationTags,
|
|
@@ -17,8 +18,7 @@ import {
|
|
|
17
18
|
getDisplayName,
|
|
18
19
|
getPromptModeLabel,
|
|
19
20
|
SPINNER,
|
|
20
|
-
|
|
21
|
-
} from "../ui/agent-widget.js";
|
|
21
|
+
} from "../ui/display.js";
|
|
22
22
|
import { spawnBackground } from "./background-spawner.js";
|
|
23
23
|
import { runForeground } from "./foreground-runner.js";
|
|
24
24
|
import { buildDetails, buildTypeListText, textResult } from "./helpers.js";
|
|
@@ -2,7 +2,7 @@ import type { Model } from "@earendil-works/pi-ai";
|
|
|
2
2
|
import type { AgentSpawnConfig } from "../agent-manager.js";
|
|
3
3
|
import type { AgentInvocation, AgentRecord, IsolationMode, ThinkingLevel } from "../types.js";
|
|
4
4
|
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
5
|
-
import type { AgentDetails } from "../ui/
|
|
5
|
+
import type { AgentDetails } from "../ui/display.js";
|
|
6
6
|
import { subscribeUIObserver } from "../ui/ui-observer.js";
|
|
7
7
|
import type { AgentActivityAccess } from "./agent-tool.js";
|
|
8
8
|
import { textResult } from "./helpers.js";
|
|
@@ -2,7 +2,7 @@ import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { Type } from "@sinclair/typebox";
|
|
3
3
|
import type { AgentConfigLookup } from "../agent-types.js";
|
|
4
4
|
import type { AgentRecord } from "../types.js";
|
|
5
|
-
import { formatDuration, getDisplayName } from "../ui/
|
|
5
|
+
import { formatDuration, getDisplayName } from "../ui/display.js";
|
|
6
6
|
import { getSessionContextPercent } from "../usage.js";
|
|
7
7
|
import { formatLifetimeTokens, textResult } from "./helpers.js";
|
|
8
8
|
|
package/src/tools/helpers.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentConfigLookup } from "../agent-types.js";
|
|
2
2
|
import { AgentActivityTracker } from "../ui/agent-activity-tracker.js";
|
|
3
|
-
import { type AgentDetails, formatTokens } from "../ui/
|
|
3
|
+
import { type AgentDetails, formatTokens } from "../ui/display.js";
|
|
4
4
|
import { getLifetimeTotal, type LifetimeUsage } from "../usage.js";
|
|
5
5
|
|
|
6
6
|
/** Parenthetical status note for completed agent result text. */
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import type { ModelRegistry } from "../model-resolver.js";
|
|
11
11
|
import type { AgentConfig, AgentRecord } from "../types.js";
|
|
12
12
|
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
13
|
-
import { formatDuration, getDisplayName } from "./
|
|
13
|
+
import { formatDuration, getDisplayName } from "./display.js";
|
|
14
14
|
|
|
15
15
|
// ---- Deps interface ----
|
|
16
16
|
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -7,40 +7,29 @@
|
|
|
7
7
|
|
|
8
8
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
9
9
|
import type { AgentManager } from "../agent-manager.js";
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
10
|
+
import { AgentTypeRegistry } from "../agent-types.js";
|
|
11
|
+
import type { SubagentType } from "../types.js";
|
|
12
12
|
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
13
13
|
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
14
|
+
import {
|
|
15
|
+
describeActivity,
|
|
16
|
+
ERROR_STATUSES,
|
|
17
|
+
formatMs,
|
|
18
|
+
formatSessionTokens,
|
|
19
|
+
formatTurns,
|
|
20
|
+
getDisplayName,
|
|
21
|
+
getPromptModeLabel,
|
|
22
|
+
SPINNER,
|
|
23
|
+
type Theme,
|
|
24
|
+
} from "./display.js";
|
|
14
25
|
|
|
15
26
|
// ---- Constants ----
|
|
16
27
|
|
|
17
28
|
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
18
29
|
const MAX_WIDGET_LINES = 12;
|
|
19
30
|
|
|
20
|
-
/** Braille spinner frames for animated running indicator. */
|
|
21
|
-
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
22
|
-
|
|
23
|
-
/** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
|
|
24
|
-
export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
|
|
25
|
-
|
|
26
|
-
/** Tool name → human-readable action for activity descriptions. */
|
|
27
|
-
const TOOL_DISPLAY: Record<string, string> = {
|
|
28
|
-
read: "reading",
|
|
29
|
-
bash: "running command",
|
|
30
|
-
edit: "editing",
|
|
31
|
-
write: "writing",
|
|
32
|
-
grep: "searching",
|
|
33
|
-
find: "finding files",
|
|
34
|
-
ls: "listing",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
31
|
// ---- Types ----
|
|
38
32
|
|
|
39
|
-
export type Theme = {
|
|
40
|
-
fg(color: string, text: string): string;
|
|
41
|
-
bold(text: string): string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
33
|
export type UICtx = {
|
|
45
34
|
setStatus(key: string, text: string | undefined): void;
|
|
46
35
|
setWidget(
|
|
@@ -50,147 +39,6 @@ export type UICtx = {
|
|
|
50
39
|
): void;
|
|
51
40
|
};
|
|
52
41
|
|
|
53
|
-
/** Metadata attached to Agent tool results for custom rendering. */
|
|
54
|
-
export interface AgentDetails {
|
|
55
|
-
displayName: string;
|
|
56
|
-
description: string;
|
|
57
|
-
subagentType: string;
|
|
58
|
-
toolUses: number;
|
|
59
|
-
tokens: string;
|
|
60
|
-
durationMs: number;
|
|
61
|
-
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error" | "background";
|
|
62
|
-
/** Human-readable description of what the agent is currently doing. */
|
|
63
|
-
activity?: string;
|
|
64
|
-
/** Current spinner frame index (for animated running indicator). */
|
|
65
|
-
spinnerFrame?: number;
|
|
66
|
-
/** Short model name if different from parent (e.g. "haiku", "sonnet"). */
|
|
67
|
-
modelName?: string;
|
|
68
|
-
/** Notable config tags (e.g. ["thinking: high", "isolated"]). */
|
|
69
|
-
tags?: string[];
|
|
70
|
-
/** Current turn count. */
|
|
71
|
-
turnCount?: number;
|
|
72
|
-
/** Effective max turns (undefined = unlimited). */
|
|
73
|
-
maxTurns?: number;
|
|
74
|
-
agentId?: string;
|
|
75
|
-
error?: string;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---- Formatting helpers ----
|
|
79
|
-
|
|
80
|
-
/** Format a token count compactly: "33.8k token", "1.2M token". */
|
|
81
|
-
export function formatTokens(count: number): string {
|
|
82
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
83
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
84
|
-
return `${count} token`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Token count with optional context-fill % and compaction-count annotations.
|
|
89
|
-
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
90
|
-
* Compaction count rendered as `↻N` in dim.
|
|
91
|
-
*
|
|
92
|
-
* "12.3k token" — no annotations
|
|
93
|
-
* "12.3k token (45%)" — percent only
|
|
94
|
-
* "12.3k token (↻2)" — compactions only (e.g. right after compact)
|
|
95
|
-
* "12.3k token (45% · ↻2)" — both
|
|
96
|
-
*/
|
|
97
|
-
export function formatSessionTokens(
|
|
98
|
-
tokens: number,
|
|
99
|
-
percent: number | null,
|
|
100
|
-
theme: Theme,
|
|
101
|
-
compactions = 0,
|
|
102
|
-
): string {
|
|
103
|
-
const tokenStr = formatTokens(tokens);
|
|
104
|
-
const annot: string[] = [];
|
|
105
|
-
if (percent !== null) {
|
|
106
|
-
const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
107
|
-
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
108
|
-
}
|
|
109
|
-
if (compactions > 0) {
|
|
110
|
-
annot.push(theme.fg("dim", `↻${compactions}`));
|
|
111
|
-
}
|
|
112
|
-
if (annot.length === 0) return tokenStr;
|
|
113
|
-
return `${tokenStr} (${annot.join(" · ")})`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/** Format turn count with optional max limit: "⟳5≤30" or "⟳5". */
|
|
117
|
-
export function formatTurns(turnCount: number, maxTurns?: number | null): string {
|
|
118
|
-
return maxTurns != null ? `⟳${turnCount}≤${maxTurns}` : `⟳${turnCount}`;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/** Format milliseconds as human-readable duration. */
|
|
122
|
-
export function formatMs(ms: number): string {
|
|
123
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** Format duration from start/completed timestamps. */
|
|
127
|
-
export function formatDuration(startedAt: number, completedAt?: number): string {
|
|
128
|
-
if (completedAt) return formatMs(completedAt - startedAt);
|
|
129
|
-
return `${formatMs(Date.now() - startedAt)} (running)`;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/** Get display name for any agent type (built-in or custom). */
|
|
133
|
-
export function getDisplayName(type: SubagentType, registry: AgentConfigLookup): string {
|
|
134
|
-
const config = registry.resolveAgentConfig(type);
|
|
135
|
-
return config.displayName ?? config.name;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
139
|
-
export function getPromptModeLabel(type: SubagentType, registry: AgentConfigLookup): string | undefined {
|
|
140
|
-
const config = registry.resolveAgentConfig(type);
|
|
141
|
-
return config.promptMode === "append" ? "twin" : undefined;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** Mode label is not included — callers add it where they want it. */
|
|
145
|
-
export function buildInvocationTags(
|
|
146
|
-
invocation: AgentInvocation | undefined,
|
|
147
|
-
): { modelName?: string; tags: string[] } {
|
|
148
|
-
const tags: string[] = [];
|
|
149
|
-
if (!invocation) return { tags };
|
|
150
|
-
if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
|
|
151
|
-
if (invocation.isolated) tags.push("isolated");
|
|
152
|
-
if (invocation.isolation === "worktree") tags.push("worktree");
|
|
153
|
-
if (invocation.inheritContext) tags.push("inherit context");
|
|
154
|
-
if (invocation.runInBackground) tags.push("background");
|
|
155
|
-
if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
|
|
156
|
-
return { modelName: invocation.modelName, tags };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Truncate text to a single line, max `len` chars. */
|
|
160
|
-
function truncateLine(text: string, len = 60): string {
|
|
161
|
-
const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
|
|
162
|
-
if (line.length <= len) return line;
|
|
163
|
-
return line.slice(0, len) + "…";
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Build a human-readable activity string from currently-running tools or response text. */
|
|
167
|
-
export function describeActivity(activeTools: ReadonlyMap<string, string>, responseText?: string): string {
|
|
168
|
-
if (activeTools.size > 0) {
|
|
169
|
-
const groups = new Map<string, number>();
|
|
170
|
-
for (const toolName of activeTools.values()) {
|
|
171
|
-
const action = TOOL_DISPLAY[toolName] ?? toolName;
|
|
172
|
-
groups.set(action, (groups.get(action) ?? 0) + 1);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const parts: string[] = [];
|
|
176
|
-
for (const [action, count] of groups) {
|
|
177
|
-
if (count > 1) {
|
|
178
|
-
parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
|
|
179
|
-
} else {
|
|
180
|
-
parts.push(action);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return parts.join(", ") + "…";
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// No tools active — show truncated response text if available
|
|
187
|
-
if (responseText && responseText.trim().length > 0) {
|
|
188
|
-
return truncateLine(responseText);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
return "thinking…";
|
|
192
|
-
}
|
|
193
|
-
|
|
194
42
|
// ---- Widget manager ----
|
|
195
43
|
|
|
196
44
|
export class AgentWidget {
|
|
@@ -12,7 +12,7 @@ import { extractText } from "../context.js";
|
|
|
12
12
|
import type { AgentRecord } from "../types.js";
|
|
13
13
|
import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
|
|
14
14
|
import type { AgentActivityTracker } from "./agent-activity-tracker.js";
|
|
15
|
-
import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./
|
|
15
|
+
import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./display.js";
|
|
16
16
|
|
|
17
17
|
// ── Local message-shape types ───────────────────────────────────────────────
|
|
18
18
|
// The Pi SDK does not export narrow types for all message content variants.
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* display.ts — Pure formatting helpers and display utilities for agent UI.
|
|
3
|
+
*
|
|
4
|
+
* All functions are stateless and dependency-free (no SDK, no widget lifecycle).
|
|
5
|
+
* Consumed by the widget, the menu, tool modules, and the notification renderer.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { AgentConfigLookup } from "../agent-types.js";
|
|
9
|
+
import type { AgentInvocation, SubagentType } from "../types.js";
|
|
10
|
+
|
|
11
|
+
// ---- Types ----
|
|
12
|
+
|
|
13
|
+
export type Theme = {
|
|
14
|
+
fg(color: string, text: string): string;
|
|
15
|
+
bold(text: string): string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Metadata attached to Agent tool results for custom rendering. */
|
|
19
|
+
export interface AgentDetails {
|
|
20
|
+
displayName: string;
|
|
21
|
+
description: string;
|
|
22
|
+
subagentType: string;
|
|
23
|
+
toolUses: number;
|
|
24
|
+
tokens: string;
|
|
25
|
+
durationMs: number;
|
|
26
|
+
status: "queued" | "running" | "completed" | "steered" | "aborted" | "stopped" | "error" | "background";
|
|
27
|
+
/** Human-readable description of what the agent is currently doing. */
|
|
28
|
+
activity?: string;
|
|
29
|
+
/** Current spinner frame index (for animated running indicator). */
|
|
30
|
+
spinnerFrame?: number;
|
|
31
|
+
/** Short model name if different from parent (e.g. "haiku", "sonnet"). */
|
|
32
|
+
modelName?: string;
|
|
33
|
+
/** Notable config tags (e.g. ["thinking: high", "isolated"]). */
|
|
34
|
+
tags?: string[];
|
|
35
|
+
/** Current turn count. */
|
|
36
|
+
turnCount?: number;
|
|
37
|
+
/** Effective max turns (undefined = unlimited). */
|
|
38
|
+
maxTurns?: number;
|
|
39
|
+
agentId?: string;
|
|
40
|
+
error?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---- Constants ----
|
|
44
|
+
|
|
45
|
+
/** Braille spinner frames for animated running indicator. */
|
|
46
|
+
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
47
|
+
|
|
48
|
+
/** Statuses that indicate an error/non-success outcome (used for linger behavior and icon rendering). */
|
|
49
|
+
export const ERROR_STATUSES = new Set(["error", "aborted", "steered", "stopped"]);
|
|
50
|
+
|
|
51
|
+
/** Tool name → human-readable action for activity descriptions. */
|
|
52
|
+
const TOOL_DISPLAY: Record<string, string> = {
|
|
53
|
+
read: "reading",
|
|
54
|
+
bash: "running command",
|
|
55
|
+
edit: "editing",
|
|
56
|
+
write: "writing",
|
|
57
|
+
grep: "searching",
|
|
58
|
+
find: "finding files",
|
|
59
|
+
ls: "listing",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ---- Pure formatters ----
|
|
63
|
+
|
|
64
|
+
/** Format a token count compactly: "33.8k token", "1.2M token". */
|
|
65
|
+
export function formatTokens(count: number): string {
|
|
66
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
67
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
68
|
+
return `${count} token`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Token count with optional context-fill % and compaction-count annotations.
|
|
73
|
+
* Thresholds for percent: <70% dim, 70–85% warning, ≥85% error.
|
|
74
|
+
* Compaction count rendered as `↻N` in dim.
|
|
75
|
+
*
|
|
76
|
+
* "12.3k token" — no annotations
|
|
77
|
+
* "12.3k token (45%)" — percent only
|
|
78
|
+
* "12.3k token (↻2)" — compactions only (e.g. right after compact)
|
|
79
|
+
* "12.3k token (45% · ↻2)" — both
|
|
80
|
+
*/
|
|
81
|
+
export function formatSessionTokens(
|
|
82
|
+
tokens: number,
|
|
83
|
+
percent: number | null,
|
|
84
|
+
theme: Theme,
|
|
85
|
+
compactions = 0,
|
|
86
|
+
): string {
|
|
87
|
+
const tokenStr = formatTokens(tokens);
|
|
88
|
+
const annot: string[] = [];
|
|
89
|
+
if (percent !== null) {
|
|
90
|
+
const color = percent >= 85 ? "error" : percent >= 70 ? "warning" : "dim";
|
|
91
|
+
annot.push(theme.fg(color, `${Math.round(percent)}%`));
|
|
92
|
+
}
|
|
93
|
+
if (compactions > 0) {
|
|
94
|
+
annot.push(theme.fg("dim", `↻${compactions}`));
|
|
95
|
+
}
|
|
96
|
+
if (annot.length === 0) return tokenStr;
|
|
97
|
+
return `${tokenStr} (${annot.join(" · ")})`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Format turn count with optional max limit: "⟳5≤30" or "⟳5". */
|
|
101
|
+
export function formatTurns(turnCount: number, maxTurns?: number | null): string {
|
|
102
|
+
return maxTurns != null ? `⟳${turnCount}≤${maxTurns}` : `⟳${turnCount}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Format milliseconds as human-readable duration. */
|
|
106
|
+
export function formatMs(ms: number): string {
|
|
107
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Format duration from start/completed timestamps. */
|
|
111
|
+
export function formatDuration(startedAt: number, completedAt?: number): string {
|
|
112
|
+
if (completedAt) return formatMs(completedAt - startedAt);
|
|
113
|
+
return `${formatMs(Date.now() - startedAt)} (running)`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- Display helpers ----
|
|
117
|
+
|
|
118
|
+
/** Get display name for any agent type (built-in or custom). */
|
|
119
|
+
export function getDisplayName(type: SubagentType, registry: AgentConfigLookup): string {
|
|
120
|
+
const config = registry.resolveAgentConfig(type);
|
|
121
|
+
return config.displayName ?? config.name;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
125
|
+
export function getPromptModeLabel(type: SubagentType, registry: AgentConfigLookup): string | undefined {
|
|
126
|
+
const config = registry.resolveAgentConfig(type);
|
|
127
|
+
return config.promptMode === "append" ? "twin" : undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Mode label is not included — callers add it where they want it. */
|
|
131
|
+
export function buildInvocationTags(
|
|
132
|
+
invocation: AgentInvocation | undefined,
|
|
133
|
+
): { modelName?: string; tags: string[] } {
|
|
134
|
+
const tags: string[] = [];
|
|
135
|
+
if (!invocation) return { tags };
|
|
136
|
+
if (invocation.thinking) tags.push(`thinking: ${invocation.thinking}`);
|
|
137
|
+
if (invocation.isolated) tags.push("isolated");
|
|
138
|
+
if (invocation.isolation === "worktree") tags.push("worktree");
|
|
139
|
+
if (invocation.inheritContext) tags.push("inherit context");
|
|
140
|
+
if (invocation.runInBackground) tags.push("background");
|
|
141
|
+
if (invocation.maxTurns != null) tags.push(`max turns: ${invocation.maxTurns}`);
|
|
142
|
+
return { modelName: invocation.modelName, tags };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Truncate text to a single line, max `len` chars. */
|
|
146
|
+
function truncateLine(text: string, len = 60): string {
|
|
147
|
+
const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
|
|
148
|
+
if (line.length <= len) return line;
|
|
149
|
+
return line.slice(0, len) + "…";
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Build a human-readable activity string from currently-running tools or response text. */
|
|
153
|
+
export function describeActivity(activeTools: ReadonlyMap<string, string>, responseText?: string): string {
|
|
154
|
+
if (activeTools.size > 0) {
|
|
155
|
+
const groups = new Map<string, number>();
|
|
156
|
+
for (const toolName of activeTools.values()) {
|
|
157
|
+
const action = TOOL_DISPLAY[toolName] ?? toolName;
|
|
158
|
+
groups.set(action, (groups.get(action) ?? 0) + 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const parts: string[] = [];
|
|
162
|
+
for (const [action, count] of groups) {
|
|
163
|
+
if (count > 1) {
|
|
164
|
+
parts.push(`${action} ${count} ${action === "searching" ? "patterns" : "files"}`);
|
|
165
|
+
} else {
|
|
166
|
+
parts.push(action);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return parts.join(", ") + "…";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// No tools active — show truncated response text if available
|
|
173
|
+
if (responseText && responseText.trim().length > 0) {
|
|
174
|
+
return truncateLine(responseText);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return "thinking…";
|
|
178
|
+
}
|