omegon 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 (160) hide show
  1. package/.gitattributes +3 -0
  2. package/AGENTS.md +16 -0
  3. package/LICENSE +15 -0
  4. package/README.md +289 -0
  5. package/bin/pi.mjs +30 -0
  6. package/extensions/00-secrets/index.ts +1126 -0
  7. package/extensions/01-auth/auth.ts +401 -0
  8. package/extensions/01-auth/index.ts +289 -0
  9. package/extensions/auto-compact.ts +42 -0
  10. package/extensions/bootstrap/deps.ts +291 -0
  11. package/extensions/bootstrap/index.ts +811 -0
  12. package/extensions/chronos/chronos.sh +487 -0
  13. package/extensions/chronos/index.ts +148 -0
  14. package/extensions/cleave/assessment.ts +754 -0
  15. package/extensions/cleave/bridge.ts +31 -0
  16. package/extensions/cleave/conflicts.ts +250 -0
  17. package/extensions/cleave/dispatcher.ts +808 -0
  18. package/extensions/cleave/guardrails.ts +426 -0
  19. package/extensions/cleave/index.ts +3121 -0
  20. package/extensions/cleave/lifecycle-emitter.ts +20 -0
  21. package/extensions/cleave/openspec.ts +811 -0
  22. package/extensions/cleave/planner.ts +260 -0
  23. package/extensions/cleave/review.ts +579 -0
  24. package/extensions/cleave/skills.ts +355 -0
  25. package/extensions/cleave/types.ts +261 -0
  26. package/extensions/cleave/workspace.ts +861 -0
  27. package/extensions/cleave/worktree.ts +243 -0
  28. package/extensions/core-renderers.ts +253 -0
  29. package/extensions/dashboard/context-gauge.ts +58 -0
  30. package/extensions/dashboard/file-watch.ts +14 -0
  31. package/extensions/dashboard/footer.ts +1145 -0
  32. package/extensions/dashboard/git.ts +185 -0
  33. package/extensions/dashboard/index.ts +478 -0
  34. package/extensions/dashboard/memory-audit.ts +34 -0
  35. package/extensions/dashboard/overlay-data.ts +705 -0
  36. package/extensions/dashboard/overlay.ts +365 -0
  37. package/extensions/dashboard/render-utils.ts +54 -0
  38. package/extensions/dashboard/types.ts +191 -0
  39. package/extensions/dashboard/uri-helper.ts +45 -0
  40. package/extensions/debug.ts +69 -0
  41. package/extensions/defaults.ts +282 -0
  42. package/extensions/design-tree/dashboard-state.ts +161 -0
  43. package/extensions/design-tree/design-card.ts +362 -0
  44. package/extensions/design-tree/index.ts +2130 -0
  45. package/extensions/design-tree/lifecycle-emitter.ts +41 -0
  46. package/extensions/design-tree/tree.ts +1607 -0
  47. package/extensions/design-tree/types.ts +163 -0
  48. package/extensions/distill.ts +127 -0
  49. package/extensions/effort/index.ts +395 -0
  50. package/extensions/effort/tiers.ts +146 -0
  51. package/extensions/effort/types.ts +105 -0
  52. package/extensions/lib/git-state.ts +227 -0
  53. package/extensions/lib/local-models.ts +157 -0
  54. package/extensions/lib/model-preferences.ts +51 -0
  55. package/extensions/lib/model-routing.ts +720 -0
  56. package/extensions/lib/operator-fallback.ts +205 -0
  57. package/extensions/lib/operator-profile.ts +360 -0
  58. package/extensions/lib/slash-command-bridge.ts +253 -0
  59. package/extensions/lib/typebox-helpers.ts +16 -0
  60. package/extensions/local-inference/index.ts +727 -0
  61. package/extensions/mcp-bridge/README.md +220 -0
  62. package/extensions/mcp-bridge/index.ts +951 -0
  63. package/extensions/mcp-bridge/lib.ts +365 -0
  64. package/extensions/mcp-bridge/mcp.json +3 -0
  65. package/extensions/mcp-bridge/package.json +11 -0
  66. package/extensions/model-budget.ts +752 -0
  67. package/extensions/offline-driver.ts +403 -0
  68. package/extensions/openspec/archive-gate.ts +164 -0
  69. package/extensions/openspec/branch-cleanup.ts +64 -0
  70. package/extensions/openspec/dashboard-state.ts +50 -0
  71. package/extensions/openspec/index.ts +1917 -0
  72. package/extensions/openspec/lifecycle-emitter.ts +65 -0
  73. package/extensions/openspec/lifecycle-files.ts +70 -0
  74. package/extensions/openspec/lifecycle.ts +50 -0
  75. package/extensions/openspec/reconcile.ts +187 -0
  76. package/extensions/openspec/spec.ts +1385 -0
  77. package/extensions/openspec/types.ts +98 -0
  78. package/extensions/project-memory/DESIGN-global-mind.md +198 -0
  79. package/extensions/project-memory/README.md +202 -0
  80. package/extensions/project-memory/api-types.ts +382 -0
  81. package/extensions/project-memory/compaction-policy.ts +29 -0
  82. package/extensions/project-memory/core.ts +164 -0
  83. package/extensions/project-memory/embeddings.ts +230 -0
  84. package/extensions/project-memory/extraction-v2.ts +861 -0
  85. package/extensions/project-memory/factstore.ts +2177 -0
  86. package/extensions/project-memory/index.ts +3459 -0
  87. package/extensions/project-memory/injection-metrics.ts +91 -0
  88. package/extensions/project-memory/jsonl-io.ts +12 -0
  89. package/extensions/project-memory/lifecycle.ts +331 -0
  90. package/extensions/project-memory/migration.ts +293 -0
  91. package/extensions/project-memory/package.json +9 -0
  92. package/extensions/project-memory/sci-renderers.ts +7 -0
  93. package/extensions/project-memory/template.ts +103 -0
  94. package/extensions/project-memory/triggers.ts +52 -0
  95. package/extensions/project-memory/types.ts +102 -0
  96. package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
  97. package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
  98. package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
  99. package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
  100. package/extensions/render/composition/package-lock.json +534 -0
  101. package/extensions/render/composition/package.json +22 -0
  102. package/extensions/render/composition/render.mjs +246 -0
  103. package/extensions/render/composition/test-comp.tsx +87 -0
  104. package/extensions/render/composition/types.ts +24 -0
  105. package/extensions/render/excalidraw/UPSTREAM.md +81 -0
  106. package/extensions/render/excalidraw/elements.ts +764 -0
  107. package/extensions/render/excalidraw/index.ts +66 -0
  108. package/extensions/render/excalidraw/types.ts +223 -0
  109. package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
  110. package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
  111. package/extensions/render/excalidraw-renderer/render_template.html +59 -0
  112. package/extensions/render/index.ts +830 -0
  113. package/extensions/render/native-diagrams/index.ts +57 -0
  114. package/extensions/render/native-diagrams/motifs.ts +542 -0
  115. package/extensions/render/native-diagrams/raster.ts +8 -0
  116. package/extensions/render/native-diagrams/scene.ts +75 -0
  117. package/extensions/render/native-diagrams/spec.ts +204 -0
  118. package/extensions/render/native-diagrams/svg.ts +116 -0
  119. package/extensions/sci-ui.ts +304 -0
  120. package/extensions/session-log.ts +174 -0
  121. package/extensions/shared-state.ts +146 -0
  122. package/extensions/spinner-verbs.ts +91 -0
  123. package/extensions/style.ts +281 -0
  124. package/extensions/terminal-title.ts +191 -0
  125. package/extensions/tool-profile/index.ts +291 -0
  126. package/extensions/tool-profile/profiles.ts +290 -0
  127. package/extensions/types.d.ts +9 -0
  128. package/extensions/vault/index.ts +185 -0
  129. package/extensions/version-check.ts +90 -0
  130. package/extensions/view/index.ts +859 -0
  131. package/extensions/view/uri-resolver.ts +148 -0
  132. package/extensions/web-search/index.ts +182 -0
  133. package/extensions/web-search/providers.ts +121 -0
  134. package/extensions/web-ui/index.ts +110 -0
  135. package/extensions/web-ui/server.ts +265 -0
  136. package/extensions/web-ui/state.ts +462 -0
  137. package/extensions/web-ui/static/index.html +145 -0
  138. package/extensions/web-ui/types.ts +284 -0
  139. package/package.json +76 -0
  140. package/prompts/init.md +75 -0
  141. package/prompts/new-repo.md +54 -0
  142. package/prompts/oci-login.md +56 -0
  143. package/prompts/status.md +50 -0
  144. package/settings.json +4 -0
  145. package/skills/cleave/SKILL.md +218 -0
  146. package/skills/git/SKILL.md +209 -0
  147. package/skills/git/_reference/ci-validation.md +204 -0
  148. package/skills/oci/SKILL.md +338 -0
  149. package/skills/openspec/SKILL.md +346 -0
  150. package/skills/pi-extensions/SKILL.md +191 -0
  151. package/skills/pi-tui/SKILL.md +517 -0
  152. package/skills/python/SKILL.md +189 -0
  153. package/skills/rust/SKILL.md +268 -0
  154. package/skills/security/SKILL.md +206 -0
  155. package/skills/style/SKILL.md +264 -0
  156. package/skills/typescript/SKILL.md +225 -0
  157. package/skills/vault/SKILL.md +102 -0
  158. package/themes/alpharius-legacy.json +85 -0
  159. package/themes/alpharius.conf +59 -0
  160. package/themes/alpharius.json +88 -0
