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 +12 -5
- package/package.json +70 -70
- package/src/ambient.ts +3 -1
- package/src/render.ts +16 -48
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
|
|
23
|
-
|
|
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
|
|
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`
|
|
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.
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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,
|