pi-cockpit 0.1.0 → 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/footer.ts +12 -8
- package/src/index.ts +37 -26
- package/src/render.ts +16 -48
- package/src/stack-widget.ts +4 -14
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/footer.ts
CHANGED
|
@@ -27,9 +27,10 @@ export interface FooterParts {
|
|
|
27
27
|
ctxTokens: number;
|
|
28
28
|
ctxWindow: number;
|
|
29
29
|
totals: UsageTotals;
|
|
30
|
-
git?: string;
|
|
31
|
-
agentSummary?: string;
|
|
32
|
-
|
|
30
|
+
git?: string;
|
|
31
|
+
agentSummary?: string;
|
|
32
|
+
bashBgStatus?: string;
|
|
33
|
+
workflowStatus?: string;
|
|
33
34
|
extensionStatuses?: readonly ExtensionStatusSegment[];
|
|
34
35
|
width: number;
|
|
35
36
|
glyphs: IconGlyphs;
|
|
@@ -322,11 +323,14 @@ export function renderFooter(p: FooterParts): string[] {
|
|
|
322
323
|
}
|
|
323
324
|
if (!selected) fittedLeft = fitLeftBeside("");
|
|
324
325
|
|
|
325
|
-
const line1 = alignRight(fittedLeft.join(identitySeparator), right1, width, utils.measure);
|
|
326
|
-
const lines = [utils.clip(line1, width, ell)];
|
|
327
|
-
if (p.
|
|
328
|
-
lines.push(utils.clip(
|
|
329
|
-
}
|
|
326
|
+
const line1 = alignRight(fittedLeft.join(identitySeparator), right1, width, utils.measure);
|
|
327
|
+
const lines = [utils.clip(line1, width, ell)];
|
|
328
|
+
if (p.bashBgStatus) {
|
|
329
|
+
lines.push(utils.clip(p.bashBgStatus, width, ell));
|
|
330
|
+
}
|
|
331
|
+
if (p.workflowStatus) {
|
|
332
|
+
lines.push(utils.clip(theme.fg("muted", p.workflowStatus), width, ell));
|
|
333
|
+
}
|
|
330
334
|
const statuses = visibleStatuses.filter((status) => !controlStatuses.includes(status));
|
|
331
335
|
if (statuses.length > 0) {
|
|
332
336
|
const statusSeparator = ` ${sep} `;
|
package/src/index.ts
CHANGED
|
@@ -2,16 +2,17 @@ import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
2
2
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
5
|
-
import { AgentsStore, type CompletePayload, type MessagePayload, type StartedPayload } from "./agents-store.ts";
|
|
6
|
-
import { statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
|
|
7
|
-
import { BashBgStore } from "./bash-bg-store.ts";
|
|
8
|
-
import { BashBgOverlay } from "./bash-bg-overlay.ts";
|
|
9
|
-
import {
|
|
5
|
+
import { AgentsStore, type CompletePayload, type MessagePayload, type StartedPayload } from "./agents-store.ts";
|
|
6
|
+
import { statusText, titleFor, workingMessage, type AmbientState } from "./ambient.ts";
|
|
7
|
+
import { BashBgStore } from "./bash-bg-store.ts";
|
|
8
|
+
import { BashBgOverlay } from "./bash-bg-overlay.ts";
|
|
9
|
+
import { renderBashBgSummary } from "./bash-bg-widget.ts";
|
|
10
|
+
import { TodoStore } from "./todo-store.ts";
|
|
10
11
|
import { makeTodoWidget, makeAgentWidget, terminalRows } from "./stack-widget.ts";
|
|
11
12
|
import { activeThemeName, ThemePicker } from "./theme-picker.ts";
|
|
12
13
|
import { getUsageTotals, invalidateUsageCache, renderFooter, type PaintTheme, type WidthUtils } from "./footer.ts";
|
|
13
14
|
import { collectExtensionStatuses } from "./extension-status.ts";
|
|
14
|
-
import { ANIMATION_PERIOD_MS, resolveGlyphs } from "./icons.ts";
|
|
15
|
+
import { ANIMATION_PERIOD_MS, resolveGlyphs, spinFrame } from "./icons.ts";
|
|
15
16
|
import { ensureConfigExists, loadConfig, saveConfig } from "./config.ts";
|
|
16
17
|
import { applyRow, buildRows, rowKeyForAccel, type SaveState } from "./settings-view.ts";
|
|
17
18
|
import {
|
|
@@ -180,13 +181,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
180
181
|
ctx.ui.setWidget(
|
|
181
182
|
AGENT_WIDGET_KEY,
|
|
182
183
|
(tui, theme) => {
|
|
183
|
-
capturedTui = tui;
|
|
184
|
-
return makeAgentWidget({
|
|
185
|
-
getAgents: () => agents.snapshot(),
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
isAnimating,
|
|
184
|
+
capturedTui = tui;
|
|
185
|
+
return makeAgentWidget({
|
|
186
|
+
getAgents: () => agents.snapshot(),
|
|
187
|
+
getConfig: () => config,
|
|
188
|
+
isRunning: () => running,
|
|
189
|
+
isAnimating,
|
|
190
190
|
})(tui, theme);
|
|
191
191
|
},
|
|
192
192
|
{ placement: "belowEditor" },
|
|
@@ -210,10 +210,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
210
210
|
const component = {
|
|
211
211
|
render(width: number): string[] {
|
|
212
212
|
if (Number.isFinite(width) && width > 0) observedWidth = width;
|
|
213
|
-
const cu = ctx.getContextUsage();
|
|
214
|
-
const branch = footerData.getGitBranch();
|
|
215
|
-
const extensionStatuses = collectExtensionStatuses(footerData.getExtensionStatuses());
|
|
216
|
-
|
|
213
|
+
const cu = ctx.getContextUsage();
|
|
214
|
+
const branch = footerData.getGitBranch();
|
|
215
|
+
const extensionStatuses = collectExtensionStatuses(footerData.getExtensionStatuses());
|
|
216
|
+
const glyphs = resolveGlyphs(config.icons.mode);
|
|
217
|
+
const now = Date.now();
|
|
218
|
+
const bashBgStatus = renderBashBgSummary(
|
|
219
|
+
bashBg.snapshot(),
|
|
220
|
+
width,
|
|
221
|
+
theme,
|
|
222
|
+
FOOTER_UTILS,
|
|
223
|
+
{
|
|
224
|
+
glyphs,
|
|
225
|
+
spin: spinFrame(glyphs, now, isAnimating()),
|
|
226
|
+
now,
|
|
227
|
+
},
|
|
228
|
+
)[0];
|
|
229
|
+
return renderFooter({
|
|
217
230
|
width,
|
|
218
231
|
model: ctx.model?.id ?? "no-model",
|
|
219
232
|
provider: ctx.model?.provider,
|
|
@@ -221,15 +234,13 @@ export default function (pi: ExtensionAPI): void {
|
|
|
221
234
|
cwd: formatCwd(ctx.sessionManager.getCwd()),
|
|
222
235
|
ctxPct: cu?.percent ?? 0,
|
|
223
236
|
ctxTokens: cu?.tokens ?? 0,
|
|
224
|
-
ctxWindow: cu?.contextWindow ?? ctx.model?.contextWindow ?? 0,
|
|
225
|
-
totals: getUsageTotals(ctx.sessionManager.getEntries()),
|
|
226
|
-
git: branch ?? undefined,
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
extensionStatuses: extensionStatuses.filter((status) => status.key !== WORKFLOW_STATUS_KEY),
|
|
232
|
-
glyphs: resolveGlyphs(config.icons.mode),
|
|
237
|
+
ctxWindow: cu?.contextWindow ?? ctx.model?.contextWindow ?? 0,
|
|
238
|
+
totals: getUsageTotals(ctx.sessionManager.getEntries()),
|
|
239
|
+
git: branch ?? undefined,
|
|
240
|
+
bashBgStatus,
|
|
241
|
+
workflowStatus: extensionStatuses.find((status) => status.key === WORKFLOW_STATUS_KEY)?.text,
|
|
242
|
+
extensionStatuses: extensionStatuses.filter((status) => status.key !== WORKFLOW_STATUS_KEY),
|
|
243
|
+
glyphs,
|
|
233
244
|
theme,
|
|
234
245
|
utils: FOOTER_UTILS,
|
|
235
246
|
});
|
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,
|
package/src/stack-widget.ts
CHANGED
|
@@ -2,11 +2,10 @@ import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import type { TUI } from "@earendil-works/pi-tui";
|
|
3
3
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { renderAgents, renderTodos, type PaintTheme, type WidthUtils } from "./render.ts";
|
|
5
|
-
import { renderBashBgSummary } from "./bash-bg-widget.ts";
|
|
6
5
|
import { fitLineByPriority, type PrioritizedSegment } from "./layout.ts";
|
|
7
6
|
import { resolveGlyphs, spinFrame } from "./icons.ts";
|
|
8
7
|
import { panelRows } from "./viewport.ts";
|
|
9
|
-
import type { AgentRow,
|
|
8
|
+
import type { AgentRow, CockpitConfig, TodoItem } from "./types.ts";
|
|
10
9
|
|
|
11
10
|
export interface TodoWidgetDeps {
|
|
12
11
|
getTodos: () => TodoItem[];
|
|
@@ -17,7 +16,6 @@ export interface TodoWidgetDeps {
|
|
|
17
16
|
|
|
18
17
|
export interface AgentWidgetDeps {
|
|
19
18
|
getAgents: () => AgentRow[];
|
|
20
|
-
getBashBgJobs: () => BashBgJob[];
|
|
21
19
|
getConfig: () => CockpitConfig;
|
|
22
20
|
isRunning: () => boolean;
|
|
23
21
|
isAnimating?: () => boolean;
|
|
@@ -71,24 +69,17 @@ export function makeAgentWidget(deps: AgentWidgetDeps) {
|
|
|
71
69
|
render(width: number): string[] {
|
|
72
70
|
const cfg = deps.getConfig();
|
|
73
71
|
const agents = deps.getAgents();
|
|
74
|
-
|
|
72
|
+
if (agents.length === 0) return [];
|
|
75
73
|
const g = resolveGlyphs(cfg.icons.mode);
|
|
76
74
|
const now = Date.now();
|
|
77
75
|
const animating = deps.isAnimating?.() ?? true;
|
|
78
76
|
const spin = spinFrame(g, now, animating);
|
|
79
|
-
const bashBgLines = renderBashBgSummary(bashBgJobs, width, paint, UTILS, {
|
|
80
|
-
glyphs: g,
|
|
81
|
-
spin,
|
|
82
|
-
now,
|
|
83
|
-
});
|
|
84
|
-
if (agents.length === 0) return bashBgLines;
|
|
85
77
|
const running = deps.isRunning();
|
|
86
|
-
// The panel budget covers
|
|
87
|
-
// and the background strip are charged to it before the roster is.
|
|
78
|
+
// The panel budget covers the roster header and its rows.
|
|
88
79
|
const panel = panelRows(terminalRows(tui));
|
|
89
80
|
const rosterRows = panel === undefined
|
|
90
81
|
? undefined
|
|
91
|
-
: Math.max(1, panel - 1
|
|
82
|
+
: Math.max(1, panel - 1);
|
|
92
83
|
const opts = { glyphs: g, spin, now, maxRows: rosterRows };
|
|
93
84
|
const dot = theme.fg(running ? "success" : "muted", running ? g.dotRunning : g.dotIdle);
|
|
94
85
|
const failedCount = agents.filter((a) => a.status === "failed").length;
|
|
@@ -111,7 +102,6 @@ export function makeAgentWidget(deps: AgentWidgetDeps) {
|
|
|
111
102
|
// Was the one line in the package pushed without any width clipping.
|
|
112
103
|
lines.push(fitLineByPriority(headerSegs, width, UTILS, theme.fg("dim", g.separator), g.ellipsis));
|
|
113
104
|
lines.push(...renderAgents(agents, cfg.agentsMode, width, paint, UTILS, { ...opts, withHead: false }));
|
|
114
|
-
lines.push(...bashBgLines);
|
|
115
105
|
return lines;
|
|
116
106
|
},
|
|
117
107
|
invalidate(): void {},
|