omegon 0.6.27 → 0.6.29

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.
@@ -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): 1 line dashboard summary only
6
- * Layer 1 (raised): uncapped section details, branch tree, and footer metadata
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
- const theme = this.theme;
222
- const lines: string[] = [];
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
- // Width breakpoints expand details as space allows
225
- const wide = width >= 120;
226
- const ultraWide = width >= 160;
226
+ // ── Raised Mode (Layer 1) ─────────────────────────────────────
227
227
 
228
- // Line 1: Dashboard summary + context gauge
229
- const dashParts: PrioritySegment[] = [];
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
- // Design tree summary — responsive expansion
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
- const recoveryLine = this.buildRecoveryCompactSummary(width, wide);
323
- if (recoveryLine) {
324
- dashParts.push({ text: recoveryLine });
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
- // Context gauge — wider bar at wider terminals
328
- const barWidth = ultraWide ? 24 : wide ? 20 : 16;
329
- const gauge = this.buildContextGauge(barWidth);
330
- if (gauge) {
331
- dashParts.push({ text: gauge });
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
- // Compact mode should stay dashboard-first, but still expose the active
335
- // provider/model in a terse way so multi-provider routing is visible.
336
- const ctx = this.ctxRef;
337
- const model = ctx?.model;
338
- if (model && wide) {
339
- const multiProvider = this.footerData.getAvailableProviderCount() > 1;
340
- const driverLabel = multiProvider ? model.provider : "default";
341
- const modelLabel = multiProvider ? `${driverLabel}/${model.id}` : model.id;
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
- // Append /dash hint for discoverability (varies by mode)
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
- // ── Raised Mode (Layer 1) ─────────────────────────────────────
273
+ private buildRaisedBodySeparator(width: number): string {
274
+ return this.theme.fg("dim", BOX.h.repeat(Math.max(0, width)));
275
+ }
366
276
 
367
- private renderRaised(width: number): string[] {
368
- if (width < RAISED_NARROW_WIDTH) return this.renderRaisedNarrow(width);
369
- if (width < RAISED_WIDE_WIDTH) return this.renderRaisedMedium(width);
370
- return this.renderRaisedWide(width);
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 leftColWidth = Math.floor((innerWidth - 1) / 2);
503
- const rightColWidth = innerWidth - leftColWidth - 1;
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 leftLines = [
513
- ...this.buildDesignTreeLines(leftColWidth),
514
- ...this.buildRecoveryLines(leftColWidth),
515
- ...this.buildCleaveLines(leftColWidth),
420
+ const rightLines = [
421
+ ...this.buildOpenSpecLines(preferredRightWidth),
422
+ ...this.buildCleaveLines(preferredRightWidth),
423
+ ...this.buildRecoveryLines(preferredRightWidth),
516
424
  ];
517
- const rightLines = this.buildOpenSpecLines(rightColWidth);
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
- ...(leftLines.length > 0 || rightLines.length > 0
522
- ? mergeColumns(leftLines, rightLines, leftColWidth, rightColWidth, colDivider)
523
- : []),
430
+ this.buildRaisedBodySeparator(innerWidth),
431
+ ...(useRail
432
+ ? mergeColumns(leftLines, rightLines, preferredLeftWidth, preferredRightWidth, colDivider)
433
+ : leftLines),
524
434
  ];
525
435
 
526
- // Same as stacked: natural content height, grows up from footer as needed.
527
- return this.renderBoxed(contentLines, this.buildFooterZone(innerWidth), topLine, width);
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) keeps the same work summary above but gives the
532
- * lower footer zone enough width to render distinct horizontal summary cards.
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 leftColWidth = Math.floor((innerWidth - 1) / 2);
537
- const rightColWidth = innerWidth - leftColWidth - 1;
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 leftLines = [
545
- ...this.buildDesignTreeLines(leftColWidth),
546
- ...this.buildRecoveryLines(leftColWidth),
547
- ...this.buildCleaveLines(leftColWidth),
458
+ const rightLines = [
459
+ ...this.buildOpenSpecLines(preferredRightWidth),
460
+ ...this.buildCleaveLines(preferredRightWidth),
461
+ ...this.buildRecoveryLines(preferredRightWidth),
548
462
  ];
549
- const rightLines = this.buildOpenSpecLines(rightColWidth);
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
- ...(leftLines.length > 0 || rightLines.length > 0
554
- ? mergeColumns(leftLines, rightLines, leftColWidth, rightColWidth, colDivider)
555
- : []),
468
+ this.buildRaisedBodySeparator(innerWidth),
469
+ ...(useRail
470
+ ? mergeColumns(leftLines, rightLines, preferredLeftWidth, preferredRightWidth, colDivider)
471
+ : leftLines),
556
472
  ];
557
473
 
558
- return this.renderBoxed(contentLines, this.buildFooterZone(innerWidth), topLine, width);
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
- // In compact mode, use single-char glyphs to save space
814
- const sourceBadge = compact
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 = compact
822
- ? "" // state is implicit in compact mode — icon color carries meaning
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 = compact ? "" : (normalized.alias ? theme.fg("dim", `alias ${normalized.alias}`) : "");
832
- const sep = compact ? " " : ` ${theme.fg("dim", "·")} `;
833
- const primary = `${theme.fg("accent", summary.label)}${sep}${theme.fg("muted", normalized.canonical)}`;
834
- return truncateToWidth(composePrimaryMetaLine(width, primary, [sourceBadge, stateBadge, summary.detail ? theme.fg("dim", summary.detail) : "", alias].filter(Boolean)), width, "…");
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 buildFooterZone(width: number): string[] {
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 contextCard = this.buildSummaryCard("context", this.buildHudContextLines(Math.max(1, width - 2)).map((l) => l.trimStart()), width);
846
- const modelCard = this.buildSummaryCard(
847
- "models",
848
- this.buildModelTopologySummaries().map((s) => this.formatModelTopologyLine(s, Math.max(1, width - 2), width < 120)),
849
- width,
850
- );
851
- const memoryCard = this.buildSummaryCard("memory", (() => {
852
- const line = this.buildHudMemoryLine(Math.max(1, width - 2));
853
- return line ? [line.trimStart()] : [];
854
- })(), width);
855
- const systemCard = this.buildSummaryCard("system", this.buildHudSystemLines(Math.max(1, width - 2)).map((l) => l.trimStart()), width);
856
- const recoveryCard = this.buildSummaryCard("recovery", this.buildRecoveryLines(Math.max(1, width - 2)).map((l) => l.trimStart()), width);
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
- return mergeColumns(left, right, colWidth, rightWidth, this.theme.fg("dim", BOX.v));
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: three columns context+memory | models | system+recovery
878
- // Gives each column ~50 chars at 160 cols, enough to avoid truncation.
879
- const leftCard = [...contextCard, ...memoryCard];
880
- const midCard = modelCard;
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 leftMid = mergeColumns(leftCard, midCard, colW, colW, divider);
886
- return mergeColumns(leftMid, rightCard, colW * 2 + 1, lastColW, divider);
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 buildRecoveryCompactSummary(width: number, wide: boolean): string {
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. 'escalate' is the only case where the operator must act.
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: terse badge. Wide adds provider/model context.
915
- // Escalate appends a dim command hint so the operator knows what to do.
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
- return composePrimaryMetaLine(width,
923
- theme.fg(actionColor, `${icon} ${actionLabel}`),
924
- [summary ? theme.fg("dim", summary) : "", cooldown ? theme.fg("dim", cooldown) : "", escalateHint].filter(Boolean),
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(actionColor, actionLabel), theme.fg("dim", recovery.classification)];
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
- return " " + theme.fg("dim", T.ann + node.title);
132
+ const title = compactAnnotationTitle(node.title);
133
+ return title ? " " + theme.fg("dim", T.ann + title) : "";
126
134
  }
127
135
 
128
136
  /**