pi-thread-engine 0.2.4 β†’ 0.4.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/README.md CHANGED
@@ -1,8 +1,11 @@
1
- # 🧡 pi-threads
1
+ # 🧡 pi-thread-engine
2
2
 
3
- Thread engineering for [pi](https://github.com/badlogic/pi-mono) β€” all 7 thread types + stories.
3
+ > *"When you start thinking in threads, you stop being a bottleneck and become an orchestrator."*
4
+ > β€” @IndyDevDan, [Thinking in Threads](https://agenticengineer.com/thinking-in-threads)
4
5
 
5
- **Wraps pi-subagents** for P/C/B threads (inheriting agent specialization, chain artifacts, live progress). **Builds natively** what nobody else has: F-Thread (multi-model fusion), Z-Thread (verify gate), and Stories (goal β†’ auto-decomposed thread phases).
6
+ **Thread-Based Engineering** for [Pi Coding Agent](https://github.com/badlogic/pi-mono).
7
+ 7 thread types + stories + fusion + zero-touch + full TUI dashboard.
8
+ Built on @IndyDevDan's framework from [agenticengineer.com](https://agenticengineer.com).
6
9
 
7
10
  ## Install
8
11
 
@@ -10,99 +13,203 @@ Thread engineering for [pi](https://github.com/badlogic/pi-mono) β€” all 7 threa
10
13
  pi install npm:pi-thread-engine
11
14
  ```
12
15
 
13
- Or from source:
16
+ ## The Thread Framework
14
17
 
15
- ```bash
16
- pi install ./path/to/pi-threads
18
+ Everything starts with the base thread:
19
+
20
+ ```
21
+ PROMPT β†’ TOOL CALLS β†’ REVIEW
22
+ You Agent You
23
+ ```
24
+
25
+ You show up at two nodes for maximum leverage. Your agent handles everything in between.
26
+
27
+ Then you multiply:
28
+
29
+ ```
30
+ Thread Types
31
+ β”œβ”€β”€ Base β†’ You β†’ Agent β†’ You (minutes)
32
+ β”œβ”€β”€ P β†’ Parallel 5+ agents (hours)
33
+ β”œβ”€β”€ C β†’ Checkpoint chains (high-stakes)
34
+ β”œβ”€β”€ F β†’ Fusion 9 agents, pick winner
35
+ β”œβ”€β”€ B β†’ Branch (orchestrator agents)
36
+ β”œβ”€β”€ L β†’ Long-running 26hrs (days)
37
+ └── Z β†’ Zero-touch, no review (ultimate)
17
38
  ```
18
39
 
19
40
  ## Commands
20
41
 
21
- | Command | Type | Backend | What |
22
- |---------|------|---------|------|
23
- | `/pthread` | P-Thread | subagent | N independent tasks in parallel |
24
- | `/cthread` | C-Thread | subagent | Sequential phases via subagent chain |
25
- | `/bthread` | B-Thread | subagent | scout β†’ plan β†’ build β†’ review pipeline |
26
- | `/fthread` | F-Thread | **native** | Same prompt β†’ N models β†’ compare (UNIQUE) |
27
- | `/zthread` | Z-Thread | **native** | Autonomous + verify command gate (UNIQUE) |
28
- | `/lthread` | L-Thread | native | Extended autonomous run |
29
- | `/story` | Stories | mixed | Auto-decompose goal into thread phases (UNIQUE) |
42
+ | Command | Type | What |
43
+ |---------|------|------|
44
+ | `/threads` | Dashboard | TUI: 3-column status view, search, inline reply, keyboard nav |
45
+ | `/pthread` | P-Thread | N independent tasks in parallel |
46
+ | `/cthread` | C-Thread | Sequential phases with checkpoint reviews |
47
+ | `/bthread` | B-Thread | scout β†’ plan β†’ build β†’ review pipeline |
48
+ | `/fthread` | F-Thread | Same prompt β†’ N models β†’ compare, pick winner |
49
+ | `/zthread` | Z-Thread | Autonomous + verification gate (ships only if tests pass) |
50
+ | `/lthread` | L-Thread | Extended autonomous run |
51
+ | `/story` | Stories | Auto-decompose goal into thread phases |
52
+ | `/agents` | Alias | Same as `/threads` β€” Claude muscle memory |
30
53
 
31
- ## Unique Features (not in pi-subagents or pi-messenger)
54
+ ## Dashboard
32
55
 
33
- ### Fusion (`/fthread`)
34
- Same prompt sent to multiple models in parallel. Compare results, pick the best.
56
+ Run `/threads` (or `/agents`) to open the TUI dashboard:
35
57
 
36
58
  ```
59
+ 🧡 Thread Dashboard
60
+ ═══════════════════════════════════════════
61
+
62
+ ⚑ Needs Input (2)
63
+ β–Έ t-001 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘ needs_input /fix bug in auth β†’ "password has special chars"
64
+ β–Έ t-003 β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘ needs_input /audit dependencies β†’ "found 3 vulnerable"
65
+
66
+ βš™ Working (3)
67
+ β–Έ t-002 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘ running /migrate database ⏱ 4m 23s
68
+ β–Έ t-004 β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘ running /build API endpoints ⏱ 2m 11s
69
+ ▸ s-001 →scout→plan→build running /story add payments
70
+
71
+ βœ“ Done (4)
72
+ β–Έ t-005 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ success /refactor auth module β†’ "cleaned up 12 files"
73
+ β–Έ t-006 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ success /update docs β†’ "30 pages updated"
74
+ β–Έ t-007 β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ error /deploy to prod βœ— "missing env vars"
75
+
76
+ nav=↑↓ exp=Enter rep=i srch=/ kill=k rev=r prune=p quit=q
77
+ ```
78
+
79
+ **Keyboard shortcuts:**
80
+ - `↑↓` β€” navigate threads
81
+ - `Enter` β€” expand/collapse thread details
82
+ - `i` β€” inline reply (send message to blocked thread)
83
+ - `/` β€” search/filter threads
84
+ - `k` β€” kill selected thread
85
+ - `r` β€” review selected thread results
86
+ - `p` β€” prune (clear) finished threads
87
+ - `q` β€” quit dashboard
88
+
89
+ ## Thread Types in Depth
90
+
91
+ ### P-Thread β€” Parallel
92
+ Five in the terminal. Five to ten in the browser. Zero apologies.
93
+ β€” *Boris Cherny, creator of Claude Code*
94
+
95
+ ```bash
96
+ /pthread "Review all PRs" --count 8
97
+ /pthread "Fix bug in auth" --count 3 --each
98
+ ```
99
+
100
+ ### C-Thread β€” Checkpoint Chains
101
+ High-stakes work needs verification at critical junctures.
102
+ Intentional chunking is a feature, not a bug.
103
+
104
+ ```bash
105
+ /cthread "Migrate to new database" --phases scout,plan,build,test,deploy
106
+ ```
107
+
108
+ ### F-Thread β€” Fusion (pi-thread-engine unique)
109
+ Nine agents. Nine parallel futures. Pick the winner.
110
+
111
+ ```bash
37
112
  /fthread "Design the caching architecture" --count 5
38
- /fthread "Refactor the auth module" --models anthropic/claude-sonnet-4,google/gemini-2.5-pro,openai/gpt-4o
113
+ /fthread "Refactor the auth module" --models claude,gpt-4o,gemini-pro
39
114
  ```
40
115
 
41
- ### Zero-Touch (`/zthread`)
42
- Autonomous execution with a verification gate. Only ships if the verify command passes.
116
+ Best-of-N gives you confidence. Cherry-picking gives you quality.
117
+ Each agent runs in complete isolation β€” no cross-contamination.
118
+
119
+ ### B-Thread β€” Branch/Meta-Agentic
120
+ When agents manage agents. You become an Orchestrator of Intelligence.
43
121
 
122
+ ```bash
123
+ /bthread "Build the checkout flow"
44
124
  ```
45
- /zthread "Fix all ESLint warnings" --verify "npm run lint"
46
- /zthread "Add input validation" --verify "npm test"
125
+
126
+ Auto-runs: plan agent β†’ scout β†’ build β†’ review β†’ deploy
127
+
128
+ ### L-Thread β€” Long-Running
129
+ A single prompt that ran 26 hours.
130
+
131
+ ```bash
132
+ /lthread "Audit and fix all security vulnerabilities" --hours 24
47
133
  ```
48
134
 
49
- ### Stories (`/story`)
50
- A goal gets auto-decomposed into thread phases. pi-threads picks the right thread type for each phase.
135
+ ### Z-Thread β€” Zero-Touch (pi-thread-engine unique)
136
+ Maximum earned trust. No review node. Ships only if verification passes.
51
137
 
138
+ ```bash
139
+ /zthread "Fix all ESLint warnings" --verify "npm run lint"
140
+ /zthread "Add dark mode" --verify "npm test && npm run build"
52
141
  ```
142
+
143
+ This is NOT vibe coding. Z-threads are the opposite:
144
+ maximum earned trust through hundreds of iterations.
145
+
146
+ ### Stories β€” Goal Decomposition (pi-thread-engine unique)
147
+ Auto-decompose a goal into thread phases. pi-thread-engine picks the right type for each.
148
+
149
+ ```bash
53
150
  /story "Add dark mode to the dashboard" --verify "npm test"
54
151
  ```
55
152
 
56
153
  Auto-generates phases:
57
154
  1. **Scout** (meta) β€” research the codebase
58
- 2. **Plan** (fusion) β€” 3 models brainstorm approaches
59
- 3. **Decide** (chained) β€” human picks the winner
155
+ 2. **Plan** (fusion) β€” 3 models brainstorm approaches
156
+ 3. **Decide** (checkpoint) β€” human picks the winner
60
157
  4. **Build** (parallel) β€” implement across files
61
158
  5. **Verify** (zero) β€” run tests
62
159
 
63
- ## Dashboard
160
+ ## LLM Tools
64
161
 
65
- ```
66
- /threads β€” show all threads + stories
67
- /threads kill t-001 β€” kill a thread
68
- /threads review β€” see completed results (fusion shows comparison)
69
- /threads prune β€” clear finished threads
70
- /stories β€” list all stories with phase progress
162
+ Use these inside any Pi prompt:
163
+
164
+ ```typescript
165
+ thread_spawn({ type: "parallel", prompts: [...] }) // Start any thread type
166
+ thread_status({ id: "t-001" }) // Check progress
167
+ thread_kill({ id: "t-001" }) // Stop a thread
71
168
  ```
72
169
 
73
- ## LLM Tools
170
+ ## The Core Four
74
171
 
75
- - `thread_spawn` β€” Start any thread type (auto-selects backend)
76
- - `thread_status` β€” Check progress of threads and stories
77
- - `thread_kill` β€” Stop a thread
172
+ Every thread runs on these four pillars. Invest in all of them:
78
173
 
79
- ## Architecture
174
+ | Pillar | Description | pi-thread-engine |
175
+ |--------|-------------|-----------------|
176
+ | **Context** | Right information (no more, no less) | `~/.pi-memory/` integration |
177
+ | **Model** | Capable of sustained reasoning | 324+ models via OpenRouter |
178
+ | **Prompt** | Crystal-clear agentic engineering | Structured commands |
179
+ | **Tools** | Self-verifying, comprehensive | 40+ pi-skills |
80
180
 
81
- ```
82
- Wrapper layer (pi-subagents): Native layer (pi -p):
83
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
84
- β”‚ /pthread β†’ /parallel β”‚ β”‚ /fthread β†’ N models β”‚
85
- β”‚ /cthread β†’ /chain β”‚ β”‚ /zthread β†’ run+verify β”‚
86
- β”‚ /bthread β†’ scout chain β”‚ β”‚ /lthread β†’ long run β”‚
87
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
88
- ↕ ↕
89
- β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
90
- β”‚ ThreadRegistry (state machine) β”‚
91
- β”‚ ThreadExecutor (dispatch) β”‚
92
- β”‚ /threads dashboard β”‚
93
- β”‚ /story orchestrator β”‚
94
- β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
95
- ```
181
+ ## Measuring Progress
96
182
 
97
- ## The Thread Framework
183
+ You know you're improving at agentic coding when:
184
+
185
+ - **Width** ↑ β€” P-threads = 5x exploration per hour
186
+ - **Time** ↑ β€” L-threads = 10min β†’ 8hrs autonomous work
187
+ - **Depth** ↑ β€” B-threads = one prompt β†’ entire teams
188
+ - **Checkpoints** ↓ β€” Earned trust through evidence
98
189
 
99
- Based on [thread-engineering](https://github.com/disler/agentic-coding-patterns):
190
+ > *The common denominator: increase tool calls per unit of your attention.*
100
191
 
101
- > A thread is a unit of engineering work: prompt at the start, review at the end, agent tool calls in between.
102
- > The metric: **tool calls per unit of your attention**. Maximize this.
192
+ ## Architecture
103
193
 
104
194
  ```
105
- Base β†’ P-Thread β†’ C-Thread β†’ F-Thread β†’ B-Thread β†’ L-Thread β†’ Z-Thread
106
- ↑
107
- The north star
195
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
196
+ β”‚ pi-thread-engine β”‚
197
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
198
+ β”‚ Dashboard β”‚ Registry β”‚ Executor β”‚
199
+ β”‚ (TUI v3) β”‚ (state) β”‚ (dispatch) β”‚
200
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
201
+ β”‚ Commands: /threads /pthread /cthread /fthread β”‚
202
+ β”‚ /bthread /zthread /lthread /story β”‚
203
+ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
204
+ β”‚ Backends β”‚
205
+ β”‚ β”œβ”€ pi-subagents (P/C/B threads) β”‚
206
+ β”‚ └─ native (F/Z/L threads, Stories) β”‚
207
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
108
208
  ```
209
+
210
+ ## Further Reading
211
+
212
+ - [Thinking in Threads](https://agenticengineer.com/thinking-in-threads) β€” @IndyDevDan
213
+ - [The Only Claude Code Competitor](https://agenticengineer.com/the-only-claude-code-competitor)
214
+ - [Compute Advantage Equation](https://agenticengineer.com/compute-advantage-equation)
215
+ - [Engineering with Exponentials](https://agenticengineer.com/state-of-ai-coding/engineering-with-exponentials)
@@ -0,0 +1,116 @@
1
+ # Thread-Based Engineering
2
+
3
+ > *"When you start thinking in threads, you stop being a bottleneck and become an orchestrator."*
4
+ > β€” @IndyDevDan, [Thinking in Threads](https://agenticengineer.com/thinking-in-threads)
5
+
6
+ This is the mental framework for agentic coding. Adapted from @IndyDevDan's article
7
+ by @IndyDevDan, co-authored with Claude Opus 4.5.
8
+
9
+ ## The Core Four
10
+
11
+ Every thread runs on these four pillars:
12
+
13
+ | Pillar | Description | pi-thread-engine |
14
+ |--------|-------------|-----------------|
15
+ | **Context** | Right information (no more, no less) | pi-memory integration |
16
+ | **Model** | Capable of sustained reasoning | Any of 324+ models |
17
+ | **Prompt** | Crystal-clear agentic engineering | Structured thread commands |
18
+ | **Tools** | Self-verifying, comprehensive | pi-skills ecosystem |
19
+
20
+ ## The Thread Taxonomy
21
+
22
+ ```
23
+ Thread Types
24
+ β”œβ”€β”€ Base β†’ You β†’ Agent β†’ You (minutes)
25
+ β”œβ”€β”€ P β†’ Parallel 5+ agents (hours)
26
+ β”œβ”€β”€ C β†’ Checkpoint chains (high-stakes)
27
+ β”œβ”€β”€ F β†’ Fusion 9 agents, pick winner
28
+ β”œβ”€β”€ B β†’ Branch (orchestrator agents)
29
+ β”œβ”€β”€ L β†’ Long-running 26hrs (days)
30
+ └── Z β†’ Zero-touch, no review (ultimate)
31
+ ```
32
+
33
+ ### Base Thread
34
+ The atomic unit. Every prompt follows this shape:
35
+
36
+ ```
37
+ PROMPT β†’ TOOL CALLS β†’ REVIEW
38
+ You Agent You
39
+ ```
40
+
41
+ You show up at two nodes for maximum leverage: Prompt and Review.
42
+ The middle is your agent executing tool calls.
43
+
44
+ ### P-Thread (Parallel)
45
+ Five in the terminal. Five to ten in the browser. Zero apologies.
46
+ β€” *Boris Cherny, creator of Claude Code*
47
+
48
+ - In-loop: Active collaboration where you guide and redirect
49
+ - Out-of-loop: Fire-and-forget tasks that complete while you focus elsewhere
50
+ - Unlock: code review at scale, rapid exploration, verification through redundancy
51
+
52
+ ### C-Thread (Checkpoint/Chained)
53
+ High-stakes work needs verification at critical junctures.
54
+
55
+ - **Context constraints**: work exceeds context window
56
+ - **Production stakes**: human verification prevents catastrophe
57
+
58
+ > *Intentional chunking is a feature, not a bug.*
59
+
60
+ ### F-Thread (Fusion)
61
+ Nine agents. Nine parallel futures. Pick the winner.
62
+
63
+ - 3 Claude Code + 3 Gemini + 3 Codex all tackling the same problem
64
+ - Best-of-N gives confidence. Cherry-picking gives quality.
65
+ - Agent sandboxes: complete isolation, no cross-contamination
66
+
67
+ > *"The future of rapid prototyping will be done with fusion threads."*
68
+
69
+ ### B-Thread (Branch/Meta-Agentic)
70
+ When agents manage agents. You become an Orchestrator of Intelligence.
71
+
72
+ - Simplest: one agent spawns another for a specific task
73
+ - Full orchestration: plan agent + scout + build + review + deploy
74
+ - Deterministic code orchestrating non-deterministic intelligence
75
+
76
+ > *"This is meta-programming for the agentic age."*
77
+
78
+ ### L-Thread (Long-Running)
79
+ A single prompt that ran 26 hours.
80
+
81
+ - Prerequisites are demanding: Core Four must all be exceptional
82
+ - Top agentic engineers kick off a prompt and go to sleep
83
+ - 100x duration = 100x decisions your agent makes without you
84
+
85
+ > *"Living software that works for us while we sleep."*
86
+
87
+ ### Z-Thread (Zero-Touch)
88
+ The hidden level beyond review. Maximum earned trust.
89
+
90
+ ```
91
+ PROMPT β†’ AGENT β†’ SHIP (no review node)
92
+ ```
93
+
94
+ > *"It isn't that we don't look at the code. It's that we know we don't have to."*
95
+
96
+ This is NOT vibe coding. Z-threads are the opposite:
97
+ maximum earned trust through hundreds of iterations of context, prompt, and tools refinement.
98
+
99
+ ## Measuring Progress
100
+
101
+ You measure improvement in threads:
102
+ - **Width** (more threads): P-threads = 5x exploration
103
+ - **Time** (longer threads): L-threads = 10min β†’ 8hrs autonomous
104
+ - **Depth** (thicker threads): B-threads = one prompt β†’ entire teams
105
+ - **Attention** (fewer checkpoints): Earn trust through evidence
106
+
107
+ > *"The common denominator: increase tool calls per unit of your attention."*
108
+
109
+ Stack these multipliers. Track them weekly. Watch them trend upward.
110
+
111
+ ## Further Reading
112
+
113
+ - [Thinking in Threads](https://agenticengineer.com/thinking-in-threads) β€” @IndyDevDan
114
+ - [The Only Claude Code Competitor](https://agenticengineer.com/the-only-claude-code-competitor)
115
+ - [Compute Advantage Equation](https://agenticengineer.com/compute-advantage-equation)
116
+ - [Engineering with Exponentials](https://agenticengineer.com/state-of-ai-coding/engineering-with-exponentials)
@@ -1,7 +1,19 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { Type } from "@sinclair/typebox";
3
- import { StringEnum } from "@mariozechner/pi-ai";
2
+ import { Type, type TUnsafe } from "@sinclair/typebox";
4
3
  import { Text } from "@mariozechner/pi-tui";
4
+
5
+ // StringEnum helper (removed from pi-ai >=0.66)
6
+ function StringEnum<T extends readonly string[]>(
7
+ values: T,
8
+ options?: { description?: string; default?: T[number] },
9
+ ): TUnsafe<T[number]> {
10
+ return Type.Unsafe<T[number]>({
11
+ type: "string",
12
+ enum: values as unknown as string[],
13
+ ...(options?.description && { description: options.description }),
14
+ ...(options?.default && { default: options.default }),
15
+ });
16
+ }
5
17
  import { ThreadRegistry, formatElapsed } from "../src/core/registry.js";
6
18
  import { ThreadExecutor } from "../src/core/executor.js";
7
19
  import { createDashboard } from "../src/dashboard.js";
@@ -178,6 +190,26 @@ export default function (pi: ExtensionAPI) {
178
190
  }
179
191
 
180
192
  // Default: open interactive TUI dashboard
193
+ // ── /agents alias (Claude-style muscle memory) ──────────────
194
+ pi.registerCommand("agents", {
195
+ description: "Alias for /threads β€” Claude-style Agent View dashboard",
196
+ handler: async (a, c) => {
197
+ // Forward to /threads
198
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => {
199
+ const dashboard = createDashboard(
200
+ registry,
201
+ theme,
202
+ () => done(),
203
+ (id) => { registry.kill(id); ctx.ui.notify(`Killed ${id}`, "warning"); tui.requestRender(); },
204
+ (id) => { const t = registry.get(id); if (t) { ctx.ui.notify(`Thread ${id}: ${t.tasks.map((tk) => `${tk.id}: ${tk.result?.slice(0,100) ?? tk.error ?? "(pending)"}`).join("\n")}`, "info"); } },
205
+ (id, message) => { executor.injectReply(id, message); ctx.ui.notify(`Replied to ${id}: ${message.slice(0, 50)}...`, "info"); tui.requestRender(); }
206
+ );
207
+ return { render: (w: number) => dashboard.render(w), invalidate: () => dashboard.invalidate(), handleInput: (data: string) => { dashboard.handleInput(data); tui.requestRender(); } };
208
+ });
209
+ },
210
+ });
211
+ // ── End /agents alias ───────────────────────────────────────
212
+
181
213
  await ctx.ui.custom<void>((tui, theme, _kb, done) => {
182
214
  const dashboard = createDashboard(
183
215
  registry,
@@ -194,6 +226,12 @@ export default function (pi: ExtensionAPI) {
194
226
  const preview = t.tasks.map((tk) => `${tk.id}: ${tk.result?.slice(0, 100) ?? tk.error ?? "(pending)"}`).join("\n");
195
227
  ctx.ui.notify(`Thread ${id} results:\n${preview}`, "info");
196
228
  }
229
+ },
230
+ (id, message) => {
231
+ // Inline reply β€” send message to blocked thread
232
+ executor.injectReply(id, message);
233
+ ctx.ui.notify(`Replied to ${id}: ${message.slice(0, 50)}...`, "info");
234
+ tui.requestRender();
197
235
  }
198
236
  );
199
237
 
@@ -542,7 +580,7 @@ export default function (pi: ExtensionAPI) {
542
580
  parameters: Type.Object({
543
581
  id: Type.Optional(Type.String({ description: "Thread ID (t-001) or Story ID (s-001). Omit for all." })),
544
582
  }),
545
- async execute(_toolCallId, params) {
583
+ async execute(_toolCallId, params, _signal?: any, _onUpdate?: any, _ctx?: any) {
546
584
  if (params.id) {
547
585
  // Check threads first
548
586
  const t = registry.get(params.id);
@@ -579,7 +617,7 @@ export default function (pi: ExtensionAPI) {
579
617
  };
580
618
  }
581
619
 
582
- return { content: [{ type: "text", text: `ID ${params.id} not found` }], isError: true };
620
+ return { content: [{ type: "text", text: `ID ${params.id} not found` }], isError: true } as any;
583
621
  }
584
622
 
585
623
  // All
@@ -604,10 +642,10 @@ export default function (pi: ExtensionAPI) {
604
642
  }
605
643
 
606
644
  if (lines.length === 0) {
607
- return { content: [{ type: "text", text: "No threads or stories." }] };
645
+ return { content: [{ type: "text", text: "No threads or stories." }] } as any;
608
646
  }
609
647
 
610
- return { content: [{ type: "text", text: lines.join("\n") }] };
648
+ return { content: [{ type: "text", text: lines.join("\n") }] } as any;
611
649
  },
612
650
  });
613
651
 
@@ -618,11 +656,11 @@ export default function (pi: ExtensionAPI) {
618
656
  parameters: Type.Object({
619
657
  id: Type.String({ description: "Thread ID to kill" }),
620
658
  }),
621
- async execute(_toolCallId, params) {
659
+ async execute(_toolCallId, params, _signal?: any, _onUpdate?: any, _ctx?: any) {
622
660
  const t = registry.get(params.id);
623
661
  if (!t) return { content: [{ type: "text", text: `Thread ${params.id} not found` }], isError: true };
624
662
  registry.kill(params.id);
625
- return { content: [{ type: "text", text: `Thread ${params.id} killed.` }] };
663
+ return { content: [{ type: "text", text: `Thread ${params.id} killed.` }] } as any;
626
664
  },
627
665
  });
628
666
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-thread-engine",
3
- "version": "0.2.4",
4
- "description": "Thread engineering for pi β€” all 7 thread types, stories, fusion, zero-touch, TUI dashboard",
3
+ "version": "0.4.1",
4
+ "description": "Thread-Based Engineering for pi β€” all 7 thread types + stories + fusion + zero-touch + TUI dashboard. Based on @IndyDevDan framework from agenticengineer.com.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "artale",
@@ -43,12 +43,16 @@
43
43
  "extensions/",
44
44
  "src/",
45
45
  "README.md",
46
- "PLAN.md"
46
+ "PLAN.md",
47
+ "docs/"
47
48
  ],
48
49
  "peerDependencies": {
49
- "@mariozechner/pi-ai": "^0.56.0",
50
- "@mariozechner/pi-coding-agent": "^0.56.0",
51
- "@mariozechner/pi-tui": "^0.56.0",
50
+ "@mariozechner/pi-coding-agent": ">=0.73.0",
51
+ "@mariozechner/pi-tui": ">=0.73.0",
52
+ "@mariozechner/pi-ai": ">=0.73.0",
52
53
  "@sinclair/typebox": "^0.34.48"
54
+ },
55
+ "devDependencies": {
56
+ "typescript": "^6.0.3"
53
57
  }
54
58
  }
@@ -197,4 +197,24 @@ export class ThreadExecutor {
197
197
  return;
198
198
  }
199
199
  }
200
+
201
+ /** Inject a reply into a running thread β€” sends message to its session */
202
+ injectReply(threadId: string, message: string): void {
203
+ const thread = this.registry.get(threadId);
204
+ if (!thread) return;
205
+
206
+ // Mark the blocked task as no longer needing input
207
+ for (const task of thread.tasks) {
208
+ if (task.state === "needs_input") {
209
+ task.state = "running";
210
+ }
211
+ }
212
+
213
+ // Forward the reply to the running subagent session via sendUserMessage
214
+ // The subagent will pick up the new context and continue
215
+ this.pi.sendUserMessage(
216
+ `[Thread ${threadId}] Reply to blocked thread: ${message}`,
217
+ { deliverAs: "followUp" }
218
+ );
219
+ }
200
220
  }
package/src/core/types.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  export type ThreadType = "base" | "parallel" | "chained" | "fusion" | "meta" | "long" | "zero";
3
3
 
4
4
  /** Thread lifecycle states */
5
- export type ThreadState = "pending" | "running" | "paused" | "completed" | "failed" | "killed";
5
+ export type ThreadState = "pending" | "running" | "paused" | "completed" | "failed" | "killed" | "needs_input";
6
6
 
7
7
  /** How a thread is executed */
8
8
  export type ExecutionBackend = "subagent" | "native";
package/src/dashboard.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Interactive TUI dashboard for pi-threads.
3
- * Shows threads + stories in a navigable overlay.
4
- *
5
- * Keys: ↑↓ navigate, Enter expand, k kill, r review, p prune, q/Esc close
2
+ * Thread Dashboard v2 β€” Agent View-style grouping + inline reply + search
3
+ * Groups: Needs Input | Working | Done
4
+ * Keys: ↑↓ navigate, Enter expand, i reply, / search, k kill, p prune, q close
6
5
  */
7
6
  import { matchesKey, Key, truncateToWidth } from "@mariozechner/pi-tui";
8
7
  import type { ThreadRegistry } from "./core/registry.js";
@@ -12,11 +11,23 @@ export interface DashboardTheme {
12
11
  bold: (text: string) => string;
13
12
  }
14
13
 
15
- interface DashboardRow {
14
+ interface Row {
16
15
  id: string;
17
16
  kind: "thread" | "story";
18
17
  label: string;
19
- display: string;
18
+ state: string;
19
+ progress: string;
20
+ elapsed: string;
21
+ result: string;
22
+ error: string;
23
+ type: string;
24
+ }
25
+
26
+ interface Group {
27
+ name: string;
28
+ icon: string;
29
+ color: string;
30
+ rows: Row[];
20
31
  }
21
32
 
22
33
  export function createDashboard(
@@ -24,13 +35,21 @@ export function createDashboard(
24
35
  theme: DashboardTheme,
25
36
  onClose: () => void,
26
37
  onKill?: (id: string) => void,
27
- onReview?: (id: string) => void
38
+ onReview?: (id: string) => void,
39
+ onReply?: (id: string, message: string) => void
28
40
  ) {
29
41
  let selected = 0;
30
42
  let expanded: string | null = null;
31
- let rows: DashboardRow[] = [];
43
+ let searchQuery = "";
44
+ let showSearch = false;
45
+ let replyTarget: string | null = null;
46
+ let replyBuffer = "";
47
+ let groups: Group[] = [];
32
48
  let cachedWidth: number | undefined;
33
49
 
50
+ // Safety: cap at 100 rows to prevent terminal overflow
51
+ const MAX_ROWS = 100;
52
+
34
53
  function stateIcon(state: string): string {
35
54
  switch (state) {
36
55
  case "running": return "⟳";
@@ -41,6 +60,7 @@ export function createDashboard(
41
60
  case "executing": return "⚑";
42
61
  case "verifying": return "πŸ”";
43
62
  case "done": return "βœ…";
63
+ case "needs_input": return "⚠";
44
64
  default: return "?";
45
65
  }
46
66
  }
@@ -50,6 +70,7 @@ export function createDashboard(
50
70
  case "running": case "executing": return "warning";
51
71
  case "completed": case "done": return "success";
52
72
  case "failed": case "killed": return "error";
73
+ case "needs_input": return "warning";
53
74
  default: return "muted";
54
75
  }
55
76
  }
@@ -66,25 +87,86 @@ export function createDashboard(
66
87
  }
67
88
  }
68
89
 
69
- function buildRows(): DashboardRow[] {
70
- const result: DashboardRow[] = [];
90
+ function buildGroups(): Group[] {
91
+ const allThreads = registry.all();
92
+ const allStories = registry.allStories();
71
93
 
72
- // Stories first
73
- for (const s of registry.allStories()) {
74
- const phases = s.phases.map((p) => `${stateIcon(p.state)}${p.name}`).join("β†’");
75
- const display = `πŸ“– ${theme.fg("accent", s.id)} [${theme.fg(stateColor(s.state), s.state)}] ${s.goal.slice(0, 45)} ${theme.fg("dim", phases)}`;
76
- result.push({ id: s.id, kind: "story", label: s.goal, display });
77
- }
94
+ const needsInput: Row[] = [];
95
+ const working: Row[] = [];
96
+ const done: Row[] = [];
78
97
 
79
- // Then threads
80
- for (const t of registry.all()) {
98
+ for (const t of allThreads) {
81
99
  const sum = registry.summarize(t);
82
- const be = sum.backend === "subagent" ? theme.fg("dim", " [sub]") : "";
83
- const display = `${typeIcon(sum.type)} ${theme.fg("accent", sum.id)} ${sum.type} [${theme.fg(stateColor(sum.state), sum.state)}] ${sum.progress} (${sum.elapsed})${be} ${sum.label.slice(0, 40)}`;
84
- result.push({ id: sum.id, kind: "thread", label: sum.label, display });
100
+ const row: Row = {
101
+ id: sum.id,
102
+ kind: "thread",
103
+ label: sum.label,
104
+ state: sum.state,
105
+ progress: sum.progress,
106
+ elapsed: sum.elapsed,
107
+ result: t.tasks.find(x => x.result)?.result?.slice(0, 80) ?? "",
108
+ error: t.tasks.find(x => x.error)?.error?.slice(0, 80) ?? "",
109
+ type: t.type ?? "",
110
+ };
111
+ if (sum.state === "needs_input") needsInput.push(row);
112
+ else if (["running", "pending", "executing", "verifying", "planning"].includes(sum.state as string)) working.push(row);
113
+ else done.push(row);
114
+ }
115
+
116
+ for (const s of allStories) {
117
+ const row: Row = {
118
+ id: s.id,
119
+ kind: "story",
120
+ label: s.goal,
121
+ state: s.state,
122
+ progress: "",
123
+ elapsed: "",
124
+ result: "",
125
+ error: "",
126
+ type: "story",
127
+ };
128
+ if ((s.state as string) === "done" || (s.state as string) === "failed" || (s.state as string) === "completed") done.push(row);
129
+ else working.push(row);
130
+ }
131
+
132
+ const result: Group[] = [];
133
+ if (needsInput.length > 0) result.push({ name: "Needs Input", icon: "⚠", color: "warning", rows: needsInput });
134
+ if (working.length > 0) result.push({ name: "Working", icon: "⟳", color: "warning", rows: working });
135
+ if (done.length > 0) result.push({ name: "Done", icon: "βœ“", color: "success", rows: done });
136
+
137
+ if (searchQuery) {
138
+ const q = searchQuery.toLowerCase();
139
+ for (const g of result) {
140
+ g.rows = g.rows.filter(r => r.label.toLowerCase().includes(q) || r.id.toLowerCase().includes(q));
141
+ }
142
+ }
143
+
144
+ for (const g of result) {
145
+ if (g.rows.length > MAX_ROWS) g.rows.length = MAX_ROWS;
146
+ }
147
+
148
+ return result.filter(g => g.rows.length > 0);
149
+ }
150
+
151
+ function totalRows(): number {
152
+ let n = 0;
153
+ for (const g of groups) n += g.rows.length;
154
+ return n;
155
+ }
156
+
157
+ function getSelected(): { group: number; row: number } | null {
158
+ let idx = 0;
159
+ for (let gi = 0; gi < groups.length; gi++) {
160
+ for (let ri = 0; ri < groups[gi].rows.length; ri++) {
161
+ if (idx === selected) return { group: gi, row: ri };
162
+ idx++;
163
+ }
85
164
  }
165
+ return null;
166
+ }
86
167
 
87
- return result;
168
+ function ensureGroups() {
169
+ if (groups.length === 0) groups = buildGroups();
88
170
  }
89
171
 
90
172
  function renderExpanded(id: string, width: number): string[] {
@@ -92,7 +174,6 @@ export function createDashboard(
92
174
  const indent = " ";
93
175
  const maxW = width - 6;
94
176
 
95
- // Thread detail
96
177
  const t = registry.get(id);
97
178
  if (t) {
98
179
  lines.push(theme.fg("accent", theme.bold(` Thread ${t.id} (${t.type}) β€” ${t.state}`)));
@@ -113,7 +194,6 @@ export function createDashboard(
113
194
  return lines;
114
195
  }
115
196
 
116
- // Story detail
117
197
  const s = registry.getStory(id);
118
198
  if (s) {
119
199
  lines.push(theme.fg("accent", theme.bold(` Story ${s.id} β€” ${s.state}`)));
@@ -132,82 +212,177 @@ export function createDashboard(
132
212
  return [theme.fg("error", ` ${id} not found`)];
133
213
  }
134
214
 
215
+ function tw(s: string, w: number): string {
216
+ return truncateToWidth(s, Math.max(1, w));
217
+ }
218
+
135
219
  const component = {
136
220
  handleInput(data: string) {
221
+ if (replyTarget !== null) {
222
+ if (matchesKey(data, Key.enter)) {
223
+ onReply?.(replyTarget, replyBuffer);
224
+ replyTarget = null;
225
+ replyBuffer = "";
226
+ cachedWidth = undefined;
227
+ } else if (matchesKey(data, Key.escape)) {
228
+ replyTarget = null;
229
+ replyBuffer = "";
230
+ cachedWidth = undefined;
231
+ } else if (data.length === 1 && !data.startsWith("\x1b") && data !== "[" && data !== "o") {
232
+ replyBuffer += data;
233
+ cachedWidth = undefined;
234
+ } else if (data === "Backspace" || data === "\x7f") {
235
+ replyBuffer = replyBuffer.slice(0, -1);
236
+ cachedWidth = undefined;
237
+ }
238
+ return;
239
+ }
240
+
137
241
  if (matchesKey(data, Key.escape) || data === "q") {
138
242
  onClose();
139
243
  return;
140
244
  }
141
- if (matchesKey(data, Key.up) && selected > 0) {
142
- selected--;
143
- expanded = null;
245
+ if (data === "/") {
246
+ showSearch = !showSearch;
247
+ if (!showSearch) { searchQuery = ""; }
144
248
  cachedWidth = undefined;
249
+ return;
145
250
  }
146
- if (matchesKey(data, Key.down) && selected < rows.length - 1) {
147
- selected++;
148
- expanded = null;
251
+ if (showSearch && data.length === 1) {
252
+ searchQuery += data;
149
253
  cachedWidth = undefined;
254
+ return;
150
255
  }
151
- if (matchesKey(data, Key.enter) && rows[selected]) {
152
- expanded = expanded === rows[selected].id ? null : rows[selected].id;
256
+ if (showSearch && (data === "Backspace" || data === "\x7f")) {
257
+ searchQuery = searchQuery.slice(0, -1);
153
258
  cachedWidth = undefined;
259
+ return;
154
260
  }
155
- if (data === "k" && rows[selected]) {
156
- onKill?.(rows[selected].id);
157
- cachedWidth = undefined;
261
+
262
+ const total = totalRows();
263
+ if (matchesKey(data, Key.up) && selected > 0) { selected--; ensureGroups(); }
264
+ if (matchesKey(data, Key.down) && selected < total - 1) { selected++; ensureGroups(); }
265
+
266
+ if (data === "i") {
267
+ const sel = getSelected();
268
+ if (sel) {
269
+ replyTarget = groups[sel.group].rows[sel.row].id;
270
+ replyBuffer = "";
271
+ cachedWidth = undefined;
272
+ }
273
+ return;
158
274
  }
159
- if (data === "r" && rows[selected]) {
160
- onReview?.(rows[selected].id);
161
- cachedWidth = undefined;
275
+ if (matchesKey(data, Key.enter)) {
276
+ const sel = getSelected();
277
+ if (sel) {
278
+ const id = groups[sel.group].rows[sel.row].id;
279
+ expanded = expanded === id ? null : id;
280
+ cachedWidth = undefined;
281
+ }
282
+ return;
162
283
  }
163
- if (data === "p") {
164
- registry.prune();
165
- cachedWidth = undefined;
284
+ if (data === "k") {
285
+ const sel = getSelected();
286
+ if (sel) { onKill?.(groups[sel.group].rows[sel.row].id); cachedWidth = undefined; }
287
+ return;
166
288
  }
289
+ if (data === "r") {
290
+ const sel = getSelected();
291
+ if (sel) { onReview?.(groups[sel.group].rows[sel.row].id); cachedWidth = undefined; }
292
+ return;
293
+ }
294
+ if (data === "p") { registry.prune(); ensureGroups(); cachedWidth = undefined; }
167
295
  },
168
296
 
169
297
  render(width: number): string[] {
170
- // Rebuild rows every render (state changes)
171
- rows = buildRows();
172
-
173
- if (selected >= rows.length) selected = Math.max(0, rows.length - 1);
298
+ groups = buildGroups();
299
+ const total = totalRows();
300
+ if (selected >= total && total > 0) selected = total - 1;
174
301
 
175
302
  const lines: string[] = [];
176
303
  const border = "─".repeat(Math.min(width - 4, 80));
177
304
 
178
- // Header
179
305
  lines.push("");
180
306
  lines.push(theme.fg("accent", theme.bold(" 🧡 Thread Dashboard")));
181
- lines.push(theme.fg("dim", ` ${border}`));
307
+ lines.push(theme.fg("dim", ` ${tw(border, width)}`));
182
308
 
183
- if (rows.length === 0) {
309
+ if (groups.length === 0 && total === 0) {
184
310
  lines.push("");
185
311
  lines.push(theme.fg("muted", " No threads or stories."));
186
312
  lines.push(theme.fg("dim", " Use /pthread /fthread /zthread /story to start."));
187
313
  } else {
188
- lines.push("");
189
- for (let i = 0; i < rows.length; i++) {
190
- const prefix = i === selected ? theme.fg("accent", " β–Έ ") : " ";
191
- const row = rows[i];
192
- const line = prefix + truncateToWidth(row.display, width - 4);
193
- lines.push(line);
194
-
195
- // Show expanded detail
196
- if (expanded === row.id) {
197
- lines.push(...renderExpanded(row.id, width));
198
- lines.push("");
314
+ let flatIdx = 0;
315
+ for (const g of groups) {
316
+ // Group header
317
+ const gColor = g.color === "success" ? "success" : "accent";
318
+ lines.push("");
319
+ lines.push(theme.fg(gColor, ` ${g.icon} ${g.name} (${g.rows.length})`));
320
+
321
+ for (const row of g.rows) {
322
+ const isSelected = flatIdx === selected;
323
+ const prefix = isSelected ? theme.fg("accent", " β–Έ ") : " ";
324
+
325
+ let display: string;
326
+ if (row.kind === "story") {
327
+ const s = registry.getStory(row.id);
328
+ if (s) {
329
+ const phases = s.phases.map(p => `${stateIcon(p.state)}${p.name}`).join("β†’");
330
+ display = `πŸ“– ${theme.fg("accent", row.id)} [${theme.fg(stateColor(row.state), row.state)}] ${tw(row.label, 22)} ${theme.fg("dim", tw(phases, 16))}`;
331
+ } else {
332
+ display = `πŸ“– ${theme.fg("accent", row.id)} ${tw(row.label, 50)}`;
333
+ }
334
+ } else {
335
+ // Thread with progress bar + last output snippet
336
+ let progressBar = "β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘β–‘";
337
+ if (row.result) {
338
+ progressBar = "β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ"; // done
339
+ } else if (row.state === "running" || row.state === "executing") {
340
+ progressBar = "β–ˆβ–ˆβ–ˆβ–ˆβ–‘β–‘β–‘β–‘β–‘β–‘"; // in progress
341
+ } else if (row.state === "failed") {
342
+ progressBar = "βœ—βœ—βœ—βœ—βœ—βœ—βœ—βœ—βœ—βœ—"; // failed
343
+ }
344
+ const snippet = row.result
345
+ ? `β†’${row.result.slice(0, 40)}`
346
+ : row.error
347
+ ? `βœ—${row.error.slice(0, 40)}`
348
+ : row.elapsed
349
+ ? `⏱${row.elapsed}`
350
+ : "";
351
+ display = `${typeIcon(row.type)} ${theme.fg("accent", row.id)} ${progressBar} [${theme.fg(stateColor(row.state), row.state)}] ${tw(row.label, 20)} ${theme.fg("muted", tw(snippet, 22))}`;
352
+ }
353
+
354
+ lines.push(tw(prefix + display, width));
355
+
356
+ if (isSelected && expanded === row.id) {
357
+ lines.push(...renderExpanded(row.id, width));
358
+ lines.push("");
359
+ }
360
+ flatIdx++;
199
361
  }
200
362
  }
201
363
  }
202
364
 
365
+ // Reply mode banner
366
+ if (replyTarget !== null) {
367
+ lines.push("");
368
+ lines.push(tw(theme.fg("warning", theme.bold(" β”Œβ”€ REPLY ─────────────────────────────┐")), width));
369
+ lines.push(tw(theme.fg("warning", ` β”‚ ${tw(replyTarget || "", 28).padEnd(28)} β”‚`), width));
370
+ lines.push(tw(theme.fg("warning", ` β”‚ ${tw(replyBuffer || "(type message)", 28).padEnd(28)} β”‚`), width));
371
+ lines.push(tw(theme.fg("warning", ` β”‚ Enter=send Esc=cancel β”‚`), width));
372
+ lines.push(tw(theme.fg("warning", theme.bold(" β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜")), width));
373
+ }
374
+
203
375
  // Footer
204
376
  lines.push("");
205
- lines.push(theme.fg("dim", ` ${border}`));
206
- const help = "↑↓ navigate Enter expand k kill r review p prune q close";
377
+ lines.push(theme.fg("dim", ` ${tw(border, width)}`));
378
+ const help = showSearch
379
+ ? tw(`Search: ${searchQuery}_ Enter done Esc cancel`, width - 4)
380
+ : tw("nav=↑↓ exp=Enter rep=i srch=/ kill=k rev=r prune=p quit=q", width - 4);
207
381
  lines.push(theme.fg("dim", ` ${help}`));
208
382
  lines.push("");
209
383
 
210
- return lines;
384
+ // Final safety: truncate ALL lines
385
+ return lines.map(l => tw(l, width));
211
386
  },
212
387
 
213
388
  invalidate() {
@@ -216,4 +391,4 @@ export function createDashboard(
216
391
  };
217
392
 
218
393
  return component;
219
- }
394
+ }