pi-messenger 0.7.3

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.
Files changed (45) hide show
  1. package/ARCHITECTURE.md +244 -0
  2. package/CHANGELOG.md +418 -0
  3. package/README.md +394 -0
  4. package/banner.png +0 -0
  5. package/config-overlay.ts +172 -0
  6. package/config.ts +178 -0
  7. package/crew/agents/crew-docs-scout.md +55 -0
  8. package/crew/agents/crew-gap-analyst.md +105 -0
  9. package/crew/agents/crew-github-scout.md +111 -0
  10. package/crew/agents/crew-interview-generator.md +79 -0
  11. package/crew/agents/crew-plan-sync.md +64 -0
  12. package/crew/agents/crew-practice-scout.md +62 -0
  13. package/crew/agents/crew-repo-scout.md +65 -0
  14. package/crew/agents/crew-reviewer.md +58 -0
  15. package/crew/agents/crew-web-scout.md +85 -0
  16. package/crew/agents/crew-worker.md +95 -0
  17. package/crew/agents.ts +200 -0
  18. package/crew/handlers/interview.ts +211 -0
  19. package/crew/handlers/plan.ts +358 -0
  20. package/crew/handlers/review.ts +341 -0
  21. package/crew/handlers/status.ts +257 -0
  22. package/crew/handlers/sync.ts +232 -0
  23. package/crew/handlers/task.ts +511 -0
  24. package/crew/handlers/work.ts +289 -0
  25. package/crew/id-allocator.ts +44 -0
  26. package/crew/index.ts +229 -0
  27. package/crew/state.ts +116 -0
  28. package/crew/store.ts +480 -0
  29. package/crew/types.ts +164 -0
  30. package/crew/utils/artifacts.ts +65 -0
  31. package/crew/utils/config.ts +104 -0
  32. package/crew/utils/discover.ts +170 -0
  33. package/crew/utils/install.ts +373 -0
  34. package/crew/utils/progress.ts +107 -0
  35. package/crew/utils/result.ts +16 -0
  36. package/crew/utils/truncate.ts +79 -0
  37. package/crew-overlay.ts +259 -0
  38. package/handlers.ts +799 -0
  39. package/index.ts +591 -0
  40. package/lib.ts +232 -0
  41. package/overlay.ts +687 -0
  42. package/package.json +20 -0
  43. package/skills/pi-messenger-crew/SKILL.md +140 -0
  44. package/store.ts +1068 -0
  45. package/tsconfig.json +19 -0
