@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.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +239 -2
  3. package/demo/pi-stardeck.tape +59 -0
  4. package/docs/architecture.md +103 -0
  5. package/docs/assets/pi-stardeck.gif +0 -0
  6. package/docs/development.md +128 -0
  7. package/licenses/README.md +17 -0
  8. package/licenses/pi-fancy-footer-LICENSE +21 -0
  9. package/licenses/pi-vim-LICENSE +21 -0
  10. package/package.json +81 -4
  11. package/packages/pi-fancy-footer-full-palette/LICENSE +21 -0
  12. package/packages/pi-fancy-footer-full-palette/LOCAL-CHANGES.md +41 -0
  13. package/packages/pi-fancy-footer-full-palette/README.md +468 -0
  14. package/packages/pi-fancy-footer-full-palette/package.json +52 -0
  15. package/packages/pi-fancy-footer-full-palette/src/api.ts +204 -0
  16. package/packages/pi-fancy-footer-full-palette/src/config-editor.ts +634 -0
  17. package/packages/pi-fancy-footer-full-palette/src/config.ts +852 -0
  18. package/packages/pi-fancy-footer-full-palette/src/data-widgets.ts +259 -0
  19. package/packages/pi-fancy-footer-full-palette/src/git.ts +130 -0
  20. package/packages/pi-fancy-footer-full-palette/src/index.ts +479 -0
  21. package/packages/pi-fancy-footer-full-palette/src/layout.ts +309 -0
  22. package/packages/pi-fancy-footer-full-palette/src/provider-status.ts +1466 -0
  23. package/packages/pi-fancy-footer-full-palette/src/render.ts +1061 -0
  24. package/packages/pi-fancy-footer-full-palette/src/shared.ts +903 -0
  25. package/packages/pi-stardeck/README.md +219 -0
  26. package/packages/pi-stardeck/index.ts +691 -0
  27. package/packages/pi-stardeck/package.json +32 -0
  28. package/packages/pi-stardeck/src/activity-age.ts +184 -0
  29. package/packages/pi-stardeck/src/capabilities.ts +105 -0
  30. package/packages/pi-stardeck/src/deck.ts +807 -0
  31. package/packages/pi-stardeck/src/gauge.ts +1323 -0
  32. package/packages/pi-stardeck/src/git-refresh.ts +5 -0
  33. package/packages/pi-stardeck/src/process-resources.ts +428 -0
  34. package/packages/pi-stardeck/src/title-state.ts +195 -0
  35. package/packages/pi-stardeck/src/title.ts +443 -0
  36. package/packages/pi-vim-top-border/LICENSE +21 -0
  37. package/packages/pi-vim-top-border/LOCAL-CHANGES.md +71 -0
  38. package/packages/pi-vim-top-border/README.md +577 -0
  39. package/packages/pi-vim-top-border/clipboard-mirror.ts +451 -0
  40. package/packages/pi-vim-top-border/clipboard-policy.ts +23 -0
  41. package/packages/pi-vim-top-border/cursor-shape.ts +131 -0
  42. package/packages/pi-vim-top-border/index.ts +4195 -0
  43. package/packages/pi-vim-top-border/input-keys.ts +49 -0
  44. package/packages/pi-vim-top-border/mode-change-command.ts +120 -0
  45. package/packages/pi-vim-top-border/mode-colors.ts +97 -0
  46. package/packages/pi-vim-top-border/mode-label.ts +62 -0
  47. package/packages/pi-vim-top-border/motions.ts +297 -0
  48. package/packages/pi-vim-top-border/package.json +47 -0
  49. package/packages/pi-vim-top-border/prompt-external-editor.ts +152 -0
  50. package/packages/pi-vim-top-border/prompt-rail.ts +334 -0
  51. package/packages/pi-vim-top-border/prompt-status.ts +131 -0
  52. package/packages/pi-vim-top-border/settings.ts +287 -0
  53. package/packages/pi-vim-top-border/text-objects.ts +389 -0
  54. package/packages/pi-vim-top-border/types.ts +51 -0
  55. package/packages/pi-vim-top-border/visual.ts +65 -0
  56. package/packages/pi-vim-top-border/word-boundary-cache.ts +196 -0
  57. package/packages/status/README.md +398 -0
  58. package/packages/status/index.ts +1717 -0
  59. package/packages/status/package.json +33 -0
  60. package/packages/status/src/active-work.ts +235 -0
  61. package/packages/status/src/config.ts +218 -0
  62. package/packages/status/src/diagnostics.ts +238 -0
  63. package/packages/status/src/goal-block.ts +377 -0
  64. package/packages/status/src/goal.ts +170 -0
  65. package/packages/status/src/ledger.ts +101 -0
  66. package/packages/status/src/openspec.ts +394 -0
  67. package/packages/status/src/orchestration.ts +237 -0
  68. package/packages/status/src/publisher.ts +1294 -0
  69. package/packages/status/src/reducer.ts +190 -0
  70. package/packages/status/src/runtime-runs.ts +267 -0
  71. package/packages/status/src/state.ts +178 -0
  72. package/packages/status/src/title-activity.ts +224 -0
  73. package/packages/status/src/title-state.ts +218 -0
  74. package/packages/status/src/turn-cost.ts +63 -0
  75. package/packages/status/src/types.ts +228 -0
  76. package/src/deck-surface.ts +46 -0
  77. package/src/health.ts +228 -0
  78. package/src/index.ts +28 -0
  79. package/src/pi-status.ts +334 -0