@@ -0,0 +1,365 @@
1
+ /**
2
+ * Dashboard interactive overlay (Layer 2).
3
+ *
4
+ * Right-anchored sidepanel with three tabs:
5
+ * [1] Design Tree — node list with status icons, expand to show questions
6
+ * [2] Implementation — change list with stage/progress
7
+ * [3] Cleave — dispatch children with status/elapsed
8
+ *
9
+ * Keyboard:
10
+ * Tab / 1-3 — switch tabs
11
+ * ↑/↓ — navigate items
12
+ * Enter/→ — expand/collapse item
13
+ * ← — collapse expanded item
14
+ * Esc / ctrl+c — close overlay
15
+ *
16
+ * Reads sharedState for all data. Subscribes to dashboard:update for live refresh.
17
+ */
18
+
19
+ import { spawn } from "node:child_process";
20
+ import type { ExtensionContext } from "@cwilson613/pi-coding-agent";
21
+ import type { Theme } from "@cwilson613/pi-coding-agent";
22
+ import type { TUI } from "@cwilson613/pi-tui";
23
+ import { matchesKey, truncateToWidth, visibleWidth } from "@cwilson613/pi-tui";
24
+ import { DASHBOARD_UPDATE_EVENT, sharedState } from "../shared-state.ts";
25
+ import {
26
+ TABS,
27
+ MAX_CONTENT_LINES,
28
+ rebuildItems,
29
+ clampIndex,
30
+ type TabId,
31
+ type ListItem,
32
+ } from "./overlay-data.ts";
33
+
34
+ /**
35
+ * Full-screen blocking overlay options for the /dashboard operator panel.
36
+ * No width/height restrictions — consumes the entire terminal.
37
+ */
38
+ export const INSPECTION_OVERLAY_OPTIONS = {
39
+ anchor: "center",
40
+ width: "100%",
41
+ minWidth: 60,
42
+ maxHeight: "100%",
43
+ margin: 0,
44
+ visible: (_termWidth: number) => true,
45
+ } as const;
46
+
47
+ // ── Overlay Component ───────────────────────────────────────────
48
+
49
+ export class DashboardOverlay {
50
+ private tui: TUI;
51
+ private theme: Theme;
52
+ private done: (result: void) => void;
53
+
54
+ private activeTab: TabId = "design";
55
+ private selectedIndex = 0;
56
+ private flatItems: ListItem[] = [];
57
+ private expandedKeys = new Set<string>();
58
+ private statusMessage: string | null = null;
59
+
60
+ /** Event unsubscribe handle for live refresh. */
61
+ private unsubscribe: (() => void) | null = null;
62
+ /** Interval handle for the 1-second elapsed ticker while children are running. */
63
+ private tickerInterval: ReturnType<typeof setInterval> | null = null;
64
+
65
+ constructor(tui: TUI, theme: Theme, done: (result: void) => void) {
66
+ this.tui = tui;
67
+ this.theme = theme;
68
+ this.done = done;
69
+ this.rebuild();
70
+ }
71
+
72
+ /** Attach to the pi event bus for live data refresh while overlay is open. */
73
+ setEventBus(events: { on(event: string, handler: (data: unknown) => void): () => void }): void {
74
+ this.unsubscribe = events.on(DASHBOARD_UPDATE_EVENT, () => {
75
+ this.rebuild();
76
+ this.syncTicker();
77
+ this.tui.requestRender();
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Start or stop the 1-second ticker based on whether any children are running.
83
+ * The ticker drives the live elapsed counter without needing a shared-state event.
84
+ */
85
+ private syncTicker(): void {
86
+ const cl = sharedState.cleave;
87
+ const anyRunning = cl?.children?.some((c: { status: string }) => c.status === "running") ?? false;
88
+
89
+ if (anyRunning && !this.tickerInterval) {
90
+ this.tickerInterval = setInterval(() => {
91
+ // Only re-render if still have running children
92
+ const cl2 = sharedState.cleave;
93
+ if (cl2?.children?.some((c: { status: string }) => c.status === "running")) {
94
+ this.tui.requestRender();
95
+ } else {
96
+ this.stopTicker();
97
+ }
98
+ }, 1_000);
99
+ } else if (!anyRunning) {
100
+ this.stopTicker();
101
+ }
102
+ }
103
+
104
+ private stopTicker(): void {
105
+ if (this.tickerInterval !== null) {
106
+ clearInterval(this.tickerInterval);
107
+ this.tickerInterval = null;
108
+ }
109
+ }
110
+
111
+ private selectFirstOpenableItem(): void {
112
+ const firstOpenable = this.flatItems.findIndex((item) => !!item.openUri);
113
+ if (firstOpenable >= 0) {
114
+ this.selectedIndex = firstOpenable;
115
+ }
116
+ }
117
+
118
+ private openSelectedItem(): void {
119
+ const item = this.flatItems[this.selectedIndex];
120
+ if (!item?.openUri) {
121
+ this.statusMessage = "Selected row has nothing to open";
122
+ this.tui.requestRender();
123
+ return;
124
+ }
125
+
126
+ this.statusMessage = "Opening selected item…";
127
+ this.tui.requestRender();
128
+
129
+ try {
130
+ if (process.platform === "darwin") {
131
+ spawn("open", [item.openUri], { stdio: "ignore", detached: true }).unref();
132
+ } else if (process.platform === "win32") {
133
+ spawn("cmd", ["/c", "start", "", item.openUri], { stdio: "ignore", detached: true }).unref();
134
+ } else {
135
+ spawn("xdg-open", [item.openUri], { stdio: "ignore", detached: true }).unref();
136
+ }
137
+ } catch {
138
+ this.statusMessage = "Open failed";
139
+ this.tui.requestRender();
140
+ // Best effort only; clickable OSC 8 links remain the primary path.
141
+ }
142
+ }
143
+
144
+ // ── Keyboard handling ───────────────────────────────────────────
145
+
146
+ handleInput(data: string): void {
147
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
148
+ this.done();
149
+ return;
150
+ }
151
+
152
+ // Tab switching
153
+ if (matchesKey(data, "tab")) {
154
+ const idx = TABS.findIndex((t) => t.id === this.activeTab);
155
+ this.activeTab = TABS[(idx + 1) % TABS.length]!.id;
156
+ this.selectedIndex = 0;
157
+ this.statusMessage = null;
158
+ this.rebuild();
159
+ this.selectFirstOpenableItem();
160
+ this.tui.requestRender();
161
+ return;
162
+ }
163
+
164
+ for (const tab of TABS) {
165
+ if (data === tab.shortcut) {
166
+ this.activeTab = tab.id;
167
+ this.selectedIndex = 0;
168
+ this.statusMessage = null;
169
+ this.rebuild();
170
+ this.selectFirstOpenableItem();
171
+ this.tui.requestRender();
172
+ return;
173
+ }
174
+ }
175
+
176
+ // Navigation — guard empty list
177
+ if (this.flatItems.length === 0) return;
178
+
179
+ if (matchesKey(data, "up")) {
180
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
181
+ this.statusMessage = null;
182
+ this.tui.requestRender();
183
+ return;
184
+ }
185
+ if (matchesKey(data, "down")) {
186
+ this.selectedIndex = Math.min(this.flatItems.length - 1, this.selectedIndex + 1);
187
+ this.statusMessage = null;
188
+ this.tui.requestRender();
189
+ return;
190
+ }
191
+
192
+ // Expand/collapse
193
+ if (matchesKey(data, "return") || matchesKey(data, "right")) {
194
+ const item = this.flatItems[this.selectedIndex];
195
+ if (item?.expandable) {
196
+ if (this.expandedKeys.has(item.key)) {
197
+ this.expandedKeys.delete(item.key);
198
+ } else {
199
+ this.expandedKeys.add(item.key);
200
+ }
201
+ this.statusMessage = null;
202
+ this.rebuild();
203
+ this.tui.requestRender();
204
+ } else if (item?.openUri) {
205
+ this.openSelectedItem();
206
+ }
207
+ return;
208
+ }
209
+
210
+ if (matchesKey(data, "left")) {
211
+ const item = this.flatItems[this.selectedIndex];
212
+ if (item && this.expandedKeys.has(item.key)) {
213
+ this.expandedKeys.delete(item.key);
214
+ this.statusMessage = null;
215
+ this.rebuild();
216
+ this.tui.requestRender();
217
+ }
218
+ return;
219
+ }
220
+
221
+ if (data === "o" || data === "O") {
222
+ this.openSelectedItem();
223
+ return;
224
+ }
225
+ }
226
+
227
+ // ── Rendering ─────────────────────────────────────────────────
228
+
229
+ render(width: number): string[] {
230
+ const th = this.theme;
231
+ const innerW = Math.max(1, width - 2);
232
+ const border = (c: string) => th.fg("border", c);
233
+ const pad = (s: string) => truncateToWidth(s, innerW, "…", true);
234
+
235
+ // Fixed chrome: top border (1) + tab bar (1) + separator (1) + footer separator (1) + 2 footer lines + bottom border (1) = 7
236
+
237
+ const lines: string[] = [];
238
+
239
+ // Top border with title
240
+ const title = " Dashboard ";
241
+ const titleW = visibleWidth(title);
242
+ const topLeft = "─".repeat(Math.floor((innerW - titleW) / 2));
243
+ const topRight = "─".repeat(Math.max(0, innerW - titleW - topLeft.length));
244
+ lines.push(border("╭" + topLeft) + th.fg("accent", title) + border(topRight + "╮"));
245
+
246
+ // Tab bar
247
+ const tabParts: string[] = [];
248
+ for (const tab of TABS) {
249
+ if (tab.id === this.activeTab) {
250
+ tabParts.push(th.fg("accent", `[${tab.shortcut}] ${tab.label}`));
251
+ } else {
252
+ tabParts.push(th.fg("dim", `[${tab.shortcut}] ${tab.label}`));
253
+ }
254
+ }
255
+ lines.push(border("│") + pad(" " + tabParts.join(" ")) + border("│"));
256
+ lines.push(border("├" + "─".repeat(innerW) + "┤"));
257
+
258
+ // Content area — always fill MAX_CONTENT_LINES and rely on maxHeight:"100%"
259
+ // in the overlay options to clip to actual terminal height. This avoids
260
+ // stale process.stdout.rows readings that cause a short overlay.
261
+ const contentLines = this.renderContent(innerW).slice(0, MAX_CONTENT_LINES);
262
+ if (contentLines.length === 0) {
263
+ lines.push(border("│") + pad(th.fg("dim", " (no data)")) + border("│"));
264
+ for (let i = 1; i < MAX_CONTENT_LINES; i++) {
265
+ lines.push(border("│") + pad("") + border("│"));
266
+ }
267
+ } else {
268
+ for (const cl of contentLines) {
269
+ lines.push(border("│") + pad(cl) + border("│"));
270
+ }
271
+ // Pad to fill MAX_CONTENT_LINES; maxHeight:"100%" clips to terminal height
272
+ for (let i = contentLines.length; i < MAX_CONTENT_LINES; i++) {
273
+ lines.push(border("│") + pad("") + border("│"));
274
+ }
275
+ }
276
+
277
+ // Footer with key hints
278
+ lines.push(border("├" + "─".repeat(innerW) + "┤"));
279
+ const footerPrimary = this.statusMessage
280
+ ? th.fg("warning", ` ${this.statusMessage}`)
281
+ : th.fg("dim", " ↵/o open selected item ↑↓ navigate ←→ expand/collapse");
282
+ lines.push(border("│") + pad(footerPrimary) + border("│"));
283
+ lines.push(border("│") + pad(th.fg("dim", " Tab switch Esc close items with ↗ are openable")) + border("│"));
284
+ lines.push(border("╰" + "─".repeat(innerW) + "╯"));
285
+
286
+ return lines;
287
+ }
288
+
289
+ private renderContent(innerW: number): string[] {
290
+ const th = this.theme;
291
+ const thFn = (color: string, text: string) => th.fg(color as any, text);
292
+ const lines: string[] = [];
293
+
294
+ for (let i = 0; i < this.flatItems.length; i++) {
295
+ const item = this.flatItems[i]!;
296
+ const isSelected = i === this.selectedIndex;
297
+ const indent = " ".repeat(item.depth);
298
+ const cursor = isSelected ? th.fg("accent", "→ ") : " ";
299
+
300
+ // Expand indicator
301
+ let expandIcon = " ";
302
+ if (item.expandable) {
303
+ expandIcon = this.expandedKeys.has(item.key)
304
+ ? th.fg("dim", "▾ ")
305
+ : th.fg("dim", "▸ ");
306
+ }
307
+
308
+ const contentWidth = Math.max(1, innerW - 4 - item.depth * 2);
309
+ const itemLines = item.lines(thFn, contentWidth);
310
+ const openMarker = item.openUri ? th.fg("accent", "↗ ") : "";
311
+ if (itemLines.length > 0) {
312
+ lines.push(`${cursor}${indent}${expandIcon}${openMarker}${truncateToWidth(itemLines[0], contentWidth, "…")}`);
313
+ for (let j = 1; j < itemLines.length; j++) {
314
+ lines.push(` ${indent} ${truncateToWidth(itemLines[j], contentWidth, "…")}`);
315
+ }
316
+ }
317
+ }
318
+
319
+ return lines;
320
+ }
321
+
322
+ // ── State ─────────────────────────────────────────────────────
323
+
324
+ private rebuild(): void {
325
+ this.flatItems = rebuildItems(this.activeTab, this.expandedKeys);
326
+ this.selectedIndex = clampIndex(this.selectedIndex, this.flatItems.length);
327
+ if (this.flatItems.length > 0 && !this.flatItems[this.selectedIndex]?.openUri && this.selectedIndex === 0) {
328
+ this.selectFirstOpenableItem();
329
+ }
330
+ }
331
+
332
+ // ── Component lifecycle ───────────────────────────────────────
333
+
334
+ invalidate(): void {}
335
+
336
+ dispose(): void {
337
+ if (this.unsubscribe) {
338
+ this.unsubscribe();
339
+ this.unsubscribe = null;
340
+ }
341
+ this.stopTicker();
342
+ }
343
+ }
344
+
345
+ // ── Public API ──────────────────────────────────────────────────
346
+
347
+ /**
348
+ * Show the dashboard overlay as a right-anchored sidepanel.
349
+ * Blocks until the user presses Esc.
350
+ */
351
+ export async function showDashboardOverlay(ctx: ExtensionContext, pi?: { events: { on(e: string, h: (data: unknown) => void): () => void } }): Promise<void> {
352
+ await ctx.ui.custom<void>(
353
+ (tui, theme, _kb, done) => {
354
+ const overlay = new DashboardOverlay(tui, theme, done);
355
+ if (pi?.events) {
356
+ overlay.setEventBus(pi.events);
357
+ }
358
+ return overlay;
359
+ },
360
+ {
361
+ overlay: true,
362
+ overlayOptions: INSPECTION_OVERLAY_OPTIONS,
363
+ },
364
+ );
365
+ }
@@ -0,0 +1,54 @@
1
+ import { truncateToWidth, visibleWidth } from "@cwilson613/pi-tui";
2
+
3
+ /**
4
+ * Pad string `s` to exactly `width` visible columns using visibleWidth().
5
+ * If `s` is already at or wider than `width`, returns `s` unchanged.
6
+ */
7
+ export function padRight(s: string, width: number): string {
8
+ const vw = visibleWidth(s);
9
+ if (vw >= width) return s;
10
+ return s + " ".repeat(width - vw);
11
+ }
12
+
13
+ /**
14
+ * Render a line with `left` flush-left and `right` flush-right within `width`.
15
+ * Falls back to truncating `left` to fit if both sides don't fit together.
16
+ */
17
+ export function leftRight(left: string, right: string, width: number): string {
18
+ const lw = visibleWidth(left);
19
+ const rw = visibleWidth(right);
20
+ const gap = width - lw - rw;
21
+ if (gap >= 0) {
22
+ return left + " ".repeat(gap) + right;
23
+ }
24
+ // Not enough space — truncate left to fit right
25
+ const leftWidth = Math.max(0, width - rw);
26
+ if (leftWidth === 0) return right;
27
+ return truncateToWidth(left, leftWidth, "…") + right;
28
+ }
29
+
30
+ /**
31
+ * Merge two column arrays side-by-side using padRight + truncateToWidth.
32
+ * Row count = Math.max(leftLines.length, rightLines.length).
33
+ * Each output row has visibleWidth === leftWidth + divider.length + rightWidth.
34
+ */
35
+ export function mergeColumns(
36
+ leftLines: string[],
37
+ rightLines: string[],
38
+ leftWidth: number,
39
+ rightWidth: number,
40
+ divider = "│"
41
+ ): string[] {
42
+ const rows = Math.max(leftLines.length, rightLines.length);
43
+ const result: string[] = [];
44
+ for (let i = 0; i < rows; i++) {
45
+ const left = i < leftLines.length
46
+ ? padRight(truncateToWidth(leftLines[i], leftWidth, "…"), leftWidth)
47
+ : " ".repeat(leftWidth);
48
+ const right = i < rightLines.length
49
+ ? truncateToWidth(rightLines[i], rightWidth, "…")
50
+ : "";
51
+ result.push(left + divider + padRight(right, rightWidth));
52
+ }
53
+ return result;
54
+ }
@@ -0,0 +1,191 @@
1
+ /**
2
+ * Dashboard type definitions.
3
+ *
4
+ * These interfaces define the shape of dashboard state
5
+ * written by producer extensions (design-tree, openspec, cleave)
6
+ * and read by the dashboard extension for rendering.
7
+ *
8
+ * Re-exported from shared-state.ts for convenience.
9
+ */
10
+
11
+ // ── Design Tree ──────────────────────────────────────────────
12
+
13
+ /** Mirrors DesignSpecBinding from openspec/archive-gate.ts for dashboard consumers. */
14
+ export interface DesignSpecBindingState {
15
+ active: boolean;
16
+ archived: boolean;
17
+ missing: boolean;
18
+ }
19
+
20
+ /** Acceptance-criteria counts derived from a design node's AC section. */
21
+ export interface AcSummary {
22
+ scenarios: number;
23
+ falsifiability: number;
24
+ constraints: number;
25
+ }
26
+
27
+ /** Outcome of an /assess spec run persisted in openspec/design/<id>/assessment.json. */
28
+ export interface DesignAssessmentResult {
29
+ outcome: "pass" | "reopen" | "ambiguous";
30
+ timestamp: string;
31
+ summary?: string;
32
+ }
33
+
34
+ /** Per-status counts across the design pipeline for funnel rendering. */
35
+ export interface DesignPipelineCounts {
36
+ /** seed/exploring nodes without a spec binding */
37
+ needsSpec: number;
38
+ /** seed/exploring nodes with an active or archived spec binding */
39
+ designing: number;
40
+ /** nodes in 'decided' status */
41
+ decided: number;
42
+ /** nodes in 'implementing' status */
43
+ implementing: number;
44
+ /** nodes in 'implemented' status */
45
+ done: number;
46
+ }
47
+
48
+ export interface DesignTreeFocusedNode {
49
+ id: string;
50
+ title: string;
51
+ status: string;
52
+ questions: string[];
53
+ branch?: string;
54
+ branchCount?: number;
55
+ filePath?: string;
56
+ }
57
+
58
+ export interface DesignTreeDashboardState {
59
+ nodeCount: number;
60
+ decidedCount: number;
61
+ exploringCount: number;
62
+ implementingCount: number;
63
+ implementedCount: number;
64
+ blockedCount: number;
65
+ deferredCount: number;
66
+ openQuestionCount: number;
67
+ focusedNode: DesignTreeFocusedNode | null;
68
+ /** All nodes for overlay list view */
69
+ nodes?: Array<{
70
+ id: string;
71
+ title: string;
72
+ status: string;
73
+ questionCount: number;
74
+ filePath?: string;
75
+ branches?: string[];
76
+ /** OpenSpec design-phase binding state (undefined for seed nodes) */
77
+ designSpec?: DesignSpecBindingState;
78
+ /** Acceptance-criteria counts (undefined if no AC section) */
79
+ acSummary?: AcSummary | null;
80
+ /** Last /assess spec result (undefined if no assessment.json) */
81
+ assessmentResult?: DesignAssessmentResult | null;
82
+ /** Name of linked openspec implementation change (set by design-tree on emit) */
83
+ openspecChange?: string | null;
84
+ }>;
85
+ /** Implementing nodes shown in raised mode with branch associations */
86
+ implementingNodes?: Array<{ id: string; title: string; branch?: string; filePath?: string }>;
87
+ /** Design pipeline funnel counts */
88
+ designPipeline?: DesignPipelineCounts;
89
+ }
90
+
91
+ // ── OpenSpec ─────────────────────────────────────────────────
92
+
93
+ export interface OpenSpecChangeEntry {
94
+ name: string;
95
+ stage: string;
96
+ tasksDone: number;
97
+ tasksTotal: number;
98
+ /** Which lifecycle artifacts exist */
99
+ artifacts?: ("proposal" | "design" | "specs" | "tasks")[];
100
+ /** Spec domain names (e.g. ["auth", "api/tokens"]) */
101
+ specDomains?: string[];
102
+ /** Absolute path to the change directory */
103
+ path?: string;
104
+ }
105
+
106
+ export interface OpenSpecDashboardState {
107
+ changes: OpenSpecChangeEntry[];
108
+ }
109
+
110
+ // ── Cleave ───────────────────────────────────────────────────
111
+
112
+ export type CleaveStatus =
113
+ | "idle"
114
+ | "assessing"
115
+ | "planning"
116
+ | "dispatching"
117
+ | "merging"
118
+ | "done"
119
+ | "failed";
120
+
121
+ export interface CleaveChildState {
122
+ label: string;
123
+ status: "pending" | "running" | "done" | "failed";
124
+ elapsed?: number;
125
+ /** Epoch ms when the child transitioned to "running". Used for live elapsed calculation. */
126
+ startedAt?: number;
127
+ /** Ring buffer of the last 30 meaningful stdout lines from the child process (filtered). */
128
+ recentLines?: string[];
129
+ /** Worktree path for this child — used to run git diff on demand. */
130
+ worktreePath?: string;
131
+ /** Last meaningful stdout line — kept in sync with recentLines tail for backward compat. */
132
+ lastLine?: string;
133
+ }
134
+
135
+ export interface CleaveState {
136
+ status: CleaveStatus;
137
+ runId?: string;
138
+ children?: CleaveChildState[];
139
+ /** Unix epoch ms of the last cleave dashboard update */
140
+ updatedAt?: number;
141
+ }
142
+
143
+ // ── Harness Recovery ─────────────────────────────────────────
144
+
145
+ export type RecoveryAction =
146
+ | "retry"
147
+ | "switch_candidate"
148
+ | "switch_offline"
149
+ | "cooldown"
150
+ | "escalate"
151
+ | "observe";
152
+
153
+ export interface RecoveryTarget {
154
+ provider: string;
155
+ modelId?: string;
156
+ label?: string;
157
+ }
158
+
159
+ export interface RecoveryCooldownSummary {
160
+ scope: "provider" | "candidate";
161
+ key: string;
162
+ provider?: string;
163
+ modelId?: string;
164
+ until: number;
165
+ reason?: string;
166
+ }
167
+
168
+ export interface RecoveryDashboardState {
169
+ provider: string;
170
+ modelId: string;
171
+ classification: string;
172
+ summary: string;
173
+ action: RecoveryAction;
174
+ retryCount?: number;
175
+ maxRetries?: number;
176
+ attemptId?: string;
177
+ timestamp: number;
178
+ escalated?: boolean;
179
+ target?: RecoveryTarget;
180
+ cooldowns?: RecoveryCooldownSummary[];
181
+ }
182
+
183
+ // ── Dashboard UI ─────────────────────────────────────────────
184
+
185
+ export type DashboardMode = "compact" | "raised" | "panel" | "focused";
186
+
187
+ /** Mutable state held by the dashboard extension, read by the footer component. */
188
+ export interface DashboardState {
189
+ mode: DashboardMode;
190
+ turns: number;
191
+ }
@@ -0,0 +1,45 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import { getMdservePort } from "../vault/index.ts";
5
+ import { loadConfig, osc8Link, resolveUri } from "../view/uri-resolver.ts";
6
+
7
+ export type OpenSpecArtifact = "proposal" | "design" | "tasks";
8
+
9
+ function resolveDashboardUri(absPath?: string): string | undefined {
10
+ if (!absPath) return undefined;
11
+ return resolveUri(absPath, {
12
+ mdservePort: getMdservePort() ?? undefined,
13
+ config: loadConfig(),
14
+ projectRoot: process.cwd(),
15
+ });
16
+ }
17
+
18
+ function linkText(text: string, absPath?: string): string {
19
+ const uri = resolveDashboardUri(absPath);
20
+ return uri ? osc8Link(uri, text) : text;
21
+ }
22
+
23
+ export function getDashboardFileUri(absPath?: string): string | undefined {
24
+ return resolveDashboardUri(absPath);
25
+ }
26
+
27
+ export function linkDashboardFile(text: string, absPath?: string): string {
28
+ return linkText(text, absPath);
29
+ }
30
+
31
+ export function getOpenSpecArtifactUri(changePath: string | undefined, artifact: OpenSpecArtifact): string | undefined {
32
+ if (!changePath) return undefined;
33
+ const artifactPath = join(changePath, `${artifact}.md`);
34
+ if (!existsSync(artifactPath)) return undefined;
35
+ return resolveDashboardUri(artifactPath);
36
+ }
37
+
38
+ export function linkOpenSpecArtifact(text: string, changePath: string | undefined, artifact: OpenSpecArtifact): string {
39
+ const uri = getOpenSpecArtifactUri(changePath, artifact);
40
+ return uri ? osc8Link(uri, text) : text;
41
+ }
42
+
43
+ export function linkOpenSpecChange(text: string, changePath?: string): string {
44
+ return linkOpenSpecArtifact(text, changePath, "proposal");
45
+ }