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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-cockpit",
3
- "version": "0.2.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.0.0",
62
+ "pi-maestro-teammate": "1.2.0",
63
63
  "typescript": "^5.5.0"
64
64
  },
65
65
  "main": "./src/index.ts",
@@ -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
- this.roster.set(id, {
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
- row.parentCorrelationId = parentCorrelationId === p.correlationId
229
- ? row.parentCorrelationId
230
- : parentCorrelationId;
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
  }
@@ -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
- // Todo widget: pinned above the editor (setWidget "cockpit-stack", aboveEditor).
38
- export function makeTodoWidget(deps: TodoWidgetDeps) {
39
- return (tui: TUI, theme: Theme) => {
40
- const paint: PaintTheme = theme;
41
- return {
42
- render(width: number): string[] {
43
- const cfg = deps.getConfig();
44
- const todos = deps.getTodos();
45
- if (todos.length === 0) return [];
46
- const g = resolveGlyphs(cfg.icons.mode);
47
- const now = Date.now();
48
- const spin = spinFrame(g, now, deps.isAnimating?.() ?? true);
49
- const opts = {
50
- glyphs: g,
51
- spin,
52
- now,
53
- expanded: cfg.todoExpanded,
54
- maxRows: panelRows(terminalRows(tui)),
55
- };
56
- return renderTodos(todos, cfg.todoExpanded ? "list" : cfg.todoMode, width, paint, UTILS, opts);
57
- },
58
- invalidate(): void {},
59
- dispose(): void {},
60
- };
61
- };
62
- }
63
-
64
- // Agent widget: pinned below the editor, near the input box (setWidget "cockpit-agents", belowEditor).
65
- export function makeAgentWidget(deps: AgentWidgetDeps) {
66
- return (tui: TUI, theme: Theme) => {
67
- const paint: PaintTheme = theme;
68
- return {
69
- render(width: number): string[] {
70
- const cfg = deps.getConfig();
71
- const agents = deps.getAgents();
72
- if (agents.length === 0) return [];
73
- const g = resolveGlyphs(cfg.icons.mode);
74
- const now = Date.now();
75
- const animating = deps.isAnimating?.() ?? true;
76
- const spin = spinFrame(g, now, animating);
77
- const running = deps.isRunning();
78
- // The panel budget covers the roster header and its rows.
79
- const panel = panelRows(terminalRows(tui));
80
- const rosterRows = panel === undefined
81
- ? undefined
82
- : Math.max(1, panel - 1);
83
- const opts = { glyphs: g, spin, now, maxRows: rosterRows };
84
- const dot = theme.fg(running ? "success" : "muted", running ? g.dotRunning : g.dotIdle);
85
- const failedCount = agents.filter((a) => a.status === "failed").length;
86
- const runCount = agents.filter((a) => a.status === "running" || a.status === "retrying").length;
87
- const pendingCount = agents.filter((a) => a.status === "pending").length;
88
- const sleepingCount = agents.filter((a) => a.status === "sleeping").length;
89
- // This header owns the roster summary, so compact mode must not print
90
- // its own count line right underneath saying the same thing.
91
- const headerSegs: PrioritizedSegment[] = [
92
- { text: dot, priority: 100, clippable: false },
93
- { text: theme.fg("muted", "Agents"), priority: 90, clippable: false },
94
- ];
95
- if (failedCount) {
96
- headerSegs.push({ text: theme.fg("error", `${failedCount} failed`), priority: 95, clippable: false });
97
- }
98
- if (runCount) headerSegs.push({ text: theme.fg("dim", `${runCount} running`), priority: 80, clippable: false });
99
- if (pendingCount) headerSegs.push({ text: theme.fg("dim", `${pendingCount} pending`), priority: 60, clippable: false });
100
- if (sleepingCount) headerSegs.push({ text: theme.fg("dim", `${sleepingCount} sleeping`), priority: 50, clippable: false });
101
- const lines: string[] = [];
102
- // Was the one line in the package pushed without any width clipping.
103
- lines.push(fitLineByPriority(headerSegs, width, UTILS, theme.fg("dim", g.separator), g.ellipsis));
104
- lines.push(...renderAgents(agents, cfg.agentsMode, width, paint, UTILS, { ...opts, withHead: false }));
105
- return lines;
106
- },
107
- invalidate(): void {},
108
- dispose(): void {},
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
- lastActivityAt: number;
36
- toolCount?: number;
37
- tokens?: number;
38
- inputTokens?: number;
39
- outputTokens?: number;
40
- taskStatus?: string;
41
- activeTool?: string;
42
- taskIndex?: number;
43
- dependencies?: number[];
44
- /** Set only on a failed row that outlived its completion, so it can be pruned. */
45
- failedAt?: number;
46
- }
47
-
48
- export type BashBgStatus = "running" | "stopping" | "completed" | "failed" | "killed";
49
-
50
- export interface BashBgJob {
51
- id: string;
52
- command: string;
53
- cwd: string;
54
- pid: number;
55
- status: BashBgStatus;
56
- startedAt: number;
57
- updatedAt: number;
58
- finishedAt?: number;
59
- exitCode: number | null;
60
- outputTail: string;
61
- outputBytes: number;
62
- logPath: string;
63
- }
64
-
65
- export type TodoState = "pending" | "in_progress" | "completed" | "blocked";
66
-
67
- export interface TodoItem {
68
- id: string;
69
- subject: string;
70
- status: TodoState;
71
- blockedBy: string[];
72
- createdBy?: TodoActor;
73
- assignee?: TodoActor;
74
- skills: TodoSkill[];
75
- updatedAt?: number;
76
- }
77
-
78
- export interface TodoActor {
79
- id: string;
80
- label: string;
81
- }
82
-
83
- export interface TodoSkill {
84
- name: string;
85
- role?: string;
86
- }
87
-
88
- export type ViewMode = "list" | "compact";
89
-
90
- export type IconMode = "auto" | "nerd" | "ascii";
91
-
92
- export interface CockpitConfig {
93
- enabled: boolean;
94
- agentsMode: ViewMode;
95
- todoMode: ViewMode;
96
- todoExpanded: boolean;
97
- hideNativeAgents: boolean;
98
- icons: { mode: IconMode };
99
- /**
100
- * Theme to apply at session start. Empty means "leave whatever pi is using",
101
- * so cockpit never overrides a theme the user picked elsewhere.
102
- */
103
- theme: string;
104
- }
105
-
106
- export const DEFAULT_CONFIG: CockpitConfig = {
107
- enabled: true,
108
- agentsMode: "list",
109
- todoMode: "list",
110
- todoExpanded: false,
111
- hideNativeAgents: true,
112
- icons: { mode: "auto" },
113
- theme: "",
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
+ };