@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,259 @@
1
+ import { Type } from "typebox";
2
+ import { Compile } from "typebox/compile";
3
+ import {
4
+ FANCY_FOOTER_PROTOCOL_VERSION,
5
+ type FancyFooterDataWidget,
6
+ type FancyFooterDataWidgetIcon,
7
+ } from "./api.ts";
8
+ import {
9
+ FOOTER_ICON_FAMILIES,
10
+ FOOTER_WIDGET_COLORS,
11
+ MAX_WIDGET_MIN_WIDTH,
12
+ MAX_WIDGET_POSITION,
13
+ MAX_WIDGET_ROW,
14
+ isFooterWidgetId,
15
+ type FooterIconFamily,
16
+ type FooterWidgetAlign,
17
+ type FooterWidgetColor,
18
+ type FooterWidgetEditorDefaults,
19
+ type FooterWidgetFill,
20
+ type FooterWidgetIcon,
21
+ } from "./shared.ts";
22
+
23
+ export const MAX_DATA_WIDGET_TEXT_CODE_POINTS = 512;
24
+ export const MAX_DATA_WIDGET_ID_LENGTH = 128;
25
+ export const MAX_DATA_WIDGET_HREF_LENGTH = 2_048;
26
+
27
+ const DATA_WIDGET_ID_PATTERN =
28
+ /^[A-Za-z0-9][A-Za-z0-9_-]*(?:\.[A-Za-z0-9][A-Za-z0-9_-]*)+$/u;
29
+ const DATA_WIDGET_HREF_PATTERN = /^https?:\/\/[^\s\u0000-\u001f\u007f-\u009f]+$/u;
30
+
31
+ const literalUnion = (values: readonly string[]) =>
32
+ Type.Union(values.map((value) => Type.Literal(value)));
33
+
34
+ const colorSchema = literalUnion(FOOTER_WIDGET_COLORS);
35
+ const dataWidgetIdSchema = Type.String({
36
+ minLength: 3,
37
+ maxLength: MAX_DATA_WIDGET_ID_LENGTH,
38
+ pattern: DATA_WIDGET_ID_PATTERN.source,
39
+ });
40
+ const glyphsSchema = Type.Union([
41
+ Type.String(),
42
+ Type.Object(
43
+ Object.fromEntries(
44
+ FOOTER_ICON_FAMILIES.map((family) => [family, Type.Optional(Type.String())]),
45
+ ),
46
+ { additionalProperties: false },
47
+ ),
48
+ ]);
49
+ const dataWidgetSchema = Type.Object(
50
+ {
51
+ id: dataWidgetIdSchema,
52
+ label: Type.Optional(Type.String({ minLength: 1 })),
53
+ description: Type.Optional(Type.String({ minLength: 1 })),
54
+ content: Type.Object(
55
+ {
56
+ type: Type.Literal("text"),
57
+ text: Type.String(),
58
+ href: Type.Optional(Type.String()),
59
+ },
60
+ { additionalProperties: false },
61
+ ),
62
+ icon: Type.Optional(
63
+ Type.Union([
64
+ Type.Literal(false),
65
+ Type.Object(
66
+ {
67
+ glyphs: glyphsSchema,
68
+ color: Type.Optional(colorSchema),
69
+ },
70
+ { additionalProperties: false },
71
+ ),
72
+ ]),
73
+ ),
74
+ style: Type.Optional(
75
+ Type.Object(
76
+ {
77
+ textColor: Type.Optional(colorSchema),
78
+ bold: Type.Optional(Type.Boolean()),
79
+ },
80
+ { additionalProperties: false },
81
+ ),
82
+ ),
83
+ layout: Type.Optional(
84
+ Type.Object(
85
+ {
86
+ enabled: Type.Optional(Type.Boolean()),
87
+ row: Type.Optional(Type.Integer({ minimum: 0, maximum: MAX_WIDGET_ROW })),
88
+ position: Type.Optional(
89
+ Type.Integer({ minimum: 0, maximum: MAX_WIDGET_POSITION }),
90
+ ),
91
+ align: Type.Optional(literalUnion(["left", "middle", "right"])),
92
+ fill: Type.Optional(literalUnion(["none", "grow"])),
93
+ minWidth: Type.Optional(
94
+ Type.Integer({ minimum: 0, maximum: MAX_WIDGET_MIN_WIDTH }),
95
+ ),
96
+ },
97
+ { additionalProperties: false },
98
+ ),
99
+ ),
100
+ },
101
+ { additionalProperties: false },
102
+ );
103
+ const upsertMessageSchema = Type.Object(
104
+ {
105
+ protocol: Type.Literal(FANCY_FOOTER_PROTOCOL_VERSION),
106
+ type: Type.Literal("upsert"),
107
+ widget: dataWidgetSchema,
108
+ },
109
+ { additionalProperties: false },
110
+ );
111
+ const removeMessageSchema = Type.Object(
112
+ {
113
+ protocol: Type.Literal(FANCY_FOOTER_PROTOCOL_VERSION),
114
+ type: Type.Literal("remove"),
115
+ id: dataWidgetIdSchema,
116
+ },
117
+ { additionalProperties: false },
118
+ );
119
+ const validateUpsertMessage = Compile(upsertMessageSchema);
120
+ const validateRemoveMessage = Compile(removeMessageSchema);
121
+
122
+ export interface NormalizedFancyFooterDataWidget {
123
+ id: string;
124
+ label: string;
125
+ description: string;
126
+ content: { type: "text"; text: string; href?: string };
127
+ icon?: FancyFooterDataWidgetIcon | false;
128
+ preferredTextColor?: FooterWidgetColor;
129
+ bold?: boolean;
130
+ defaults: FooterWidgetEditorDefaults;
131
+ }
132
+
133
+ function sanitizeInlineText(text: string, maxCodePoints: number): string {
134
+ const inline = text
135
+ .replace(/[\u0000-\u001f\u007f-\u009f]/gu, " ")
136
+ .replace(/\s+/gu, " ")
137
+ .trim();
138
+ return Array.from(inline).slice(0, maxCodePoints).join("");
139
+ }
140
+
141
+ export function sanitizeDataWidgetText(text: string): string {
142
+ return sanitizeInlineText(text, MAX_DATA_WIDGET_TEXT_CODE_POINTS);
143
+ }
144
+
145
+ function normalizeHref(value: string | undefined): string | undefined {
146
+ if (
147
+ !value ||
148
+ value.length > MAX_DATA_WIDGET_HREF_LENGTH ||
149
+ !DATA_WIDGET_HREF_PATTERN.test(value)
150
+ ) {
151
+ return undefined;
152
+ }
153
+ return value;
154
+ }
155
+
156
+ function sanitizeGlyph(text: string): string {
157
+ return sanitizeInlineText(text, 16);
158
+ }
159
+
160
+ function normalizeIcon(
161
+ icon: FancyFooterDataWidget["icon"],
162
+ ): FancyFooterDataWidgetIcon | false | undefined {
163
+ if (icon === undefined || icon === false) return icon;
164
+ const sourceGlyphs = icon.glyphs;
165
+ const glyphs =
166
+ typeof sourceGlyphs === "string"
167
+ ? sanitizeGlyph(sourceGlyphs)
168
+ : Object.fromEntries(
169
+ FOOTER_ICON_FAMILIES.flatMap((family) => {
170
+ const glyph = sourceGlyphs[family];
171
+ return glyph === undefined ? [] : [[family, sanitizeGlyph(glyph)]];
172
+ }),
173
+ );
174
+ return { glyphs, ...(icon.color ? { color: icon.color } : {}) };
175
+ }
176
+
177
+ function normalizeWidget(
178
+ widget: FancyFooterDataWidget,
179
+ ): NormalizedFancyFooterDataWidget | undefined {
180
+ const id = sanitizeInlineText(widget.id, MAX_DATA_WIDGET_ID_LENGTH);
181
+ if (!id || isFooterWidgetId(id)) return undefined;
182
+
183
+ const label = sanitizeInlineText(widget.label ?? id, 128) || id;
184
+ const description = sanitizeInlineText(widget.description ?? label, 512) || label;
185
+ const href = normalizeHref(widget.content.href);
186
+ return {
187
+ id,
188
+ label,
189
+ description,
190
+ content: {
191
+ type: "text",
192
+ text: sanitizeDataWidgetText(widget.content.text),
193
+ ...(href ? { href } : {}),
194
+ },
195
+ icon: normalizeIcon(widget.icon),
196
+ preferredTextColor: widget.style?.textColor,
197
+ bold: widget.style?.bold,
198
+ defaults: {
199
+ enabled: widget.layout?.enabled,
200
+ row: widget.layout?.row ?? 1,
201
+ position: widget.layout?.position ?? 0,
202
+ align: (widget.layout?.align ?? "right") as FooterWidgetAlign,
203
+ fill: (widget.layout?.fill ?? "none") as FooterWidgetFill,
204
+ minWidth: widget.layout?.minWidth,
205
+ },
206
+ };
207
+ }
208
+
209
+ export class FancyFooterDataWidgetStore {
210
+ private readonly widgets = new Map<string, NormalizedFancyFooterDataWidget>();
211
+
212
+ apply(raw: unknown): boolean {
213
+ if (validateUpsertMessage.Check(raw)) {
214
+ const widget = normalizeWidget(raw.widget as FancyFooterDataWidget);
215
+ if (!widget) return false;
216
+ this.widgets.set(widget.id, widget);
217
+ return true;
218
+ }
219
+ if (validateRemoveMessage.Check(raw)) {
220
+ const id = sanitizeInlineText(raw.id as string, 128);
221
+ return id !== "" && !isFooterWidgetId(id) && this.widgets.delete(id);
222
+ }
223
+ return false;
224
+ }
225
+
226
+ clear(): boolean {
227
+ if (this.widgets.size === 0) return false;
228
+ this.widgets.clear();
229
+ return true;
230
+ }
231
+
232
+ values(): NormalizedFancyFooterDataWidget[] {
233
+ return [...this.widgets.values()].sort((left, right) =>
234
+ left.label.localeCompare(right.label),
235
+ );
236
+ }
237
+ }
238
+
239
+ export function resolveDataWidgetIcon(
240
+ icon: NormalizedFancyFooterDataWidget["icon"],
241
+ iconFamily: FooterIconFamily,
242
+ ): FooterWidgetIcon | undefined {
243
+ if (!icon) return undefined;
244
+ const text =
245
+ typeof icon.glyphs === "string" ? icon.glyphs : (icon.glyphs[iconFamily] ?? "");
246
+ return text ? { text, color: icon.color ?? "text" } : undefined;
247
+ }
248
+
249
+ export function createMicrotaskCoalescer(callback: () => void): () => void {
250
+ let queued = false;
251
+ return () => {
252
+ if (queued) return;
253
+ queued = true;
254
+ queueMicrotask(() => {
255
+ queued = false;
256
+ callback();
257
+ });
258
+ };
259
+ }
@@ -0,0 +1,130 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { EMPTY_GIT_INFO, type GitInfo, parseNumstat, toNumber } from "./shared.ts";
3
+
4
+ interface ExecResult {
5
+ code: number;
6
+ stdout: string;
7
+ stderr: string;
8
+ }
9
+
10
+ const DEFAULT_COMMAND_TIMEOUT_MS = 2_000;
11
+ const GIT_NO_OPTIONAL_LOCKS_ARG = "--no-optional-locks";
12
+ async function execResult(
13
+ pi: ExtensionAPI,
14
+ command: string,
15
+ args: string[],
16
+ cwd: string,
17
+ timeout = DEFAULT_COMMAND_TIMEOUT_MS,
18
+ ): Promise<ExecResult> {
19
+ try {
20
+ const result = await pi.exec(command, args, { cwd, timeout });
21
+ return {
22
+ code: result.code,
23
+ // Keep leading whitespace (git porcelain uses it), only drop trailing newlines.
24
+ stdout: result.stdout.replace(/[\r\n]+$/, ""),
25
+ stderr: result.stderr.replace(/[\r\n]+$/, ""),
26
+ };
27
+ } catch {
28
+ return { code: -1, stdout: "", stderr: "" };
29
+ }
30
+ }
31
+
32
+ async function execGitResult(
33
+ pi: ExtensionAPI,
34
+ args: string[],
35
+ cwd: string,
36
+ timeout = DEFAULT_COMMAND_TIMEOUT_MS,
37
+ ): Promise<ExecResult> {
38
+ return execResult(pi, "git", [GIT_NO_OPTIONAL_LOCKS_ARG, ...args], cwd, timeout);
39
+ }
40
+
41
+ async function execGit(pi: ExtensionAPI, args: string[], cwd: string): Promise<string> {
42
+ const result = await execGitResult(pi, args, cwd);
43
+ if (result.code !== 0) return "";
44
+ return result.stdout;
45
+ }
46
+
47
+ export async function collectGitInfo(pi: ExtensionAPI, cwd: string): Promise<GitInfo> {
48
+ const porcelainV2 = await execGit(pi, ["status", "--porcelain=2", "--branch"], cwd);
49
+
50
+ if (!porcelainV2) return { ...EMPTY_GIT_INFO };
51
+
52
+ let branch = "";
53
+ let commit = "";
54
+ let staged = 0;
55
+ let modified = 0;
56
+ let untracked = 0;
57
+ let ahead = 0;
58
+ let behind = 0;
59
+
60
+ for (const line of porcelainV2.split(/\r?\n/)) {
61
+ if (!line) continue;
62
+
63
+ if (line.startsWith("# branch.head ")) {
64
+ const head = line.slice("# branch.head ".length).trim();
65
+ branch = head === "(detached)" ? "" : head;
66
+ continue;
67
+ }
68
+
69
+ if (line.startsWith("# branch.oid ")) {
70
+ const oid = line.slice("# branch.oid ".length).trim();
71
+ if (oid && oid !== "(initial)") commit = oid.slice(0, 7);
72
+ continue;
73
+ }
74
+
75
+ if (line.startsWith("# branch.ab ")) {
76
+ const match = line.match(/^# branch\.ab \+(\d+) -(\d+)$/);
77
+ if (match) {
78
+ ahead = Math.max(0, Math.floor(toNumber(match[1])));
79
+ behind = Math.max(0, Math.floor(toNumber(match[2])));
80
+ }
81
+ continue;
82
+ }
83
+
84
+ if (line.startsWith("? ")) {
85
+ untracked += 1;
86
+ continue;
87
+ }
88
+
89
+ if (line.startsWith("1 ") || line.startsWith("2 ") || line.startsWith("u ")) {
90
+ const xy = line.split(" ")[1] || "..";
91
+ const x = xy[0] || ".";
92
+ const y = xy[1] || ".";
93
+ if (x !== ".") staged += 1;
94
+ if (y !== ".") modified += 1;
95
+ }
96
+ }
97
+
98
+ let added = 0;
99
+ let removed = 0;
100
+
101
+ const headDiff = await execGit(pi, ["diff", "--numstat", "HEAD"], cwd);
102
+ if (headDiff) {
103
+ const stats = parseNumstat(headDiff);
104
+ added = stats.added;
105
+ removed = stats.removed;
106
+ } else {
107
+ const [stagedDiff, unstagedDiff] = await Promise.all([
108
+ execGit(pi, ["diff", "--numstat", "--cached"], cwd),
109
+ execGit(pi, ["diff", "--numstat"], cwd),
110
+ ]);
111
+ const stagedStats = parseNumstat(stagedDiff);
112
+ const unstagedStats = parseNumstat(unstagedDiff);
113
+ added = stagedStats.added + unstagedStats.added;
114
+ removed = stagedStats.removed + unstagedStats.removed;
115
+ }
116
+
117
+ return {
118
+ branch,
119
+ commit,
120
+ added,
121
+ removed,
122
+ counts: {
123
+ staged,
124
+ modified,
125
+ untracked,
126
+ ahead,
127
+ behind,
128
+ },
129
+ };
130
+ }