@groeponline/pi-wishcraft 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.3.0] - 2026-08-25
6
+
5
7
  ## [1.2.0] - 2026-08-24
6
8
 
7
9
  ### Changed
package/ROADMAP.md CHANGED
@@ -1,208 +1,146 @@
1
1
  # Roadmap — pi-wishcraft
2
2
 
3
- Herschreven 2026-08-18. Vijfde pass 2026-08-20: 0.19.0–0.19.2 staan
4
- op npm. CHE-40 (`/powerline` tab) is Done via #18. Overlay-submenus
5
- (CHE-42 / #19) starten 0.20. ROADMAP was achter op de code: hooks,
6
- repairs-subset en skills-manager v2 UI zitten al op `main`.
7
-
8
- Pi core is de engine. Wishcraft is de cockpit. Elke feature dient één van
9
- drie doelen: **grip** (skills, tokens, config), **prestatie** (repairs,
10
- hooks, read-hints), of **leven** (overlays, vibes, detail views).
11
-
12
- Elke release is één campagne met een done-criterium. P1 = deze release,
13
- P2 = volgende, P3 = richting 1.0. Wat in 0.19 staat, shipt. Wat later
14
- staat, start niet eerder.
15
-
16
- ---
17
-
18
- ## Waarom wij bestaan
19
-
20
- De pi-extensiewereld heeft statusbalken. Ze heeft geen cockpit.
21
-
22
- **Upstream `nicobailon/pi-powerline-footer`** is een goede balk: git,
23
- context, stash, compaction-queue, vibes, welcome, bash-mode. Wij zijn
24
- daaruit gegroeid. Wat zij niet hebben, en wat onze publieke identiteit
25
- is:
26
-
27
- | Wij | Zij / de rest |
28
- |---|---|
29
- | Skills als OS: `/skills`, inline `/$`, usage, later doctor | Geen skill-manager |
30
- | Idee → actie: `#`, `/idea`, `/ideas`, `/ideas issue` | Alleen compaction-hold queue |
31
- | Eerlijke TPS: 1s-venster over 5s-ring, in/out gescheiden | Session-average of helemaal niks |
32
- | Tab-token completion + git ahead/behind | Niet of later |
33
- | Harness-laag (0.20): hooks + tool-input repairs op stock pi | Nergens in het extensie-ecosysteem |
34
-
35
- **oh-my-pi** is een hele agent-fork (~80k regels Rust-core, eigen tools,
36
- LSP, DAP). Dat is een ander product. Wij forken Mario's pi niet. Alles
37
- wat we willen van oh-my-pi vertalen we naar een extensie of we laten het
38
- liggen. De weddenschap: de beste cockpit op stock pi wint van een
39
- tweede engine.
40
-
41
- **Command Code** is een commerciële harness (hooks, tool-call repairs,
42
- read-tool engineering). Hun inzicht klopt: open modellen falen op het
43
- contract, niet op "slimheid". Wij kopiëren hun product niet. We gieten
44
- dezelfde principes in wat Pi al native biedt:
45
-
46
- - `pi.on("tool_call")` `event.input` is mutable; `block` + `reason` +
47
- `terminate` bestaan (`pi-coding-agent` 0.84.x
48
- `dist/core/extensions/types.d.ts`).
49
- - `pi.on("tool_result")`resultaat muteren.
50
- - `pi.on("session_start" | "input" | "turn_end")`.
51
-
52
- Geen core-patch. Geen tweede agent. Iedereen die `pi` draait kan de
53
- cockpit + harness installeren.
54
-
55
- **ChefGroep** (ChefBar-statuskeys, fleet-ports) is een privé-bonus, niet
56
- de publieke pitch. De npm-pagina moet leesbaar zijn voor iemand die pi
57
- gisteren installeerde.
58
-
59
- Kort: **wij zijn de cockpit + harness voor stock pi.** Niet de balk.
60
- Niet de fork. Niet de SaaS-agent.
61
-
62
- ---
63
-
64
- ## Wat wij niet worden
65
-
66
- - Geen agent-fork. Geen oh-my-pi-lite.
67
- - Geen derde control surface naast ChefBar / Kater. Overlays blijven in
68
- deze extensie.
69
- - Geen muis op de live footer. Pi core bezit die; overlay-navigatie is
70
- het pad.
71
- - Geen eigen bulk-read tool. Core's verantwoordelijkheid; wij leveren
72
- repairs + hints eromheen.
73
- - Geen PostHog in de balk. Events alleen op expliciete Joep-opt-in.
74
- - Geen custom embed/component-registratie voor footer-segments.
75
- Segments zijn data; `customItems` + `command/env/static` dekken
76
- gebruikerscontent.
77
- - Geen skills-markt als identiteit vóór 1.0. Eerst discovery die klopt
78
- en een manager die zoekt.
79
- - Geen fleet-SSH `open_ports` zonder expliciete opt-in (`segmentOptions.openPorts.host`; sanitized, geen shell-injectie).
80
- - Geen versie-reset naar 1.0.0. We blijven op 0.19 → 0.20 → 1.0 wanneer
81
- de cockpit stabiel is.
82
-
83
- ---
84
-
85
- ## Mijlpalen
86
-
87
- - **0.19.0–0.19.2 — "Correctheid"** (geland). Bugs, hygiëne, catalogus,
88
- auto-release, `/powerline` tab. Hooks, repairs-subset en skills
89
- manager v2 UI gingen mee in #12, eerder dan deze sectie beloofde.
90
- Done = npm 0.19.2 live, `/skills` filtert, `$test` expandeert geen
91
- debris, verify-trio groen op de tag.
92
- - **0.20.0–0.22 "Harness"** (0.20.0 + 0.21.0 op npm; leftovers in GRO-1414).
93
- Overlay-chrome + CHE-42 drill-down, Configure als SelectList, token-overlays,
94
- rest-repairs, README-hooks. Done = drie README-hookvoorbeelden, repair-teller,
95
- `alt+p` overlay-boom, `/tps` deelt de ring met het segment.
96
- - **1.0 "Cockpit"**. Skills-doctor/install, declaratieve policy,
97
- preset-editor, idee-review, stabiele ChefGroep-statuskeys,
98
- documentatie die waar is. Done = README dekt alles wat we shipten,
99
- geen kapotte footer-belofte.
100
- - **vNext — "Operator Layer"** (in uitvoering). Stacked PRs PR0–PR8:
101
- Deck control surface (`ctx.ui.custom`), animated Signal powerline,
102
- zero-overhead Motion Engine (0 FPS idle), 10 signature structural presets,
103
- semantische tokens, en first-class accessibility (`NO_COLOR`, reduced motion).
104
- Zie [docs/design/vnext-release-plan.md](docs/design/vnext-release-plan.md).
105
- - PR0 design corpus — geland (`docs/design/`).
106
- - PR1 motion engine — geland (`src/motion/`): één scheduler op de bestaande
107
- coalescing timer, semantische events, 6 channels, 0 FPS zonder consumers.
108
- - PR2 semantic tokens — geland (`src/config/tokens.ts`): `PresetDef.tokens`
109
- is optioneel en `DEFAULT_TOKENS` reproduceert `getDefaultColors()`.
110
- - PR3 structural presets — geland (`src/config/structural-presets.ts`,
111
- `src/config/appearance.ts`): 10 signature presets, mixable layers, Nerd/ASCII
112
- glyph fallbacks; legacy layout presets ongewijzigd.
113
- - PR4 animated Signal — geland (`src/signal/`): drie lanes, lifecycle-driven
114
- motion op één gedeelde scheduler, `/signal` primair en 0 FPS in rust.
115
- - PR5 Deck — geland (`src/extension/ui/deck/`): unified overlay via
116
- `ctx.ui.custom`, Alt+P en `/wishcraft`, elf routes, jump/search navigatie.
117
- - PR6 Appearance / Gallery — geland: Deck Motion gallery + composer,
118
- 50+ catalog defs, Enter assigns `powerline.appearance.motion`.
119
- - PR7 accessibility — geland: `src/theme/detect.ts`,
120
- `src/motion/accessibility.ts`, `powerline.motionLevel`.
121
- - PR8 Craft + docs — geland: Deck Skill Workbench, idea/guardrail panes,
122
- `skills/wishcraft-tui/references/*`, project subagents.
123
-
124
- ## Top-15 track: remaining maturity gaps
125
-
126
- Written 2026-08-20, rebaselined on 0.27.1 (code review, not this ROADMAP
127
- alone). Feature density is high; several original gaps closed in
128
- 0.23–0.27. What remains is the maturity layer that separates a top-15 pi
129
- extension from a feature-rich prototype.
130
-
131
- ### Already shipped (0.22.x–0.27.x)
132
- - **CHE-41 per-segment detail** — `→` in Navigate, snapshot on open (0.22.1).
133
- - **CHE-42 drill-down** — #19 + Configure in #13.
134
- - **Changelog roll** — version headers drive the what's-new panel.
135
- - **Per-segment fault isolation** — throwing custom segments show `!id`
136
- instead of blanking the footer (#34, 0.23.1).
137
- - **macOS open_ports** — netstat dot-address parsing (#34, 0.23.1).
138
- - **GitHub Release per npm tag** (#25, 0.23.2).
139
- - **CodeQL proto-pollution hardening** (#27, 0.23.3).
140
- - **setupHooks wired** (#26, 0.23.4).
141
- - **Policy engine** — declarative deny/inject, no spawn (GRO-1418, #29,
142
- 0.24.0).
143
- - **`/skills doctor`** — broken frontmatter, dupes, unused, budget
144
- (GRO-1416, #28, 0.24.0 / 0.25.0 tag).
145
- - **`/skills new` templates** — no marketplace (GRO-1417, #32, 0.26.0).
146
- - **`/ideas` review overlay** — status, tags, skill insert (GRO-1419, #31,
147
- 0.27.0).
148
- - **English operator UI** — overlays, skill manager, `/wishcraft` TUI
149
- (GRO-1422, #24, 0.27.1).
150
- - **`powerline.skills.count` + read hints + Status trim** (GRO-1420, 0.23.0).
151
- - **1.0.0 cockpit cut** — README lists doctor, templates, policy, and
152
- idea-review (GRO-1421). Semver leaves 0.x.
153
-
154
- ### Remaining open gaps (maturity)
155
- P1 — differentiation and quality:
156
- 1. **Settings contract to pi core.** No `contributes.settings`/schema;
157
- users hand-edit JSON. *Fix: typed settings schema + contributions.*
158
- 2. **Zero-config first run.** Install still expects JSON edits for the most
159
- useful features. *Fix: sensible defaults + first-run setup overlay.*
160
- 3. **Perf budget / low-power mode.** Status renders every ~33ms; heavy
161
- segments (bash-history, git) can hit the hot path. *Fix: configurable
162
- refresh + lite mode / repeating scheduler with 0 FPS idle. Scheduler landed
163
- in PR1 (`src/motion/scheduler.ts`); the segment hot path is still open.*
164
- 4. **Accessibility (no-color / reduced-motion).** Truecolor + animations
165
- (vibes, rainbow think) break on terminals without truecolor. *Fix:
166
- `NO_COLOR`/8-color + reduced-motion respect (PR7).*
167
-
168
- P2 — full product, post-1.0:
169
- 5. **Preset editor in-menu** — custom JSON-only today (addressed in PR3 & PR6).
170
- 6. **Skill install from repo/npm** — discovery + doctor exist; install and
171
- curate missing (addressed in PR8).
172
- 7. **Host-status integration** — `ctx.ui.setStatus` beside the footer so
173
- status also shows in host UI (`skills.count` is a start; full coverage
174
- remains).
175
-
176
- ---
177
-
178
- ## vNext Milestone: The Stacked PR Plan
179
-
180
- The complete vNext architecture is detailed in **[`docs/design/vnext-release-plan.md`](docs/design/vnext-release-plan.md)** and executed across 9 stacked pull requests:
181
-
182
- ```mermaid
183
- graph LR
184
- PR0["PR0: Design Corpus"] --> PR1["PR1: Motion Engine"]
185
- PR1 --> PR2["PR2: Semantic Tokens"]
186
- PR2 --> PR3["PR3: Preset Contract"]
187
- PR3 --> PR4["PR4: Animated Signal"]
188
- PR4 --> PR5["PR5: Wishcraft Deck"]
189
- PR5 --> PR6["PR6: Appearance & Gallery"]
190
- PR6 --> PR7["PR7: First-Class A11y"]
191
- PR7 --> PR8["PR8: Craft, Skills & Docs"]
3
+ Baseline: **v1.2.0**, published 2026-08-24.
4
+
5
+ Pi is the engine. Wishcraft is the operator experience layer: Signal, Deck, skills, ideas, shell UX, hooks, repairs and policy without forking Pi core.
6
+
7
+ Historical 0.x/1.0/vNext campaign detail belongs in `CHANGELOG.md` and `docs/design/`. This file describes only the current product contract and work that is still open.
8
+
9
+ ## Product boundary
10
+
11
+ Wishcraft may extend the Pi experience, but it does not become:
12
+
13
+ - a Pi fork or second agent engine;
14
+ - a fleet/orchestration control plane;
15
+ - a SaaS integrations bundle;
16
+ - a second plugin runtime competing with Pi extensions;
17
+ - a telemetry collector by default.
18
+
19
+ External systems should connect through small contribution/integration contracts. Vendor-specific control planes belong outside Wishcraft.
20
+
21
+ ## Release line
22
+
23
+ | Release | Theme | State | Done means |
24
+ | --- | --- | --- | --- |
25
+ | **1.2** | Operator Layer | shipped | Deck, universal Signal, structural appearance, motion gallery/composer, skill workbench and first-class motion accessibility |
26
+ | **1.3** | Hardening | current | release invariant, render-path performance, one token source of truth, explicit renderer/lifecycle contracts, regression coverage |
27
+ | **1.4** | Extension Contract | next | typed settings registry plus small contribution APIs; no new plugin runtime |
28
+ | **1.5** | Craft Ecosystem | later | curated skill/package workflows built on Pi-native distribution and the contribution contract |
29
+ | **2.0** | Stable Experience Platform | target | documented compatibility policy, migrations, performance budgets and stable public extension points |
30
+
31
+ ## 1.3 Hardening
32
+
33
+ No feature dump. This release exists to make the v1.2 surface trustworthy.
34
+
35
+ ### P0 release integrity
36
+
37
+ - One reusable `Verify` contract for PR verification and release gating.
38
+ - Verify includes whitespace, typecheck, unit tests, circular dependency check and Pi package contract.
39
+ - Release jobs depend on Verify and re-check the synchronized `origin/main` tree before tagging.
40
+ - `main` should require the Verify check in repository rules before merge.
41
+
42
+ ### P0 render/runtime performance
43
+
44
+ - Deck paint must not run filesystem-backed skill discovery, skill doctor or settings parsing.
45
+ - Expensive Deck data is cached as static snapshot state and refreshed only on open, relevant navigation or mutation.
46
+ - Live model/Git/context/queue/Signal state remains cheap to repaint.
47
+ - Idle motion stays 0 FPS.
48
+
49
+ ### P0appearance consistency
50
+
51
+ - `src/config/types.ts` + `src/config/tokens.ts` are the canonical token contract.
52
+ - `src/theme/tokens/*` is compatibility-only; it may re-export but may not define a second palette or semantic mapping.
53
+ - One structural base must paint the same semantic roles regardless of the configuration path used to select it.
54
+
55
+ ### P0 Signal contract
56
+
57
+ - Signal is the universal status renderer for legacy and structural presets.
58
+ - Legacy presets keep their existing segment/layout/color contract unless a structural appearance layer is explicitly selected.
59
+ - Terminal one-shots (`success`, `warning`, `error`) settle to `idle/ready` after their finite burst.
60
+ - Reduced/off/screen-reader modes communicate state without requiring animation.
61
+
62
+ ### P1 — verification matrix
63
+
64
+ Add/keep regression coverage for:
65
+
66
+ - widths around 40 / 80 / 120+ columns;
67
+ - ASCII, Nerd Font and `NO_COLOR` rendering;
68
+ - full / reduced / functional / off motion;
69
+ - Deck render hot-path invariants;
70
+ - legacy preset color compatibility;
71
+ - release-gate dependency order;
72
+ - Linux and macOS system-segment parsing.
73
+
74
+ ## 1.4 Extension Contract
75
+
76
+ The next architecture step is **composability**, not more built-in routes.
77
+
78
+ ### Typed settings registry
79
+
80
+ One registry should drive:
81
+
82
+ ```text
83
+ settings definition
84
+
85
+ ├─ parsing + validation
86
+ ├─ defaults + migrations
87
+ ├─ /wishcraft settings
88
+ ├─ Pi settings contribution (where supported)
89
+ └─ generated documentation/examples
90
+ ```
91
+
92
+ This removes duplicate knowledge about settings and enables a real zero-config first run.
93
+
94
+ ### Small contribution API
95
+
96
+ Target capabilities, subject to Pi host APIs:
97
+
98
+ ```text
99
+ registerDeckRoute()
100
+ registerSignalSource()
101
+ registerMotion()
102
+ registerAppearanceContribution()
103
+ registerRecipeOrAction()
104
+ contributeSettings()
192
105
  ```
193
106
 
194
- ---
195
-
196
- ## Kwaliteit (altijd)
197
-
198
- - Overlay logic is testable via pure functions. No headless `ctx.ui`.
199
- - **English UI:** operator overlays, notify strings, and `/wishcraft` copy are
200
- English. Do not add Dutch UI strings.
201
- - `prepublishOnly` = `tsc --noEmit`. Een slecht type ship't niet.
202
- - `madge --circular` blijft CI. Nieuwe map in `src/` → check mee.
203
- - Dependabot-vulns (devDep-transitief): waiven, track op GRO-603.
204
- Niet required in CI. Geen stille bump van TypeScript 7 of
205
- `@types/node` 26 in een bug-PR.
206
- - TPS-core blijft 1s sliding window over 5s-ring. Geen regressie
207
- naar session-average of per-render EMA (beide spikten:
208
- `tps:12775`).
107
+ Rules:
108
+
109
+ - Pi remains the package/extension runtime.
110
+ - Contributions are data/callback contracts, not arbitrary nested plugin loaders.
111
+ - Public contracts are versioned and capability-scoped.
112
+ - A failing contribution cannot take down Signal or the Deck.
113
+
114
+ ## 1.5 Craft Ecosystem
115
+
116
+ After the contribution/settings contracts are stable:
117
+
118
+ - skill import/install workflows from explicit trusted sources;
119
+ - curated recipes/actions that compose existing Wishcraft/Pi capabilities;
120
+ - package discovery surfaced through Pi-native package distribution rather than a parallel Wishcraft marketplace;
121
+ - export/import of appearance and motion recipes with validation.
122
+
123
+ ## 2.0 — Stable Experience Platform
124
+
125
+ 2.0 is justified when the public contracts, not the feature count, are stable.
126
+
127
+ Required before 2.0:
128
+
129
+ - compatibility and deprecation policy;
130
+ - settings migrations with round-trip tests;
131
+ - measured render/performance budgets;
132
+ - stable contribution API with fault isolation;
133
+ - release provenance and required repository checks;
134
+ - docs generated from canonical contracts where practical;
135
+ - no known duplicate sources of truth for tokens, settings or release verification.
136
+
137
+ ## Always-on engineering rules
138
+
139
+ - English operator UI.
140
+ - No synchronous discovery work in paint/render loops.
141
+ - No background animation when nothing consumes it.
142
+ - Core Pi tools are not silently rewritten by Wishcraft repairs.
143
+ - Policies and hooks remain explicitly disableable.
144
+ - Security-sensitive paths fail closed.
145
+ - Package/release verification runs before npm publication.
146
+ - New features must fit the operator-layer boundary above.
@@ -1,68 +1,71 @@
1
- # Signal — The Animated Powerline Specification
1
+ # Signal — Animated status contract
2
2
 
3
3
  ## Overview
4
4
 
5
- **Signal** is Wishcraft's animated powerline. While previous terminal footers were static rows of plain text, Signal turns status into an informative, living track.
5
+ Signal is Wishcraft's status renderer for stock Pi. `/signal` is primary; `/powerline` remains a compatibility alias. No-args toggles the surface, `/signal menu` opens Navigate / Configure / Status, and `/signal <preset>` changes the information preset.
6
6
 
7
- - **Primary Command**: `/signal` (opens the Signal configuration deck or toggles view modes).
8
- - **Compatibility Alias**: `/powerline` (retained for backward compatibility).
7
+ Signal divides status into three lanes:
9
8
 
10
- ---
11
-
12
- ## 3-Lane Architecture
9
+ ```text
10
+ LEFT / IDENTITY CENTER / ACTIVITY RIGHT / CONTEXT
11
+ model · git · workspace lifecycle · tool · motion context · queue
12
+ ```
13
13
 
14
- Signal divides status information into three distinct, customizable lanes:
14
+ A typical frame:
15
15
 
16
- ```
17
- ┌──────────────────────────────────────────────────────────────────────────────────┐
18
- │ LEFT LANE CENTER LANE RIGHT LANE │
19
- │ Model · Git Status Live Activity & Tools Context · Queue │
20
- ├──────────────────────────────┼──────────────────────────────┼────────────────────┤
21
- │ ◆ GPT-5.6 ╾━━━━ main ━━━━ │ ╾✦╼━━━━ read_file ━━━━━━━ │ ctx █████░ 47% │
22
- └──────────────────────────────┴──────────────────────────────┴────────────────────┘
16
+ ```text
17
+ ◆ GPT-5.6 main ╾✦╼ read_file ctx █████░ 47% · q1
23
18
  ```
24
19
 
25
- 1. **Left Lane (Identity & Workspace)**
26
- - Active Model badge (with provider color accent)
27
- - Git branch and repository clean/dirty status
28
- - Workspace directory (truncated gracefully on compact displays)
20
+ ## Renderer contract
29
21
 
30
- 2. **Center Lane (Live Activity & Motion)**
31
- - Current agent state (Thinking, Streaming, Tool Execution)
32
- - Active tool call indicator (`read_file`, `grep`, `execute_command`)
33
- - Traveling motion pulse representing data throughput
22
+ **Signal is the universal status-line renderer.** This is intentional, including for the legacy layout preset names (`default`, `minimal`, `compact`, `full`, `nerd`, `ascii`, `chef`). There is no second legacy renderer hidden behind those names.
34
23
 
35
- 3. **Right Lane (Metrics & Session)**
36
- - Context window progress bar and percentage
37
- - Queued user/agent instructions count
38
- - Session cost / token consumption (if enabled)
24
+ Compatibility is preserved at the data/config layer:
39
25
 
40
- ---
26
+ - legacy presets still define their existing segment selection, layout options, separators, and explicit colors;
27
+ - with no structural appearance layer selected, legacy preset colors remain exact;
28
+ - structural preset names (`lanternwake` … `crucible`) opt into the vNext appearance personality and semantic-token palette;
29
+ - `powerline.appearance.*` may mix palette, Signal grammar, chrome, glyphs, deck, welcome, and motion independently.
41
30
 
42
- ## Live Motion Sweeps
31
+ In other words: **one renderer, two compatible preset contracts**. A future change that restores a second renderer or silently remaps legacy colors is a breaking change and requires explicit migration tests.
43
32
 
44
- Signal animates only when actual work is being processed. The track reflects execution states through traveling pulses:
33
+ ## Three lanes
45
34
 
46
- ### Token Streaming Wave
47
- ```
48
- t0: ◆ GPT-5.6 ╾▓▒░━━━━ main ━━━━━━━━━ read ━━━━━━━━━ ctx 47%
49
- t1: ◆ GPT-5.6 ━╾▓▒░━━━ main ━━━━━━━━━ read ━━━━━━━━━ ctx 47%
50
- t2: ◆ GPT-5.6 ━━━╾▓▒░━ main ━━━━━━━━━ read ━━━━━━━━━ ctx 47%
51
- t3: ◆ GPT-5.6 ━━━━━━━━ main ╾▓▒░━━━━━ read ━━━━━━━━━ ctx 47%
52
- ```
35
+ 1. **Identity / workspace** — model, Git state, path/workspace identity.
36
+ 2. **Activity** — thinking, streaming, tool execution, compacting and one-shot outcomes.
37
+ 3. **Context / queue** context usage and queued/parked work.
53
38
 
54
- ### Tool Execution Traveling Pulse
55
- ```
56
- t0: read ━━━╾✦╼━━━ grep ━━━━━━━━━ edit ━━━━━━━━━
57
- t1: read ✓ ━━━━━━━ grep ━━━╾✦╼━━━ edit ━━━━━━━━━
58
- t2: read ━━━━━━━ grep ━━━━━━━ edit ━━━╾✦╼━━━
39
+ Width pressure is resolved inside Signal; lanes may compact or omit optional detail, but their semantic order stays identity → activity → context.
40
+
41
+ ## Motion lifecycle
42
+
43
+ Signal owns no timer. It leases the shared MotionScheduler only while a motion channel needs frames.
44
+
45
+ ```text
46
+ idle / ready 0 FPS
47
+
48
+ ├─ thinking / streaming / tool.start / compact → active state
49
+
50
+ └─ success | warning | error → finite burst
51
+
52
+ idle / ready
53
+ 0 FPS
59
54
  ```
60
55
 
61
- ---
56
+ Terminal one-shots (`success`, `warning`, `error`) settle their semantic state back to `idle/ready` when the final frame completes. This prevents a completed agent run from leaving `done` painted indefinitely without keeping a background timer alive.
57
+
58
+ Reduced/functional/off motion policies may suppress frames entirely; status text must still communicate the state. `NO_COLOR`, screen-reader flags and ASCII fallback are first-class inputs to the same policy.
59
+
60
+ ## Fault isolation
61
+
62
+ Signal builds on Wishcraft's segment isolation:
63
+
64
+ - a failing custom segment is isolated instead of blanking the full status surface;
65
+ - cached Git/session state is preferred during transient refreshes;
66
+ - separators collapse around hidden/empty segments;
67
+ - rendering must not perform unrelated discovery work such as the Deck skill doctor.
62
68
 
63
- ## Fault Isolation & Error Boundaries
69
+ ## Performance invariant
64
70
 
65
- Signal builds on Pi-Wishcraft's existing segment architecture:
66
- - Every segment executes in an isolated `try/catch` wrapper.
67
- - If an individual segment fails (e.g. git command timeout or unexpected API response), it degrades gracefully to a silent fallback or a compact warning glyph without crashing the surrounding status line.
68
- - Segment separators adjust dynamically when neighboring segments are hidden or empty.
71
+ Idle is 0 FPS. Continuous animation uses the shared coalescing scheduler. Expensive filesystem-backed discovery belongs outside paint/render paths; status rendering consumes already-resolved/cached state.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groeponline/pi-wishcraft",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Operator cockpit for Pi: live powerline status, searchable skills, idea queue, sticky Bash, hooks, policy controls, and session UX.",
5
5
  "type": "module",
6
6
  "files": [
@@ -62,7 +62,6 @@ export function resolveAppearanceMix(
62
62
  config: AppearanceMixConfig = {},
63
63
  ): ResolvedAppearance {
64
64
  const baseName = config.base ?? "lanternwake";
65
- const base = getStructuralPreset(baseName);
66
65
 
67
66
  const paletteName = layerPreset(config, "palette", baseName);
68
67
  const signalName = layerPreset(config, "signalLayout", baseName);
@@ -15,14 +15,8 @@ import { getDefaultColors } from "../../theme/theme.ts";
15
15
  import { getGitStatus } from "../../git/status.ts";
16
16
  import { getQueueContext } from "../queue/queue-context.ts";
17
17
  import { getUsageTokenTotal } from "../../usage/ledger.ts";
18
- import {
19
- dayKey,
20
- loadUsageFileFromDisk,
21
- tokenTotal,
22
- totalsForRange,
23
- } from "../../usage/usage-store.ts";
24
- import { parseTokenBudget } from "../../usage/token-budget.ts";
25
- import { readSettings } from "../settings/settings-io.ts";
18
+ import { tokenBudgetSnapshotForDay } from "../../usage/token-budget.ts";
19
+ import { dayKey } from "../../usage/usage-store.ts";
26
20
  import {
27
21
  CUSTOM_COMPACTION_STATUS_KEY,
28
22
  EDITOR_STATUS_DEFER_MS,
@@ -120,10 +114,16 @@ export function installFooterStatusRepaintHook(
120
114
  };
121
115
  }
122
116
 
117
+ export interface BuildSegmentContextOptions {
118
+ /** Omit token-budget data from lightweight UI snapshots. */
119
+ includeTokenBudget?: boolean;
120
+ }
121
+
123
122
  export function buildSegmentContext(
124
123
  rt: RuntimeState,
125
124
  ctx: any,
126
125
  theme: Theme,
126
+ options: BuildSegmentContextOptions = {},
127
127
  ): SegmentContext {
128
128
  const presetDef = getPreset(config.preset);
129
129
  const colors: ColorScheme = liveColorScheme(
@@ -205,6 +205,10 @@ export function buildSegmentContext(
205
205
  getQueueContext(ctx),
206
206
  rt.powerlineCompacting,
207
207
  );
208
+ const budgetSnapshot = tokenBudgetSnapshotForDay(
209
+ rt.tokenBudgetSnapshot,
210
+ dayKey(Date.now()),
211
+ );
208
212
 
209
213
  return {
210
214
  model: ctx.model,
@@ -234,18 +238,14 @@ export function buildSegmentContext(
234
238
  effectiveCustomItems,
235
239
  options: segmentOptions,
236
240
  segmentLabels: new Map(Object.entries(config.segmentLabels)),
237
- tokenBudget: (() => {
238
- const dailyLimit = parseTokenBudget(
239
- readSettings(ctx.cwd ?? process.cwd()).wishcraft,
240
- ).daily;
241
- const now = Date.now();
242
- const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
243
- const dailyUsed = tokenTotal(
244
- totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
245
- );
246
- return { dailyLimit, dailyUsed };
247
- })(),
241
+ tokenBudget:
242
+ options.includeTokenBudget === false
243
+ ? undefined
244
+ : {
245
+ dailyLimit: budgetSnapshot.dailyLimit,
246
+ dailyUsed: budgetSnapshot.dailyUsed,
247
+ },
248
248
  theme,
249
249
  colors,
250
250
  };
251
- }
251
+ }
@@ -117,6 +117,7 @@ export function createRuntimeState(
117
117
  currentEditor: null,
118
118
  costAlertNotified: false,
119
119
  tokenBudgetNotifiedLevel: 0,
120
+ tokenBudgetSnapshot: { day: "", dailyLimit: null, dailyUsed: 0 },
120
121
  bashModeActive: false,
121
122
  bashTranscript: new BashTranscriptStore(bashModeSettings),
122
123
  bashCompletionEngine: new BashCompletionEngine(),
@@ -174,4 +175,4 @@ export function createRuntimeState(
174
175
  });
175
176
 
176
177
  return rt;
177
- }
178
+ }
@@ -11,6 +11,7 @@ import type {
11
11
  SessionTokenStatsCache,
12
12
  } from "../../usage/ledger.ts";
13
13
  import type { CoreContextUsageCache } from "../../usage/context.ts";
14
+ import type { TokenBudgetSnapshot } from "../../usage/token-budget.ts";
14
15
  import type { WelcomeDismissScheduler } from "../../welcome/auto-dismiss.ts";
15
16
  import type { RenderScheduler } from "../../render/timer.ts";
16
17
  import type { MotionPolicy, MotionScheduler } from "../../motion/index.ts";
@@ -68,6 +69,8 @@ export interface RuntimeState {
68
69
  costAlertNotified: boolean;
69
70
  /** Highest daily-budget warning already shown this session (0 / 80 / 100). */
70
71
  tokenBudgetNotifiedLevel: 0 | 80 | 100;
72
+ /** Event-driven token-budget snapshot consumed by render paths without disk I/O. */
73
+ tokenBudgetSnapshot: TokenBudgetSnapshot;
71
74
  bashModeActive: boolean;
72
75
  bashTranscript: BashTranscriptStore;
73
76
  bashCompletionEngine: BashCompletionEngine;
@@ -105,4 +108,4 @@ export interface RuntimeState {
105
108
  motionPolicy: MotionPolicy;
106
109
  /** Live Signal event, motion frame, and activity label. */
107
110
  signal: SignalRuntime;
108
- }
111
+ }
@@ -16,26 +16,10 @@ import {
16
16
  onVibeToolCall,
17
17
  } from "../../working-vibes/index.ts";
18
18
  import {
19
- getSessionTotalCost,
20
19
  getUsageTokenTotal,
21
20
  isSessionAssistantMessage,
22
21
  } from "../../usage/ledger.ts";
23
- import {
24
- formatCostAlertMessage,
25
- shouldTriggerCostAlert,
26
- } from "./cost-alert.ts";
27
- import {
28
- formatTokenBudgetWarning,
29
- parseTokenBudget,
30
- tokenBudgetLevel,
31
- } from "../../usage/token-budget.ts";
32
- import {
33
- recordUsageEvent,
34
- loadUsageFileFromDisk,
35
- tokenTotal,
36
- totalsForRange,
37
- dayKey,
38
- } from "../../usage/usage-store.ts";
22
+ import { recordUsageEvent } from "../../usage/usage-store.ts";
39
23
  import {
40
24
  detectCustomCompactionEnabled,
41
25
  readSettings,
@@ -81,52 +65,10 @@ import {
81
65
  dispatchSignalEvent,
82
66
  settleSignal,
83
67
  } from "../../signal/integration.ts";
84
-
85
- /**
86
- * Fire the configured `powerline.costAlert` warning at most once per session.
87
- * Reads the running cost from the (cached) token ledger so repeated calls are
88
- * cheap; a UI-less or already-notified session short-circuits immediately.
89
- */
90
- function maybeNotifyCostAlert(rt: RuntimeState, ctx: any): void {
91
- if (!ctx?.hasUI || rt.costAlertNotified) return;
92
- const threshold = config.costAlert;
93
- const sessionEvents = rt.sessionBranchCache.get(ctx.sessionManager);
94
- const totalCost = getSessionTotalCost(rt.tokenStatsCache.get(sessionEvents));
95
- if (
96
- !shouldTriggerCostAlert({
97
- totalCost,
98
- threshold,
99
- alreadyNotified: rt.costAlertNotified,
100
- })
101
- ) {
102
- return;
103
- }
104
- rt.costAlertNotified = true;
105
- ctx.ui.notify(
106
- formatCostAlertMessage(
107
- totalCost,
108
- threshold as number,
109
- config.segmentOptions?.cost?.currency ?? "USD",
110
- ),
111
- "warning",
112
- );
113
- }
114
-
115
- function maybeNotifyTokenBudget(rt: RuntimeState, ctx: any): void {
116
- if (!ctx?.hasUI) return;
117
- const daily = parseTokenBudget(readSettings(ctx.cwd ?? process.cwd()).wishcraft)
118
- .daily;
119
- if (!daily) return;
120
- const now = Date.now();
121
- const todayStart = Date.parse(`${dayKey(now)}T00:00:00`);
122
- const used = tokenTotal(
123
- totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
124
- );
125
- const { level } = tokenBudgetLevel(used, daily);
126
- if (level === 0 || level <= rt.tokenBudgetNotifiedLevel) return;
127
- rt.tokenBudgetNotifiedLevel = level;
128
- ctx.ui.notify(formatTokenBudgetWarning(used, daily, level), "warning");
129
- }
68
+ import {
69
+ maybeNotifyCostAlert,
70
+ maybeNotifyTokenBudget,
71
+ } from "./session-notifications.ts";
130
72
 
131
73
  // Helper to extract recent agent response text (skipping thinking blocks)
132
74
  function getRecentAgentContext(ctx: any): string | undefined {
@@ -180,6 +122,7 @@ export function registerSessionLifecycle(
180
122
  rt.liveAssistantUsage = null;
181
123
  rt.costAlertNotified = false;
182
124
  rt.tokenBudgetNotifiedLevel = 0;
125
+ rt.tokenBudgetSnapshot = { day: "", dailyLimit: null, dailyUsed: 0 };
183
126
  rt.powerlineCompacting = false;
184
127
  rt.deliverAfterRetrySettles = false;
185
128
  rt.stashedEditorText = null;
@@ -232,7 +175,7 @@ export function registerSessionLifecycle(
232
175
  } else {
233
176
  dismissWelcome(rt, ctx);
234
177
  }
235
- maybeNotifyTokenBudget(rt, ctx);
178
+ maybeNotifyTokenBudget(rt, ctx, settings);
236
179
  }
237
180
  });
238
181
 
@@ -264,6 +207,7 @@ export function registerSessionLifecycle(
264
207
  rt.getThinkingLevelFn = null;
265
208
  rt.currentThinkingLevel = null;
266
209
  rt.liveAssistantUsage = null;
210
+ rt.tokenBudgetSnapshot = { day: "", dailyLimit: null, dailyUsed: 0 };
267
211
  rt.tuiRef = null;
268
212
  rt.currentEditor = null;
269
213
  resetLayoutCache(rt);
@@ -482,4 +426,4 @@ export function registerSessionLifecycle(
482
426
  schedulePostCompactionDelivery(pi, rt, ctx);
483
427
  }
484
428
  });
485
- }
429
+ }
@@ -0,0 +1,91 @@
1
+ import { getSessionTotalCost } from "../../usage/ledger.ts";
2
+ import {
3
+ formatTokenBudgetWarning,
4
+ parseTokenBudget,
5
+ tokenBudgetLevel,
6
+ } from "../../usage/token-budget.ts";
7
+ import {
8
+ dayKey,
9
+ loadUsageFileFromDisk,
10
+ tokenTotal,
11
+ totalsForRange,
12
+ } from "../../usage/usage-store.ts";
13
+ import { config } from "../core/state.ts";
14
+ import type { RuntimeState } from "../core/types.ts";
15
+ import { readSettings } from "../settings/settings-io.ts";
16
+ import {
17
+ formatCostAlertMessage,
18
+ shouldTriggerCostAlert,
19
+ } from "./cost-alert.ts";
20
+
21
+ /** Notify once when the configured per-session cost threshold is crossed. */
22
+ export function maybeNotifyCostAlert(rt: RuntimeState, ctx: any): void {
23
+ if (!ctx?.hasUI || rt.costAlertNotified) return;
24
+ const threshold = config.costAlert;
25
+ const sessionEvents = rt.sessionBranchCache.get(ctx.sessionManager);
26
+ const totalCost = getSessionTotalCost(rt.tokenStatsCache.get(sessionEvents));
27
+ if (
28
+ !shouldTriggerCostAlert({
29
+ totalCost,
30
+ threshold,
31
+ alreadyNotified: rt.costAlertNotified,
32
+ })
33
+ ) {
34
+ return;
35
+ }
36
+
37
+ rt.costAlertNotified = true;
38
+ ctx.ui.notify(
39
+ formatCostAlertMessage(
40
+ totalCost,
41
+ threshold as number,
42
+ config.segmentOptions?.cost?.currency ?? "USD",
43
+ ),
44
+ "warning",
45
+ );
46
+ }
47
+
48
+ /**
49
+ * Refresh disk-backed budget data at lifecycle boundaries, never during paint.
50
+ * A local-day rollover also resets the per-day warning latch.
51
+ */
52
+ export function refreshTokenBudgetSnapshot(
53
+ rt: RuntimeState,
54
+ ctx: any,
55
+ settings?: ReturnType<typeof readSettings>,
56
+ ): { daily: number | null; used: number; level: 0 | 80 | 100 } {
57
+ const now = Date.now();
58
+ const day = dayKey(now);
59
+ if (rt.tokenBudgetSnapshot.day !== day) {
60
+ rt.tokenBudgetNotifiedLevel = 0;
61
+ }
62
+
63
+ const resolvedSettings = settings ?? readSettings(ctx.cwd ?? process.cwd());
64
+ const daily = parseTokenBudget(resolvedSettings.wishcraft).daily;
65
+ if (!daily) {
66
+ rt.tokenBudgetSnapshot = { day, dailyLimit: null, dailyUsed: 0 };
67
+ return { daily: null, used: 0, level: 0 };
68
+ }
69
+
70
+ const todayStart = Date.parse(`${day}T00:00:00`);
71
+ const used = tokenTotal(
72
+ totalsForRange(loadUsageFileFromDisk(), todayStart, now + 1),
73
+ );
74
+ rt.tokenBudgetSnapshot = { day, dailyLimit: daily, dailyUsed: used };
75
+ const { level } = tokenBudgetLevel(used, daily);
76
+ return { daily, used, level };
77
+ }
78
+
79
+ /** Refresh the daily budget and emit its 80%/100% warning at most once per day. */
80
+ export function maybeNotifyTokenBudget(
81
+ rt: RuntimeState,
82
+ ctx: any,
83
+ settings?: ReturnType<typeof readSettings>,
84
+ ): void {
85
+ if (!ctx?.hasUI) return;
86
+ const { daily, used, level } = refreshTokenBudgetSnapshot(rt, ctx, settings);
87
+ if (!daily || level === 0 || level <= rt.tokenBudgetNotifiedLevel) return;
88
+
89
+ rt.tokenBudgetNotifiedLevel = level;
90
+ ctx.ui.notify(formatTokenBudgetWarning(used, daily, level), "warning");
91
+ }
@@ -26,7 +26,10 @@ import {
26
26
  import type { RuntimeState } from "../../core/types.ts";
27
27
  import { appearanceDisplayName } from "../../../config/structural-presets.ts";
28
28
  import { filterSkillRows, selectedGalleryMotion } from "./route-bodies.ts";
29
- import { buildDeckSessionSnapshot } from "./session-snapshot.ts";
29
+ import {
30
+ buildDeckSessionSnapshot,
31
+ buildDeckStaticSnapshot,
32
+ } from "./session-snapshot.ts";
30
33
  import { deckRouteByJump, deckRouteIndex } from "./routes.ts";
31
34
  import { filterDeckRoutes, renderDeckFrame } from "./render.ts";
32
35
  import { DECK_ROUTE_DEFS } from "./routes.ts";
@@ -66,14 +69,23 @@ export function createDeckComponent(
66
69
  theme: import("@earendil-works/pi-coding-agent").Theme,
67
70
  done: () => void,
68
71
  ) {
69
- const refreshSnapshot = () => buildDeckSessionSnapshot(rt, ctx);
72
+ let staticSnapshot = buildDeckStaticSnapshot(ctx);
73
+ const liveSnapshot = () => buildDeckSessionSnapshot(rt, ctx, staticSnapshot);
74
+ const refreshStaticSnapshot = (): void => {
75
+ staticSnapshot = buildDeckStaticSnapshot(ctx);
76
+ };
77
+
70
78
  let state = createDeckNavState(
71
79
  initialRoute,
72
- appearanceIndex(refreshSnapshot().appearanceBase),
80
+ appearanceIndex(liveSnapshot().appearanceBase),
73
81
  );
74
82
  let composer: ComposerDraft | null = null;
75
83
 
76
84
  const setRoute = (route: DeckRoute): void => {
85
+ const routeChanged = route !== state.route;
86
+ if (routeChanged && (route === "skills" || route === "guardrails")) {
87
+ refreshStaticSnapshot();
88
+ }
77
89
  const jumpingToAppearance = route === "appearance" && state.route !== "appearance";
78
90
  state = {
79
91
  ...state,
@@ -83,7 +95,7 @@ export function createDeckComponent(
83
95
  searchOpen: false,
84
96
  searchQuery: "",
85
97
  selectedAppearance: jumpingToAppearance
86
- ? appearanceIndex(refreshSnapshot().appearanceBase)
98
+ ? appearanceIndex(liveSnapshot().appearanceBase)
87
99
  : state.selectedAppearance,
88
100
  };
89
101
  composer = null;
@@ -100,7 +112,7 @@ export function createDeckComponent(
100
112
  return renderDeckFrame(
101
113
  theme,
102
114
  width,
103
- refreshSnapshot(),
115
+ liveSnapshot(),
104
116
  state,
105
117
  rt.resolvedShortcuts,
106
118
  composer,
@@ -209,6 +221,7 @@ export function createDeckComponent(
209
221
  if (!name) return;
210
222
  try {
211
223
  const { filePath } = writeSkillFromTemplate(name, "standard");
224
+ refreshStaticSnapshot();
212
225
  state = { ...state, skillCreate: false, skillCreateName: "" };
213
226
  ctx.ui.notify(`Created ${name}. ${filePath}`, "info");
214
227
  } catch (error) {
@@ -233,7 +246,7 @@ export function createDeckComponent(
233
246
  state = { ...state, skillCreate: true, skillCreateName: "" };
234
247
  return;
235
248
  }
236
- const snapshot = refreshSnapshot();
249
+ const snapshot = liveSnapshot();
237
250
  const rows = filterSkillRows(snapshot.skills, state.searchQuery);
238
251
  if (handleList(data, "selectedSkill", rows.length)) return;
239
252
  if (matchesKey(data, "enter")) {
@@ -253,7 +266,7 @@ export function createDeckComponent(
253
266
  }
254
267
 
255
268
  if (state.route === "ideas") {
256
- if (handleList(data, "selectedIdea", refreshSnapshot().ideas.length)) return;
269
+ if (handleList(data, "selectedIdea", liveSnapshot().ideas.length)) return;
257
270
  }
258
271
 
259
272
  if (matchesKey(data, "up")) {
@@ -9,21 +9,13 @@ import { collectSkillDoctorInputs, diagnoseSkills } from "../../skills/skill-doc
9
9
  import { parsePolicySettings } from "../../hooks/policy-config.ts";
10
10
  import { readSettings } from "../../settings/settings-io.ts";
11
11
  import { describePolicy } from "../../../motion/accessibility.ts";
12
- import type { DeckSessionSnapshot } from "./types.ts";
12
+ import type { DeckSessionSnapshot, DeckStaticSnapshot } from "./types.ts";
13
13
 
14
- export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessionSnapshot {
15
- const theme = { fg: (_color: string, text: string) => text } as Theme;
16
- let segmentCtx;
17
- try {
18
- segmentCtx = buildSegmentContext(rt, ctx, theme);
19
- } catch {
20
- segmentCtx = null;
21
- }
22
-
23
- const queue = rt.queueStore.summarize(
24
- getQueueContext(ctx),
25
- rt.powerlineCompacting,
26
- );
14
+ /**
15
+ * Build the expensive Deck data once per open/navigation refresh.
16
+ * This intentionally owns filesystem-backed skill discovery/doctor and settings reads.
17
+ */
18
+ export function buildDeckStaticSnapshot(ctx: any): DeckStaticSnapshot {
27
19
  const cwd = ctx.cwd ?? process.cwd();
28
20
  const skills = loadSkillCatalog(cwd);
29
21
  const doctorInputs = collectSkillDoctorInputs(cwd);
@@ -35,6 +27,51 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
35
27
  const warnings = doctor.filter((row) => row.status !== "ok").length;
36
28
  const settings = readSettings(cwd);
37
29
  const policy = parsePolicySettings(settings.wishcraft);
30
+
31
+ return {
32
+ skillsTotal: skills.length,
33
+ skillsWarnings: warnings,
34
+ policyEnabled: policy.enabled,
35
+ policyRuleCount: policy.rules.length,
36
+ skills: skills.slice(0, 24).map((skill) => {
37
+ const row = doctor.find((entry) => entry.skill === skill.name);
38
+ const usage = doctorInputs.usage.get(skill.name);
39
+ return {
40
+ name: skill.name,
41
+ category: skill.category,
42
+ status: row?.status ?? (skill.warning ? "warn" : "ok"),
43
+ description: skill.description.slice(0, 72),
44
+ usage: usage?.count ?? 0,
45
+ };
46
+ }),
47
+ guardrailRules: policy.rules.slice(0, 8).map((rule) => ({
48
+ action: rule.action,
49
+ tool: rule.tool,
50
+ reason: rule.action === "deny" ? rule.reason : rule.context.slice(0, 48),
51
+ })),
52
+ };
53
+ }
54
+
55
+ /** Build only runtime-backed data during paint; no filesystem discovery here. */
56
+ export function buildDeckSessionSnapshot(
57
+ rt: RuntimeState,
58
+ ctx: any,
59
+ staticSnapshot: DeckStaticSnapshot = buildDeckStaticSnapshot(ctx),
60
+ ): DeckSessionSnapshot {
61
+ const theme = { fg: (_color: string, text: string) => text } as Theme;
62
+ let segmentCtx;
63
+ try {
64
+ segmentCtx = buildSegmentContext(rt, ctx, theme, {
65
+ includeTokenBudget: false,
66
+ });
67
+ } catch {
68
+ segmentCtx = null;
69
+ }
70
+
71
+ const queue = rt.queueStore.summarize(
72
+ getQueueContext(ctx),
73
+ rt.powerlineCompacting,
74
+ );
38
75
  const appearance = resolveAppearanceMix(
39
76
  effectiveAppearanceMix(config.appearance, config.preset),
40
77
  );
@@ -56,6 +93,7 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
56
93
  }
57
94
 
58
95
  return {
96
+ ...staticSnapshot,
59
97
  modelLabel,
60
98
  branchLabel,
61
99
  contextPercent: Math.round(segmentCtx?.contextPercent ?? 0),
@@ -65,10 +103,6 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
65
103
  signalMotion: rt.signal.motionId,
66
104
  queueCount: queue.queueCount,
67
105
  ideaCount: queue.ideaCount,
68
- skillsTotal: skills.length,
69
- skillsWarnings: warnings,
70
- policyEnabled: policy.enabled,
71
- policyRuleCount: policy.rules.length,
72
106
  shellName: rt.shellSession?.state.shellName ?? null,
73
107
  bashModeActive: rt.bashModeActive,
74
108
  appearanceBase: appearance.base,
@@ -76,17 +110,6 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
76
110
  nextIntent: queue.leadingText,
77
111
  motionLevel: config.motionLevel,
78
112
  policySummary: describePolicy(rt.motionPolicy),
79
- skills: skills.slice(0, 24).map((skill) => {
80
- const row = doctor.find((entry) => entry.skill === skill.name);
81
- const usage = doctorInputs.usage.get(skill.name);
82
- return {
83
- name: skill.name,
84
- category: skill.category,
85
- status: row?.status ?? (skill.warning ? "warn" : "ok"),
86
- description: skill.description.slice(0, 72),
87
- usage: usage?.count ?? 0,
88
- };
89
- }),
90
113
  ideas: rt.queueStore
91
114
  .list()
92
115
  .filter((item) => item.intent === "idea")
@@ -95,10 +118,5 @@ export function buildDeckSessionSnapshot(rt: RuntimeState, ctx: any): DeckSessio
95
118
  text: item.text.slice(0, 64),
96
119
  reviewStatus: item.reviewStatus ?? "idea",
97
120
  })),
98
- guardrailRules: policy.rules.slice(0, 8).map((rule) => ({
99
- action: rule.action,
100
- tool: rule.tool,
101
- reason: rule.action === "deny" ? rule.reason : rule.context.slice(0, 48),
102
- })),
103
121
  };
104
122
  }
@@ -28,7 +28,18 @@ export interface DeckRouteDef {
28
28
  description: string;
29
29
  }
30
30
 
31
- export interface DeckSessionSnapshot {
31
+ /** Expensive discovery/config data cached outside the render hot path. */
32
+ export interface DeckStaticSnapshot {
33
+ skillsTotal: number;
34
+ skillsWarnings: number;
35
+ policyEnabled: boolean;
36
+ policyRuleCount: number;
37
+ skills: DeckSkillRow[];
38
+ guardrailRules: DeckGuardrailRow[];
39
+ }
40
+
41
+ /** Cheap session/runtime data that may be rebuilt for each paint. */
42
+ export interface DeckSessionSnapshot extends DeckStaticSnapshot {
32
43
  modelLabel: string;
33
44
  branchLabel: string;
34
45
  contextPercent: number;
@@ -38,10 +49,6 @@ export interface DeckSessionSnapshot {
38
49
  signalMotion: string;
39
50
  queueCount: number;
40
51
  ideaCount: number;
41
- skillsTotal: number;
42
- skillsWarnings: number;
43
- policyEnabled: boolean;
44
- policyRuleCount: number;
45
52
  shellName: string | null;
46
53
  bashModeActive: boolean;
47
54
  appearanceBase: string;
@@ -49,9 +56,7 @@ export interface DeckSessionSnapshot {
49
56
  nextIntent: string | null;
50
57
  motionLevel: string;
51
58
  policySummary: string;
52
- skills: DeckSkillRow[];
53
59
  ideas: DeckIdeaRow[];
54
- guardrailRules: DeckGuardrailRow[];
55
60
  }
56
61
 
57
62
  export interface DeckSkillRow {
@@ -6,7 +6,6 @@
6
6
  * ---------------------------------------------------------------------------
7
7
  */
8
8
 
9
- import { visibleWidth } from "@earendil-works/pi-tui";
10
9
  import type { PresetDef, SegmentContext } from "../../config/types.ts";
11
10
  import { renderSegmentWithWidth } from "./layout.ts";
12
11
  import { getSeparator } from "../../theme/separators.ts";
@@ -39,8 +39,12 @@ export function createSignalRuntime(now = Date.now()): SignalRuntime {
39
39
  export interface SetSignalEventOptions {
40
40
  activity?: string;
41
41
  motionId?: string;
42
+ /** Motion to restore when a terminal one-shot settles back to semantic idle. */
43
+ settleMotionId?: string;
42
44
  /** Finite events stop after this many frames. */
43
45
  maxTicks?: number;
46
+ /** Terminal one-shots return to semantic idle when their final frame completes. */
47
+ settleOnDone?: boolean;
44
48
  }
45
49
 
46
50
  export function setSignalEvent(
@@ -79,6 +83,14 @@ export function setSignalEvent(
79
83
  onDone() {
80
84
  runtime.release = null;
81
85
  if (options.maxTicks !== undefined) runtime.active = false;
86
+ if (options.settleOnDone) {
87
+ runtime.event = "idle";
88
+ runtime.motionId = options.settleMotionId ?? defaultMotionFor("idle");
89
+ runtime.tick = 0;
90
+ runtime.startedAt = Date.now();
91
+ runtime.activity = "ready";
92
+ runtime.active = false;
93
+ }
82
94
  },
83
95
  });
84
96
  }
@@ -17,7 +17,9 @@ export function dispatchSignalEvent(
17
17
  setSignalEvent(rt.signal, rt.motionScheduler, rt.motionPolicy, event, {
18
18
  activity,
19
19
  motionId: resolved.motion[event] ?? resolved.signal.animation,
20
+ settleMotionId: resolved.motion.idle,
20
21
  maxTicks: isFiniteEvent(event) ? 6 : undefined,
22
+ settleOnDone: isTerminalEvent(event),
21
23
  });
22
24
  rt.lastLayoutResult = null;
23
25
  rt.tuiRef?.requestRender();
@@ -35,8 +37,10 @@ function isFiniteEvent(event: MotionEvent): boolean {
35
37
  event === "idea.capture" ||
36
38
  event === "skill.insert" ||
37
39
  event === "policy.deny" ||
38
- event === "success" ||
39
- event === "warning" ||
40
- event === "error"
40
+ isTerminalEvent(event)
41
41
  );
42
42
  }
43
+
44
+ function isTerminalEvent(event: MotionEvent): boolean {
45
+ return event === "success" || event === "warning" || event === "error";
46
+ }
@@ -1,62 +1,21 @@
1
1
  /**
2
- * src/theme/tokens/mapping.ts
3
- * ---------------------------------------------------------------------------
4
- * Default token mapping and legacy SemanticColor bridge for Wishcraft.
5
- * ---------------------------------------------------------------------------
2
+ * Compatibility bridge for the canonical Wishcraft token mapping.
3
+ * New code should import from src/config/tokens.ts.
6
4
  */
7
5
 
8
- import type { ColorScheme, SemanticColor } from "../../config/types.ts";
9
- import type { WishcraftTokens } from "./types.ts";
6
+ import type { ColorScheme, WishcraftTokens } from "../../config/types.ts";
7
+ import {
8
+ DEFAULT_TOKENS as CANONICAL_DEFAULT_TOKENS,
9
+ colorSchemeFromTokens,
10
+ resolveTokens,
11
+ } from "../../config/tokens.ts";
10
12
 
11
- export const DEFAULT_TOKENS: WishcraftTokens = {
12
- surface: "#0f172a",
13
- surfaceRaised: "#1e293b",
14
- text: "#f8fafc",
15
- textMuted: "dim",
16
- primary: "#f59e0b",
17
- secondary: "#00afaf",
18
- accent: "#ea580c",
19
- success: "success",
20
- warning: "warning",
21
- error: "error",
22
- focus: "#38bdf8",
23
- selection: "#334155",
24
- motionDim: "dim",
25
- motionHot: "#fbbf24",
26
- motionTrail: "#78350f",
27
- };
13
+ export const DEFAULT_TOKENS: WishcraftTokens = CANONICAL_DEFAULT_TOKENS;
28
14
 
29
- /**
30
- * Derives legacy SemanticColor configuration from modern WishcraftTokens.
31
- */
32
15
  export function deriveColorSchemeFromTokens(tokens: WishcraftTokens): Required<ColorScheme> {
33
- return {
34
- model: tokens.primary,
35
- shellMode: tokens.accent,
36
- path: tokens.secondary,
37
- gitClean: tokens.success,
38
- gitDirty: tokens.warning,
39
- thinking: "thinkingOff",
40
- thinkingMinimal: "thinkingMinimal",
41
- thinkingLow: "thinkingLow",
42
- thinkingMedium: "thinkingMedium",
43
- context: tokens.textMuted,
44
- contextWarn: tokens.warning,
45
- contextError: tokens.error,
46
- cost: tokens.text,
47
- tokens: tokens.textMuted,
48
- queue: tokens.accent,
49
- separator: tokens.textMuted,
50
- border: tokens.surfaceRaised,
51
- };
16
+ return colorSchemeFromTokens(tokens);
52
17
  }
53
18
 
54
- /**
55
- * Creates WishcraftTokens by merging overrides onto defaults.
56
- */
57
19
  export function createTokens(overrides?: Partial<WishcraftTokens>): WishcraftTokens {
58
- return {
59
- ...DEFAULT_TOKENS,
60
- ...overrides,
61
- };
20
+ return resolveTokens(overrides);
62
21
  }
@@ -1,39 +1,6 @@
1
1
  /**
2
- * src/theme/tokens/types.ts
3
- * ---------------------------------------------------------------------------
4
- * Semantic Design Tokens for Wishcraft (Crush pattern).
5
- * ---------------------------------------------------------------------------
2
+ * Compatibility exports for the canonical Wishcraft token contract.
3
+ * New code should import from src/config/types.ts.
6
4
  */
7
5
 
8
- import type { ThemeColor } from "@earendil-works/pi-coding-agent";
9
-
10
- export type ColorValue = ThemeColor | `#${string}`;
11
-
12
- export interface WishcraftTokens {
13
- // Surfaces & Backgrounds
14
- surface: ColorValue;
15
- surfaceRaised: ColorValue;
16
-
17
- // Typography
18
- text: ColorValue;
19
- textMuted: ColorValue;
20
-
21
- // Brand & Accents
22
- primary: ColorValue;
23
- secondary: ColorValue;
24
- accent: ColorValue;
25
-
26
- // Feedback & State
27
- success: ColorValue;
28
- warning: ColorValue;
29
- error: ColorValue;
30
-
31
- // Interaction
32
- focus: ColorValue;
33
- selection: ColorValue;
34
-
35
- // Motion & Animation Channels
36
- motionDim: ColorValue;
37
- motionHot: ColorValue;
38
- motionTrail: ColorValue;
39
- }
6
+ export type { ColorValue, WishcraftTokens } from "../../config/types.ts";
@@ -12,6 +12,12 @@ export interface TokenBudgetLevel {
12
12
  level: 0 | 80 | 100;
13
13
  }
14
14
 
15
+ export interface TokenBudgetSnapshot {
16
+ day: string;
17
+ dailyLimit: number | null;
18
+ dailyUsed: number;
19
+ }
20
+
15
21
  export function parseTokenBudget(wishcraftSettings: unknown): TokenBudget {
16
22
  if (!wishcraftSettings || typeof wishcraftSettings !== "object") {
17
23
  return { daily: null };
@@ -36,6 +42,23 @@ export function tokenBudgetLevel(used: number, daily: number | null): TokenBudge
36
42
  return { ratio, level: 0 };
37
43
  }
38
44
 
45
+ /**
46
+ * Return the budget visible for a local day. A long-lived session that crosses
47
+ * midnight must never carry yesterday's usage into the new day; the next
48
+ * lifecycle refresh will repopulate usage from disk after a new usage event.
49
+ */
50
+ export function tokenBudgetSnapshotForDay(
51
+ snapshot: TokenBudgetSnapshot,
52
+ day: string,
53
+ ): TokenBudgetSnapshot {
54
+ if (snapshot.day === day) return snapshot;
55
+ return {
56
+ day,
57
+ dailyLimit: snapshot.dailyLimit,
58
+ dailyUsed: 0,
59
+ };
60
+ }
61
+
39
62
  export function formatTokenBudgetWarning(
40
63
  used: number,
41
64
  daily: number,