pi-cockpit 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  A list-mode **agent cockpit** for [Pi](https://pi.dev): a status stack pinned **above the editor** showing live teammates and the current todo plan, plus a Starship-style footer. Everything is drawn through Pi's public extension APIs only (`setWidget` / `setFooter`) — no core patches.
4
4
 
5
+ It is the third plugin of the `pi-maestro-flow` project (alongside `pi-maestro-flow` and `pi-maestro-teammate`). Since `pi-maestro-flow@0.6.1` it is an exact-pinned dependency of `pi-maestro-flow` — installed together and auto-registered into Pi's `settings.packages` on postinstall — but it also installs and runs standalone.
6
+
5
7
  ```
6
8
  ┌─ AGENTS · 2 running ─────────────────────────────┐ ← setWidget(aboveEditor)
7
9
  │ ⠋ explorer #a1f3c2 map auth read routes.ts │
@@ -18,16 +20,19 @@ A list-mode **agent cockpit** for [Pi](https://pi.dev): a status stack pinned **
18
20
 
19
21
  ## Install
20
22
 
23
+ `pi-cockpit` comes automatically with the orchestration layer — installing `pi-maestro-flow@0.6.1` pulls `pi-cockpit@0.1.1` and registers it into `settings.packages` on postinstall, no manual setup required. To use it on its own:
24
+
21
25
  ```bash
22
- pi install <path-or-spec> # or, for one run:
23
- pi -e ./extensions/pi-cockpit
26
+ pi install npm:pi-cockpit # standalone from npm
27
+ # or, for one run:
28
+ pi -e ./packages/pi-cockpit
24
29
  ```
25
30
 
26
31
  ## What it shows
27
32
 
28
33
  - **AGENTS** — every running teammate as a table row (status spinner · role · id · label · live tail), sorted running-first. Collapses to a one-line `N agents running` summary in compact mode.
29
34
  - **TODO** — the active plan as numbered rows with four states (done ✓ / in-progress spinner / blocked ! / pending ·). Collapses to a segmented progress bar + current step + percent.
30
- - **Footer** — `provider/model · context gauge · ↑in ↓out · $cost · elapsed · git branch`.
35
+ - **Footer** — `provider/model · context gauge · ↑in ↓out · $cost · elapsed · git branch`. Since v0.1.1, `bash_bg` background-job state lives on a **dedicated second footer row** so it no longer competes with the primary line.
31
36
 
32
37
  Toggle each block between list and compact with `/cockpit`.
33
38
 
@@ -41,6 +46,8 @@ Toggle each block between list and compact with `/cockpit`.
41
46
 
42
47
  So on a stock Pi (no teammate / no todo tool) the extension loads without error, the status stack renders nothing, and only the footer appears. The roster is **self-accumulated from event deltas** — teammates already running before the extension loads are not back-filled (the teammate extension broadcasts deltas, never a full roster). The todo list **is** back-filled on `session_start` from the persisted snapshot.
43
48
 
49
+ This is the whole coupling story: cockpit has **no package dependency on `pi-maestro-flow`** and only a peer dependency on `pi-maestro-teammate`. It observes the other two plugins through public channels alone — teammate's event broadcasts for AGENTS, and the `todo-state` snapshot flow's `todo` tool persists for TODO. Remove either source and the matching block simply hides; the footer always remains.
50
+
44
51
  ## Configuration
45
52
 
46
53
  `~/.pi/agent/cockpit.json` (created on first run):
@@ -75,12 +82,12 @@ The original mockup was a browser page; a terminal is an ANSI stream with no DOM
75
82
  ## Local development
76
83
 
77
84
  ```bash
78
- cd extensions/pi-cockpit
85
+ cd packages/pi-cockpit
79
86
  node --test --experimental-transform-types tests/agents-store.test.ts tests/todo-store.test.ts tests/render.test.ts tests/footer.test.ts
80
87
  ../../node_modules/.bin/tsc -p tsconfig.json # type-check (uses the monorepo root's tsc + types)
81
88
  ```
82
89
 
83
- The package lives inside the `pi-maestro-flow` tree so it resolves `@earendil-works/*` types from the root `node_modules`, but it is **not** part of the `packages/*` workspace — it neither pollutes the lockfile nor gets swept by monorepo builds.
90
+ The package lives inside the `pi-maestro-flow` monorepo under `packages/` so it resolves `@earendil-works/*` types from the root `node_modules`. Since v0.6.1 it is also an exact-pinned dependency of `pi-maestro-flow` and ships as its own npm package (`pi-cockpit`).
84
91
 
85
92
  ## License
86
93
 
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
- {
2
- "name": "pi-cockpit",
3
- "version": "0.1.1",
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
- "type": "module",
6
- "keywords": [
7
- "pi-package",
8
- "pi",
9
- "pi-extension",
10
- "tui",
11
- "teammate",
12
- "todo"
13
- ],
14
- "license": "MIT",
15
- "files": [
16
- "src/",
17
- "themes/",
18
- "README.md"
19
- ],
20
- "scripts": {
21
- "test": "node --test --experimental-transform-types tests/agents-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/settings-view.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts",
22
- "typecheck": "tsc --noEmit"
23
- },
24
- "pi": {
25
- "extensions": [
26
- "./src/extension/index.ts"
27
- ],
28
- "themes": [
29
- "./themes"
30
- ]
31
- },
32
- "peerDependencies": {
33
- "@earendil-works/pi-agent-core": "*",
34
- "@earendil-works/pi-ai": "*",
35
- "@earendil-works/pi-coding-agent": "*",
36
- "@earendil-works/pi-tui": "*",
37
- "pi-maestro-teammate": "^0.6.0"
38
- },
39
- "peerDependenciesMeta": {
40
- "@earendil-works/pi-agent-core": {
41
- "optional": true
42
- },
43
- "@earendil-works/pi-ai": {
44
- "optional": true
45
- },
46
- "@earendil-works/pi-coding-agent": {
47
- "optional": true
48
- },
49
- "@earendil-works/pi-tui": {
50
- "optional": true
51
- },
52
- "pi-maestro-teammate": {
53
- "optional": true
54
- }
55
- },
56
- "devDependencies": {
57
- "@earendil-works/pi-agent-core": "0.82.1",
58
- "@earendil-works/pi-ai": "0.82.1",
59
- "@earendil-works/pi-coding-agent": "0.82.1",
60
- "@earendil-works/pi-tui": "0.82.1",
61
- "@types/node": "^22.0.0",
62
- "pi-maestro-teammate": "0.6.0",
63
- "typescript": "^5.5.0"
64
- },
65
- "main": "./src/index.ts",
66
- "exports": {
67
- ".": "./src/index.ts",
68
- "./src/*": "./src/*"
69
- }
70
- }
1
+ {
2
+ "name": "pi-cockpit",
3
+ "version": "0.1.2",
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
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi",
9
+ "pi-extension",
10
+ "tui",
11
+ "teammate",
12
+ "todo"
13
+ ],
14
+ "license": "MIT",
15
+ "files": [
16
+ "src/",
17
+ "themes/",
18
+ "README.md"
19
+ ],
20
+ "scripts": {
21
+ "test": "node --test --experimental-transform-types tests/agents-store.test.ts tests/bash-bg-store.test.ts tests/bash-bg-widget.test.ts tests/bash-bg-overlay.test.ts tests/todo-store.test.ts tests/render.test.ts tests/footer.test.ts tests/extension-status.test.ts tests/integration-contract.test.ts tests/stack-widget.test.ts tests/settings-view.test.ts tests/ambient.test.ts tests/viewport.test.ts tests/theme-picker.test.ts",
22
+ "typecheck": "tsc --noEmit"
23
+ },
24
+ "pi": {
25
+ "extensions": [
26
+ "./src/extension/index.ts"
27
+ ],
28
+ "themes": [
29
+ "./themes"
30
+ ]
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-agent-core": "*",
34
+ "@earendil-works/pi-ai": "*",
35
+ "@earendil-works/pi-coding-agent": "*",
36
+ "@earendil-works/pi-tui": "*",
37
+ "pi-maestro-teammate": "^0.6.0"
38
+ },
39
+ "peerDependenciesMeta": {
40
+ "@earendil-works/pi-agent-core": {
41
+ "optional": true
42
+ },
43
+ "@earendil-works/pi-ai": {
44
+ "optional": true
45
+ },
46
+ "@earendil-works/pi-coding-agent": {
47
+ "optional": true
48
+ },
49
+ "@earendil-works/pi-tui": {
50
+ "optional": true
51
+ },
52
+ "pi-maestro-teammate": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "devDependencies": {
57
+ "@earendil-works/pi-agent-core": "0.82.1",
58
+ "@earendil-works/pi-ai": "0.82.1",
59
+ "@earendil-works/pi-coding-agent": "0.82.1",
60
+ "@earendil-works/pi-tui": "0.82.1",
61
+ "@types/node": "^22.0.0",
62
+ "pi-maestro-teammate": "0.6.0",
63
+ "typescript": "^5.5.0"
64
+ },
65
+ "main": "./src/index.ts",
66
+ "exports": {
67
+ ".": "./src/index.ts",
68
+ "./src/*": "./src/*"
69
+ }
70
+ }
package/src/ambient.ts CHANGED
@@ -37,7 +37,9 @@ function failedJobs(jobs: readonly BashBgJob[]): BashBgJob[] {
37
37
  * Only replace the label while a foreground tool is actively executing.
38
38
  */
39
39
  export function workingMessage(state: AmbientState): string | undefined {
40
- return state.activeTool;
40
+ if (state.activeTool) return state.activeTool;
41
+ if (state.running) return "working";
42
+ return undefined;
41
43
  }
42
44
 
43
45
  /**
package/src/render.ts CHANGED
@@ -246,22 +246,12 @@ export function renderAgents(
246
246
  }
247
247
 
248
248
  const TODO_MAX_VISIBLE = 8;
249
- const RECENT_COMPLETED_MS = 30_000;
250
249
 
251
- function todoDisplayRank(item: TodoItem, now: number): number {
252
- if (item.status === "completed") {
253
- const recent = item.updatedAt !== undefined && now - item.updatedAt < RECENT_COMPLETED_MS;
254
- return recent ? 0 : 4;
255
- }
256
- if (item.status === "in_progress") return 1;
257
- if (item.status === "blocked") return 2;
258
- return 3;
259
- }
260
-
261
- function findNextTodo(items: readonly TodoItem[]): TodoItem | undefined {
262
- return items.find((i) => i.status === "in_progress")
263
- ?? items.find((i) => i.status === "pending" && i.blockedBy.length === 0)
264
- ?? items.find((i) => i.status === "blocked" || i.status === "pending");
250
+ // Tasks keep their creation order (the numeric id assigned at allocateTaskId).
251
+ // Non-numeric ids (workflow mirrors) sort after numeric ones, then lexicographically.
252
+ function todoIdOrder(id: string): number {
253
+ const n = Number(id);
254
+ return Number.isInteger(n) ? n : Number.POSITIVE_INFINITY;
265
255
  }
266
256
 
267
257
  function todoActorLabel(actor: NonNullable<TodoItem["assignee"]>, items: readonly TodoItem[]): string {
@@ -318,22 +308,6 @@ function todoSubject(paint: TodoPaint, subject: string, theme: PaintTheme): stri
318
308
  return paint.bold ? bold(theme, text) : text;
319
309
  }
320
310
 
321
- function todoNextLabel(
322
- item: TodoItem,
323
- items: readonly TodoItem[],
324
- glyphs: IconGlyphs,
325
- spin: string,
326
- theme: PaintTheme,
327
- ): string {
328
- const actor = todoActor(item, items);
329
- const paint = todoPaint(item.status, glyphs, spin);
330
- return [
331
- theme.fg(paint.glyphColor, paint.glyph),
332
- actor ? theme.fg("mdLink", actor) : "",
333
- todoSubject(paint, item.subject, theme),
334
- ].filter(Boolean).join(" ");
335
- }
336
-
337
311
  export function renderTodos(
338
312
  items: readonly TodoItem[],
339
313
  mode: ViewMode,
@@ -346,7 +320,6 @@ export function renderTodos(
346
320
  const g = opts.glyphs;
347
321
  const ell = theme.fg("dim", g.ellipsis);
348
322
  const spin = opts.spin ?? "~";
349
- const now = opts.now ?? Date.now();
350
323
  const expanded = opts.expanded !== false;
351
324
  const toggleHint = opts.toggleHint ?? DEFAULT_TOGGLE_HINT;
352
325
 
@@ -355,17 +328,11 @@ export function renderTodos(
355
328
  const done = items.filter((i) => i.status === "completed").length;
356
329
  const blocked = items.filter((i) => i.status === "blocked").length;
357
330
  const pct = total ? Math.round((done / total) * 100) : 0;
358
- const nxt = findNextTodo(items);
359
- const label = nxt
360
- ? `${theme.fg("dim", g.arrow)} ${todoNextLabel(nxt, items, g, spin, theme)}`
361
- : done === total ? theme.fg("success", `${g.check} all done`) : "";
362
331
  const segs: PrioritizedSegment[] = [
363
332
  { text: renderTodoBar(items, width, theme, g), priority: 60, clippable: false },
364
333
  { text: theme.fg("muted", `${pct}%`), priority: 90, clippable: false },
365
334
  ];
366
- // A glyph run is opaque to a screen reader, so the blocked count is spelled out.
367
335
  if (blocked > 0) segs.push({ text: theme.fg("error", `${blocked} blocked`), priority: 95, clippable: false });
368
- if (label) segs.push({ text: label, priority: 100, minWidth: 8 });
369
336
  segs.push({ text: theme.fg("dim", `(${toggleHint} expand)`), priority: 10, clippable: false });
370
337
  return [fitLineByPriority(segs, width, utils, " ", g.ellipsis)];
371
338
  }
@@ -377,17 +344,10 @@ export function renderTodos(
377
344
  const blocked = items.filter((i) => i.status === "blocked").length;
378
345
  const members = new Set(items.map((item) => item.assignee?.id).filter(Boolean)).size;
379
346
  const sep = theme.fg("dim", g.separator.trim());
380
- const nxt = findNextTodo(items);
381
- const nextText = nxt
382
- ? `${theme.fg(nxt.status === "blocked" ? "error" : "dim", g.arrow)} ${todoNextLabel(nxt, items, g, spin, theme)}`
383
- : theme.fg("success", `${g.check} all done`);
384
347
 
385
- // The next actionable task outranks the counts: it is the one token that tells
386
- // the user what to do, so it must survive when the terminal is narrow.
387
348
  const summarySegs: PrioritizedSegment[] = [
388
349
  { text: theme.fg("syntaxFunction", "Todo"), priority: 40, clippable: false },
389
350
  { text: theme.fg("dim", `${done}/${total}`), priority: 80, clippable: false },
390
- { text: nextText, priority: 100, minWidth: 8 },
391
351
  ];
392
352
  if (blocked > 0) {
393
353
  summarySegs.push({ text: theme.fg("error", `${blocked} blocked`), priority: 90, clippable: false });
@@ -398,6 +358,17 @@ export function renderTodos(
398
358
  if (members > 0) {
399
359
  summarySegs.push({ text: theme.fg("dim", `${members} members`), priority: 30, clippable: false });
400
360
  }
361
+ // Creation order drives both the next-task pointer and the expanded rows, so
362
+ // compute it once up front. The pointer is the first task the user can act on:
363
+ // not done, not blocked, and free of unresolved dependencies.
364
+ const ordered = [...items].sort((a, b) => todoIdOrder(a.id) - todoIdOrder(b.id) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
365
+ const next = ordered.find((it) => it.status !== "completed" && it.status !== "blocked" && it.blockedBy.length === 0);
366
+ if (next) {
367
+ const np = todoPaint(next.status, g, spin);
368
+ const nactor = todoActor(next, items);
369
+ const ntext = `${g.arrow} ${theme.fg(np.glyphColor, np.glyph)}${nactor ? ` ${theme.fg("mdLink", nactor)}` : ""} ${todoSubject(np, next.subject, theme)}`;
370
+ summarySegs.push({ text: ntext, priority: 20, clippable: false });
371
+ }
401
372
  summarySegs.push({
402
373
  text: theme.fg("dim", `(${toggleHint} ${expanded ? "collapse" : "expand"})`),
403
374
  priority: 10,
@@ -406,9 +377,6 @@ export function renderTodos(
406
377
  const summaryLine = fitLineByPriority(summarySegs, width, utils, ` ${sep} `, g.ellipsis);
407
378
 
408
379
  if (!expanded) return [summaryLine];
409
-
410
- // sorted + capped task rows
411
- const ordered = [...items].sort((a, b) => todoDisplayRank(a, now) - todoDisplayRank(b, now));
412
380
  // The summary line is already spent, so the task rows compete for what is left.
413
381
  const budget = Math.min(
414
382
  TODO_MAX_VISIBLE + 1,