pi-messenger 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +20 -2
- package/crew/agents/crew-worker.md +9 -0
- package/crew/agents.ts +140 -11
- package/crew/handlers/plan.ts +4 -5
- package/crew/handlers/review.ts +11 -9
- package/crew/handlers/work.ts +79 -45
- package/crew/index.ts +5 -4
- package/crew/live-progress.ts +57 -0
- package/crew/state.ts +0 -16
- package/crew/types.ts +6 -0
- package/crew/utils/config.ts +26 -11
- package/crew/utils/progress.ts +4 -1
- package/crew-overlay.ts +51 -14
- package/index.ts +23 -3
- package/overlay.ts +47 -2
- package/package.json +9 -3
- package/vitest.config.ts +10 -0
- package/ARCHITECTURE.md +0 -220
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AgentProgress } from "./utils/progress.js";
|
|
2
|
+
|
|
3
|
+
export interface LiveWorkerInfo {
|
|
4
|
+
cwd: string;
|
|
5
|
+
taskId: string;
|
|
6
|
+
agent: string;
|
|
7
|
+
progress: AgentProgress;
|
|
8
|
+
startedAt: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const liveWorkers = new Map<string, LiveWorkerInfo>();
|
|
12
|
+
const listeners = new Set<() => void>();
|
|
13
|
+
|
|
14
|
+
function getWorkerKey(cwd: string, taskId: string): string {
|
|
15
|
+
return `${cwd}::${taskId}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function updateLiveWorker(cwd: string, taskId: string, info: Omit<LiveWorkerInfo, "cwd">): void {
|
|
19
|
+
liveWorkers.set(getWorkerKey(cwd, taskId), {
|
|
20
|
+
...info,
|
|
21
|
+
cwd,
|
|
22
|
+
});
|
|
23
|
+
notifyListeners();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function removeLiveWorker(cwd: string, taskId: string): void {
|
|
27
|
+
liveWorkers.delete(getWorkerKey(cwd, taskId));
|
|
28
|
+
notifyListeners();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getLiveWorkers(cwd?: string): ReadonlyMap<string, LiveWorkerInfo> {
|
|
32
|
+
if (!cwd) return new Map(liveWorkers);
|
|
33
|
+
|
|
34
|
+
const filtered = new Map<string, LiveWorkerInfo>();
|
|
35
|
+
for (const info of liveWorkers.values()) {
|
|
36
|
+
if (info.cwd !== cwd) continue;
|
|
37
|
+
filtered.set(info.taskId, info);
|
|
38
|
+
}
|
|
39
|
+
return filtered;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function hasLiveWorkers(cwd?: string): boolean {
|
|
43
|
+
if (!cwd) return liveWorkers.size > 0;
|
|
44
|
+
for (const info of liveWorkers.values()) {
|
|
45
|
+
if (info.cwd === cwd) return true;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function onLiveWorkersChanged(fn: () => void): () => void {
|
|
51
|
+
listeners.add(fn);
|
|
52
|
+
return () => listeners.delete(fn);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function notifyListeners(): void {
|
|
56
|
+
for (const fn of listeners) fn();
|
|
57
|
+
}
|
package/crew/state.ts
CHANGED
|
@@ -17,7 +17,6 @@ export interface AutonomousState {
|
|
|
17
17
|
active: boolean;
|
|
18
18
|
cwd: string | null;
|
|
19
19
|
waveNumber: number;
|
|
20
|
-
attemptsPerTask: Record<string, number>;
|
|
21
20
|
waveHistory: WaveResult[];
|
|
22
21
|
startedAt: string | null;
|
|
23
22
|
stoppedAt: string | null;
|
|
@@ -33,52 +32,37 @@ export const autonomousState: AutonomousState = {
|
|
|
33
32
|
active: false,
|
|
34
33
|
cwd: null,
|
|
35
34
|
waveNumber: 0,
|
|
36
|
-
attemptsPerTask: {},
|
|
37
35
|
waveHistory: [],
|
|
38
36
|
startedAt: null,
|
|
39
37
|
stoppedAt: null,
|
|
40
38
|
stopReason: null,
|
|
41
39
|
};
|
|
42
40
|
|
|
43
|
-
/**
|
|
44
|
-
* Start autonomous mode.
|
|
45
|
-
*/
|
|
46
41
|
export function startAutonomous(cwd: string): void {
|
|
47
42
|
autonomousState.active = true;
|
|
48
43
|
autonomousState.cwd = cwd;
|
|
49
44
|
autonomousState.waveNumber = 1;
|
|
50
|
-
autonomousState.attemptsPerTask = {};
|
|
51
45
|
autonomousState.waveHistory = [];
|
|
52
46
|
autonomousState.startedAt = new Date().toISOString();
|
|
53
47
|
autonomousState.stoppedAt = null;
|
|
54
48
|
autonomousState.stopReason = null;
|
|
55
49
|
}
|
|
56
50
|
|
|
57
|
-
/**
|
|
58
|
-
* Stop autonomous mode.
|
|
59
|
-
*/
|
|
60
51
|
export function stopAutonomous(reason: "completed" | "blocked" | "manual"): void {
|
|
61
52
|
autonomousState.active = false;
|
|
62
53
|
autonomousState.stoppedAt = new Date().toISOString();
|
|
63
54
|
autonomousState.stopReason = reason;
|
|
64
55
|
}
|
|
65
56
|
|
|
66
|
-
/**
|
|
67
|
-
* Add a wave result to history.
|
|
68
|
-
*/
|
|
69
57
|
export function addWaveResult(result: WaveResult): void {
|
|
70
58
|
autonomousState.waveHistory.push(result);
|
|
71
59
|
autonomousState.waveNumber++;
|
|
72
60
|
}
|
|
73
61
|
|
|
74
|
-
/**
|
|
75
|
-
* Restore autonomous state from session data.
|
|
76
|
-
*/
|
|
77
62
|
export function restoreAutonomousState(data: Partial<AutonomousState>): void {
|
|
78
63
|
if (data.active !== undefined) autonomousState.active = data.active;
|
|
79
64
|
if (data.cwd !== undefined) autonomousState.cwd = data.cwd;
|
|
80
65
|
if (data.waveNumber !== undefined) autonomousState.waveNumber = data.waveNumber;
|
|
81
|
-
if (data.attemptsPerTask !== undefined) autonomousState.attemptsPerTask = data.attemptsPerTask;
|
|
82
66
|
if (data.waveHistory !== undefined) autonomousState.waveHistory = data.waveHistory;
|
|
83
67
|
if (data.startedAt !== undefined) autonomousState.startedAt = data.startedAt;
|
|
84
68
|
if (data.stoppedAt !== undefined) autonomousState.stoppedAt = data.stoppedAt;
|
package/crew/types.ts
CHANGED
|
@@ -36,6 +36,7 @@ export interface Task {
|
|
|
36
36
|
id: string; // task-N format
|
|
37
37
|
title: string;
|
|
38
38
|
status: TaskStatus;
|
|
39
|
+
model?: string;
|
|
39
40
|
depends_on: string[]; // Task IDs this depends on
|
|
40
41
|
created_at: string; // ISO timestamp
|
|
41
42
|
updated_at: string; // ISO timestamp
|
|
@@ -91,6 +92,7 @@ export interface CrewParams {
|
|
|
91
92
|
// Work options
|
|
92
93
|
autonomous?: boolean;
|
|
93
94
|
concurrency?: number;
|
|
95
|
+
model?: string;
|
|
94
96
|
|
|
95
97
|
// Task reset
|
|
96
98
|
cascade?: boolean;
|
|
@@ -131,6 +133,8 @@ export interface ReviewResult {
|
|
|
131
133
|
export interface AgentTask {
|
|
132
134
|
agent: string;
|
|
133
135
|
task: string;
|
|
136
|
+
taskId?: string;
|
|
137
|
+
modelOverride?: string;
|
|
134
138
|
maxOutput?: MaxOutputConfig;
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -141,6 +145,8 @@ export interface AgentResult {
|
|
|
141
145
|
truncated: boolean;
|
|
142
146
|
progress: AgentProgress;
|
|
143
147
|
config?: CrewAgentConfig;
|
|
148
|
+
taskId?: string;
|
|
149
|
+
wasGracefullyShutdown?: boolean;
|
|
144
150
|
error?: string;
|
|
145
151
|
artifactPaths?: {
|
|
146
152
|
input: string;
|
package/crew/utils/config.ts
CHANGED
|
@@ -13,6 +13,12 @@ const USER_CONFIG_PATH = path.join(os.homedir(), ".pi", "agent", "pi-messenger.j
|
|
|
13
13
|
const PROJECT_CONFIG_FILE = "config.json";
|
|
14
14
|
|
|
15
15
|
export interface CrewConfig {
|
|
16
|
+
models?: {
|
|
17
|
+
planner?: string;
|
|
18
|
+
worker?: string;
|
|
19
|
+
reviewer?: string;
|
|
20
|
+
analyst?: string;
|
|
21
|
+
};
|
|
16
22
|
concurrency: {
|
|
17
23
|
workers: number;
|
|
18
24
|
};
|
|
@@ -30,7 +36,13 @@ export interface CrewConfig {
|
|
|
30
36
|
planSync: { enabled: boolean };
|
|
31
37
|
review: { enabled: boolean; maxIterations: number };
|
|
32
38
|
planning: { maxPasses: number };
|
|
33
|
-
work: {
|
|
39
|
+
work: {
|
|
40
|
+
maxAttemptsPerTask: number;
|
|
41
|
+
maxWaves: number;
|
|
42
|
+
stopOnBlock: boolean;
|
|
43
|
+
env?: Record<string, string>;
|
|
44
|
+
shutdownGracePeriodMs?: number;
|
|
45
|
+
};
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
const DEFAULT_CONFIG: CrewConfig = {
|
|
@@ -48,7 +60,7 @@ const DEFAULT_CONFIG: CrewConfig = {
|
|
|
48
60
|
planSync: { enabled: false },
|
|
49
61
|
review: { enabled: true, maxIterations: 3 },
|
|
50
62
|
planning: { maxPasses: 3 },
|
|
51
|
-
work: { maxAttemptsPerTask: 5, maxWaves: 50, stopOnBlock: false },
|
|
63
|
+
work: { maxAttemptsPerTask: 5, maxWaves: 50, stopOnBlock: false, shutdownGracePeriodMs: 30000 },
|
|
52
64
|
};
|
|
53
65
|
|
|
54
66
|
function loadJson(filePath: string): Record<string, unknown> {
|
|
@@ -60,19 +72,25 @@ function loadJson(filePath: string): Record<string, unknown> {
|
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
function deepMerge<T extends object>(target: T, ...sources: Partial<T>[]): T {
|
|
63
|
-
const result =
|
|
75
|
+
const result: Record<string, unknown> = target && typeof target === "object"
|
|
76
|
+
? { ...(target as Record<string, unknown>) }
|
|
77
|
+
: {};
|
|
64
78
|
for (const source of sources) {
|
|
65
|
-
|
|
79
|
+
const src = source as Record<string, unknown>;
|
|
80
|
+
for (const key of Object.keys(src)) {
|
|
66
81
|
const targetVal = result[key];
|
|
67
|
-
const sourceVal =
|
|
82
|
+
const sourceVal = src[key];
|
|
68
83
|
if (sourceVal && typeof sourceVal === "object" && !Array.isArray(sourceVal)) {
|
|
69
|
-
|
|
84
|
+
const base = targetVal && typeof targetVal === "object" && !Array.isArray(targetVal)
|
|
85
|
+
? targetVal as object
|
|
86
|
+
: {};
|
|
87
|
+
result[key] = deepMerge(base, sourceVal as object);
|
|
70
88
|
} else if (sourceVal !== undefined) {
|
|
71
|
-
result[key] = sourceVal
|
|
89
|
+
result[key] = sourceVal;
|
|
72
90
|
}
|
|
73
91
|
}
|
|
74
92
|
}
|
|
75
|
-
return result;
|
|
93
|
+
return result as T;
|
|
76
94
|
}
|
|
77
95
|
|
|
78
96
|
/**
|
|
@@ -90,9 +108,6 @@ export function loadCrewConfig(crewDir: string): CrewConfig {
|
|
|
90
108
|
return deepMerge(DEFAULT_CONFIG, userCrewConfig, projectConfig);
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
/**
|
|
94
|
-
* Get truncation config for a specific role.
|
|
95
|
-
*/
|
|
96
111
|
export function getTruncationForRole(config: CrewConfig, role: string): MaxOutputConfig {
|
|
97
112
|
switch (role) {
|
|
98
113
|
case "planner": return config.truncation.planners;
|
package/crew/utils/progress.ts
CHANGED
|
@@ -10,13 +10,14 @@ export interface AgentProgress {
|
|
|
10
10
|
currentTool?: string;
|
|
11
11
|
currentToolArgs?: string;
|
|
12
12
|
recentTools: Array<{ tool: string; args: string; endMs: number }>;
|
|
13
|
+
toolCallCount: number;
|
|
13
14
|
tokens: number;
|
|
14
15
|
durationMs: number;
|
|
15
16
|
error?: string;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
// Event types from pi's --mode json output
|
|
19
|
-
interface PiEvent {
|
|
20
|
+
export interface PiEvent {
|
|
20
21
|
type: string;
|
|
21
22
|
toolName?: string;
|
|
22
23
|
args?: Record<string, unknown>;
|
|
@@ -34,6 +35,7 @@ export function createProgress(agent: string): AgentProgress {
|
|
|
34
35
|
agent,
|
|
35
36
|
status: "pending",
|
|
36
37
|
recentTools: [],
|
|
38
|
+
toolCallCount: 0,
|
|
37
39
|
tokens: 0,
|
|
38
40
|
durationMs: 0,
|
|
39
41
|
};
|
|
@@ -59,6 +61,7 @@ export function updateProgress(progress: AgentProgress, event: PiEvent, startTim
|
|
|
59
61
|
break;
|
|
60
62
|
|
|
61
63
|
case "tool_execution_end":
|
|
64
|
+
progress.toolCallCount++;
|
|
62
65
|
if (progress.currentTool) {
|
|
63
66
|
progress.recentTools.unshift({
|
|
64
67
|
tool: progress.currentTool,
|
package/crew-overlay.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
|
10
10
|
import * as crewStore from "./crew/store.js";
|
|
11
11
|
import { autonomousState } from "./crew/state.js";
|
|
12
12
|
import type { Task } from "./crew/types.js";
|
|
13
|
+
import { getLiveWorkers, type LiveWorkerInfo } from "./crew/live-progress.js";
|
|
13
14
|
|
|
14
15
|
// Status icons
|
|
15
16
|
const STATUS_ICONS: Record<string, string> = {
|
|
@@ -49,9 +50,9 @@ export function renderCrewContent(
|
|
|
49
50
|
}
|
|
50
51
|
|
|
51
52
|
const tasks = crewStore.getTasks(cwd);
|
|
53
|
+
const workers = getLiveWorkers(cwd);
|
|
52
54
|
|
|
53
55
|
// Header: PRD with progress
|
|
54
|
-
const pct = plan.task_count > 0 ? Math.round((plan.completed_count / plan.task_count) * 100) : 0;
|
|
55
56
|
const progressText = `[${plan.completed_count}/${plan.task_count}]`;
|
|
56
57
|
const prdLine = `📋 ${plan.prd}`;
|
|
57
58
|
const prdWidth = visibleWidth(prdLine);
|
|
@@ -61,13 +62,42 @@ export function renderCrewContent(
|
|
|
61
62
|
lines.push(prdLine + " ".repeat(padding) + theme.fg("accent", progressText));
|
|
62
63
|
lines.push("");
|
|
63
64
|
|
|
65
|
+
if (workers.size > 0) {
|
|
66
|
+
lines.push(theme.fg("dim", "─".repeat(Math.min(width, 40)) + " Active Workers"));
|
|
67
|
+
|
|
68
|
+
for (const [taskId, info] of workers) {
|
|
69
|
+
const activity = info.progress.currentTool
|
|
70
|
+
? `${info.progress.currentTool}${info.progress.currentToolArgs ? ` ${info.progress.currentToolArgs}` : ""}`
|
|
71
|
+
: "thinking...";
|
|
72
|
+
const calls = `${info.progress.toolCallCount} calls`;
|
|
73
|
+
const tokens = info.progress.tokens > 1000
|
|
74
|
+
? `${(info.progress.tokens / 1000).toFixed(0)}k tokens`
|
|
75
|
+
: `${info.progress.tokens} tokens`;
|
|
76
|
+
const elapsed = `${Math.floor((Date.now() - info.startedAt) / 1000)}s`;
|
|
77
|
+
|
|
78
|
+
const line = ` ⚡ ${taskId}: ${activity}`;
|
|
79
|
+
const stats = `${calls} ${tokens} ${elapsed}`;
|
|
80
|
+
lines.push(truncateToWidth(line + " " + theme.fg("dim", stats), width));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
lines.push(theme.fg("dim", "─".repeat(Math.min(width, 40))));
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
// Task list
|
|
88
|
+
const taskListStartLine = lines.length;
|
|
65
89
|
if (tasks.length === 0) {
|
|
66
90
|
lines.push(theme.fg("dim", " (no tasks yet)"));
|
|
67
91
|
} else {
|
|
68
92
|
for (let i = 0; i < tasks.length; i++) {
|
|
69
93
|
const task = tasks[i];
|
|
70
|
-
const taskLine = renderTaskLine(
|
|
94
|
+
const taskLine = renderTaskLine(
|
|
95
|
+
theme,
|
|
96
|
+
task,
|
|
97
|
+
i === viewState.selectedTaskIndex,
|
|
98
|
+
width,
|
|
99
|
+
workers.get(task.id),
|
|
100
|
+
);
|
|
71
101
|
lines.push(taskLine);
|
|
72
102
|
}
|
|
73
103
|
}
|
|
@@ -83,16 +113,22 @@ export function renderCrewContent(
|
|
|
83
113
|
|
|
84
114
|
// Handle scrolling if content exceeds height
|
|
85
115
|
if (lines.length > height) {
|
|
86
|
-
|
|
87
|
-
|
|
116
|
+
// Auto-scroll to keep the selected task visible
|
|
117
|
+
if (tasks.length > 0) {
|
|
118
|
+
const selectedLine = taskListStartLine + Math.min(viewState.selectedTaskIndex, tasks.length - 1);
|
|
119
|
+
if (selectedLine < viewState.scrollOffset) {
|
|
120
|
+
viewState.scrollOffset = selectedLine;
|
|
121
|
+
} else if (selectedLine >= viewState.scrollOffset + height) {
|
|
122
|
+
viewState.scrollOffset = selectedLine - height + 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
viewState.scrollOffset = Math.max(0, Math.min(viewState.scrollOffset, lines.length - height));
|
|
126
|
+
return lines.slice(viewState.scrollOffset, viewState.scrollOffset + height);
|
|
88
127
|
}
|
|
89
128
|
|
|
90
129
|
return lines.slice(0, height);
|
|
91
130
|
}
|
|
92
131
|
|
|
93
|
-
/**
|
|
94
|
-
* Render the status bar for autonomous mode.
|
|
95
|
-
*/
|
|
96
132
|
export function renderCrewStatusBar(theme: Theme, cwd: string, width: number): string {
|
|
97
133
|
const plan = crewStore.getPlan(cwd);
|
|
98
134
|
|
|
@@ -168,7 +204,8 @@ function renderTaskLine(
|
|
|
168
204
|
theme: Theme,
|
|
169
205
|
task: Task,
|
|
170
206
|
isSelected: boolean,
|
|
171
|
-
width: number
|
|
207
|
+
width: number,
|
|
208
|
+
liveWorker?: LiveWorkerInfo
|
|
172
209
|
): string {
|
|
173
210
|
const icon = STATUS_ICONS[task.status] ?? "?";
|
|
174
211
|
const selectIndicator = isSelected ? theme.fg("accent", "▸ ") : " ";
|
|
@@ -191,7 +228,12 @@ function renderTaskLine(
|
|
|
191
228
|
|
|
192
229
|
// Build task suffix (assigned agent or dependencies)
|
|
193
230
|
let suffix = "";
|
|
194
|
-
if (task.status === "in_progress" &&
|
|
231
|
+
if (task.status === "in_progress" && liveWorker) {
|
|
232
|
+
const activity = liveWorker.progress.currentTool
|
|
233
|
+
? `${liveWorker.progress.currentTool}${liveWorker.progress.currentToolArgs ? ` ${liveWorker.progress.currentToolArgs}` : ""}`
|
|
234
|
+
: "thinking...";
|
|
235
|
+
suffix = ` (${activity})`;
|
|
236
|
+
} else if (task.status === "in_progress" && task.assigned_to) {
|
|
195
237
|
suffix = ` (${task.assigned_to})`;
|
|
196
238
|
} else if (task.status === "todo" && task.depends_on.length > 0) {
|
|
197
239
|
suffix = ` → deps: ${task.depends_on.join(", ")}`;
|
|
@@ -219,9 +261,6 @@ function renderLegend(theme: Theme, width: number): string {
|
|
|
219
261
|
return truncateToWidth(theme.fg("dim", legend), width);
|
|
220
262
|
}
|
|
221
263
|
|
|
222
|
-
/**
|
|
223
|
-
* Navigate to next/prev task.
|
|
224
|
-
*/
|
|
225
264
|
export function navigateTask(viewState: CrewViewState, direction: 1 | -1, taskCount: number): void {
|
|
226
265
|
if (taskCount === 0) return;
|
|
227
266
|
viewState.selectedTaskIndex = Math.max(
|
|
@@ -229,5 +268,3 @@ export function navigateTask(viewState: CrewViewState, direction: 1 | -1, taskCo
|
|
|
229
268
|
Math.min(taskCount - 1, viewState.selectedTaskIndex + direction)
|
|
230
269
|
);
|
|
231
270
|
}
|
|
232
|
-
|
|
233
|
-
|
package/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import type { CrewParams } from "./crew/types.js";
|
|
|
36
36
|
import { autonomousState, restoreAutonomousState, stopAutonomous } from "./crew/state.js";
|
|
37
37
|
import { loadCrewConfig } from "./crew/utils/config.js";
|
|
38
38
|
import * as crewStore from "./crew/store.js";
|
|
39
|
+
import { getLiveWorkers, onLiveWorkersChanged } from "./crew/live-progress.js";
|
|
39
40
|
|
|
40
41
|
let overlayTui: TUI | null = null;
|
|
41
42
|
|
|
@@ -225,13 +226,22 @@ export default function piMessengerExtension(pi: ExtensionAPI) {
|
|
|
225
226
|
const cwd = ctx.cwd ?? process.cwd();
|
|
226
227
|
const plan = crewStore.getPlan(cwd);
|
|
227
228
|
if (plan) {
|
|
229
|
+
const workerCount = getLiveWorkers(cwd).size;
|
|
228
230
|
crewStr = theme.fg("accent", ` ⚡${plan.completed_count}/${plan.task_count}`);
|
|
231
|
+
if (workerCount > 0) {
|
|
232
|
+
crewStr += theme.fg("dim", ` 🔨${workerCount}`);
|
|
233
|
+
}
|
|
229
234
|
}
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
ctx.ui.setStatus("messenger", `msg: ${nameStr}${countStr}${unreadStr}${crewStr}`);
|
|
233
238
|
}
|
|
234
239
|
|
|
240
|
+
let latestCtx: ExtensionContext | null = null;
|
|
241
|
+
onLiveWorkersChanged(() => {
|
|
242
|
+
if (latestCtx) updateStatus(latestCtx);
|
|
243
|
+
});
|
|
244
|
+
|
|
235
245
|
// ===========================================================================
|
|
236
246
|
// Tool Registration
|
|
237
247
|
// ===========================================================================
|
|
@@ -303,6 +313,7 @@ Mode: action (if provided) > legacy key-based routing`,
|
|
|
303
313
|
type: Type.Optional(StringEnum(["plan", "impl"], { description: "Review type (inferred from target if omitted)" })),
|
|
304
314
|
autonomous: Type.Optional(Type.Boolean({ description: "Run work continuously until done/blocked" })),
|
|
305
315
|
concurrency: Type.Optional(Type.Number({ description: "Override worker concurrency" })),
|
|
316
|
+
model: Type.Optional(Type.String({ description: "Override worker model for this work wave" })),
|
|
306
317
|
cascade: Type.Optional(Type.Boolean({ description: "For task.reset - also reset dependent tasks" })),
|
|
307
318
|
limit: Type.Optional(Type.Number({ description: "Number of events to return (for feed action, default 20)" })),
|
|
308
319
|
paths: Type.Optional(Type.Array(Type.String(), { description: "Paths for reserve/release actions" })),
|
|
@@ -349,7 +360,8 @@ Mode: action (if provided) > legacy key-based routing`,
|
|
|
349
360
|
autoRegisterPath?: "add" | "remove" | "list";
|
|
350
361
|
list?: boolean;
|
|
351
362
|
limit?: number;
|
|
352
|
-
},
|
|
363
|
+
}, signal, _onUpdate, ctx) {
|
|
364
|
+
latestCtx = ctx;
|
|
353
365
|
const {
|
|
354
366
|
action,
|
|
355
367
|
join,
|
|
@@ -384,7 +396,8 @@ Mode: action (if provided) > legacy key-based routing`,
|
|
|
384
396
|
deliverMessage,
|
|
385
397
|
updateStatus,
|
|
386
398
|
(type, data) => pi.appendEntry(type, data),
|
|
387
|
-
{ stuckThreshold: config.stuckThreshold, crewEventsInFeed: config.crewEventsInFeed, nameTheme, feedRetention: config.feedRetention }
|
|
399
|
+
{ stuckThreshold: config.stuckThreshold, crewEventsInFeed: config.crewEventsInFeed, nameTheme, feedRetention: config.feedRetention },
|
|
400
|
+
signal
|
|
388
401
|
);
|
|
389
402
|
}
|
|
390
403
|
|
|
@@ -699,6 +712,7 @@ Mode: action (if provided) > legacy key-based routing`,
|
|
|
699
712
|
// ===========================================================================
|
|
700
713
|
|
|
701
714
|
pi.on("session_start", async (_event, ctx) => {
|
|
715
|
+
latestCtx = ctx;
|
|
702
716
|
for (const entry of ctx.sessionManager.getEntries()) {
|
|
703
717
|
if (entry.type === "custom" && entry.customType === "crew-state") {
|
|
704
718
|
restoreAutonomousState(entry.data as Parameters<typeof restoreAutonomousState>[0]);
|
|
@@ -741,16 +755,22 @@ Mode: action (if provided) > legacy key-based routing`,
|
|
|
741
755
|
}
|
|
742
756
|
|
|
743
757
|
pi.on("session_switch", async (_event, ctx) => {
|
|
758
|
+
latestCtx = ctx;
|
|
744
759
|
recoverWatcherIfNeeded();
|
|
745
760
|
updateStatus(ctx);
|
|
746
761
|
});
|
|
747
762
|
pi.on("session_fork", async (_event, ctx) => {
|
|
763
|
+
latestCtx = ctx;
|
|
748
764
|
recoverWatcherIfNeeded();
|
|
749
765
|
updateStatus(ctx);
|
|
750
766
|
});
|
|
751
|
-
pi.on("session_tree", async (_event, ctx) =>
|
|
767
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
768
|
+
latestCtx = ctx;
|
|
769
|
+
updateStatus(ctx);
|
|
770
|
+
});
|
|
752
771
|
|
|
753
772
|
pi.on("turn_end", async (event, ctx) => {
|
|
773
|
+
latestCtx = ctx;
|
|
754
774
|
store.processAllPendingMessages(state, dirs, deliverMessage);
|
|
755
775
|
recoverWatcherIfNeeded();
|
|
756
776
|
updateStatus(ctx);
|
package/overlay.ts
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
navigateTask,
|
|
35
35
|
type CrewViewState,
|
|
36
36
|
} from "./crew-overlay.js";
|
|
37
|
+
import { hasLiveWorkers, onLiveWorkersChanged } from "./crew/live-progress.js";
|
|
37
38
|
import { loadConfig } from "./config.js";
|
|
38
39
|
|
|
39
40
|
const AGENTS_TAB = "[agents]";
|
|
@@ -50,6 +51,8 @@ export class MessengerOverlay implements Component, Focusable {
|
|
|
50
51
|
private crewViewState: CrewViewState = createCrewViewState();
|
|
51
52
|
private cwd: string;
|
|
52
53
|
private stuckThresholdMs: number;
|
|
54
|
+
private progressTimer: ReturnType<typeof setInterval> | null = null;
|
|
55
|
+
private progressUnsubscribe: (() => void) | null = null;
|
|
53
56
|
|
|
54
57
|
constructor(
|
|
55
58
|
private tui: TUI,
|
|
@@ -68,6 +71,18 @@ export class MessengerOverlay implements Component, Focusable {
|
|
|
68
71
|
if (this.selectedAgent && this.selectedAgent !== AGENTS_TAB && this.selectedAgent !== CREW_TAB) {
|
|
69
72
|
state.unreadCounts.set(this.selectedAgent, 0);
|
|
70
73
|
}
|
|
74
|
+
|
|
75
|
+
this.progressUnsubscribe = onLiveWorkersChanged(() => {
|
|
76
|
+
if (this.selectedAgent === CREW_TAB) {
|
|
77
|
+
if (hasLiveWorkers(this.cwd)) this.startProgressRefresh();
|
|
78
|
+
else this.stopProgressRefresh();
|
|
79
|
+
}
|
|
80
|
+
this.tui.requestRender();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (this.selectedAgent === CREW_TAB && hasLiveWorkers(this.cwd)) {
|
|
84
|
+
this.startProgressRefresh();
|
|
85
|
+
}
|
|
71
86
|
}
|
|
72
87
|
|
|
73
88
|
private getAgentsSorted(): AgentRegistration[] {
|
|
@@ -101,6 +116,30 @@ export class MessengerOverlay implements Component, Focusable {
|
|
|
101
116
|
this.state.unreadCounts.set(agentName, 0);
|
|
102
117
|
}
|
|
103
118
|
this.scrollPosition = 0;
|
|
119
|
+
|
|
120
|
+
if (agentName === CREW_TAB && hasLiveWorkers(this.cwd)) {
|
|
121
|
+
this.startProgressRefresh();
|
|
122
|
+
} else {
|
|
123
|
+
this.stopProgressRefresh();
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private startProgressRefresh(): void {
|
|
128
|
+
if (this.progressTimer) return;
|
|
129
|
+
this.progressTimer = setInterval(() => {
|
|
130
|
+
if (hasLiveWorkers(this.cwd)) {
|
|
131
|
+
this.tui.requestRender();
|
|
132
|
+
} else {
|
|
133
|
+
this.stopProgressRefresh();
|
|
134
|
+
}
|
|
135
|
+
}, 1000);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private stopProgressRefresh(): void {
|
|
139
|
+
if (this.progressTimer) {
|
|
140
|
+
clearInterval(this.progressTimer);
|
|
141
|
+
this.progressTimer = null;
|
|
142
|
+
}
|
|
104
143
|
}
|
|
105
144
|
|
|
106
145
|
private scroll(delta: number): void {
|
|
@@ -341,7 +380,9 @@ export class MessengerOverlay implements Component, Focusable {
|
|
|
341
380
|
lines.push(row(this.renderTabBar(innerW - 2, agents)));
|
|
342
381
|
lines.push(border("├" + "─".repeat(innerW) + "┤"));
|
|
343
382
|
|
|
344
|
-
const
|
|
383
|
+
const chromeLines = 7; // top border, empty row, tab bar, separator, separator, input bar, bottom border
|
|
384
|
+
const termRows = process.stdout.rows ?? 24;
|
|
385
|
+
const messageAreaHeight = Math.min(25, Math.max(8, termRows - chromeLines - 2));
|
|
345
386
|
const messageLines = this.renderMessages(innerW - 2, messageAreaHeight, agents);
|
|
346
387
|
for (const line of messageLines) {
|
|
347
388
|
lines.push(row(line));
|
|
@@ -709,5 +750,9 @@ export class MessengerOverlay implements Component, Focusable {
|
|
|
709
750
|
// No cached state to invalidate
|
|
710
751
|
}
|
|
711
752
|
|
|
712
|
-
dispose(): void {
|
|
753
|
+
dispose(): void {
|
|
754
|
+
this.stopProgressRefresh();
|
|
755
|
+
this.progressUnsubscribe?.();
|
|
756
|
+
this.progressUnsubscribe = null;
|
|
757
|
+
}
|
|
713
758
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-messenger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Inter-agent messaging and file reservation system for pi coding agent",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "Nico Bailon",
|
|
@@ -34,9 +34,15 @@
|
|
|
34
34
|
"crew/**",
|
|
35
35
|
"skills/**",
|
|
36
36
|
"README.md",
|
|
37
|
-
"CHANGELOG.md"
|
|
38
|
-
"ARCHITECTURE.md"
|
|
37
|
+
"CHANGELOG.md"
|
|
39
38
|
],
|
|
39
|
+
"scripts": {
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"test:watch": "vitest"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"vitest": "^2.1.8"
|
|
45
|
+
},
|
|
40
46
|
"pi": {
|
|
41
47
|
"extensions": ["./index.ts"]
|
|
42
48
|
}
|