pi-cockpit 0.2.0 → 0.4.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/package.json +2 -2
- package/src/agents-store.ts +72 -7
- package/src/render.ts +1 -1
- package/src/stack-widget.ts +133 -111
- package/src/types.ts +116 -114
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-cockpit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent cockpit for Pi: a list-mode status stack (live teammates + todo plan) pinned above the editor, plus a Starship-style footer. Renders only via public extension APIs (setWidget/setFooter).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"@earendil-works/pi-coding-agent": "0.82.1",
|
|
60
60
|
"@earendil-works/pi-tui": "0.82.1",
|
|
61
61
|
"@types/node": "^22.0.0",
|
|
62
|
-
"pi-maestro-teammate": "1.
|
|
62
|
+
"pi-maestro-teammate": "1.2.0",
|
|
63
63
|
"typescript": "^5.5.0"
|
|
64
64
|
},
|
|
65
65
|
"main": "./src/index.ts",
|
package/src/agents-store.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface ProgressPayload {
|
|
|
37
37
|
dependencies?: number[];
|
|
38
38
|
status?: string;
|
|
39
39
|
startedAt?: number | string;
|
|
40
|
+
completedAt?: number | string;
|
|
41
|
+
durationMs?: number;
|
|
40
42
|
lastActivityAt?: number | string;
|
|
41
43
|
recentTools?: Array<string | { name?: string; status?: string }>;
|
|
42
44
|
toolCount?: number;
|
|
@@ -48,6 +50,10 @@ export interface ProgressPayload {
|
|
|
48
50
|
export interface MessagePayload {
|
|
49
51
|
correlationId: string;
|
|
50
52
|
taskCorrelationId?: string;
|
|
53
|
+
// Present on progress/interaction deltas (teammate/.../index.ts publishProgress);
|
|
54
|
+
// absent on send deltas. Used to self-heal a row when `started` was missed.
|
|
55
|
+
agent?: string;
|
|
56
|
+
name?: string;
|
|
51
57
|
taskIndex?: number;
|
|
52
58
|
dependencies?: number[];
|
|
53
59
|
message?: string;
|
|
@@ -61,7 +67,8 @@ export interface MessagePayload {
|
|
|
61
67
|
lastActivityAt?: number | string;
|
|
62
68
|
progress?: ProgressPayload[];
|
|
63
69
|
}
|
|
64
|
-
export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exitCode"
|
|
70
|
+
export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exitCode">
|
|
71
|
+
& Partial<Pick<TeammateCompleteEvent, "durationMs">>;
|
|
65
72
|
|
|
66
73
|
/**
|
|
67
74
|
* How long a failed agent stays on screen after it completes.
|
|
@@ -105,6 +112,19 @@ function normalizeStartedAt(value: number | string | undefined, fallback: number
|
|
|
105
112
|
return fallback;
|
|
106
113
|
}
|
|
107
114
|
|
|
115
|
+
function terminalTime(
|
|
116
|
+
row: AgentRow,
|
|
117
|
+
payload: { completedAt?: number | string; durationMs?: number },
|
|
118
|
+
now: number,
|
|
119
|
+
): number {
|
|
120
|
+
const completedAt = normalizeStartedAt(payload.completedAt, Number.NaN);
|
|
121
|
+
if (Number.isFinite(completedAt)) return Math.max(row.startedAt, completedAt);
|
|
122
|
+
if (typeof payload.durationMs === "number" && Number.isFinite(payload.durationMs)) {
|
|
123
|
+
return row.startedAt + Math.max(0, payload.durationMs);
|
|
124
|
+
}
|
|
125
|
+
return row.finishedAt ?? now;
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
function latestTool(tools: MessagePayload["recentTools"]): string | undefined {
|
|
109
129
|
if (!tools?.length) return undefined;
|
|
110
130
|
const tool = tools.find((candidate) => typeof candidate === "object" && candidate?.status === "running")
|
|
@@ -120,13 +140,16 @@ function latestTool(tools: MessagePayload["recentTools"]): string | undefined {
|
|
|
120
140
|
// Self-accumulating roster. The teammate extension only broadcasts deltas
|
|
121
141
|
// (started/message/complete), never a full snapshot, so we rebuild the list here.
|
|
122
142
|
// Cold start is empty by design — we only reflect activity observed after load.
|
|
143
|
+
// The roster is self-healing: message/progress deltas for an agent whose
|
|
144
|
+
// `started` delta was missed still materialize a row, so a running agent is
|
|
145
|
+
// always represented as long as it keeps emitting activity.
|
|
123
146
|
export class AgentsStore {
|
|
124
147
|
private readonly roster = new Map<string, AgentRow>();
|
|
125
148
|
|
|
126
149
|
applyStarted(p: StartedPayload, now: number = Date.now()): void {
|
|
127
150
|
const id = p.correlationId;
|
|
128
151
|
const prev = this.roster.get(id);
|
|
129
|
-
|
|
152
|
+
const row: AgentRow = {
|
|
130
153
|
...prev,
|
|
131
154
|
correlationId: id,
|
|
132
155
|
agent: clean(p.agent) || prev?.agent || "",
|
|
@@ -142,7 +165,9 @@ export class AgentsStore {
|
|
|
142
165
|
: prev?.parentCorrelationId
|
|
143
166
|
? { parentCorrelationId: prev.parentCorrelationId }
|
|
144
167
|
: {}),
|
|
145
|
-
}
|
|
168
|
+
};
|
|
169
|
+
if (row.status !== "done" && row.status !== "failed") delete row.finishedAt;
|
|
170
|
+
this.roster.set(id, row);
|
|
146
171
|
}
|
|
147
172
|
|
|
148
173
|
applyMessage(p: MessagePayload, now = Date.now()): void {
|
|
@@ -150,6 +175,22 @@ export class AgentsStore {
|
|
|
150
175
|
for (const progress of p.progress) this.applyProgress(p.correlationId, progress, now);
|
|
151
176
|
}
|
|
152
177
|
const targetId = p.taskCorrelationId ?? p.correlationId;
|
|
178
|
+
if (!this.roster.has(targetId)) {
|
|
179
|
+
// Self-heal the roster: a running agent must stay visible even when its
|
|
180
|
+
// `started` delta was missed (cold start, event reorder, or a foreground
|
|
181
|
+
// dispatch that detached to background). Materialize the row from this
|
|
182
|
+
// event instead of dropping it; later deltas refine the placeholder.
|
|
183
|
+
this.applyStarted({
|
|
184
|
+
correlationId: targetId,
|
|
185
|
+
agent: p.agent ?? "",
|
|
186
|
+
name: p.name,
|
|
187
|
+
status: p.status,
|
|
188
|
+
lastActivityAt: p.lastActivityAt,
|
|
189
|
+
...(p.taskCorrelationId && p.taskCorrelationId !== p.correlationId
|
|
190
|
+
? { spawnedBy: p.correlationId }
|
|
191
|
+
: {}),
|
|
192
|
+
}, now);
|
|
193
|
+
}
|
|
153
194
|
const row = this.roster.get(targetId);
|
|
154
195
|
if (!row) return;
|
|
155
196
|
const progressActivity = p.progress?.find((progress) => progress.correlationId === targetId)?.lastActivityAt;
|
|
@@ -168,6 +209,8 @@ export class AgentsStore {
|
|
|
168
209
|
if (typeof p.status === "string") {
|
|
169
210
|
row.taskStatus = p.status;
|
|
170
211
|
row.status = mapAgentStatus(p.status);
|
|
212
|
+
if (row.status === "done" || row.status === "failed") row.finishedAt ??= now;
|
|
213
|
+
else delete row.finishedAt;
|
|
171
214
|
}
|
|
172
215
|
if (typeof p.taskIndex === "number") row.taskIndex = p.taskIndex;
|
|
173
216
|
if (Array.isArray(p.dependencies)) row.dependencies = [...p.dependencies];
|
|
@@ -193,6 +236,7 @@ export class AgentsStore {
|
|
|
193
236
|
if (row && (row.status === "failed" || failedByExitCode)) {
|
|
194
237
|
row.status = "failed";
|
|
195
238
|
row.taskStatus = "failed";
|
|
239
|
+
row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
|
|
196
240
|
row.failedAt = now;
|
|
197
241
|
row.lastActivityAt = now;
|
|
198
242
|
delete row.activeTool;
|
|
@@ -223,22 +267,43 @@ export class AgentsStore {
|
|
|
223
267
|
}
|
|
224
268
|
|
|
225
269
|
private applyProgress(parentCorrelationId: string, p: ProgressPayload, now: number): void {
|
|
270
|
+
if (!this.roster.has(p.correlationId)) {
|
|
271
|
+
// Self-heal: see applyMessage. A graph child whose `started` delta was
|
|
272
|
+
// missed still materializes from its first progress event.
|
|
273
|
+
this.applyStarted({
|
|
274
|
+
correlationId: p.correlationId,
|
|
275
|
+
agent: p.agent,
|
|
276
|
+
name: p.name,
|
|
277
|
+
status: p.status,
|
|
278
|
+
startedAt: p.startedAt,
|
|
279
|
+
lastActivityAt: p.lastActivityAt,
|
|
280
|
+
...(parentCorrelationId !== p.correlationId
|
|
281
|
+
? { spawnedBy: parentCorrelationId }
|
|
282
|
+
: {}),
|
|
283
|
+
}, now);
|
|
284
|
+
}
|
|
226
285
|
const row = this.roster.get(p.correlationId);
|
|
227
286
|
if (!row) return;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
287
|
+
if (
|
|
288
|
+
parentCorrelationId !== p.correlationId
|
|
289
|
+
&& row.parentCorrelationId === undefined
|
|
290
|
+
) row.parentCorrelationId = parentCorrelationId;
|
|
231
291
|
row.agent = clean(p.agent) || row.agent;
|
|
232
292
|
row.name = p.name === undefined ? row.name : clean(p.name);
|
|
233
293
|
row.role = deriveRole(p.agent, p.name);
|
|
234
294
|
row.task = p.name === undefined ? row.task : clean(p.name);
|
|
295
|
+
if (p.startedAt !== undefined) row.startedAt = normalizeStartedAt(p.startedAt, row.startedAt);
|
|
235
296
|
if (typeof p.status === "string") {
|
|
236
297
|
row.taskStatus = p.status;
|
|
237
298
|
row.status = mapAgentStatus(p.status);
|
|
299
|
+
if (row.status === "done" || row.status === "failed") {
|
|
300
|
+
row.finishedAt = terminalTime(row, p, now);
|
|
301
|
+
} else {
|
|
302
|
+
delete row.finishedAt;
|
|
303
|
+
}
|
|
238
304
|
}
|
|
239
305
|
row.taskIndex = p.taskIndex;
|
|
240
306
|
row.dependencies = Array.isArray(p.dependencies) ? [...p.dependencies] : [];
|
|
241
|
-
if (p.startedAt !== undefined) row.startedAt = normalizeStartedAt(p.startedAt, row.startedAt);
|
|
242
307
|
row.lastActivityAt = normalizeStartedAt(p.lastActivityAt, now);
|
|
243
308
|
if (p.recentTools) {
|
|
244
309
|
const tool = latestTool(p.recentTools);
|
package/src/render.ts
CHANGED
|
@@ -239,7 +239,7 @@ export function renderAgents(
|
|
|
239
239
|
if (status.label) segs.push({ text: theme.fg(status.color, status.label), priority: 95, clippable: false });
|
|
240
240
|
segs.push({ text: theme.fg(rc, r.role), priority: 90 });
|
|
241
241
|
if (r.task) segs.push({ text: r.task, priority: 80, minWidth: 6 });
|
|
242
|
-
segs.push({ text: theme.fg("muted", formatDuration(now - r.startedAt)), priority: 70, clippable: false });
|
|
242
|
+
segs.push({ text: theme.fg("muted", formatDuration((r.finishedAt ?? now) - r.startedAt)), priority: 70, clippable: false });
|
|
243
243
|
if (r.dependencies?.length) {
|
|
244
244
|
segs.push({ text: theme.fg("dim", formatDependencies(r.dependencies, g)), priority: 60, clippable: false });
|
|
245
245
|
}
|
package/src/stack-widget.ts
CHANGED
|
@@ -1,111 +1,133 @@
|
|
|
1
|
-
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { TUI } from "@earendil-works/pi-tui";
|
|
3
|
-
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import { renderAgents, renderTodos, type PaintTheme, type WidthUtils } from "./render.ts";
|
|
5
|
-
import { fitLineByPriority, type PrioritizedSegment } from "./layout.ts";
|
|
6
|
-
import { resolveGlyphs, spinFrame } from "./icons.ts";
|
|
7
|
-
import { panelRows } from "./viewport.ts";
|
|
8
|
-
import type { AgentRow, CockpitConfig, TodoItem } from "./types.ts";
|
|
9
|
-
|
|
10
|
-
export interface TodoWidgetDeps {
|
|
11
|
-
getTodos: () => TodoItem[];
|
|
12
|
-
getConfig: () => CockpitConfig;
|
|
13
|
-
/** False when no redraw loop is running, so spinners must not freeze mid-cycle. */
|
|
14
|
-
isAnimating?: () => boolean;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface AgentWidgetDeps {
|
|
18
|
-
getAgents: () => AgentRow[];
|
|
19
|
-
getConfig: () => CockpitConfig;
|
|
20
|
-
isRunning: () => boolean;
|
|
21
|
-
isAnimating?: () => boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const UTILS: WidthUtils = { measure: visibleWidth, clip: truncateToWidth };
|
|
25
|
-
|
|
26
|
-
// The TUI exposes the live terminal size, but a widget must never fail to render
|
|
27
|
-
// because a host handed it a stub without one.
|
|
28
|
-
export function terminalRows(tui: TUI): number | undefined {
|
|
29
|
-
try {
|
|
30
|
-
const rows = tui.terminal?.rows;
|
|
31
|
-
return typeof rows === "number" && rows > 0 ? rows : undefined;
|
|
32
|
-
} catch {
|
|
33
|
-
return undefined;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
];
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { renderAgents, renderTodos, type PaintTheme, type WidthUtils } from "./render.ts";
|
|
5
|
+
import { fitLineByPriority, type PrioritizedSegment } from "./layout.ts";
|
|
6
|
+
import { resolveGlyphs, spinFrame } from "./icons.ts";
|
|
7
|
+
import { panelRows } from "./viewport.ts";
|
|
8
|
+
import type { AgentRow, CockpitConfig, TodoItem } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export interface TodoWidgetDeps {
|
|
11
|
+
getTodos: () => TodoItem[];
|
|
12
|
+
getConfig: () => CockpitConfig;
|
|
13
|
+
/** False when no redraw loop is running, so spinners must not freeze mid-cycle. */
|
|
14
|
+
isAnimating?: () => boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentWidgetDeps {
|
|
18
|
+
getAgents: () => AgentRow[];
|
|
19
|
+
getConfig: () => CockpitConfig;
|
|
20
|
+
isRunning: () => boolean;
|
|
21
|
+
isAnimating?: () => boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const UTILS: WidthUtils = { measure: visibleWidth, clip: truncateToWidth };
|
|
25
|
+
|
|
26
|
+
// The TUI exposes the live terminal size, but a widget must never fail to render
|
|
27
|
+
// because a host handed it a stub without one.
|
|
28
|
+
export function terminalRows(tui: TUI): number | undefined {
|
|
29
|
+
try {
|
|
30
|
+
const rows = tui.terminal?.rows;
|
|
31
|
+
return typeof rows === "number" && rows > 0 ? rows : undefined;
|
|
32
|
+
} catch {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function visibleAgentRows(rows: AgentRow[]): AgentRow[] {
|
|
38
|
+
const byId = new Map(rows.map((row) => [row.correlationId, row]));
|
|
39
|
+
return rows.filter((row) => !row.agent.startsWith("graph(")).map((row) => {
|
|
40
|
+
let parent = row.parentCorrelationId;
|
|
41
|
+
const visited = new Set([row.correlationId]);
|
|
42
|
+
while (parent) {
|
|
43
|
+
if (visited.has(parent)) {
|
|
44
|
+
parent = undefined;
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
visited.add(parent);
|
|
48
|
+
const parentRow = byId.get(parent);
|
|
49
|
+
if (!parentRow || !parentRow.agent.startsWith("graph(")) break;
|
|
50
|
+
parent = parentRow.parentCorrelationId;
|
|
51
|
+
}
|
|
52
|
+
return parent === row.parentCorrelationId ? row : { ...row, parentCorrelationId: parent };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Todo widget: pinned above the editor (setWidget "cockpit-stack", aboveEditor).
|
|
57
|
+
export function makeTodoWidget(deps: TodoWidgetDeps) {
|
|
58
|
+
return (tui: TUI, theme: Theme) => {
|
|
59
|
+
const paint: PaintTheme = theme;
|
|
60
|
+
return {
|
|
61
|
+
render(width: number): string[] {
|
|
62
|
+
const cfg = deps.getConfig();
|
|
63
|
+
const todos = deps.getTodos();
|
|
64
|
+
if (todos.length === 0) return [];
|
|
65
|
+
const g = resolveGlyphs(cfg.icons.mode);
|
|
66
|
+
const now = Date.now();
|
|
67
|
+
const spin = spinFrame(g, now, deps.isAnimating?.() ?? true);
|
|
68
|
+
const opts = {
|
|
69
|
+
glyphs: g,
|
|
70
|
+
spin,
|
|
71
|
+
now,
|
|
72
|
+
expanded: cfg.todoExpanded,
|
|
73
|
+
maxRows: panelRows(terminalRows(tui)),
|
|
74
|
+
};
|
|
75
|
+
return renderTodos(todos, cfg.todoExpanded ? "list" : cfg.todoMode, width, paint, UTILS, opts);
|
|
76
|
+
},
|
|
77
|
+
invalidate(): void {},
|
|
78
|
+
dispose(): void {},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Agent widget: pinned below the editor, near the input box (setWidget "cockpit-agents", belowEditor).
|
|
84
|
+
export function makeAgentWidget(deps: AgentWidgetDeps) {
|
|
85
|
+
return (tui: TUI, theme: Theme) => {
|
|
86
|
+
const paint: PaintTheme = theme;
|
|
87
|
+
return {
|
|
88
|
+
render(width: number): string[] {
|
|
89
|
+
const cfg = deps.getConfig();
|
|
90
|
+
// graph(...) is the dispatch container, not an additional worker. Keep it
|
|
91
|
+
// in AgentsStore for linkage and cleanup, and bridge its visible descendants
|
|
92
|
+
// to the nearest non-graph ancestor for rendering.
|
|
93
|
+
const agents = visibleAgentRows(deps.getAgents());
|
|
94
|
+
if (agents.length === 0) return [];
|
|
95
|
+
const g = resolveGlyphs(cfg.icons.mode);
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
const animating = deps.isAnimating?.() ?? true;
|
|
98
|
+
const spin = spinFrame(g, now, animating);
|
|
99
|
+
const running = deps.isRunning();
|
|
100
|
+
// The panel budget covers the roster header and its rows.
|
|
101
|
+
const panel = panelRows(terminalRows(tui));
|
|
102
|
+
const rosterRows = panel === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: Math.max(1, panel - 1);
|
|
105
|
+
const opts = { glyphs: g, spin, now, maxRows: rosterRows };
|
|
106
|
+
const dot = theme.fg(running ? "success" : "muted", running ? g.dotRunning : g.dotIdle);
|
|
107
|
+
const failedCount = agents.filter((a) => a.status === "failed").length;
|
|
108
|
+
const runCount = agents.filter((a) => a.status === "running" || a.status === "retrying").length;
|
|
109
|
+
const pendingCount = agents.filter((a) => a.status === "pending").length;
|
|
110
|
+
const sleepingCount = agents.filter((a) => a.status === "sleeping").length;
|
|
111
|
+
// This header owns the roster summary, so compact mode must not print
|
|
112
|
+
// its own count line right underneath saying the same thing.
|
|
113
|
+
const headerSegs: PrioritizedSegment[] = [
|
|
114
|
+
{ text: dot, priority: 100, clippable: false },
|
|
115
|
+
{ text: theme.fg("muted", "Agents"), priority: 90, clippable: false },
|
|
116
|
+
];
|
|
117
|
+
if (failedCount) {
|
|
118
|
+
headerSegs.push({ text: theme.fg("error", `${failedCount} failed`), priority: 95, clippable: false });
|
|
119
|
+
}
|
|
120
|
+
if (runCount) headerSegs.push({ text: theme.fg("dim", `${runCount} running`), priority: 80, clippable: false });
|
|
121
|
+
if (pendingCount) headerSegs.push({ text: theme.fg("dim", `${pendingCount} pending`), priority: 60, clippable: false });
|
|
122
|
+
if (sleepingCount) headerSegs.push({ text: theme.fg("dim", `${sleepingCount} sleeping`), priority: 50, clippable: false });
|
|
123
|
+
const lines: string[] = [];
|
|
124
|
+
// Was the one line in the package pushed without any width clipping.
|
|
125
|
+
lines.push(fitLineByPriority(headerSegs, width, UTILS, theme.fg("dim", g.separator), g.ellipsis));
|
|
126
|
+
lines.push(...renderAgents(agents, cfg.agentsMode, width, paint, UTILS, { ...opts, withHead: false }));
|
|
127
|
+
return lines;
|
|
128
|
+
},
|
|
129
|
+
invalidate(): void {},
|
|
130
|
+
dispose(): void {},
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,114 +1,116 @@
|
|
|
1
|
-
// Cross-extension event names broadcast by pi-maestro-teammate.
|
|
2
|
-
// Hard-coded strings (stable contract; see pi-maestro-teammate/src/shared/types.ts:184-186)
|
|
3
|
-
// so this package needs no import path into the teammate package.
|
|
4
|
-
export const TEAMMATE_STARTED_EVENT = "teammate:started";
|
|
5
|
-
export const TEAMMATE_MESSAGE_EVENT = "teammate:message";
|
|
6
|
-
export const TEAMMATE_COMPLETE_EVENT = "teammate:complete";
|
|
7
|
-
export const COCKPIT_UI_OWNERSHIP_EVENT = "cockpit:ui-ownership";
|
|
8
|
-
export const COCKPIT_TODO_TOGGLE_EVENT = "cockpit:toggle-todo";
|
|
9
|
-
export const BASH_BG_UPDATE_EVENT = "bash-bg:update";
|
|
10
|
-
export const BASH_BG_QUERY_EVENT = "bash-bg:query";
|
|
11
|
-
export const WORKFLOW_STATUS_KEY = "maestro-workflow";
|
|
12
|
-
|
|
13
|
-
// Widget keys owned by pi-cockpit. Native Flow/teammate widgets yield through
|
|
14
|
-
// COCKPIT_UI_OWNERSHIP_EVENT instead of being cleared across extension boundaries.
|
|
15
|
-
export const STACK_WIDGET_KEY = "cockpit-stack";
|
|
16
|
-
export const AGENT_WIDGET_KEY = "cockpit-agents";
|
|
17
|
-
|
|
18
|
-
// todo tool name registered by pi-maestro-flow (src/extension/index.ts:594).
|
|
19
|
-
export const TODO_TOOL_NAME = "todo";
|
|
20
|
-
// appendEntry customType the todo tool persists after every mutation (tools/todo.ts:145).
|
|
21
|
-
export const TODO_STATE_ENTRY_TYPE = "todo-state";
|
|
22
|
-
|
|
23
|
-
export type AgentStatus = "pending" | "running" | "retrying" | "sleeping" | "done" | "failed";
|
|
24
|
-
|
|
25
|
-
export interface AgentRow {
|
|
26
|
-
correlationId: string;
|
|
27
|
-
parentCorrelationId?: string;
|
|
28
|
-
agent: string;
|
|
29
|
-
name: string | undefined;
|
|
30
|
-
role: string;
|
|
31
|
-
task: string;
|
|
32
|
-
status: AgentStatus;
|
|
33
|
-
tail: string;
|
|
34
|
-
startedAt: number;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
export
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
export type
|
|
91
|
-
|
|
92
|
-
export
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
1
|
+
// Cross-extension event names broadcast by pi-maestro-teammate.
|
|
2
|
+
// Hard-coded strings (stable contract; see pi-maestro-teammate/src/shared/types.ts:184-186)
|
|
3
|
+
// so this package needs no import path into the teammate package.
|
|
4
|
+
export const TEAMMATE_STARTED_EVENT = "teammate:started";
|
|
5
|
+
export const TEAMMATE_MESSAGE_EVENT = "teammate:message";
|
|
6
|
+
export const TEAMMATE_COMPLETE_EVENT = "teammate:complete";
|
|
7
|
+
export const COCKPIT_UI_OWNERSHIP_EVENT = "cockpit:ui-ownership";
|
|
8
|
+
export const COCKPIT_TODO_TOGGLE_EVENT = "cockpit:toggle-todo";
|
|
9
|
+
export const BASH_BG_UPDATE_EVENT = "bash-bg:update";
|
|
10
|
+
export const BASH_BG_QUERY_EVENT = "bash-bg:query";
|
|
11
|
+
export const WORKFLOW_STATUS_KEY = "maestro-workflow";
|
|
12
|
+
|
|
13
|
+
// Widget keys owned by pi-cockpit. Native Flow/teammate widgets yield through
|
|
14
|
+
// COCKPIT_UI_OWNERSHIP_EVENT instead of being cleared across extension boundaries.
|
|
15
|
+
export const STACK_WIDGET_KEY = "cockpit-stack";
|
|
16
|
+
export const AGENT_WIDGET_KEY = "cockpit-agents";
|
|
17
|
+
|
|
18
|
+
// todo tool name registered by pi-maestro-flow (src/extension/index.ts:594).
|
|
19
|
+
export const TODO_TOOL_NAME = "todo";
|
|
20
|
+
// appendEntry customType the todo tool persists after every mutation (tools/todo.ts:145).
|
|
21
|
+
export const TODO_STATE_ENTRY_TYPE = "todo-state";
|
|
22
|
+
|
|
23
|
+
export type AgentStatus = "pending" | "running" | "retrying" | "sleeping" | "done" | "failed";
|
|
24
|
+
|
|
25
|
+
export interface AgentRow {
|
|
26
|
+
correlationId: string;
|
|
27
|
+
parentCorrelationId?: string;
|
|
28
|
+
agent: string;
|
|
29
|
+
name: string | undefined;
|
|
30
|
+
role: string;
|
|
31
|
+
task: string;
|
|
32
|
+
status: AgentStatus;
|
|
33
|
+
tail: string;
|
|
34
|
+
startedAt: number;
|
|
35
|
+
/** Terminal wall-clock used to freeze elapsed time for completed/failed rows. */
|
|
36
|
+
finishedAt?: number;
|
|
37
|
+
lastActivityAt: number;
|
|
38
|
+
toolCount?: number;
|
|
39
|
+
tokens?: number;
|
|
40
|
+
inputTokens?: number;
|
|
41
|
+
outputTokens?: number;
|
|
42
|
+
taskStatus?: string;
|
|
43
|
+
activeTool?: string;
|
|
44
|
+
taskIndex?: number;
|
|
45
|
+
dependencies?: number[];
|
|
46
|
+
/** Set only on a failed row that outlived its completion, so it can be pruned. */
|
|
47
|
+
failedAt?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type BashBgStatus = "running" | "stopping" | "completed" | "failed" | "killed";
|
|
51
|
+
|
|
52
|
+
export interface BashBgJob {
|
|
53
|
+
id: string;
|
|
54
|
+
command: string;
|
|
55
|
+
cwd: string;
|
|
56
|
+
pid: number;
|
|
57
|
+
status: BashBgStatus;
|
|
58
|
+
startedAt: number;
|
|
59
|
+
updatedAt: number;
|
|
60
|
+
finishedAt?: number;
|
|
61
|
+
exitCode: number | null;
|
|
62
|
+
outputTail: string;
|
|
63
|
+
outputBytes: number;
|
|
64
|
+
logPath: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type TodoState = "pending" | "in_progress" | "completed" | "blocked";
|
|
68
|
+
|
|
69
|
+
export interface TodoItem {
|
|
70
|
+
id: string;
|
|
71
|
+
subject: string;
|
|
72
|
+
status: TodoState;
|
|
73
|
+
blockedBy: string[];
|
|
74
|
+
createdBy?: TodoActor;
|
|
75
|
+
assignee?: TodoActor;
|
|
76
|
+
skills: TodoSkill[];
|
|
77
|
+
updatedAt?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface TodoActor {
|
|
81
|
+
id: string;
|
|
82
|
+
label: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface TodoSkill {
|
|
86
|
+
name: string;
|
|
87
|
+
role?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export type ViewMode = "list" | "compact";
|
|
91
|
+
|
|
92
|
+
export type IconMode = "auto" | "nerd" | "ascii";
|
|
93
|
+
|
|
94
|
+
export interface CockpitConfig {
|
|
95
|
+
enabled: boolean;
|
|
96
|
+
agentsMode: ViewMode;
|
|
97
|
+
todoMode: ViewMode;
|
|
98
|
+
todoExpanded: boolean;
|
|
99
|
+
hideNativeAgents: boolean;
|
|
100
|
+
icons: { mode: IconMode };
|
|
101
|
+
/**
|
|
102
|
+
* Theme to apply at session start. Empty means "leave whatever pi is using",
|
|
103
|
+
* so cockpit never overrides a theme the user picked elsewhere.
|
|
104
|
+
*/
|
|
105
|
+
theme: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const DEFAULT_CONFIG: CockpitConfig = {
|
|
109
|
+
enabled: true,
|
|
110
|
+
agentsMode: "list",
|
|
111
|
+
todoMode: "list",
|
|
112
|
+
todoExpanded: false,
|
|
113
|
+
hideNativeAgents: true,
|
|
114
|
+
icons: { mode: "auto" },
|
|
115
|
+
theme: "",
|
|
116
|
+
};
|