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.
- package/ARCHITECTURE.md +244 -0
- package/CHANGELOG.md +418 -0
- package/README.md +394 -0
- package/banner.png +0 -0
- package/config-overlay.ts +172 -0
- package/config.ts +178 -0
- package/crew/agents/crew-docs-scout.md +55 -0
- package/crew/agents/crew-gap-analyst.md +105 -0
- package/crew/agents/crew-github-scout.md +111 -0
- package/crew/agents/crew-interview-generator.md +79 -0
- package/crew/agents/crew-plan-sync.md +64 -0
- package/crew/agents/crew-practice-scout.md +62 -0
- package/crew/agents/crew-repo-scout.md +65 -0
- package/crew/agents/crew-reviewer.md +58 -0
- package/crew/agents/crew-web-scout.md +85 -0
- package/crew/agents/crew-worker.md +95 -0
- package/crew/agents.ts +200 -0
- package/crew/handlers/interview.ts +211 -0
- package/crew/handlers/plan.ts +358 -0
- package/crew/handlers/review.ts +341 -0
- package/crew/handlers/status.ts +257 -0
- package/crew/handlers/sync.ts +232 -0
- package/crew/handlers/task.ts +511 -0
- package/crew/handlers/work.ts +289 -0
- package/crew/id-allocator.ts +44 -0
- package/crew/index.ts +229 -0
- package/crew/state.ts +116 -0
- package/crew/store.ts +480 -0
- package/crew/types.ts +164 -0
- package/crew/utils/artifacts.ts +65 -0
- package/crew/utils/config.ts +104 -0
- package/crew/utils/discover.ts +170 -0
- package/crew/utils/install.ts +373 -0
- package/crew/utils/progress.ts +107 -0
- package/crew/utils/result.ts +16 -0
- package/crew/utils/truncate.ts +79 -0
- package/crew-overlay.ts +259 -0
- package/handlers.ts +799 -0
- package/index.ts +591 -0
- package/lib.ts +232 -0
- package/overlay.ts +687 -0
- package/package.json +20 -0
- package/skills/pi-messenger-crew/SKILL.md +140 -0
- package/store.ts +1068 -0
- 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
|
+
}
|