pi-cockpit 0.3.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.3.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;
@@ -65,7 +67,8 @@ export interface MessagePayload {
65
67
  lastActivityAt?: number | string;
66
68
  progress?: ProgressPayload[];
67
69
  }
68
- export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exitCode">;
70
+ export type CompletePayload = Pick<TeammateCompleteEvent, "correlationId" | "exitCode">
71
+ & Partial<Pick<TeammateCompleteEvent, "durationMs">>;
69
72
 
70
73
  /**
71
74
  * How long a failed agent stays on screen after it completes.
@@ -109,6 +112,19 @@ function normalizeStartedAt(value: number | string | undefined, fallback: number
109
112
  return fallback;
110
113
  }
111
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
+
112
128
  function latestTool(tools: MessagePayload["recentTools"]): string | undefined {
113
129
  if (!tools?.length) return undefined;
114
130
  const tool = tools.find((candidate) => typeof candidate === "object" && candidate?.status === "running")
@@ -133,7 +149,7 @@ export class AgentsStore {
133
149
  applyStarted(p: StartedPayload, now: number = Date.now()): void {
134
150
  const id = p.correlationId;
135
151
  const prev = this.roster.get(id);
136
- this.roster.set(id, {
152
+ const row: AgentRow = {
137
153
  ...prev,
138
154
  correlationId: id,
139
155
  agent: clean(p.agent) || prev?.agent || "",
@@ -149,7 +165,9 @@ export class AgentsStore {
149
165
  : prev?.parentCorrelationId
150
166
  ? { parentCorrelationId: prev.parentCorrelationId }
151
167
  : {}),
152
- });
168
+ };
169
+ if (row.status !== "done" && row.status !== "failed") delete row.finishedAt;
170
+ this.roster.set(id, row);
153
171
  }
154
172
 
155
173
  applyMessage(p: MessagePayload, now = Date.now()): void {
@@ -191,6 +209,8 @@ export class AgentsStore {
191
209
  if (typeof p.status === "string") {
192
210
  row.taskStatus = p.status;
193
211
  row.status = mapAgentStatus(p.status);
212
+ if (row.status === "done" || row.status === "failed") row.finishedAt ??= now;
213
+ else delete row.finishedAt;
194
214
  }
195
215
  if (typeof p.taskIndex === "number") row.taskIndex = p.taskIndex;
196
216
  if (Array.isArray(p.dependencies)) row.dependencies = [...p.dependencies];
@@ -216,6 +236,7 @@ export class AgentsStore {
216
236
  if (row && (row.status === "failed" || failedByExitCode)) {
217
237
  row.status = "failed";
218
238
  row.taskStatus = "failed";
239
+ row.finishedAt = terminalTime(row, id === p.correlationId ? p : {}, now);
219
240
  row.failedAt = now;
220
241
  row.lastActivityAt = now;
221
242
  delete row.activeTool;
@@ -263,20 +284,26 @@ export class AgentsStore {
263
284
  }
264
285
  const row = this.roster.get(p.correlationId);
265
286
  if (!row) return;
266
- row.parentCorrelationId = parentCorrelationId === p.correlationId
267
- ? row.parentCorrelationId
268
- : parentCorrelationId;
287
+ if (
288
+ parentCorrelationId !== p.correlationId
289
+ && row.parentCorrelationId === undefined
290
+ ) row.parentCorrelationId = parentCorrelationId;
269
291
  row.agent = clean(p.agent) || row.agent;
270
292
  row.name = p.name === undefined ? row.name : clean(p.name);
271
293
  row.role = deriveRole(p.agent, p.name);
272
294
  row.task = p.name === undefined ? row.task : clean(p.name);
295
+ if (p.startedAt !== undefined) row.startedAt = normalizeStartedAt(p.startedAt, row.startedAt);
273
296
  if (typeof p.status === "string") {
274
297
  row.taskStatus = p.status;
275
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
+ }
276
304
  }
277
305
  row.taskIndex = p.taskIndex;
278
306
  row.dependencies = Array.isArray(p.dependencies) ? [...p.dependencies] : [];
279
- if (p.startedAt !== undefined) row.startedAt = normalizeStartedAt(p.startedAt, row.startedAt);
280
307
  row.lastActivityAt = normalizeStartedAt(p.lastActivityAt, now);
281
308
  if (p.recentTools) {
282
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
+ };