thurview 0.8.1 → 0.8.3
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 +7 -4
- package/dist/cli.js +72 -63
- package/dist/cli.js.map +1 -1
- package/dist/presence.js +34 -0
- package/dist/presence.js.map +1 -0
- package/dist/server/server.js +4 -0
- package/dist/server/server.js.map +1 -1
- package/dist/store.js +10 -0
- package/dist/store.js.map +1 -1
- package/dist/thread-state.js +17 -0
- package/dist/thread-state.js.map +1 -0
- package/dist/threads.js +10 -9
- package/dist/threads.js.map +1 -1
- package/dist/ui/app.css +18 -0
- package/dist/ui/app.js +82 -24
- package/dist/ui/app.js.map +4 -4
- package/package.json +1 -1
- package/skills/thurview/SKILL.md +12 -3
- package/skills/thurview/references/lifecycle.md +54 -7
package/dist/store.js
CHANGED
|
@@ -19,6 +19,15 @@ export function revisionDir(id, n) {
|
|
|
19
19
|
export function serverStateFile() {
|
|
20
20
|
return join(home(), "server.json");
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Heartbeat of an agent draining this review's threads. It lives outside
|
|
24
|
+
* `reviewDir` on purpose: the server watches that directory to push changes to
|
|
25
|
+
* the browser, and a file rewritten every few seconds would reload the page
|
|
26
|
+
* under the reader's hands.
|
|
27
|
+
*/
|
|
28
|
+
export function agentFile(id) {
|
|
29
|
+
return join(home(), "agents", `${id}.json`);
|
|
30
|
+
}
|
|
22
31
|
export function now() {
|
|
23
32
|
return new Date().toISOString();
|
|
24
33
|
}
|
|
@@ -68,6 +77,7 @@ export async function listReviews() {
|
|
|
68
77
|
}
|
|
69
78
|
export async function deleteReview(id) {
|
|
70
79
|
await rm(reviewDir(id), { recursive: true, force: true });
|
|
80
|
+
await rm(agentFile(id), { force: true });
|
|
71
81
|
}
|
|
72
82
|
export async function readThreads(id) {
|
|
73
83
|
return ((await readJson(join(reviewDir(id), "threads.json"))) ?? {
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACzF,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** Threads the agent must act on: open, submitted, and the last message is from the reviewer. */
|
|
2
|
+
export function needsAgent(th) {
|
|
3
|
+
if (th.status !== "open" || !th.submitted)
|
|
4
|
+
return false;
|
|
5
|
+
const last = th.messages[th.messages.length - 1];
|
|
6
|
+
return !!last && last.role === "reviewer";
|
|
7
|
+
}
|
|
8
|
+
export function deliveryOf(th, agent) {
|
|
9
|
+
if (th.status === "resolved")
|
|
10
|
+
return "closed";
|
|
11
|
+
if (!th.submitted)
|
|
12
|
+
return "held";
|
|
13
|
+
if (!needsAgent(th))
|
|
14
|
+
return "answered";
|
|
15
|
+
return agent.attached ? "listening" : "queued";
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=thread-state.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"thread-state.js","sourceRoot":"","sources":["../src/thread-state.ts"],"names":[],"mappings":"AAGA,iGAAiG;AACjG,MAAM,UAAU,UAAU,CAAC,EAAU;IACnC,IAAI,EAAE,CAAC,MAAM,KAAK,MAAM,IAAI,CAAC,EAAE,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IACxD,MAAM,IAAI,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACjD,OAAO,CAAC,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,UAAU,CAAC;AAC5C,CAAC;AAeD,MAAM,UAAU,UAAU,CAAC,EAAU,EAAE,KAAe;IACpD,IAAI,EAAE,CAAC,MAAM,KAAK,UAAU;QAAE,OAAO,QAAQ,CAAC;IAC9C,IAAI,CAAC,EAAE,CAAC,SAAS;QAAE,OAAO,MAAM,CAAC;IACjC,IAAI,CAAC,UAAU,CAAC,EAAE,CAAC;QAAE,OAAO,UAAU,CAAC;IACvC,OAAO,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC;AACjD,CAAC"}
|
package/dist/threads.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readThreads, writeThreads, readReview, writeReview, newId, now, } from "./store.js";
|
|
2
|
+
export { needsAgent } from "./thread-state.js";
|
|
2
3
|
export async function createThread(reviewId, input) {
|
|
3
4
|
const review = await readReview(reviewId);
|
|
4
5
|
if (!review)
|
|
@@ -28,8 +29,15 @@ export async function replyThread(reviewId, threadId, role, body) {
|
|
|
28
29
|
throw new Error(`thread ${threadId} not found`);
|
|
29
30
|
thread.messages.push({ role, body, at: now() });
|
|
30
31
|
thread.updatedAt = now();
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
// A message from the reader is a message that wants an answer. Leaving the
|
|
33
|
+
// thread resolved would drop it: `needsAgent` is false there, so `wait` never
|
|
34
|
+
// reports it and `threads list --open` never shows it. The reader would be
|
|
35
|
+
// writing to nobody, with a Reply button that says otherwise.
|
|
36
|
+
if (role === "reviewer") {
|
|
37
|
+
thread.status = "open";
|
|
38
|
+
if (thread.mode === "ask")
|
|
39
|
+
thread.submitted = true;
|
|
40
|
+
}
|
|
33
41
|
await writeThreads(reviewId, t);
|
|
34
42
|
return thread;
|
|
35
43
|
}
|
|
@@ -73,13 +81,6 @@ export async function submitReview(reviewId, decision, body) {
|
|
|
73
81
|
await writeReview(review);
|
|
74
82
|
return t;
|
|
75
83
|
}
|
|
76
|
-
/** Threads the agent must act on: open, submitted, and the last message is from the reviewer. */
|
|
77
|
-
export function needsAgent(th) {
|
|
78
|
-
if (th.status !== "open" || !th.submitted)
|
|
79
|
-
return false;
|
|
80
|
-
const last = th.messages[th.messages.length - 1];
|
|
81
|
-
return !!last && last.role === "reviewer";
|
|
82
|
-
}
|
|
83
84
|
export function threadSummary(th) {
|
|
84
85
|
return {
|
|
85
86
|
id: th.id,
|
package/dist/threads.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"threads.js","sourceRoot":"","sources":["../src/threads.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,YAAY,EACZ,UAAU,EACV,WAAW,EACX,KAAK,EACL,GAAG,GAIJ,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"threads.js","sourceRoot":"","sources":["../src/threads.ts"],"names":[],"mappings":"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"}
|
package/dist/ui/app.css
CHANGED
|
@@ -539,6 +539,24 @@ kbd {
|
|
|
539
539
|
.thread-pin.resolved {
|
|
540
540
|
opacity: 0.55;
|
|
541
541
|
}
|
|
542
|
+
/* Where the reader's last message stands. `queued` is the one that matters:
|
|
543
|
+
nobody is listening, and the page says so instead of implying delivery. */
|
|
544
|
+
.receipt {
|
|
545
|
+
font-size: 12px;
|
|
546
|
+
line-height: 1.45;
|
|
547
|
+
color: var(--muted);
|
|
548
|
+
margin: 4px 0 2px;
|
|
549
|
+
padding-left: 8px;
|
|
550
|
+
border-left: 2px solid var(--line);
|
|
551
|
+
}
|
|
552
|
+
.receipt.listening {
|
|
553
|
+
color: var(--ok);
|
|
554
|
+
border-left-color: var(--ok);
|
|
555
|
+
}
|
|
556
|
+
.receipt.queued {
|
|
557
|
+
color: var(--warn);
|
|
558
|
+
border-left-color: var(--warn);
|
|
559
|
+
}
|
|
542
560
|
.comment-popover {
|
|
543
561
|
border-radius: var(--radius);
|
|
544
562
|
position: absolute;
|
package/dist/ui/app.js
CHANGED
|
@@ -143,6 +143,7 @@
|
|
|
143
143
|
review: (id, revision) => j(`/api/reviews/${id}${revision ? `?revision=${revision}` : ""}`),
|
|
144
144
|
revisions: (id) => j(`/api/reviews/${id}/revisions`),
|
|
145
145
|
commits: (id) => j(`/api/reviews/${id}/commits`),
|
|
146
|
+
presence: (id) => j(`/api/reviews/${id}/presence`),
|
|
146
147
|
diff: (id, path) => j(`/api/reviews/${id}/diff?path=${encodeURIComponent(path)}`),
|
|
147
148
|
file: (id, path, graph, from, to) => j(
|
|
148
149
|
`/api/reviews/${id}/file?path=${encodeURIComponent(path)}&graph=${graph}${from ? `&from=${from}` : ""}${to ? `&to=${to}` : ""}`
|
|
@@ -232,32 +233,46 @@
|
|
|
232
233
|
|
|
233
234
|
// src/ui/threads.ts
|
|
234
235
|
init_dom();
|
|
236
|
+
|
|
237
|
+
// src/thread-state.ts
|
|
238
|
+
function needsAgent(th) {
|
|
239
|
+
if (th.status !== "open" || !th.submitted) return false;
|
|
240
|
+
const last = th.messages[th.messages.length - 1];
|
|
241
|
+
return !!last && last.role === "reviewer";
|
|
242
|
+
}
|
|
243
|
+
function deliveryOf(th, agent2) {
|
|
244
|
+
if (th.status === "resolved") return "closed";
|
|
245
|
+
if (!th.submitted) return "held";
|
|
246
|
+
if (!needsAgent(th)) return "answered";
|
|
247
|
+
return agent2.attached ? "listening" : "queued";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/ui/threads.ts
|
|
235
251
|
async function reload() {
|
|
236
252
|
state.data = await api.review(state.id, state.viewingRevision ?? void 0);
|
|
237
253
|
emit("threads");
|
|
238
254
|
}
|
|
255
|
+
var NOBODY = { attached: false, lastSeen: null };
|
|
256
|
+
function agent() {
|
|
257
|
+
return state.data?.agent ?? NOBODY;
|
|
258
|
+
}
|
|
259
|
+
var RECEIPT = {
|
|
260
|
+
held: "Held. It reaches the agent when you submit.",
|
|
261
|
+
queued: "Queued. Nobody is listening right now \u2014 it is delivered the next time an agent checks this document.",
|
|
262
|
+
listening: "Delivered. An agent is listening and answers here.",
|
|
263
|
+
answered: "The agent answered. Reply, or resolve it.",
|
|
264
|
+
closed: "Resolved. Writing here reopens it."
|
|
265
|
+
};
|
|
266
|
+
function listeningLine() {
|
|
267
|
+
return agent().attached ? "An agent is listening to this document now." : "No agent is listening right now. Anything you send is queued until one checks in.";
|
|
268
|
+
}
|
|
239
269
|
function commentPopover(target, quote) {
|
|
240
270
|
const ta = h("textarea", { placeholder: "Comment, or a question for the agent\u2026" });
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
const b1 = h("button", { class: "small primary", onclick: () => set("review") }, "Add to review");
|
|
244
|
-
const b2 = h("button", { class: "small", onclick: () => set("ask") }, "Ask now");
|
|
245
|
-
const set = (m2) => {
|
|
246
|
-
mode = m2;
|
|
247
|
-
b1.className = `small ${m2 === "review" ? "primary" : ""}`;
|
|
248
|
-
b2.className = `small ${m2 === "ask" ? "primary" : ""}`;
|
|
249
|
-
hint.textContent = m2 === "review" ? "Held until you submit the review." : "Sent to the agent at once; it answers in this thread.";
|
|
250
|
-
};
|
|
251
|
-
const hint = h(
|
|
252
|
-
"span",
|
|
253
|
-
{ class: "muted", style: { fontSize: "12px" } },
|
|
254
|
-
"Held until you submit the review."
|
|
255
|
-
);
|
|
256
|
-
modeBtns.append(b1, b2, hint);
|
|
257
|
-
const submit = async () => {
|
|
271
|
+
const held = kind() === "explainer" ? "Add to my notes" : "Add to the review";
|
|
272
|
+
const submit = async (mode) => {
|
|
258
273
|
const body = ta.value.trim();
|
|
259
274
|
if (!body) return;
|
|
260
|
-
await api.createThread(state.id, {
|
|
275
|
+
const th = await api.createThread(state.id, {
|
|
261
276
|
kind: mode === "ask" ? "question" : "comment",
|
|
262
277
|
mode,
|
|
263
278
|
target: quote ? { ...target, quote } : target,
|
|
@@ -265,9 +280,10 @@
|
|
|
265
280
|
});
|
|
266
281
|
closePopover();
|
|
267
282
|
await reload();
|
|
283
|
+
focusThread(th.id);
|
|
268
284
|
};
|
|
269
285
|
ta.addEventListener("keydown", (e) => {
|
|
270
|
-
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") void submit();
|
|
286
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") void submit("ask");
|
|
271
287
|
});
|
|
272
288
|
const el = h(
|
|
273
289
|
"div",
|
|
@@ -279,12 +295,28 @@
|
|
|
279
295
|
describeTarget(target)
|
|
280
296
|
),
|
|
281
297
|
ta,
|
|
282
|
-
modeBtns,
|
|
283
298
|
h(
|
|
284
299
|
"div",
|
|
285
|
-
{ class: "row"
|
|
286
|
-
h(
|
|
287
|
-
|
|
300
|
+
{ class: "row" },
|
|
301
|
+
h(
|
|
302
|
+
"button",
|
|
303
|
+
{ class: "small ok", onclick: () => void submit("ask") },
|
|
304
|
+
"Send to the agent ",
|
|
305
|
+
h("kbd", null, "\u2318\u21B5")
|
|
306
|
+
),
|
|
307
|
+
h("button", { class: "small", onclick: () => void submit("review") }, held),
|
|
308
|
+
h("span", { style: { flex: "1" } }),
|
|
309
|
+
h("button", { class: "small ghost", onclick: () => closePopover() }, "Cancel")
|
|
310
|
+
),
|
|
311
|
+
h(
|
|
312
|
+
"div",
|
|
313
|
+
{ class: "muted", style: { fontSize: "12px", marginTop: "6px" } },
|
|
314
|
+
h("div", null, `Send to the agent \u2014 ${listeningLine()}`),
|
|
315
|
+
h(
|
|
316
|
+
"div",
|
|
317
|
+
null,
|
|
318
|
+
`${held} \u2014 held until you ${kind() === "explainer" ? "send the document back or finish" : "submit the review"}.`
|
|
319
|
+
)
|
|
288
320
|
)
|
|
289
321
|
);
|
|
290
322
|
setTimeout(() => ta.focus());
|
|
@@ -381,6 +413,14 @@
|
|
|
381
413
|
const body = h("div", { class: "side-body" });
|
|
382
414
|
const draw = () => {
|
|
383
415
|
body.innerHTML = "";
|
|
416
|
+
if (!readOnly())
|
|
417
|
+
body.appendChild(
|
|
418
|
+
h(
|
|
419
|
+
"div",
|
|
420
|
+
{ class: `receipt ${agent().attached ? "listening" : "queued"}` },
|
|
421
|
+
listeningLine()
|
|
422
|
+
)
|
|
423
|
+
);
|
|
384
424
|
if (pending.length && !readOnly()) {
|
|
385
425
|
body.appendChild(
|
|
386
426
|
h(
|
|
@@ -414,6 +454,7 @@
|
|
|
414
454
|
setTimeout(() => body.querySelector(".thread.active")?.scrollIntoView({ block: "center" }));
|
|
415
455
|
}
|
|
416
456
|
function threadCard(t) {
|
|
457
|
+
const delivery = deliveryOf(t, agent());
|
|
417
458
|
const ta = h("textarea", { placeholder: "Reply\u2026", rows: 2 });
|
|
418
459
|
const send = async () => {
|
|
419
460
|
const v = ta.value.trim();
|
|
@@ -439,6 +480,7 @@
|
|
|
439
480
|
h("span", { class: "target", onclick: () => goToTarget(t) }, describeTarget(t.target)),
|
|
440
481
|
h("span", { class: `badge ${t.status === "resolved" ? "ok" : ""}` }, t.status)
|
|
441
482
|
),
|
|
483
|
+
h("div", { class: `receipt ${delivery}` }, RECEIPT[delivery]),
|
|
442
484
|
quote ? h("div", { class: "quote" }, `\u201C${quote.length > 160 ? quote.slice(0, 160) + "\u2026" : quote}\u201D`) : null,
|
|
443
485
|
t.messages.map(
|
|
444
486
|
(m2) => h(
|
|
@@ -471,12 +513,16 @@
|
|
|
471
513
|
"button",
|
|
472
514
|
{
|
|
473
515
|
class: "small",
|
|
516
|
+
// An unanswered question is not resolved, it is abandoned.
|
|
517
|
+
// Saying so is what stops a reader closing their own
|
|
518
|
+
// question and then waiting for an answer to it.
|
|
519
|
+
title: delivery === "answered" ? "Nothing more is owed here" : "Stop waiting for an answer; no agent will pick this up",
|
|
474
520
|
onclick: async () => {
|
|
475
521
|
await api.resolve(state.id, t.id);
|
|
476
522
|
await reload();
|
|
477
523
|
}
|
|
478
524
|
},
|
|
479
|
-
"Resolve"
|
|
525
|
+
delivery === "answered" || delivery === "held" ? "Resolve" : "Withdraw"
|
|
480
526
|
) : h(
|
|
481
527
|
"button",
|
|
482
528
|
{
|
|
@@ -2871,6 +2917,17 @@
|
|
|
2871
2917
|
banner.append("Closed without approval. This review is complete.");
|
|
2872
2918
|
}
|
|
2873
2919
|
}
|
|
2920
|
+
function pollPresence() {
|
|
2921
|
+
setInterval(async () => {
|
|
2922
|
+
const cur = state.data;
|
|
2923
|
+
if (!cur) return;
|
|
2924
|
+
const agent2 = await api.presence(state.id).catch(() => cur.agent);
|
|
2925
|
+
if (agent2.attached !== cur.agent.attached) {
|
|
2926
|
+
state.data = { ...cur, agent: agent2 };
|
|
2927
|
+
emit("threads");
|
|
2928
|
+
}
|
|
2929
|
+
}, 5e3);
|
|
2930
|
+
}
|
|
2874
2931
|
function connectEvents() {
|
|
2875
2932
|
const es = new EventSource(`/api/reviews/${state.id}/events`);
|
|
2876
2933
|
es.onmessage = async (ev) => {
|
|
@@ -2941,6 +2998,7 @@
|
|
|
2941
2998
|
window.matchMedia(NARROW).addEventListener("change", () => emit("view"));
|
|
2942
2999
|
emit("data");
|
|
2943
3000
|
connectEvents();
|
|
3001
|
+
pollPresence();
|
|
2944
3002
|
}
|
|
2945
3003
|
var m = /^\/review\/([^/]+)/.exec(location.pathname);
|
|
2946
3004
|
if (m) void reviewPage(m[1]);
|