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.
- package/.gitattributes +3 -0
- package/AGENTS.md +16 -0
- package/LICENSE +15 -0
- package/README.md +289 -0
- package/bin/pi.mjs +30 -0
- package/extensions/00-secrets/index.ts +1126 -0
- package/extensions/01-auth/auth.ts +401 -0
- package/extensions/01-auth/index.ts +289 -0
- package/extensions/auto-compact.ts +42 -0
- package/extensions/bootstrap/deps.ts +291 -0
- package/extensions/bootstrap/index.ts +811 -0
- package/extensions/chronos/chronos.sh +487 -0
- package/extensions/chronos/index.ts +148 -0
- package/extensions/cleave/assessment.ts +754 -0
- package/extensions/cleave/bridge.ts +31 -0
- package/extensions/cleave/conflicts.ts +250 -0
- package/extensions/cleave/dispatcher.ts +808 -0
- package/extensions/cleave/guardrails.ts +426 -0
- package/extensions/cleave/index.ts +3121 -0
- package/extensions/cleave/lifecycle-emitter.ts +20 -0
- package/extensions/cleave/openspec.ts +811 -0
- package/extensions/cleave/planner.ts +260 -0
- package/extensions/cleave/review.ts +579 -0
- package/extensions/cleave/skills.ts +355 -0
- package/extensions/cleave/types.ts +261 -0
- package/extensions/cleave/workspace.ts +861 -0
- package/extensions/cleave/worktree.ts +243 -0
- package/extensions/core-renderers.ts +253 -0
- package/extensions/dashboard/context-gauge.ts +58 -0
- package/extensions/dashboard/file-watch.ts +14 -0
- package/extensions/dashboard/footer.ts +1145 -0
- package/extensions/dashboard/git.ts +185 -0
- package/extensions/dashboard/index.ts +478 -0
- package/extensions/dashboard/memory-audit.ts +34 -0
- package/extensions/dashboard/overlay-data.ts +705 -0
- package/extensions/dashboard/overlay.ts +365 -0
- package/extensions/dashboard/render-utils.ts +54 -0
- package/extensions/dashboard/types.ts +191 -0
- package/extensions/dashboard/uri-helper.ts +45 -0
- package/extensions/debug.ts +69 -0
- package/extensions/defaults.ts +282 -0
- package/extensions/design-tree/dashboard-state.ts +161 -0
- package/extensions/design-tree/design-card.ts +362 -0
- package/extensions/design-tree/index.ts +2130 -0
- package/extensions/design-tree/lifecycle-emitter.ts +41 -0
- package/extensions/design-tree/tree.ts +1607 -0
- package/extensions/design-tree/types.ts +163 -0
- package/extensions/distill.ts +127 -0
- package/extensions/effort/index.ts +395 -0
- package/extensions/effort/tiers.ts +146 -0
- package/extensions/effort/types.ts +105 -0
- package/extensions/lib/git-state.ts +227 -0
- package/extensions/lib/local-models.ts +157 -0
- package/extensions/lib/model-preferences.ts +51 -0
- package/extensions/lib/model-routing.ts +720 -0
- package/extensions/lib/operator-fallback.ts +205 -0
- package/extensions/lib/operator-profile.ts +360 -0
- package/extensions/lib/slash-command-bridge.ts +253 -0
- package/extensions/lib/typebox-helpers.ts +16 -0
- package/extensions/local-inference/index.ts +727 -0
- package/extensions/mcp-bridge/README.md +220 -0
- package/extensions/mcp-bridge/index.ts +951 -0
- package/extensions/mcp-bridge/lib.ts +365 -0
- package/extensions/mcp-bridge/mcp.json +3 -0
- package/extensions/mcp-bridge/package.json +11 -0
- package/extensions/model-budget.ts +752 -0
- package/extensions/offline-driver.ts +403 -0
- package/extensions/openspec/archive-gate.ts +164 -0
- package/extensions/openspec/branch-cleanup.ts +64 -0
- package/extensions/openspec/dashboard-state.ts +50 -0
- package/extensions/openspec/index.ts +1917 -0
- package/extensions/openspec/lifecycle-emitter.ts +65 -0
- package/extensions/openspec/lifecycle-files.ts +70 -0
- package/extensions/openspec/lifecycle.ts +50 -0
- package/extensions/openspec/reconcile.ts +187 -0
- package/extensions/openspec/spec.ts +1385 -0
- package/extensions/openspec/types.ts +98 -0
- package/extensions/project-memory/DESIGN-global-mind.md +198 -0
- package/extensions/project-memory/README.md +202 -0
- package/extensions/project-memory/api-types.ts +382 -0
- package/extensions/project-memory/compaction-policy.ts +29 -0
- package/extensions/project-memory/core.ts +164 -0
- package/extensions/project-memory/embeddings.ts +230 -0
- package/extensions/project-memory/extraction-v2.ts +861 -0
- package/extensions/project-memory/factstore.ts +2177 -0
- package/extensions/project-memory/index.ts +3459 -0
- package/extensions/project-memory/injection-metrics.ts +91 -0
- package/extensions/project-memory/jsonl-io.ts +12 -0
- package/extensions/project-memory/lifecycle.ts +331 -0
- package/extensions/project-memory/migration.ts +293 -0
- package/extensions/project-memory/package.json +9 -0
- package/extensions/project-memory/sci-renderers.ts +7 -0
- package/extensions/project-memory/template.ts +103 -0
- package/extensions/project-memory/triggers.ts +52 -0
- package/extensions/project-memory/types.ts +102 -0
- package/extensions/render/composition/fonts/Inter-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Inter-Regular.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Bold.ttf +0 -0
- package/extensions/render/composition/fonts/Tomorrow-Regular.ttf +0 -0
- package/extensions/render/composition/package-lock.json +534 -0
- package/extensions/render/composition/package.json +22 -0
- package/extensions/render/composition/render.mjs +246 -0
- package/extensions/render/composition/test-comp.tsx +87 -0
- package/extensions/render/composition/types.ts +24 -0
- package/extensions/render/excalidraw/UPSTREAM.md +81 -0
- package/extensions/render/excalidraw/elements.ts +764 -0
- package/extensions/render/excalidraw/index.ts +66 -0
- package/extensions/render/excalidraw/types.ts +223 -0
- package/extensions/render/excalidraw-renderer/pyproject.toml +8 -0
- package/extensions/render/excalidraw-renderer/render_excalidraw.py +182 -0
- package/extensions/render/excalidraw-renderer/render_template.html +59 -0
- package/extensions/render/index.ts +830 -0
- package/extensions/render/native-diagrams/index.ts +57 -0
- package/extensions/render/native-diagrams/motifs.ts +542 -0
- package/extensions/render/native-diagrams/raster.ts +8 -0
- package/extensions/render/native-diagrams/scene.ts +75 -0
- package/extensions/render/native-diagrams/spec.ts +204 -0
- package/extensions/render/native-diagrams/svg.ts +116 -0
- package/extensions/sci-ui.ts +304 -0
- package/extensions/session-log.ts +174 -0
- package/extensions/shared-state.ts +146 -0
- package/extensions/spinner-verbs.ts +91 -0
- package/extensions/style.ts +281 -0
- package/extensions/terminal-title.ts +191 -0
- package/extensions/tool-profile/index.ts +291 -0
- package/extensions/tool-profile/profiles.ts +290 -0
- package/extensions/types.d.ts +9 -0
- package/extensions/vault/index.ts +185 -0
- package/extensions/version-check.ts +90 -0
- package/extensions/view/index.ts +859 -0
- package/extensions/view/uri-resolver.ts +148 -0
- package/extensions/web-search/index.ts +182 -0
- package/extensions/web-search/providers.ts +121 -0
- package/extensions/web-ui/index.ts +110 -0
- package/extensions/web-ui/server.ts +265 -0
- package/extensions/web-ui/state.ts +462 -0
- package/extensions/web-ui/static/index.html +145 -0
- package/extensions/web-ui/types.ts +284 -0
- package/package.json +76 -0
- package/prompts/init.md +75 -0
- package/prompts/new-repo.md +54 -0
- package/prompts/oci-login.md +56 -0
- package/prompts/status.md +50 -0
- package/settings.json +4 -0
- package/skills/cleave/SKILL.md +218 -0
- package/skills/git/SKILL.md +209 -0
- package/skills/git/_reference/ci-validation.md +204 -0
- package/skills/oci/SKILL.md +338 -0
- package/skills/openspec/SKILL.md +346 -0
- package/skills/pi-extensions/SKILL.md +191 -0
- package/skills/pi-tui/SKILL.md +517 -0
- package/skills/python/SKILL.md +189 -0
- package/skills/rust/SKILL.md +268 -0
- package/skills/security/SKILL.md +206 -0
- package/skills/style/SKILL.md +264 -0
- package/skills/typescript/SKILL.md +225 -0
- package/skills/vault/SKILL.md +102 -0
- package/themes/alpharius-legacy.json +85 -0
- package/themes/alpharius.conf +59 -0
- 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
|
+
}
|