thurview 0.11.1 → 0.13.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/README.md +139 -91
- package/dist/cli.js +300 -120
- package/dist/cli.js.map +1 -1
- package/dist/document/compile.js +97 -13
- package/dist/document/compile.js.map +1 -1
- package/dist/forge/submission.js +123 -0
- package/dist/forge/submission.js.map +1 -1
- package/dist/store.js +12 -1
- package/dist/store.js.map +1 -1
- package/dist/thread-state.js +23 -0
- package/dist/thread-state.js.map +1 -1
- package/dist/ui/app.css +12 -0
- package/dist/ui/app.js +68 -44
- package/dist/ui/app.js.map +2 -2
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +18 -3
- package/skills/thurview/references/lifecycle.md +26 -0
- package/skills/thurview-design/SKILL.md +265 -0
- package/skills/thurview-design/references/anchors-and-proposals.md +90 -0
package/dist/ui/app.js
CHANGED
|
@@ -165,10 +165,12 @@
|
|
|
165
165
|
// src/ui/state.ts
|
|
166
166
|
var VIEWS = {
|
|
167
167
|
review: ["review", "commits", "files", "map"],
|
|
168
|
-
explainer: ["review", "map", "coverage"]
|
|
168
|
+
explainer: ["review", "map", "coverage"],
|
|
169
|
+
design: ["review", "map"]
|
|
169
170
|
};
|
|
170
171
|
function kind() {
|
|
171
|
-
|
|
172
|
+
const k = state.data?.review.kind;
|
|
173
|
+
return k === "explainer" || k === "design" ? k : "review";
|
|
172
174
|
}
|
|
173
175
|
function view() {
|
|
174
176
|
const allowed = VIEWS[kind()];
|
|
@@ -268,7 +270,8 @@
|
|
|
268
270
|
}
|
|
269
271
|
function commentPopover(target, quote) {
|
|
270
272
|
const ta = h("textarea", { placeholder: "Comment, or a question for the agent\u2026" });
|
|
271
|
-
const
|
|
273
|
+
const k = kind();
|
|
274
|
+
const held = k === "explainer" ? "Add to my notes" : k === "design" ? "Add to my decision" : "Add to the review";
|
|
272
275
|
const submit = async (mode) => {
|
|
273
276
|
const body = ta.value.trim();
|
|
274
277
|
if (!body) return;
|
|
@@ -315,7 +318,7 @@
|
|
|
315
318
|
h(
|
|
316
319
|
"div",
|
|
317
320
|
null,
|
|
318
|
-
`${held} \u2014 held until you ${
|
|
321
|
+
`${held} \u2014 held until you ${k === "explainer" ? "send the document back or finish" : k === "design" ? "approve the design or send it back" : "submit the review"}.`
|
|
319
322
|
)
|
|
320
323
|
)
|
|
321
324
|
);
|
|
@@ -541,8 +544,9 @@
|
|
|
541
544
|
}
|
|
542
545
|
function submitDialog() {
|
|
543
546
|
const pending = (state.data?.threads ?? []).filter((t) => !t.submitted).length;
|
|
544
|
-
const
|
|
545
|
-
const
|
|
547
|
+
const k = kind();
|
|
548
|
+
const explainer = k === "explainer";
|
|
549
|
+
const words = k === "explainer" ? { title: "Send this back or finish", accept: "Done reading", back: "Send it back" } : k === "design" ? { title: "Decide on this design", accept: "Approve the design", back: "Send it back" } : { title: "Submit review", accept: "Approve", back: "Request changes" };
|
|
546
550
|
const ta = h("textarea", { rows: 4, placeholder: "Summary for the agent (optional)" });
|
|
547
551
|
const d = dialog(
|
|
548
552
|
h(
|
|
@@ -562,7 +566,7 @@
|
|
|
562
566
|
h(
|
|
563
567
|
"button",
|
|
564
568
|
{
|
|
565
|
-
title: explainer ? "End it without reading it through" : "End the review without approving it",
|
|
569
|
+
title: explainer ? "End it without reading it through" : k === "design" ? "Drop this design without deciding on it" : "End the review without approving it",
|
|
566
570
|
onclick: () => decide("close")
|
|
567
571
|
},
|
|
568
572
|
"Close"
|
|
@@ -803,6 +807,8 @@
|
|
|
803
807
|
const p = current;
|
|
804
808
|
if (!p) return;
|
|
805
809
|
const inChanges = !!state.data?.changes.some((c) => c.path === p.path);
|
|
810
|
+
const hasFiles = VIEWS[kind()].includes("files");
|
|
811
|
+
const openFile = () => navigate("files", { path: p.path, line: p.highlightLine ?? p.from, side: p.graph });
|
|
806
812
|
const head = h(
|
|
807
813
|
"div",
|
|
808
814
|
{ class: "side-head" },
|
|
@@ -813,23 +819,20 @@
|
|
|
813
819
|
h(
|
|
814
820
|
"div",
|
|
815
821
|
{
|
|
816
|
-
class:
|
|
822
|
+
class: `path${hasFiles ? "" : " inert"}`,
|
|
817
823
|
title: p.path,
|
|
818
|
-
|
|
824
|
+
...hasFiles ? { onclick: openFile } : {}
|
|
819
825
|
},
|
|
820
826
|
`${p.path}:${p.from}-${p.to}`,
|
|
821
827
|
" ",
|
|
822
828
|
h("span", { class: `badge ${p.graph === "base" ? "del" : ""}` }, p.graph)
|
|
823
829
|
)
|
|
824
830
|
),
|
|
825
|
-
h(
|
|
831
|
+
hasFiles ? h(
|
|
826
832
|
"button",
|
|
827
|
-
{
|
|
828
|
-
class: "small",
|
|
829
|
-
onclick: () => navigate("files", { path: p.path, line: p.highlightLine ?? p.from, side: p.graph })
|
|
830
|
-
},
|
|
833
|
+
{ class: "small", onclick: openFile },
|
|
831
834
|
inChanges ? "Open in Files" : "Open file"
|
|
832
|
-
),
|
|
835
|
+
) : null,
|
|
833
836
|
h(
|
|
834
837
|
"button",
|
|
835
838
|
{
|
|
@@ -1212,6 +1215,7 @@
|
|
|
1212
1215
|
added: "ok"
|
|
1213
1216
|
};
|
|
1214
1217
|
function interfaceDelta(delta) {
|
|
1218
|
+
const design = kind() === "design";
|
|
1215
1219
|
const wrap = h("div", { class: "block ifd", "data-block": DELTA_BLOCK });
|
|
1216
1220
|
const threads = threadsFor((t) => t.type === "document" && t.blockId === DELTA_BLOCK);
|
|
1217
1221
|
if (threads.length) wrap.classList.add("has-threads");
|
|
@@ -1222,7 +1226,7 @@
|
|
|
1222
1226
|
"button",
|
|
1223
1227
|
{
|
|
1224
1228
|
class: threads.length ? "count" : "",
|
|
1225
|
-
title: "Comment on the interface delta",
|
|
1229
|
+
title: design ? "Comment on what this design proposes" : "Comment on the interface delta",
|
|
1226
1230
|
onclick: (e) => {
|
|
1227
1231
|
if (state.viewingRevision !== null) return;
|
|
1228
1232
|
popover(commentPopover({ type: "document", blockId: DELTA_BLOCK }), {
|
|
@@ -1246,7 +1250,7 @@
|
|
|
1246
1250
|
h(
|
|
1247
1251
|
"div",
|
|
1248
1252
|
{ class: "ifd-head" },
|
|
1249
|
-
h("span", { class: "t" }, "Interface delta"),
|
|
1253
|
+
h("span", { class: "t" }, design ? "What this design proposes" : "Interface delta"),
|
|
1250
1254
|
counts.map(([c, n]) => h("span", { class: `badge ${CHANGE_CLASS[c]}` }, `${n} ${c}`)),
|
|
1251
1255
|
h("span", { class: "spacer" }),
|
|
1252
1256
|
toggle,
|
|
@@ -1257,7 +1261,7 @@
|
|
|
1257
1261
|
h(
|
|
1258
1262
|
"p",
|
|
1259
1263
|
{ class: "ifd-verdict" },
|
|
1260
|
-
delta ? delta.verdict : "Unavailable: the code graph could not be built for these commits."
|
|
1264
|
+
delta ? delta.verdict : design ? "Unavailable: this revision declared no proposal." : "Unavailable: the code graph could not be built for these commits."
|
|
1261
1265
|
)
|
|
1262
1266
|
);
|
|
1263
1267
|
const entries = delta?.entries ?? [];
|
|
@@ -1347,12 +1351,13 @@
|
|
|
1347
1351
|
return wrap;
|
|
1348
1352
|
}
|
|
1349
1353
|
function interfaceRow(e) {
|
|
1354
|
+
const anchor = kind() === "design" && e.anchor ? state.data?.document?.anchors[e.anchor] : null;
|
|
1350
1355
|
return h(
|
|
1351
1356
|
"div",
|
|
1352
1357
|
{
|
|
1353
1358
|
class: `ifd-entry ifd-${e.change}`,
|
|
1354
|
-
title: "Open in Files",
|
|
1355
|
-
onclick: () => navigate("files", { path: e.file, line: e.line, side: e.graph })
|
|
1359
|
+
title: anchor ? `Open ${anchor.title}` : "Open in Files",
|
|
1360
|
+
onclick: () => anchor ? openAnchorPeek(anchor) : navigate("files", { path: e.file, line: e.line, side: e.graph })
|
|
1356
1361
|
},
|
|
1357
1362
|
h("span", { class: `badge ${CHANGE_CLASS[e.change]}` }, e.change),
|
|
1358
1363
|
h(
|
|
@@ -1362,7 +1367,11 @@
|
|
|
1362
1367
|
h("code", { class: "ifd-name" }, e.name),
|
|
1363
1368
|
e.capability ? h("div", { class: "ifd-cap" }, e.capability) : null
|
|
1364
1369
|
),
|
|
1365
|
-
h(
|
|
1370
|
+
h(
|
|
1371
|
+
"span",
|
|
1372
|
+
{ class: "ifd-where mono", title: anchor ? "Where it lands in the code today" : null },
|
|
1373
|
+
`${anchor ? "at " : ""}${e.file}:${e.line}`
|
|
1374
|
+
)
|
|
1366
1375
|
);
|
|
1367
1376
|
}
|
|
1368
1377
|
function scrollToBlock(id) {
|
|
@@ -1415,9 +1424,12 @@
|
|
|
1415
1424
|
"div",
|
|
1416
1425
|
{ class: "frame-head" },
|
|
1417
1426
|
h("span", { class: "title" }, a.title),
|
|
1427
|
+
// Only a review has a Files tab; on the other kinds this would
|
|
1428
|
+
// navigate to a tab that does not exist and bounce the reader to
|
|
1429
|
+
// the document, so it offers nothing rather than offering that.
|
|
1418
1430
|
h(
|
|
1419
1431
|
"span",
|
|
1420
|
-
{
|
|
1432
|
+
VIEWS[kind()].includes("files") ? {
|
|
1421
1433
|
class: "path",
|
|
1422
1434
|
title: "Open in Files",
|
|
1423
1435
|
onclick: () => navigate("files", {
|
|
@@ -1425,7 +1437,7 @@
|
|
|
1425
1437
|
line: a.peek.from,
|
|
1426
1438
|
side: a.peek.graph
|
|
1427
1439
|
})
|
|
1428
|
-
},
|
|
1440
|
+
} : { class: "path inert" },
|
|
1429
1441
|
`${a.peek.file}:${a.peek.from}-${a.peek.to}`
|
|
1430
1442
|
),
|
|
1431
1443
|
h("span", { class: `badge ${a.peek.graph === "base" ? "del" : ""}` }, a.peek.graph),
|
|
@@ -2009,6 +2021,7 @@
|
|
|
2009
2021
|
function renderMap(root) {
|
|
2010
2022
|
const map = state.data?.map;
|
|
2011
2023
|
const explainer = kind() === "explainer";
|
|
2024
|
+
const design = kind() === "design";
|
|
2012
2025
|
if (!map || !map.head.nodes.length) {
|
|
2013
2026
|
append(root, [
|
|
2014
2027
|
explainer ? h(
|
|
@@ -2025,17 +2038,17 @@
|
|
|
2025
2038
|
) : h(
|
|
2026
2039
|
"div",
|
|
2027
2040
|
{ class: "empty-state" },
|
|
2028
|
-
h("p", null, "No map in this review."),
|
|
2041
|
+
h("p", null, design ? "No map in this design." : "No map in this review."),
|
|
2029
2042
|
h(
|
|
2030
2043
|
"p",
|
|
2031
2044
|
null,
|
|
2032
|
-
"The map places a change inside the system and shows what sits next to it. "
|
|
2033
|
-
"A change that lands in one place, where the Files tab already answers that, ships without one."
|
|
2045
|
+
design ? "The map is where a design shows the shape it proposes: the parts it would add, change or remove, beside the structure as it stands. A design that changes one part in place ships without one." : "The map places a change inside the system and shows what sits next to it. A change that lands in one place, where the Files tab already answers that, ships without one."
|
|
2034
2046
|
)
|
|
2035
2047
|
)
|
|
2036
2048
|
]);
|
|
2037
2049
|
return;
|
|
2038
2050
|
}
|
|
2051
|
+
const unmarked = design && !map.base;
|
|
2039
2052
|
const headNodes = new Map(map.head.nodes.map((n) => [n.id, n]));
|
|
2040
2053
|
const baseNodes = new Map((map.base?.nodes ?? []).map((n) => [n.id, n]));
|
|
2041
2054
|
const all = collect(map);
|
|
@@ -2083,18 +2096,20 @@
|
|
|
2083
2096
|
return h(
|
|
2084
2097
|
"div",
|
|
2085
2098
|
{ class: "map-intro" },
|
|
2086
|
-
h("h2", null, "Where the change landed"),
|
|
2099
|
+
h("h2", null, design ? "The shape this design proposes" : "Where the change landed"),
|
|
2087
2100
|
h(
|
|
2088
2101
|
"p",
|
|
2089
2102
|
null,
|
|
2090
|
-
"The Files tab has the lines. This has the parts of the system they landed in, and what"
|
|
2091
|
-
" those parts connect to \u2014 the neighbours a diff never names. Marked parts are the change;",
|
|
2092
|
-
" the rest is the context you need to judge it."
|
|
2103
|
+
unmarked ? "These are the parts the design proposes. Nothing is marked added, changed or removed: the document declares no structure as it stands to compare them against." : design ? "Marked parts are what the design would add, change or remove; the rest is the structure as it stands, which is what it has to fit." : "The Files tab has the lines. This has the parts of the system they landed in, and what those parts connect to \u2014 the neighbours a diff never names. Marked parts are the change; the rest is the context you need to judge it."
|
|
2093
2104
|
),
|
|
2094
2105
|
h(
|
|
2095
2106
|
"div",
|
|
2096
2107
|
{ class: "map-route" },
|
|
2097
|
-
marks.length ? marks.map(([n, cls, word]) => h("span", { class: `badge ${cls}` }, `${n} ${word}`)) :
|
|
2108
|
+
marks.length ? marks.map(([n, cls, word]) => h("span", { class: `badge ${cls}` }, `${n} ${word}`)) : unmarked ? null : h(
|
|
2109
|
+
"span",
|
|
2110
|
+
{ class: "muted" },
|
|
2111
|
+
design ? "This design changes no part of the map." : "This change touched no part of the map."
|
|
2112
|
+
),
|
|
2098
2113
|
route ? h(
|
|
2099
2114
|
"button",
|
|
2100
2115
|
{
|
|
@@ -2103,7 +2118,9 @@
|
|
|
2103
2118
|
},
|
|
2104
2119
|
`Start at ${route.label} \u203A`
|
|
2105
2120
|
) : null,
|
|
2106
|
-
|
|
2121
|
+
// On a design the paragraph above already says this, so saying it again
|
|
2122
|
+
// two lines down is the same sentence twice.
|
|
2123
|
+
map.base || unmarked ? null : h(
|
|
2107
2124
|
"span",
|
|
2108
2125
|
{ class: "muted" },
|
|
2109
2126
|
"No base map, so changed means touched by the diff and nothing reads as added or removed."
|
|
@@ -2129,7 +2146,7 @@
|
|
|
2129
2146
|
return h(
|
|
2130
2147
|
"div",
|
|
2131
2148
|
{
|
|
2132
|
-
class: `map-node ${n.status} ${explainer || touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
|
|
2149
|
+
class: `map-node ${n.status} ${explainer || unmarked || touched(all, n) ? "" : "quiet"} ${n.id === selected ? "selected" : ""}`,
|
|
2133
2150
|
onclick: () => navigate("map", { node: n.id }),
|
|
2134
2151
|
ondblclick: () => inside && openLevel(n.id)
|
|
2135
2152
|
},
|
|
@@ -2167,19 +2184,26 @@
|
|
|
2167
2184
|
title ? h("h3", null, title) : null,
|
|
2168
2185
|
h("div", { class: "map-grid" }, items.map(card))
|
|
2169
2186
|
) : null;
|
|
2170
|
-
if (explainer) return group(null, kids) ?? h("div", null);
|
|
2187
|
+
if (explainer || unmarked) return group(null, kids) ?? h("div", null);
|
|
2171
2188
|
if (!hot.length)
|
|
2172
2189
|
return h(
|
|
2173
2190
|
"div",
|
|
2174
2191
|
null,
|
|
2175
|
-
h(
|
|
2192
|
+
h(
|
|
2193
|
+
"div",
|
|
2194
|
+
{ class: "map-note muted" },
|
|
2195
|
+
design ? "Nothing here changes. The design is elsewhere." : "Nothing here was touched. The change is elsewhere."
|
|
2196
|
+
),
|
|
2176
2197
|
group(null, cold)
|
|
2177
2198
|
);
|
|
2178
2199
|
return h(
|
|
2179
2200
|
"div",
|
|
2180
2201
|
null,
|
|
2181
|
-
group(
|
|
2182
|
-
|
|
2202
|
+
group(
|
|
2203
|
+
cold.length ? design ? "What the design changes" : "What the change touched" : null,
|
|
2204
|
+
hot
|
|
2205
|
+
),
|
|
2206
|
+
group(design ? "Context \u2014 untouched by the design" : "Context \u2014 unchanged here", cold)
|
|
2183
2207
|
);
|
|
2184
2208
|
};
|
|
2185
2209
|
const links2 = () => {
|
|
@@ -2206,7 +2230,7 @@
|
|
|
2206
2230
|
h(
|
|
2207
2231
|
"div",
|
|
2208
2232
|
{ class: "muted map-note" },
|
|
2209
|
-
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."
|
|
2233
|
+
explainer ? "A link is a dependency the parts have on each other, drawn as the author declared it." : unmarked ? "A link is a dependency the design says the parts would have on each other." : design ? "A link touching a part the design changes is where the two sides can fall out of step." : "A link touching a changed part is where the two sides can fall out of step."
|
|
2210
2234
|
),
|
|
2211
2235
|
rankEdges(folded, all).map(
|
|
2212
2236
|
({ edge, touchesChange }) => h(
|
|
@@ -2241,7 +2265,7 @@
|
|
|
2241
2265
|
h(
|
|
2242
2266
|
"div",
|
|
2243
2267
|
{ class: "muted" },
|
|
2244
|
-
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."
|
|
2268
|
+
explainer || unmarked ? "Pick a part to see what it owns, the code behind it, and what it connects to." : design ? "Pick a part to see what the design changes in it, 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."
|
|
2245
2269
|
)
|
|
2246
2270
|
);
|
|
2247
2271
|
return;
|
|
@@ -2622,7 +2646,7 @@
|
|
|
2622
2646
|
async function home() {
|
|
2623
2647
|
clear(app);
|
|
2624
2648
|
const reviews = await api.reviews();
|
|
2625
|
-
const el = h("div", { class: "home" }, h("h2", null, "Reviews and
|
|
2649
|
+
const el = h("div", { class: "home" }, h("h2", null, "Reviews, explainers and designs"));
|
|
2626
2650
|
const active = reviews.filter((r) => !r.dismissed);
|
|
2627
2651
|
const dismissed = reviews.filter((r) => r.dismissed);
|
|
2628
2652
|
const item = (r) => h(
|
|
@@ -2639,7 +2663,7 @@
|
|
|
2639
2663
|
h(
|
|
2640
2664
|
"div",
|
|
2641
2665
|
{ class: "empty-state" },
|
|
2642
|
-
"Nothing published yet. Ask your agent for a review of a change,
|
|
2666
|
+
"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."
|
|
2643
2667
|
)
|
|
2644
2668
|
);
|
|
2645
2669
|
active.forEach((r) => el.appendChild(item(r)));
|
|
@@ -2693,9 +2717,9 @@
|
|
|
2693
2717
|
const r = d.review;
|
|
2694
2718
|
const pending = d.threads.filter((t) => !t.submitted).length;
|
|
2695
2719
|
const open = d.threads.filter((t) => t.status === "open").length;
|
|
2696
|
-
const
|
|
2720
|
+
const k = kind();
|
|
2697
2721
|
const labels = {
|
|
2698
|
-
review: explainer ? "Explainer" : "Review",
|
|
2722
|
+
review: k === "explainer" ? "Explainer" : k === "design" ? "Design" : "Review",
|
|
2699
2723
|
commits: "Commits",
|
|
2700
2724
|
files: `Files${d.changes.length ? ` (${d.changes.length})` : ""}`,
|
|
2701
2725
|
map: "Map",
|
|
@@ -2826,7 +2850,7 @@
|
|
|
2826
2850
|
h(
|
|
2827
2851
|
"div",
|
|
2828
2852
|
{ class: "item muted" },
|
|
2829
|
-
kind() === "
|
|
2853
|
+
kind() === "review" ? `base ${r.pins.base.slice(0, 12)} \xB7 head ${r.pins.head.slice(0, 12)}` : `commit ${r.pins.head.slice(0, 12)}`
|
|
2830
2854
|
),
|
|
2831
2855
|
h(
|
|
2832
2856
|
"div",
|