thurview 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ui/app.js CHANGED
@@ -162,6 +162,17 @@
162
162
  init_dom();
163
163
 
164
164
  // src/ui/state.ts
165
+ var VIEWS = {
166
+ review: ["review", "commits", "files", "map"],
167
+ explainer: ["review", "map", "coverage"]
168
+ };
169
+ function kind() {
170
+ return state.data?.review.kind === "explainer" ? "explainer" : "review";
171
+ }
172
+ function view() {
173
+ const allowed = VIEWS[kind()];
174
+ return allowed.includes(state.view) ? state.view : allowed[0];
175
+ }
165
176
  var NARROW = "(max-width: 900px)";
166
177
  function isNarrow() {
167
178
  return window.matchMedia(NARROW).matches;
@@ -196,16 +207,16 @@
196
207
  if (t.type === "map") return `map \xB7 ${t.node}`;
197
208
  return "review";
198
209
  }
199
- function navigate(view, params = {}) {
210
+ function navigate(view2, params = {}) {
200
211
  const p = new URLSearchParams();
201
212
  for (const [k, v] of Object.entries(params)) if (v !== void 0 && v !== "") p.set(k, String(v));
202
213
  const q = p.toString();
203
- location.hash = `#/${view}${q ? `?${q}` : ""}`;
214
+ location.hash = `#/${view2}${q ? `?${q}` : ""}`;
204
215
  }
205
216
  function readHash() {
206
217
  const m2 = /^#\/(\w+)(?:\?(.*))?$/.exec(location.hash);
207
- const view = m2?.[1] ?? "review";
208
- state.view = ["review", "commits", "files", "map"].includes(view) ? view : "review";
218
+ const v = m2?.[1] ?? "review";
219
+ state.view = ["review", "commits", "files", "map", "coverage"].includes(v) ? v : "review";
209
220
  state.params = new URLSearchParams(m2?.[2] ?? "");
210
221
  }
211
222
  function isTerminal() {
@@ -484,12 +495,14 @@
484
495
  }
485
496
  function submitDialog() {
486
497
  const pending = (state.data?.threads ?? []).filter((t) => !t.submitted).length;
498
+ const explainer = kind() === "explainer";
499
+ const words = explainer ? { title: "Send this back or finish", accept: "Done reading", back: "Send it back" } : { title: "Submit review", accept: "Approve", back: "Request changes" };
487
500
  const ta = h("textarea", { rows: 4, placeholder: "Summary for the agent (optional)" });
488
501
  const d = dialog(
489
502
  h(
490
503
  "div",
491
504
  null,
492
- h("h3", null, "Submit review"),
505
+ h("h3", null, words.title),
493
506
  h(
494
507
  "p",
495
508
  { class: "muted" },
@@ -502,11 +515,14 @@
502
515
  h("button", { class: "ghost", onclick: () => d.close() }, "Cancel"),
503
516
  h(
504
517
  "button",
505
- { title: "End the review without approving it", onclick: () => decide("close") },
518
+ {
519
+ title: explainer ? "End it without reading it through" : "End the review without approving it",
520
+ onclick: () => decide("close")
521
+ },
506
522
  "Close"
507
523
  ),
508
- h("button", { onclick: () => decide("request-changes") }, "Request changes"),
509
- h("button", { class: "ok", onclick: () => decide("approve") }, "Approve")
524
+ h("button", { onclick: () => decide("request-changes") }, words.back),
525
+ h("button", { class: "ok", onclick: () => decide("approve") }, words.accept)
510
526
  )
511
527
  )
512
528
  );
@@ -788,7 +804,7 @@
788
804
  const frame = h("div", { class: "code" });
789
805
  const draw = () => {
790
806
  frame.innerHTML = "";
791
- const bar = (dir) => {
807
+ const bar2 = (dir) => {
792
808
  const can = dir === "up" ? p.from > 1 : p.to < p.total;
793
809
  return h(
794
810
  "div",
@@ -800,7 +816,7 @@
800
816
  ) : h("span", { class: "muted" }, dir === "up" ? "start of file" : "end of file")
801
817
  );
802
818
  };
803
- frame.appendChild(bar("up"));
819
+ frame.appendChild(bar2("up"));
804
820
  frame.appendChild(
805
821
  codeTable({
806
822
  path: p.path,
@@ -811,7 +827,7 @@
811
827
  commentable: true
812
828
  })
813
829
  );
814
- frame.appendChild(bar("down"));
830
+ frame.appendChild(bar2("down"));
815
831
  };
816
832
  const expand = async (dir) => {
817
833
  if (dir === "up") {
@@ -901,12 +917,12 @@
901
917
  } else {
902
918
  g.appendChild(svg("line", { x1, y1: y, x2: x2 + (x2 > x1 ? -4 : 4), y2: y }));
903
919
  }
904
- const label = svg(
920
+ const label2 = svg(
905
921
  "text",
906
922
  { x: self ? x1 + 36 : (x1 + x2) / 2, y: y - 6, "text-anchor": self ? "start" : "middle" },
907
923
  `${i + 1}. ${m2.label}`
908
924
  );
909
- label.addEventListener("click", (e) => {
925
+ label2.addEventListener("click", (e) => {
910
926
  if (m2.anchor) openAnchor(m2.anchor);
911
927
  else if (m2.code)
912
928
  popover(
@@ -918,7 +934,7 @@
918
934
  { x: e.pageX, y: e.pageY + 8 }
919
935
  );
920
936
  });
921
- g.appendChild(label);
937
+ g.appendChild(label2);
922
938
  el.appendChild(g);
923
939
  });
924
940
  return h("div", { class: "diagram" }, h("div", { class: "dhead" }, b.label), el);
@@ -970,12 +986,12 @@
970
986
  );
971
987
  const uc = b.usecases[active];
972
988
  body.innerHTML = "";
973
- const touched = /* @__PURE__ */ new Map();
989
+ const touched2 = /* @__PURE__ */ new Map();
974
990
  for (const op of uc.ops) {
975
991
  const [s, c, f] = op.store.split(".");
976
992
  const key = `${s}.${c}`;
977
- if (!touched.has(key)) touched.set(key, /* @__PURE__ */ new Set());
978
- if (f) touched.get(key).add(f);
993
+ if (!touched2.has(key)) touched2.set(key, /* @__PURE__ */ new Set());
994
+ if (f) touched2.get(key).add(f);
979
995
  }
980
996
  const actorIds = [...new Set(uc.ops.map((o) => o.actor))];
981
997
  body.appendChild(
@@ -996,7 +1012,7 @@
996
1012
  { class: "store" },
997
1013
  h("div", { class: "sname" }, s.label, " ", h("span", { class: "badge" }, s.kind)),
998
1014
  Object.entries(colls).map(([cid, c]) => {
999
- const hit = touched.get(`${sid}.${cid}`);
1015
+ const hit = touched2.get(`${sid}.${cid}`);
1000
1016
  return h(
1001
1017
  "div",
1002
1018
  { class: `table ${hit ? "hit" : ""}` },
@@ -1067,7 +1083,9 @@
1067
1083
  );
1068
1084
  return;
1069
1085
  }
1070
- root.appendChild(interfaceDelta(doc.interfaces));
1086
+ root.appendChild(
1087
+ kind() === "explainer" ? coveragePanel(state.data?.coverage ?? null) : interfaceDelta(doc.interfaces)
1088
+ );
1071
1089
  const layout = h("div", { class: "doc-layout" });
1072
1090
  const toc = h(
1073
1091
  "nav",
@@ -1140,6 +1158,7 @@
1140
1158
  docEl.querySelectorAll(".block.heading").forEach((el) => io.observe(el));
1141
1159
  }
1142
1160
  var DELTA_BLOCK = "interface-delta";
1161
+ var COVERAGE_BLOCK = "coverage";
1143
1162
  var DELTA_SHOWN = 12;
1144
1163
  var CHANGE_CLASS = {
1145
1164
  removed: "del",
@@ -1216,6 +1235,71 @@
1216
1235
  wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
1217
1236
  return wrap;
1218
1237
  }
1238
+ function coveragePanel(cov) {
1239
+ const wrap = h("div", { class: "block ifd", "data-block": COVERAGE_BLOCK });
1240
+ const threads = threadsFor((t) => t.type === "document" && t.blockId === COVERAGE_BLOCK);
1241
+ if (threads.length) wrap.classList.add("has-threads");
1242
+ const actions = h(
1243
+ "div",
1244
+ { class: "block-actions" },
1245
+ h(
1246
+ "button",
1247
+ {
1248
+ class: threads.length ? "count" : "",
1249
+ title: "Comment on coverage",
1250
+ onclick: (e) => {
1251
+ if (state.viewingRevision !== null) return;
1252
+ popover(commentPopover({ type: "document", blockId: COVERAGE_BLOCK }), {
1253
+ x: e.pageX + 10,
1254
+ y: e.pageY
1255
+ });
1256
+ }
1257
+ },
1258
+ threads.length ? String(threads.length) : "+"
1259
+ )
1260
+ );
1261
+ const body = h("div", { class: "ifd-body" });
1262
+ const toggle = h("span", { class: "heading-toggle" }, "hide");
1263
+ toggle.addEventListener("click", () => {
1264
+ const hidden = body.hidden;
1265
+ body.hidden = !hidden;
1266
+ toggle.textContent = hidden ? "hide" : "show";
1267
+ });
1268
+ wrap.appendChild(
1269
+ h(
1270
+ "div",
1271
+ { class: "ifd-head" },
1272
+ h("span", { class: "t" }, "Coverage"),
1273
+ cov ? h("span", { class: "badge" }, `${cov.states.uncovered} not examined`) : null,
1274
+ h("span", { class: "spacer" }),
1275
+ toggle,
1276
+ actions
1277
+ )
1278
+ );
1279
+ body.appendChild(
1280
+ h(
1281
+ "p",
1282
+ { class: "ifd-verdict" },
1283
+ cov ? cov.verdict : "Unavailable: coverage could not be derived for this revision."
1284
+ )
1285
+ );
1286
+ body.appendChild(
1287
+ h(
1288
+ "p",
1289
+ { class: "ifd-verdict" },
1290
+ "A codebase does not fit in one document, so this one selects. ",
1291
+ h(
1292
+ "button",
1293
+ { class: "small", onclick: () => navigate("coverage") },
1294
+ "See what it left out \u25B8"
1295
+ )
1296
+ )
1297
+ );
1298
+ wrap.appendChild(body);
1299
+ if (threads.length)
1300
+ wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
1301
+ return wrap;
1302
+ }
1219
1303
  function interfaceRow(e) {
1220
1304
  return h(
1221
1305
  "div",
@@ -1416,15 +1500,15 @@
1416
1500
  list.appendChild(
1417
1501
  h("div", { class: "empty-state" }, "No changed files: base and head are the same commit.")
1418
1502
  );
1419
- const view = h("div", { class: "file-view", "data-path": path });
1420
- root.appendChild(h("div", { class: "files-layout" }, list, view));
1421
- if (path) void renderDiff(view, path);
1503
+ const view2 = h("div", { class: "file-view", "data-path": path });
1504
+ root.appendChild(h("div", { class: "files-layout" }, list, view2));
1505
+ if (path) void renderDiff(view2, path);
1422
1506
  }
1423
- async function renderDiff(view, path) {
1507
+ async function renderDiff(view2, path) {
1424
1508
  const inChanges = state.data?.changes.some((c) => c.path === path);
1425
1509
  const line = Number(state.params.get("line") ?? 0);
1426
1510
  const side2 = state.params.get("side") === "base" ? "base" : "head";
1427
- view.appendChild(h("div", { class: "muted", style: { padding: "12px" } }, "Loading\u2026"));
1511
+ view2.appendChild(h("div", { class: "muted", style: { padding: "12px" } }, "Loading\u2026"));
1428
1512
  let d;
1429
1513
  try {
1430
1514
  if (inChanges) d = await api.diff(state.id, path);
@@ -1453,11 +1537,11 @@
1453
1537
  };
1454
1538
  }
1455
1539
  } catch (e) {
1456
- view.innerHTML = "";
1457
- view.appendChild(h("div", { class: "empty-state" }, e.message));
1540
+ view2.innerHTML = "";
1541
+ view2.appendChild(h("div", { class: "empty-state" }, e.message));
1458
1542
  return;
1459
1543
  }
1460
- view.innerHTML = "";
1544
+ view2.innerHTML = "";
1461
1545
  const split = state.splitDiff && inChanges && !isNarrow();
1462
1546
  const toggle = h(
1463
1547
  "button",
@@ -1466,8 +1550,8 @@
1466
1550
  onclick: () => {
1467
1551
  state.splitDiff = !state.splitDiff;
1468
1552
  localStorage.setItem("thurview.split", state.splitDiff ? "1" : "0");
1469
- view.innerHTML = "";
1470
- void renderDiff(view, path);
1553
+ view2.innerHTML = "";
1554
+ void renderDiff(view2, path);
1471
1555
  }
1472
1556
  },
1473
1557
  state.splitDiff ? "Unified" : "Split"
@@ -1475,7 +1559,7 @@
1475
1559
  const fileThreads = (state.data?.threads ?? []).filter(
1476
1560
  (t) => t.target.type === "file" && t.target.path === path && t.target.line === 0
1477
1561
  );
1478
- view.appendChild(
1562
+ view2.appendChild(
1479
1563
  h(
1480
1564
  "div",
1481
1565
  { class: "file-head" },
@@ -1502,7 +1586,7 @@
1502
1586
  )
1503
1587
  );
1504
1588
  if (fileThreads.length)
1505
- view.appendChild(
1589
+ view2.appendChild(
1506
1590
  h(
1507
1591
  "div",
1508
1592
  { class: "thread-pins", style: { margin: "8px 14px" } },
@@ -1510,15 +1594,15 @@
1510
1594
  )
1511
1595
  );
1512
1596
  if (d.binary) {
1513
- view.appendChild(h("div", { class: "empty-state" }, "Binary file"));
1597
+ view2.appendChild(h("div", { class: "empty-state" }, "Binary file"));
1514
1598
  return;
1515
1599
  }
1516
1600
  const threads = (state.data?.threads ?? []).filter(
1517
1601
  (t) => t.target.type === "file" && t.target.path === path
1518
1602
  );
1519
1603
  const el = split ? splitView(d, threads) : unifiedView(d, threads);
1520
- view.appendChild(el);
1521
- codeSelectionHandler(view);
1604
+ view2.appendChild(el);
1605
+ codeSelectionHandler(view2);
1522
1606
  if (line) {
1523
1607
  const tr = el.querySelector(`tr[data-${side2}="${line}"]`);
1524
1608
  if (tr) {
@@ -1797,22 +1881,12 @@
1797
1881
 
1798
1882
  // src/ui/views/map.ts
1799
1883
  init_dom();
1800
- function renderMap(root) {
1801
- const map = state.data?.map;
1802
- if (!map) {
1803
- root.appendChild(
1804
- h(
1805
- "div",
1806
- { class: "empty-state" },
1807
- "No software map in this revision. The agent authors ",
1808
- h("code", null, "map.yaml"),
1809
- " and publishes again."
1810
- )
1811
- );
1812
- return;
1813
- }
1814
- const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
1815
- const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
1884
+
1885
+ // src/ui/views/map-model.ts
1886
+ function parentOf(id) {
1887
+ return id.includes(".") ? id.slice(0, id.lastIndexOf(".")) : "";
1888
+ }
1889
+ function collect(map) {
1816
1890
  const all = /* @__PURE__ */ new Map();
1817
1891
  for (const n of map.head.nodes)
1818
1892
  all.set(n.id, {
@@ -1820,145 +1894,297 @@
1820
1894
  status: map.diff.added.includes(n.id) ? "added" : map.diff.changed.includes(n.id) ? "changed" : ""
1821
1895
  });
1822
1896
  for (const id of map.diff.removed) {
1823
- const n = baseNodes.get(id);
1897
+ const n = (map.base?.nodes ?? []).find((x) => x.id === id);
1824
1898
  if (n) all.set(id, { ...n, status: "removed" });
1825
1899
  }
1900
+ return all;
1901
+ }
1902
+ function changedInside(all, id) {
1903
+ let n = 0;
1904
+ for (const item of all.values()) if (item.id.startsWith(id + ".") && item.status) n++;
1905
+ return n;
1906
+ }
1907
+ function touched(all, item) {
1908
+ return !!item.status || changedInside(all, item.id) > 0;
1909
+ }
1910
+ function childrenOf(all, parent, filesByNode) {
1911
+ const kids = [...all.values()].filter((n) => parentOf(n.id) === parent);
1912
+ const weight = (n) => (filesByNode[n.id]?.length ?? 0) + changedInside(all, n.id);
1913
+ return kids.map((n, i) => ({ n, i })).sort((a, b) => {
1914
+ const t = Number(touched(all, b.n)) - Number(touched(all, a.n));
1915
+ if (t) return t;
1916
+ const w = weight(b.n) - weight(a.n);
1917
+ return w || a.i - b.i;
1918
+ }).map((x) => x.n);
1919
+ }
1920
+ function routeIn(all, filesByNode) {
1921
+ const changed = [...all.values()].filter((n) => n.status);
1922
+ const hasChild = (id) => [...all.keys()].some((k) => parentOf(k) === id);
1923
+ const leaves = changed.filter((n) => !hasChild(n.id));
1924
+ const pool = leaves.length ? leaves : changed;
1925
+ let best = null;
1926
+ let bestFiles = -1;
1927
+ for (const n of pool) {
1928
+ const files = filesByNode[n.id]?.length ?? 0;
1929
+ if (files > bestFiles) {
1930
+ best = n;
1931
+ bestFiles = files;
1932
+ }
1933
+ }
1934
+ return best;
1935
+ }
1936
+ function neighboursOf(map, id) {
1937
+ const out = [];
1938
+ const into = [];
1939
+ for (const e of allEdges(map)) {
1940
+ if (e.from === id)
1941
+ out.push({ dir: "out", other: e.to, ...e.label ? { label: e.label } : {} });
1942
+ if (e.to === id)
1943
+ into.push({ dir: "in", other: e.from, ...e.label ? { label: e.label } : {} });
1944
+ }
1945
+ return [...out, ...into];
1946
+ }
1947
+ function allEdges(map) {
1826
1948
  const edges = [...map.head.edges];
1827
1949
  for (const e of map.base?.edges ?? [])
1828
1950
  if (!edges.some((x) => x.from === e.from && x.to === e.to)) edges.push(e);
1829
- const selected = state.params.get("node") ?? "";
1830
- const parentOf = (id) => id.includes(".") ? id.slice(0, id.lastIndexOf(".")) : "";
1831
- let level = selected && all.has(selected) ? childrenOf(selected).length ? selected : parentOf(selected) : "";
1832
- function childrenOf(p) {
1833
- return [...all.values()].filter((n) => parentOf(n.id) === p);
1834
- }
1835
- function descendantsChanged(id) {
1836
- return [...all.values()].filter((n) => n.id.startsWith(id + ".") && n.status).length;
1951
+ return edges;
1952
+ }
1953
+ function rankEdges(edges, all) {
1954
+ const hot = (id) => {
1955
+ const item = all.get(id);
1956
+ return item && touched(all, item) ? 1 : 0;
1957
+ };
1958
+ return edges.map((edge, i) => ({ edge, i, ends: hot(edge.from) + hot(edge.to) })).sort((a, b) => b.ends - a.ends || a.i - b.i).map(({ edge, ends }) => ({ edge, touchesChange: ends > 0 }));
1959
+ }
1960
+
1961
+ // src/ui/views/map.ts
1962
+ var statusClass = (s) => s === "added" ? "ok" : s === "removed" ? "del" : "warn";
1963
+ function renderMap(root) {
1964
+ const map = state.data?.map;
1965
+ const explainer = kind() === "explainer";
1966
+ if (!map || !map.head.nodes.length) {
1967
+ append(root, [
1968
+ explainer ? h(
1969
+ "div",
1970
+ { class: "empty-state" },
1971
+ h("p", null, "No map in this explainer."),
1972
+ h(
1973
+ "p",
1974
+ null,
1975
+ "The map is where an explainer carries breadth: it places the parts the prose ",
1976
+ "had no room for. Without one, every file the document does not anchor counts ",
1977
+ "as not examined on the Coverage tab."
1978
+ )
1979
+ ) : h(
1980
+ "div",
1981
+ { class: "empty-state" },
1982
+ h("p", null, "No map in this review."),
1983
+ h(
1984
+ "p",
1985
+ null,
1986
+ "The map places a change inside the system and shows what sits next to it. ",
1987
+ "A change that lands in one place, where the Files tab already answers that, ships without one."
1988
+ )
1989
+ )
1990
+ ]);
1991
+ return;
1837
1992
  }
1993
+ const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
1994
+ const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
1995
+ const all = collect(map);
1996
+ const edges = allEdges(map);
1997
+ const route = routeIn(all, map.filesByNode);
1998
+ const selected = state.params.get("node") ?? "";
1999
+ const hasChildren = (id) => [...all.keys()].some((k) => parentOf(k) === id);
2000
+ let level = selected && all.has(selected) ? hasChildren(selected) ? selected : parentOf(selected) : "";
1838
2001
  const canvas = h("div", { class: "map-canvas" });
1839
2002
  const side2 = h("div", { class: "map-side" });
1840
2003
  root.appendChild(h("div", { class: "map-layout" }, canvas, side2));
1841
- const draw = () => {
1842
- canvas.innerHTML = "";
1843
- const crumbs = h("div", { class: "crumbs" });
1844
- const parts = level ? level.split(".") : [];
1845
- crumbs.appendChild(
2004
+ const openLevel = (id) => {
2005
+ level = id;
2006
+ draw();
2007
+ };
2008
+ const intro2 = () => {
2009
+ if (explainer)
2010
+ return h(
2011
+ "div",
2012
+ { class: "map-intro" },
2013
+ h("h2", null, "The parts of the system"),
2014
+ h(
2015
+ "p",
2016
+ null,
2017
+ "The document carries depth on the parts it selected; this carries breadth over the",
2018
+ " rest \u2014 what the parts are, what they own, and what they connect to at the pinned",
2019
+ " commit. Open a part to see its files, its code and its neighbours."
2020
+ ),
2021
+ h(
2022
+ "div",
2023
+ { class: "map-route" },
2024
+ h(
2025
+ "button",
2026
+ { class: "small", onclick: () => navigate("coverage") },
2027
+ "What this explainer left out \u25B8"
2028
+ )
2029
+ )
2030
+ );
2031
+ const counts = [
2032
+ [map.diff.added.length, "ok", "added"],
2033
+ [map.diff.changed.length, "warn", "changed"],
2034
+ [map.diff.removed.length, "del", "removed"]
2035
+ ];
2036
+ const marks = counts.filter(([n]) => n > 0);
2037
+ return h(
2038
+ "div",
2039
+ { class: "map-intro" },
2040
+ h("h2", null, "Where the change landed"),
1846
2041
  h(
1847
- "span",
1848
- {
1849
- onclick: () => {
1850
- level = "";
1851
- draw();
1852
- }
1853
- },
1854
- "System"
2042
+ "p",
2043
+ null,
2044
+ "The Files tab has the lines. This has the parts of the system they landed in, and what",
2045
+ " those parts connect to \u2014 the neighbours a diff never names. Marked parts are the change;",
2046
+ " the rest is the context you need to judge it."
2047
+ ),
2048
+ h(
2049
+ "div",
2050
+ { class: "map-route" },
2051
+ marks.length ? marks.map(([n, cls, word]) => h("span", { class: `badge ${cls}` }, `${n} ${word}`)) : h("span", { class: "muted" }, "This change touched no part of the map."),
2052
+ route ? h(
2053
+ "button",
2054
+ {
2055
+ class: "small",
2056
+ onclick: () => navigate("map", { node: route.id })
2057
+ },
2058
+ `Start at ${route.label} \u203A`
2059
+ ) : null,
2060
+ map.base ? null : h(
2061
+ "span",
2062
+ { class: "muted" },
2063
+ "No base map, so changed means touched by the diff and nothing reads as added or removed."
2064
+ )
1855
2065
  )
1856
2066
  );
2067
+ };
2068
+ const crumbs = () => {
2069
+ const el = h("div", { class: "crumbs" });
2070
+ const parts = level ? level.split(".") : [];
2071
+ el.appendChild(h("span", { onclick: () => openLevel("") }, "System"));
1857
2072
  parts.forEach((_, i) => {
1858
2073
  const id = parts.slice(0, i + 1).join(".");
1859
- crumbs.appendChild(document.createTextNode(" \u203A "));
1860
- crumbs.appendChild(
1861
- h(
1862
- "span",
1863
- {
1864
- onclick: () => {
1865
- level = id;
1866
- draw();
1867
- }
1868
- },
1869
- all.get(id)?.label ?? id
1870
- )
1871
- );
2074
+ el.appendChild(document.createTextNode(" \u203A "));
2075
+ el.appendChild(h("span", { onclick: () => openLevel(id) }, all.get(id)?.label ?? id));
1872
2076
  });
1873
- canvas.appendChild(crumbs);
1874
- const kids = childrenOf(level);
1875
- const legend = h(
2077
+ return el;
2078
+ };
2079
+ const card = (n) => {
2080
+ const inside = [...all.keys()].filter((k) => parentOf(k) === n.id).length;
2081
+ const hidden = changedInside(all, n.id);
2082
+ const files = map.filesByNode[n.id]?.length ?? 0;
2083
+ return h(
1876
2084
  "div",
1877
- { class: "muted", style: { fontSize: "12px", marginBottom: "10px" } },
1878
- h("span", { class: "badge ok" }, `${map.diff.added.length} added`),
1879
- " ",
1880
- h("span", { class: "badge del" }, `${map.diff.removed.length} removed`),
1881
- " ",
1882
- h("span", { class: "badge warn" }, `${map.diff.changed.length} changed`),
1883
- map.base ? "" : " (no base map: changed = touched by the diff)"
1884
- );
1885
- canvas.appendChild(legend);
1886
- const grid = h("div", { class: "map-grid" });
1887
- for (const n of kids) {
1888
- const nk = childrenOf(n.id).length;
1889
- const files = map.filesByNode[n.id]?.length ?? 0;
1890
- grid.appendChild(
1891
- h(
1892
- "div",
2085
+ {
2086
+ class: `map-node ${n.status} ${explainer || touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
2087
+ onclick: () => navigate("map", { node: n.id }),
2088
+ ondblclick: () => inside && openLevel(n.id)
2089
+ },
2090
+ n.status ? h("span", { class: `status badge ${statusClass(n.status)}` }, n.status) : null,
2091
+ h("div", { class: "kind" }, n.kind),
2092
+ h("div", { class: "label" }, n.label),
2093
+ n.description ? h("div", { class: "desc" }, n.description) : null,
2094
+ h(
2095
+ "div",
2096
+ { class: "meta" },
2097
+ files ? h("span", { class: "warn-text" }, `${files} changed file${files > 1 ? "s" : ""}`) : null,
2098
+ hidden ? h("span", { class: "warn-text" }, `${hidden} changed inside`) : null,
2099
+ inside ? h(
2100
+ "button",
1893
2101
  {
1894
- class: `map-node ${n.status} ${n.id === selected ? "selected" : ""}`,
1895
- onclick: () => {
1896
- navigate("map", { node: n.id });
1897
- },
1898
- ondblclick: () => {
1899
- if (nk) {
1900
- level = n.id;
1901
- draw();
1902
- }
2102
+ class: "small drill",
2103
+ onclick: (e) => {
2104
+ e.stopPropagation();
2105
+ openLevel(n.id);
1903
2106
  }
1904
2107
  },
1905
- n.status ? h(
1906
- "span",
1907
- {
1908
- class: `status badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
1909
- },
1910
- n.status
1911
- ) : null,
1912
- h("div", { class: "kind" }, n.kind),
1913
- h("div", { class: "label" }, n.label),
1914
- n.description ? h("div", { class: "desc" }, n.description) : null,
1915
- h(
1916
- "div",
1917
- { class: "children" },
1918
- nk ? `${nk} inside \xB7 dbl-click to open` : "",
1919
- nk && descendantsChanged(n.id) ? ` \xB7 ${descendantsChanged(n.id)} changed` : "",
1920
- files ? ` \xB7 ${files} changed file${files > 1 ? "s" : ""}` : ""
1921
- )
1922
- )
2108
+ `Open ${inside} inside \u203A`
2109
+ ) : null
2110
+ )
2111
+ );
2112
+ };
2113
+ const board = () => {
2114
+ const kids = childrenOf(all, level, map.filesByNode);
2115
+ if (!kids.length) return h("div", { class: "empty-state" }, "Nothing at this level.");
2116
+ const hot = kids.filter((n) => touched(all, n));
2117
+ const cold = kids.filter((n) => !touched(all, n));
2118
+ const group = (title, items) => items.length ? h(
2119
+ "div",
2120
+ { class: "map-group" },
2121
+ title ? h("h3", null, title) : null,
2122
+ h("div", { class: "map-grid" }, items.map(card))
2123
+ ) : null;
2124
+ if (explainer) return group(null, kids) ?? h("div", null);
2125
+ if (!hot.length)
2126
+ return h(
2127
+ "div",
2128
+ null,
2129
+ h("div", { class: "map-note muted" }, "Nothing here was touched. The change is elsewhere."),
2130
+ group(null, cold)
1923
2131
  );
1924
- }
1925
- if (!kids.length)
1926
- grid.appendChild(h("div", { class: "empty-state" }, "Nothing at this level."));
1927
- canvas.appendChild(grid);
1928
- const here = new Set(kids.map((k) => k.id));
2132
+ return h(
2133
+ "div",
2134
+ null,
2135
+ group(cold.length ? "What the change touched" : null, hot),
2136
+ group("Context \u2014 unchanged here", cold)
2137
+ );
2138
+ };
2139
+ const links2 = () => {
2140
+ const here = new Set(childrenOf(all, level, map.filesByNode).map((k) => k.id));
1929
2141
  const top = (id) => {
1930
2142
  let x = id;
1931
2143
  while (x && !here.has(x)) x = parentOf(x);
1932
2144
  return x;
1933
2145
  };
1934
- const shown = edges.filter((e) => here.has(top(e.from)) || here.has(top(e.to)));
1935
- if (shown.length) {
1936
- canvas.appendChild(
1937
- h(
2146
+ const folded = [];
2147
+ for (const e of edges) {
2148
+ const from = top(e.from) || e.from;
2149
+ const to = top(e.to) || e.to;
2150
+ if (from === to) continue;
2151
+ if (!here.has(from) && !here.has(to)) continue;
2152
+ if (folded.some((x) => x.from === from && x.to === to)) continue;
2153
+ folded.push({ from, to, ...e.label ? { label: e.label } : {} });
2154
+ }
2155
+ if (!folded.length) return null;
2156
+ return h(
2157
+ "div",
2158
+ { class: "map-edges" },
2159
+ h("h3", null, "What connects to what here"),
2160
+ h(
2161
+ "div",
2162
+ { class: "muted map-note" },
2163
+ explainer ? "A link is a dependency the parts have on each other, drawn as the author declared it." : "A link touching a changed part is where the two sides can fall out of step."
2164
+ ),
2165
+ rankEdges(folded, all).map(
2166
+ ({ edge, touchesChange }) => h(
1938
2167
  "div",
1939
- { class: "map-edges" },
1940
- h("div", { class: "muted", style: { marginBottom: "4px" } }, "Relationships"),
1941
- shown.map(
1942
- (e) => h(
1943
- "div",
1944
- { class: "edge" },
1945
- h(
1946
- "b",
1947
- { onclick: () => navigate("map", { node: e.from }) },
1948
- all.get(e.from)?.label ?? e.from
1949
- ),
1950
- "\u2192",
1951
- h(
1952
- "b",
1953
- { onclick: () => navigate("map", { node: e.to }) },
1954
- all.get(e.to)?.label ?? e.to
1955
- ),
1956
- h("span", { class: "muted" }, e.label ?? "")
1957
- )
1958
- )
2168
+ { class: `edge ${touchesChange ? "hot" : ""}` },
2169
+ h(
2170
+ "b",
2171
+ { onclick: () => navigate("map", { node: edge.from }) },
2172
+ all.get(edge.from)?.label ?? edge.from
2173
+ ),
2174
+ "\u2192",
2175
+ h(
2176
+ "b",
2177
+ { onclick: () => navigate("map", { node: edge.to }) },
2178
+ all.get(edge.to)?.label ?? edge.to
2179
+ ),
2180
+ h("span", { class: "muted" }, edge.label ?? "")
1959
2181
  )
1960
- );
1961
- }
2182
+ )
2183
+ );
2184
+ };
2185
+ const draw = () => {
2186
+ canvas.innerHTML = "";
2187
+ append(canvas, [intro2(), crumbs(), board(), links2()]);
1962
2188
  drawSide();
1963
2189
  };
1964
2190
  const drawSide = () => {
@@ -1969,7 +2195,7 @@
1969
2195
  h(
1970
2196
  "div",
1971
2197
  { class: "muted" },
1972
- "Select a node to see its files, code and relationships. Double-click to drill in."
2198
+ explainer ? "Pick a part to see what it owns, the code behind it, and what it connects to." : "Pick a part to see what changed in it, the code behind it, and what it connects to."
1973
2199
  )
1974
2200
  );
1975
2201
  return;
@@ -1977,19 +2203,14 @@
1977
2203
  const anchor = n.anchor ? state.data?.document?.anchors[n.anchor] : null;
1978
2204
  const files = map.filesByNode[n.id] ?? [];
1979
2205
  const threads = threadsFor((t) => t.type === "map" && t.node === n.id);
2206
+ const neighbours = neighboursOf(map, n.id);
1980
2207
  append(side2, [
1981
2208
  h(
1982
2209
  "div",
1983
2210
  { class: "kind muted", style: { fontSize: "11px", textTransform: "uppercase" } },
1984
2211
  n.kind,
1985
2212
  " ",
1986
- n.status ? h(
1987
- "span",
1988
- {
1989
- class: `badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
1990
- },
1991
- n.status
1992
- ) : ""
2213
+ n.status ? h("span", { class: `badge ${statusClass(n.status)}` }, n.status) : ""
1993
2214
  ),
1994
2215
  h("h3", { style: { margin: "2px 0 6px" } }, n.label),
1995
2216
  h("div", { class: "mono muted", style: { fontSize: "11px" } }, n.id),
@@ -2015,6 +2236,31 @@
2015
2236
  (f) => h("span", { class: "file", onclick: () => navigate("files", { path: f }) }, f)
2016
2237
  )
2017
2238
  ) : null,
2239
+ neighbours.length ? h(
2240
+ "div",
2241
+ { class: "neighbours", style: { marginTop: "12px" } },
2242
+ h("b", null, "Next to it"),
2243
+ h(
2244
+ "div",
2245
+ { class: "muted", style: { fontSize: "11px" } },
2246
+ "\u2192 this reaches \xB7 \u2190 reaches this"
2247
+ ),
2248
+ neighbours.map((x) => {
2249
+ const other = all.get(x.other);
2250
+ return h(
2251
+ "div",
2252
+ { class: "neighbour" },
2253
+ h("span", { class: "mono muted" }, x.dir === "out" ? "\u2192" : "\u2190"),
2254
+ h(
2255
+ "span",
2256
+ { class: "nb", onclick: () => navigate("map", { node: x.other }) },
2257
+ other?.label ?? x.other
2258
+ ),
2259
+ other?.status ? h("span", { class: `badge ${statusClass(other.status)}` }, other.status) : null,
2260
+ x.label ? h("span", { class: "muted" }, x.label) : null
2261
+ );
2262
+ })
2263
+ ) : null,
2018
2264
  h(
2019
2265
  "div",
2020
2266
  { style: { marginTop: "12px" } },
@@ -2055,30 +2301,300 @@
2055
2301
  draw();
2056
2302
  }
2057
2303
 
2304
+ // src/ui/views/coverage.ts
2305
+ init_dom();
2306
+ var SHOWN = 8;
2307
+ function renderCoverage(root) {
2308
+ const cov = state.data?.coverage;
2309
+ if (!cov) {
2310
+ append(root, [
2311
+ h(
2312
+ "div",
2313
+ { class: "empty-state" },
2314
+ h("p", null, "No coverage recorded for this revision."),
2315
+ h("p", null, "Coverage is derived when an explainer is published.")
2316
+ )
2317
+ ]);
2318
+ return;
2319
+ }
2320
+ append(root, [intro(cov), clusters(cov), links(cov), sharedNames(cov), unread(cov)]);
2321
+ }
2322
+ function bar(cov) {
2323
+ const total = Math.max(cov.files.total, 1);
2324
+ const seg = (n, cls, label2) => n ? h("span", {
2325
+ class: `cov-seg ${cls}`,
2326
+ style: { width: `${n / total * 100}%` },
2327
+ title: `${n} ${label2}`
2328
+ }) : null;
2329
+ return h(
2330
+ "div",
2331
+ { class: "cov-bar" },
2332
+ seg(cov.states.explained, "explained", "anchored in the document"),
2333
+ seg(cov.states.placed, "placed", "placed on the map only"),
2334
+ seg(cov.states.uncovered, "uncovered", "not examined")
2335
+ );
2336
+ }
2337
+ function intro(cov) {
2338
+ const key = [
2339
+ [cov.states.explained, "explained", "anchored in the document"],
2340
+ [cov.states.placed, "placed", "placed on the map only"],
2341
+ [cov.states.uncovered, "uncovered", "not examined"]
2342
+ ];
2343
+ return h(
2344
+ "div",
2345
+ { class: "cov-intro" },
2346
+ h("h2", null, "What this explainer covers"),
2347
+ h(
2348
+ "p",
2349
+ null,
2350
+ "A codebase does not fit in a short document, so this one selects. Here is the",
2351
+ " selection, counted rather than claimed: every file in scope at the pinned commit,",
2352
+ " and which of the three states it is in."
2353
+ ),
2354
+ bar(cov),
2355
+ h(
2356
+ "div",
2357
+ { class: "cov-key" },
2358
+ key.map(
2359
+ ([n, cls, label2]) => h("span", { class: "cov-key-item" }, h("i", { class: `cov-dot ${cls}` }), `${n} ${label2}`)
2360
+ )
2361
+ ),
2362
+ h(
2363
+ "div",
2364
+ { class: "cov-facts" },
2365
+ fact("scope", cov.scope === "**" ? "the whole repository" : cov.scope),
2366
+ fact("commit", cov.commit.slice(0, 12)),
2367
+ fact("files in scope", String(cov.files.total)),
2368
+ fact("read by the code graph", `${cov.files.inGraph} of ${cov.files.total}`),
2369
+ cov.unresolved ? fact("references the graph could not place", String(cov.unresolved)) : null,
2370
+ cov.truncated ? fact("file list capped", "the graph is partial; treat every count as a floor") : null
2371
+ )
2372
+ );
2373
+ }
2374
+ function fact(label2, value) {
2375
+ return h("div", { class: "cov-fact" }, h("b", null, value), h("span", { class: "muted" }, label2));
2376
+ }
2377
+ function fileList(files, cls) {
2378
+ if (!files.length) return null;
2379
+ const wrap = h("div", { class: `cov-files ${cls}` });
2380
+ const chip = (f) => h("code", { class: "cov-file", title: f }, f.split("/").slice(-2).join("/"));
2381
+ files.slice(0, SHOWN).forEach((f) => wrap.appendChild(chip(f)));
2382
+ const rest = files.slice(SHOWN);
2383
+ if (rest.length) {
2384
+ const more = h(
2385
+ "button",
2386
+ {
2387
+ class: "small ghost",
2388
+ onclick: () => more.replaceWith(...rest.map(chip))
2389
+ },
2390
+ `show ${rest.length} more`
2391
+ );
2392
+ wrap.appendChild(more);
2393
+ }
2394
+ return wrap;
2395
+ }
2396
+ function clusterRow(c) {
2397
+ const anchors = state.data?.document?.anchors ?? {};
2398
+ const firstAnchor = c.explained.length ? Object.values(anchors).find((a) => a.peek && c.explained.includes(a.peek.file)) : void 0;
2399
+ return h(
2400
+ "div",
2401
+ { class: "cov-cluster" },
2402
+ h(
2403
+ "div",
2404
+ { class: "cov-cluster-head" },
2405
+ h("code", { class: "cov-label" }, c.label),
2406
+ h("span", { class: "muted" }, `${c.files} files \xB7 ${c.symbols} symbols`),
2407
+ h("span", { class: "spacer" }),
2408
+ c.explained.length ? h("span", { class: "badge cov-b-explained" }, `${c.explained.length} anchored`) : null,
2409
+ c.placed.length ? h("span", { class: "badge cov-b-placed" }, `${c.placed.length} placed`) : null,
2410
+ c.uncovered.length ? h("span", { class: "badge cov-b-uncovered" }, `${c.uncovered.length} not examined`) : null,
2411
+ firstAnchor ? h("button", { class: "small", onclick: () => openAnchorPeek(firstAnchor) }, "Peek code \u25B8") : null
2412
+ ),
2413
+ c.hubs.length ? h(
2414
+ "div",
2415
+ { class: "muted cov-hubs" },
2416
+ "most referenced here: ",
2417
+ c.hubs.map((n) => h("code", null, n))
2418
+ ) : null,
2419
+ fileList(c.uncovered, "uncovered")
2420
+ );
2421
+ }
2422
+ function clusters(cov) {
2423
+ return h(
2424
+ "div",
2425
+ { class: "cov-section" },
2426
+ h("h3", null, "By part of the system"),
2427
+ h(
2428
+ "p",
2429
+ { class: "muted" },
2430
+ "Files clustered by how they reference each other at the pinned commit, largest first.",
2431
+ " The files listed under a part are the ones this explainer never examined."
2432
+ ),
2433
+ cov.clusters.map(clusterRow)
2434
+ );
2435
+ }
2436
+ function links(cov) {
2437
+ if (!cov.links.length) return null;
2438
+ return h(
2439
+ "div",
2440
+ { class: "cov-section" },
2441
+ h("h3", null, "What reaches what"),
2442
+ h(
2443
+ "p",
2444
+ { class: "muted" },
2445
+ "References that cross from one part to another, with how many there are.",
2446
+ " A pair marked both ways references each other in both directions."
2447
+ ),
2448
+ h(
2449
+ "div",
2450
+ { class: "cov-links" },
2451
+ cov.links.slice(0, 24).map(
2452
+ (l) => h(
2453
+ "div",
2454
+ { class: "cov-link" },
2455
+ h("code", null, label(cov, l.from)),
2456
+ h("span", { class: "muted" }, "\u2192"),
2457
+ h("code", null, label(cov, l.to)),
2458
+ h("span", { class: "muted mono" }, `${l.references} refs`),
2459
+ l.bothWays ? h("span", { class: "badge" }, "both ways") : null
2460
+ )
2461
+ )
2462
+ )
2463
+ );
2464
+ }
2465
+ function label(cov, id) {
2466
+ return cov.clusters.find((c) => c.id === id)?.label ?? id;
2467
+ }
2468
+ function sharedNames(cov) {
2469
+ if (!cov.sharedNames.length) return null;
2470
+ return h(
2471
+ "div",
2472
+ { class: "cov-section" },
2473
+ h("h3", null, "Names defined in more than one part"),
2474
+ h(
2475
+ "p",
2476
+ { class: "muted" },
2477
+ `${cov.sharedNamesTotal} name${cov.sharedNamesTotal === 1 ? " at this commit is" : "s at this commit are"}`,
2478
+ " defined in two or more parts. What that means here - the same idea in two places,",
2479
+ " two different ideas sharing a word, or a name too common to mean anything - is",
2480
+ " what the code says and this page does not."
2481
+ ),
2482
+ h(
2483
+ "div",
2484
+ { class: "cov-shared" },
2485
+ cov.sharedNames.map(
2486
+ (n) => h(
2487
+ "div",
2488
+ { class: "cov-link" },
2489
+ h("code", null, n.name),
2490
+ h("span", { class: "muted mono" }, `${n.clusters.length} parts`),
2491
+ h("span", { class: "muted" }, n.files.join(" \xB7 "))
2492
+ )
2493
+ )
2494
+ )
2495
+ );
2496
+ }
2497
+ function unread(cov) {
2498
+ if (!cov.outsideGraph.length && !cov.files.capped && !cov.owners.length) return null;
2499
+ return h(
2500
+ "div",
2501
+ { class: "cov-section" },
2502
+ h("h3", null, "Outside the code graph"),
2503
+ cov.outsideGraph.length ? h(
2504
+ "div",
2505
+ null,
2506
+ h(
2507
+ "p",
2508
+ { class: "muted" },
2509
+ `${cov.files.outsideGraph} files in scope are not in a language the graph reads,`,
2510
+ " so they are in no part above. They are absent from the structure, not empty."
2511
+ ),
2512
+ h(
2513
+ "div",
2514
+ { class: "cov-links" },
2515
+ cov.outsideGraph.slice(0, 16).map(
2516
+ (e) => h(
2517
+ "div",
2518
+ { class: "cov-link" },
2519
+ h("code", null, `.${e.extension}`),
2520
+ h("span", { class: "muted mono" }, `${e.files} files`)
2521
+ )
2522
+ )
2523
+ ),
2524
+ fileList(
2525
+ cov.unclustered.filter((u) => u.state === "uncovered" && u.reason === "outsideGraph").map((u) => u.file),
2526
+ "uncovered"
2527
+ )
2528
+ ) : null,
2529
+ cov.files.capped ? h(
2530
+ "div",
2531
+ null,
2532
+ h(
2533
+ "p",
2534
+ { class: "muted" },
2535
+ `${cov.files.capped} files in scope are in a language the graph reads, but the`,
2536
+ " repo-wide file cap was hit before this scope was read, so they were never parsed."
2537
+ ),
2538
+ fileList(
2539
+ cov.unclustered.filter((u) => u.state === "uncovered" && u.reason === "capped").map((u) => u.file),
2540
+ "uncovered"
2541
+ )
2542
+ ) : null,
2543
+ cov.owners.length ? h(
2544
+ "div",
2545
+ null,
2546
+ h("h4", null, "What each map node owns"),
2547
+ h(
2548
+ "p",
2549
+ { class: "muted" },
2550
+ "A file counts as placed because a map node's globs match it. The globs are here",
2551
+ " so a broad one is visible rather than silently inflating the count."
2552
+ ),
2553
+ h(
2554
+ "div",
2555
+ { class: "cov-links" },
2556
+ cov.owners.map(
2557
+ (o) => h(
2558
+ "div",
2559
+ {
2560
+ class: "cov-link",
2561
+ onclick: () => navigate("map", { node: o.node }),
2562
+ style: { cursor: "pointer" }
2563
+ },
2564
+ h("code", null, o.node),
2565
+ h("span", { class: "muted" }, o.globs.join(" ")),
2566
+ h("span", { class: "muted mono" }, `${o.files} files`)
2567
+ )
2568
+ )
2569
+ )
2570
+ ) : null
2571
+ );
2572
+ }
2573
+
2058
2574
  // src/ui/app.ts
2059
2575
  var app = document.getElementById("app");
2060
2576
  async function home() {
2061
2577
  clear(app);
2062
2578
  const reviews = await api.reviews();
2063
- const el = h("div", { class: "home" }, h("h2", null, "Reviews"));
2579
+ const el = h("div", { class: "home" }, h("h2", null, "Reviews and explainers"));
2064
2580
  const active = reviews.filter((r) => !r.dismissed);
2065
2581
  const dismissed = reviews.filter((r) => r.dismissed);
2066
2582
  const item = (r) => h(
2067
2583
  "div",
2068
2584
  { class: "item", onclick: () => location.href = `/review/${r.id}` },
2069
2585
  h("span", { class: "t" }, r.title),
2070
- h("span", { class: `badge ${statusClass(r.status)}` }, r.status),
2586
+ h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
2071
2587
  r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
2072
- h(
2073
- "span",
2074
- { class: "muted mono", style: { fontSize: "12px" } },
2075
- r.binding.kind === "pr" ? `PR #${r.binding.name}` : r.binding.name
2076
- ),
2588
+ h("span", { class: "muted mono", style: { fontSize: "12px" } }, bindingLabel(r)),
2077
2589
  h("span", { class: "muted", style: { fontSize: "12px" } }, timeAgo(r.updatedAt))
2078
2590
  );
2079
2591
  if (!active.length)
2080
2592
  el.appendChild(
2081
- h("div", { class: "empty-state" }, "No reviews. Ask your agent to scaffold and publish one.")
2593
+ h(
2594
+ "div",
2595
+ { class: "empty-state" },
2596
+ "Nothing published yet. Ask your agent for a review of a change, or an explainer of the codebase."
2597
+ )
2082
2598
  );
2083
2599
  active.forEach((r) => el.appendChild(item(r)));
2084
2600
  if (dismissed.length) {
@@ -2087,7 +2603,12 @@
2087
2603
  }
2088
2604
  app.appendChild(el);
2089
2605
  }
2090
- function statusClass(s) {
2606
+ function bindingLabel(r) {
2607
+ if (r.binding.kind === "pr") return `PR #${r.binding.name}`;
2608
+ if (r.binding.kind === "codebase") return r.binding.name === "**" ? "codebase" : r.binding.name;
2609
+ return r.binding.name;
2610
+ }
2611
+ function statusClass2(s) {
2091
2612
  return s === "accepted" ? "ok" : s === "awaiting-agent-updates" ? "warn" : s === "closed" ? "del" : s === "awaiting-review" ? "accent" : "";
2092
2613
  }
2093
2614
  var center;
@@ -2126,12 +2647,16 @@
2126
2647
  const r = d.review;
2127
2648
  const pending = d.threads.filter((t) => !t.submitted).length;
2128
2649
  const open = d.threads.filter((t) => t.status === "open").length;
2129
- const tabs = [
2130
- ["review", "Review"],
2131
- ["commits", "Commits"],
2132
- ["files", `Files${d.changes.length ? ` (${d.changes.length})` : ""}`],
2133
- ["map", "Map"]
2134
- ];
2650
+ const explainer = kind() === "explainer";
2651
+ const labels = {
2652
+ review: explainer ? "Explainer" : "Review",
2653
+ commits: "Commits",
2654
+ files: `Files${d.changes.length ? ` (${d.changes.length})` : ""}`,
2655
+ map: "Map",
2656
+ coverage: "Coverage"
2657
+ };
2658
+ const current2 = view();
2659
+ const tabs = VIEWS[kind()].map((v) => [v, labels[v]]);
2135
2660
  const revSel = h("select", {
2136
2661
  class: "small",
2137
2662
  style: { font: "inherit", fontSize: "12px" },
@@ -2153,7 +2678,7 @@
2153
2678
  const identity = h("div", { class: "bar-row bar-identity" }, [
2154
2679
  h("a", { href: "/", class: "brand", title: "All reviews" }, "thurview"),
2155
2680
  h("span", { class: "title", title: r.title }, r.title),
2156
- h("span", { class: `badge ${statusClass(r.status)}` }, r.status),
2681
+ h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
2157
2682
  r.revision > 1 ? revSel : h("span", { class: "badge bar-rev" }, `rev ${r.revision}`),
2158
2683
  h(
2159
2684
  "span",
@@ -2162,7 +2687,7 @@
2162
2687
  style: { fontSize: "12px" },
2163
2688
  title: `${r.pins.base} \u2192 ${r.pins.head}`
2164
2689
  },
2165
- r.binding.kind === "pr" ? `PR #${r.binding.name}` : r.binding.name
2690
+ bindingLabel(r)
2166
2691
  )
2167
2692
  ]);
2168
2693
  const actions = h("div", { class: "bar-row bar-actions" }, [
@@ -2170,7 +2695,7 @@
2170
2695
  "div",
2171
2696
  { class: "tabs" },
2172
2697
  tabs.map(
2173
- ([v, label]) => h("button", { class: v === state.view ? "active" : "", onclick: () => navigate(v) }, label)
2698
+ ([v, label2]) => h("button", { class: v === current2 ? "active" : "", onclick: () => navigate(v) }, label2)
2174
2699
  )
2175
2700
  ),
2176
2701
  h("span", { class: "spacer" }),
@@ -2255,7 +2780,7 @@
2255
2780
  h(
2256
2781
  "div",
2257
2782
  { class: "item muted" },
2258
- `base ${r.pins.base.slice(0, 12)} \xB7 head ${r.pins.head.slice(0, 12)}`
2783
+ kind() === "explainer" ? `commit ${r.pins.head.slice(0, 12)}` : `base ${r.pins.base.slice(0, 12)} \xB7 head ${r.pins.head.slice(0, 12)}`
2259
2784
  ),
2260
2785
  h(
2261
2786
  "div",
@@ -2268,7 +2793,7 @@
2268
2793
  function renderCenter() {
2269
2794
  const scroll = center.scrollTop;
2270
2795
  clear(center);
2271
- switch (state.view) {
2796
+ switch (view()) {
2272
2797
  case "review":
2273
2798
  renderReview(center);
2274
2799
  break;
@@ -2281,6 +2806,9 @@
2281
2806
  case "map":
2282
2807
  renderMap(center);
2283
2808
  break;
2809
+ case "coverage":
2810
+ renderCoverage(center);
2811
+ break;
2284
2812
  }
2285
2813
  center.scrollTop = scroll;
2286
2814
  }