@@ -0,0 +1,334 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+
3
+ export const PI_STATUS_SOURCE_CHANNEL = 'pi-stardeck:status-source/v1';
4
+ export const PI_NATIVE_STATUS_CHANNEL = 'pi-stardeck:native-status/v1';
5
+ export const PI_STATUS_SNAPSHOT_CHANNEL = 'pi-stardeck:pi-status/v1';
6
+
7
+ const MAX_SOURCES = 64;
8
+ const MAX_CONDITIONS_PER_SOURCE = 32;
9
+ const MAX_NATIVE_STATUSES = 999;
10
+ const MAX_ID_LENGTH = 96;
11
+ const MAX_SUMMARY_LENGTH = 240;
12
+ const ANSI_ESCAPE = new RegExp(
13
+ '\\u001b(?:\\][\\s\\S]*?(?:\\u0007|\\u001b\\\\)|\\[[0-?]*[ -/]*[@-~]|[@-_])',
14
+ 'gu',
15
+ );
16
+ const CONTROL_CHARACTER = new RegExp('[\\u0000-\\u001f\\u007f-\\u009f]', 'gu');
17
+
18
+ export type PiStatusSeverity = 'warning' | 'error';
19
+
20
+ export interface PiStatusCondition {
21
+ id: string;
22
+ source: string;
23
+ severity: PiStatusSeverity;
24
+ summary: string;
25
+ }
26
+
27
+ export interface PiNativeStatus {
28
+ key: string;
29
+ text: string;
30
+ }
31
+
32
+ export interface PiStatusSnapshot {
33
+ protocol: 1;
34
+ type: 'snapshot';
35
+ errors: number;
36
+ warnings: number;
37
+ nativeStatusCount: number;
38
+ conditions: PiStatusCondition[];
39
+ nativeStatuses: PiNativeStatus[];
40
+ }
41
+
42
+ export interface PiStatusSourceMessage {
43
+ protocol: 1;
44
+ type: 'snapshot';
45
+ source: string;
46
+ conditions: Array<{
47
+ id: string;
48
+ severity: PiStatusSeverity;
49
+ summary: string;
50
+ }>;
51
+ }
52
+
53
+ export interface PiNativeStatusMessage {
54
+ protocol: 1;
55
+ type: 'snapshot';
56
+ statuses: PiNativeStatus[];
57
+ }
58
+
59
+ function isRecord(value: unknown): value is Record<string, unknown> {
60
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
61
+ }
62
+
63
+ function sanitizeInline(value: unknown, maximum: number): string {
64
+ if (typeof value !== 'string') return '';
65
+ return Array.from(
66
+ value
67
+ .replace(ANSI_ESCAPE, '')
68
+ .replace(CONTROL_CHARACTER, ' ')
69
+ .replace(/\s+/gu, ' ')
70
+ .trim(),
71
+ )
72
+ .slice(0, maximum)
73
+ .join('');
74
+ }
75
+
76
+ function safeId(value: unknown): string {
77
+ const id = sanitizeInline(value, MAX_ID_LENGTH);
78
+ return /^[a-z0-9][a-z0-9._:-]*$/iu.test(id) ? id : '';
79
+ }
80
+
81
+ export function parsePiStatusSourceMessage(raw: unknown): PiStatusSourceMessage | null {
82
+ if (!isRecord(raw) || raw.protocol !== 1 || raw.type !== 'snapshot') return null;
83
+ const source = safeId(raw.source);
84
+ if (!source || !Array.isArray(raw.conditions)) return null;
85
+ if (raw.conditions.length > MAX_CONDITIONS_PER_SOURCE) return null;
86
+
87
+ const conditions: PiStatusSourceMessage['conditions'] = [];
88
+ const ids = new Set<string>();
89
+ for (const entry of raw.conditions) {
90
+ if (!isRecord(entry)) return null;
91
+ const id = safeId(entry.id);
92
+ const summary = sanitizeInline(entry.summary, MAX_SUMMARY_LENGTH);
93
+ if (
94
+ !id ||
95
+ ids.has(id) ||
96
+ !summary ||
97
+ (entry.severity !== 'warning' && entry.severity !== 'error')
98
+ ) {
99
+ return null;
100
+ }
101
+ ids.add(id);
102
+ conditions.push({ id, severity: entry.severity, summary });
103
+ }
104
+ return { protocol: 1, type: 'snapshot', source, conditions };
105
+ }
106
+
107
+ export function parsePiNativeStatusMessage(raw: unknown): PiNativeStatusMessage | null {
108
+ if (!isRecord(raw) || raw.protocol !== 1 || raw.type !== 'snapshot') return null;
109
+ if (!Array.isArray(raw.statuses) || raw.statuses.length > MAX_NATIVE_STATUSES) {
110
+ return null;
111
+ }
112
+
113
+ const statuses: PiNativeStatus[] = [];
114
+ const keys = new Set<string>();
115
+ for (const entry of raw.statuses) {
116
+ if (!isRecord(entry)) return null;
117
+ const key = sanitizeInline(entry.key, MAX_ID_LENGTH);
118
+ const text = sanitizeInline(entry.text, MAX_SUMMARY_LENGTH);
119
+ if (!key || keys.has(key) || !text) return null;
120
+ keys.add(key);
121
+ statuses.push({ key, text });
122
+ }
123
+ return { protocol: 1, type: 'snapshot', statuses };
124
+ }
125
+
126
+ function snapshotSignature(snapshot: PiStatusSnapshot): string {
127
+ return JSON.stringify(snapshot);
128
+ }
129
+
130
+ export class ObservablePiStatusStore {
131
+ private readonly sources = new Map<string, PiStatusCondition[]>();
132
+ private nativeStatuses: PiNativeStatus[] = [];
133
+ private lastSignature = '';
134
+
135
+ applySource(raw: unknown): boolean {
136
+ const message = parsePiStatusSourceMessage(raw);
137
+ if (!message) return false;
138
+ if (!this.sources.has(message.source) && this.sources.size >= MAX_SOURCES) {
139
+ return false;
140
+ }
141
+ const next = message.conditions.map((condition) => ({
142
+ ...condition,
143
+ source: message.source,
144
+ }));
145
+ if (
146
+ JSON.stringify(this.sources.get(message.source) ?? []) === JSON.stringify(next)
147
+ ) {
148
+ return false;
149
+ }
150
+ if (next.length === 0) this.sources.delete(message.source);
151
+ else this.sources.set(message.source, next);
152
+ return true;
153
+ }
154
+
155
+ applyNative(raw: unknown): boolean {
156
+ const message = parsePiNativeStatusMessage(raw);
157
+ if (!message) return false;
158
+ if (JSON.stringify(this.nativeStatuses) === JSON.stringify(message.statuses)) {
159
+ return false;
160
+ }
161
+ this.nativeStatuses = message.statuses;
162
+ return true;
163
+ }
164
+
165
+ setSource(message: PiStatusSourceMessage): boolean {
166
+ return this.applySource(message);
167
+ }
168
+
169
+ clear(): boolean {
170
+ if (this.sources.size === 0 && this.nativeStatuses.length === 0) return false;
171
+ this.sources.clear();
172
+ this.nativeStatuses = [];
173
+ return true;
174
+ }
175
+
176
+ snapshot(): PiStatusSnapshot {
177
+ const conditions = [...this.sources.values()]
178
+ .flat()
179
+ .sort(
180
+ (left, right) =>
181
+ (left.severity === right.severity ? 0 : left.severity === 'error' ? -1 : 1) ||
182
+ left.source.localeCompare(right.source) ||
183
+ left.id.localeCompare(right.id),
184
+ );
185
+ return {
186
+ protocol: 1,
187
+ type: 'snapshot',
188
+ errors: conditions.filter(({ severity }) => severity === 'error').length,
189
+ warnings: conditions.filter(({ severity }) => severity === 'warning').length,
190
+ nativeStatusCount: this.nativeStatuses.length,
191
+ conditions,
192
+ nativeStatuses: [...this.nativeStatuses],
193
+ };
194
+ }
195
+
196
+ changedSnapshot(): PiStatusSnapshot | null {
197
+ const snapshot = this.snapshot();
198
+ const signature = snapshotSignature(snapshot);
199
+ if (signature === this.lastSignature) return null;
200
+ this.lastSignature = signature;
201
+ return snapshot;
202
+ }
203
+ }
204
+
205
+ export function formatPiStatus(snapshot: PiStatusSnapshot): string {
206
+ const headline =
207
+ snapshot.errors > 0 || snapshot.warnings > 0
208
+ ? `Pi status · ${snapshot.errors} errors · ${snapshot.warnings} warnings`
209
+ : 'Pi status · no observed issues';
210
+ const lines = [headline];
211
+
212
+ for (const condition of snapshot.conditions) {
213
+ lines.push(
214
+ `${condition.severity === 'error' ? 'ERROR' : 'WARN '} ${condition.source} · ${condition.summary}`,
215
+ );
216
+ }
217
+
218
+ if (snapshot.nativeStatuses.length > 0) {
219
+ lines.push('', `Native extension statuses · ${snapshot.nativeStatuses.length}`);
220
+ for (const status of snapshot.nativeStatuses) {
221
+ lines.push(`${status.key} · ${status.text}`);
222
+ }
223
+ } else {
224
+ lines.push('', 'Native extension statuses · none');
225
+ }
226
+
227
+ lines.push(
228
+ '',
229
+ 'Coverage · observable session events and cooperating loaded extensions',
230
+ 'Gap · Pi does not publish arbitrary extension-load or handler exceptions',
231
+ );
232
+ return lines.join('\n');
233
+ }
234
+
235
+ export function registerPiStatus(pi: ExtensionAPI): ObservablePiStatusStore {
236
+ const store = new ObservablePiStatusStore();
237
+
238
+ const publish = (force = false) => {
239
+ const snapshot = store.changedSnapshot();
240
+ if (snapshot) pi.events.emit(PI_STATUS_SNAPSHOT_CHANNEL, snapshot);
241
+ else if (force) pi.events.emit(PI_STATUS_SNAPSHOT_CHANNEL, store.snapshot());
242
+ };
243
+
244
+ pi.events.on(PI_STATUS_SOURCE_CHANNEL, (raw) => {
245
+ if (store.applySource(raw)) publish();
246
+ });
247
+ pi.events.on(PI_NATIVE_STATUS_CHANNEL, (raw) => {
248
+ if (store.applyNative(raw)) publish();
249
+ });
250
+
251
+ pi.on('session_start', () => {
252
+ store.clear();
253
+ publish(true);
254
+ });
255
+ pi.on('session_shutdown', () => {
256
+ store.clear();
257
+ });
258
+ pi.on('session_compact', () => {
259
+ if (
260
+ store.setSource({
261
+ protocol: 1,
262
+ type: 'snapshot',
263
+ source: 'pi.compaction',
264
+ conditions: [],
265
+ })
266
+ ) {
267
+ publish();
268
+ }
269
+ });
270
+ pi.on('session_compact_failed', (event) => {
271
+ if (event.aborted) return;
272
+ const summary = event.errorMessage
273
+ ? `Compaction failed: ${sanitizeInline(event.errorMessage, 160)}`
274
+ : 'Compaction failed';
275
+ if (
276
+ store.setSource({
277
+ protocol: 1,
278
+ type: 'snapshot',
279
+ source: 'pi.compaction',
280
+ conditions: [
281
+ {
282
+ id: 'failure',
283
+ severity: event.willRetry ? 'warning' : 'error',
284
+ summary,
285
+ },
286
+ ],
287
+ })
288
+ ) {
289
+ publish();
290
+ }
291
+ });
292
+ pi.on('message_end', (event) => {
293
+ if (event.message.role !== 'assistant') return;
294
+ const message = event.message as typeof event.message & {
295
+ stopReason?: string;
296
+ errorMessage?: string;
297
+ };
298
+ const conditions: PiStatusSourceMessage['conditions'] =
299
+ message.stopReason === 'error'
300
+ ? [
301
+ {
302
+ id: 'response-failure',
303
+ severity: 'error',
304
+ summary: message.errorMessage
305
+ ? `Assistant response failed: ${sanitizeInline(message.errorMessage, 160)}`
306
+ : 'Assistant response failed',
307
+ },
308
+ ]
309
+ : [];
310
+ if (
311
+ store.setSource({
312
+ protocol: 1,
313
+ type: 'snapshot',
314
+ source: 'pi.agent',
315
+ conditions,
316
+ })
317
+ ) {
318
+ publish();
319
+ }
320
+ });
321
+
322
+ pi.registerCommand('pi-status', {
323
+ description: 'Show observable Pi session conditions and native extension statuses',
324
+ handler: async (_args, ctx) => {
325
+ const snapshot = store.snapshot();
326
+ ctx.ui.notify(
327
+ formatPiStatus(snapshot),
328
+ snapshot.errors > 0 ? 'error' : snapshot.warnings > 0 ? 'warning' : 'info',
329
+ );
330
+ },
331
+ });
332
+
333
+ return store;
334
+ }