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/overlay.ts ADDED
@@ -0,0 +1,687 @@
1
+ /**
2
+ * Pi Messenger - Chat Overlay Component
3
+ */
4
+
5
+ import { randomUUID } from "node:crypto";
6
+ import type { Component, Focusable, TUI } from "@mariozechner/pi-tui";
7
+ import { matchesKey, truncateToWidth, visibleWidth } from "@mariozechner/pi-tui";
8
+ import type { Theme } from "@mariozechner/pi-coding-agent";
9
+ import {
10
+ MAX_CHAT_HISTORY,
11
+ formatRelativeTime,
12
+ coloredAgentName,
13
+ stripAnsiCodes,
14
+ extractFolder,
15
+ truncatePathLeft,
16
+ getDisplayMode,
17
+ displaySpecPath,
18
+ type MessengerState,
19
+ type Dirs,
20
+ type AgentMailMessage,
21
+ type AgentRegistration,
22
+ } from "./lib.js";
23
+ import * as store from "./store.js";
24
+ import * as crewStore from "./crew/store.js";
25
+ import {
26
+ renderCrewContent,
27
+ renderCrewStatusBar,
28
+ createCrewViewState,
29
+ navigateTask,
30
+ type CrewViewState,
31
+ } from "./crew-overlay.js";
32
+
33
+ const AGENTS_TAB = "[agents]";
34
+ const CREW_TAB = "[crew]";
35
+
36
+ export class MessengerOverlay implements Component, Focusable {
37
+ readonly width = 80;
38
+ focused = false;
39
+
40
+ private selectedAgent: string | null = null;
41
+ private inputText = "";
42
+ private scrollPosition = 0;
43
+ private cachedAgents: AgentRegistration[] | null = null;
44
+ private crewViewState: CrewViewState = createCrewViewState();
45
+ private cwd: string;
46
+
47
+ constructor(
48
+ private tui: TUI,
49
+ private theme: Theme,
50
+ private state: MessengerState,
51
+ private dirs: Dirs,
52
+ private done: () => void
53
+ ) {
54
+ this.cwd = process.cwd();
55
+ const agents = this.getAgentsSorted();
56
+ const withUnread = agents.find(a => (state.unreadCounts.get(a.name) ?? 0) > 0);
57
+ this.selectedAgent = withUnread?.name ?? agents[0]?.name ?? null;
58
+
59
+ if (this.selectedAgent) {
60
+ state.unreadCounts.set(this.selectedAgent, 0);
61
+ }
62
+ }
63
+
64
+ private getAgentsSorted(): AgentRegistration[] {
65
+ if (this.cachedAgents) return this.cachedAgents;
66
+ this.cachedAgents = store.getActiveAgents(this.state, this.dirs).sort((a, b) => a.name.localeCompare(b.name));
67
+ return this.cachedAgents;
68
+ }
69
+
70
+ private hasAnySpec(agents: AgentRegistration[]): boolean {
71
+ if (this.state.spec) return true;
72
+ return agents.some(agent => agent.spec);
73
+ }
74
+
75
+ private hasPlan(): boolean {
76
+ return crewStore.hasPlan(this.cwd);
77
+ }
78
+
79
+ private getMessages(): AgentMailMessage[] {
80
+ if (this.selectedAgent === null) {
81
+ return this.state.broadcastHistory;
82
+ }
83
+ if (this.selectedAgent === AGENTS_TAB || this.selectedAgent === CREW_TAB) {
84
+ return [];
85
+ }
86
+ return this.state.chatHistory.get(this.selectedAgent) ?? [];
87
+ }
88
+
89
+ private selectTab(agentName: string | null): void {
90
+ this.selectedAgent = agentName;
91
+ if (agentName && agentName !== AGENTS_TAB && agentName !== CREW_TAB) {
92
+ this.state.unreadCounts.set(agentName, 0);
93
+ }
94
+ this.scrollPosition = 0;
95
+ }
96
+
97
+ private scroll(delta: number): void {
98
+ const messages = this.getMessages();
99
+ const maxScroll = Math.max(0, messages.length - 1);
100
+ this.scrollPosition = Math.max(0, Math.min(maxScroll, this.scrollPosition + delta));
101
+ }
102
+
103
+ handleInput(data: string): void {
104
+ const agents = this.getAgentsSorted();
105
+
106
+ // Allow escape always
107
+ if (matchesKey(data, "escape")) {
108
+ this.done();
109
+ return;
110
+ }
111
+
112
+ // If no agents AND no plan, only allow escape
113
+ if (agents.length === 0 && !this.hasPlan()) {
114
+ return;
115
+ }
116
+
117
+ if (matchesKey(data, "tab") || matchesKey(data, "right")) {
118
+ this.cycleTab(1, agents);
119
+ this.tui.requestRender();
120
+ return;
121
+ }
122
+
123
+ if (matchesKey(data, "shift+tab") || matchesKey(data, "left")) {
124
+ this.cycleTab(-1, agents);
125
+ this.tui.requestRender();
126
+ return;
127
+ }
128
+
129
+ if (matchesKey(data, "up")) {
130
+ if (this.selectedAgent === CREW_TAB) {
131
+ // Navigate tasks in crew view
132
+ const tasks = crewStore.getTasks(this.cwd);
133
+ navigateTask(this.crewViewState, -1, tasks.length);
134
+ } else {
135
+ this.scroll(1);
136
+ }
137
+ this.tui.requestRender();
138
+ return;
139
+ }
140
+
141
+ if (matchesKey(data, "down")) {
142
+ if (this.selectedAgent === CREW_TAB) {
143
+ // Navigate tasks in crew view
144
+ const tasks = crewStore.getTasks(this.cwd);
145
+ navigateTask(this.crewViewState, 1, tasks.length);
146
+ } else {
147
+ this.scroll(-1);
148
+ }
149
+ this.tui.requestRender();
150
+ return;
151
+ }
152
+
153
+ if (matchesKey(data, "home")) {
154
+ if (this.selectedAgent === CREW_TAB) {
155
+ this.crewViewState.selectedTaskIndex = 0;
156
+ this.crewViewState.scrollOffset = 0;
157
+ } else {
158
+ const messages = this.getMessages();
159
+ this.scrollPosition = Math.max(0, messages.length - 1);
160
+ }
161
+ this.tui.requestRender();
162
+ return;
163
+ }
164
+
165
+ if (matchesKey(data, "end")) {
166
+ if (this.selectedAgent === CREW_TAB) {
167
+ const tasks = crewStore.getTasks(this.cwd);
168
+ this.crewViewState.selectedTaskIndex = Math.max(0, tasks.length - 1);
169
+ } else {
170
+ this.scrollPosition = 0;
171
+ }
172
+ this.tui.requestRender();
173
+ return;
174
+ }
175
+
176
+ if (matchesKey(data, "enter")) {
177
+ if (this.selectedAgent === CREW_TAB) {
178
+ // Enter does nothing in crew view - it's a read-only status display
179
+ return;
180
+ }
181
+ if (this.selectedAgent !== AGENTS_TAB && this.inputText.trim()) {
182
+ this.sendMessage(agents);
183
+ }
184
+ return;
185
+ }
186
+
187
+ if (matchesKey(data, "backspace")) {
188
+ if (this.inputText.length > 0) {
189
+ this.inputText = this.inputText.slice(0, -1);
190
+ this.tui.requestRender();
191
+ }
192
+ return;
193
+ }
194
+
195
+ if (data.length > 0 && data.charCodeAt(0) >= 32) {
196
+ this.inputText += data;
197
+ this.tui.requestRender();
198
+ }
199
+ }
200
+
201
+ private cycleTab(direction: number, agents: AgentRegistration[]): void {
202
+ // Build tab list: Agents, Crew (if plan exists), individual agents, All
203
+ const tabNames: (string | null)[] = [AGENTS_TAB];
204
+ if (this.hasPlan()) {
205
+ tabNames.push(CREW_TAB);
206
+ }
207
+ tabNames.push(...agents.map(a => a.name));
208
+ tabNames.push(null); // "All" broadcast tab
209
+
210
+ const currentIdx = this.selectedAgent === null
211
+ ? tabNames.length - 1
212
+ : tabNames.indexOf(this.selectedAgent);
213
+
214
+ const newIdx = (currentIdx + direction + tabNames.length) % tabNames.length;
215
+ this.selectTab(tabNames[newIdx]);
216
+ }
217
+
218
+ private sendMessage(agents: AgentRegistration[]): void {
219
+ const text = this.inputText.trim();
220
+ if (!text) return;
221
+
222
+ if (this.selectedAgent === null) {
223
+ // Broadcast: best-effort delivery to all agents
224
+ for (const agent of agents) {
225
+ try {
226
+ store.sendMessageToAgent(this.state, this.dirs, agent.name, text);
227
+ } catch {
228
+ // Ignore individual failures
229
+ }
230
+ }
231
+ // Store broadcast message regardless of send failures
232
+ const broadcastMsg: AgentMailMessage = {
233
+ id: randomUUID(),
234
+ from: this.state.agentName,
235
+ to: "broadcast",
236
+ text,
237
+ timestamp: new Date().toISOString(),
238
+ replyTo: null
239
+ };
240
+ this.state.broadcastHistory.push(broadcastMsg);
241
+ if (this.state.broadcastHistory.length > MAX_CHAT_HISTORY) {
242
+ this.state.broadcastHistory.shift();
243
+ }
244
+ this.inputText = "";
245
+ this.scrollPosition = 0;
246
+ this.tui.requestRender();
247
+ } else {
248
+ // Regular send: keep input on failure so user can retry
249
+ try {
250
+ const msg = store.sendMessageToAgent(this.state, this.dirs, this.selectedAgent, text);
251
+ let history = this.state.chatHistory.get(this.selectedAgent);
252
+ if (!history) {
253
+ history = [];
254
+ this.state.chatHistory.set(this.selectedAgent, history);
255
+ }
256
+ history.push(msg);
257
+ if (history.length > MAX_CHAT_HISTORY) history.shift();
258
+ this.inputText = "";
259
+ this.scrollPosition = 0;
260
+ this.tui.requestRender();
261
+ } catch {
262
+ // On error, keep input text so user can retry
263
+ }
264
+ }
265
+ }
266
+
267
+ render(_width: number): string[] {
268
+ this.cachedAgents = null; // Clear cache at start of render cycle
269
+ const w = this.width;
270
+ const innerW = w - 2;
271
+ const agents = this.getAgentsSorted();
272
+
273
+ // Handle agent death - don't reset if we're on a meta tab (AGENTS_TAB, CREW_TAB)
274
+ if (this.selectedAgent &&
275
+ this.selectedAgent !== AGENTS_TAB &&
276
+ this.selectedAgent !== CREW_TAB &&
277
+ !agents.find(a => a.name === this.selectedAgent)) {
278
+ this.selectedAgent = agents[0]?.name ?? (this.hasPlan() ? CREW_TAB : AGENTS_TAB);
279
+ this.scrollPosition = 0;
280
+ }
281
+
282
+ const border = (s: string) => this.theme.fg("dim", s);
283
+ const pad = (s: string, len: number) => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
284
+ const row = (content: string) => border("│") + pad(" " + content, innerW) + border("│");
285
+ const emptyRow = () => border("│") + " ".repeat(innerW) + border("│");
286
+
287
+ const lines: string[] = [];
288
+
289
+ // Top border with title
290
+ const titleContent = this.renderTitleContent(agents.length);
291
+ const titleText = ` ${titleContent} `;
292
+ const titleLen = visibleWidth(titleContent) + 2;
293
+ const borderLen = Math.max(0, innerW - titleLen);
294
+ const leftBorder = Math.floor(borderLen / 2);
295
+ const rightBorder = borderLen - leftBorder;
296
+ lines.push(border("╭" + "─".repeat(leftBorder)) + titleText + border("─".repeat(rightBorder) + "╮"));
297
+
298
+ if (agents.length === 0 && !this.hasPlan()) {
299
+ // Simple empty state - no height filling
300
+ lines.push(emptyRow());
301
+ lines.push(emptyRow());
302
+ lines.push(row(this.centerText("No other agents active", innerW - 2)));
303
+ lines.push(emptyRow());
304
+ lines.push(row(this.theme.fg("dim", this.centerText("Start another pi instance to chat", innerW - 2))));
305
+ lines.push(emptyRow());
306
+ lines.push(emptyRow());
307
+ } else {
308
+ lines.push(emptyRow());
309
+ lines.push(row(this.renderTabBar(innerW - 2, agents)));
310
+ lines.push(border("├" + "─".repeat(innerW) + "┤"));
311
+
312
+ const messageAreaHeight = 10; // Fixed height for message area
313
+ const messageLines = this.renderMessages(innerW - 2, messageAreaHeight, agents);
314
+ for (const line of messageLines) {
315
+ lines.push(row(line));
316
+ }
317
+
318
+ lines.push(border("├" + "─".repeat(innerW) + "┤"));
319
+ lines.push(row(this.renderInputBar(innerW - 2)));
320
+ }
321
+
322
+ // Bottom border
323
+ lines.push(border("╰" + "─".repeat(innerW) + "╯"));
324
+
325
+ return lines;
326
+ }
327
+
328
+ private centerText(text: string, width: number): string {
329
+ const padding = Math.max(0, width - visibleWidth(text));
330
+ const left = Math.floor(padding / 2);
331
+ return " ".repeat(left) + text;
332
+ }
333
+
334
+ private renderTitleContent(peerCount: number): string {
335
+ const label = this.theme.fg("accent", "Messenger");
336
+ const name = coloredAgentName(this.state.agentName);
337
+ const peers = this.theme.fg("dim", `${peerCount} peer${peerCount === 1 ? "" : "s"}`);
338
+
339
+ return `${label} ─ ${name} ─ ${peers}`;
340
+ }
341
+
342
+ private renderTabBar(width: number, agents: AgentRegistration[]): string {
343
+ const parts: string[] = [];
344
+ const hasAnySpec = this.hasAnySpec(agents);
345
+ const mode = getDisplayMode(agents);
346
+
347
+ // Agents tab
348
+ const isAgentsSelected = this.selectedAgent === AGENTS_TAB;
349
+ let agentsTab = isAgentsSelected ? "▸ " : "";
350
+ agentsTab += this.theme.fg("accent", "Agents");
351
+ parts.push(agentsTab);
352
+
353
+ // Crew tab (only if plan exists)
354
+ if (this.hasPlan()) {
355
+ const isCrewSelected = this.selectedAgent === CREW_TAB;
356
+ let crewTab = isCrewSelected ? "▸ " : "";
357
+ crewTab += this.theme.fg("accent", "Crew");
358
+
359
+ // Show task progress
360
+ const plan = crewStore.getPlan(this.cwd);
361
+ if (plan && plan.task_count > 0) {
362
+ crewTab += ` (${plan.completed_count}/${plan.task_count})`;
363
+ }
364
+ parts.push(crewTab);
365
+ }
366
+
367
+ for (const agent of agents) {
368
+ const isSelected = this.selectedAgent === agent.name;
369
+ const unread = this.state.unreadCounts.get(agent.name) ?? 0;
370
+
371
+ let tab = isSelected ? "▸ " : "";
372
+ tab += "● ";
373
+ tab += coloredAgentName(agent.name);
374
+
375
+ if (hasAnySpec) {
376
+ if (agent.spec) {
377
+ const specLabel = truncatePathLeft(displaySpecPath(agent.spec, process.cwd()), 14);
378
+ tab += `:${specLabel}`;
379
+ }
380
+ } else if (mode === "same-folder") {
381
+ if (agent.gitBranch) {
382
+ tab += `:${agent.gitBranch}`;
383
+ }
384
+ } else if (mode === "different") {
385
+ tab += `/${extractFolder(agent.cwd)}`;
386
+ }
387
+
388
+ if (unread > 0 && !isSelected) {
389
+ tab += ` (${unread})`;
390
+ }
391
+
392
+ parts.push(tab);
393
+ }
394
+
395
+ const isAllSelected = this.selectedAgent === null;
396
+ let allTab = isAllSelected ? "▸ " : "";
397
+ allTab += this.theme.fg("accent", "+ All");
398
+ parts.push(allTab);
399
+
400
+ const content = parts.join(" │ ");
401
+ return truncateToWidth(content, width);
402
+ }
403
+
404
+ private renderMessages(width: number, height: number, agents: AgentRegistration[]): string[] {
405
+ if (this.selectedAgent === AGENTS_TAB) {
406
+ return this.renderAgentsOverview(width, height, agents);
407
+ }
408
+
409
+ if (this.selectedAgent === CREW_TAB) {
410
+ return renderCrewContent(this.theme, this.cwd, width, height, this.crewViewState);
411
+ }
412
+
413
+ const messages = this.getMessages();
414
+
415
+ if (messages.length === 0) {
416
+ return this.renderNoMessages(width, height, agents);
417
+ }
418
+
419
+ const maxVisibleMessages = Math.max(1, Math.floor(height / 3));
420
+ const endIdx = messages.length - this.scrollPosition;
421
+ const startIdx = Math.max(0, endIdx - maxVisibleMessages);
422
+ const visibleMessages = messages.slice(startIdx, endIdx);
423
+
424
+ const allRenderedLines: string[] = [];
425
+ for (const msg of visibleMessages) {
426
+ const msgLines = this.renderMessageBox(msg, width - 2);
427
+ allRenderedLines.push(...msgLines);
428
+ }
429
+
430
+ if (allRenderedLines.length > height) {
431
+ return allRenderedLines.slice(allRenderedLines.length - height);
432
+ }
433
+
434
+ while (allRenderedLines.length < height) {
435
+ allRenderedLines.unshift("");
436
+ }
437
+ return allRenderedLines;
438
+ }
439
+
440
+ private renderAgentsOverview(width: number, height: number, agents: AgentRegistration[]): string[] {
441
+ const lines: string[] = [];
442
+ const hasAnySpec = this.hasAnySpec(agents);
443
+
444
+ if (hasAnySpec) {
445
+ const claims = store.getClaims(this.dirs);
446
+ const claimByAgent = new Map<string, { taskId: string; reason?: string }>();
447
+ for (const tasks of Object.values(claims)) {
448
+ for (const [taskId, claim] of Object.entries(tasks)) {
449
+ claimByAgent.set(claim.agent, { taskId, reason: claim.reason });
450
+ }
451
+ }
452
+
453
+ const entries: Array<{ name: string; spec?: string; isSelf: boolean }> = agents.map(agent => ({
454
+ name: agent.name,
455
+ spec: agent.spec,
456
+ isSelf: false
457
+ }));
458
+ entries.push({ name: this.state.agentName, spec: this.state.spec, isSelf: true });
459
+
460
+ const groups = new Map<string, Array<{ name: string; isSelf: boolean }>>();
461
+ for (const entry of entries) {
462
+ const key = entry.spec ? displaySpecPath(entry.spec, process.cwd()) : "No spec";
463
+ if (!groups.has(key)) groups.set(key, []);
464
+ groups.get(key)?.push({ name: entry.name, isSelf: entry.isSelf });
465
+ }
466
+
467
+ const mySpec = this.state.spec ? displaySpecPath(this.state.spec, process.cwd()) : undefined;
468
+ const specKeys = Array.from(groups.keys()).filter(key => groups.get(key)?.length);
469
+ const ordered = specKeys
470
+ .filter(key => key !== "No spec" && key !== mySpec)
471
+ .sort((a, b) => a.localeCompare(b));
472
+ if (mySpec && groups.get(mySpec)) ordered.unshift(mySpec);
473
+ if (groups.get("No spec")?.length) ordered.push("No spec");
474
+
475
+ for (const spec of ordered) {
476
+ lines.push(`${spec}:`);
477
+ const group = (groups.get(spec) ?? []).sort((a, b) => {
478
+ if (a.isSelf && !b.isSelf) return -1;
479
+ if (!a.isSelf && b.isSelf) return 1;
480
+ return a.name.localeCompare(b.name);
481
+ });
482
+ for (const entry of group) {
483
+ const claim = claimByAgent.get(entry.name);
484
+ const nameLabel = entry.isSelf ? `${entry.name} (you)` : entry.name;
485
+ const taskLabel = claim ? claim.taskId : "(idle)";
486
+ const reasonLabel = claim?.reason ? truncateToWidth(claim.reason, 24) : "";
487
+ const row = ` ${nameLabel.padEnd(20)} ${taskLabel.padEnd(10)} ${reasonLabel}`;
488
+ lines.push(truncateToWidth(row, width));
489
+ }
490
+ lines.push("");
491
+ }
492
+ } else {
493
+ const mode = getDisplayMode(agents);
494
+ if (mode === "same-folder-branch") {
495
+ const folder = extractFolder(agents[0].cwd);
496
+ const branch = agents.find(a => a.gitBranch)?.gitBranch;
497
+ const header = branch ? `Peers in ${folder} (${branch}):` : `Peers in ${folder}:`;
498
+ lines.push(header, "");
499
+ } else if (mode === "same-folder") {
500
+ const folder = extractFolder(agents[0].cwd);
501
+ lines.push(`Peers in ${folder}:`, "");
502
+ } else {
503
+ lines.push("Peers:", "");
504
+ }
505
+
506
+ for (const agent of agents) {
507
+ const time = formatRelativeTime(agent.startedAt);
508
+ const branch = agent.gitBranch ?? "";
509
+ const folder = extractFolder(agent.cwd);
510
+ if (mode === "same-folder-branch") {
511
+ lines.push(` ${agent.name.padEnd(14)} ${agent.model.padEnd(20)} ${time}`);
512
+ } else if (mode === "same-folder") {
513
+ lines.push(` ${agent.name.padEnd(14)} ${branch.padEnd(12)} ${agent.model.padEnd(20)} ${time}`);
514
+ } else {
515
+ lines.push(` ${agent.name.padEnd(14)} ${folder.padEnd(20)} ${branch.padEnd(12)} ${agent.model.padEnd(20)} ${time}`);
516
+ }
517
+ if (agent.reservations && agent.reservations.length > 0) {
518
+ for (const r of agent.reservations) {
519
+ lines.push(` 🔒 ${truncatePathLeft(r.pattern, 40)}`);
520
+ }
521
+ }
522
+ }
523
+ }
524
+
525
+ if (lines.length > height) {
526
+ return lines.slice(0, height);
527
+ }
528
+ while (lines.length < height) lines.push("");
529
+ return lines;
530
+ }
531
+
532
+ private renderNoMessages(width: number, height: number, agents: AgentRegistration[]): string[] {
533
+ const lines: string[] = [];
534
+
535
+ if (this.selectedAgent === null) {
536
+ const msg = "No broadcasts sent yet";
537
+ const padTop = Math.floor((height - 1) / 2);
538
+ for (let i = 0; i < padTop; i++) lines.push("");
539
+ const pad = " ".repeat(Math.max(0, Math.floor((width - visibleWidth(msg)) / 2)));
540
+ lines.push(pad + this.theme.fg("dim", msg));
541
+ } else {
542
+ const agent = agents.find(a => a.name === this.selectedAgent);
543
+ const msg1 = `No messages with ${this.selectedAgent}`;
544
+
545
+ const details: string[] = [];
546
+ if (agent) {
547
+ const folder = extractFolder(agent.cwd);
548
+ const infoParts = [folder];
549
+ if (agent.gitBranch) infoParts.push(agent.gitBranch);
550
+ infoParts.push(agent.model);
551
+ infoParts.push(formatRelativeTime(agent.startedAt));
552
+ details.push(infoParts.join(" • "));
553
+
554
+ if (agent.reservations && agent.reservations.length > 0) {
555
+ for (const r of agent.reservations) {
556
+ details.push(`🔒 ${truncatePathLeft(r.pattern, 40)}`);
557
+ }
558
+ }
559
+ }
560
+
561
+ const totalLines = 1 + details.length + 1;
562
+ const padTop = Math.floor((height - totalLines) / 2);
563
+ for (let i = 0; i < padTop; i++) lines.push("");
564
+
565
+ const pad1 = " ".repeat(Math.max(0, Math.floor((width - visibleWidth(msg1)) / 2)));
566
+ lines.push(pad1 + msg1);
567
+ lines.push("");
568
+
569
+ for (const detail of details) {
570
+ const pad = " ".repeat(Math.max(0, Math.floor((width - visibleWidth(detail)) / 2)));
571
+ lines.push(pad + this.theme.fg("dim", detail));
572
+ }
573
+ }
574
+
575
+ while (lines.length < height) lines.push("");
576
+ return lines;
577
+ }
578
+
579
+ private renderMessageBox(msg: AgentMailMessage, maxWidth: number): string[] {
580
+ const isOutgoing = msg.from === this.state.agentName;
581
+ const senderLabel = isOutgoing
582
+ ? (msg.to === "broadcast" ? "You → All" : "You")
583
+ : stripAnsiCodes(msg.from);
584
+ const senderColored = isOutgoing
585
+ ? this.theme.fg("accent", senderLabel)
586
+ : coloredAgentName(msg.from);
587
+
588
+ const timeStr = formatRelativeTime(msg.timestamp);
589
+ const time = this.theme.fg("dim", timeStr);
590
+ const safeText = stripAnsiCodes(msg.text);
591
+
592
+ const boxWidth = Math.max(6, Math.min(maxWidth, 60));
593
+ const contentWidth = Math.max(1, boxWidth - 4);
594
+
595
+ const wrappedLines = this.wrapText(safeText, contentWidth);
596
+
597
+ const headerLeft = `┌─ ${senderColored} `;
598
+ const headerRight = ` ${time} ─┐`;
599
+ const headerLeftLen = 4 + visibleWidth(senderLabel);
600
+ const headerRightLen = visibleWidth(timeStr) + 4;
601
+ const dashCount = Math.max(0, boxWidth - headerLeftLen - headerRightLen);
602
+
603
+ const lines: string[] = [];
604
+ lines.push(headerLeft + "─".repeat(dashCount) + headerRight);
605
+
606
+ for (const line of wrappedLines) {
607
+ const padRight = contentWidth - visibleWidth(line);
608
+ lines.push(`│ ${line}${" ".repeat(Math.max(0, padRight))} │`);
609
+ }
610
+
611
+ lines.push(`└${"─".repeat(Math.max(0, boxWidth - 2))}┘`);
612
+ lines.push("");
613
+
614
+ return lines;
615
+ }
616
+
617
+ private wrapText(text: string, maxWidth: number): string[] {
618
+ const result: string[] = [];
619
+ const paragraphs = text.split("\n");
620
+
621
+ for (const para of paragraphs) {
622
+ if (para === "") {
623
+ result.push("");
624
+ continue;
625
+ }
626
+
627
+ const words = para.split(" ");
628
+ let currentLine = "";
629
+
630
+ for (const word of words) {
631
+ const testLine = currentLine ? `${currentLine} ${word}` : word;
632
+ if (visibleWidth(testLine) <= maxWidth) {
633
+ currentLine = testLine;
634
+ } else {
635
+ if (currentLine) result.push(currentLine);
636
+ if (visibleWidth(word) > maxWidth) {
637
+ currentLine = truncateToWidth(word, maxWidth - 1) + "…";
638
+ } else {
639
+ currentLine = word;
640
+ }
641
+ }
642
+ }
643
+
644
+ if (currentLine) result.push(currentLine);
645
+ }
646
+
647
+ return result.length > 0 ? result : [""];
648
+ }
649
+
650
+ private renderInputBar(width: number): string {
651
+ // Crew tab has a status bar instead of input
652
+ if (this.selectedAgent === CREW_TAB) {
653
+ return renderCrewStatusBar(this.theme, this.cwd, width);
654
+ }
655
+
656
+ const prompt = this.theme.fg("accent", "> ");
657
+
658
+ let placeholder: string;
659
+ if (this.selectedAgent === AGENTS_TAB) {
660
+ placeholder = "Agents overview";
661
+ } else if (this.selectedAgent === null) {
662
+ placeholder = "Broadcast to all agents...";
663
+ } else {
664
+ placeholder = `Message ${this.selectedAgent}...`;
665
+ }
666
+
667
+ const hint = this.theme.fg("dim", "[Tab] [Enter]");
668
+ const hintLen = visibleWidth("[Tab] [Enter]");
669
+
670
+ if (this.inputText) {
671
+ const maxInputLen = Math.max(1, width - 2 - hintLen - 2);
672
+ const displayText = truncateToWidth(this.inputText, maxInputLen);
673
+ const padLen = width - 2 - visibleWidth(displayText) - hintLen;
674
+ return prompt + displayText + " ".repeat(Math.max(0, padLen)) + hint;
675
+ } else {
676
+ const displayPlaceholder = truncateToWidth(placeholder, Math.max(1, width - 2 - hintLen - 2));
677
+ const padLen = width - 2 - visibleWidth(displayPlaceholder) - hintLen;
678
+ return prompt + this.theme.fg("dim", displayPlaceholder) + " ".repeat(Math.max(0, padLen)) + hint;
679
+ }
680
+ }
681
+
682
+ invalidate(): void {
683
+ // No cached state to invalidate
684
+ }
685
+
686
+ dispose(): void {}
687
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "pi-messenger",
3
+ "version": "0.7.3",
4
+ "description": "Inter-agent messaging and file reservation system for pi coding agent",
5
+ "type": "module",
6
+ "pi": {
7
+ "extensions": ["./index.ts"]
8
+ },
9
+ "keywords": [
10
+ "pi-package",
11
+ "pi",
12
+ "pi-coding-agent",
13
+ "extension",
14
+ "messaging",
15
+ "multi-agent",
16
+ "coordination"
17
+ ],
18
+ "author": "Nico Bailon",
19
+ "license": "MIT"
20
+ }