thurview 0.5.1 → 0.7.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
@@ -970,12 +970,12 @@
970
970
  );
971
971
  const uc = b.usecases[active];
972
972
  body.innerHTML = "";
973
- const touched = /* @__PURE__ */ new Map();
973
+ const touched2 = /* @__PURE__ */ new Map();
974
974
  for (const op of uc.ops) {
975
975
  const [s, c, f] = op.store.split(".");
976
976
  const key = `${s}.${c}`;
977
- if (!touched.has(key)) touched.set(key, /* @__PURE__ */ new Set());
978
- if (f) touched.get(key).add(f);
977
+ if (!touched2.has(key)) touched2.set(key, /* @__PURE__ */ new Set());
978
+ if (f) touched2.get(key).add(f);
979
979
  }
980
980
  const actorIds = [...new Set(uc.ops.map((o) => o.actor))];
981
981
  body.appendChild(
@@ -996,7 +996,7 @@
996
996
  { class: "store" },
997
997
  h("div", { class: "sname" }, s.label, " ", h("span", { class: "badge" }, s.kind)),
998
998
  Object.entries(colls).map(([cid, c]) => {
999
- const hit = touched.get(`${sid}.${cid}`);
999
+ const hit = touched2.get(`${sid}.${cid}`);
1000
1000
  return h(
1001
1001
  "div",
1002
1002
  { class: `table ${hit ? "hit" : ""}` },
@@ -1067,6 +1067,7 @@
1067
1067
  );
1068
1068
  return;
1069
1069
  }
1070
+ root.appendChild(interfaceDelta(doc.interfaces));
1070
1071
  const layout = h("div", { class: "doc-layout" });
1071
1072
  const toc = h(
1072
1073
  "nav",
@@ -1138,6 +1139,102 @@
1138
1139
  );
1139
1140
  docEl.querySelectorAll(".block.heading").forEach((el) => io.observe(el));
1140
1141
  }
1142
+ var DELTA_BLOCK = "interface-delta";
1143
+ var DELTA_SHOWN = 12;
1144
+ var CHANGE_CLASS = {
1145
+ removed: "del",
1146
+ changed: "warn",
1147
+ added: "ok"
1148
+ };
1149
+ function interfaceDelta(delta) {
1150
+ const wrap = h("div", { class: "block ifd", "data-block": DELTA_BLOCK });
1151
+ const threads = threadsFor((t) => t.type === "document" && t.blockId === DELTA_BLOCK);
1152
+ if (threads.length) wrap.classList.add("has-threads");
1153
+ const actions = h(
1154
+ "div",
1155
+ { class: "block-actions" },
1156
+ h(
1157
+ "button",
1158
+ {
1159
+ class: threads.length ? "count" : "",
1160
+ title: "Comment on the interface delta",
1161
+ onclick: (e) => {
1162
+ if (state.viewingRevision !== null) return;
1163
+ popover(commentPopover({ type: "document", blockId: DELTA_BLOCK }), {
1164
+ x: e.pageX + 10,
1165
+ y: e.pageY
1166
+ });
1167
+ }
1168
+ },
1169
+ threads.length ? String(threads.length) : "+"
1170
+ )
1171
+ );
1172
+ const body = h("div", { class: "ifd-body" });
1173
+ const toggle = h("span", { class: "heading-toggle" }, "hide");
1174
+ toggle.addEventListener("click", () => {
1175
+ const hidden = body.hidden;
1176
+ body.hidden = !hidden;
1177
+ toggle.textContent = hidden ? "hide" : "show";
1178
+ });
1179
+ const counts = delta ? ["removed", "changed", "added"].map((c) => [c, delta.entries.filter((e) => e.change === c).length]).filter(([, n]) => n > 0) : [];
1180
+ wrap.appendChild(
1181
+ h(
1182
+ "div",
1183
+ { class: "ifd-head" },
1184
+ h("span", { class: "t" }, "Interface delta"),
1185
+ counts.map(([c, n]) => h("span", { class: `badge ${CHANGE_CLASS[c]}` }, `${n} ${c}`)),
1186
+ h("span", { class: "spacer" }),
1187
+ toggle,
1188
+ actions
1189
+ )
1190
+ );
1191
+ body.appendChild(
1192
+ h(
1193
+ "p",
1194
+ { class: "ifd-verdict" },
1195
+ delta ? delta.verdict : "Unavailable: the code graph could not be built for these commits."
1196
+ )
1197
+ );
1198
+ const entries = delta?.entries ?? [];
1199
+ const rest = entries.slice(DELTA_SHOWN);
1200
+ for (const e of entries.slice(0, DELTA_SHOWN)) body.appendChild(interfaceRow(e));
1201
+ if (rest.length) {
1202
+ const more = h(
1203
+ "button",
1204
+ {
1205
+ class: "small ghost",
1206
+ onclick: () => {
1207
+ more.replaceWith(...rest.map(interfaceRow));
1208
+ }
1209
+ },
1210
+ `show ${rest.length} more`
1211
+ );
1212
+ body.appendChild(more);
1213
+ }
1214
+ wrap.appendChild(body);
1215
+ if (threads.length)
1216
+ wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
1217
+ return wrap;
1218
+ }
1219
+ function interfaceRow(e) {
1220
+ return h(
1221
+ "div",
1222
+ {
1223
+ class: `ifd-entry ifd-${e.change}`,
1224
+ title: "Open in Files",
1225
+ onclick: () => navigate("files", { path: e.file, line: e.line, side: e.graph })
1226
+ },
1227
+ h("span", { class: `badge ${CHANGE_CLASS[e.change]}` }, e.change),
1228
+ h(
1229
+ "div",
1230
+ { class: "ifd-what" },
1231
+ e.was ? h("div", { class: "ifd-was" }, h("code", null, e.was)) : null,
1232
+ h("code", { class: "ifd-name" }, e.name),
1233
+ e.capability ? h("div", { class: "ifd-cap" }, e.capability) : null
1234
+ ),
1235
+ h("span", { class: "ifd-where mono" }, `${e.file}:${e.line}`)
1236
+ );
1237
+ }
1141
1238
  function scrollToBlock(id) {
1142
1239
  const el = document.querySelector(`[data-block="${id}"]`);
1143
1240
  if (!el) return;
@@ -1700,22 +1797,12 @@
1700
1797
 
1701
1798
  // src/ui/views/map.ts
1702
1799
  init_dom();
1703
- function renderMap(root) {
1704
- const map = state.data?.map;
1705
- if (!map) {
1706
- root.appendChild(
1707
- h(
1708
- "div",
1709
- { class: "empty-state" },
1710
- "No software map in this revision. The agent authors ",
1711
- h("code", null, "map.yaml"),
1712
- " and publishes again."
1713
- )
1714
- );
1715
- return;
1716
- }
1717
- const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
1718
- const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
1800
+
1801
+ // src/ui/views/map-model.ts
1802
+ function parentOf(id) {
1803
+ return id.includes(".") ? id.slice(0, id.lastIndexOf(".")) : "";
1804
+ }
1805
+ function collect(map) {
1719
1806
  const all = /* @__PURE__ */ new Map();
1720
1807
  for (const n of map.head.nodes)
1721
1808
  all.set(n.id, {
@@ -1723,145 +1810,262 @@
1723
1810
  status: map.diff.added.includes(n.id) ? "added" : map.diff.changed.includes(n.id) ? "changed" : ""
1724
1811
  });
1725
1812
  for (const id of map.diff.removed) {
1726
- const n = baseNodes.get(id);
1813
+ const n = (map.base?.nodes ?? []).find((x) => x.id === id);
1727
1814
  if (n) all.set(id, { ...n, status: "removed" });
1728
1815
  }
1816
+ return all;
1817
+ }
1818
+ function changedInside(all, id) {
1819
+ let n = 0;
1820
+ for (const item of all.values()) if (item.id.startsWith(id + ".") && item.status) n++;
1821
+ return n;
1822
+ }
1823
+ function touched(all, item) {
1824
+ return !!item.status || changedInside(all, item.id) > 0;
1825
+ }
1826
+ function childrenOf(all, parent, filesByNode) {
1827
+ const kids = [...all.values()].filter((n) => parentOf(n.id) === parent);
1828
+ const weight = (n) => (filesByNode[n.id]?.length ?? 0) + changedInside(all, n.id);
1829
+ return kids.map((n, i) => ({ n, i })).sort((a, b) => {
1830
+ const t = Number(touched(all, b.n)) - Number(touched(all, a.n));
1831
+ if (t) return t;
1832
+ const w = weight(b.n) - weight(a.n);
1833
+ return w || a.i - b.i;
1834
+ }).map((x) => x.n);
1835
+ }
1836
+ function routeIn(all, filesByNode) {
1837
+ const changed = [...all.values()].filter((n) => n.status);
1838
+ const hasChild = (id) => [...all.keys()].some((k) => parentOf(k) === id);
1839
+ const leaves = changed.filter((n) => !hasChild(n.id));
1840
+ const pool = leaves.length ? leaves : changed;
1841
+ let best = null;
1842
+ let bestFiles = -1;
1843
+ for (const n of pool) {
1844
+ const files = filesByNode[n.id]?.length ?? 0;
1845
+ if (files > bestFiles) {
1846
+ best = n;
1847
+ bestFiles = files;
1848
+ }
1849
+ }
1850
+ return best;
1851
+ }
1852
+ function neighboursOf(map, id) {
1853
+ const out = [];
1854
+ const into = [];
1855
+ for (const e of allEdges(map)) {
1856
+ if (e.from === id)
1857
+ out.push({ dir: "out", other: e.to, ...e.label ? { label: e.label } : {} });
1858
+ if (e.to === id)
1859
+ into.push({ dir: "in", other: e.from, ...e.label ? { label: e.label } : {} });
1860
+ }
1861
+ return [...out, ...into];
1862
+ }
1863
+ function allEdges(map) {
1729
1864
  const edges = [...map.head.edges];
1730
1865
  for (const e of map.base?.edges ?? [])
1731
1866
  if (!edges.some((x) => x.from === e.from && x.to === e.to)) edges.push(e);
1732
- const selected = state.params.get("node") ?? "";
1733
- const parentOf = (id) => id.includes(".") ? id.slice(0, id.lastIndexOf(".")) : "";
1734
- let level = selected && all.has(selected) ? childrenOf(selected).length ? selected : parentOf(selected) : "";
1735
- function childrenOf(p) {
1736
- return [...all.values()].filter((n) => parentOf(n.id) === p);
1737
- }
1738
- function descendantsChanged(id) {
1739
- return [...all.values()].filter((n) => n.id.startsWith(id + ".") && n.status).length;
1867
+ return edges;
1868
+ }
1869
+ function rankEdges(edges, all) {
1870
+ const hot = (id) => {
1871
+ const item = all.get(id);
1872
+ return item && touched(all, item) ? 1 : 0;
1873
+ };
1874
+ 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 }));
1875
+ }
1876
+
1877
+ // src/ui/views/map.ts
1878
+ var statusClass = (s) => s === "added" ? "ok" : s === "removed" ? "del" : "warn";
1879
+ function renderMap(root) {
1880
+ const map = state.data?.map;
1881
+ if (!map || !map.head.nodes.length) {
1882
+ append(root, [
1883
+ h(
1884
+ "div",
1885
+ { class: "empty-state" },
1886
+ h("p", null, "No map in this review."),
1887
+ h(
1888
+ "p",
1889
+ null,
1890
+ "The map places a change inside the system and shows what sits next to it. ",
1891
+ "A change that lands in one place, where the Files tab already answers that, ships without one."
1892
+ )
1893
+ )
1894
+ ]);
1895
+ return;
1740
1896
  }
1897
+ const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
1898
+ const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
1899
+ const all = collect(map);
1900
+ const edges = allEdges(map);
1901
+ const route = routeIn(all, map.filesByNode);
1902
+ const selected = state.params.get("node") ?? "";
1903
+ const hasChildren = (id) => [...all.keys()].some((k) => parentOf(k) === id);
1904
+ let level = selected && all.has(selected) ? hasChildren(selected) ? selected : parentOf(selected) : "";
1741
1905
  const canvas = h("div", { class: "map-canvas" });
1742
1906
  const side2 = h("div", { class: "map-side" });
1743
1907
  root.appendChild(h("div", { class: "map-layout" }, canvas, side2));
1744
- const draw = () => {
1745
- canvas.innerHTML = "";
1746
- const crumbs = h("div", { class: "crumbs" });
1747
- const parts = level ? level.split(".") : [];
1748
- crumbs.appendChild(
1908
+ const openLevel = (id) => {
1909
+ level = id;
1910
+ draw();
1911
+ };
1912
+ const intro = () => {
1913
+ const counts = [
1914
+ [map.diff.added.length, "ok", "added"],
1915
+ [map.diff.changed.length, "warn", "changed"],
1916
+ [map.diff.removed.length, "del", "removed"]
1917
+ ];
1918
+ const marks = counts.filter(([n]) => n > 0);
1919
+ return h(
1920
+ "div",
1921
+ { class: "map-intro" },
1922
+ h("h2", null, "Where the change landed"),
1749
1923
  h(
1750
- "span",
1751
- {
1752
- onclick: () => {
1753
- level = "";
1754
- draw();
1755
- }
1756
- },
1757
- "System"
1924
+ "p",
1925
+ null,
1926
+ "The Files tab has the lines. This has the parts of the system they landed in, and what",
1927
+ " those parts connect to \u2014 the neighbours a diff never names. Marked parts are the change;",
1928
+ " the rest is the context you need to judge it."
1929
+ ),
1930
+ h(
1931
+ "div",
1932
+ { class: "map-route" },
1933
+ 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."),
1934
+ route ? h(
1935
+ "button",
1936
+ {
1937
+ class: "small",
1938
+ onclick: () => navigate("map", { node: route.id })
1939
+ },
1940
+ `Start at ${route.label} \u203A`
1941
+ ) : null,
1942
+ map.base ? null : h(
1943
+ "span",
1944
+ { class: "muted" },
1945
+ "No base map, so changed means touched by the diff and nothing reads as added or removed."
1946
+ )
1758
1947
  )
1759
1948
  );
1949
+ };
1950
+ const crumbs = () => {
1951
+ const el = h("div", { class: "crumbs" });
1952
+ const parts = level ? level.split(".") : [];
1953
+ el.appendChild(h("span", { onclick: () => openLevel("") }, "System"));
1760
1954
  parts.forEach((_, i) => {
1761
1955
  const id = parts.slice(0, i + 1).join(".");
1762
- crumbs.appendChild(document.createTextNode(" \u203A "));
1763
- crumbs.appendChild(
1764
- h(
1765
- "span",
1766
- {
1767
- onclick: () => {
1768
- level = id;
1769
- draw();
1770
- }
1771
- },
1772
- all.get(id)?.label ?? id
1773
- )
1774
- );
1956
+ el.appendChild(document.createTextNode(" \u203A "));
1957
+ el.appendChild(h("span", { onclick: () => openLevel(id) }, all.get(id)?.label ?? id));
1775
1958
  });
1776
- canvas.appendChild(crumbs);
1777
- const kids = childrenOf(level);
1778
- const legend = h(
1959
+ return el;
1960
+ };
1961
+ const card = (n) => {
1962
+ const inside = [...all.keys()].filter((k) => parentOf(k) === n.id).length;
1963
+ const hidden = changedInside(all, n.id);
1964
+ const files = map.filesByNode[n.id]?.length ?? 0;
1965
+ return h(
1779
1966
  "div",
1780
- { class: "muted", style: { fontSize: "12px", marginBottom: "10px" } },
1781
- h("span", { class: "badge ok" }, `${map.diff.added.length} added`),
1782
- " ",
1783
- h("span", { class: "badge del" }, `${map.diff.removed.length} removed`),
1784
- " ",
1785
- h("span", { class: "badge warn" }, `${map.diff.changed.length} changed`),
1786
- map.base ? "" : " (no base map: changed = touched by the diff)"
1787
- );
1788
- canvas.appendChild(legend);
1789
- const grid = h("div", { class: "map-grid" });
1790
- for (const n of kids) {
1791
- const nk = childrenOf(n.id).length;
1792
- const files = map.filesByNode[n.id]?.length ?? 0;
1793
- grid.appendChild(
1794
- h(
1795
- "div",
1967
+ {
1968
+ class: `map-node ${n.status} ${touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
1969
+ onclick: () => navigate("map", { node: n.id }),
1970
+ ondblclick: () => inside && openLevel(n.id)
1971
+ },
1972
+ n.status ? h("span", { class: `status badge ${statusClass(n.status)}` }, n.status) : null,
1973
+ h("div", { class: "kind" }, n.kind),
1974
+ h("div", { class: "label" }, n.label),
1975
+ n.description ? h("div", { class: "desc" }, n.description) : null,
1976
+ h(
1977
+ "div",
1978
+ { class: "meta" },
1979
+ files ? h("span", { class: "warn-text" }, `${files} changed file${files > 1 ? "s" : ""}`) : null,
1980
+ hidden ? h("span", { class: "warn-text" }, `${hidden} changed inside`) : null,
1981
+ inside ? h(
1982
+ "button",
1796
1983
  {
1797
- class: `map-node ${n.status} ${n.id === selected ? "selected" : ""}`,
1798
- onclick: () => {
1799
- navigate("map", { node: n.id });
1800
- },
1801
- ondblclick: () => {
1802
- if (nk) {
1803
- level = n.id;
1804
- draw();
1805
- }
1984
+ class: "small drill",
1985
+ onclick: (e) => {
1986
+ e.stopPropagation();
1987
+ openLevel(n.id);
1806
1988
  }
1807
1989
  },
1808
- n.status ? h(
1809
- "span",
1810
- {
1811
- class: `status badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
1812
- },
1813
- n.status
1814
- ) : null,
1815
- h("div", { class: "kind" }, n.kind),
1816
- h("div", { class: "label" }, n.label),
1817
- n.description ? h("div", { class: "desc" }, n.description) : null,
1818
- h(
1819
- "div",
1820
- { class: "children" },
1821
- nk ? `${nk} inside \xB7 dbl-click to open` : "",
1822
- nk && descendantsChanged(n.id) ? ` \xB7 ${descendantsChanged(n.id)} changed` : "",
1823
- files ? ` \xB7 ${files} changed file${files > 1 ? "s" : ""}` : ""
1824
- )
1825
- )
1990
+ `Open ${inside} inside \u203A`
1991
+ ) : null
1992
+ )
1993
+ );
1994
+ };
1995
+ const board = () => {
1996
+ const kids = childrenOf(all, level, map.filesByNode);
1997
+ if (!kids.length) return h("div", { class: "empty-state" }, "Nothing at this level.");
1998
+ const hot = kids.filter((n) => touched(all, n));
1999
+ const cold = kids.filter((n) => !touched(all, n));
2000
+ const group = (title, items) => items.length ? h(
2001
+ "div",
2002
+ { class: "map-group" },
2003
+ title ? h("h3", null, title) : null,
2004
+ h("div", { class: "map-grid" }, items.map(card))
2005
+ ) : null;
2006
+ if (!hot.length)
2007
+ return h(
2008
+ "div",
2009
+ null,
2010
+ h("div", { class: "map-note muted" }, "Nothing here was touched. The change is elsewhere."),
2011
+ group(null, cold)
1826
2012
  );
1827
- }
1828
- if (!kids.length)
1829
- grid.appendChild(h("div", { class: "empty-state" }, "Nothing at this level."));
1830
- canvas.appendChild(grid);
1831
- const here = new Set(kids.map((k) => k.id));
2013
+ return h(
2014
+ "div",
2015
+ null,
2016
+ group(cold.length ? "What the change touched" : null, hot),
2017
+ group("Context \u2014 unchanged here", cold)
2018
+ );
2019
+ };
2020
+ const links = () => {
2021
+ const here = new Set(childrenOf(all, level, map.filesByNode).map((k) => k.id));
1832
2022
  const top = (id) => {
1833
2023
  let x = id;
1834
2024
  while (x && !here.has(x)) x = parentOf(x);
1835
2025
  return x;
1836
2026
  };
1837
- const shown = edges.filter((e) => here.has(top(e.from)) || here.has(top(e.to)));
1838
- if (shown.length) {
1839
- canvas.appendChild(
1840
- h(
2027
+ const folded = [];
2028
+ for (const e of edges) {
2029
+ const from = top(e.from) || e.from;
2030
+ const to = top(e.to) || e.to;
2031
+ if (from === to) continue;
2032
+ if (!here.has(from) && !here.has(to)) continue;
2033
+ if (folded.some((x) => x.from === from && x.to === to)) continue;
2034
+ folded.push({ from, to, ...e.label ? { label: e.label } : {} });
2035
+ }
2036
+ if (!folded.length) return null;
2037
+ return h(
2038
+ "div",
2039
+ { class: "map-edges" },
2040
+ h("h3", null, "What connects to what here"),
2041
+ h(
2042
+ "div",
2043
+ { class: "muted map-note" },
2044
+ "A link touching a changed part is where the two sides can fall out of step."
2045
+ ),
2046
+ rankEdges(folded, all).map(
2047
+ ({ edge, touchesChange }) => h(
1841
2048
  "div",
1842
- { class: "map-edges" },
1843
- h("div", { class: "muted", style: { marginBottom: "4px" } }, "Relationships"),
1844
- shown.map(
1845
- (e) => h(
1846
- "div",
1847
- { class: "edge" },
1848
- h(
1849
- "b",
1850
- { onclick: () => navigate("map", { node: e.from }) },
1851
- all.get(e.from)?.label ?? e.from
1852
- ),
1853
- "\u2192",
1854
- h(
1855
- "b",
1856
- { onclick: () => navigate("map", { node: e.to }) },
1857
- all.get(e.to)?.label ?? e.to
1858
- ),
1859
- h("span", { class: "muted" }, e.label ?? "")
1860
- )
1861
- )
2049
+ { class: `edge ${touchesChange ? "hot" : ""}` },
2050
+ h(
2051
+ "b",
2052
+ { onclick: () => navigate("map", { node: edge.from }) },
2053
+ all.get(edge.from)?.label ?? edge.from
2054
+ ),
2055
+ "\u2192",
2056
+ h(
2057
+ "b",
2058
+ { onclick: () => navigate("map", { node: edge.to }) },
2059
+ all.get(edge.to)?.label ?? edge.to
2060
+ ),
2061
+ h("span", { class: "muted" }, edge.label ?? "")
1862
2062
  )
1863
- );
1864
- }
2063
+ )
2064
+ );
2065
+ };
2066
+ const draw = () => {
2067
+ canvas.innerHTML = "";
2068
+ append(canvas, [intro(), crumbs(), board(), links()]);
1865
2069
  drawSide();
1866
2070
  };
1867
2071
  const drawSide = () => {
@@ -1872,7 +2076,7 @@
1872
2076
  h(
1873
2077
  "div",
1874
2078
  { class: "muted" },
1875
- "Select a node to see its files, code and relationships. Double-click to drill in."
2079
+ "Pick a part to see what changed in it, the code behind it, and what it connects to."
1876
2080
  )
1877
2081
  );
1878
2082
  return;
@@ -1880,19 +2084,14 @@
1880
2084
  const anchor = n.anchor ? state.data?.document?.anchors[n.anchor] : null;
1881
2085
  const files = map.filesByNode[n.id] ?? [];
1882
2086
  const threads = threadsFor((t) => t.type === "map" && t.node === n.id);
2087
+ const neighbours = neighboursOf(map, n.id);
1883
2088
  append(side2, [
1884
2089
  h(
1885
2090
  "div",
1886
2091
  { class: "kind muted", style: { fontSize: "11px", textTransform: "uppercase" } },
1887
2092
  n.kind,
1888
2093
  " ",
1889
- n.status ? h(
1890
- "span",
1891
- {
1892
- class: `badge ${n.status === "added" ? "ok" : n.status === "removed" ? "del" : "warn"}`
1893
- },
1894
- n.status
1895
- ) : ""
2094
+ n.status ? h("span", { class: `badge ${statusClass(n.status)}` }, n.status) : ""
1896
2095
  ),
1897
2096
  h("h3", { style: { margin: "2px 0 6px" } }, n.label),
1898
2097
  h("div", { class: "mono muted", style: { fontSize: "11px" } }, n.id),
@@ -1918,6 +2117,31 @@
1918
2117
  (f) => h("span", { class: "file", onclick: () => navigate("files", { path: f }) }, f)
1919
2118
  )
1920
2119
  ) : null,
2120
+ neighbours.length ? h(
2121
+ "div",
2122
+ { class: "neighbours", style: { marginTop: "12px" } },
2123
+ h("b", null, "Next to it"),
2124
+ h(
2125
+ "div",
2126
+ { class: "muted", style: { fontSize: "11px" } },
2127
+ "\u2192 this reaches \xB7 \u2190 reaches this"
2128
+ ),
2129
+ neighbours.map((x) => {
2130
+ const other = all.get(x.other);
2131
+ return h(
2132
+ "div",
2133
+ { class: "neighbour" },
2134
+ h("span", { class: "mono muted" }, x.dir === "out" ? "\u2192" : "\u2190"),
2135
+ h(
2136
+ "span",
2137
+ { class: "nb", onclick: () => navigate("map", { node: x.other }) },
2138
+ other?.label ?? x.other
2139
+ ),
2140
+ other?.status ? h("span", { class: `badge ${statusClass(other.status)}` }, other.status) : null,
2141
+ x.label ? h("span", { class: "muted" }, x.label) : null
2142
+ );
2143
+ })
2144
+ ) : null,
1921
2145
  h(
1922
2146
  "div",
1923
2147
  { style: { marginTop: "12px" } },
@@ -1970,7 +2194,7 @@
1970
2194
  "div",
1971
2195
  { class: "item", onclick: () => location.href = `/review/${r.id}` },
1972
2196
  h("span", { class: "t" }, r.title),
1973
- h("span", { class: `badge ${statusClass(r.status)}` }, r.status),
2197
+ h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
1974
2198
  r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
1975
2199
  h(
1976
2200
  "span",
@@ -1990,7 +2214,7 @@
1990
2214
  }
1991
2215
  app.appendChild(el);
1992
2216
  }
1993
- function statusClass(s) {
2217
+ function statusClass2(s) {
1994
2218
  return s === "accepted" ? "ok" : s === "awaiting-agent-updates" ? "warn" : s === "closed" ? "del" : s === "awaiting-review" ? "accent" : "";
1995
2219
  }
1996
2220
  var center;
@@ -2056,7 +2280,7 @@
2056
2280
  const identity = h("div", { class: "bar-row bar-identity" }, [
2057
2281
  h("a", { href: "/", class: "brand", title: "All reviews" }, "thurview"),
2058
2282
  h("span", { class: "title", title: r.title }, r.title),
2059
- h("span", { class: `badge ${statusClass(r.status)}` }, r.status),
2283
+ h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
2060
2284
  r.revision > 1 ? revSel : h("span", { class: "badge bar-rev" }, `rev ${r.revision}`),
2061
2285
  h(
2062
2286
  "span",