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,185 @@
1
+ /**
2
+ * Git utilities for the dashboard extension.
3
+ *
4
+ * Reads local branches from .git/refs/heads/ without shell spawning,
5
+ * and renders a unicode branch tree for the raised layout.
6
+ */
7
+
8
+ import * as fs from "node:fs";
9
+ import * as path from "node:path";
10
+ import { visibleWidth } from "@cwilson613/pi-tui";
11
+ import type { Theme } from "@cwilson613/pi-coding-agent";
12
+
13
+ // Shared ASCII-compat flag — same logic as footer.ts
14
+ const useAscii = (() => {
15
+ if (process.env["PI_ASCII"] === "1") return true;
16
+ if (process.env["TERM"] === "dumb") return true;
17
+ const locale = (process.env["LC_ALL"] ?? process.env["LC_CTYPE"] ?? process.env["LANG"] ?? "").toUpperCase();
18
+ if (locale && !locale.includes("UTF")) return true;
19
+ return false;
20
+ })();
21
+
22
+ const T = useAscii
23
+ ? { single: "---", fork: "-+-", mid: "+-", last: "+-", ann: "# " }
24
+ : { single: " ─── ", fork: " ─┬─ ", mid: "├─ ", last: "└─ ", ann: " ◈ " };
25
+
26
+ // ── Branch reader ──────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Recursively collect branch names from a directory, returning
30
+ * slash-joined paths relative to the base directory.
31
+ */
32
+ function collectRefs(dir: string, base: string): string[] {
33
+ let results: string[] = [];
34
+ let entries: fs.Dirent[];
35
+ try {
36
+ entries = fs.readdirSync(dir, { withFileTypes: true });
37
+ } catch {
38
+ return [];
39
+ }
40
+ for (const entry of entries) {
41
+ const fullPath = path.join(dir, entry.name);
42
+ if (entry.isDirectory()) {
43
+ const sub = collectRefs(fullPath, base);
44
+ results = results.concat(sub);
45
+ } else if (entry.isFile()) {
46
+ const rel = path.relative(base, fullPath).split(path.sep).join("/");
47
+ // Exclude HEAD and any name with illegal ref chars
48
+ // Exclude HEAD and any name with illegal ref chars (spaces, control chars, ~^:?*\[)
49
+ if (rel !== "HEAD" && !/[\x00-\x20\x7f ~^:?*[\\]/.test(rel)) {
50
+ results.push(rel);
51
+ }
52
+ }
53
+ }
54
+ return results;
55
+ }
56
+
57
+ /**
58
+ * Sort priority for branch names.
59
+ * Lower number = earlier in list.
60
+ */
61
+ function branchPriority(b: string): number {
62
+ if (b === "main" || b === "master") return 0;
63
+ if (b.startsWith("feature/")) return 1;
64
+ if (b.startsWith("refactor/")) return 2;
65
+ if (b.startsWith("fix/") || b.startsWith("hotfix/")) return 3;
66
+ return 4;
67
+ }
68
+
69
+ /**
70
+ * Read local branches from .git/refs/heads/ without spawning a shell.
71
+ *
72
+ * Returns branch names sorted: main/master first, then feature/*, refactor/*,
73
+ * fix/hotfix, then the rest alphabetically.
74
+ * Returns [] gracefully if the directory does not exist (detached HEAD, worktree, etc.).
75
+ */
76
+ export function readLocalBranches(cwd: string): string[] {
77
+ const headsDir = path.join(cwd, ".git", "refs", "heads");
78
+ const branches = collectRefs(headsDir, headsDir);
79
+ branches.sort((a, b) => {
80
+ const pa = branchPriority(a);
81
+ const pb = branchPriority(b);
82
+ if (pa !== pb) return pa - pb;
83
+ return a.localeCompare(b);
84
+ });
85
+ return branches;
86
+ }
87
+
88
+ // ── Branch tree renderer ───────────────────────────────────────────────────────
89
+
90
+ export interface BranchTreeParams {
91
+ repoName: string;
92
+ currentBranch: string | null;
93
+ allBranches: string[];
94
+ designNodes?: Array<{ branches?: string[]; title: string }>;
95
+ }
96
+
97
+ /**
98
+ * Style a branch name according to its type and whether it is current.
99
+ */
100
+ function styledBranch(b: string, isCurrent: boolean, theme: Theme): string {
101
+ // Use ASCII "*" rather than "●" (U+25CF): the Black Circle glyph is
102
+ // "ambiguous width" in Unicode East Asian metrics and many terminals
103
+ // (e.g. iTerm2 on macOS) render it as 2 cells. pi-tui's visibleWidth()
104
+ // counts it as 1, causing a 1-char overflow in the top-border of the
105
+ // raised dashboard box and a TUI crash at exactly-full-width terminals.
106
+ const label = isCurrent ? "* " + b : b;
107
+ if (isCurrent) return theme.fg("success", label);
108
+ if (b.startsWith("feature/")) return theme.fg("accent", b);
109
+ if (b.startsWith("fix/") || b.startsWith("hotfix/")) return theme.fg("warning", b);
110
+ if (b.startsWith("refactor/")) return theme.fg("accent", b); // dim accent via same color
111
+ return theme.fg("muted", b);
112
+ }
113
+
114
+ /**
115
+ * Find annotation for a branch from design nodes.
116
+ */
117
+ function branchAnnotation(
118
+ b: string,
119
+ designNodes: Array<{ branches?: string[]; title: string }> | undefined,
120
+ theme: Theme
121
+ ): string {
122
+ if (!designNodes) return "";
123
+ const node = designNodes.find((n) => n.branches?.includes(b));
124
+ if (!node) return "";
125
+ return " " + theme.fg("dim", T.ann + node.title);
126
+ }
127
+
128
+ /**
129
+ * Build the branch tree lines for the raised layout.
130
+ *
131
+ * - 0 branches: [dim(repoName)]
132
+ * - 1 branch: repoName + " ─── " + styledBranch
133
+ * - N branches: repoName + " ─┬─ " + styledBranch(branches[0])
134
+ * indent + "├─ " + styledBranch(branches[i]) (middle)
135
+ * indent + "└─ " + styledBranch(branches[N-1]) (last)
136
+ *
137
+ * Current branch is placed first; deduplication ensures it appears only once.
138
+ */
139
+ export function buildBranchTreeLines(params: BranchTreeParams, theme: Theme): string[] {
140
+ const { repoName, currentBranch, allBranches, designNodes } = params;
141
+
142
+ // Build ordered, deduplicated branch list: current first
143
+ const ordered: string[] = [];
144
+ if (currentBranch) {
145
+ ordered.push(currentBranch);
146
+ }
147
+ for (const b of allBranches) {
148
+ if (!ordered.includes(b)) {
149
+ ordered.push(b);
150
+ }
151
+ }
152
+
153
+ if (ordered.length === 0) {
154
+ return [theme.fg("dim", repoName)];
155
+ }
156
+
157
+ if (ordered.length === 1) {
158
+ const b = ordered[0]!;
159
+ const isCurrent = b === currentBranch;
160
+ const annotation = branchAnnotation(b, designNodes, theme);
161
+ return [repoName + T.single + styledBranch(b, isCurrent, theme) + annotation];
162
+ }
163
+
164
+ // Multiple branches — indent aligned to just after the fork connector
165
+ const indentWidth = visibleWidth(repoName + (useAscii ? "-" : " ─"));
166
+ const indent = " ".repeat(indentWidth);
167
+
168
+ const lines: string[] = [];
169
+ for (let i = 0; i < ordered.length; i++) {
170
+ const b = ordered[i]!;
171
+ const isCurrent = b === currentBranch;
172
+ const styled = styledBranch(b, isCurrent, theme);
173
+ const annotation = branchAnnotation(b, designNodes, theme);
174
+
175
+ if (i === 0) {
176
+ lines.push(repoName + T.fork + styled + annotation);
177
+ } else if (i < ordered.length - 1) {
178
+ lines.push(indent + T.mid + styled + annotation);
179
+ } else {
180
+ lines.push(indent + T.last + styled + annotation);
181
+ }
182
+ }
183
+
184
+ return lines;
185
+ }
@@ -0,0 +1,478 @@
1
+ /**
2
+ * dashboard — Unified live dashboard for Design Tree + OpenSpec + Cleave
3
+ *
4
+ * Renders a custom footer via setFooter() that supports modes:
5
+ * compact: Dashboard summary + context gauge + original footer data
6
+ * raised: Section details for design tree, openspec, cleave + footer data
7
+ * panel: Non-capturing overlay (visible but doesn't steal input)
8
+ * focused: Interactive overlay with keyboard navigation
9
+ *
10
+ * Toggle: ctrl+` or /dashboard command.
11
+ * Cycle: compact → raised → panel → focused → compact
12
+ *
13
+ * Reads sharedState written by producer extensions (design-tree, openspec, cleave).
14
+ * Subscribes to "dashboard:update" events for live re-rendering.
15
+ */
16
+
17
+ import type { ExtensionAPI, ExtensionContext } from "@cwilson613/pi-coding-agent";
18
+ import type { OverlayHandle } from "@cwilson613/pi-tui";
19
+ import { DASHBOARD_UPDATE_EVENT } from "../shared-state.ts";
20
+ import { getSharedBridge, buildSlashCommandResult } from "../lib/slash-command-bridge.ts";
21
+ import { DashboardFooter } from "./footer.ts";
22
+ import { DashboardOverlay, showDashboardOverlay } from "./overlay.ts";
23
+ import type { DashboardState, DashboardMode } from "./types.ts";
24
+ import { debug } from "../debug.ts";
25
+
26
+ /** Valid /dashboard subcommands for tab completion (legacy) */
27
+ const DASHBOARD_SUBCOMMANDS = ["compact", "raised", "panel", "focus", "open"];
28
+
29
+ export default function (pi: ExtensionAPI) {
30
+ const state: DashboardState = {
31
+ mode: "compact",
32
+ turns: 0,
33
+ };
34
+
35
+ let footer: DashboardFooter | null = null;
36
+ let tui: any = null; // TUI reference for requestRender
37
+ let unsubscribeEvents: (() => void) | null = null;
38
+
39
+ // ── Non-capturing overlay state ─────────────────────────────
40
+ /** Overlay handle for non-capturing panel (visibility + focus control) */
41
+ let overlayHandle: OverlayHandle | null = null;
42
+ /** The done() callback to resolve the custom() promise on permanent close */
43
+ let overlayDone: ((result: void) => void) | null = null;
44
+ /** Whether the non-capturing overlay has been created this session */
45
+ let overlayCreated = false;
46
+ /** Whether focus should be applied once the handle arrives (handles async creation) */
47
+ let pendingFocus = false;
48
+ /** True while the agent is actively streaming — blocks focused overlay to prevent input lockup */
49
+ let agentRunning = false;
50
+
51
+ /**
52
+ * Restore persisted dashboard mode from session entries.
53
+ * Panel/focused modes restore to raised (overlay is session-transient).
54
+ */
55
+ function restoreMode(ctx: ExtensionContext): void {
56
+ try {
57
+ const entries = ctx.sessionManager.getEntries();
58
+ for (let i = entries.length - 1; i >= 0; i--) {
59
+ const entry = entries[i] as any;
60
+ if (entry.type === "dashboard-state" && entry.data?.mode) {
61
+ const saved = entry.data.mode as DashboardMode;
62
+ // Overlay modes don't persist — fall back to raised
63
+ state.mode = (saved === "panel" || saved === "focused") ? "raised" : saved;
64
+ return;
65
+ }
66
+ }
67
+ } catch { /* first session, no entries yet */ }
68
+ }
69
+
70
+ /**
71
+ * Persist the current mode to the session.
72
+ */
73
+ function persistMode(_ctx: ExtensionContext): void {
74
+ try {
75
+ // Persist the base mode (panel/focused stored as raised)
76
+ const persistable = (state.mode === "panel" || state.mode === "focused") ? "raised" : state.mode;
77
+ pi.appendEntry("dashboard-state", { mode: persistable });
78
+ } catch { /* session may not support it */ }
79
+ }
80
+
81
+ /**
82
+ * Update footer context and trigger re-render.
83
+ */
84
+ function refresh(ctx: ExtensionContext): void {
85
+ debug("dashboard", "refresh", {
86
+ hasFooter: !!footer,
87
+ hasTui: !!tui,
88
+ footerType: footer?.constructor?.name,
89
+ });
90
+ if (footer) {
91
+ footer.setContext(ctx);
92
+ }
93
+ tui?.requestRender();
94
+ }
95
+
96
+ /**
97
+ * Show the non-capturing overlay panel.
98
+ * Creates it on first call, then toggles visibility via setHidden.
99
+ */
100
+ function showPanel(ctx: ExtensionContext): void {
101
+ if (overlayHandle && !overlayHandle.isHidden()) {
102
+ // Already visible — nothing to do
103
+ return;
104
+ }
105
+
106
+ if (overlayHandle) {
107
+ // Was hidden — show it
108
+ overlayHandle.setHidden(false);
109
+ tui?.requestRender();
110
+ return;
111
+ }
112
+
113
+ if (overlayCreated) {
114
+ // Overlay was created but handle hasn't arrived yet (async), or
115
+ // was permanently destroyed — don't recreate in same session
116
+ return;
117
+ }
118
+
119
+ // Create the non-capturing overlay (fire-and-forget — don't await)
120
+ overlayCreated = true;
121
+ void ctx.ui.custom<void>(
122
+ (tuiRef, theme, _kb, done) => {
123
+ overlayDone = done;
124
+ const overlay = new DashboardOverlay(tuiRef, theme, () => {
125
+ // Esc → close the panel entirely
126
+ hidePanel();
127
+ });
128
+ overlay.setEventBus(pi.events);
129
+ return overlay;
130
+ },
131
+ {
132
+ overlay: true,
133
+ overlayOptions: {
134
+ anchor: "right-center",
135
+ width: "42%",
136
+ minWidth: 42,
137
+ maxHeight: "80%",
138
+ margin: { top: 1, right: 1, bottom: 1 },
139
+ visible: (termWidth: number) => termWidth >= 80,
140
+ nonCapturing: true,
141
+ },
142
+ onHandle: (handle) => {
143
+ overlayHandle = handle;
144
+ // Apply deferred focus if cycleTo("focused") requested it before handle arrived
145
+ if (pendingFocus) {
146
+ pendingFocus = false;
147
+ handle.focus();
148
+ }
149
+ },
150
+ },
151
+ );
152
+ }
153
+
154
+ /**
155
+ * Hide the non-capturing overlay without destroying it.
156
+ */
157
+ function hidePanel(): void {
158
+ pendingFocus = false;
159
+ if (overlayHandle) {
160
+ if (overlayHandle.isFocused()) {
161
+ overlayHandle.unfocus();
162
+ }
163
+ overlayHandle.setHidden(true);
164
+ }
165
+ state.mode = "compact";
166
+ tui?.requestRender();
167
+ }
168
+
169
+ /**
170
+ * Focus the non-capturing overlay for interactive keyboard navigation.
171
+ * Blocked while the agent is streaming — focusing during active output
172
+ * causes the TUI input loop to deadlock with the render loop.
173
+ */
174
+ function focusPanel(): void {
175
+ if (agentRunning) {
176
+ // Can't safely capture input while agent is streaming — stay as panel
177
+ state.mode = "panel";
178
+ return;
179
+ }
180
+ if (overlayHandle && !overlayHandle.isHidden()) {
181
+ overlayHandle.focus();
182
+ } else {
183
+ // Handle not yet available — defer until onHandle fires
184
+ pendingFocus = true;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Cycle to a specific dashboard mode.
190
+ */
191
+ function cycleTo(ctx: ExtensionContext, targetMode: DashboardMode): void {
192
+ state.mode = targetMode;
193
+
194
+ switch (targetMode) {
195
+ case "compact":
196
+ case "raised":
197
+ hidePanel();
198
+ // hidePanel sets mode to "compact"; override for "raised"
199
+ state.mode = targetMode;
200
+ break;
201
+ case "panel":
202
+ pendingFocus = false;
203
+ showPanel(ctx);
204
+ break;
205
+ case "focused":
206
+ showPanel(ctx);
207
+ focusPanel();
208
+ break;
209
+ }
210
+
211
+ persistMode(ctx);
212
+ tui?.requestRender();
213
+ }
214
+
215
+ /**
216
+ * Toggle between compact and raised (2-state /dash toggle).
217
+ * Panel modes are closed first and footer returns to compact.
218
+ */
219
+ function dashToggle(ctx: ExtensionContext): void {
220
+ // If panel is open, close it first and go to compact
221
+ if (state.mode === "panel" || state.mode === "focused") {
222
+ hidePanel();
223
+ return;
224
+ }
225
+ // 2-state toggle: compact ↔ raised
226
+ const next = state.mode === "raised" ? "compact" : "raised";
227
+ cycleTo(ctx, next);
228
+ }
229
+
230
+ /**
231
+ * Toggle panel on/off. Panel and raised footer are mutually exclusive:
232
+ * opening the panel collapses the footer to compact and focuses the overlay.
233
+ */
234
+ function panelToggle(ctx: ExtensionContext): void {
235
+ if (state.mode === "panel" || state.mode === "focused") {
236
+ hidePanel();
237
+ } else {
238
+ // Opening panel forces compact footer and focuses overlay for key input
239
+ state.mode = "compact";
240
+ cycleTo(ctx, "focused");
241
+ }
242
+ }
243
+
244
+ // ── Session start: set up the custom footer ──────────────────
245
+
246
+ pi.on("session_start", async (_event, ctx) => {
247
+ debug("dashboard", "session_start:enter", {
248
+ hasUI: ctx.hasUI,
249
+ cwd: ctx.cwd,
250
+ hasSetFooter: typeof ctx.ui?.setFooter === "function",
251
+ });
252
+ if (!ctx.hasUI) {
253
+ debug("dashboard", "session_start:bail", { reason: "no UI" });
254
+ return;
255
+ }
256
+
257
+ state.turns = 0;
258
+ overlayHandle = null;
259
+ overlayDone = null;
260
+ overlayCreated = false;
261
+ pendingFocus = false;
262
+ restoreMode(ctx);
263
+ debug("dashboard", "session_start:mode", { mode: state.mode });
264
+
265
+ // Set the custom footer
266
+ try {
267
+ ctx.ui.setFooter((tuiRef, theme, footerData) => {
268
+ debug("dashboard", "footer:factory:enter", {
269
+ hasTui: !!tuiRef,
270
+ hasTheme: !!theme,
271
+ hasFooterData: !!footerData,
272
+ themeFgType: typeof theme?.fg,
273
+ });
274
+ try {
275
+ tui = tuiRef;
276
+ footer = new DashboardFooter(tuiRef, theme, footerData, state);
277
+ footer.setContext(ctx);
278
+ debug("dashboard", "footer:factory:ok", {
279
+ footerType: footer?.constructor?.name,
280
+ hasRender: typeof footer?.render === "function",
281
+ });
282
+ return footer;
283
+ } catch (factoryErr: any) {
284
+ debug("dashboard", "footer:factory:ERROR", {
285
+ error: factoryErr?.message,
286
+ stack: factoryErr?.stack?.split("\n").slice(0, 5).join(" | "),
287
+ });
288
+ throw factoryErr;
289
+ }
290
+ });
291
+ debug("dashboard", "session_start:setFooter:ok");
292
+ } catch (err: any) {
293
+ debug("dashboard", "session_start:setFooter:ERROR", {
294
+ error: err?.message,
295
+ stack: err?.stack?.split("\n").slice(0, 5).join(" | "),
296
+ });
297
+ }
298
+
299
+ // Subscribe to dashboard:update events from producer extensions.
300
+ unsubscribeEvents = pi.events.on(DASHBOARD_UPDATE_EVENT, (_data) => {
301
+ debug("dashboard", "update-event", _data as Record<string, unknown>);
302
+ tui?.requestRender();
303
+ });
304
+
305
+ // Deferred initial render
306
+ queueMicrotask(() => {
307
+ debug("dashboard", "microtask:render", {
308
+ tuiSet: !!tui,
309
+ footerSet: !!footer,
310
+ footerType: footer?.constructor?.name,
311
+ });
312
+ tui?.requestRender();
313
+ });
314
+
315
+ // Non-blocking capability health check — probes Omegon's own runtime deps
316
+ // (ollama, d2, pandoc, etc.) using the bootstrap DEPS registry.
317
+ // This is NOT a project linter — it tells the user which Omegon features
318
+ // won't work in the current environment.
319
+ setTimeout(async () => {
320
+ try {
321
+ const { DEPS } = await import("../bootstrap/deps.ts");
322
+ const probed = DEPS.filter(d => d.tier === "core" || d.tier === "recommended");
323
+ const missing = probed.filter(d => !d.check());
324
+ if (missing.length === 0) return;
325
+
326
+ const summary = missing.map(d => d.name).join(", ");
327
+ const details = missing.map(d => `• ${d.name} — ${d.purpose}`).join("\n");
328
+ ctx.ui.notify(`Missing Omegon deps: ${summary}`, "info");
329
+ pi.sendMessage({
330
+ customType: "guardrail-health-check",
331
+ content: `[omegon startup check] Missing runtime dependencies: ${summary}.\n\n`
332
+ + `These Omegon features may not work:\n${details}\n\n`
333
+ + `Run \`/bootstrap\` to install interactively.`,
334
+ display: true,
335
+ });
336
+ } catch {
337
+ /* non-fatal */
338
+ }
339
+ }, 2000);
340
+ });
341
+
342
+ // ── Session shutdown: cleanup ─────────────────────────────────
343
+
344
+ pi.on("session_shutdown", async () => {
345
+ if (unsubscribeEvents) {
346
+ unsubscribeEvents();
347
+ unsubscribeEvents = null;
348
+ }
349
+ // Permanently close the non-capturing overlay
350
+ if (overlayHandle) {
351
+ overlayHandle.hide();
352
+ overlayHandle = null;
353
+ }
354
+ if (overlayDone) {
355
+ overlayDone();
356
+ overlayDone = null;
357
+ }
358
+ overlayCreated = false;
359
+ pendingFocus = false;
360
+ footer = null;
361
+ tui = null;
362
+ });
363
+
364
+ // ── Agent running state — guards focused overlay during streaming ─────
365
+
366
+ pi.on("before_agent_start", async () => {
367
+ agentRunning = true;
368
+ // If focus was pending and agent starts before handle arrived, cancel it
369
+ pendingFocus = false;
370
+ // If overlay is currently focused, unfocus to avoid input deadlock
371
+ if (overlayHandle?.isFocused()) {
372
+ overlayHandle.unfocus();
373
+ state.mode = "panel";
374
+ }
375
+ });
376
+
377
+ // ── Events that trigger re-render ─────────────────────────────
378
+
379
+ pi.on("turn_end", async (_event, ctx) => {
380
+ agentRunning = false;
381
+ state.turns++;
382
+ refresh(ctx);
383
+ });
384
+
385
+ pi.on("message_end", async (_event, ctx) => {
386
+ refresh(ctx);
387
+ });
388
+
389
+ pi.on("tool_execution_end", async (_event, ctx) => {
390
+ refresh(ctx);
391
+ });
392
+
393
+ // ── Keyboard shortcut: ctrl+` ────────────────────────────────
394
+ // Cycles through: compact → raised → panel → focused → compact
395
+
396
+ pi.registerShortcut("ctrl+`", {
397
+ description: "Toggle dashboard footer (compact ↔ raised)",
398
+ handler: (ctx) => {
399
+ dashToggle(ctx);
400
+ },
401
+ });
402
+
403
+ // ── Slash commands: /dash and /dashboard ─────────────────────
404
+ // Registered with the shared bridge as interactive-only (agentCallable: false)
405
+ // so the agent gets a structured refusal instead of an opaque "not registered" error.
406
+
407
+ const bridge = getSharedBridge();
408
+
409
+ bridge.register(pi, {
410
+ name: "dash",
411
+ description: "Toggle dashboard footer: compact ↔ raised. /dashboard opens the side panel.",
412
+ bridge: {
413
+ agentCallable: false,
414
+ sideEffectClass: "read",
415
+ summary: "Interactive-only dashboard footer toggle",
416
+ },
417
+ structuredExecutor: async (_args, ctx) => {
418
+ dashToggle(ctx as ExtensionContext);
419
+ const label = state.mode === "raised" ? "raised" : "compact";
420
+ return buildSlashCommandResult("dash", [], {
421
+ ok: true,
422
+ summary: `Dashboard: ${label}`,
423
+ humanText: `Dashboard: ${label}`,
424
+ effects: { sideEffectClass: "read" },
425
+ });
426
+ },
427
+ });
428
+
429
+ bridge.register(pi, {
430
+ name: "dashboard",
431
+ description: "Toggle dashboard side panel (open/close). Use /dash to raise/lower the footer.",
432
+ getArgumentCompletions: (prefix) => {
433
+ const lower = (prefix ?? "").toLowerCase();
434
+ return DASHBOARD_SUBCOMMANDS
435
+ .filter(s => s.startsWith(lower))
436
+ .map(s => ({ label: s, value: s }));
437
+ },
438
+ bridge: {
439
+ agentCallable: false,
440
+ sideEffectClass: "read",
441
+ summary: "Interactive-only dashboard panel toggle",
442
+ },
443
+ structuredExecutor: async (args, ctx) => {
444
+ const arg = (args ?? "").trim().toLowerCase();
445
+ const extCtx = ctx as ExtensionContext;
446
+
447
+ if (arg === "open") {
448
+ state.mode = "raised";
449
+ persistMode(extCtx);
450
+ tui?.requestRender();
451
+ await showDashboardOverlay(extCtx, pi);
452
+ return buildSlashCommandResult("dashboard", [arg], {
453
+ ok: true,
454
+ summary: "Dashboard: raised + panel",
455
+ humanText: "Dashboard: raised + panel",
456
+ effects: { sideEffectClass: "read" },
457
+ });
458
+ }
459
+ if (arg === "compact") { cycleTo(extCtx, "compact"); return buildSlashCommandResult("dashboard", [arg], { ok: true, summary: "Dashboard: compact", humanText: "Dashboard: compact", effects: { sideEffectClass: "read" } }); }
460
+ if (arg === "raised") { cycleTo(extCtx, "raised"); return buildSlashCommandResult("dashboard", [arg], { ok: true, summary: "Dashboard: raised", humanText: "Dashboard: raised", effects: { sideEffectClass: "read" } }); }
461
+ if (arg === "panel") { cycleTo(extCtx, "panel"); return buildSlashCommandResult("dashboard", [arg], { ok: true, summary: "Dashboard: panel", humanText: "Dashboard: panel", effects: { sideEffectClass: "read" } }); }
462
+ if (arg === "focus") { cycleTo(extCtx, "focused"); return buildSlashCommandResult("dashboard", [arg], { ok: true, summary: "Dashboard: focused", humanText: "Dashboard: focused", effects: { sideEffectClass: "read" } }); }
463
+
464
+ // Default: open blocking full-page operator panel
465
+ await showDashboardOverlay(extCtx, pi);
466
+ return buildSlashCommandResult("dashboard", [], {
467
+ ok: true,
468
+ summary: "Dashboard: closed",
469
+ humanText: "Dashboard: closed",
470
+ effects: { sideEffectClass: "read" },
471
+ });
472
+ },
473
+ interactiveHandler: async (result) => {
474
+ // The structuredExecutor already performs the toggle; just suppress double notification
475
+ // since dashToggle/cycleTo/panelToggle already update visual state.
476
+ },
477
+ });
478
+ }
@@ -0,0 +1,34 @@
1
+ import type { MemoryInjectionMetrics } from "../project-memory/injection-metrics.ts";
2
+
3
+ export function formatMemoryAuditSummary(
4
+ metrics: MemoryInjectionMetrics | undefined,
5
+ opts?: { wide?: boolean },
6
+ ): string {
7
+ if (!metrics) {
8
+ return "Memory · pending first injection";
9
+ }
10
+
11
+ const wide = opts?.wide ?? false;
12
+ if (wide) {
13
+ return [
14
+ `Memory audit: ${metrics.mode}`,
15
+ `facts:${metrics.projectFactCount}`,
16
+ `edges:${metrics.edgeCount}`,
17
+ `wm:${metrics.workingMemoryFactCount}`,
18
+ `hits:${metrics.semanticHitCount}`,
19
+ `ep:${metrics.episodeCount}`,
20
+ `global:${metrics.globalFactCount}`,
21
+ `chars:${metrics.payloadChars}`,
22
+ `~${metrics.estimatedTokens} tok`,
23
+ ].join(" · ");
24
+ }
25
+
26
+ return [
27
+ `Memory ${metrics.mode}`,
28
+ `facts:${metrics.projectFactCount}`,
29
+ `wm:${metrics.workingMemoryFactCount}`,
30
+ `ep:${metrics.episodeCount}`,
31
+ `global:${metrics.globalFactCount}`,
32
+ `~${metrics.estimatedTokens} tok`,
33
+ ].join(" · ");
34
+ }