omp-fabric 1.9.0 → 1.10.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.
@@ -25,6 +25,7 @@ import {
25
25
  import {
26
26
  formatActorDataPreview,
27
27
  formatClock,
28
+ formatCost,
28
29
  formatDuration,
29
30
  formatTokens,
30
31
  padToWidth,
@@ -53,8 +54,8 @@ import {
53
54
  getKeybindings as getKeybindings3,
54
55
  Key,
55
56
  matchesKey,
56
- truncateToWidth as truncateToWidth4,
57
- visibleWidth as visibleWidth4
57
+ truncateToWidth as truncateToWidth5,
58
+ visibleWidth as visibleWidth5
58
59
  } from "@oh-my-pi/pi-tui";
59
60
 
60
61
  // src/ui/topology.ts
@@ -988,13 +989,257 @@ var entityTail = (entity, now) => {
988
989
  return [entity.value.owner, entity.value.detail, `v${entity.value.version}`].filter((value) => Boolean(value)).join(" \xB7 ");
989
990
  };
990
991
 
992
+ // src/ui/lcm-panel.ts
993
+ import { truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
994
+ var LCM_NODE_PAGE = 200;
995
+ var COVERAGE_COMPLETE_RATIO = 0.95;
996
+ var EMERGENCY_MODEL_HASH = "emergency";
997
+ var readLcmSnapshot = (source, offset) => {
998
+ const nodes = source.nodes(LCM_NODE_PAGE, offset);
999
+ return {
1000
+ report: source.report(),
1001
+ coverage: source.coverage(),
1002
+ preview: source.preview(),
1003
+ coverageMap: source.coverageMap(),
1004
+ nodes,
1005
+ offset,
1006
+ hasMore: nodes.length === LCM_NODE_PAGE
1007
+ };
1008
+ };
1009
+ var lcmNodeDetail = (source, nodeId) => {
1010
+ const detail = source.node(nodeId);
1011
+ if (!detail) return void 0;
1012
+ return {
1013
+ node: detail.node,
1014
+ revisions: [...detail.revisions].sort((left, right) => left.revision - right.revision),
1015
+ ancestors: detail.ancestors
1016
+ };
1017
+ };
1018
+ var formatBytes = (bytes) => {
1019
+ if (!Number.isFinite(bytes) || bytes <= 0) return "0 B";
1020
+ if (bytes < 1024) return `${Math.round(bytes)} B`;
1021
+ if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} kB`;
1022
+ return `${(bytes / 1048576).toFixed(1)} MB`;
1023
+ };
1024
+ var formatBudget = (used, limit) => Number.isFinite(limit) ? `${used}/${limit}` : `${used}/no limit`;
1025
+ var formatPercent = (value) => value > 0 && value < 1 ? `${value.toFixed(value < 0.1 ? 2 : 1)}%` : `${Math.round(value)}%`;
1026
+ var isEmergency = (modelHash) => modelHash === EMERGENCY_MODEL_HASH;
1027
+ var originLabel = (modelHash) => isEmergency(modelHash) ? "excerpt" : "model";
1028
+ var shortId = (value) => value.length > 10 ? `${value.slice(0, 10)}\u2026` : value;
1029
+ var textBytes = (text) => text ? Buffer.byteLength(text, "utf8") : 0;
1030
+ var lcmUnavailableLine = () => "LCM ledger unavailable \xB7 set compaction.engine to lcm and start a session to populate it";
1031
+ var renderLcmHeaderLines = (theme, snapshot, width) => {
1032
+ const { report } = snapshot;
1033
+ const summary = [
1034
+ report.state,
1035
+ report.degraded ? `degraded ${report.degraded}` : void 0,
1036
+ `session ${report.sessionId ?? "none"} \xB7 ${report.sessionEntries} entries`,
1037
+ `ledger ${report.rawEntries} entries`,
1038
+ `model ${report.summaryModel || "inherit"}`,
1039
+ `${report.modelNodes} model \xB7 ${report.emergencyNodes} excerpt \xB7 ${report.pendingNodes} pending`,
1040
+ `today ${formatBudget(report.usage.calls, report.budget.calls)} calls \xB7 ${formatCost(report.usage.cost)}`
1041
+ ].filter((value) => Boolean(value)).join(" \xB7 ");
1042
+ return wrapPlainText(summary, width, 2).map((line) => theme.fg("dim", line));
1043
+ };
1044
+ var bandRun = (theme, char, length) => {
1045
+ const color = char === "\u2588" ? "success" : char === "\u2593" ? "warning" : "dim";
1046
+ return theme.fg(color, char.repeat(length));
1047
+ };
1048
+ var coverageBand = (theme, map, cells) => {
1049
+ if (cells <= 0) return "";
1050
+ if (map.length === 0) return theme.fg("dim", "\xB7".repeat(cells));
1051
+ let band = "";
1052
+ let runChar = "";
1053
+ let runLength = 0;
1054
+ for (let cell = 0; cell < cells; cell++) {
1055
+ const start = Math.min(map.length - 1, Math.floor(cell * map.length / cells));
1056
+ const end = Math.min(map.length, Math.max(start + 1, Math.floor((cell + 1) * map.length / cells)));
1057
+ let covered = 0;
1058
+ for (let index = start; index < end; index++) {
1059
+ if (map[index].covered) covered++;
1060
+ }
1061
+ const char = covered === 0 ? "\u2591" : covered === end - start ? "\u2588" : "\u2593";
1062
+ if (char === runChar) {
1063
+ runLength++;
1064
+ continue;
1065
+ }
1066
+ if (runLength > 0) band += bandRun(theme, runChar, runLength);
1067
+ runChar = char;
1068
+ runLength = 1;
1069
+ }
1070
+ if (runLength > 0) band += bandRun(theme, runChar, runLength);
1071
+ return band;
1072
+ };
1073
+ var renderLcmCoverageLine = (theme, snapshot, width) => {
1074
+ const { active, covered } = snapshot.coverage;
1075
+ const label = `${covered}/${active} covered ${formatPercent(active > 0 ? covered / active * 100 : 0)}`;
1076
+ const cells = Math.max(0, width - visibleWidth(label) - 1);
1077
+ const band = coverageBand(theme, snapshot.coverageMap, cells);
1078
+ return truncateToWidth(`${band} ${theme.fg("muted", label)}`, width, "");
1079
+ };
1080
+ var nodeRow = (node) => {
1081
+ const indent = " ".repeat(Math.min(node.depth, 8));
1082
+ const glyph = node.kind === "condensed" ? "\u25C6" : "\u25C7";
1083
+ return safeText(
1084
+ `${indent}${glyph} ${node.kind} ${originLabel(node.modelHash)} \xB7 ${node.sources.length} src \xB7 ${node.children.length} ch \xB7 ${formatBytes(textBytes(node.text))} \xB7 ${shortId(node.nodeId)}`
1085
+ );
1086
+ };
1087
+ var renderLcmNodeLines = (theme, snapshot, width, height, selectedIndex) => {
1088
+ const available = Math.max(1, height);
1089
+ const total = snapshot.nodes.length;
1090
+ const heading = `nodes ${total === 0 ? 0 : snapshot.offset + 1}-${snapshot.offset + total}${snapshot.hasMore ? "+" : ""}`;
1091
+ const lines = [theme.fg("accent", truncateToWidth(heading, width, ""))];
1092
+ if (total === 0) {
1093
+ lines.push(theme.fg("dim", " (no summary nodes on the active branch yet)"));
1094
+ while (lines.length < available) lines.push("");
1095
+ return lines.slice(0, available);
1096
+ }
1097
+ const rows = Math.max(1, available - 1);
1098
+ const start = Math.max(
1099
+ 0,
1100
+ Math.min(selectedIndex - Math.floor(rows / 2), Math.max(0, total - rows))
1101
+ );
1102
+ for (let index = start; index < Math.min(total, start + rows); index++) {
1103
+ const node = snapshot.nodes[index];
1104
+ const emergency = isEmergency(node.modelHash);
1105
+ let line = theme.fg(emergency ? "warning" : "muted", nodeRow(node));
1106
+ if (index === selectedIndex) line = theme.bg("selectedBg", padToWidth(line, width));
1107
+ lines.push(truncateToWidth(line, width, ""));
1108
+ }
1109
+ while (lines.length < available) lines.push("");
1110
+ return lines.slice(0, available);
1111
+ };
1112
+ var renderLcmPreviewLines = (theme, snapshot, width, height) => {
1113
+ const available = Math.max(1, height);
1114
+ const preview = snapshot.preview;
1115
+ const complete = preview.activeSources > 0 && preview.coveredSources / preview.activeSources >= COVERAGE_COMPLETE_RATIO;
1116
+ const ratio = complete && preview.sourceBytes > 0 ? ` \xB7 assembled text is ${formatPercent(preview.summaryBytes / preview.sourceBytes * 100)} of the session payload it replaces` : "";
1117
+ const headline = `preview \xB7 frontier holds ${preview.coveredSources}/${preview.activeSources} sources \xB7 ${preview.nodes} nodes assemble to ${formatBytes(preview.summaryBytes)} \xB7 session stored payload ${formatBytes(preview.sourceBytes)}${ratio}`;
1118
+ const lines = wrapPlainText(headline, width, 2).map((line) => theme.fg("accent", line));
1119
+ const body = wrapPlainText(preview.text, width, Math.max(1, available - lines.length));
1120
+ for (const line of body) lines.push(theme.fg("dim", line));
1121
+ if (body.length === 0) lines.push(theme.fg("dim", "(the frontier assembles no text yet)"));
1122
+ while (lines.length < available) lines.push("");
1123
+ return lines.slice(0, available);
1124
+ };
1125
+ var revisionColumns = (theme, detail, width, rows) => {
1126
+ const current = detail.revisions[detail.revisions.length - 1];
1127
+ const previous = detail.revisions[detail.revisions.length - 2];
1128
+ if (!current) return [];
1129
+ if (!previous) {
1130
+ const lines2 = [theme.fg("accent", `text \xB7 current revision ${current.revision} \xB7 ${originLabel(current.modelHash)}`)];
1131
+ for (const line of wrapPlainText(current.text, width, Math.max(1, rows - 1))) {
1132
+ lines2.push(theme.fg("dim", line));
1133
+ }
1134
+ return lines2;
1135
+ }
1136
+ const label = (revision, name) => `${name} revision ${revision.revision} \xB7 ${originLabel(revision.modelHash)}`;
1137
+ if (width < 60) {
1138
+ const lines2 = [theme.fg("accent", label(current, "current"))];
1139
+ for (const line of wrapPlainText(current.text, width, Math.max(1, Math.floor((rows - 2) / 2)))) {
1140
+ lines2.push(theme.fg("dim", line));
1141
+ }
1142
+ lines2.push(theme.fg("accent", label(previous, "previous")));
1143
+ for (const line of wrapPlainText(previous.text, width, Math.max(1, Math.floor((rows - 2) / 2)))) {
1144
+ lines2.push(theme.fg("muted", line));
1145
+ }
1146
+ return lines2;
1147
+ }
1148
+ const columnWidth = Math.floor((width - 1) / 2);
1149
+ const body = Math.max(1, rows - 1);
1150
+ const previousBody = wrapPlainText(previous.text, columnWidth, body);
1151
+ const currentBody = wrapPlainText(current.text, columnWidth, body);
1152
+ const lines = [
1153
+ `${padToWidth(theme.fg("accent", label(previous, "previous")), columnWidth)}${theme.fg("borderMuted", "\u2502")}${theme.fg("accent", label(current, "current"))}`
1154
+ ];
1155
+ for (let index = 0; index < body; index++) {
1156
+ const left = previousBody[index];
1157
+ const right = currentBody[index];
1158
+ if (left === void 0 && right === void 0) break;
1159
+ lines.push(
1160
+ `${padToWidth(theme.fg("muted", left ?? ""), columnWidth)}${theme.fg("borderMuted", "\u2502")}${theme.fg("dim", right ?? "")}`
1161
+ );
1162
+ }
1163
+ return lines;
1164
+ };
1165
+ var renderLcmDetailLines = (theme, detail, sourceIndex, entry, width, height) => {
1166
+ const available = Math.max(1, height);
1167
+ const node = detail.node;
1168
+ const lines = [
1169
+ theme.fg(
1170
+ isEmergency(node.modelHash) ? "warning" : "accent",
1171
+ truncateToWidth(
1172
+ safeText(
1173
+ `${node.kind} \xB7 ${originLabel(node.modelHash)} \xB7 depth ${node.depth} \xB7 ${node.state} \xB7 ${formatClock(node.createdAt)} \xB7 ${node.nodeId}`
1174
+ ),
1175
+ width,
1176
+ ""
1177
+ )
1178
+ ),
1179
+ theme.fg(
1180
+ "dim",
1181
+ truncateToWidth(safeText(`model hash ${node.modelHash} \xB7 branch ${node.branch ?? "none"}`), width, "")
1182
+ ),
1183
+ theme.fg(
1184
+ "dim",
1185
+ truncateToWidth(
1186
+ safeText(`ancestors ${detail.ancestors.length > 0 ? detail.ancestors.join(" \u203A ") : "none"}`),
1187
+ width,
1188
+ ""
1189
+ )
1190
+ ),
1191
+ theme.fg(
1192
+ "dim",
1193
+ truncateToWidth(
1194
+ safeText(
1195
+ `children (${node.children.length}) ${node.children.length > 0 ? node.children.map(shortId).join(" ") : "none"}`
1196
+ ),
1197
+ width,
1198
+ ""
1199
+ )
1200
+ ),
1201
+ theme.fg("accent", truncateToWidth(`sources (${node.sources.length})`, width, ""))
1202
+ ];
1203
+ const sourceRows = Math.min(node.sources.length, Math.max(1, Math.floor(available / 4)));
1204
+ const sourceStart = Math.max(
1205
+ 0,
1206
+ Math.min(sourceIndex - Math.floor(sourceRows / 2), Math.max(0, node.sources.length - sourceRows))
1207
+ );
1208
+ for (let index = sourceStart; index < Math.min(node.sources.length, sourceStart + sourceRows); index++) {
1209
+ const source = node.sources[index];
1210
+ const text = ` ${source.sessionId}/${source.entryId}@${source.revision}`;
1211
+ const line = index === sourceIndex ? theme.bg("selectedBg", padToWidth(theme.fg("muted", text), width)) : theme.fg("muted", text);
1212
+ lines.push(truncateToWidth(line, width, ""));
1213
+ }
1214
+ if (node.sources.length === 0) lines.push(theme.fg("dim", " (no sources)"));
1215
+ if (entry) {
1216
+ lines.push(
1217
+ theme.fg(
1218
+ "accent",
1219
+ truncateToWidth(
1220
+ safeText(`raw entry \xB7 ${entry.role} \xB7 ${formatClock(entry.createdAt)} \xB7 ${formatBytes(Buffer.byteLength(entry.payloadJson, "utf8"))} stored`),
1221
+ width,
1222
+ ""
1223
+ )
1224
+ )
1225
+ );
1226
+ for (const line of wrapPlainText(entry.content, width, Math.max(1, Math.floor(available / 4)))) {
1227
+ lines.push(theme.fg("dim", line));
1228
+ }
1229
+ }
1230
+ const remaining = Math.max(1, available - lines.length);
1231
+ for (const line of revisionColumns(theme, detail, width, remaining)) lines.push(line);
1232
+ while (lines.length < available) lines.push("");
1233
+ return lines.slice(0, available);
1234
+ };
1235
+
991
1236
  // src/ui/dashboard-detail.ts
992
- import { Markdown, truncateToWidth as truncateToWidth2, wrapTextWithAnsi, visibleWidth as visibleWidth2 } from "@oh-my-pi/pi-tui";
1237
+ import { Markdown, truncateToWidth as truncateToWidth3, wrapTextWithAnsi, visibleWidth as visibleWidth3 } from "@oh-my-pi/pi-tui";
993
1238
 
994
1239
  // src/ui/state-file-preview.ts
995
1240
  import fs from "node:fs";
996
1241
  import path from "node:path";
997
- import { truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
1242
+ import { truncateToWidth as truncateToWidth2, visibleWidth as visibleWidth2 } from "@oh-my-pi/pi-tui";
998
1243
  var MAX_FILE_BYTES = 64 * 1024;
999
1244
  var MAX_FILE_LINES = 240;
1000
1245
  var CACHE_LIMIT = 64;
@@ -1070,14 +1315,14 @@ var renderStateFilePreview = (preview, theme, width, maxLines, invalidate) => {
1070
1315
  const digits = String(Math.max(1, shown.length)).length;
1071
1316
  const output = highlighted.map((line, index) => {
1072
1317
  const gutter = theme.fg("dim", `${String(index + 1).padStart(digits)} \u2502 `);
1073
- return truncateToWidth(gutter + line, width, "");
1318
+ return truncateToWidth2(gutter + line, width, "");
1074
1319
  });
1075
1320
  if ((preview.truncated || preview.lines.length > shown.length) && output.length < maxLines) {
1076
1321
  const omitted = Math.max(0, preview.lines.length - shown.length);
1077
1322
  const label = omitted > 0 ? `\u2026 ${omitted}+ lines omitted` : "\u2026 file truncated";
1078
- output.push(truncateToWidth(theme.fg("dim", label), width, ""));
1323
+ output.push(truncateToWidth2(theme.fg("dim", label), width, ""));
1079
1324
  }
1080
- return output.filter((line) => visibleWidth(line) <= width);
1325
+ return output.filter((line) => visibleWidth2(line) <= width);
1081
1326
  };
1082
1327
 
1083
1328
  // src/ui/dashboard-detail.ts
@@ -1209,7 +1454,7 @@ var DashboardDetailRenderer = class {
1209
1454
  lines.push(this.row(width, this.theme.fg("muted", ` ${actionLine}`)));
1210
1455
  }
1211
1456
  lines.push(this.bottomBorder(width));
1212
- return lines.map((line) => truncateToWidth2(line, width, ""));
1457
+ return lines.map((line) => truncateToWidth3(line, width, ""));
1213
1458
  }
1214
1459
  transcriptLines(entity, width) {
1215
1460
  const transcript = this.transcriptFor(entity);
@@ -1240,7 +1485,7 @@ var DashboardDetailRenderer = class {
1240
1485
  statusGlyph(entry.status ?? "completed")
1241
1486
  );
1242
1487
  lines.push(
1243
- truncateToWidth2(
1488
+ truncateToWidth3(
1244
1489
  `${glyph} ${this.theme.fg(
1245
1490
  entry.kind === "assistant" ? "accent" : "muted",
1246
1491
  safeText(entry.label)
@@ -1263,7 +1508,7 @@ var DashboardDetailRenderer = class {
1263
1508
  }
1264
1509
  for (const paragraph of entry.text.split("\n")) {
1265
1510
  const wrapped = wrapPlainText(paragraph, Math.max(1, width - 2), 1e4);
1266
- for (const line of wrapped) lines.push(truncateToWidth2(` ${line}`, width, ""));
1511
+ for (const line of wrapped) lines.push(truncateToWidth3(` ${line}`, width, ""));
1267
1512
  }
1268
1513
  }
1269
1514
  if (transcript.hasNewer) {
@@ -1291,7 +1536,7 @@ var DashboardDetailRenderer = class {
1291
1536
  const headline = title ?? this.theme.fg("toolTitle", this.theme.bold(entry.toolName ?? entry.label));
1292
1537
  const collapsedSummary = !this.transcriptToolsExpanded && entry.text ? ` \xB7 ${safeText(entry.text).replace(/\s+/g, " ").trim()}` : "";
1293
1538
  const lines = [
1294
- truncateToWidth2(
1539
+ truncateToWidth3(
1295
1540
  `${padding}${glyph} ${headline}${this.theme.fg("dim", `${status}${collapsedSummary}`)}`,
1296
1541
  width,
1297
1542
  ""
@@ -1308,8 +1553,8 @@ var DashboardDetailRenderer = class {
1308
1553
  rendered.lines,
1309
1554
  this.theme,
1310
1555
  this.codePreviewSettings?.diffIntensity ?? "off"
1311
- ).render(Math.max(1, width - visibleWidth2(bodyPadding)))) {
1312
- lines.push(truncateToWidth2(`${bodyPadding}${row}`, width, ""));
1556
+ ).render(Math.max(1, width - visibleWidth3(bodyPadding)))) {
1557
+ lines.push(truncateToWidth3(`${bodyPadding}${row}`, width, ""));
1313
1558
  }
1314
1559
  if (rendered.hidden > 0) {
1315
1560
  lines.push(this.theme.fg("dim", `${bodyPadding}\u2026 ${rendered.hidden} more lines`));
@@ -1321,10 +1566,10 @@ var DashboardDetailRenderer = class {
1321
1566
  } else if (entry.text) {
1322
1567
  for (const row of wrapPlainText(
1323
1568
  entry.text,
1324
- Math.max(1, width - visibleWidth2(bodyPadding)),
1569
+ Math.max(1, width - visibleWidth3(bodyPadding)),
1325
1570
  1e4
1326
1571
  )) {
1327
- lines.push(truncateToWidth2(`${bodyPadding}${row}`, width, ""));
1572
+ lines.push(truncateToWidth3(`${bodyPadding}${row}`, width, ""));
1328
1573
  }
1329
1574
  }
1330
1575
  if (entry.result !== void 0) {
@@ -1361,11 +1606,11 @@ var DashboardDetailRenderer = class {
1361
1606
  const yamlLines = yaml.split("\n");
1362
1607
  const shownYamlLines = yamlLines.slice(0, TRANSCRIPT_STRUCTURED_LINES);
1363
1608
  const highlighted = highlightCode(shownYamlLines.join("\n"), "yaml", this.highlightInvalidate) ?? shownYamlLines.map((line) => this.theme.fg("mdCodeBlock", line || " "));
1364
- const lines = [truncateToWidth2(`${padding}${this.theme.fg("dim", `${label}:`)}`, width, "")];
1609
+ const lines = [truncateToWidth3(`${padding}${this.theme.fg("dim", `${label}:`)}`, width, "")];
1365
1610
  const nestedPadding = `${padding} `;
1366
1611
  for (const row of highlighted) {
1367
- for (const wrapped of wrapTextWithAnsi(row, Math.max(1, width - visibleWidth2(nestedPadding)))) {
1368
- lines.push(truncateToWidth2(`${nestedPadding}${wrapped}`, width, ""));
1612
+ for (const wrapped of wrapTextWithAnsi(row, Math.max(1, width - visibleWidth3(nestedPadding)))) {
1613
+ lines.push(truncateToWidth3(`${nestedPadding}${wrapped}`, width, ""));
1369
1614
  if (lines.length > TRANSCRIPT_STRUCTURED_LINES) break;
1370
1615
  }
1371
1616
  if (lines.length > TRANSCRIPT_STRUCTURED_LINES) break;
@@ -1405,7 +1650,7 @@ var DashboardDetailRenderer = class {
1405
1650
  }
1406
1651
  }
1407
1652
  const padding = " ".repeat(Math.max(0, indent));
1408
- return cached.component.render(Math.max(1, width - visibleWidth2(padding))).map((line) => truncateToWidth2(`${padding}${line}`, width, ""));
1653
+ return cached.component.render(Math.max(1, width - visibleWidth3(padding))).map((line) => truncateToWidth3(`${padding}${line}`, width, ""));
1409
1654
  }
1410
1655
  detailLines(entity, width, now, cwd) {
1411
1656
  const lines = [];
@@ -1413,10 +1658,10 @@ var DashboardDetailRenderer = class {
1413
1658
  const text = safeText(value);
1414
1659
  if (!text) return;
1415
1660
  const prefix = `${this.theme.fg("dim", `${label}:`)} `;
1416
- const wrapped = wrapPlainText(text, Math.max(1, width - visibleWidth2(prefix)), 12);
1417
- if (wrapped[0]) lines.push(truncateToWidth2(prefix + wrapped[0], width));
1661
+ const wrapped = wrapPlainText(text, Math.max(1, width - visibleWidth3(prefix)), 12);
1662
+ if (wrapped[0]) lines.push(truncateToWidth3(prefix + wrapped[0], width));
1418
1663
  for (const continuation of wrapped.slice(1)) {
1419
- lines.push(truncateToWidth2(" ".repeat(visibleWidth2(prefix)) + continuation, width));
1664
+ lines.push(truncateToWidth3(" ".repeat(visibleWidth3(prefix)) + continuation, width));
1420
1665
  }
1421
1666
  };
1422
1667
  const markdownField = (label, value, key) => {
@@ -1435,7 +1680,7 @@ var DashboardDetailRenderer = class {
1435
1680
  const highlighted = highlightCode(yaml, "yaml", this.highlightInvalidate) ?? yaml.split("\n").map((line) => this.theme.fg("mdCodeBlock", line || " "));
1436
1681
  for (const highlightedLine of highlighted) {
1437
1682
  for (const wrapped of wrapTextWithAnsi(highlightedLine, Math.max(1, width - 2))) {
1438
- lines.push(truncateToWidth2(` ${wrapped}`, width, ""));
1683
+ lines.push(truncateToWidth3(` ${wrapped}`, width, ""));
1439
1684
  }
1440
1685
  }
1441
1686
  };
@@ -1498,7 +1743,7 @@ var DashboardDetailRenderer = class {
1498
1743
  this.theme,
1499
1744
  settings.diffIntensity
1500
1745
  ).render(Math.max(1, width - 2));
1501
- for (const row of body) lines.push(truncateToWidth2(` ${row}`, width, ""));
1746
+ for (const row of body) lines.push(truncateToWidth3(` ${row}`, width, ""));
1502
1747
  if (rendered.hidden > 0) {
1503
1748
  lines.push(this.theme.fg("muted", ` \u2026 ${rendered.hidden} more lines`));
1504
1749
  }
@@ -1529,7 +1774,7 @@ ${command}
1529
1774
  this.highlightInvalidate
1530
1775
  );
1531
1776
  if (diff) {
1532
- for (const line of diff) lines.push(truncateToWidth2(` ${line}`, width, ""));
1777
+ for (const line of diff) lines.push(truncateToWidth3(` ${line}`, width, ""));
1533
1778
  } else {
1534
1779
  structuredField("Edits", edits);
1535
1780
  }
@@ -1777,13 +2022,13 @@ ${command}
1777
2022
  }
1778
2023
  const title = `${entity.label}${transcriptView ? " \xB7 transcript" : ""}`;
1779
2024
  const hint = transcriptView ? `${this.transcriptToolToggleHint()} \xB7 g page top \xB7 G follow:${this.transcriptFollowing ? "on" : "off"}/tail \xB7 t summary \xB7 esc` : `${this.hasTranscript(entity) ? "t transcript \xB7 " : ""}esc`;
1780
- return [title, ...content.slice(this.detailScroll, this.detailScroll + maxBody), hint].map((line) => truncateToWidth2(line, width, "")).filter((line) => visibleWidth2(line) > 0);
2025
+ return [title, ...content.slice(this.detailScroll, this.detailScroll + maxBody), hint].map((line) => truncateToWidth3(line, width, "")).filter((line) => visibleWidth3(line) > 0);
1781
2026
  }
1782
2027
  topBorder(width, title) {
1783
2028
  const border = (value) => this.theme.fg("borderMuted", value);
1784
- const safeTitle = truncateToWidth2(safeText(title), Math.max(0, width - 6));
2029
+ const safeTitle = truncateToWidth3(safeText(title), Math.max(0, width - 6));
1785
2030
  const styledTitle = ` ${this.theme.fg("accent", safeTitle)} `;
1786
- const remaining = Math.max(0, width - 2 - visibleWidth2(styledTitle));
2031
+ const remaining = Math.max(0, width - 2 - visibleWidth3(styledTitle));
1787
2032
  const left = Math.floor(remaining / 2);
1788
2033
  const right = remaining - left;
1789
2034
  return `${border(`\u256D${"\u2500".repeat(left)}`)}${styledTitle}${border(`${"\u2500".repeat(right)}\u256E`)}`;
@@ -1805,7 +2050,7 @@ ${command}
1805
2050
 
1806
2051
  // src/ui/dashboard-fabric-graph.ts
1807
2052
  import { Ellipsis as Ellipsis2 } from "@oh-my-pi/pi-tui";
1808
- import { truncateToWidth as truncateToWidth3, visibleWidth as visibleWidth3 } from "@oh-my-pi/pi-tui";
2053
+ import { truncateToWidth as truncateToWidth4, visibleWidth as visibleWidth4 } from "@oh-my-pi/pi-tui";
1809
2054
  var kindRank = {
1810
2055
  agent: 0,
1811
2056
  actor: 1,
@@ -1848,13 +2093,13 @@ var graphLabel = (value, maxWidth) => {
1848
2093
  let output = "";
1849
2094
  let width = 0;
1850
2095
  for (const char of value) {
1851
- const charWidth = visibleWidth3(char);
2096
+ const charWidth = visibleWidth4(char);
1852
2097
  if (width + charWidth > maxWidth) {
1853
2098
  while (width + 1 > maxWidth && output.length > 0) {
1854
2099
  const parts = [...output];
1855
2100
  const removed = parts.pop();
1856
2101
  output = parts.join("");
1857
- width -= removed ? visibleWidth3(removed) : 0;
2102
+ width -= removed ? visibleWidth4(removed) : 0;
1858
2103
  }
1859
2104
  return output + "\u2026";
1860
2105
  }
@@ -2259,7 +2504,7 @@ var renderCanvas = (theme, layout, selectedEntityId, width, height, camera, anim
2259
2504
  }
2260
2505
  };
2261
2506
  const edgePathPoints = (from, to) => {
2262
- const fromEnd = from.x + Math.min(16, visibleWidth3(safeText(from.label)) + 3);
2507
+ const fromEnd = from.x + Math.min(16, visibleWidth4(safeText(from.label)) + 3);
2263
2508
  const toStart = to.x - 2;
2264
2509
  const bend = Math.max(fromEnd + 1, Math.floor((fromEnd + toStart) / 2));
2265
2510
  const points = [];
@@ -2296,7 +2541,7 @@ var renderCanvas = (theme, layout, selectedEntityId, width, height, camera, anim
2296
2541
  const from = nodeById.get(edge.from);
2297
2542
  const to = nodeById.get(edge.to);
2298
2543
  if (!from || !to) continue;
2299
- const fromEnd = from.x + Math.min(16, visibleWidth3(safeText(from.label)) + 3);
2544
+ const fromEnd = from.x + Math.min(16, visibleWidth4(safeText(from.label)) + 3);
2300
2545
  const toStart = to.x - 2;
2301
2546
  const bend = Math.max(fromEnd + 1, Math.floor((fromEnd + toStart) / 2));
2302
2547
  horizontal(fromEnd, bend, from.y);
@@ -2366,7 +2611,7 @@ var renderCanvas = (theme, layout, selectedEntityId, width, height, camera, anim
2366
2611
  let offset = 2;
2367
2612
  for (const char of label) {
2368
2613
  setCell(point.x + offset, point.y, char, routeStyle(route));
2369
- offset += visibleWidth3(char);
2614
+ offset += visibleWidth4(char);
2370
2615
  }
2371
2616
  } else {
2372
2617
  const count = group.routes.reduce((total, candidate) => total + candidate.count, 0);
@@ -2390,7 +2635,7 @@ var renderCanvas = (theme, layout, selectedEntityId, width, height, camera, anim
2390
2635
  let offset = 0;
2391
2636
  for (const char of label) {
2392
2637
  setCell(node.x + 2 + offset, node.y, char, selected ? "accent" : spotlighted ? "muted" : "dim");
2393
- offset += visibleWidth3(char);
2638
+ offset += visibleWidth4(char);
2394
2639
  }
2395
2640
  }
2396
2641
  const apply = (style, value) => {
@@ -2416,12 +2661,12 @@ var renderCanvas = (theme, layout, selectedEntityId, width, height, camera, anim
2416
2661
  run += cell.char;
2417
2662
  }
2418
2663
  rendered += apply(style, run);
2419
- return truncateToWidth3(rendered, width, "");
2664
+ return truncateToWidth4(rendered, width, "");
2420
2665
  });
2421
2666
  };
2422
2667
  var wrapInspector = (theme, label, value, width) => {
2423
2668
  const clean = safeText(value);
2424
- const first = truncateToWidth3(clean, Math.max(1, width - label.length - 1), Ellipsis2.Unicode);
2669
+ const first = truncateToWidth4(clean, Math.max(1, width - label.length - 1), Ellipsis2.Unicode);
2425
2670
  return [theme.fg("muted", `${label} ${first}`)];
2426
2671
  };
2427
2672
  var inspectorLines = (theme, entity, snapshot, run, width, height, invalidate) => {
@@ -2429,10 +2674,10 @@ var inspectorLines = (theme, entity, snapshot, run, width, height, invalidate) =
2429
2674
  const border = (value) => theme.fg("borderMuted", value);
2430
2675
  const content = [];
2431
2676
  if (entity) {
2432
- content.push(theme.fg("accent", theme.bold(truncateToWidth3(safeText(entity.label), inner - 2, Ellipsis2.Unicode))));
2677
+ content.push(theme.fg("accent", theme.bold(truncateToWidth4(safeText(entity.label), inner - 2, Ellipsis2.Unicode))));
2433
2678
  content.push(colorStatus(theme, entity.status, `${statusGlyph(entity.status)} ${entity.kind} \xB7 ${entity.status}`));
2434
2679
  content.push("");
2435
- content.push(theme.fg("dim", truncateToWidth3(safeText(entityTail(entity, snapshot.now)), inner - 2, Ellipsis2.Unicode)));
2680
+ content.push(theme.fg("dim", truncateToWidth4(safeText(entityTail(entity, snapshot.now)), inner - 2, Ellipsis2.Unicode)));
2436
2681
  if (entity.kind === "agent") {
2437
2682
  const agentRun = snapshot.runs.find((candidate) => candidate.id === entity.value.runId) ?? run;
2438
2683
  const phase = agentRun?.phases.find((candidate) => candidate.id === entity.value.phaseId);
@@ -2498,7 +2743,7 @@ var inspectorLines = (theme, entity, snapshot, run, width, height, invalidate) =
2498
2743
  content.push(theme.fg("dim", "No node selected"));
2499
2744
  }
2500
2745
  const title = " selected ";
2501
- const rows = [border(`\u256D${title}${"\u2500".repeat(Math.max(0, inner - visibleWidth3(title)))}\u256E`)];
2746
+ const rows = [border(`\u256D${title}${"\u2500".repeat(Math.max(0, inner - visibleWidth4(title)))}\u256E`)];
2502
2747
  for (let index = 0; index < height - 2; index++) {
2503
2748
  rows.push(`${border("\u2502")}${padToWidth(` ${content[index] ?? ""}`, inner)}${border("\u2502")}`);
2504
2749
  }
@@ -2584,8 +2829,8 @@ var renderFabricTopologyPanel = ({
2584
2829
  animation.reducedMotion ? "reduced motion" : void 0,
2585
2830
  filter !== "all" ? `${entities.length}/${allEntities.length} ${filter}` : void 0
2586
2831
  ].filter((value) => Boolean(value)).join(" \xB7 ");
2587
- const graphLegend = padToWidth(theme.fg("dim", truncateToWidth3(legend, graphWidth, "")), graphWidth);
2588
- lines[0] = truncateToWidth3(
2832
+ const graphLegend = padToWidth(theme.fg("dim", truncateToWidth4(legend, graphWidth, "")), graphWidth);
2833
+ lines[0] = truncateToWidth4(
2589
2834
  graphLegend + (inspectorWidth > 0 ? inspector[0] ?? "" : ""),
2590
2835
  width,
2591
2836
  ""
@@ -2988,6 +3233,7 @@ var FabricDashboard = class {
2988
3233
  this.done = done;
2989
3234
  this.focused = true;
2990
3235
  this.modelSource = options.modelSource;
3236
+ this.lcmStatus = options.lcmStatus;
2991
3237
  this.codePreviewSettings = options.codePreviewSettings;
2992
3238
  this.keybindings = options.keybindings;
2993
3239
  this.claudeModelSource = options.claudeModelSource;
@@ -3024,6 +3270,13 @@ var FabricDashboard = class {
3024
3270
  focused = false;
3025
3271
  pane = "phases";
3026
3272
  overviewView = "activity";
3273
+ lcmSnapshot;
3274
+ lcmSnapshotTick;
3275
+ lcmOffset = 0;
3276
+ lcmIndex = 0;
3277
+ lcmDetail;
3278
+ lcmSourceIndex = 0;
3279
+ lcmSourceEntry;
3027
3280
  graphPositions = /* @__PURE__ */ new Map();
3028
3281
  graphCamera = { x: 0, y: 0 };
3029
3282
  graphCameraTarget = { x: 0, y: 0 };
@@ -3065,6 +3318,7 @@ var FabricDashboard = class {
3065
3318
  editorActorName;
3066
3319
  agentMessageTarget;
3067
3320
  pendingStop;
3321
+ lcmStatus;
3068
3322
  modelSource;
3069
3323
  claudeModelSource;
3070
3324
  onAgentSteer;
@@ -3152,6 +3406,17 @@ var FabricDashboard = class {
3152
3406
  this.tui.requestRender();
3153
3407
  return;
3154
3408
  }
3409
+ if (this.overviewView === "lcm") {
3410
+ const outcome = this.handleLcmInput(data);
3411
+ if (outcome === "exit") {
3412
+ this.done();
3413
+ return;
3414
+ }
3415
+ if (outcome === "handled") {
3416
+ this.tui.requestRender();
3417
+ return;
3418
+ }
3419
+ }
3155
3420
  if (this.detailId) {
3156
3421
  if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c")) || matchesKey(data, Key.left) || data === "h") {
3157
3422
  this.closeDetail();
@@ -3264,10 +3529,10 @@ var FabricDashboard = class {
3264
3529
  this.tui.requestRender();
3265
3530
  return;
3266
3531
  }
3267
- if (data === "1" || data === "2") {
3268
- const nextOverview = data === "1" ? "activity" : "topology";
3532
+ if (data === "1" || data === "2" || data === "3") {
3533
+ const nextOverview = data === "1" ? "activity" : data === "2" ? "topology" : "lcm";
3269
3534
  if (nextOverview !== this.overviewView) {
3270
- if (nextOverview === "activity") {
3535
+ if (nextOverview !== "topology") {
3271
3536
  this.stopGraphAnimation();
3272
3537
  this.stopGraphEffectsAnimation();
3273
3538
  }
@@ -3276,6 +3541,8 @@ var FabricDashboard = class {
3276
3541
  this.entityIndex = 0;
3277
3542
  this.selectedEntityId = void 0;
3278
3543
  this.pendingStop = void 0;
3544
+ if (nextOverview === "lcm") this.enterLcmView();
3545
+ else this.closeLcmDetail();
3279
3546
  }
3280
3547
  this.tui.requestRender();
3281
3548
  return;
@@ -3481,6 +3748,7 @@ var FabricDashboard = class {
3481
3748
  if ((this.mode === "modelPicker" || this.mode === "thinkingPicker" || this.mode === "deliveryPicker" || this.mode === "eventsPicker" || this.mode === "toolsPicker") && this.picker) {
3482
3749
  return this.renderPicker(width);
3483
3750
  }
3751
+ if (this.overviewView === "lcm") return this.renderLcmView(width);
3484
3752
  const snapshot = this.snapshot();
3485
3753
  const run = this.selectRun(snapshot);
3486
3754
  const panels = phasePanels(snapshot, run);
@@ -3647,7 +3915,7 @@ var FabricDashboard = class {
3647
3915
  )
3648
3916
  );
3649
3917
  lines.push(this.bottomBorder(width));
3650
- return lines.map((line) => truncateToWidth4(line, width, ""));
3918
+ return lines.map((line) => truncateToWidth5(line, width, ""));
3651
3919
  }
3652
3920
  renderHelp(width) {
3653
3921
  if (width < 24) return this.renderNarrowFallback(width, "dashboard help", "? or esc close");
@@ -3684,7 +3952,11 @@ var FabricDashboard = class {
3684
3952
  ].filter((value) => Boolean(value));
3685
3953
  const help = [
3686
3954
  ["Navigate", "Topology: arrows/h/l move spatially \xB7 j/k ordered selection \xB7 tab next \xB7 enter inspect \xB7 esc back"],
3687
- ["Views", "1 Activity \xB7 2 unified Topology"],
3955
+ ["Views", "1 Activity \xB7 2 unified Topology \xB7 3 LCM ledger"],
3956
+ [
3957
+ "LCM",
3958
+ "\u2191\u2193/jk node \xB7 g/G first/last node \xB7 [ ] page 200 nodes \xB7 enter inspect node \xB7 in a node: \u2191\u2193/jk source \xB7 enter open/close the stored raw entry \xB7 esc back"
3959
+ ],
3688
3960
  ...this.onConversation ? [["Chat", "Shift+C opens the selected participant conversation \xB7 Esc returns to Main"]] : [],
3689
3961
  ["Topology", "Main branches into Participants (sessions, agents, actors) and Mesh (namespaced topics and hierarchical state); traffic travels on decaying edges"],
3690
3962
  ["Motion", "r replay/live \xB7 space pause/play \xB7 \u2190/\u2192 step \xB7 +/- speed \xB7 H history \xB7 M reduced motion"],
@@ -3700,16 +3972,16 @@ var FabricDashboard = class {
3700
3972
  ];
3701
3973
  for (const [label, value] of help) {
3702
3974
  const prefix = `${this.theme.fg("accent", `${label}:`)} `;
3703
- const wrapped = wrapPlainText(value ?? "", Math.max(1, width - 2 - visibleWidth4(prefix)), 3);
3975
+ const wrapped = wrapPlainText(value ?? "", Math.max(1, width - 2 - visibleWidth5(prefix)), 3);
3704
3976
  if (wrapped[0]) lines.push(this.row(width, prefix + wrapped[0]));
3705
3977
  for (const continuation of wrapped.slice(1)) {
3706
- lines.push(this.row(width, " ".repeat(visibleWidth4(prefix)) + continuation));
3978
+ lines.push(this.row(width, " ".repeat(visibleWidth5(prefix)) + continuation));
3707
3979
  }
3708
3980
  }
3709
3981
  lines.push(this.middleBorder(width));
3710
3982
  lines.push(this.row(width, this.theme.fg("dim", " ? or esc close")));
3711
3983
  lines.push(this.bottomBorder(width));
3712
- return lines.map((line) => truncateToWidth4(line, width, ""));
3984
+ return lines.map((line) => truncateToWidth5(line, width, ""));
3713
3985
  }
3714
3986
  modelSourceForActor(actor) {
3715
3987
  return actor.runner === "claude" ? this.claudeModelSource : this.modelSource;
@@ -3886,7 +4158,7 @@ var FabricDashboard = class {
3886
4158
  )
3887
4159
  );
3888
4160
  lines.push(this.bottomBorder(width));
3889
- return lines.map((line) => truncateToWidth4(line, width, ""));
4161
+ return lines.map((line) => truncateToWidth5(line, width, ""));
3890
4162
  }
3891
4163
  renderInstructionsEditor(width) {
3892
4164
  if (!this.editor) return [];
@@ -3904,7 +4176,7 @@ var FabricDashboard = class {
3904
4176
  )
3905
4177
  );
3906
4178
  lines.push(this.bottomBorder(width));
3907
- return lines.map((line) => truncateToWidth4(line, width, ""));
4179
+ return lines.map((line) => truncateToWidth5(line, width, ""));
3908
4180
  }
3909
4181
  projectMesh(snapshot) {
3910
4182
  if (this.overviewView !== "topology") return void 0;
@@ -4029,7 +4301,7 @@ var FabricDashboard = class {
4029
4301
  }
4030
4302
  renderOverview(width, snapshot, run, panels, entities, allEntities, meshModel) {
4031
4303
  if (width < 24) {
4032
- return [truncateToWidth4("too narrow \xB7 need 24 cols", width)];
4304
+ return [truncateToWidth5("too narrow \xB7 need 24 cols", width)];
4033
4305
  }
4034
4306
  const innerWidth = width - 2;
4035
4307
  const terminalRows = Math.max(
@@ -4068,7 +4340,7 @@ var FabricDashboard = class {
4068
4340
  let headerLine = summaryText;
4069
4341
  if (run?.description && this.overviewView === "activity") {
4070
4342
  const gap = " ";
4071
- const availableDescription = innerWidth - visibleWidth4(summaryText) - gap.length;
4343
+ const availableDescription = innerWidth - visibleWidth5(summaryText) - gap.length;
4072
4344
  headerLine = availableDescription >= 12 ? `${padToWidth(
4073
4345
  this.theme.fg("muted", safeText(run.description)),
4074
4346
  availableDescription
@@ -4082,7 +4354,7 @@ var FabricDashboard = class {
4082
4354
  title,
4083
4355
  this.theme.fg("dim", summaryText || "No Fabric activity yet"),
4084
4356
  this.theme.fg("dim", "1 activity \xB7 2 topology \xB7 arrows move \xB7 esc close")
4085
- ].slice(0, overlayRows).map((line) => truncateToWidth4(line, width, ""));
4357
+ ].slice(0, overlayRows).map((line) => truncateToWidth5(line, width, ""));
4086
4358
  }
4087
4359
  lines.push(this.row(width, headerLine || this.theme.fg("muted", "No Fabric activity yet")));
4088
4360
  lines.push(this.middleBorder(width));
@@ -4219,7 +4491,7 @@ var FabricDashboard = class {
4219
4491
  const actionHint = this.pane === "entities" && selectedEntity ? this.theme.fg("muted", ` ${this.overviewActionHint(selectedEntity)}`) : "";
4220
4492
  lines.push(this.row(width, actionHint));
4221
4493
  lines.push(this.bottomBorder(width));
4222
- return lines.map((line) => truncateToWidth4(line, width, ""));
4494
+ return lines.map((line) => truncateToWidth5(line, width, ""));
4223
4495
  }
4224
4496
  overviewActionHint(entity) {
4225
4497
  if (entity.kind === "main") {
@@ -4289,7 +4561,7 @@ var FabricDashboard = class {
4289
4561
  }
4290
4562
  renderPhasePanel(panels, width, height) {
4291
4563
  const lines = [
4292
- truncateToWidth4(
4564
+ truncateToWidth5(
4293
4565
  `${this.pane === "phases" ? this.theme.fg("accent", "\u25B8 ") : " "}${this.theme.fg(
4294
4566
  "accent",
4295
4567
  "Activity"
@@ -4311,13 +4583,13 @@ var FabricDashboard = class {
4311
4583
  const raw = `${prefix}${colorStatus(this.theme, panel.status, statusGlyph(panel.status))} ${this.theme.fg("muted", safeText(
4312
4584
  panel.name
4313
4585
  ))}`;
4314
- const countWidth = visibleWidth4(count);
4586
+ const countWidth = visibleWidth5(count);
4315
4587
  const contentWidth = Math.max(0, width - countWidth - (count ? 1 : 0));
4316
4588
  let line = `${padToWidth(raw, contentWidth)}${count ? ` ${this.theme.fg("dim", count)}` : ""}`;
4317
4589
  if (selected && this.pane === "phases") {
4318
4590
  line = this.theme.bg("selectedBg", padToWidth(line, width));
4319
4591
  }
4320
- lines.push(truncateToWidth4(line, width, ""));
4592
+ lines.push(truncateToWidth5(line, width, ""));
4321
4593
  }
4322
4594
  while (lines.length < height) lines.push("");
4323
4595
  return lines.slice(0, height);
@@ -4357,7 +4629,7 @@ var FabricDashboard = class {
4357
4629
  }
4358
4630
  if (row.type === "group") {
4359
4631
  lines.push(
4360
- truncateToWidth4(
4632
+ truncateToWidth5(
4361
4633
  this.theme.fg(
4362
4634
  "muted",
4363
4635
  ` ${this.theme.bold(row.group.label)} (${row.group.entries.length})`
@@ -4379,7 +4651,7 @@ var FabricDashboard = class {
4379
4651
  if (selected && this.pane === "entities") {
4380
4652
  line = this.theme.bg("selectedBg", padToWidth(line, width));
4381
4653
  }
4382
- lines.push(truncateToWidth4(line, width, ""));
4654
+ lines.push(truncateToWidth5(line, width, ""));
4383
4655
  }
4384
4656
  if (entities.length === 0 && available > 0) {
4385
4657
  const label = this.filter === "all" ? "activity" : `${this.filter} activity`;
@@ -4572,14 +4844,176 @@ var FabricDashboard = class {
4572
4844
  this.detailView = "summary";
4573
4845
  this.transcriptFollowing = true;
4574
4846
  }
4847
+ enterLcmView() {
4848
+ this.lcmOffset = 0;
4849
+ this.lcmIndex = 0;
4850
+ this.closeLcmDetail();
4851
+ this.refreshLcm();
4852
+ }
4853
+ closeLcmDetail() {
4854
+ this.lcmDetail = void 0;
4855
+ this.lcmSourceIndex = 0;
4856
+ this.lcmSourceEntry = void 0;
4857
+ }
4858
+ refreshLcm() {
4859
+ this.lcmSnapshotTick = this.snapshot().now;
4860
+ const source = this.lcmStatus?.();
4861
+ this.lcmSnapshot = source ? readLcmSnapshot(source, this.lcmOffset) : void 0;
4862
+ const total = this.lcmSnapshot?.nodes.length ?? 0;
4863
+ this.lcmIndex = total === 0 ? 0 : Math.max(0, Math.min(this.lcmIndex, total - 1));
4864
+ }
4865
+ openLcmNode() {
4866
+ const nodeId = this.lcmSnapshot?.nodes[this.lcmIndex]?.nodeId;
4867
+ const source = nodeId ? this.lcmStatus?.() : void 0;
4868
+ this.lcmDetail = source && nodeId ? lcmNodeDetail(source, nodeId) : void 0;
4869
+ this.lcmSourceIndex = 0;
4870
+ this.lcmSourceEntry = void 0;
4871
+ }
4872
+ openLcmSource() {
4873
+ const selected = this.lcmDetail?.node.sources[this.lcmSourceIndex];
4874
+ if (!selected) return;
4875
+ if (this.lcmSourceEntry) {
4876
+ this.lcmSourceEntry = void 0;
4877
+ return;
4878
+ }
4879
+ this.lcmSourceEntry = this.lcmStatus?.()?.source(
4880
+ selected.sessionId,
4881
+ selected.entryId,
4882
+ selected.revision
4883
+ );
4884
+ }
4885
+ pageLcmNodes(direction) {
4886
+ const snapshot = this.lcmSnapshot;
4887
+ if (!snapshot) return;
4888
+ if (direction === 1 && !snapshot.hasMore) return;
4889
+ const next = Math.max(0, snapshot.offset + direction * LCM_NODE_PAGE);
4890
+ if (next === snapshot.offset) return;
4891
+ this.lcmOffset = next;
4892
+ this.lcmIndex = 0;
4893
+ this.refreshLcm();
4894
+ }
4895
+ handleLcmInput(data) {
4896
+ if (data === "1" || data === "2" || data === "3") return "ignored";
4897
+ const back = matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"));
4898
+ if (this.lcmDetail) {
4899
+ const sources = this.lcmDetail.node.sources.length;
4900
+ if (back || matchesKey(data, Key.left) || data === "h") {
4901
+ if (this.lcmSourceEntry) this.lcmSourceEntry = void 0;
4902
+ else this.closeLcmDetail();
4903
+ } else if (matchesKey(data, Key.up) || data === "k") {
4904
+ this.lcmSourceIndex = Math.max(0, this.lcmSourceIndex - 1);
4905
+ this.lcmSourceEntry = void 0;
4906
+ } else if (matchesKey(data, Key.down) || data === "j") {
4907
+ this.lcmSourceIndex = Math.min(Math.max(0, sources - 1), this.lcmSourceIndex + 1);
4908
+ this.lcmSourceEntry = void 0;
4909
+ } else if (matchesKey(data, Key.enter)) {
4910
+ this.openLcmSource();
4911
+ }
4912
+ return "handled";
4913
+ }
4914
+ if (back) return "exit";
4915
+ const total = this.lcmSnapshot?.nodes.length ?? 0;
4916
+ if (matchesKey(data, Key.up) || data === "k") {
4917
+ this.lcmIndex = Math.max(0, this.lcmIndex - 1);
4918
+ } else if (matchesKey(data, Key.down) || data === "j") {
4919
+ this.lcmIndex = Math.min(Math.max(0, total - 1), this.lcmIndex + 1);
4920
+ } else if (data === "g") {
4921
+ this.lcmIndex = 0;
4922
+ } else if (data === "G") {
4923
+ this.lcmIndex = Math.max(0, total - 1);
4924
+ } else if (data === "[") {
4925
+ this.pageLcmNodes(-1);
4926
+ } else if (data === "]") {
4927
+ this.pageLcmNodes(1);
4928
+ } else if (matchesKey(data, Key.enter)) {
4929
+ this.openLcmNode();
4930
+ }
4931
+ return "handled";
4932
+ }
4933
+ renderLcmView(width) {
4934
+ if (width < 24) return this.renderNarrowFallback(width, "Fabric \xB7 LCM", "1 activity \xB7 esc close");
4935
+ if (this.snapshot().now !== this.lcmSnapshotTick) this.refreshLcm();
4936
+ const innerWidth = width - 2;
4937
+ const overlayRows = dashboardOverlayRows(
4938
+ Math.max(1, this.tui.terminal?.rows ?? process.stdout.rows ?? 28)
4939
+ );
4940
+ const lines = [this.topBorder(width, "Fabric \xB7 LCM")];
4941
+ const snapshot = this.lcmSnapshot;
4942
+ if (!snapshot) {
4943
+ lines.push(this.row(width, this.theme.fg("muted", lcmUnavailableLine())));
4944
+ lines.push(this.middleBorder(width));
4945
+ lines.push(
4946
+ this.row(width, this.theme.fg("dim", "1 activity \xB7 2 topology \xB7 ? help \xB7 esc close"))
4947
+ );
4948
+ lines.push(this.bottomBorder(width));
4949
+ return lines.map((line) => truncateToWidth5(line, width, ""));
4950
+ }
4951
+ const maxBody = Math.max(3, Math.min(34, overlayRows - 5));
4952
+ if (this.lcmDetail) {
4953
+ for (const line of renderLcmDetailLines(
4954
+ this.theme,
4955
+ this.lcmDetail,
4956
+ this.lcmSourceIndex,
4957
+ this.lcmSourceEntry,
4958
+ innerWidth,
4959
+ maxBody
4960
+ )) {
4961
+ lines.push(this.row(width, line));
4962
+ }
4963
+ lines.push(this.middleBorder(width));
4964
+ lines.push(
4965
+ this.row(
4966
+ width,
4967
+ this.theme.fg(
4968
+ "dim",
4969
+ `\u2191\u2193/jk source \xB7 enter ${this.lcmSourceEntry ? "close" : "open"} raw entry \xB7 esc back \xB7 ? help`
4970
+ )
4971
+ )
4972
+ );
4973
+ lines.push(this.bottomBorder(width));
4974
+ return lines.map((line) => truncateToWidth5(line, width, ""));
4975
+ }
4976
+ const headerLines = renderLcmHeaderLines(this.theme, snapshot, innerWidth);
4977
+ for (const line of headerLines) lines.push(this.row(width, line));
4978
+ lines.push(this.row(width, renderLcmCoverageLine(this.theme, snapshot, innerWidth)));
4979
+ lines.push(this.middleBorder(width));
4980
+ const body = Math.max(2, maxBody - headerLines.length - 2);
4981
+ const previewRows = Math.max(2, Math.min(6, Math.floor(body / 3)));
4982
+ const nodeRows = Math.max(1, body - previewRows - 1);
4983
+ for (const line of renderLcmNodeLines(
4984
+ this.theme,
4985
+ snapshot,
4986
+ innerWidth,
4987
+ nodeRows,
4988
+ this.lcmIndex
4989
+ )) {
4990
+ lines.push(this.row(width, line));
4991
+ }
4992
+ lines.push(this.middleBorder(width));
4993
+ for (const line of renderLcmPreviewLines(this.theme, snapshot, innerWidth, previewRows)) {
4994
+ lines.push(this.row(width, line));
4995
+ }
4996
+ lines.push(this.middleBorder(width));
4997
+ lines.push(
4998
+ this.row(
4999
+ width,
5000
+ this.theme.fg(
5001
+ "dim",
5002
+ `\u2191\u2193/jk node \xB7 g/G first/last \xB7 [ ] node page \xB7 enter inspect \xB7 1 activity \xB7 2 topology \xB7 ? help`
5003
+ )
5004
+ )
5005
+ );
5006
+ lines.push(this.bottomBorder(width));
5007
+ return lines.map((line) => truncateToWidth5(line, width, ""));
5008
+ }
4575
5009
  renderNarrowFallback(width, label, hint) {
4576
- return [safeText(label), hint].map((line) => truncateToWidth4(line, width, "")).filter((line) => visibleWidth4(line) > 0);
5010
+ return [safeText(label), hint].map((line) => truncateToWidth5(line, width, "")).filter((line) => visibleWidth5(line) > 0);
4577
5011
  }
4578
5012
  topBorder(width, title) {
4579
5013
  const border = (value) => this.theme.fg("borderMuted", value);
4580
- const safeTitle = truncateToWidth4(safeText(title), Math.max(0, width - 6));
5014
+ const safeTitle = truncateToWidth5(safeText(title), Math.max(0, width - 6));
4581
5015
  const styledTitle = ` ${this.theme.fg("accent", safeTitle)} `;
4582
- const remaining = Math.max(0, width - 2 - visibleWidth4(styledTitle));
5016
+ const remaining = Math.max(0, width - 2 - visibleWidth5(styledTitle));
4583
5017
  const left = Math.floor(remaining / 2);
4584
5018
  const right = remaining - left;
4585
5019
  return `${border(`\u256D${"\u2500".repeat(left)}`)}${styledTitle}${border(`${"\u2500".repeat(right)}\u256E`)}`;