omegon 0.6.26 → 0.6.28
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/bootstrap/index.ts +12 -2
- package/extensions/dashboard/footer.ts +228 -207
- package/extensions/dashboard/git.ts +9 -1
- package/extensions/openspec/lifecycle-files.ts +50 -0
- package/extensions/project-memory/README.md +2 -0
- package/extensions/project-memory/index.ts +51 -14
- package/extensions/project-memory/jsonl-io.ts +29 -5
- package/node_modules/fast-xml-builder/CHANGELOG.md +3 -1
- package/node_modules/fast-xml-builder/lib/fxb.cjs +1 -1
- package/node_modules/fast-xml-builder/lib/fxb.d.cts +7 -0
- package/node_modules/fast-xml-builder/lib/fxb.min.js +1 -1
- package/node_modules/fast-xml-builder/lib/fxb.min.js.map +1 -1
- package/node_modules/fast-xml-builder/package.json +1 -1
- package/node_modules/fast-xml-builder/src/fxb.d.ts +7 -0
- package/node_modules/fast-xml-builder/src/fxb.js +4 -1
- package/node_modules/fast-xml-builder/src/orderedJs2Xml.js +4 -0
- package/node_modules/undici/lib/web/fetch/index.js +4 -1
- package/node_modules/undici/package.json +1 -1
- package/package.json +1 -1
|
@@ -471,8 +471,9 @@ function restartOmegon(): never {
|
|
|
471
471
|
"done",
|
|
472
472
|
// Extra grace period for fd/terminal release
|
|
473
473
|
"sleep 0.2",
|
|
474
|
-
//
|
|
475
|
-
//
|
|
474
|
+
// Pop kitty keyboard protocol and bracketed paste before resetting —
|
|
475
|
+
// stty sane only resets line discipline, not terminal protocol state
|
|
476
|
+
"printf '\\033[<u\\033[>4;0m\\033[?2004l' 2>/dev/null",
|
|
476
477
|
"stty sane 2>/dev/null",
|
|
477
478
|
// Clean up this script
|
|
478
479
|
`rm -f "${script}"`,
|
|
@@ -483,6 +484,15 @@ function restartOmegon(): never {
|
|
|
483
484
|
// Reset terminal to cooked mode BEFORE exiting so the restart script
|
|
484
485
|
// (and the user) aren't stuck with raw-mode terminal if something goes wrong.
|
|
485
486
|
try {
|
|
487
|
+
// Pop kitty keyboard protocol and disable bracketed paste BEFORE
|
|
488
|
+
// releasing raw mode — the terminal needs these escape sequences to
|
|
489
|
+
// stop encoding keystrokes in CSI-u format. Without this, the restart
|
|
490
|
+
// script (and any keystrokes during the wait) dump raw kitty sequences.
|
|
491
|
+
process.stdout.write(
|
|
492
|
+
"\x1b[<u" + // Pop kitty keyboard protocol flags
|
|
493
|
+
"\x1b[>4;0m" + // Disable modifyOtherKeys
|
|
494
|
+
"\x1b[?2004l" // Disable bracketed paste
|
|
495
|
+
);
|
|
486
496
|
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
487
497
|
process.stdin.setRawMode(false);
|
|
488
498
|
}
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Custom footer component for the unified dashboard.
|
|
3
3
|
*
|
|
4
4
|
* Implements two rendering modes:
|
|
5
|
-
* Layer 0 (compact):
|
|
6
|
-
* Layer 1 (raised): uncapped
|
|
5
|
+
* Layer 0 (compact): persistent runtime HUD with compact telemetry cards
|
|
6
|
+
* Layer 1 (raised): uncapped workspace + lifecycle surfaces above the HUD
|
|
7
7
|
*
|
|
8
8
|
* Reads sharedState for design-tree, openspec, and cleave data.
|
|
9
9
|
* Reads footerData for git branch, extension statuses, provider count.
|
|
@@ -218,156 +218,66 @@ export class DashboardFooter implements Component {
|
|
|
218
218
|
// ── Compact Mode (Layer 0) ────────────────────────────────────
|
|
219
219
|
|
|
220
220
|
private renderCompact(width: number): string[] {
|
|
221
|
-
|
|
222
|
-
|
|
221
|
+
// Compact mode is the persistent runtime HUD. Raised mode should reveal the
|
|
222
|
+
// work surfaces above it, not replace it with a different footer grammar.
|
|
223
|
+
return this.buildFooterZone(width, width, true);
|
|
224
|
+
}
|
|
223
225
|
|
|
224
|
-
|
|
225
|
-
const wide = width >= 120;
|
|
226
|
-
const ultraWide = width >= 160;
|
|
226
|
+
// ── Raised Mode (Layer 1) ─────────────────────────────────────
|
|
227
227
|
|
|
228
|
-
|
|
229
|
-
|
|
228
|
+
private renderRaised(width: number): string[] {
|
|
229
|
+
if (width < RAISED_NARROW_WIDTH) return this.renderRaisedNarrow(width);
|
|
230
|
+
if (width < RAISED_WIDE_WIDTH) return this.renderRaisedMedium(width);
|
|
231
|
+
return this.renderRaisedWide(width);
|
|
232
|
+
}
|
|
230
233
|
|
|
231
|
-
|
|
234
|
+
private buildRaisedHeaderSummary(width: number): string {
|
|
235
|
+
const theme = this.theme;
|
|
236
|
+
const summary: string[] = [];
|
|
232
237
|
const dt = sharedState.designTree;
|
|
233
|
-
if (dt && dt.nodeCount > 0) {
|
|
234
|
-
if (ultraWide && dt.focusedNode) {
|
|
235
|
-
// Ultra-wide: show focused node title inline
|
|
236
|
-
const statusIcon = dt.focusedNode.status === "resolved" ? "◉"
|
|
237
|
-
: dt.focusedNode.status === "decided" ? "●"
|
|
238
|
-
: dt.focusedNode.status === "implementing" ? "⚙"
|
|
239
|
-
: dt.focusedNode.status === "exploring" ? "◐"
|
|
240
|
-
: "○";
|
|
241
|
-
const qSuffix = dt.focusedNode.questions.length > 0
|
|
242
|
-
? theme.fg("dim", ` (${dt.focusedNode.questions.length}?)`)
|
|
243
|
-
: "";
|
|
244
|
-
dashParts.push({
|
|
245
|
-
text: theme.fg("accent", `◈ ${dt.decidedCount}/${dt.nodeCount}`) +
|
|
246
|
-
` ${statusIcon} ${dt.focusedNode.title}${qSuffix}`,
|
|
247
|
-
});
|
|
248
|
-
} else if (wide) {
|
|
249
|
-
// Wide: spell out counts, no node IDs (visible in raised mode)
|
|
250
|
-
const parts = [`${dt.decidedCount} decided`];
|
|
251
|
-
if (dt.exploringCount > 0) parts.push(`${dt.exploringCount} exploring`);
|
|
252
|
-
if (dt.implementingCount > 0) parts.push(`${dt.implementingCount} impl`);
|
|
253
|
-
if (dt.openQuestionCount > 0) parts.push(`${dt.openQuestionCount}?`);
|
|
254
|
-
dashParts.push({ text: theme.fg("accent", `◈ Design`) + theme.fg("dim", ` ${parts.join(", ")}`) });
|
|
255
|
-
} else {
|
|
256
|
-
// Narrow: terse
|
|
257
|
-
let dtSummary = `◈ D:${dt.decidedCount}`;
|
|
258
|
-
if (dt.implementingCount > 0) dtSummary += ` I:${dt.implementingCount}`;
|
|
259
|
-
dtSummary += `/${dt.nodeCount}`;
|
|
260
|
-
dashParts.push({ text: theme.fg("accent", dtSummary) });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// OpenSpec summary — responsive expansion
|
|
265
238
|
const os = sharedState.openspec;
|
|
266
|
-
if (os && os.changes.length > 0) {
|
|
267
|
-
const active = os.changes.filter(c => c.stage !== "archived");
|
|
268
|
-
if (active.length > 0) {
|
|
269
|
-
if (wide) {
|
|
270
|
-
// Wide: aggregate progress only — individual changes visible in raised mode
|
|
271
|
-
const totalDone = active.reduce((s, c) => s + c.tasksDone, 0);
|
|
272
|
-
const totalAll = active.reduce((s, c) => s + c.tasksTotal, 0);
|
|
273
|
-
const allDone = totalAll > 0 && totalDone >= totalAll;
|
|
274
|
-
const progress = totalAll > 0
|
|
275
|
-
? theme.fg(allDone ? "success" : "dim", ` ${totalDone}/${totalAll}`)
|
|
276
|
-
: "";
|
|
277
|
-
const icon = allDone ? theme.fg("success", " ✓") : "";
|
|
278
|
-
dashParts.push({
|
|
279
|
-
text: theme.fg("accent", `◎ Impl`) +
|
|
280
|
-
theme.fg("dim", ` ${active.length} change${active.length > 1 ? "s" : ""}`) +
|
|
281
|
-
progress + icon,
|
|
282
|
-
});
|
|
283
|
-
} else {
|
|
284
|
-
dashParts.push({ text: theme.fg("accent", `◎ Impl:${active.length}`) });
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Cleave summary — responsive expansion
|
|
290
239
|
const cl = sharedState.cleave;
|
|
291
|
-
if (cl) {
|
|
292
|
-
if (cl.status === "idle") {
|
|
293
|
-
dashParts.push({ text: theme.fg("dim", "⚡ idle") });
|
|
294
|
-
} else if (cl.status === "done") {
|
|
295
|
-
const childInfo = wide && cl.children
|
|
296
|
-
? ` ${cl.children.filter(c => c.status === "done").length}/${cl.children.length}`
|
|
297
|
-
: "";
|
|
298
|
-
dashParts.push({ text: theme.fg("success", `⚡ done${childInfo}`) });
|
|
299
|
-
} else if (cl.status === "failed") {
|
|
300
|
-
dashParts.push({ text: theme.fg("error", "⚡ fail") });
|
|
301
|
-
} else {
|
|
302
|
-
// Active dispatch — show child progress + lastLine activity hint
|
|
303
|
-
if (wide && cl.children && cl.children.length > 0) {
|
|
304
|
-
const done = cl.children.filter(c => c.status === "done").length;
|
|
305
|
-
const running = cl.children.filter(c => c.status === "running").length;
|
|
306
|
-
// Show the last active line from whichever running child has one
|
|
307
|
-
const activeChild = cl.children.find(c => c.status === "running" && c.lastLine);
|
|
308
|
-
const activityHint = activeChild?.lastLine
|
|
309
|
-
? theme.fg("dim", ` ${activeChild.lastLine.slice(0, 40)}…`)
|
|
310
|
-
: "";
|
|
311
|
-
dashParts.push({
|
|
312
|
-
text: theme.fg("warning", `⚡ ${cl.status}`) +
|
|
313
|
-
theme.fg("dim", ` ${done}✓ ${running}⟳ /${cl.children.length}`) +
|
|
314
|
-
activityHint,
|
|
315
|
-
});
|
|
316
|
-
} else {
|
|
317
|
-
dashParts.push({ text: theme.fg("warning", `⚡ ${cl.status}`) });
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
240
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
241
|
+
if (dt) {
|
|
242
|
+
const parts: string[] = [];
|
|
243
|
+
if (dt.decidedCount > 0) parts.push(theme.fg("success", `${dt.decidedCount} decided`));
|
|
244
|
+
if (dt.implementingCount > 0) parts.push(theme.fg("accent", `${dt.implementingCount} implementing`));
|
|
245
|
+
if (dt.exploringCount > 0) parts.push(theme.fg("muted", `${dt.exploringCount} exploring`));
|
|
246
|
+
if (dt.openQuestionCount > 0) parts.push(theme.fg("dim", `${dt.openQuestionCount}?`));
|
|
247
|
+
if (parts.length > 0) summary.push(`${theme.fg("accent", "Design")} ${parts.join(theme.fg("dim", " · "))}`);
|
|
325
248
|
}
|
|
326
249
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
250
|
+
if (os) {
|
|
251
|
+
const active = os.changes.filter((c) => c.stage !== "archived");
|
|
252
|
+
if (active.length > 0) {
|
|
253
|
+
const totalDone = active.reduce((sum, c) => sum + c.tasksDone, 0);
|
|
254
|
+
const totalAll = active.reduce((sum, c) => sum + c.tasksTotal, 0);
|
|
255
|
+
const progress = totalAll > 0 ? theme.fg("dim", `${totalDone}/${totalAll}`) : theme.fg("dim", `${active.length}`);
|
|
256
|
+
summary.push(`${theme.fg("accent", "Impl")} ${progress}`);
|
|
257
|
+
}
|
|
332
258
|
}
|
|
333
259
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
dashParts.push({
|
|
343
|
-
text: theme.fg("dim", "Model ") + theme.fg("muted", modelLabel),
|
|
344
|
-
priority: "low",
|
|
345
|
-
});
|
|
260
|
+
if (cl && cl.status !== "idle") {
|
|
261
|
+
const children = cl.children ?? [];
|
|
262
|
+
const doneCount = children.filter((c) => c.status === "done").length;
|
|
263
|
+
const failCount = children.filter((c) => c.status === "failed").length;
|
|
264
|
+
const parts = [theme.fg(cl.status === "done" ? "success" : cl.status === "failed" ? "error" : "warning", cl.status)];
|
|
265
|
+
if (children.length > 0) parts.push(theme.fg("dim", `${doneCount}/${children.length}`));
|
|
266
|
+
if (failCount > 0) parts.push(theme.fg("error", `${failCount}✕`));
|
|
267
|
+
summary.push(`${theme.fg("accent", "Cleave")} ${parts.join(theme.fg("dim", " · "))}`);
|
|
346
268
|
}
|
|
347
269
|
|
|
348
|
-
|
|
349
|
-
const dashHint = this.dashState.mode === "panel"
|
|
350
|
-
? theme.fg("dim", "/dashboard to close")
|
|
351
|
-
: theme.fg("dim", "/dash to expand");
|
|
352
|
-
|
|
353
|
-
const compactLine = joinPrioritySegments(width, [
|
|
354
|
-
...dashParts,
|
|
355
|
-
{ text: dashHint, priority: "low" },
|
|
356
|
-
]);
|
|
357
|
-
lines.push(compactLine || truncateToWidth(dashHint, width, "…"));
|
|
358
|
-
|
|
359
|
-
// Compact mode is intentionally dashboard-only. Detailed footer metadata
|
|
360
|
-
// stays in raised mode so the compact footer does not look like the built-in
|
|
361
|
-
// footer is still leaking through.
|
|
362
|
-
return lines;
|
|
270
|
+
return joinPrioritySegments(width, summary.map((text) => ({ text, priority: "low" as const })), " ");
|
|
363
271
|
}
|
|
364
272
|
|
|
365
|
-
|
|
273
|
+
private buildRaisedBodySeparator(width: number): string {
|
|
274
|
+
return this.theme.fg("dim", BOX.h.repeat(Math.max(0, width)));
|
|
275
|
+
}
|
|
366
276
|
|
|
367
|
-
private
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
return
|
|
277
|
+
private buildRaisedTopLine(topLine: string, innerWidth: number): string {
|
|
278
|
+
const summaryWidth = Math.max(0, innerWidth - visibleWidth(topLine) - 1);
|
|
279
|
+
const headerSummary = this.buildRaisedHeaderSummary(summaryWidth);
|
|
280
|
+
return headerSummary ? leftRight(topLine, headerSummary, innerWidth - 1) : topLine;
|
|
371
281
|
}
|
|
372
282
|
|
|
373
283
|
/**
|
|
@@ -491,7 +401,7 @@ export class DashboardFooter implements Component {
|
|
|
491
401
|
// Render at natural content height — the box grows upward from the footer
|
|
492
402
|
// as branches/specs/cleave tasks are added. Full-screen expansion lives
|
|
493
403
|
// in the /dashboard overlay (overlay.ts), not here.
|
|
494
|
-
return this.renderBoxed(contentLines, this.buildFooterZone(innerWidth), topLine, width);
|
|
404
|
+
return this.renderBoxed(contentLines, this.buildFooterZone(innerWidth, width), topLine, width);
|
|
495
405
|
}
|
|
496
406
|
|
|
497
407
|
/**
|
|
@@ -499,63 +409,74 @@ export class DashboardFooter implements Component {
|
|
|
499
409
|
*/
|
|
500
410
|
private renderRaisedMedium(width: number): string[] {
|
|
501
411
|
const innerWidth = width - 4;
|
|
502
|
-
const
|
|
503
|
-
const
|
|
412
|
+
const preferredLeftWidth = Math.floor((innerWidth - 1) * 0.6);
|
|
413
|
+
const preferredRightWidth = innerWidth - preferredLeftWidth - 1;
|
|
504
414
|
const colDivider = this.theme.fg("dim", BOX.v);
|
|
505
415
|
|
|
506
416
|
const branchLines = this.buildBranchTree(innerWidth);
|
|
507
417
|
const [topLine = "", ...extraBranchLines] = branchLines;
|
|
508
|
-
|
|
509
|
-
// Same 1-char alignment correction as renderRaisedStacked.
|
|
510
418
|
const alignedBranchLines = extraBranchLines.map((l) => " " + l);
|
|
511
419
|
|
|
512
|
-
const
|
|
513
|
-
...this.
|
|
514
|
-
...this.
|
|
515
|
-
...this.
|
|
420
|
+
const rightLines = [
|
|
421
|
+
...this.buildOpenSpecLines(preferredRightWidth),
|
|
422
|
+
...this.buildCleaveLines(preferredRightWidth),
|
|
423
|
+
...this.buildRecoveryLines(preferredRightWidth),
|
|
516
424
|
];
|
|
517
|
-
const
|
|
425
|
+
const useRail = rightLines.length > 0;
|
|
426
|
+
const leftLines = this.buildDesignTreeLines(useRail ? preferredLeftWidth : innerWidth);
|
|
518
427
|
|
|
519
428
|
const contentLines: string[] = [
|
|
520
429
|
...alignedBranchLines,
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
430
|
+
this.buildRaisedBodySeparator(innerWidth),
|
|
431
|
+
...(useRail
|
|
432
|
+
? mergeColumns(leftLines, rightLines, preferredLeftWidth, preferredRightWidth, colDivider)
|
|
433
|
+
: leftLines),
|
|
524
434
|
];
|
|
525
435
|
|
|
526
|
-
|
|
527
|
-
|
|
436
|
+
return this.renderBoxed(
|
|
437
|
+
contentLines,
|
|
438
|
+
this.buildFooterZone(innerWidth, width),
|
|
439
|
+
this.buildRaisedTopLine(topLine, innerWidth),
|
|
440
|
+
width,
|
|
441
|
+
);
|
|
528
442
|
}
|
|
529
443
|
|
|
530
444
|
/**
|
|
531
|
-
* Wide layout (140+ cols)
|
|
532
|
-
*
|
|
445
|
+
* Wide layout (140+ cols) prioritizes a design-dominant main workspace with a
|
|
446
|
+
* narrower contextual rail for implementation, cleave, and recovery state.
|
|
533
447
|
*/
|
|
534
448
|
private renderRaisedWide(width: number): string[] {
|
|
535
449
|
const innerWidth = width - 4;
|
|
536
|
-
const
|
|
537
|
-
const
|
|
450
|
+
const preferredLeftWidth = Math.floor((innerWidth - 1) * 0.72);
|
|
451
|
+
const preferredRightWidth = innerWidth - preferredLeftWidth - 1;
|
|
538
452
|
const colDivider = this.theme.fg("dim", BOX.v);
|
|
539
453
|
|
|
540
454
|
const branchLines = this.buildBranchTree(innerWidth);
|
|
541
455
|
const [topLine = "", ...extraBranchLines] = branchLines;
|
|
542
456
|
const alignedBranchLines = extraBranchLines.map((l) => " " + l);
|
|
543
457
|
|
|
544
|
-
const
|
|
545
|
-
...this.
|
|
546
|
-
...this.
|
|
547
|
-
...this.
|
|
458
|
+
const rightLines = [
|
|
459
|
+
...this.buildOpenSpecLines(preferredRightWidth),
|
|
460
|
+
...this.buildCleaveLines(preferredRightWidth),
|
|
461
|
+
...this.buildRecoveryLines(preferredRightWidth),
|
|
548
462
|
];
|
|
549
|
-
const
|
|
463
|
+
const useRail = rightLines.length > 0;
|
|
464
|
+
const leftLines = this.buildDesignTreeLines(useRail ? preferredLeftWidth : innerWidth);
|
|
550
465
|
|
|
551
466
|
const contentLines: string[] = [
|
|
552
467
|
...alignedBranchLines,
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
468
|
+
this.buildRaisedBodySeparator(innerWidth),
|
|
469
|
+
...(useRail
|
|
470
|
+
? mergeColumns(leftLines, rightLines, preferredLeftWidth, preferredRightWidth, colDivider)
|
|
471
|
+
: leftLines),
|
|
556
472
|
];
|
|
557
473
|
|
|
558
|
-
return this.renderBoxed(
|
|
474
|
+
return this.renderBoxed(
|
|
475
|
+
contentLines,
|
|
476
|
+
this.buildFooterZone(innerWidth),
|
|
477
|
+
this.buildRaisedTopLine(topLine, innerWidth),
|
|
478
|
+
width,
|
|
479
|
+
);
|
|
559
480
|
}
|
|
560
481
|
|
|
561
482
|
// ── HUD Footer Zone (raised mode) ────────────────────────────
|
|
@@ -810,16 +731,17 @@ export class DashboardFooter implements Component {
|
|
|
810
731
|
|
|
811
732
|
private formatModelTopologyLine(summary: DashboardModelRoleSummary, width: number, compact = false): string {
|
|
812
733
|
const theme = this.theme;
|
|
813
|
-
|
|
814
|
-
|
|
734
|
+
const forceCompact = compact || width < 40;
|
|
735
|
+
// In compact mode, use single-char glyphs to save space.
|
|
736
|
+
const sourceBadge = forceCompact
|
|
815
737
|
? (summary.source === "local" ? theme.fg("accent", "⌂") : summary.source === "cloud" ? theme.fg("muted", "☁") : theme.fg("dim", "?"))
|
|
816
738
|
: (summary.source === "local"
|
|
817
739
|
? theme.fg("accent", "local")
|
|
818
740
|
: summary.source === "cloud"
|
|
819
741
|
? theme.fg("muted", "cloud")
|
|
820
742
|
: theme.fg("dim", summary.source));
|
|
821
|
-
const stateBadge =
|
|
822
|
-
? ""
|
|
743
|
+
const stateBadge = forceCompact
|
|
744
|
+
? ""
|
|
823
745
|
: (summary.state === "active"
|
|
824
746
|
? theme.fg("success", "active")
|
|
825
747
|
: summary.state === "offline"
|
|
@@ -828,10 +750,18 @@ export class DashboardFooter implements Component {
|
|
|
828
750
|
? theme.fg("warning", "fallback")
|
|
829
751
|
: theme.fg("dim", summary.state));
|
|
830
752
|
const normalized = normalizeLocalModelLabel(summary.model);
|
|
831
|
-
const alias =
|
|
832
|
-
const
|
|
833
|
-
const primary = `${theme.fg("accent",
|
|
834
|
-
return truncateToWidth(
|
|
753
|
+
const alias = forceCompact ? "" : (normalized.alias ? theme.fg("dim", `alias ${normalized.alias}`) : "");
|
|
754
|
+
const roleLabel = forceCompact ? summary.label.slice(0, 1) : summary.label;
|
|
755
|
+
const primary = `${theme.fg("accent", roleLabel)} ${theme.fg("muted", normalized.canonical)}`;
|
|
756
|
+
return truncateToWidth(
|
|
757
|
+
composePrimaryMetaLine(
|
|
758
|
+
width,
|
|
759
|
+
primary,
|
|
760
|
+
[sourceBadge, stateBadge, summary.detail ? theme.fg("dim", summary.detail) : "", alias].filter(Boolean),
|
|
761
|
+
),
|
|
762
|
+
width,
|
|
763
|
+
"…",
|
|
764
|
+
);
|
|
835
765
|
}
|
|
836
766
|
|
|
837
767
|
private buildSummaryCard(title: string, lines: string[], width: number): string[] {
|
|
@@ -839,21 +769,48 @@ export class DashboardFooter implements Component {
|
|
|
839
769
|
return [this.buildHudSectionDivider(title, width), ...lines.map((line) => truncateToWidth(` ${line}`, width, "…"))];
|
|
840
770
|
}
|
|
841
771
|
|
|
842
|
-
private
|
|
772
|
+
private buildSummaryCardForColumn(title: string, lines: string[], columnWidth: number, contentWidth: number): string[] {
|
|
773
|
+
if (lines.length === 0) return [];
|
|
774
|
+
return this.buildSummaryCard(title, lines, Math.max(1, columnWidth)).map((line) => truncateToWidth(line, Math.max(1, contentWidth), "…"));
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
private buildFooterHintLine(width: number): string {
|
|
778
|
+
const hint = this.dashState.mode === "panel"
|
|
779
|
+
? "/dashboard to close"
|
|
780
|
+
: "/dash to expand · /dashboard modal";
|
|
781
|
+
return truncateToWidth(this.theme.fg("dim", hint), Math.max(1, width), "…");
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
private buildFooterZone(width: number, totalWidth = width, compactPersistent = false): string[] {
|
|
843
785
|
this._updateTokenCache();
|
|
844
786
|
|
|
845
|
-
const
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
787
|
+
const buildCards = (cardWidth: number) => {
|
|
788
|
+
const safeWidth = Math.max(1, cardWidth);
|
|
789
|
+
return {
|
|
790
|
+
contextCard: this.buildSummaryCard("context", this.buildHudContextLines(Math.max(1, safeWidth - 2)).map((l) => l.trimStart()), safeWidth),
|
|
791
|
+
modelCard: this.buildSummaryCard(
|
|
792
|
+
"models",
|
|
793
|
+
this.buildModelTopologySummaries().map((s) => this.formatModelTopologyLine(s, Math.max(1, safeWidth - 2), safeWidth < 44)),
|
|
794
|
+
safeWidth,
|
|
795
|
+
),
|
|
796
|
+
memoryCard: this.buildSummaryCard("memory", (() => {
|
|
797
|
+
const line = this.buildHudMemoryLine(Math.max(1, safeWidth - 2));
|
|
798
|
+
return line ? [line.trimStart()] : [];
|
|
799
|
+
})(), safeWidth),
|
|
800
|
+
systemCard: this.buildSummaryCard("system", this.buildHudSystemLines(Math.max(1, safeWidth - 2)).map((l) => l.trimStart()), safeWidth),
|
|
801
|
+
recoveryCard: this.buildSummaryCard(
|
|
802
|
+
"recovery",
|
|
803
|
+
(compactPersistent
|
|
804
|
+
? this.buildRecoveryCompactLines(Math.max(1, safeWidth - 2))
|
|
805
|
+
: this.buildRecoveryLines(Math.max(1, safeWidth - 2))
|
|
806
|
+
).map((l) => l.trimStart()),
|
|
807
|
+
safeWidth,
|
|
808
|
+
),
|
|
809
|
+
};
|
|
810
|
+
};
|
|
811
|
+
|
|
812
|
+
const { contextCard, modelCard, memoryCard, systemCard, recoveryCard } = buildCards(width);
|
|
813
|
+
const footerHintLine = compactPersistent ? this.buildFooterHintLine(width) : undefined;
|
|
857
814
|
|
|
858
815
|
if (width < RAISED_NARROW_WIDTH) {
|
|
859
816
|
return [
|
|
@@ -862,6 +819,7 @@ export class DashboardFooter implements Component {
|
|
|
862
819
|
...memoryCard,
|
|
863
820
|
...(recoveryCard.length > 0 ? recoveryCard : []),
|
|
864
821
|
...systemCard,
|
|
822
|
+
...(footerHintLine ? [footerHintLine] : []),
|
|
865
823
|
];
|
|
866
824
|
}
|
|
867
825
|
|
|
@@ -871,34 +829,89 @@ export class DashboardFooter implements Component {
|
|
|
871
829
|
const right = [...modelCard, ...(recoveryCard.length > 0 ? recoveryCard : []), ...systemCard];
|
|
872
830
|
const colWidth = Math.floor((width - 1) / 2);
|
|
873
831
|
const rightWidth = width - colWidth - 1;
|
|
874
|
-
|
|
832
|
+
const merged = mergeColumns(left, right, colWidth, rightWidth, this.theme.fg("dim", BOX.v));
|
|
833
|
+
return footerHintLine ? [...merged, footerHintLine] : merged;
|
|
875
834
|
}
|
|
876
835
|
|
|
877
|
-
// Wide:
|
|
878
|
-
//
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
const rightCard = [...(recoveryCard.length > 0 ? recoveryCard : []), ...systemCard];
|
|
882
|
-
const colW = Math.floor((width - 2) / 3);
|
|
883
|
-
const lastColW = width - colW * 2 - 2;
|
|
836
|
+
// Wide/full-screen: keep the final memory|system divider running all the way
|
|
837
|
+
// to the box base, and align that divider with the raised body split above.
|
|
838
|
+
// In persistent compact mode there is no upper split, so use a balanced
|
|
839
|
+
// four-column HUD instead of the raised work-area proportions.
|
|
884
840
|
const divider = this.theme.fg("dim", BOX.v);
|
|
885
|
-
const
|
|
886
|
-
|
|
841
|
+
const mainSplit = compactPersistent
|
|
842
|
+
? Math.floor((totalWidth - 1) * 0.75)
|
|
843
|
+
: Math.floor((totalWidth - 1) * 0.72);
|
|
844
|
+
const leftTelemetryWidth = Math.max(3, mainSplit - 2);
|
|
845
|
+
const rightTelemetryWidth = Math.max(1, totalWidth - mainSplit - 1);
|
|
846
|
+
|
|
847
|
+
const col1W = Math.max(1, Math.floor(leftTelemetryWidth * 0.35));
|
|
848
|
+
const col2W = Math.max(1, Math.floor(leftTelemetryWidth * 0.40));
|
|
849
|
+
const col3W = Math.max(1, leftTelemetryWidth - col1W - col2W);
|
|
850
|
+
const col4W = rightTelemetryWidth;
|
|
851
|
+
const wideCards = {
|
|
852
|
+
contextCard: this.buildSummaryCardForColumn("context", this.buildHudContextLines(Math.max(1, col1W - 2)).map((l) => l.trimStart()), col1W, col1W),
|
|
853
|
+
modelCard: this.buildSummaryCardForColumn(
|
|
854
|
+
"models",
|
|
855
|
+
this.buildModelTopologySummaries().map((s) => this.formatModelTopologyLine(s, Math.max(1, col2W - 2), col2W < 44)),
|
|
856
|
+
col2W,
|
|
857
|
+
col2W,
|
|
858
|
+
),
|
|
859
|
+
memoryCard: this.buildSummaryCardForColumn("memory", (() => {
|
|
860
|
+
const line = this.buildHudMemoryLine(Math.max(1, col3W - 2));
|
|
861
|
+
return line ? [line.trimStart()] : [];
|
|
862
|
+
})(), col3W, col3W),
|
|
863
|
+
systemCard: this.buildSummaryCardForColumn("system", this.buildHudSystemLines(Math.max(1, col4W - 2)).map((l) => l.trimStart()), col4W, col4W),
|
|
864
|
+
recoveryCard: this.buildSummaryCardForColumn(
|
|
865
|
+
"recovery",
|
|
866
|
+
(compactPersistent
|
|
867
|
+
? this.buildRecoveryCompactLines(Math.max(1, col4W - 2))
|
|
868
|
+
: this.buildRecoveryLines(Math.max(1, col4W - 2))
|
|
869
|
+
).map((l) => l.trimStart()),
|
|
870
|
+
col4W,
|
|
871
|
+
col4W,
|
|
872
|
+
),
|
|
873
|
+
};
|
|
874
|
+
const col1 = wideCards.contextCard;
|
|
875
|
+
const col2 = wideCards.modelCard;
|
|
876
|
+
const col3 = wideCards.memoryCard;
|
|
877
|
+
const col4 = [...(wideCards.recoveryCard.length > 0 ? wideCards.recoveryCard : []), ...wideCards.systemCard];
|
|
878
|
+
|
|
879
|
+
const rows = Math.max(col1.length, col2.length, col3.length, col4.length);
|
|
880
|
+
const merged: string[] = [];
|
|
881
|
+
for (let i = 0; i < rows; i++) {
|
|
882
|
+
const cell1 = i < col1.length
|
|
883
|
+
? padRight(truncateToWidth(col1[i], col1W, "…"), col1W)
|
|
884
|
+
: " ".repeat(col1W);
|
|
885
|
+
const cell2 = i < col2.length
|
|
886
|
+
? padRight(truncateToWidth(col2[i], col2W, "…"), col2W)
|
|
887
|
+
: " ".repeat(col2W);
|
|
888
|
+
const cell3 = i < col3.length
|
|
889
|
+
? padRight(truncateToWidth(col3[i], col3W, "…"), col3W)
|
|
890
|
+
: " ".repeat(col3W);
|
|
891
|
+
const cell4 = i < col4.length
|
|
892
|
+
? padRight(truncateToWidth(col4[i], col4W, "…"), col4W)
|
|
893
|
+
: " ".repeat(col4W);
|
|
894
|
+
merged.push(`${cell1}${divider}${cell2}${divider}${cell3}${divider}${cell4}`);
|
|
895
|
+
}
|
|
896
|
+
return footerHintLine ? [...merged, footerHintLine] : merged;
|
|
887
897
|
}
|
|
888
898
|
|
|
889
899
|
// ── Section builders (shared by stacked + wide layouts) ───────
|
|
890
900
|
|
|
891
|
-
private
|
|
901
|
+
private buildRecoveryCompactLines(width: number): string[] {
|
|
892
902
|
const theme = this.theme;
|
|
893
903
|
const recovery = getRecoveryState();
|
|
894
|
-
if (!recovery) return
|
|
904
|
+
if (!recovery) return [];
|
|
895
905
|
|
|
896
906
|
// Auto-suppress stale recovery notices in compact mode — they outlive their
|
|
897
907
|
// usefulness quickly and crowd out model/driver/thinking info.
|
|
898
|
-
if (Date.now() - recovery.timestamp > RECOVERY_STALE_MS) return
|
|
908
|
+
if (Date.now() - recovery.timestamp > RECOVERY_STALE_MS) return [];
|
|
909
|
+
|
|
910
|
+
// Collapse non-actionable observational notices in compact mode.
|
|
911
|
+
if (recovery.action === "observe") return [];
|
|
899
912
|
|
|
900
913
|
// Past-tense labels for auto-handled actions so they read as status, not
|
|
901
|
-
// directives.
|
|
914
|
+
// directives. 'escalate' is the only case where the operator must act.
|
|
902
915
|
const actionColor: ThemeColor = recovery.action === "retry" ? "warning"
|
|
903
916
|
: recovery.action === "switch_candidate" || recovery.action === "switch_offline" ? "accent"
|
|
904
917
|
: recovery.action === "cooldown" ? "warning"
|
|
@@ -911,18 +924,26 @@ export class DashboardFooter implements Component {
|
|
|
911
924
|
: recovery.action === "escalate" ? "escalated"
|
|
912
925
|
: "observed";
|
|
913
926
|
|
|
914
|
-
// Compact mode
|
|
915
|
-
//
|
|
916
|
-
const summary = wide ? `${recovery.provider}/${recovery.modelId}` : "";
|
|
927
|
+
// Compact mode stays terse: badge + cooldown, with an explicit command hint
|
|
928
|
+
// only when operator intervention is required.
|
|
917
929
|
const cooldown = summarizeCooldown(recovery.cooldowns);
|
|
918
930
|
const escalateHint = recovery.action === "escalate"
|
|
919
931
|
? theme.fg("dim", "→ /set-model-tier")
|
|
920
932
|
: "";
|
|
921
933
|
const icon = recovery.action === "escalate" ? "⚠" : "↺";
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
934
|
+
|
|
935
|
+
const header = composePrimaryMetaLine(
|
|
936
|
+
width,
|
|
937
|
+
theme.fg("accent", `${icon} Recovery`) + theme.fg("dim", " · ") + theme.fg(actionColor, actionLabel),
|
|
938
|
+
[],
|
|
925
939
|
);
|
|
940
|
+
const meta = [cooldown ? theme.fg("dim", cooldown) : "", escalateHint].filter(Boolean);
|
|
941
|
+
if (meta.length === 0) return [header];
|
|
942
|
+
|
|
943
|
+
return [
|
|
944
|
+
header,
|
|
945
|
+
composePrimaryMetaLine(width, "", meta),
|
|
946
|
+
];
|
|
926
947
|
}
|
|
927
948
|
|
|
928
949
|
private buildRecoveryLines(width: number): string[] {
|
|
@@ -951,10 +972,10 @@ export class DashboardFooter implements Component {
|
|
|
951
972
|
? theme.fg("dim", "→ /set-model-tier to switch provider/driver")
|
|
952
973
|
: "";
|
|
953
974
|
|
|
954
|
-
const headerParts = [theme.fg(
|
|
975
|
+
const headerParts = [theme.fg("dim", recovery.classification)];
|
|
955
976
|
const lines = [composePrimaryMetaLine(
|
|
956
977
|
width,
|
|
957
|
-
theme.fg("accent", `${recoveryIcon} Recovery`),
|
|
978
|
+
theme.fg("accent", `${recoveryIcon} Recovery`) + theme.fg("dim", " · ") + theme.fg(actionColor, actionLabel),
|
|
958
979
|
headerParts,
|
|
959
980
|
)];
|
|
960
981
|
if (escalateHint) lines.push(escalateHint);
|
|
@@ -114,6 +114,13 @@ function styledBranch(b: string, isCurrent: boolean, theme: Theme): string {
|
|
|
114
114
|
/**
|
|
115
115
|
* Find annotation for a branch from design nodes.
|
|
116
116
|
*/
|
|
117
|
+
function compactAnnotationTitle(title: string | undefined): string {
|
|
118
|
+
if (!title) return "";
|
|
119
|
+
const trimmed = title.trim();
|
|
120
|
+
const split = trimmed.split(/\s+[—–:]\s+/, 2);
|
|
121
|
+
return split[0] || trimmed;
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
function branchAnnotation(
|
|
118
125
|
b: string,
|
|
119
126
|
designNodes: Array<{ branches?: string[]; title: string }> | undefined,
|
|
@@ -122,7 +129,8 @@ function branchAnnotation(
|
|
|
122
129
|
if (!designNodes) return "";
|
|
123
130
|
const node = designNodes.find((n) => n.branches?.includes(b));
|
|
124
131
|
if (!node) return "";
|
|
125
|
-
|
|
132
|
+
const title = compactAnnotationTitle(node.title);
|
|
133
|
+
return title ? " " + theme.fg("dim", T.ann + title) : "";
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
/**
|