@aemonge-dev/pi-stardeck 0.0.0-stage → 0.6.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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +240 -2
  3. package/demo/pi-stardeck.tape +59 -0
  4. package/docs/architecture.md +106 -0
  5. package/docs/assets/pi-stardeck.gif +0 -0
  6. package/docs/development.md +128 -0
  7. package/licenses/README.md +19 -0
  8. package/licenses/pi-vim-LICENSE +21 -0
  9. package/package.json +81 -4
  10. package/packages/footer/README.md +15 -0
  11. package/packages/footer/index.ts +37 -0
  12. package/packages/footer/package.json +32 -0
  13. package/packages/footer/src/protocol.ts +10 -0
  14. package/packages/footer/src/quota.ts +26 -0
  15. package/packages/footer/src/telemetry.ts +59 -0
  16. package/packages/pi-stardeck/README.md +220 -0
  17. package/packages/pi-stardeck/index.ts +691 -0
  18. package/packages/pi-stardeck/package.json +32 -0
  19. package/packages/pi-stardeck/src/activity-age.ts +184 -0
  20. package/packages/pi-stardeck/src/capabilities.ts +105 -0
  21. package/packages/pi-stardeck/src/deck.ts +807 -0
  22. package/packages/pi-stardeck/src/gauge.ts +1323 -0
  23. package/packages/pi-stardeck/src/git-refresh.ts +5 -0
  24. package/packages/pi-stardeck/src/process-resources.ts +428 -0
  25. package/packages/pi-stardeck/src/title-state.ts +195 -0
  26. package/packages/pi-stardeck/src/title.ts +443 -0
  27. package/packages/pi-vim-top-border/LICENSE +21 -0
  28. package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
  29. package/packages/pi-vim-top-border/README.md +577 -0
  30. package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
  31. package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
  32. package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
  33. package/packages/pi-vim-top-border/index.ts +4195 -0
  34. package/packages/pi-vim-top-border/input-keys.ts +49 -0
  35. package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
  36. package/packages/pi-vim-top-border/mode-colors.ts +97 -0
  37. package/packages/pi-vim-top-border/mode-label.ts +62 -0
  38. package/packages/pi-vim-top-border/motions.ts +297 -0
  39. package/packages/pi-vim-top-border/package.json +47 -0
  40. package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
  41. package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
  42. package/packages/pi-vim-top-border/prompt-status.ts +131 -0
  43. package/packages/pi-vim-top-border/settings.ts +287 -0
  44. package/packages/pi-vim-top-border/text-objects.ts +389 -0
  45. package/packages/pi-vim-top-border/types.ts +51 -0
  46. package/packages/pi-vim-top-border/visual.ts +65 -0
  47. package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
  48. package/packages/status/README.md +398 -0
  49. package/packages/status/index.ts +1717 -0
  50. package/packages/status/package.json +33 -0
  51. package/packages/status/src/active-work.ts +235 -0
  52. package/packages/status/src/config.ts +218 -0
  53. package/packages/status/src/diagnostics.ts +238 -0
  54. package/packages/status/src/goal-block.ts +377 -0
  55. package/packages/status/src/goal.ts +170 -0
  56. package/packages/status/src/ledger.ts +101 -0
  57. package/packages/status/src/openspec.ts +394 -0
  58. package/packages/status/src/orchestration.ts +237 -0
  59. package/packages/status/src/publisher.ts +1294 -0
  60. package/packages/status/src/reducer.ts +190 -0
  61. package/packages/status/src/runtime-runs.ts +267 -0
  62. package/packages/status/src/state.ts +178 -0
  63. package/packages/status/src/title-activity.ts +224 -0
  64. package/packages/status/src/title-state.ts +218 -0
  65. package/packages/status/src/turn-cost.ts +63 -0
  66. package/packages/status/src/types.ts +228 -0
  67. package/src/deck-surface.ts +46 -0
  68. package/src/health.ts +227 -0
  69. package/src/index.ts +27 -0
  70. package/src/pi-status.ts +334 -0
