@aemonge-dev/pi-stardeck 0.0.0-stage → 0.5.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/LICENSE +21 -0
- package/README.md +239 -2
- package/demo/pi-stardeck.tape +59 -0
- package/docs/architecture.md +103 -0
- package/docs/assets/pi-stardeck.gif +0 -0
- package/docs/development.md +128 -0
- package/licenses/README.md +17 -0
- package/licenses/pi-fancy-footer-LICENSE +21 -0
- package/licenses/pi-vim-LICENSE +21 -0
- package/package.json +81 -4
- package/packages/pi-fancy-footer-full-palette/LICENSE +21 -0
- package/packages/pi-fancy-footer-full-palette/LOCAL-CHANGES.md +41 -0
- package/packages/pi-fancy-footer-full-palette/README.md +468 -0
- package/packages/pi-fancy-footer-full-palette/package.json +52 -0
- package/packages/pi-fancy-footer-full-palette/src/api.ts +204 -0
- package/packages/pi-fancy-footer-full-palette/src/config-editor.ts +634 -0
- package/packages/pi-fancy-footer-full-palette/src/config.ts +852 -0
- package/packages/pi-fancy-footer-full-palette/src/data-widgets.ts +259 -0
- package/packages/pi-fancy-footer-full-palette/src/git.ts +130 -0
- package/packages/pi-fancy-footer-full-palette/src/index.ts +479 -0
- package/packages/pi-fancy-footer-full-palette/src/layout.ts +309 -0
- package/packages/pi-fancy-footer-full-palette/src/provider-status.ts +1466 -0
- package/packages/pi-fancy-footer-full-palette/src/render.ts +1061 -0
- package/packages/pi-fancy-footer-full-palette/src/shared.ts +903 -0
- package/packages/pi-stardeck/README.md +219 -0
- package/packages/pi-stardeck/index.ts +691 -0
- package/packages/pi-stardeck/package.json +32 -0
- package/packages/pi-stardeck/src/activity-age.ts +184 -0
- package/packages/pi-stardeck/src/capabilities.ts +105 -0
- package/packages/pi-stardeck/src/deck.ts +807 -0
- package/packages/pi-stardeck/src/gauge.ts +1323 -0
- package/packages/pi-stardeck/src/git-refresh.ts +5 -0
- package/packages/pi-stardeck/src/process-resources.ts +428 -0
- package/packages/pi-stardeck/src/title-state.ts +195 -0
- package/packages/pi-stardeck/src/title.ts +443 -0
- package/packages/pi-vim-top-border/LICENSE +21 -0
- package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
- package/packages/pi-vim-top-border/README.md +577 -0
- package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
- package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
- package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
- package/packages/pi-vim-top-border/index.ts +4195 -0
- package/packages/pi-vim-top-border/input-keys.ts +49 -0
- package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
- package/packages/pi-vim-top-border/mode-colors.ts +97 -0
- package/packages/pi-vim-top-border/mode-label.ts +62 -0
- package/packages/pi-vim-top-border/motions.ts +297 -0
- package/packages/pi-vim-top-border/package.json +47 -0
- package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
- package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
- package/packages/pi-vim-top-border/prompt-status.ts +131 -0
- package/packages/pi-vim-top-border/settings.ts +287 -0
- package/packages/pi-vim-top-border/text-objects.ts +389 -0
- package/packages/pi-vim-top-border/types.ts +51 -0
- package/packages/pi-vim-top-border/visual.ts +65 -0
- package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
- package/packages/status/README.md +398 -0
- package/packages/status/index.ts +1717 -0
- package/packages/status/package.json +33 -0
- package/packages/status/src/active-work.ts +235 -0
- package/packages/status/src/config.ts +218 -0
- package/packages/status/src/diagnostics.ts +238 -0
- package/packages/status/src/goal-block.ts +377 -0
- package/packages/status/src/goal.ts +170 -0
- package/packages/status/src/ledger.ts +101 -0
- package/packages/status/src/openspec.ts +394 -0
- package/packages/status/src/orchestration.ts +237 -0
- package/packages/status/src/publisher.ts +1294 -0
- package/packages/status/src/reducer.ts +190 -0
- package/packages/status/src/runtime-runs.ts +267 -0
- package/packages/status/src/state.ts +178 -0
- package/packages/status/src/title-activity.ts +224 -0
- package/packages/status/src/title-state.ts +218 -0
- package/packages/status/src/turn-cost.ts +63 -0
- package/packages/status/src/types.ts +228 -0
- package/src/deck-surface.ts +46 -0
- package/src/health.ts +228 -0
- package/src/index.ts +28 -0
- package/src/pi-status.ts +334 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-stardeck",
|
|
3
|
+
"version": "1.0.4",
|
|
4
|
+
"private": true,
|
|
5
|
+
"description": "Lightweight runtime status above the Pi editor",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"pi-package",
|
|
9
|
+
"pi-extension",
|
|
10
|
+
"context",
|
|
11
|
+
"widget"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.19.0"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"format": "prettier --write . --ignore-path .prettierignore",
|
|
18
|
+
"format:check": "prettier --check . --ignore-path .prettierignore",
|
|
19
|
+
"typecheck": "tsc --noEmit",
|
|
20
|
+
"test": "node --test --experimental-strip-types test/*.test.ts",
|
|
21
|
+
"check": "npm run format:check && npm run typecheck && npm test"
|
|
22
|
+
},
|
|
23
|
+
"pi": {
|
|
24
|
+
"extensions": [
|
|
25
|
+
"./index.ts"
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
30
|
+
"@earendil-works/pi-tui": "*"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { visibleWidth } from '@earendil-works/pi-tui';
|
|
2
|
+
|
|
3
|
+
export const WIDE_ACTIVITY_AGE_CELLS = 6;
|
|
4
|
+
export const STANDARD_ACTIVITY_AGE_CELLS = 4;
|
|
5
|
+
export const COMPACT_ACTIVITY_AGE_CELLS = 3;
|
|
6
|
+
export const RAIL_CHRONOMETER_CELLS = 8;
|
|
7
|
+
export const RUNTIME_STATUS_WIDGET_ID = 'galactica.runtime-state';
|
|
8
|
+
export const RUNTIME_SCOPE_SEPARATOR_WIDGET_ID = 'galactica.runtime-scope-separator';
|
|
9
|
+
|
|
10
|
+
export type ActivityAgeSize = 6 | 4 | 3;
|
|
11
|
+
|
|
12
|
+
const ACTIVITY_AGE_SIZES: readonly ActivityAgeSize[] = [
|
|
13
|
+
WIDE_ACTIVITY_AGE_CELLS,
|
|
14
|
+
STANDARD_ACTIVITY_AGE_CELLS,
|
|
15
|
+
COMPACT_ACTIVITY_AGE_CELLS,
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function boundedAge(ageMs: number): number {
|
|
19
|
+
return Number.isFinite(ageMs) ? Math.max(0, Math.floor(ageMs)) : 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function activityHeartbeat(ageMs: number): string {
|
|
23
|
+
return Math.floor(boundedAge(ageMs) / 500) % 2 === 0 ? '·' : '•';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wideActivityAge(ageMs: number): string {
|
|
27
|
+
const age = boundedAge(ageMs);
|
|
28
|
+
if (age < 100) return `${age}ms`;
|
|
29
|
+
if (age < 60_000) {
|
|
30
|
+
return `${Math.min(59.9, Math.floor(age / 100) / 10).toFixed(1)}s`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const seconds = Math.floor(age / 1_000);
|
|
34
|
+
const minutes = Math.floor(seconds / 60);
|
|
35
|
+
if (minutes < 60) {
|
|
36
|
+
return `${minutes}m${String(seconds % 60).padStart(2, '0')}s`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const hours = Math.floor(minutes / 60);
|
|
40
|
+
const heartbeat = activityHeartbeat(age);
|
|
41
|
+
if (hours < 10) {
|
|
42
|
+
return `${hours}h${String(minutes % 60).padStart(2, '0')}m${heartbeat}`;
|
|
43
|
+
}
|
|
44
|
+
if (hours < 100) {
|
|
45
|
+
return `${hours}:${String(minutes % 60).padStart(2, '0')}${heartbeat}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const days = Math.min(99, Math.floor(hours / 24));
|
|
49
|
+
if (days < 10) {
|
|
50
|
+
return `${days}d${String(hours % 24).padStart(2, '0')}h${heartbeat}`;
|
|
51
|
+
}
|
|
52
|
+
return `${days}d${String(hours % 24).padStart(2, '0')}${heartbeat}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function standardActivityAge(ageMs: number): string {
|
|
56
|
+
const age = boundedAge(ageMs);
|
|
57
|
+
if (age < 100) return `${age}ms`;
|
|
58
|
+
if (age < 10_000) return `${Math.min(9.9, age / 1_000).toFixed(1)}s`;
|
|
59
|
+
|
|
60
|
+
const seconds = Math.floor(age / 1_000);
|
|
61
|
+
if (seconds < 100) return `${seconds}s`;
|
|
62
|
+
const minutes = Math.floor(seconds / 60);
|
|
63
|
+
if (minutes < 100) return `${minutes}m`;
|
|
64
|
+
const hours = Math.floor(minutes / 60);
|
|
65
|
+
if (hours < 100) return `${hours}h`;
|
|
66
|
+
return `${Math.min(99, Math.floor(hours / 24))}d`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function compactActivityAge(ageMs: number): string {
|
|
70
|
+
const age = boundedAge(ageMs);
|
|
71
|
+
const seconds = Math.floor(age / 1_000);
|
|
72
|
+
if (seconds < 100) return `${seconds}s`;
|
|
73
|
+
const minutes = Math.floor(seconds / 60);
|
|
74
|
+
if (minutes < 100) return `${minutes}m`;
|
|
75
|
+
const hours = Math.floor(minutes / 60);
|
|
76
|
+
if (hours < 100) return `${hours}h`;
|
|
77
|
+
return `${Math.min(99, Math.floor(hours / 24))}d`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatActivityAge(ageMs: number, size: ActivityAgeSize): string {
|
|
81
|
+
const text =
|
|
82
|
+
size === WIDE_ACTIVITY_AGE_CELLS
|
|
83
|
+
? wideActivityAge(ageMs)
|
|
84
|
+
: size === STANDARD_ACTIVITY_AGE_CELLS
|
|
85
|
+
? standardActivityAge(ageMs)
|
|
86
|
+
: compactActivityAge(ageMs);
|
|
87
|
+
return `${' '.repeat(Math.max(0, size - visibleWidth(text)))}${text}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function passiveActivityStatus(
|
|
91
|
+
lifecycle: string,
|
|
92
|
+
size: ActivityAgeSize,
|
|
93
|
+
): string {
|
|
94
|
+
const normalized = lifecycle.trim().toLowerCase();
|
|
95
|
+
const full =
|
|
96
|
+
normalized === 'listening'
|
|
97
|
+
? 'idle'
|
|
98
|
+
: normalized === 'blocked'
|
|
99
|
+
? 'hold'
|
|
100
|
+
: normalized === 'aborted'
|
|
101
|
+
? 'stop'
|
|
102
|
+
: 'wait';
|
|
103
|
+
if (size !== COMPACT_ACTIVITY_AGE_CELLS) {
|
|
104
|
+
return `${' '.repeat(Math.max(0, size - visibleWidth(full)))}${full}`;
|
|
105
|
+
}
|
|
106
|
+
const compact =
|
|
107
|
+
full === 'idle' ? '—' : full === 'hold' ? '!' : full === 'stop' ? '×' : '…';
|
|
108
|
+
return `${' '.repeat(size - visibleWidth(compact))}${compact}`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatRailChronometer(ageMs: number | null): string {
|
|
112
|
+
if (ageMs === null) return "00:00'00";
|
|
113
|
+
|
|
114
|
+
const centiseconds = Math.floor(boundedAge(ageMs) / 10);
|
|
115
|
+
const minutes = Math.floor(centiseconds / 6_000);
|
|
116
|
+
if (minutes < 100) {
|
|
117
|
+
const seconds = Math.floor(centiseconds / 100) % 60;
|
|
118
|
+
const remainder = centiseconds % 100;
|
|
119
|
+
return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}'${String(remainder).padStart(2, '0')}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const totalSeconds = Math.floor(centiseconds / 100);
|
|
123
|
+
const hours = Math.floor(totalSeconds / 3_600);
|
|
124
|
+
if (hours < 100) {
|
|
125
|
+
const remainderMinutes = Math.floor(totalSeconds / 60) % 60;
|
|
126
|
+
const seconds = totalSeconds % 60;
|
|
127
|
+
return `${String(hours).padStart(2, '0')}h${String(remainderMinutes).padStart(2, '0')}'${String(seconds).padStart(2, '0')}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const days = Math.min(99, Math.floor(hours / 24));
|
|
131
|
+
const remainderHours = hours % 24;
|
|
132
|
+
const remainderMinutes = Math.floor(totalSeconds / 60) % 60;
|
|
133
|
+
return `${String(days).padStart(2, '0')}d${String(remainderHours).padStart(2, '0')}'${String(remainderMinutes).padStart(2, '0')}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function activityAgeCandidates(
|
|
137
|
+
ageMs: number | null,
|
|
138
|
+
lifecycle = 'listening',
|
|
139
|
+
): ReadonlyArray<{ size: ActivityAgeSize; age: string }> {
|
|
140
|
+
return ACTIVITY_AGE_SIZES.map((size) => ({
|
|
141
|
+
size,
|
|
142
|
+
age:
|
|
143
|
+
ageMs === null
|
|
144
|
+
? passiveActivityStatus(lifecycle, size)
|
|
145
|
+
: formatActivityAge(ageMs, size),
|
|
146
|
+
}));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildRuntimeFooterWidgets(status: string, active = false) {
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
protocol: 1,
|
|
153
|
+
type: 'upsert',
|
|
154
|
+
widget: {
|
|
155
|
+
id: RUNTIME_STATUS_WIDGET_ID,
|
|
156
|
+
label: 'Runtime state',
|
|
157
|
+
description: 'Time since foreground activity or the settled runtime state',
|
|
158
|
+
content: { type: 'text', text: status },
|
|
159
|
+
icon: false,
|
|
160
|
+
style: { textColor: active ? 'accent' : 'dim' },
|
|
161
|
+
layout: {
|
|
162
|
+
row: 1,
|
|
163
|
+
position: 4,
|
|
164
|
+
align: 'left',
|
|
165
|
+
fill: 'none',
|
|
166
|
+
minWidth: RAIL_CHRONOMETER_CELLS,
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
protocol: 1,
|
|
172
|
+
type: 'upsert',
|
|
173
|
+
widget: {
|
|
174
|
+
id: RUNTIME_SCOPE_SEPARATOR_WIDGET_ID,
|
|
175
|
+
label: 'Runtime scope separator',
|
|
176
|
+
description: 'Separates the active LLM from runtime state',
|
|
177
|
+
content: { type: 'text', text: '⟩' },
|
|
178
|
+
icon: false,
|
|
179
|
+
style: { textColor: 'thinkingHigh' },
|
|
180
|
+
layout: { row: 1, position: 3, align: 'left', fill: 'none' },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
] as const;
|
|
184
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
export const MCP_STATUS_EVENT = 'pi-mcp-adapter/status/v1';
|
|
2
|
+
export const LSP_STATUS_EVENT = 'pi-lsp-adapter/status/v1';
|
|
3
|
+
export const CAPABILITY_WIDGET_ID = 'galactica.project-capabilities';
|
|
4
|
+
|
|
5
|
+
export type CapabilityCount = {
|
|
6
|
+
healthy: number;
|
|
7
|
+
total: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type McpCapability = CapabilityCount;
|
|
11
|
+
export type LspCapability = CapabilityCount;
|
|
12
|
+
|
|
13
|
+
const MCP_SERVER_STATUSES = new Set([
|
|
14
|
+
'connected',
|
|
15
|
+
'cached',
|
|
16
|
+
'failed',
|
|
17
|
+
'needs-auth',
|
|
18
|
+
'not-connected',
|
|
19
|
+
'disabled',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
function boundedCount(value: unknown): number | undefined {
|
|
23
|
+
if (!Number.isInteger(value) || Number(value) < 0 || Number(value) > 999) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
return Number(value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseLspCapability(value: unknown): LspCapability | null {
|
|
30
|
+
if (!value || typeof value !== 'object') return null;
|
|
31
|
+
const snapshot = value as Record<string, unknown>;
|
|
32
|
+
const healthy = boundedCount(snapshot.healthy);
|
|
33
|
+
const total = boundedCount(snapshot.total);
|
|
34
|
+
if (snapshot.version !== 1 || healthy === undefined || total === undefined) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (healthy > total) return null;
|
|
38
|
+
return { healthy, total };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseMcpCapability(value: unknown): McpCapability | null {
|
|
42
|
+
if (!value || typeof value !== 'object') return null;
|
|
43
|
+
const snapshot = value as Record<string, unknown>;
|
|
44
|
+
if (
|
|
45
|
+
snapshot.version !== 1 ||
|
|
46
|
+
!Array.isArray(snapshot.servers) ||
|
|
47
|
+
snapshot.servers.length > 999
|
|
48
|
+
) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let healthy = 0;
|
|
53
|
+
let total = 0;
|
|
54
|
+
for (const rawServer of snapshot.servers) {
|
|
55
|
+
if (!rawServer || typeof rawServer !== 'object') return null;
|
|
56
|
+
const server = rawServer as Record<string, unknown>;
|
|
57
|
+
if (
|
|
58
|
+
typeof server.status !== 'string' ||
|
|
59
|
+
!MCP_SERVER_STATUSES.has(server.status) ||
|
|
60
|
+
typeof server.disabled !== 'boolean' ||
|
|
61
|
+
boundedCount(server.toolCount) === undefined
|
|
62
|
+
) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
if (server.disabled || server.status === 'disabled') continue;
|
|
66
|
+
total += 1;
|
|
67
|
+
if (server.status !== 'failed' && server.status !== 'needs-auth') healthy += 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return total > 0 ? { healthy, total } : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function capabilityText(capability: CapabilityCount | null): string {
|
|
74
|
+
// Gray-out amendment: absent capability reports an empty slot, not a dash.
|
|
75
|
+
return capability ? `${capability.healthy}/${capability.total}` : '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function buildCapabilityFooterWidget(mcp: McpCapability | null) {
|
|
79
|
+
const text = capabilityText(mcp);
|
|
80
|
+
const color = mcp ? (mcp.healthy < mcp.total ? 'warning' : 'accent') : 'dim';
|
|
81
|
+
return {
|
|
82
|
+
protocol: 1,
|
|
83
|
+
type: 'upsert',
|
|
84
|
+
widget: {
|
|
85
|
+
id: CAPABILITY_WIDGET_ID,
|
|
86
|
+
label: 'MCP availability',
|
|
87
|
+
description: 'Available/total MCP servers from the live adapter status feed',
|
|
88
|
+
content: { type: 'text', text },
|
|
89
|
+
icon: {
|
|
90
|
+
glyphs: { nerd: '', emoji: '🔌', unicode: '◇', ascii: 'MCP' },
|
|
91
|
+
color,
|
|
92
|
+
},
|
|
93
|
+
style: {
|
|
94
|
+
textColor: color,
|
|
95
|
+
},
|
|
96
|
+
layout: {
|
|
97
|
+
row: 0,
|
|
98
|
+
position: 7,
|
|
99
|
+
align: 'left',
|
|
100
|
+
fill: 'grow',
|
|
101
|
+
minWidth: 0,
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
} as const;
|
|
105
|
+
}
|