samagotchi 0.124.0 → 0.126.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +53 -1
- data/docs/architecture.md +4 -2
- data/docs/cli.md +20 -8
- data/docs/guardrails.md +22 -0
- data/docs/internals/participants.md +96 -3
- data/docs/internals/remote-sessions.md +2 -0
- data/docs/internals/web-push.md +10 -6
- data/docs/sessions.md +5 -2
- data/lib/samagotchi/bridge.rb +4 -3
- data/lib/samagotchi/bridge_client.rb +8 -1
- data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +25 -4
- data/lib/samagotchi/bundles/known-names/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +3 -0
- data/lib/samagotchi/children_status.rb +6 -1
- data/lib/samagotchi/engine.rb +43 -7
- data/lib/samagotchi/guardrail_wiring.rb +10 -1
- data/lib/samagotchi/llm/chat_loop.rb +1 -0
- data/lib/samagotchi/process_group.rb +10 -1
- data/lib/samagotchi/reminder_carry.rb +92 -0
- data/lib/samagotchi/reminder_store.rb +49 -0
- data/lib/samagotchi/session.rb +40 -3
- data/lib/samagotchi/session_inbox.rb +14 -8
- data/lib/samagotchi/session_manager.rb +39 -8
- data/lib/samagotchi/session_participants.rb +10 -2
- data/lib/samagotchi/sessions_command.rb +1 -1
- data/lib/samagotchi/terminal_ui/attached_loop/composer.rb +225 -0
- data/lib/samagotchi/terminal_ui/attached_loop/exit_commands.rb +138 -0
- data/lib/samagotchi/terminal_ui/attached_loop/questions.rb +225 -0
- data/lib/samagotchi/terminal_ui/attached_loop/queueing.rb +154 -0
- data/lib/samagotchi/terminal_ui/attached_loop/sending.rb +302 -0
- data/lib/samagotchi/terminal_ui/attached_loop.rb +16 -1253
- data/lib/samagotchi/terminal_ui/exit_lines.rb +141 -0
- data/lib/samagotchi/terminal_ui/join_replay.rb +235 -0
- data/lib/samagotchi/verdict.rb +20 -0
- data/lib/samagotchi/verify_anchor.rb +28 -0
- data/lib/samagotchi/verify_fresh.rb +275 -0
- data/lib/samagotchi/verify_fresh_origin.rb +71 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +10 -3
- data/lib/samagotchi/web/participant_access.rb +15 -4
- data/lib/samagotchi/web/participant_review.rb +7 -1
- data/lib/samagotchi/web/participant_view.rb +46 -5
- data/lib/samagotchi/web/participants_admin.rb +15 -4
- data/lib/samagotchi/web/public/annotate_bar.css +8 -4
- data/lib/samagotchi/web/public/annotate_bar.js +43 -7
- data/lib/samagotchi/web/public/app.js +134 -8
- data/lib/samagotchi/web/public/claim_link.js +347 -0
- data/lib/samagotchi/web/public/data.js +2 -1
- data/lib/samagotchi/web/public/index.html +1 -0
- data/lib/samagotchi/web/public/notify.js +13 -2
- data/lib/samagotchi/web/public/participant.html +3 -1
- data/lib/samagotchi/web/public/participant.js +113 -9
- data/lib/samagotchi/web/public/participants_panel.js +15 -0
- data/lib/samagotchi/web/public/route.js +3 -2
- data/lib/samagotchi/web/public/send_turn.js +6 -1
- data/lib/samagotchi/web/public/session_state.js +5 -0
- data/lib/samagotchi/web/public/stream_handlers.js +3 -0
- data/lib/samagotchi/web/public/tokens.css +2 -1
- data/lib/samagotchi/web/public/verdict_card.js +65 -0
- data/lib/samagotchi/web/public/verdict_fresh.js +120 -0
- data/lib/samagotchi/web/public/verify_card.css +17 -0
- data/lib/samagotchi/web/public/verify_preset.js +18 -0
- data/lib/samagotchi/web/push/attention.rb +25 -5
- data/lib/samagotchi/web/push/notifier.rb +4 -3
- data/lib/samagotchi/web/session_control.rb +2 -0
- data/lib/samagotchi/web/session_summary.rb +25 -1
- data/lib/samagotchi/web/verify_fresh_routes.rb +110 -0
- data/lib/samagotchi/worker.rb +125 -12
- data/lib/samagotchi/worker_idle_exit.rb +7 -6
- data/lib/samagotchi/worker_inbound.rb +4 -4
- metadata +19 -1
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
//
|
|
10
10
|
// A bundle's pills (chi.annotation) come after the presets: each prepares
|
|
11
11
|
// its command line with the selection (annotate_actions.js), at most 3
|
|
12
|
-
// inline, the rest behind "⋯".
|
|
12
|
+
// inline, the rest behind "⋯". Last, "Copy link" where the page has a
|
|
13
|
+
// link for the selection (a claim link, claim_link.js).
|
|
13
14
|
//
|
|
14
15
|
// On a touch screen the bar docks above the composer instead: the OS's own
|
|
15
16
|
// selection menu (Copy, Look Up…) and the handles sit next to the selection,
|
|
@@ -21,7 +22,8 @@ import { annotateBarLeft, annotationSource, quoteBlock } from "./annotations.js"
|
|
|
21
22
|
import { presetNote } from "./annotate_presets.js";
|
|
22
23
|
import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline } from "./annotate_actions.js";
|
|
23
24
|
|
|
24
|
-
// @param presets the
|
|
25
|
+
// @param presets the presets: each a text (annotate_presets.js
|
|
26
|
+
// parsePresets) or {label, text} (verify_preset.js)
|
|
25
27
|
// @param canAnnotate () => whether the open session takes annotations (one
|
|
26
28
|
// is open and the composer can send)
|
|
27
29
|
// @param roots () => the elements a selection may start in (the history,
|
|
@@ -45,12 +47,17 @@ import { actionEnabled, actionTitle, sameActions, selectionFields, splitInline }
|
|
|
45
47
|
// passes its own, participant_annotate.js)
|
|
46
48
|
// @param quote (text, source) => the quote block for the selected +text+
|
|
47
49
|
// (annotations.js quoteBlock by default), or null for no text
|
|
50
|
+
// @param link (source, range) => the selection's link, {url, title}, or
|
|
51
|
+
// null for no "Copy link" (the default); +range+ the selection clipped
|
|
52
|
+
// to +source+'s root
|
|
53
|
+
// @param onLink (link) runs on "Copy link"; the selection is cleared and
|
|
54
|
+
// the bar hidden first
|
|
48
55
|
// @param doc, win the page's document and window
|
|
49
56
|
// @return {schedule(), hide(), el}; schedule() re-reads the selection on the
|
|
50
57
|
// next frame (call it on scroll)
|
|
51
58
|
export function createAnnotateBar({
|
|
52
|
-
presets, canAnnotate, roots, isLive, onQuote, actions = () => [], running = () => false, onAction = () => {},
|
|
53
|
-
dock = () => null, source: sourceOf = annotationSource, quote = quoteBlock,
|
|
59
|
+
presets, canAnnotate, roots, isLive, onQuote, onPreset = () => {}, actions = () => [], running = () => false, onAction = () => {},
|
|
60
|
+
dock = () => null, source: sourceOf = annotationSource, quote = quoteBlock, link = () => null, onLink = () => {},
|
|
54
61
|
doc = globalThis.document, win = globalThis.window,
|
|
55
62
|
}) {
|
|
56
63
|
const bar = doc.createElement("div");
|
|
@@ -66,8 +73,9 @@ export function createAnnotateBar({
|
|
|
66
73
|
return btn;
|
|
67
74
|
}
|
|
68
75
|
bar.appendChild(button("Annotate", "annotate-main"));
|
|
69
|
-
presets.forEach((
|
|
70
|
-
const
|
|
76
|
+
presets.forEach((preset, i) => {
|
|
77
|
+
const { label, text } = presetParts(preset);
|
|
78
|
+
const btn = button(label, "annotate-preset");
|
|
71
79
|
btn.dataset.index = String(i);
|
|
72
80
|
btn.title = text;
|
|
73
81
|
bar.appendChild(btn);
|
|
@@ -78,10 +86,16 @@ export function createAnnotateBar({
|
|
|
78
86
|
group.className = "annotate-actions";
|
|
79
87
|
group.hidden = true;
|
|
80
88
|
bar.appendChild(group);
|
|
89
|
+
const linkBtn = button("Copy link", "annotate-link");
|
|
90
|
+
linkBtn.hidden = true;
|
|
91
|
+
bar.appendChild(linkBtn);
|
|
81
92
|
doc.body.appendChild(bar);
|
|
82
93
|
const touch = win.matchMedia?.("(pointer: coarse)");
|
|
83
94
|
let pendingQuote = null;
|
|
84
95
|
let pendingFields = null;
|
|
96
|
+
let pendingSource = null;
|
|
97
|
+
let pendingRange = null;
|
|
98
|
+
let pendingLink = null;
|
|
85
99
|
let drawn = [];
|
|
86
100
|
let moreList = null;
|
|
87
101
|
|
|
@@ -129,6 +143,9 @@ export function createAnnotateBar({
|
|
|
129
143
|
function hide() {
|
|
130
144
|
pendingQuote = null;
|
|
131
145
|
pendingFields = null;
|
|
146
|
+
pendingSource = null;
|
|
147
|
+
pendingRange = null;
|
|
148
|
+
pendingLink = null;
|
|
132
149
|
if (moreList) moreList.hidden = true;
|
|
133
150
|
bar.hidden = true;
|
|
134
151
|
}
|
|
@@ -151,6 +168,11 @@ export function createAnnotateBar({
|
|
|
151
168
|
if (!block) return hide();
|
|
152
169
|
pendingQuote = block;
|
|
153
170
|
pendingFields = selectionFields(raw, source);
|
|
171
|
+
pendingSource = source;
|
|
172
|
+
pendingRange = clipped;
|
|
173
|
+
pendingLink = link(source, clipped) || null;
|
|
174
|
+
linkBtn.hidden = !pendingLink;
|
|
175
|
+
linkBtn.title = pendingLink?.title || "";
|
|
154
176
|
const list = actions() || [];
|
|
155
177
|
if (!sameActions(list, drawn)) drawActions(list);
|
|
156
178
|
markActions();
|
|
@@ -209,6 +231,13 @@ export function createAnnotateBar({
|
|
|
209
231
|
moreList.hidden = !moreList.hidden;
|
|
210
232
|
return;
|
|
211
233
|
}
|
|
234
|
+
if (btn === linkBtn) {
|
|
235
|
+
const target = pendingLink;
|
|
236
|
+
win.getSelection()?.removeAllRanges();
|
|
237
|
+
hide();
|
|
238
|
+
if (target) onLink(target);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
212
241
|
if (btn.dataset.action !== undefined) {
|
|
213
242
|
const action = drawn[Number(btn.dataset.action)];
|
|
214
243
|
if (!action || btn.disabled) return;
|
|
@@ -219,11 +248,18 @@ export function createAnnotateBar({
|
|
|
219
248
|
return;
|
|
220
249
|
}
|
|
221
250
|
const preset = btn.dataset.index === undefined ? null : presets[Number(btn.dataset.index)];
|
|
222
|
-
const text = preset ? presetNote(pendingQuote, preset) : pendingQuote;
|
|
251
|
+
const text = preset ? presetNote(pendingQuote, presetParts(preset).text) : pendingQuote;
|
|
252
|
+
const fields = { ...pendingFields, source: pendingSource, range: pendingRange };
|
|
223
253
|
win.getSelection()?.removeAllRanges();
|
|
224
254
|
hide();
|
|
255
|
+
if (preset) onPreset(preset, fields);
|
|
225
256
|
onQuote(text);
|
|
226
257
|
});
|
|
227
258
|
|
|
228
259
|
return { schedule, hide, el: bar };
|
|
229
260
|
}
|
|
261
|
+
|
|
262
|
+
// A preset's button label and note: a text is both.
|
|
263
|
+
function presetParts(preset) {
|
|
264
|
+
return typeof preset === "string" ? { label: preset, text: preset } : { label: preset.label, text: preset.text };
|
|
265
|
+
}
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
restartWorkers,
|
|
31
31
|
restartWeb,
|
|
32
32
|
onAuthLost,
|
|
33
|
+
api,
|
|
33
34
|
} from "./data.js";
|
|
34
35
|
import { authProbe, createAuthLost } from "./auth_lost.js";
|
|
35
36
|
import { REPLACE_SESSION_TEXT, fragmentCode, tradeFragmentCode } from "./token_fragment.js";
|
|
@@ -58,9 +59,12 @@ import { createTaskStopper } from "./task_stop.js";
|
|
|
58
59
|
import { createSystemOneWrongAction } from "./system_one_wrong.js";
|
|
59
60
|
import { appendQuote } from "./annotations.js";
|
|
60
61
|
import { applyComposerText, applyRowTemplate, fetchApply } from "./review_apply.js";
|
|
62
|
+
import { nameVerdictCards } from "./verdict_card.js";
|
|
63
|
+
import { freshURL, placeVerdictWithFresh, settleRunning } from "./verdict_fresh.js";
|
|
61
64
|
import { authorBadge, nameUnnamedBadges } from "./participant_names.js";
|
|
62
65
|
import { parsePresets } from "./annotate_presets.js";
|
|
63
66
|
import { createAnnotateBar } from "./annotate_bar.js";
|
|
67
|
+
import { VERIFY_PRESET, withVerify } from "./verify_preset.js";
|
|
64
68
|
import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
|
|
65
69
|
import { copyText, installCopy } from "./copy.js";
|
|
66
70
|
import { createQuestionCards } from "./question_cards.js";
|
|
@@ -70,6 +74,9 @@ import { mountComposer } from "./composer_mount.js";
|
|
|
70
74
|
import { readStored, writeStored } from "./storage.js";
|
|
71
75
|
import { createCommandList } from "./command_list.js";
|
|
72
76
|
import { ALL_SESSIONS_HASH, sessionHash, sessionIdFromHash } from "./route.js";
|
|
77
|
+
import {
|
|
78
|
+
claimFromHash, ownerClaimHash, participantClaimLink, selectionClaim, showClaim, turnIdOf,
|
|
79
|
+
} from "./claim_link.js";
|
|
73
80
|
import { scopeDir, scopeTemplate } from "./scope.js";
|
|
74
81
|
import { html, render } from "./vendor/lit-html.js";
|
|
75
82
|
import { shouldFollowScroll } from "./scroll.js";
|
|
@@ -175,7 +182,13 @@ const participantsControl = createParticipantsControl({
|
|
|
175
182
|
startReview: (opts) => sessionActions.reviewSelected(opts),
|
|
176
183
|
reviewing: () => sessionActions.reviewing.has(selected),
|
|
177
184
|
// An older prompt drawn before the list came says "a participant": named now.
|
|
178
|
-
onNames: () =>
|
|
185
|
+
onNames: () => {
|
|
186
|
+
nameUnnamedBadges(document);
|
|
187
|
+
nameVerdictCards(historyEl, (author) => {
|
|
188
|
+
const badge = authorBadge(author);
|
|
189
|
+
return badge && !badge.unnamed ? badge.name : null;
|
|
190
|
+
});
|
|
191
|
+
},
|
|
179
192
|
onChange: () => updateInfoBar(),
|
|
180
193
|
toast: (text, action, options) => showToast(text, action, options),
|
|
181
194
|
});
|
|
@@ -220,6 +233,7 @@ const infoCancelBtn = $("#infoCancelBtn");
|
|
|
220
233
|
const composeHintEl = $("#composeHint");
|
|
221
234
|
|
|
222
235
|
let selected = null;
|
|
236
|
+
let pendingVerifyAnchor = null;
|
|
223
237
|
// The session the composer's text was last written in ("" the start page,
|
|
224
238
|
// undefined unknown): the composer is one for every session, and continue →
|
|
225
239
|
// keeps only a draft of its own (fillOpener).
|
|
@@ -719,9 +733,61 @@ function setSessionHash(id) {
|
|
|
719
733
|
history.replaceState(null, "", location.pathname + location.search + hash);
|
|
720
734
|
}
|
|
721
735
|
// A #/s/<id> typed or pasted into the address bar, or reached by back.
|
|
736
|
+
// A claim link (claim_link.js; #/s/<id>?t=…&q=…, or the participant's
|
|
737
|
+
// #s=<id>&t=… opened in this browser) opens its session and, once drawn,
|
|
738
|
+
// shows the claim.
|
|
722
739
|
function selectFromHash() {
|
|
723
|
-
const
|
|
740
|
+
const claim = claimFromHash(location.hash);
|
|
741
|
+
const id = sessionIdFromHash(location.hash) || claim?.sessionId || null;
|
|
742
|
+
if (claim) pendingClaim = claim;
|
|
724
743
|
if (id && id !== selected) select(id);
|
|
744
|
+
else if (claim) showPendingClaim();
|
|
745
|
+
}
|
|
746
|
+
let pendingClaim = null;
|
|
747
|
+
// Where a claim's text can be on this page: answers, own messages, steps.
|
|
748
|
+
const CLAIM_ROOTS = ".bubble.output, .bubble.user .user-message, .gen-text";
|
|
749
|
+
// The claim link's quote scrolled to and marked, its text read as the
|
|
750
|
+
// participant page shows it (the session's folder relative, home ~); a
|
|
751
|
+
// quote not found marks its turn; neither found: the top, and a toast.
|
|
752
|
+
// The hash is the session's again (select() already set it when it
|
|
753
|
+
// switched), so the same link opened again is a hashchange.
|
|
754
|
+
function showPendingClaim() {
|
|
755
|
+
const claim = pendingClaim;
|
|
756
|
+
if (!claim || claim.sessionId !== selected) return;
|
|
757
|
+
pendingClaim = null;
|
|
758
|
+
if (!allViewOpen() && location.hash !== sessionHash(selected)) {
|
|
759
|
+
history.replaceState(null, "", location.pathname + location.search + sessionHash(selected));
|
|
760
|
+
}
|
|
761
|
+
if (showClaim(historyEl, claim, { roots: CLAIM_ROOTS, opts: claimOpts() })) return;
|
|
762
|
+
historyEl.scrollTop = 0;
|
|
763
|
+
showToast("The linked turn isn't in this session.");
|
|
764
|
+
}
|
|
765
|
+
// How the participant page shows this page's text (claim_link.js).
|
|
766
|
+
function claimOpts() {
|
|
767
|
+
return { home: document.body.dataset.home || "", cwd: current.dir || "" };
|
|
768
|
+
}
|
|
769
|
+
// Annotate's "Copy link" for a selection in a turn's answer, step or own
|
|
770
|
+
// message: the participant page's link when the session has a person
|
|
771
|
+
// active (it opens in the browser their review link was opened in: one
|
|
772
|
+
// link per browser, the cookie), else this page's own.
|
|
773
|
+
function claimLinkFor(source, range) {
|
|
774
|
+
if (!selected || !["answer", "step", "user"].includes(source.kind) || !historyEl.contains(source.root)) return null;
|
|
775
|
+
const turnId = turnIdOf(historyEl, source.root);
|
|
776
|
+
if (!turnId) return null;
|
|
777
|
+
const claim = { turnId, ...selectionClaim(source.root, range, claimOpts()) };
|
|
778
|
+
if (!claim.q) return null;
|
|
779
|
+
const target = participantsControl.claimTarget();
|
|
780
|
+
if (!target) {
|
|
781
|
+
return { url: `${location.origin}${location.pathname}${location.search}${ownerClaimHash(selected, claim)}`,
|
|
782
|
+
title: "A link to this on your page (no person is invited to this session)" };
|
|
783
|
+
}
|
|
784
|
+
const whose = target.names.map((name) => `${name}'s`).join(" or ");
|
|
785
|
+
return { url: participantClaimLink(target.base, selected, claim),
|
|
786
|
+
title: `A link to this for ${whose} browser: it opens where their review link was opened (one link per browser). Opened here, it shows your page.` };
|
|
787
|
+
}
|
|
788
|
+
async function copyClaimLink({ url }) {
|
|
789
|
+
if (await copyText(url)) showToast("Link copied");
|
|
790
|
+
else showToast(`Couldn't copy: ${url}`, null, { sticky: true });
|
|
725
791
|
}
|
|
726
792
|
function applyView() {
|
|
727
793
|
const open = allViewOpen();
|
|
@@ -914,13 +980,16 @@ function clearNotice() {
|
|
|
914
980
|
}
|
|
915
981
|
promptEl.addEventListener("input", clearNotice);
|
|
916
982
|
promptEl.addEventListener("input", () => noteDraft());
|
|
917
|
-
// An emptied composer lets go of its Apply turn.
|
|
983
|
+
// An emptied composer lets go of its Apply turn and its Verify claim.
|
|
918
984
|
promptEl.addEventListener("input", () => {
|
|
919
|
-
if (
|
|
985
|
+
if (promptEl.value.trim()) return;
|
|
986
|
+
deliveryControl.release();
|
|
987
|
+
pendingVerifyAnchor = null;
|
|
920
988
|
});
|
|
921
989
|
|
|
922
990
|
async function select(id) {
|
|
923
991
|
clearNotice();
|
|
992
|
+
if (selected !== id) pendingVerifyAnchor = null;
|
|
924
993
|
selected = id;
|
|
925
994
|
setSessionHash(id);
|
|
926
995
|
lists.paintActive();
|
|
@@ -1002,6 +1071,7 @@ async function resync({ endText = null } = {}) {
|
|
|
1002
1071
|
const kept = keptCommandBubbles(historyEl, id);
|
|
1003
1072
|
resetTurnView();
|
|
1004
1073
|
renderSessionView(data, { kept });
|
|
1074
|
+
showPendingClaim();
|
|
1005
1075
|
// Preview is read-only: a worker is woken only by POST /turn, so attach
|
|
1006
1076
|
// only when a live bridge exists.
|
|
1007
1077
|
current.streamSeq = typeof data.last_event_seq === "number" ? (data.last_event_id || data.last_event_seq) : null;
|
|
@@ -1116,6 +1186,7 @@ function renderHistory(items) {
|
|
|
1116
1186
|
}
|
|
1117
1187
|
if (typeof items[0] === "object" && items[0].role) {
|
|
1118
1188
|
historyEl.replaceChildren(turnHistoryNodes(items, current.timing, { thumbs: (images) => thumbsTemplate(selected, images), workingSet: !!workingSet }));
|
|
1189
|
+
renderSavedVerdicts();
|
|
1119
1190
|
// The live dividers count from the history's last activity.
|
|
1120
1191
|
gapClock.seed(lastActivity(turnGroups(items, current.timing), items));
|
|
1121
1192
|
linkTextRefs();
|
|
@@ -1257,6 +1328,44 @@ function placeApply(data) {
|
|
|
1257
1328
|
appendToHistory(elementOf(applyRowTemplate(data.turn_id, badge)));
|
|
1258
1329
|
}
|
|
1259
1330
|
|
|
1331
|
+
function placeVerdict(record) {
|
|
1332
|
+
if (!record) return;
|
|
1333
|
+
const known = (current.verdicts || []).some((item) => item.check_turn_id === record.check_turn_id);
|
|
1334
|
+
if (!known) current.verdicts = [...(current.verdicts || []), record];
|
|
1335
|
+
// A fresh verdict ends its check's "verifying fresh" (the card shows it).
|
|
1336
|
+
if (record.fresh) current.verifyFresh = settleRunning(current.verifyFresh, record);
|
|
1337
|
+
const badge = record.by && authorBadge(record.by);
|
|
1338
|
+
placeVerdictWithFresh(historyEl, record, {
|
|
1339
|
+
who: badge ? badge.name : "You",
|
|
1340
|
+
author: record.by || null,
|
|
1341
|
+
}, verifyFreshState());
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// The owner's Verify fresh on an unclear verdict's card (verdict_fresh.js):
|
|
1345
|
+
// always theirs to start; its running check links to the child's session.
|
|
1346
|
+
function verifyFreshState() {
|
|
1347
|
+
const id = selected;
|
|
1348
|
+
if (!Array.isArray(current.verifyFresh)) current.verifyFresh = [];
|
|
1349
|
+
return {
|
|
1350
|
+
verdicts: current.verdicts || [],
|
|
1351
|
+
running: current.verifyFresh,
|
|
1352
|
+
canStart: !!id,
|
|
1353
|
+
start: async (of) => {
|
|
1354
|
+
try {
|
|
1355
|
+
const body = await api(freshURL(id, of), { method: "POST", body: "{}" });
|
|
1356
|
+
return { ok: true, child_id: body.child_id || null };
|
|
1357
|
+
} catch (error) {
|
|
1358
|
+
return { ok: false, error: error.message };
|
|
1359
|
+
}
|
|
1360
|
+
},
|
|
1361
|
+
childHref: (childId) => sessionHash(childId),
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
function renderSavedVerdicts() {
|
|
1366
|
+
for (const record of current.verdicts || []) placeVerdict(record);
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1260
1369
|
// Apply: the owner's Apply turn into the composer, after what is typed
|
|
1261
1370
|
// there; never sent from here.
|
|
1262
1371
|
historyEl.addEventListener("click", async (e) => {
|
|
@@ -1494,6 +1603,7 @@ const streamHandlers = createStreamHandlers({
|
|
|
1494
1603
|
workingSet,
|
|
1495
1604
|
participantsControl,
|
|
1496
1605
|
placeApply,
|
|
1606
|
+
placeVerdict,
|
|
1497
1607
|
sentIds,
|
|
1498
1608
|
earlyRestores,
|
|
1499
1609
|
stream: {
|
|
@@ -1599,6 +1709,8 @@ const sendTurnControl = createSendTurn({
|
|
|
1599
1709
|
sentIds,
|
|
1600
1710
|
earlyRestores,
|
|
1601
1711
|
api: { createIdleSession, uploadImage, sendTurn },
|
|
1712
|
+
verify: () => pendingVerifyAnchor,
|
|
1713
|
+
clearVerify: (anchor) => { if (pendingVerifyAnchor === anchor) pendingVerifyAnchor = null; },
|
|
1602
1714
|
dir: scope.dir,
|
|
1603
1715
|
startPage,
|
|
1604
1716
|
takeDelivery: () => deliveryControl.take(),
|
|
@@ -1821,9 +1933,11 @@ const updateSheet = createUpdateSheet({
|
|
|
1821
1933
|
function showToast(text, action = null, options = {}) { toasts.show(text, action, options); }
|
|
1822
1934
|
|
|
1823
1935
|
// Annotate (annotate_bar.js): a selection in the conversation offers
|
|
1824
|
-
// "Annotate" and the presets
|
|
1825
|
-
// session's plugins' pills
|
|
1826
|
-
//
|
|
1936
|
+
// "Annotate" and the presets (then the built-in Verify: verify_preset.js),
|
|
1937
|
+
// which append a quote to the composer, the session's plugins' pills
|
|
1938
|
+
// (chi.annotation, annotate_actions.js), which fill the composer with their
|
|
1939
|
+
// command line and the quote, or send it at once, and Copy link in a turn
|
|
1940
|
+
// (claimLinkFor).
|
|
1827
1941
|
function fillComposer(text) {
|
|
1828
1942
|
openForVisit();
|
|
1829
1943
|
promptEl.value = text;
|
|
@@ -1835,11 +1949,14 @@ function fillComposer(text) {
|
|
|
1835
1949
|
}
|
|
1836
1950
|
|
|
1837
1951
|
const annotateBar = createAnnotateBar({
|
|
1838
|
-
presets: parsePresets(document.body.dataset.annotatePresets),
|
|
1952
|
+
presets: withVerify(parsePresets(document.body.dataset.annotatePresets)),
|
|
1839
1953
|
canAnnotate: () => !!selected && current.owner !== "tui",
|
|
1840
1954
|
roots: turnRoots,
|
|
1841
1955
|
isLive: (root) => liveTurn.running && turnDom.containsLive(root),
|
|
1842
1956
|
onQuote: (text) => fillComposer(appendQuote(promptEl.value, text)),
|
|
1957
|
+
onPreset: (preset, fields) => {
|
|
1958
|
+
pendingVerifyAnchor = preset === VERIFY_PRESET ? verifyAnchorFor(fields) : null;
|
|
1959
|
+
},
|
|
1843
1960
|
actions: () => annotationActions(current.commands),
|
|
1844
1961
|
running: () => liveTurn.running,
|
|
1845
1962
|
onAction: (action, fields) => {
|
|
@@ -1847,8 +1964,17 @@ const annotateBar = createAnnotateBar({
|
|
|
1847
1964
|
if (action.send) runSessionCommand(line);
|
|
1848
1965
|
else fillComposer(composerWithAction(promptEl.value, action, line));
|
|
1849
1966
|
},
|
|
1967
|
+
link: claimLinkFor,
|
|
1968
|
+
onLink: copyClaimLink,
|
|
1850
1969
|
dock: () => $("#dock"),
|
|
1851
1970
|
});
|
|
1971
|
+
|
|
1972
|
+
function verifyAnchorFor({ source, range }) {
|
|
1973
|
+
if (!source || !["answer", "step", "user"].includes(source.kind) || !historyEl.contains(source.root)) return null;
|
|
1974
|
+
const turnId = turnIdOf(historyEl, source.root);
|
|
1975
|
+
const quote = selectionClaim(source.root, range, claimOpts()).q;
|
|
1976
|
+
return turnId && quote ? { turn_id: turnId, quote } : null;
|
|
1977
|
+
}
|
|
1852
1978
|
historyEl.addEventListener("scroll", annotateBar.schedule);
|
|
1853
1979
|
// A command bubble's Apply (CacheConfirm): sends the change's --yes line
|
|
1854
1980
|
// once; its own bubble then says what it did.
|