package/package.json CHANGED
@@ -1,6 +1,83 @@
1
1
  {
2
2
  "name": "@aemonge-dev/pi-stardeck",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.6.0",
4
+ "description": "A Vim- and tmux-inspired command deck for Pi",
5
+ "type": "module",
6
+ "keywords": [
7
+ "pi-package",
8
+ "pi-extension",
9
+ "vim",
10
+ "tmux",
11
+ "cockpit"
12
+ ],
13
+ "license": "MIT",
14
+ "files": [
15
+ "README.md",
16
+ "docs/architecture.md",
17
+ "docs/development.md",
18
+ "docs/assets/pi-stardeck.gif",
19
+ "demo/pi-stardeck.tape",
20
+ "licenses",
21
+ "src/*.ts",
22
+ "!src/local-load-probe.ts",
23
+ "packages/*/*.ts",
24
+ "packages/*/src/**/*.ts",
25
+ "!packages/*/src/**/*.test.ts",
26
+ "packages/*/package.json",
27
+ "packages/*/README.md",
28
+ "packages/*/LICENSE",
29
+ "packages/*/LOCAL-CHANGES.md"
30
+ ],
31
+ "engines": {
32
+ "node": ">=24.0.0"
33
+ },
34
+ "workspaces": [
35
+ "packages/*"
36
+ ],
37
+ "scripts": {
38
+ "format": "prettier --write . --ignore-path .prettierignore",
39
+ "format:check": "prettier --check . --ignore-path .prettierignore",
40
+ "lint": "biome check --config-path=biome.json packages/pi-vim-top-border && eslint packages scripts src test",
41
+ "typecheck": "tsc --noEmit && npm run typecheck --workspaces --if-present",
42
+ "test": "npm run test:tooling && npm run test:components",
43
+ "check": "npm run check:baseline && npm run check:package:load && npm run format:check && npm run lint && npm run typecheck && npm test && npm run check:composition && npm run check:openspec",
44
+ "check:baseline": "node scripts/check-baseline.mjs",
45
+ "check:package": "node scripts/check-package.mjs",
46
+ "check:package:load": "node scripts/check-package.mjs --load",
47
+ "check:composition": "node --import tsx/esm src/local-load-probe.ts",
48
+ "check:openspec": "node scripts/check-openspec.mjs",
49
+ "test:tooling": "node --import tsx/esm --test test/*.test.ts",
50
+ "test:status": "npm --prefix packages/status test",
51
+ "test:stardeck": "npm --prefix packages/pi-stardeck test",
52
+ "test:footer": "npm --prefix packages/footer test",
53
+ "test:vim": "npm --prefix packages/pi-vim-top-border test",
54
+ "test:components": "npm run test:status && npm run test:stardeck && npm run test:footer && npm run test:vim"
55
+ },
56
+ "peerDependencies": {
57
+ "@earendil-works/pi-ai": "*",
58
+ "@earendil-works/pi-coding-agent": "*",
59
+ "@earendil-works/pi-tui": "*",
60
+ "typebox": "*"
61
+ },
62
+ "devDependencies": {
63
+ "@biomejs/biome": "2.4.8",
64
+ "@earendil-works/pi-ai": "0.84.4",
65
+ "@earendil-works/pi-coding-agent": "0.84.4",
66
+ "@earendil-works/pi-tui": "0.84.4",
67
+ "@eslint/js": "9.39.5",
68
+ "@types/node": "24.13.3",
69
+ "eslint": "9.39.5",
70
+ "lefthook": "2.1.12",
71
+ "prettier": "3.9.6",
72
+ "tsx": "4.23.13",
73
+ "typebox": "1.3.25",
74
+ "typescript": "5.9.3",
75
+ "typescript-eslint": "8.69.0"
76
+ },
77
+ "pi": {
78
+ "image": "docs/assets/pi-stardeck.gif",
79
+ "extensions": [
80
+ "./src/index.ts"
81
+ ]
82
+ }
83
+ }
@@ -0,0 +1,15 @@
1
+ # footer
2
+
3
+ Footer telemetry producer for the [pi-stardeck](../../README.md) deck.
4
+
5
+ The deck renders the telemetry row; this module only feeds it. It walks the
6
+ session branch for assistant usage cost and publishes snapshots on
7
+ `pi-stardeck:footer-telemetry/v1` after the events that can change them
8
+ (session start, message end, model selection, compaction).
9
+
10
+ It renders nothing, reads no files, sets no timers, and talks to no network.
11
+ Quota gauges are a seam (`src/quota.ts`) with no implementations: register a
12
+ provider there and the deck row gains its gauge back without renderer
13
+ changes.
14
+
15
+ Composed by the root package; do not install separately.
@@ -0,0 +1,37 @@
1
+ import type { ExtensionAPI, ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+
3
+ import { activeQuotaProviders } from './src/quota.ts';
4
+ import { FOOTER_TELEMETRY_CHANNEL } from './src/protocol.ts';
5
+ import { buildFooterTelemetryMessage, collectTotalCost } from './src/telemetry.ts';
6
+
7
+ /**
8
+ * Footer telemetry producer for the pi-stardeck deck. Renders nothing,
9
+ * configures nothing, polls nothing: it walks the session branch for usage
10
+ * cost and publishes deck-consumable snapshots on the footer-telemetry
11
+ * channel after the events that can change them.
12
+ */
13
+ export default function footerTelemetry(pi: ExtensionAPI): void {
14
+ let context: ExtensionContext | undefined;
15
+
16
+ const publish = () => {
17
+ if (!context) return;
18
+ const quota = activeQuotaProviders()
19
+ .map((provider) => provider.snapshot())
20
+ .find((snapshot) => snapshot !== undefined);
21
+ pi.events.emit(
22
+ FOOTER_TELEMETRY_CHANNEL,
23
+ buildFooterTelemetryMessage(collectTotalCost(context), quota),
24
+ );
25
+ };
26
+
27
+ pi.on('session_start', (_event, ctx) => {
28
+ context = ctx;
29
+ publish();
30
+ });
31
+ pi.on('message_end', publish);
32
+ pi.on('model_select', publish);
33
+ pi.on('session_compact', publish);
34
+ pi.on('session_shutdown', () => {
35
+ context = undefined;
36
+ });
37
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "footer",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Footer telemetry producer for the pi-stardeck deck",
6
+ "type": "module",
7
+ "keywords": [
8
+ "pi-package",
9
+ "pi-extension",
10
+ "telemetry"
11
+ ],
12
+ "engines": {
13
+ "node": ">=24.0.0"
14
+ },
15
+ "scripts": {
16
+ "format": "prettier --write \"**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}\" --ignore-path .prettierignore",
17
+ "format:check": "prettier --check \"**/*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}\" --ignore-path .prettierignore",
18
+ "typecheck": "tsc --noEmit",
19
+ "test": "node --test --experimental-strip-types test/*.test.ts",
20
+ "check": "npm run format:check && npm run typecheck && npm test"
21
+ },
22
+ "pi": {
23
+ "extensions": [
24
+ "./index.ts"
25
+ ]
26
+ },
27
+ "peerDependencies": {
28
+ "@earendil-works/pi-ai": "*",
29
+ "@earendil-works/pi-coding-agent": "*",
30
+ "typebox": "*"
31
+ }
32
+ }
@@ -0,0 +1,10 @@
1
+ export const FOOTER_TELEMETRY_PROTOCOL_VERSION = 1 as const;
2
+
3
+ export const FOOTER_TELEMETRY_CHANNEL = 'pi-stardeck:footer-telemetry/v1';
4
+
5
+ export interface FooterTelemetryMessage {
6
+ protocol: typeof FOOTER_TELEMETRY_PROTOCOL_VERSION;
7
+ type: 'snapshot';
8
+ totalCost: number;
9
+ quotaPercent?: number;
10
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Quota seam. The provider ships with no implementations: every quota source
3
+ * this cockpit had (Codex OAuth, Claude OAuth, z.ai coding plan) is gone with
4
+ * the previous footer module. A future slice registers a provider here
5
+ * and the deck row gains its gauge back without any renderer change.
6
+ */
7
+ export interface QuotaSnapshot {
8
+ /** Whole-session quota usage, 0–100. */
9
+ percent: number;
10
+ }
11
+
12
+ export interface QuotaProvider {
13
+ readonly id: string;
14
+ snapshot(): QuotaSnapshot | undefined;
15
+ }
16
+
17
+ const quotaProviders: QuotaProvider[] = [];
18
+
19
+ export function registerQuotaProvider(provider: QuotaProvider): void {
20
+ if (quotaProviders.some(({ id }) => id === provider.id)) return;
21
+ quotaProviders.push(provider);
22
+ }
23
+
24
+ export function activeQuotaProviders(): readonly QuotaProvider[] {
25
+ return quotaProviders;
26
+ }
@@ -0,0 +1,59 @@
1
+ import type { ExtensionContext } from '@earendil-works/pi-coding-agent';
2
+
3
+ import type { QuotaSnapshot } from './quota.ts';
4
+ import {
5
+ FOOTER_TELEMETRY_PROTOCOL_VERSION,
6
+ type FooterTelemetryMessage,
7
+ } from './protocol.ts';
8
+
9
+ function toCost(value: unknown): number {
10
+ return typeof value === 'number' && Number.isFinite(value) ? Math.max(0, value) : 0;
11
+ }
12
+
13
+ /**
14
+ * Sums assistant usage cost over a session branch. Non-message entries,
15
+ * non-assistant messages, and malformed usage contribute nothing, matching
16
+ * the walk the deck row has consumed since 0.1.0.
17
+ */
18
+ export function totalCostFromEntries(entries: readonly unknown[]): number {
19
+ let totalCost = 0;
20
+ for (const entry of entries) {
21
+ if (typeof entry !== 'object' || entry === null) continue;
22
+ const { type, message } = entry as { type?: unknown; message?: unknown };
23
+ if (type !== 'message') continue;
24
+ if (typeof message !== 'object' || message === null) continue;
25
+ const { role, usage } = message as { role?: unknown; usage?: unknown };
26
+ if (role !== 'assistant') continue;
27
+ if (typeof usage !== 'object' || usage === null) continue;
28
+ const cost = (usage as { cost?: unknown }).cost;
29
+ if (typeof cost !== 'object' || cost === null) continue;
30
+ totalCost += toCost((cost as { total?: unknown }).total);
31
+ }
32
+ return totalCost;
33
+ }
34
+
35
+ export function collectTotalCost(ctx: ExtensionContext): number {
36
+ return totalCostFromEntries(ctx.sessionManager.getBranch());
37
+ }
38
+
39
+ export function buildFooterTelemetryMessage(
40
+ totalCost: number,
41
+ quota?: QuotaSnapshot,
42
+ ): FooterTelemetryMessage {
43
+ const cost =
44
+ typeof totalCost === 'number' && Number.isFinite(totalCost)
45
+ ? Math.max(0, totalCost)
46
+ : 0;
47
+ const percent =
48
+ quota !== undefined &&
49
+ typeof quota.percent === 'number' &&
50
+ Number.isFinite(quota.percent)
51
+ ? Math.min(100, Math.max(0, quota.percent))
52
+ : undefined;
53
+ return {
54
+ protocol: FOOTER_TELEMETRY_PROTOCOL_VERSION,
55
+ type: 'snapshot',
56
+ totalCost: cost,
57
+ ...(percent !== undefined ? { quotaPercent: percent } : {}),
58
+ };
59
+ }
@@ -0,0 +1,220 @@
1
+ # pi-stardeck
2
+
3
+ A dependency-free Pi extension that coordinates the prompt information architecture
4
+ through Pi's supported widget and event APIs.
5
+
6
+ ```text
7
+ ─ 󰠭 › ─────────────────────────────────────────────────────────────────────
8
+ ( 3 ›  2 · 00:05'12) working ⟩ checking Stabilize runtime and focus · step 3/5  task 1/2 ›  stps 3/5
9
+ 󰓾 Stabilize runtime and focus in the cockpit
10
+ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
11
+ [󰆧]  ~/projects/example  main ⟩  clean
12
+ ┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈┈
13
+ 󰚩 Opus › high ⟩ 󰾆 42% › 󰎞 2  10% › 󰜦 $0.10 ⟩  12% ›  375M ›  3/3
14
+ ─ 󰏫 ─────────────────────────────────────────────────────────────── ‹ 󰠭 ─
15
+ ```
16
+
17
+ The preferred editor-deck surface keeps two stable semantic rows. The first is a
18
+ left-packed operational strip: runtime capsule, lifecycle, activity, controlled next
19
+ action, and Task/Step progress. The second is a full-width explicit selection canvas
20
+ that wraps to at most two lines. Every slot remains present; zero, idle, unavailable,
21
+ and unselected states are dim instead of appearing or disappearing. Long content may
22
+ wrap responsively, but runtime state never changes the deck topology. Decorative rails
23
+ remain slim and contain no telemetry.
24
+
25
+ The editor deck uses blue for the active runtime capsule, the bold lifecycle anchor,
26
+ and regular-weight explicit selection. Ordinary active activity remains neutral, except
27
+ that `recovering` and its ordinary suggestion are blue together while only the lifecycle
28
+ is bold. Other ordinary suggestions remain green. Incomplete Task/Step progress is
29
+ neutral until each counter completes and stays right-aligned without a major boundary.
30
+ The lifecycle/activity boundary and rails remain purple; minor separators are dim.
31
+ Human-input and validation cues stay yellow, blockers stay red, and every inactive value
32
+ stays dim.
33
+
34
+ The legacy above-editor widget owns only current work identity and preserves the
35
+ ` ready ⟩ no focused task` idle row when no work is selected. The leading icon is
36
+ runtime-authoritative: `` while Pi is busy in an active lifecycle and `` while
37
+ settled, waiting, blocked, listening, or aborted. Internal lifecycle states render as
38
+ short readable verbs. The Ramona loop remains exact: `understand`, `work`, `assure`,
39
+ `learn`, and `answer`. Runtime boundaries are `wait`, `blocked`, `ready`, or `stopped`.
40
+ The title has four styled groups: lifecycle, a structured active ancestry, focus, and
41
+ optional focus detail. Each activity-path segment receives the blue minor `›`; the
42
+ violet-pink major `⟩` divides runtime ancestry from the green narrative focus. Human
43
+ prompt text is never converted into focus automatically. OpenSpec may add its exact Task
44
+ after another green `›`. Focus selection prefers an exact OpenSpec Task, then Goal, then
45
+ Session Work. Active turns without a selection use the explicit `OpenSpec › no focus`,
46
+ `no OpenSpec`, or `no focus` state supplied by the status producer; `Direct response` is
47
+ never presented as a focus. Empty states are dim while real focus remains bold semantic
48
+ green.
49
+
50
+ Wide rows render lifecycle → active ancestry → optional focus → optional focus detail,
51
+ for example `assure › checking quality › assuring tests ⟩ Implement global flow`. The
52
+ renderer generates every separator. Empty or rejected paths leave no separator or
53
+ placeholder whitespace. As width falls it tries full labels, compact labels, a collapsed
54
+ `… › deepest` ancestry, and finally compact lifecycle plus the deepest active segment.
55
+ Narrative focus disappears before meaningful active ancestry. Terminal display-cell
56
+ measurement and final truncation remain Unicode- and Nerd-Font-aware.
57
+
58
+ Runtime anchor and lifecycle are bold blue `#076678`; activity ancestry and its minor
59
+ `›` are regular blue; the major `⟩` is purple; focus is bold semantic success green; and
60
+ optional focus hierarchy is regular success green. Dim is reserved for the two empty
61
+ focus fallbacks; muted, text, cyan, yellow, and gray are never used in the title.
62
+
63
+ The extension hides Pi's separate built-in `Working...` loader row with
64
+ `ctx.ui.setWorkingVisible(false)`, including its vertical space. The header therefore
65
+ uses its complete width for lifecycle, bounded activity, and focus. Runtime age is
66
+ published for the bottom prompt rail as an eight-cell chronometer measuring time since
67
+ the most recent observed foreground provider, assistant, or tool event. It stays
68
+ fixed-width as it advances: `00:00'00`, `00:00'45`, `00:12'30`, or `12:34'56`; hour/day
69
+ fallbacks also remain eight cells. Active timing uses the high-contrast deep-cyan
70
+ `accent` color; settled states display dim `00:00'00` rather than repeating the header's
71
+ `wait`, `ready`, or other lifecycle word. A session-scoped ticker publishes only when
72
+ the formatted chronometer changes and is cleared on shutdown. No message content,
73
+ thinking, reasoning, prompts, tool arguments, or child-agent output enters the widget.
74
+
75
+ The additive header protocol projects selection independently from live work:
76
+
77
+ ```ts
78
+ selection: {
79
+ source: 'openspec' | 'goal' | 'session-work';
80
+ titles: string[]; // one or two bounded, normalized titles
81
+ color: HeaderColor;
82
+ } | null;
83
+ suggestion: HeaderSuggestion | null; // controlled enum, never generated prose
84
+ ```
85
+
86
+ Selection priority remains exact OpenSpec Task, Goal, then Session Work. Older
87
+ protocol-1 events may use sanitized non-placeholder work titles as a legacy selection;
88
+ explicit malformed selection is rejected rather than borrowing a fallback. The
89
+ suggestion vocabulary is producer-owned, appears only in waiting, blocked, approval,
90
+ or aborted states, and is bounded to reporting cues such as
91
+ `requesting validation or redirection`, `awaiting continuation`, and
92
+ `requesting redirection`. While work is active, the same slot reports the current
93
+ WHAT: the focused task title with live step progress.
94
+
95
+ The preferred activity contract is a structured path that never stores separators:
96
+
97
+ ```ts
98
+ work.activityPath?: Array<{
99
+ id: string; // lowercase letters, digits, and hyphens; at most 48 code points
100
+ label: string; // one line; normalized to at most 48 code points
101
+ compact?: string; // normalized to at most 24 code points
102
+ current?: number;
103
+ total?: number;
104
+ }>; // at most six unique segments; lifecycle is not duplicated here
105
+ ```
106
+
107
+ An explicit valid array, including `[]`, takes precedence. A malformed array is rejected
108
+ as a whole and falls back to the legacy allowlisted `work.activity` field. Legacy
109
+ activity is normalized to one internal segment, preserving existing producers until they
110
+ migrate. Progress is bounded to `0..9,999` and appended as `current/total` only when
111
+ both values are valid.
112
+
113
+ The consumer removes ANSI escapes and control characters, normalizes whitespace, rejects
114
+ empty labels and malformed or duplicate IDs, and fails soft. Semantic safety remains a
115
+ producer obligation: labels must be bounded display vocabulary, never prompts, commands,
116
+ paths, secrets, generated paragraphs, or tool output. The consumer cannot infer those
117
+ semantics from arbitrary text. The future global-flow producer selects one primary
118
+ active path; completed, skipped, sibling, and parallel aggregation remain out of scope.
119
+
120
+ The legacy `work.activity` enum remains temporarily supported. Its renderer-owned labels
121
+ convert operation starts such as `inspection` and `mutation` into
122
+ `inspecting project context` and `updating selected files`. Their bounded completion
123
+ transitions render as `finalizing current request`, `reviewing applied changes`, or
124
+ `reviewing check results`; unknown kinds and raw activity text remain omitted.
125
+
126
+ The extension refreshes bounded local Git branch and status data at session start, after
127
+ successful mutating tools, and when the agent settles. Footer row one keeps project
128
+ identity on the left and work scope on the right. The bold PWD is shown in full relative
129
+ to home (`/home/aemonge/projects/example` → `~/projects/example`) and yields ANSI-safely
130
+ only when the terminal cannot preserve both aligned groups. Git branch and state follow
131
+ a blue minor `›`. Outside a repository, one dim responsive `›  (no Git)` state fills
132
+ the otherwise missing project slot and yields before core project/work information on
133
+ narrow terminals. Slash-prefixed Git branches collapse the prefix to its initial and cap
134
+ display at 30 cells with `...` (`feat/persist-versioned-prompts-in-duckdb` →
135
+ `f/persist-versioned-prompts...`). The right side shows compact OpenSpec Plan/Task
136
+ progress, or the simple Goal, Focus, or `no OpenSpec` fallback when no OpenSpec
137
+ portfolio is available. The footer reserves a four-cell visual split between alignment
138
+ groups. OpenSpec uses icons instead of repeated labels: task-list `` for Plans and
139
+ notebook `` for Tasks, with minor `›` between them. Their Nerd Font glyphs exactly
140
+ match Starship: ``, ``, ``, ``, and ``. When `DEVBOX_PROJECT_ROOT` is present, the
141
+ PWD begins `[󰆧]  ~/project`; the boxed Devbox marker precedes the folder icon without
142
+ exposing the variable's value. The top prompt rail may prepend MCP availability to work
143
+ progress as ` healthy/total ⟩  Plans ›  Tasks`. MCP status comes only from the live
144
+ `pi-mcp-adapter/status/v1` feed; lazy cached or not-yet-connected servers remain
145
+ available, while failed or authentication-blocked servers reduce the numerator. This is
146
+ availability telemetry, not a tool-call counter. Before the first valid snapshot, or
147
+ when status is empty or malformed, the segment remains visible as dim ` 0/0`. The
148
+ entire segment yields before right-aligned work progress is truncated. Neovim LSP
149
+ inventory is intentionally neither collected nor shown because it does not establish an
150
+ agent-usable LSP capability. Plugin and extension inventory is not shown without a
151
+ trustworthy runtime health source. Adjacent left and right segments use the same Gruvbox
152
+ colors as the configured Starship prompts: blue `#076678`, green `#79740e`, bright green
153
+ `#689d6a`, aqua `#427b58`, yellow `#b57614`, orange `#af3a03`, major-separator Gruvbox
154
+ purple `#b16286`, blue-violet `#5c5ca8`, and red `#9d0006`. Project, Git, work-scope,
155
+ model, quota, and local telemetry avoid generic gray; only missing Git and the runtime
156
+ chronometer are deliberately dim. PWD and model identity share the same blue anchor
157
+ color; reasoning retains its own level color. The single footer row keeps PWD and Git on
158
+ the left and model, reasoning, and quota on the right. Other bounded widget snapshots
159
+ feed the prompt rails: mode alone on the left and optional MCP availability plus work
160
+ progress on the right of the top rail; chronometer left and context, compactions,
161
+ memory, CPU, and cost right on the bottom rail. A major separator distinguishes model
162
+ context from process resources. On wide terminals the footer ends with the dedicated
163
+ five-cell avatar tail `⟨ 󰠭`: a purple left-pointing boundary, violet ladybug, and one
164
+ blank cell to her right. Each rail aligns its own groups independently, so the header is
165
+ never padded for cross-row alignment. CPU is normalized against Pi's available logical
166
+ CPU capacity and capped at 100%, so the number reads as whole-machine share rather than
167
+ one-core units. CPU and RAM are blue normally and independently turn yellow only for
168
+ sustained CPU pressure above 80% or unusually high session memory; one resource warning
169
+ does not recolor its neighbors. Pink and red are never used in these three telemetry
170
+ groups. In the title, lifecycle and bounded activity are blue; the major boundary is
171
+ purple; real focus and optional Task hierarchy are green; empty focus fallbacks and the
172
+ footer runtime age/state are dim. Persistent cache-hit presentation is disabled and no
173
+ module keeps cache accounting since the footer fork's replacement. Compaction count is hidden at zero.
174
+ The first deck row always begins with the compact runtime capsule
175
+ `( children ›  subagents · 00:00'00)`. Each zero count and the settled timer remain
176
+ dim in place; a live count or timer changes color without moving neighboring content.
177
+ The active-run aggregate no longer appears in the lower resource row. Final
178
+ narrow-width fitting remains ANSI-aware and removes exposed trailing separators. The
179
+ renderer never shows the main Pi process, historical worker totals, completed-file
180
+ progress, queued subagent placeholders, or runtime identity and output fields.
181
+ Quota is reduced to the most constrained provider-reported percentage with no weekly
182
+ analysis. Cost is Pi's native estimated session cost in USD, rendered without trailing
183
+ fractional zeros; its preceding `⟩` retains the shared purple major-boundary color while
184
+ the value remains blue. No local daily aggregation or currency conversion is performed.
185
+ No center group or gauges are used. The deck's widget protocol (historically the
186
+ Fancy Footer protocol, `pi-fancy-footer:widget`) accepts these native theme tokens. The narrative lifecycle header begins with
187
+ its active/passive terminal icon; the prompt border keeps only its right-aligned vi-mode
188
+ glyph. Concurrent Git refreshes coalesce without polling.
189
+
190
+ Agent-environment, context, CPU, RAM, and compaction widgets use the same widget
191
+ protocol, including producer-requested bold text for the PWD. Every three seconds on
192
+ Linux, the extension discovers only Pi descendants through bounded
193
+ `/proc/<pid>/task/<pid>/children` reads, then reads their `stat` and `status` records
194
+ without shelling out or retaining process names, arguments, or paths. Memory sums
195
+ private anonymous resident pages (`RssAnon`) instead of counting shared file-backed
196
+ pages once per process. Individual status-read failures fall back to that process's RSS;
197
+ unavailable `/proc` falls back honestly to current-process CPU and RSS. Sampling,
198
+ smoothing, publication, timers, and resource widgets are session-scoped and cleaned up
199
+ on shutdown. Session-cost collection lives in `packages/footer`; cache and quota
200
+ collection were removed with the fork, leaving quota as an empty seam.
201
+ When the footer reports quota windows, the compact telemetry row paints icon-native
202
+ tiles in cost, long-window, coding-window order — `󰜦 $5.21 ⟩ 󰃰 12% ⟩ 󰈸 62% › 󰅐 1:45`.
203
+ The flame is the coding window's permanent icon and always carries the clock
204
+ countdown; 80% used turns a tile's accent color into a warning. Without windows the
205
+ slot degrades to the single footer-reported percent, and without telemetry it stays a
206
+ dim dash.
207
+ Unavailable temperature and unsupported environment claims are hidden; environment
208
+ classification defaults honestly to `host` unless an explicit supported marker exists.
209
+
210
+ ## Test
211
+
212
+ ```sh
213
+ npm install
214
+ npm run check
215
+ ```
216
+
217
+ ## Remove
218
+
219
+ Delete `./packages/pi-stardeck` from `.pi/agent/settings.json`, remove this
220
+ directory, and run `/reload`.