thurview 0.14.0 → 0.16.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
@@ -1135,6 +1135,7 @@
1135
1135
  root.appendChild(
1136
1136
  kind() === "explainer" ? coveragePanel(state.data?.coverage ?? null) : interfaceDelta(doc.interfaces)
1137
1137
  );
1138
+ if (kind() === "review") root.appendChild(securityPanel(doc.security));
1138
1139
  const layout = h("div", { class: "doc-layout" });
1139
1140
  const toc = h(
1140
1141
  "nav",
@@ -1208,6 +1209,7 @@
1208
1209
  }
1209
1210
  var DELTA_BLOCK = "interface-delta";
1210
1211
  var COVERAGE_BLOCK = "coverage";
1212
+ var SECURITY_BLOCK = "security";
1211
1213
  var DELTA_SHOWN = 12;
1212
1214
  var CHANGE_CLASS = {
1213
1215
  removed: "del",
@@ -1350,6 +1352,80 @@
1350
1352
  wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
1351
1353
  return wrap;
1352
1354
  }
1355
+ function securityPanel(sec) {
1356
+ const wrap = h("div", { class: "block ifd", "data-block": SECURITY_BLOCK });
1357
+ const threads = threadsFor((t) => t.type === "document" && t.blockId === SECURITY_BLOCK);
1358
+ if (threads.length) wrap.classList.add("has-threads");
1359
+ const actions = h(
1360
+ "div",
1361
+ { class: "block-actions" },
1362
+ h(
1363
+ "button",
1364
+ {
1365
+ class: threads.length ? "count" : "",
1366
+ title: "Comment on the trust boundaries",
1367
+ onclick: (e) => {
1368
+ if (state.viewingRevision !== null) return;
1369
+ popover(commentPopover({ type: "document", blockId: SECURITY_BLOCK }), {
1370
+ x: e.pageX + 10,
1371
+ y: e.pageY
1372
+ });
1373
+ }
1374
+ },
1375
+ threads.length ? String(threads.length) : "+"
1376
+ )
1377
+ );
1378
+ const body = h("div", { class: "ifd-body" });
1379
+ const toggle = h("span", { class: "heading-toggle" }, "hide");
1380
+ toggle.addEventListener("click", () => {
1381
+ const hidden = body.hidden;
1382
+ body.hidden = !hidden;
1383
+ toggle.textContent = hidden ? "hide" : "show";
1384
+ });
1385
+ const badge = !sec ? null : sec.state === "none" ? h("span", { class: "badge ok" }, "none") : sec.state === "pending" ? h("span", { class: "badge warn" }, "not assessed") : h("span", { class: "badge warn" }, `${sec.crossings.length} crossed`);
1386
+ wrap.appendChild(
1387
+ h(
1388
+ "div",
1389
+ { class: "ifd-head" },
1390
+ h("span", { class: "t" }, "Trust boundaries"),
1391
+ badge,
1392
+ h("span", { class: "spacer" }),
1393
+ toggle,
1394
+ actions
1395
+ )
1396
+ );
1397
+ body.appendChild(
1398
+ h(
1399
+ "p",
1400
+ { class: "ifd-verdict" },
1401
+ sec ? sec.verdict : "Unavailable: this revision carries no security dimension."
1402
+ )
1403
+ );
1404
+ for (const c of sec?.crossings ?? []) {
1405
+ const anchor = state.data?.document?.anchors[c.anchor];
1406
+ if (!anchor) continue;
1407
+ body.appendChild(
1408
+ h(
1409
+ "div",
1410
+ {
1411
+ class: "ifd-entry",
1412
+ title: `Open ${anchor.title}`,
1413
+ onclick: () => openAnchorPeek(anchor)
1414
+ },
1415
+ h("div", { class: "ifd-what" }, h("div", { class: "ifd-cap" }, c.boundary)),
1416
+ h(
1417
+ "span",
1418
+ { class: "ifd-where mono" },
1419
+ anchor.peek ? `${anchor.peek.file}:${anchor.peek.from}` : anchor.title
1420
+ )
1421
+ )
1422
+ );
1423
+ }
1424
+ wrap.appendChild(body);
1425
+ if (threads.length)
1426
+ wrap.appendChild(h("div", { class: "thread-pins" }, threads.map(threadPinRow)));
1427
+ return wrap;
1428
+ }
1353
1429
  function interfaceRow(e) {
1354
1430
  const anchor = kind() === "design" && e.anchor ? state.data?.document?.anchors[e.anchor] : null;
1355
1431
  return h(
@@ -2646,18 +2722,9 @@
2646
2722
  async function home() {
2647
2723
  clear(app);
2648
2724
  const reviews = await api.reviews();
2649
- const el = h("div", { class: "home" }, h("h2", null, "Reviews, explainers and designs"));
2725
+ const el = h("div", { class: "home" }, h("h2", null, "Queue"));
2650
2726
  const active = reviews.filter((r) => !r.dismissed);
2651
2727
  const dismissed = reviews.filter((r) => r.dismissed);
2652
- const item = (r) => h(
2653
- "div",
2654
- { class: "item", onclick: () => location.href = `/review/${r.id}` },
2655
- h("span", { class: "t" }, r.title),
2656
- h("span", { class: `badge ${statusClass2(r.status)}` }, r.status),
2657
- r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
2658
- h("span", { class: "muted mono", style: { fontSize: "12px" } }, bindingLabel(r)),
2659
- h("span", { class: "muted", style: { fontSize: "12px" } }, timeAgo(r.updatedAt))
2660
- );
2661
2728
  if (!active.length)
2662
2729
  el.appendChild(
2663
2730
  h(
@@ -2666,13 +2733,75 @@
2666
2733
  "Nothing published yet. Ask your agent for a review of a change, an explainer of the codebase, or a design of what to build next."
2667
2734
  )
2668
2735
  );
2669
- active.forEach((r) => el.appendChild(item(r)));
2736
+ const groups = /* @__PURE__ */ new Map();
2737
+ for (const r of active) groups.set(r.queue.repo, [...groups.get(r.queue.repo) ?? [], r]);
2738
+ for (const [repo, rows] of groups) {
2739
+ const yours = rows.filter((r) => r.queue.turn === "you").length;
2740
+ el.appendChild(
2741
+ h(
2742
+ "h3",
2743
+ { class: "repo" },
2744
+ repo,
2745
+ yours ? h("span", { class: "badge accent" }, `${yours} your turn`) : null
2746
+ )
2747
+ );
2748
+ rows.forEach((r) => el.appendChild(queueItem(r)));
2749
+ }
2670
2750
  if (dismissed.length) {
2671
2751
  el.appendChild(h("h3", { class: "muted" }, "Dismissed"));
2672
- dismissed.forEach((r) => el.appendChild(item(r)));
2752
+ dismissed.forEach((r) => el.appendChild(queueItem(r)));
2673
2753
  }
2674
2754
  app.appendChild(el);
2675
2755
  }
2756
+ var TURN = { you: "your turn", agent: "agent", nobody: "nobody" };
2757
+ var DECISION = { approve: "approved", "request-changes": "changes requested", close: "closed" };
2758
+ function queueItem(r) {
2759
+ const q = r.queue;
2760
+ const cr = q.change;
2761
+ const age = q.factsAt ? `forge read ${timeAgo(q.factsAt)}` : cr ? "forge not read yet" : "";
2762
+ return h(
2763
+ "div",
2764
+ { class: "item", onclick: () => location.href = `/review/${r.id}` },
2765
+ h("span", { class: `badge turn ${q.turn === "you" ? "accent" : ""}` }, TURN[q.turn]),
2766
+ h(
2767
+ "span",
2768
+ { class: "t" },
2769
+ r.title,
2770
+ h("span", { class: "why" }, [r.kind ?? "review", q.why, age].filter(Boolean).join(" \xB7 "))
2771
+ ),
2772
+ cr ? h(
2773
+ "a",
2774
+ {
2775
+ class: "mono muted cr",
2776
+ href: cr.url ?? void 0,
2777
+ target: "_blank",
2778
+ rel: "noopener",
2779
+ onclick: (e) => e.stopPropagation()
2780
+ },
2781
+ `#${cr.number}${cr.state && cr.state !== "open" ? ` ${cr.state}` : ""}`
2782
+ ) : h("span", { class: "mono muted" }, bindingLabel(r)),
2783
+ r.openThreads ? h("span", { class: "badge accent" }, `${r.openThreads} open`) : null,
2784
+ q.pin ? h(
2785
+ "span",
2786
+ { class: `badge ${q.pin === "behind" ? "warn" : "ok"}` },
2787
+ q.pin === "behind" ? "pin behind head" : "at head"
2788
+ ) : null,
2789
+ q.decision ? h(
2790
+ "span",
2791
+ {
2792
+ class: `badge ${q.decision.posted === false ? "warn" : ""}`,
2793
+ title: q.decision.posted === null ? null : q.decision.posted ? "posted to the change request" : "not posted to the change request yet"
2794
+ },
2795
+ DECISION[q.decision.decision] + (q.decision.posted === null ? "" : q.decision.posted ? " \xB7 posted" : " \xB7 not posted")
2796
+ ) : null,
2797
+ q.ci ? h(
2798
+ "span",
2799
+ { class: `badge ${q.ci.trustworthy ? "ok" : "warn"}`, title: q.ci.verdict },
2800
+ q.ci.trustworthy ? "CI gates" : "CI not a gate"
2801
+ ) : null,
2802
+ h("span", { class: "muted", style: { fontSize: "12px" } }, timeAgo(r.updatedAt))
2803
+ );
2804
+ }
2676
2805
  function bindingLabel(r) {
2677
2806
  if (r.binding.kind === "pr") return `PR #${r.binding.name}`;
2678
2807
  if (r.binding.kind === "codebase") return r.binding.name === "**" ? "codebase" : r.binding.name;