omegon 0.7.7 → 0.8.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/extensions/cleave/dispatcher.ts +1 -1
- package/extensions/cleave/index.ts +24 -174
- package/extensions/cleave/workspace.ts +4 -3
- package/extensions/dashboard/footer.ts +84 -21
- package/extensions/defaults.ts +1 -1
- package/extensions/design-tree/index.ts +87 -56
- package/extensions/design-tree/tree.ts +113 -0
- package/extensions/lib/shared-state.ts +24 -0
- package/extensions/openspec/index.ts +25 -0
- package/extensions/project-memory/factstore.ts +176 -63
- package/extensions/project-memory/index.ts +91 -0
- package/node_modules/@babel/runtime/helpers/esm/wrapAsyncGenerator.js +18 -31
- package/node_modules/@babel/runtime/helpers/wrapAsyncGenerator.js +18 -31
- package/node_modules/@babel/runtime/package.json +1 -1
- package/package.json +1 -1
|
@@ -294,7 +294,7 @@ export function buildChildPrompt(
|
|
|
294
294
|
" - Fill in Summary, Artifacts, Decisions Made, Interfaces Published",
|
|
295
295
|
"3. **Commits**: Commit your work with clear messages. Do not push.",
|
|
296
296
|
"4. **No side effects**: Do not install global packages or modify system state.",
|
|
297
|
-
"5. **
|
|
297
|
+
"5. **Testing**: Write tests for new functions and changed behavior in co-located *.test.ts files. Run them and report results in the Verification section. Untested code is incomplete.",
|
|
298
298
|
`6. **Workspace**: ${workspacePath}`,
|
|
299
299
|
];
|
|
300
300
|
|
|
@@ -19,8 +19,8 @@ import { truncateTail, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize } from "
|
|
|
19
19
|
|
|
20
20
|
import { Text } from "@styrene-lab/pi-tui";
|
|
21
21
|
import { Type } from "@sinclair/typebox";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
22
|
+
import { execFile } from "node:child_process";
|
|
23
|
+
import { killAllCleaveSubprocesses, cleanupOrphanedProcesses } from "./subprocess-tracker.ts";
|
|
24
24
|
import * as fs from "node:fs";
|
|
25
25
|
import * as path from "node:path";
|
|
26
26
|
import { promisify } from "node:util";
|
|
@@ -32,7 +32,7 @@ import { debug } from "../lib/debug.ts";
|
|
|
32
32
|
import { emitOpenSpecState } from "../openspec/dashboard-state.ts";
|
|
33
33
|
import { getSharedBridge, buildSlashCommandResult, parseBridgedArgs } from "../lib/slash-command-bridge.ts";
|
|
34
34
|
import { buildAssessBridgeResult } from "./bridge.ts";
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
import { scanDesignDocs, getNodeSections } from "../design-tree/tree.ts";
|
|
37
37
|
import {
|
|
38
38
|
assessDirective,
|
|
@@ -659,29 +659,6 @@ function normalizeSpecAssessment(payload: AssessSpecAgentResult, expectedTotal:
|
|
|
659
659
|
};
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
-
function extractJsonObject(text: string): string | null {
|
|
663
|
-
const fenced = text.match(/```json\s*([\s\S]*?)```/i);
|
|
664
|
-
if (fenced?.[1]) return fenced[1].trim();
|
|
665
|
-
const firstBrace = text.indexOf("{");
|
|
666
|
-
const lastBrace = text.lastIndexOf("}");
|
|
667
|
-
if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) return null;
|
|
668
|
-
return text.slice(firstBrace, lastBrace + 1).trim();
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function extractAssistantText(content: unknown): string {
|
|
672
|
-
if (typeof content === "string") return content.trim();
|
|
673
|
-
if (!Array.isArray(content)) return "";
|
|
674
|
-
return content
|
|
675
|
-
.map((item) => {
|
|
676
|
-
if (typeof item === "string") return item;
|
|
677
|
-
if (!item || typeof item !== "object") return "";
|
|
678
|
-
return typeof (item as { text?: unknown }).text === "string"
|
|
679
|
-
? (item as { text: string }).text
|
|
680
|
-
: "";
|
|
681
|
-
})
|
|
682
|
-
.join("\n")
|
|
683
|
-
.trim();
|
|
684
|
-
}
|
|
685
662
|
|
|
686
663
|
function formatSpecOutcomeLabel(outcome: AssessLifecycleOutcome): string {
|
|
687
664
|
switch (outcome) {
|
|
@@ -721,151 +698,6 @@ function buildSpecAssessmentHumanText(changeName: string, assessed: AssessSpecAg
|
|
|
721
698
|
return lines.join("\n");
|
|
722
699
|
}
|
|
723
700
|
|
|
724
|
-
async function runSpecAssessmentSubprocess(
|
|
725
|
-
input: SpecAssessmentRunnerInput,
|
|
726
|
-
): Promise<SpecAssessmentRunnerOutput> {
|
|
727
|
-
const prompt = [
|
|
728
|
-
"You are performing a read-only OpenSpec compliance assessment.",
|
|
729
|
-
"Operate in read-only plan mode. Never call edit, write, or any workspace-mutating command.",
|
|
730
|
-
"Inspect the repository and determine whether the implementation satisfies every OpenSpec scenario below.",
|
|
731
|
-
"Return ONLY a JSON object with this exact shape:",
|
|
732
|
-
"{",
|
|
733
|
-
' "summary": { "total": number, "pass": number, "fail": number, "unclear": number },',
|
|
734
|
-
' "scenarios": [',
|
|
735
|
-
' { "domain": string, "requirement": string, "scenario": string, "status": "PASS"|"FAIL"|"UNCLEAR", "evidence": string[], "notes"?: string }',
|
|
736
|
-
" ],",
|
|
737
|
-
' "changedFiles": string[],',
|
|
738
|
-
' "constraints": string[],',
|
|
739
|
-
' "overallNotes"?: string',
|
|
740
|
-
"}",
|
|
741
|
-
"Rules:",
|
|
742
|
-
`- Emit exactly ${input.expectedScenarioCount} scenario entries.`,
|
|
743
|
-
"- Use FAIL when the code clearly contradicts or omits the scenario.",
|
|
744
|
-
"- Use UNCLEAR only when code inspection cannot safely prove PASS or FAIL.",
|
|
745
|
-
"- Evidence must cite concrete files, symbols, or line references when possible.",
|
|
746
|
-
"- changedFiles should list files that would need modification if the result reopens work.",
|
|
747
|
-
"- constraints should list newly discovered implementation constraints.",
|
|
748
|
-
"- Do not wrap the JSON in explanatory prose.",
|
|
749
|
-
"",
|
|
750
|
-
`Change: ${input.changeName}`,
|
|
751
|
-
"",
|
|
752
|
-
"## Acceptance Criteria",
|
|
753
|
-
"",
|
|
754
|
-
input.scenarioText,
|
|
755
|
-
"",
|
|
756
|
-
...input.designContext,
|
|
757
|
-
...input.apiContractContext,
|
|
758
|
-
...(input.diffContent ? ["### Recent Changes", "", "```diff", input.diffContent, "```", ""] : []),
|
|
759
|
-
].join("\n");
|
|
760
|
-
|
|
761
|
-
const omegon = resolveOmegonSubprocess();
|
|
762
|
-
const args = [...omegon.argvPrefix, "--mode", "json", "--plan", "-p", "--no-session"];
|
|
763
|
-
if (input.modelId) args.push("--model", input.modelId);
|
|
764
|
-
|
|
765
|
-
return await new Promise<SpecAssessmentRunnerOutput>((resolve, reject) => {
|
|
766
|
-
const proc = spawn(omegon.command, args, {
|
|
767
|
-
cwd: input.repoPath,
|
|
768
|
-
shell: false,
|
|
769
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
770
|
-
detached: true,
|
|
771
|
-
env: {
|
|
772
|
-
...process.env,
|
|
773
|
-
PI_CHILD: "1",
|
|
774
|
-
TERM: process.env.TERM ?? "dumb",
|
|
775
|
-
},
|
|
776
|
-
});
|
|
777
|
-
registerCleaveProc(proc);
|
|
778
|
-
let stdout = "";
|
|
779
|
-
let stderr = "";
|
|
780
|
-
let buffer = "";
|
|
781
|
-
let assistantText = "";
|
|
782
|
-
let settled = false;
|
|
783
|
-
const settleReject = (error: Error) => {
|
|
784
|
-
if (settled) return;
|
|
785
|
-
settled = true;
|
|
786
|
-
clearTimeout(timer);
|
|
787
|
-
reject(error);
|
|
788
|
-
};
|
|
789
|
-
const settleResolve = (value: SpecAssessmentRunnerOutput) => {
|
|
790
|
-
if (settled) return;
|
|
791
|
-
settled = true;
|
|
792
|
-
clearTimeout(timer);
|
|
793
|
-
resolve(value);
|
|
794
|
-
};
|
|
795
|
-
let escalationTimer: ReturnType<typeof setTimeout> | undefined;
|
|
796
|
-
const timer = setTimeout(() => {
|
|
797
|
-
killCleaveProc(proc);
|
|
798
|
-
escalationTimer = setTimeout(() => {
|
|
799
|
-
if (!proc.killed) {
|
|
800
|
-
try {
|
|
801
|
-
if (proc.pid) process.kill(-proc.pid, "SIGKILL");
|
|
802
|
-
} catch {
|
|
803
|
-
try { proc.kill("SIGKILL"); } catch { /* already dead */ }
|
|
804
|
-
}
|
|
805
|
-
}
|
|
806
|
-
}, 5_000);
|
|
807
|
-
settleReject(new Error(`Timed out after 120s while assessing ${input.changeName}.`));
|
|
808
|
-
}, 120_000);
|
|
809
|
-
|
|
810
|
-
const processLine = (line: string) => {
|
|
811
|
-
if (!line.trim()) return;
|
|
812
|
-
stdout += line + "\n";
|
|
813
|
-
let event: unknown;
|
|
814
|
-
try {
|
|
815
|
-
event = JSON.parse(line);
|
|
816
|
-
} catch {
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
if (!event || typeof event !== "object") return;
|
|
820
|
-
const typed = event as { type?: string; message?: { role?: string; content?: unknown } };
|
|
821
|
-
if (typed.type === "message_end" && typed.message?.role === "assistant") {
|
|
822
|
-
assistantText = extractAssistantText(typed.message.content);
|
|
823
|
-
}
|
|
824
|
-
};
|
|
825
|
-
|
|
826
|
-
proc.stdout.on("data", (data) => {
|
|
827
|
-
buffer += data.toString();
|
|
828
|
-
const lines = buffer.split("\n");
|
|
829
|
-
buffer = lines.pop() || "";
|
|
830
|
-
for (const line of lines) processLine(line);
|
|
831
|
-
});
|
|
832
|
-
proc.stderr.on("data", (data) => {
|
|
833
|
-
stderr += data.toString();
|
|
834
|
-
});
|
|
835
|
-
proc.on("error", (error) => {
|
|
836
|
-
deregisterCleaveProc(proc);
|
|
837
|
-
clearTimeout(escalationTimer);
|
|
838
|
-
settleReject(error);
|
|
839
|
-
});
|
|
840
|
-
proc.on("close", (code) => {
|
|
841
|
-
deregisterCleaveProc(proc);
|
|
842
|
-
clearTimeout(escalationTimer);
|
|
843
|
-
if (buffer.trim()) processLine(buffer.trim());
|
|
844
|
-
if ((code ?? 1) !== 0) {
|
|
845
|
-
settleReject(new Error(stderr.trim() || `Assessment subprocess exited with code ${code ?? 1}.`));
|
|
846
|
-
return;
|
|
847
|
-
}
|
|
848
|
-
const sourceText = assistantText || stdout;
|
|
849
|
-
const jsonText = extractJsonObject(sourceText);
|
|
850
|
-
if (!jsonText) {
|
|
851
|
-
settleReject(new Error(`Assessment subprocess did not return parseable JSON.\n${stderr || stdout}`));
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
try {
|
|
855
|
-
const parsed = JSON.parse(jsonText) as AssessSpecAgentResult;
|
|
856
|
-
settleResolve({
|
|
857
|
-
assessed: normalizeSpecAssessment(parsed, input.expectedScenarioCount),
|
|
858
|
-
});
|
|
859
|
-
} catch (error) {
|
|
860
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
861
|
-
settleReject(new Error(`Assessment JSON was invalid: ${message}`));
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
proc.stdin.write(prompt);
|
|
866
|
-
proc.stdin.end();
|
|
867
|
-
});
|
|
868
|
-
}
|
|
869
701
|
|
|
870
702
|
function applyAssessEffects(pi: ExtensionAPI, result: AssessStructuredResult): void {
|
|
871
703
|
for (const effect of result.effects) {
|
|
@@ -1313,14 +1145,18 @@ async function executeAssessSpec(
|
|
|
1313
1145
|
...(diffContent ? ["", "### Recent Changes (for context)", "", "```diff", diffContent, "```"] : []),
|
|
1314
1146
|
].join("\n");
|
|
1315
1147
|
|
|
1316
|
-
|
|
1148
|
+
// Use the follow-up pattern for any in-session context: return the prepared
|
|
1149
|
+
// assessment prompt and let the current LLM session evaluate scenarios.
|
|
1150
|
+
// The subprocess path below is retained only for programmatic callers that
|
|
1151
|
+
// inject overrides.runSpecAssessment (e.g. tests).
|
|
1152
|
+
if (isInteractiveAssessContext(ctx) || ctx.bridgeInvocation) {
|
|
1317
1153
|
const lifecycleRecord = await buildLifecycleRecord(pi, ctx.cwd, {
|
|
1318
1154
|
changeName: target.name,
|
|
1319
1155
|
assessmentKind: "spec",
|
|
1320
1156
|
outcome: "ambiguous",
|
|
1321
1157
|
recommendedAction: `Run openspec_manage reconcile_after_assess ${target.name} with outcome pass, reopen, or ambiguous after scenario evaluation completes.`,
|
|
1322
1158
|
});
|
|
1323
|
-
|
|
1159
|
+
const result = makeAssessResult({
|
|
1324
1160
|
subcommand: "spec",
|
|
1325
1161
|
args,
|
|
1326
1162
|
ok: true,
|
|
@@ -1343,9 +1179,22 @@ async function executeAssessSpec(
|
|
|
1343
1179
|
lifecycle,
|
|
1344
1180
|
lifecycleRecord,
|
|
1345
1181
|
});
|
|
1182
|
+
// For bridge invocations, eagerly deliver effects so the follow-up prompt
|
|
1183
|
+
// reaches the LLM even if the caller pipeline doesn't include a handler
|
|
1184
|
+
// that calls applyAssessEffects. Clear effects after to prevent double-
|
|
1185
|
+
// delivery (the agentHandler also calls applyAssessEffects).
|
|
1186
|
+
// Interactive callers leave effects intact for interactiveHandler to apply.
|
|
1187
|
+
if (ctx.bridgeInvocation) {
|
|
1188
|
+
applyAssessEffects(pi, result);
|
|
1189
|
+
result.effects = [];
|
|
1190
|
+
}
|
|
1191
|
+
return result;
|
|
1346
1192
|
}
|
|
1347
1193
|
|
|
1348
|
-
|
|
1194
|
+
if (!overrides?.runSpecAssessment) {
|
|
1195
|
+
throw new Error("[assess spec] Unexpected code path: neither interactive nor bridge context, and no runSpecAssessment override provided.");
|
|
1196
|
+
}
|
|
1197
|
+
const runSpecAssessment = overrides.runSpecAssessment;
|
|
1349
1198
|
const completed = await runSpecAssessment({
|
|
1350
1199
|
repoPath,
|
|
1351
1200
|
changeName: target.name,
|
|
@@ -1734,6 +1583,7 @@ export default function cleaveExtension(pi: ExtensionAPI) {
|
|
|
1734
1583
|
promptSnippet:
|
|
1735
1584
|
"Assess task complexity for decomposition — returns pattern match, complexity score, and execute/cleave decision",
|
|
1736
1585
|
promptGuidelines: [
|
|
1586
|
+
"Every non-trivial code change must include tests in co-located *.test.ts files. Untested code is incomplete — do not commit without tests for new functions and changed behavior.",
|
|
1737
1587
|
"Call cleave_assess before starting any multi-system or cross-cutting task to determine if decomposition is needed",
|
|
1738
1588
|
"If decision is 'execute', proceed directly. If 'cleave', use /cleave to decompose. If 'needs_assessment', proceed directly — it means no pattern matched but the task is likely simple enough for in-session execution.",
|
|
1739
1589
|
"Complexity formula: (1 + systems) × (1 + 0.5 × modifiers). Threshold default: 2.0.",
|
|
@@ -238,9 +238,10 @@ ${skillSection}${designSection}${guardrailSection ?? ""}
|
|
|
238
238
|
## Contract
|
|
239
239
|
|
|
240
240
|
1. Only work on files within your scope
|
|
241
|
-
2.
|
|
242
|
-
3.
|
|
243
|
-
4.
|
|
241
|
+
2. Write tests for new functions and changed behavior — co-locate as *.test.ts
|
|
242
|
+
3. Update the Result section below when done
|
|
243
|
+
4. Commit your work with clear messages — do not push
|
|
244
|
+
5. If the task is too complex, set status to NEEDS_DECOMPOSITION
|
|
244
245
|
|
|
245
246
|
## Result
|
|
246
247
|
|
|
@@ -546,27 +546,33 @@ export class DashboardFooter implements Component {
|
|
|
546
546
|
// ── Provider / model / thinking line ──────────────────────
|
|
547
547
|
const m = ctx.model;
|
|
548
548
|
if (m) {
|
|
549
|
-
const multiProvider = this.footerData.getAvailableProviderCount() > 1;
|
|
550
549
|
const pointer = theme.fg("accent", "▸");
|
|
551
|
-
const dot = theme.fg("dim", "
|
|
550
|
+
const dot = theme.fg("dim", " · ");
|
|
552
551
|
|
|
552
|
+
// Width-aware: narrow cards drop provider prefix, abbreviate thinking
|
|
553
|
+
const narrow = width < 40;
|
|
554
|
+
const multiProvider = !narrow && this.footerData.getAvailableProviderCount() > 1;
|
|
555
|
+
|
|
556
|
+
// Shorten model ID for display: "claude-opus-4-6" stays, but drop provider/ prefix if present in ID
|
|
557
|
+
const modelDisplay = m.id.replace(/^.*\//, "");
|
|
553
558
|
const providerModel = multiProvider
|
|
554
|
-
? `${pointer} ${theme.fg("muted", m.provider)}
|
|
555
|
-
: `${pointer} ${theme.fg("muted",
|
|
559
|
+
? `${pointer} ${theme.fg("muted", m.provider)}${theme.fg("dim", "/")}${theme.fg("muted", modelDisplay)}`
|
|
560
|
+
: `${pointer} ${theme.fg("muted", modelDisplay)}`;
|
|
556
561
|
|
|
557
562
|
const parts: string[] = [providerModel];
|
|
558
563
|
|
|
559
|
-
if (m.reasoning) {
|
|
564
|
+
if (m.reasoning && this.cachedThinkingLevel) {
|
|
560
565
|
const thinkColor: ThemeColor = this.cachedThinkingLevel === "high" ? "accent"
|
|
561
566
|
: this.cachedThinkingLevel === "medium" ? "muted"
|
|
562
567
|
: "dim";
|
|
563
568
|
const thinkIcon = this.cachedThinkingLevel === "off"
|
|
564
569
|
? theme.fg("dim", "○")
|
|
565
570
|
: theme.fg(thinkColor, "◉");
|
|
566
|
-
|
|
571
|
+
// Narrow: icon only, no label
|
|
572
|
+
parts.push(narrow ? thinkIcon : `${thinkIcon} ${theme.fg(thinkColor, this.cachedThinkingLevel)}`);
|
|
567
573
|
}
|
|
568
574
|
|
|
569
|
-
if (this.cachedTokens.cost > 0) {
|
|
575
|
+
if (this.cachedTokens.cost > 0 && !narrow) {
|
|
570
576
|
parts.push(theme.fg("dim", `$${this.cachedTokens.cost.toFixed(3)}`));
|
|
571
577
|
}
|
|
572
578
|
|
|
@@ -592,7 +598,8 @@ export class DashboardFooter implements Component {
|
|
|
592
598
|
|
|
593
599
|
if (!metrics && totalFacts === null) return "";
|
|
594
600
|
|
|
595
|
-
const sep = theme.fg("dim", "
|
|
601
|
+
const sep = theme.fg("dim", " · ");
|
|
602
|
+
const narrow = width < 35;
|
|
596
603
|
const parts: string[] = [];
|
|
597
604
|
|
|
598
605
|
if (totalFacts !== null) {
|
|
@@ -600,17 +607,24 @@ export class DashboardFooter implements Component {
|
|
|
600
607
|
}
|
|
601
608
|
|
|
602
609
|
if (metrics) {
|
|
610
|
+
// Always show injection count and token estimate
|
|
603
611
|
if (metrics.projectFactCount > 0)
|
|
604
612
|
parts.push(theme.fg("dim", "inj ") + theme.fg("muted", String(metrics.projectFactCount)));
|
|
605
|
-
|
|
613
|
+
// Show working memory and episodes only when there's room
|
|
614
|
+
if (!narrow && metrics.workingMemoryFactCount > 0)
|
|
606
615
|
parts.push(theme.fg("dim", "wm ") + theme.fg("muted", String(metrics.workingMemoryFactCount)));
|
|
607
|
-
if (metrics.episodeCount > 0)
|
|
616
|
+
if (!narrow && metrics.episodeCount > 0)
|
|
608
617
|
parts.push(theme.fg("dim", "ep ") + theme.fg("muted", String(metrics.episodeCount)));
|
|
609
|
-
|
|
618
|
+
// Drop global count in narrow — least useful
|
|
619
|
+
if (!narrow && metrics.globalFactCount > 0)
|
|
610
620
|
parts.push(theme.fg("dim", "gl ") + theme.fg("muted", String(metrics.globalFactCount)));
|
|
611
|
-
|
|
621
|
+
// Token estimate — always show, use compact format
|
|
622
|
+
const tokStr = metrics.estimatedTokens >= 1000
|
|
623
|
+
? `~${(metrics.estimatedTokens / 1000).toFixed(1)}k`
|
|
624
|
+
: `~${metrics.estimatedTokens}`;
|
|
625
|
+
parts.push(theme.fg("dim", tokStr));
|
|
612
626
|
} else {
|
|
613
|
-
parts.push(theme.fg("dim", "pending
|
|
627
|
+
parts.push(theme.fg("dim", "pending"));
|
|
614
628
|
}
|
|
615
629
|
|
|
616
630
|
return truncateToWidth(` ${parts.join(sep)}`, width, "…");
|
|
@@ -674,6 +688,41 @@ export class DashboardFooter implements Component {
|
|
|
674
688
|
return lines;
|
|
675
689
|
}
|
|
676
690
|
|
|
691
|
+
/**
|
|
692
|
+
* Derive a short directive label from the active mind name.
|
|
693
|
+
* "directive/my-feature" → "my-feature", null/default → null.
|
|
694
|
+
*/
|
|
695
|
+
private getDirectiveLabel(): string | null {
|
|
696
|
+
const mind = sharedState.activeMind;
|
|
697
|
+
if (!mind || mind === "default") return null;
|
|
698
|
+
// Strip common prefixes: "directive/", "mind/"
|
|
699
|
+
const label = mind.replace(/^(?:directive|mind)\//, "");
|
|
700
|
+
return label || null;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Build directive indicator lines for the model card area.
|
|
705
|
+
* Shows "▸ directive: my-feature ✓" (branch match) or "▸ directive: my-feature ⚠ main" (mismatch).
|
|
706
|
+
* Returns empty array when no directive is active.
|
|
707
|
+
*/
|
|
708
|
+
private buildDirectiveIndicatorLines(width: number): string[] {
|
|
709
|
+
const label = this.getDirectiveLabel();
|
|
710
|
+
if (!label) return [];
|
|
711
|
+
const theme = this.theme;
|
|
712
|
+
const directive = sharedState.activeDirective;
|
|
713
|
+
const currentBranch = this.footerData.getGitBranch?.() ?? "";
|
|
714
|
+
|
|
715
|
+
let branchBadge = "";
|
|
716
|
+
if (directive && currentBranch) {
|
|
717
|
+
branchBadge = currentBranch === directive.branch
|
|
718
|
+
? ` ${theme.fg("success", "✓")}`
|
|
719
|
+
: ` ${theme.fg("error", "⚠")} ${theme.fg("dim", currentBranch)}`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const line = `${theme.fg("warning", "▸")} ${theme.fg("dim", "directive:")} ${theme.fg("warning", label)}${branchBadge}`;
|
|
723
|
+
return [truncateToWidth(line, width, "…")];
|
|
724
|
+
}
|
|
725
|
+
|
|
677
726
|
private buildModelTopologySummaries(): DashboardModelRoleSummary[] {
|
|
678
727
|
const ctx = this.ctxRef;
|
|
679
728
|
const summaries: DashboardModelRoleSummary[] = [];
|
|
@@ -751,8 +800,12 @@ export class DashboardFooter implements Component {
|
|
|
751
800
|
: theme.fg("dim", summary.state));
|
|
752
801
|
const normalized = normalizeLocalModelLabel(summary.model);
|
|
753
802
|
const alias = forceCompact ? "" : (normalized.alias ? theme.fg("dim", `alias ${normalized.alias}`) : "");
|
|
754
|
-
|
|
755
|
-
const
|
|
803
|
+
// In very compact mode, drop the role label to give more room to the model name
|
|
804
|
+
const veryCompact = width < 30;
|
|
805
|
+
const roleLabel = veryCompact ? "" : (forceCompact ? summary.label.slice(0, 1) : summary.label);
|
|
806
|
+
const primary = roleLabel
|
|
807
|
+
? `${theme.fg("accent", roleLabel)} ${theme.fg("muted", normalized.canonical)}`
|
|
808
|
+
: theme.fg("muted", normalized.canonical);
|
|
756
809
|
return truncateToWidth(
|
|
757
810
|
composePrimaryMetaLine(
|
|
758
811
|
width,
|
|
@@ -790,7 +843,10 @@ export class DashboardFooter implements Component {
|
|
|
790
843
|
contextCard: this.buildSummaryCard("context", this.buildHudContextLines(Math.max(1, safeWidth - 2)).map((l) => l.trimStart()), safeWidth),
|
|
791
844
|
modelCard: this.buildSummaryCard(
|
|
792
845
|
"models",
|
|
793
|
-
|
|
846
|
+
[
|
|
847
|
+
...this.buildDirectiveIndicatorLines(Math.max(1, safeWidth - 2)),
|
|
848
|
+
...this.buildModelTopologySummaries().map((s) => this.formatModelTopologyLine(s, Math.max(1, safeWidth - 2), safeWidth < 44)),
|
|
849
|
+
],
|
|
794
850
|
safeWidth,
|
|
795
851
|
),
|
|
796
852
|
memoryCard: this.buildSummaryCard("memory", (() => {
|
|
@@ -852,7 +908,10 @@ export class DashboardFooter implements Component {
|
|
|
852
908
|
contextCard: this.buildSummaryCardForColumn("context", this.buildHudContextLines(Math.max(1, col1W - 2)).map((l) => l.trimStart()), col1W, col1W),
|
|
853
909
|
modelCard: this.buildSummaryCardForColumn(
|
|
854
910
|
"models",
|
|
855
|
-
|
|
911
|
+
[
|
|
912
|
+
...this.buildDirectiveIndicatorLines(Math.max(1, col2W - 2)),
|
|
913
|
+
...this.buildModelTopologySummaries().map((s) => this.formatModelTopologyLine(s, Math.max(1, col2W - 2), col2W < 44)),
|
|
914
|
+
],
|
|
856
915
|
col2W,
|
|
857
916
|
col2W,
|
|
858
917
|
),
|
|
@@ -1074,10 +1133,14 @@ export class DashboardFooter implements Component {
|
|
|
1074
1133
|
}
|
|
1075
1134
|
}
|
|
1076
1135
|
|
|
1077
|
-
// If no focused node and no implementing nodes, show
|
|
1136
|
+
// If no focused node and no implementing nodes, show actionable nodes only.
|
|
1137
|
+
// Filter out decided/implemented/resolved — those are done; the footer should
|
|
1138
|
+
// surface work in progress, not a historical catalog.
|
|
1078
1139
|
if (!dt.focusedNode && (!dt.implementingNodes || dt.implementingNodes.length === 0) && dt.nodes) {
|
|
1140
|
+
const TERMINAL_STATUSES = new Set(["decided", "implemented", "resolved"]);
|
|
1141
|
+
const actionable = dt.nodes.filter(n => !TERMINAL_STATUSES.has(n.status));
|
|
1079
1142
|
const MAX_NODES = 6;
|
|
1080
|
-
for (const n of
|
|
1143
|
+
for (const n of actionable.slice(0, MAX_NODES)) {
|
|
1081
1144
|
const icon = this.nodeStatusIcon(n.status);
|
|
1082
1145
|
const badge = designSpecBadge(n.designSpec, n.assessmentResult, (c, t) => theme.fg(c as any, t));
|
|
1083
1146
|
const badgeSep = badge ? " " : "";
|
|
@@ -1090,9 +1153,9 @@ export class DashboardFooter implements Component {
|
|
|
1090
1153
|
[qSuffix + linkSuffix],
|
|
1091
1154
|
));
|
|
1092
1155
|
}
|
|
1093
|
-
if (
|
|
1156
|
+
if (actionable.length > MAX_NODES) {
|
|
1094
1157
|
lines.push(
|
|
1095
|
-
theme.fg("dim", ` … ${
|
|
1158
|
+
theme.fg("dim", ` … ${actionable.length - MAX_NODES} more`) +
|
|
1096
1159
|
theme.fg("dim", " — /dashboard to expand"),
|
|
1097
1160
|
);
|
|
1098
1161
|
}
|
package/extensions/defaults.ts
CHANGED
|
@@ -77,7 +77,7 @@ function restoreTerminalColors(): void {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** Ownership marker written by scripts/export-kitty-theme.ts into alpharius.conf */
|
|
80
|
-
const KITTY_OWNERSHIP_MARKER = "# Generated
|
|
80
|
+
const KITTY_OWNERSHIP_MARKER = "# Generated by scripts/export-kitty-theme.ts";
|
|
81
81
|
|
|
82
82
|
/** Installed location — standard Kitty themes directory */
|
|
83
83
|
const KITTY_CONF_PATH = path.join(
|