package/README.md ADDED
@@ -0,0 +1,394 @@
1
+ <p>
2
+ <img src="banner.png" alt="pi-messenger" width="1100">
3
+ </p>
4
+
5
+ # Pi Messenger
6
+
7
+ **What if multiple agents in different terminals sharing a folder could talk to each other like they're in a chat room?** Join, see who's online. Claim tasks, reserve files, send messages. Built on [Pi's](https://github.com/badlogic/pi-mono) extension system. No daemon, no server, just files.
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=for-the-badge)](LICENSE)
10
+ [![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux-blue?style=for-the-badge)]()
11
+
12
+ > ⚠️ **Beta** - Core messaging and file reservations are stable. **Crew task orchestration** (plan/work/review) is newer and not fully tested yet. Please [open an issue](https://github.com/nicobailon/pi-messenger/issues) if you encounter problems.
13
+
14
+ Pi-messenger adds a `pi_messenger` tool that **agents use** for coordination. You don't type these commands - you ask your agent to do things, and it calls `pi_messenger` behind the scenes.
15
+
16
+ ## Quick Start
17
+
18
+ ### Multi-Agent Coordination
19
+
20
+ Once joined (manually or via auto-join config), agents can coordinate:
21
+
22
+ ```typescript
23
+ pi_messenger({ action: "reserve", paths: ["src/auth/"], reason: "Refactoring" })
24
+ // → Reserved src/auth/ - other agents will be blocked
25
+
26
+ // ... does the work ...
27
+
28
+ pi_messenger({ action: "release" })
29
+ // → Released all reservations
30
+ ```
31
+
32
+ > **Tip:** Set `autoRegister: true` in your config to auto-join on startup. Otherwise, agents join with `pi_messenger({ action: "join" })`.
33
+
34
+ ### Crew Task Orchestration
35
+
36
+ Ask your agent to plan and execute from a PRD:
37
+
38
+ ```typescript
39
+ pi_messenger({ action: "plan" })
40
+ // → Scouts analyze codebase, gap-analyst creates tasks
41
+
42
+ pi_messenger({ action: "work", autonomous: true })
43
+ // → Workers execute tasks in waves until done
44
+ ```
45
+
46
+ > **Note:** Crew agents (scouts, workers, reviewers) automatically join the mesh as their first action.
47
+
48
+ ## Install
49
+
50
+ Copy to your extensions directory and restart pi:
51
+
52
+ ```
53
+ ~/.pi/agent/extensions/pi-messenger/
54
+ ```
55
+
56
+ After joining, your agent name appears in the status bar:
57
+
58
+ ```
59
+ msg: SwiftRaven (2 peers) ●3
60
+ ```
61
+
62
+ ## Features
63
+
64
+ **Discovery** - Agents register with memorable names (SwiftRaven, IronKnight). See who's active, what model they're using, which git branch they're on.
65
+
66
+ **Messaging** - Send messages between agents. Recipients wake up immediately and see the message as a steering prompt. Great for handoffs and coordination.
67
+
68
+ **File Reservations** - Claim files or directories. Other agents get blocked with a clear message telling them who to coordinate with. Auto-releases on exit.
69
+
70
+ **Swarm Coordination** - Multiple agents work on the same spec file. Claim tasks atomically, mark them complete, see who's doing what.
71
+
72
+ ## Crew: Task Orchestration
73
+
74
+ Crew provides multi-agent task orchestration with a simplified PRD-based workflow.
75
+
76
+ ### Basic Workflow
77
+
78
+ 1. **Plan** - Scouts analyze your codebase and PRD, gap-analyst creates tasks
79
+ 2. **Work** - Workers implement tasks in parallel waves
80
+ 3. **Review** - Reviewer checks each implementation
81
+
82
+ ```typescript
83
+ // Plan from your PRD (auto-discovers PRD.md, SPEC.md, etc.)
84
+ pi_messenger({ action: "plan" })
85
+
86
+ // Or specify PRD path explicitly
87
+ pi_messenger({ action: "plan", prd: "docs/PRD.md" })
88
+
89
+ // Execute tasks (spawns parallel workers)
90
+ pi_messenger({ action: "work" })
91
+
92
+ // Or run autonomously until done/blocked
93
+ pi_messenger({ action: "work", autonomous: true })
94
+
95
+ // Review a specific task
96
+ pi_messenger({ action: "review", target: "task-1" })
97
+ // → SHIP ✅ or NEEDS_WORK 🔄
98
+ ```
99
+
100
+ ### Crew API
101
+
102
+ **Planning**
103
+ | Action | Description | Example |
104
+ |--------|-------------|---------|
105
+ | `plan` | Create plan from PRD | `{ action: "plan" }` or `{ action: "plan", prd: "..." }` |
106
+ | `status` | Show progress | `{ action: "status" }` |
107
+
108
+ **Work Execution**
109
+ | Action | Description | Example |
110
+ |--------|-------------|---------|
111
+ | `work` | Run ready tasks | `{ action: "work" }` |
112
+ | `work` (auto) | Run until done/blocked | `{ action: "work", autonomous: true }` |
113
+
114
+ **Task Management**
115
+ | Action | Description | Example |
116
+ |--------|-------------|---------|
117
+ | `task.show` | Show task details | `{ action: "task.show", id: "task-1" }` |
118
+ | `task.list` | List all tasks | `{ action: "task.list" }` |
119
+ | `task.start` | Start task | `{ action: "task.start", id: "task-1" }` |
120
+ | `task.done` | Complete task | `{ action: "task.done", id: "task-1", summary: "..." }` |
121
+ | `task.block` | Block task | `{ action: "task.block", id: "task-1", reason: "..." }` |
122
+ | `task.unblock` | Unblock task | `{ action: "task.unblock", id: "task-1" }` |
123
+ | `task.ready` | List ready tasks | `{ action: "task.ready" }` |
124
+ | `task.reset` | Reset task | `{ action: "task.reset", id: "task-1", cascade: true }` |
125
+
126
+ **Review**
127
+ | Action | Description | Example |
128
+ |--------|-------------|---------|
129
+ | `review` | Review implementation | `{ action: "review", target: "task-1" }` |
130
+
131
+ **Maintenance**
132
+ | Action | Description | Example |
133
+ |--------|-------------|---------|
134
+ | `crew.status` | Overall status | `{ action: "crew.status" }` |
135
+ | `crew.validate` | Validate plan | `{ action: "crew.validate" }` |
136
+ | `crew.agents` | List crew agents | `{ action: "crew.agents" }` |
137
+ | `crew.install` | Install crew agents | `{ action: "crew.install" }` |
138
+
139
+ ### Planning Workflow
140
+
141
+ The `plan` action orchestrates a multi-agent analysis:
142
+
143
+ ```
144
+ ┌─────────────────────────────────────────────────────────────────┐
145
+ │ Your Project │
146
+ │ ├── PRD.md ◄── Scouts discover and read these │
147
+ │ ├── DESIGN.md │
148
+ │ ├── src/ │
149
+ │ └── ... │
150
+ └─────────────────────────────────────────────────────────────────┘
151
+
152
+
153
+ ┌─────────────────────────────────────────────────────────────────┐
154
+ │ Phase 1: Scouts (parallel) │
155
+ │ ├── crew-repo-scout → Analyzes codebase structure │
156
+ │ ├── crew-docs-scout → Reads project documentation │
157
+ │ ├── crew-practice-scout → Finds coding conventions │
158
+ │ ├── crew-web-scout → Searches web for best practices │
159
+ │ └── crew-github-scout → Examines real repos via gh CLI │
160
+ └─────────────────────────────────────────────────────────────────┘
161
+
162
+
163
+ ┌─────────────────────────────────────────────────────────────────┐
164
+ │ Phase 2: Gap Analyst │
165
+ │ └── Synthesizes findings → Creates task breakdown │
166
+ └─────────────────────────────────────────────────────────────────┘
167
+
168
+
169
+ ┌─────────────────────────────────────────────────────────────────┐
170
+ │ Result: Tasks with Dependencies │
171
+ │ ├── task-1: Setup types (no deps) │
172
+ │ ├── task-2: Core logic (depends on task-1) │
173
+ │ ├── task-3: API endpoints (depends on task-1) │
174
+ │ └── task-4: Tests (depends on task-2, task-3) │
175
+ └─────────────────────────────────────────────────────────────────┘
176
+ ```
177
+
178
+ **No special format required** - just put your docs in the project. Scouts will find and read markdown files, READMEs, and code comments.
179
+
180
+ ### Autonomous Mode
181
+
182
+ Run tasks continuously until completion:
183
+
184
+ ```typescript
185
+ pi_messenger({ action: "work", autonomous: true })
186
+ ```
187
+
188
+ Autonomous mode:
189
+ - Executes waves of parallel workers
190
+ - Reviews each task after completion
191
+ - Auto-blocks on failure
192
+ - Stops when all tasks done or blocked
193
+ - Respects `maxWaves` limit (default: 50)
194
+
195
+ ### Crew Overlay Tab
196
+
197
+ The `/messenger` overlay includes a Crew tab showing task status:
198
+
199
+ ```
200
+ ╭─ Messenger ── SwiftRaven ── 2 peers ─────────────────╮
201
+ │ Agents │ ▸ Crew (2/5) │ ● GoldFalcon │ + All │
202
+ ├──────────────────────────────────────────────────────┤
203
+ │ │
204
+ │ 📋 docs/PRD.md [2/5] │
205
+ │ │
206
+ │ ✓ task-1 Setup OAuth config │
207
+ │ ✓ task-2 Implement token storage │
208
+ │ ● task-3 Add Google provider (SwiftRaven) │
209
+ │ ○ task-4 Add GitHub provider → task-2 │
210
+ │ ○ task-5 Write tests → task-3, task-4 │
211
+ │ │
212
+ ├──────────────────────────────────────────────────────┤
213
+ │ ● AUTO Wave 2 │ 2/5 done │ 1 ready │ ⏱️ 3:42 │
214
+ ╰──────────────────────────────────────────────────────╯
215
+ ```
216
+
217
+ ### Crew Data Storage
218
+
219
+ ```
220
+ .pi/messenger/crew/
221
+ ├── plan.json # Plan metadata (PRD path, progress)
222
+ ├── plan.md # Gap analyst output
223
+ ├── tasks/
224
+ │ ├── task-1.json # Task metadata
225
+ │ ├── task-1.md # Task specification
226
+ │ └── ...
227
+ ├── artifacts/ # Debug artifacts
228
+ └── config.json # Project-level crew config
229
+ ```
230
+
231
+ ### Crew Configuration
232
+
233
+ Add to `~/.pi/agent/pi-messenger.json`:
234
+
235
+ ```json
236
+ {
237
+ "crew": {
238
+ "concurrency": { "scouts": 4, "workers": 2 },
239
+ "review": { "enabled": true, "maxIterations": 3 },
240
+ "work": { "maxAttemptsPerTask": 5, "maxWaves": 50 }
241
+ }
242
+ }
243
+ ```
244
+
245
+ | Setting | Description | Default |
246
+ |---------|-------------|---------|
247
+ | `concurrency.scouts` | Max parallel scouts during planning | `4` |
248
+ | `concurrency.workers` | Max parallel workers during work | `2` |
249
+ | `review.enabled` | Auto-review tasks after completion | `true` |
250
+ | `review.maxIterations` | Max review cycles before blocking | `3` |
251
+ | `work.maxAttemptsPerTask` | Retries before blocking a task | `5` |
252
+ | `work.maxWaves` | Max waves in autonomous mode | `50` |
253
+
254
+ ### Crew Install
255
+
256
+ Crew agents are **auto-installed** on first use of `plan`, `work`, or `review`. To manually install or update:
257
+
258
+ ```typescript
259
+ pi_messenger({ action: "crew.install" })
260
+ ```
261
+
262
+ **What gets installed:**
263
+ - **10 agents** in `~/.pi/agent/agents/` (scouts, analysts, worker, reviewer)
264
+ - **1 skill** in `~/.pi/agent/skills/` (pi-messenger-crew quick reference)
265
+
266
+ To remove:
267
+ ```typescript
268
+ pi_messenger({ action: "crew.uninstall" })
269
+ ```
270
+
271
+ ## Chat Overlay
272
+
273
+ `/messenger` opens an interactive chat UI:
274
+
275
+ ```
276
+ ╭─ Messenger ── SwiftRaven ── 2 peers ────────────────╮
277
+ │ ▸ Agents │ ● GoldFalcon │ ● IronKnight (1) │ + All │
278
+ ├─────────────────────────────────────────────────────┤
279
+ │ ./feature-spec.md: │
280
+ │ SwiftRaven (you) TASK-03 Implementing auth │
281
+ │ GoldFalcon TASK-04 API endpoints │
282
+ ├─────────────────────────────────────────────────────┤
283
+ │ > Agents overview [Tab] [Enter] │
284
+ ╰─────────────────────────────────────────────────────╯
285
+ ```
286
+
287
+ | Key | Action |
288
+ |-----|--------|
289
+ | `Tab` / `←` `→` | Switch tabs |
290
+ | `↑` `↓` | Scroll history |
291
+ | `Enter` | Send message |
292
+ | `Esc` | Close |
293
+
294
+ ## Tool Reference
295
+
296
+ ### Action-Based API (Recommended)
297
+
298
+ ```typescript
299
+ pi_messenger({
300
+ action: string, // Action to perform
301
+
302
+ // Plan
303
+ prd?: string, // PRD file path
304
+
305
+ // Task identifiers
306
+ id?: string, // Task ID (task-N)
307
+ target?: string, // Target for review
308
+
309
+ // Creation
310
+ title?: string, // For task.create
311
+ dependsOn?: string[], // Task dependencies
312
+
313
+ // Completion
314
+ summary?: string, // For task.done
315
+
316
+ // Work options
317
+ autonomous?: boolean, // Run continuously
318
+ concurrency?: number, // Override concurrency
319
+
320
+ // Reset
321
+ cascade?: boolean, // Reset dependent tasks too
322
+ })
323
+ ```
324
+
325
+ ### Legacy API
326
+
327
+ ```typescript
328
+ pi_messenger({
329
+ // Join
330
+ join?: boolean, // Join the agent mesh
331
+ spec?: string, // Spec file to work on
332
+
333
+ // Swarm
334
+ swarm?: boolean, // Get swarm status
335
+ claim?: string, // Claim a task
336
+ unclaim?: string, // Release without completing
337
+ complete?: string, // Mark task complete
338
+ notes?: string, // Completion notes
339
+
340
+ // Messaging
341
+ to?: string | string[], // Recipient(s)
342
+ broadcast?: boolean, // Send to all
343
+ message?: string, // Message text
344
+
345
+ // Reservations
346
+ reserve?: string[], // Paths to reserve
347
+ reason?: string, // Why reserving/claiming
348
+ release?: string[] | true, // Release reservations
349
+
350
+ // Other
351
+ rename?: string, // Change your name
352
+ list?: boolean, // List active agents
353
+ })
354
+ ```
355
+
356
+ ## Configuration
357
+
358
+ Create `~/.pi/agent/pi-messenger.json`:
359
+
360
+ ```json
361
+ {
362
+ "autoRegister": false,
363
+ "autoRegisterPaths": ["~/projects/team-collab"],
364
+ "scopeToFolder": false
365
+ }
366
+ ```
367
+
368
+ | Setting | Description | Default |
369
+ |---------|-------------|---------|
370
+ | `autoRegister` | Join mesh on startup | `false` |
371
+ | `autoRegisterPaths` | Folders where auto-join is enabled | `[]` |
372
+ | `scopeToFolder` | Only see agents in same directory | `false` |
373
+
374
+ ## How It Works
375
+
376
+ ```
377
+ ~/.pi/agent/messenger/
378
+ ├── registry/ # Agent registrations (PID, cwd, model, spec)
379
+ ├── inbox/ # Message delivery
380
+ ├── claims.json # Active task claims
381
+ ├── completions.json # Completed tasks
382
+ └── swarm.lock # Atomic lock for claims
383
+ ```
384
+
385
+ File-based coordination. No daemon. Dead agents detected via PID and cleaned up automatically.
386
+
387
+ ## Credits
388
+
389
+ - **[mcp_agent_mail](https://github.com/Dicklesworthstone/mcp_agent_mail)** by [@doodlestein](https://x.com/doodlestein) - Inspiration for agent-to-agent messaging
390
+ - **[Pi coding agent](https://github.com/badlogic/pi-mono/)** by [@badlogicgames](https://x.com/badlogicgames)
391
+
392
+ ## License
393
+
394
+ MIT
package/banner.png ADDED
Binary file
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Pi Messenger - Config Overlay Component
3
+ */
4
+
5
+ import type { Component, Focusable, TUI } from "@mariozechner/pi-tui";
6
+ import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
7
+ import type { Theme } from "@mariozechner/pi-coding-agent";
8
+ import { getAutoRegisterPaths, saveAutoRegisterPaths, matchesAutoRegisterPath } from "./config.js";
9
+
10
+ export class MessengerConfigOverlay implements Component, Focusable {
11
+ readonly width = 60;
12
+ focused = false;
13
+
14
+ private paths: string[];
15
+ private selectedIndex = 0;
16
+ private dirty = false;
17
+ private statusMessage = "";
18
+
19
+ constructor(
20
+ private tui: TUI,
21
+ private theme: Theme,
22
+ private done: () => void
23
+ ) {
24
+ this.paths = getAutoRegisterPaths();
25
+ }
26
+
27
+ handleInput(data: string): void {
28
+ if (matchesKey(data, "escape") || matchesKey(data, "q")) {
29
+ if (this.dirty) {
30
+ saveAutoRegisterPaths(this.paths);
31
+ }
32
+ this.done();
33
+ return;
34
+ }
35
+
36
+ if (matchesKey(data, "a")) {
37
+ this.addCurrentPath();
38
+ this.tui.requestRender();
39
+ return;
40
+ }
41
+
42
+ if (matchesKey(data, "d") || matchesKey(data, "backspace")) {
43
+ this.deleteSelected();
44
+ this.tui.requestRender();
45
+ return;
46
+ }
47
+
48
+ if (matchesKey(data, "up")) {
49
+ if (this.paths.length > 0) {
50
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
51
+ this.tui.requestRender();
52
+ }
53
+ return;
54
+ }
55
+
56
+ if (matchesKey(data, "down")) {
57
+ if (this.paths.length > 0) {
58
+ this.selectedIndex = Math.min(this.paths.length - 1, this.selectedIndex + 1);
59
+ this.tui.requestRender();
60
+ }
61
+ return;
62
+ }
63
+ }
64
+
65
+ private addCurrentPath(): void {
66
+ const cwd = process.cwd();
67
+ if (this.paths.includes(cwd)) {
68
+ this.statusMessage = "Already in list";
69
+ return;
70
+ }
71
+ this.paths.push(cwd);
72
+ this.selectedIndex = this.paths.length - 1;
73
+ this.dirty = true;
74
+ this.statusMessage = "Added current folder";
75
+ }
76
+
77
+ private deleteSelected(): void {
78
+ if (this.paths.length === 0) return;
79
+
80
+ const removed = this.paths[this.selectedIndex];
81
+ this.paths.splice(this.selectedIndex, 1);
82
+ this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.paths.length - 1));
83
+ this.dirty = true;
84
+ this.statusMessage = `Removed: ${removed.split("/").pop()}`;
85
+ }
86
+
87
+ render(_width: number): string[] {
88
+ const w = this.width;
89
+ const innerW = w - 2;
90
+ const lines: string[] = [];
91
+ const cwd = process.cwd();
92
+ const isCurrentInList = matchesAutoRegisterPath(cwd, this.paths);
93
+
94
+ const border = (s: string) => this.theme.fg("dim", s);
95
+ const pad = (s: string, len: number) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
96
+ const row = (content: string) => border("│") + pad(" " + content, innerW) + border("│");
97
+ const emptyRow = () => border("│") + " ".repeat(innerW) + border("│");
98
+
99
+ // Top border with title
100
+ const titleText = " Messenger Config ";
101
+ const borderLen = innerW - titleText.length;
102
+ const leftBorder = Math.floor(borderLen / 2);
103
+ const rightBorder = borderLen - leftBorder;
104
+ lines.push(border("╭" + "─".repeat(leftBorder)) + this.theme.fg("accent", titleText) + border("─".repeat(rightBorder) + "╮"));
105
+
106
+ lines.push(emptyRow());
107
+
108
+ // Current folder status
109
+ const cwdDisplay = truncateToWidth(cwd, Math.max(10, innerW - 20));
110
+ lines.push(row(`Current folder: ${cwdDisplay}`));
111
+ const statusColor = isCurrentInList ? "accent" : "dim";
112
+ lines.push(row(`Auto-register: ${this.theme.fg(statusColor, isCurrentInList ? "YES" : "NO")}`));
113
+
114
+ lines.push(emptyRow());
115
+
116
+ // Divider
117
+ lines.push(border("├" + "─".repeat(innerW) + "┤"));
118
+
119
+ lines.push(emptyRow());
120
+ lines.push(row(this.theme.fg("dim", "Auto-register paths:")));
121
+ lines.push(emptyRow());
122
+
123
+ if (this.paths.length === 0) {
124
+ lines.push(row(this.theme.fg("dim", " (none configured)")));
125
+ } else {
126
+ for (let i = 0; i < this.paths.length; i++) {
127
+ const path = this.paths[i];
128
+ const isSelected = i === this.selectedIndex;
129
+ const isCurrent = path === cwd;
130
+
131
+ const marker = isSelected ? this.theme.fg("accent", "▸") : " ";
132
+ const suffix = isCurrent ? this.theme.fg("dim", " (current)") : "";
133
+ const pathDisplay = truncateToWidth(path, Math.max(10, innerW - 15));
134
+
135
+ if (isSelected) {
136
+ lines.push(row(`${marker} ${this.theme.fg("accent", pathDisplay)}${suffix}`));
137
+ } else {
138
+ lines.push(row(`${marker} ${pathDisplay}${suffix}`));
139
+ }
140
+ }
141
+ }
142
+
143
+ lines.push(emptyRow());
144
+
145
+ // Divider
146
+ lines.push(border("├" + "─".repeat(innerW) + "┤"));
147
+
148
+ lines.push(emptyRow());
149
+
150
+ // Status message
151
+ if (this.statusMessage) {
152
+ lines.push(row(this.theme.fg("accent", this.statusMessage)));
153
+ } else {
154
+ lines.push(emptyRow());
155
+ }
156
+
157
+ // Help
158
+ const help = "a add d delete ↑↓ navigate Esc save & close";
159
+ lines.push(row(this.theme.fg("dim", help)));
160
+
161
+ // Bottom border
162
+ lines.push(border("╰" + "─".repeat(innerW) + "╯"));
163
+
164
+ return lines;
165
+ }
166
+
167
+ invalidate(): void {
168
+ this.statusMessage = "";
169
+ }
170
+
171
+ dispose(): void {}
172
+ }