samagotchi 0.170.0 → 0.171.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 +29 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
// liveTurn, turnDom, questionCards, shownTexts, objectives, featureGates, markdownTip
|
|
17
17
|
// the page's views
|
|
18
18
|
// workingSet the stage view's working set (web.working_set), null when off
|
|
19
|
+
// wsPreview its reader and peek (ws_reader.js), null without them: reset with the session
|
|
19
20
|
// gapClock the page's last activity (time_gaps.js createGapClock)
|
|
20
21
|
// bubbles, cards bubbles.js, cards_place.js
|
|
21
22
|
// markdown markdown_place.js (linkTextRefs, applyFinalMarkdown)
|
|
22
23
|
// capabilities() the page's capabilities (card.js shownElsewhere, the history's cards)
|
|
24
|
+
// can(name) the viewer's (viewer.js): the history's Apply row needs apply
|
|
23
25
|
// page: {setReviseAnchor(anchor), renderInitRow(), setMarkdownWarning(warning),
|
|
24
|
-
// setCapabilities(capabilities, editorUrl), findInTurnRoots(selector), appendToHistory(el),
|
|
26
|
+
// setCapabilities(capabilities, editorUrl, viewer), findInTurnRoots(selector), appendToHistory(el),
|
|
25
27
|
// setTurnRunning(running), mergeTurnTiming(id, timing), applySessionPayload(data, opts),
|
|
26
28
|
// turnEndStatus(data), replaceSelectedCard(change)} the page's hooks
|
|
27
29
|
// later: {handlers(), composer(), ownerVerify(), commandList(), initTasks(), resetTurnView()}
|
|
@@ -35,7 +37,7 @@ import { seedInitTasks } from "./init_row.js";
|
|
|
35
37
|
import { restoreCommandBubbles } from "./commands.js";
|
|
36
38
|
import { failedTurnText, normalize } from "./format.js";
|
|
37
39
|
import { splitSnapshotCards } from "./card.js";
|
|
38
|
-
import { turnGroups } from "./timing.js";
|
|
40
|
+
import { turnGroups, normalizeTiming } from "./timing.js";
|
|
39
41
|
import { groupItems } from "./working_set_view.js";
|
|
40
42
|
import { thumbsTemplate } from "./images.js";
|
|
41
43
|
import { hintEl, outputBubbleTemplate } from "./bubbles.js";
|
|
@@ -47,8 +49,8 @@ import { stillOn } from "./latest.js";
|
|
|
47
49
|
export function createSessionView(deps) {
|
|
48
50
|
const {
|
|
49
51
|
store, historyEl, getSession,
|
|
50
|
-
liveTurn, turnDom, questionCards, shownTexts, objectives, featureGates, markdownTip, workingSet = null, gapClock,
|
|
51
|
-
bubbles, cards, markdown, capabilities, page, later,
|
|
52
|
+
liveTurn, turnDom, questionCards, shownTexts, objectives, featureGates, markdownTip, workingSet = null, wsPreview = null, gapClock,
|
|
53
|
+
bubbles, cards, markdown, capabilities, can = () => true, page, later,
|
|
52
54
|
} = deps;
|
|
53
55
|
const { makeUserBubble, addEndBubble, showGuardrailWarning, showRecap } = bubbles;
|
|
54
56
|
const { placeSnapshotCards, upsertCard } = cards;
|
|
@@ -84,6 +86,9 @@ export function createSessionView(deps) {
|
|
|
84
86
|
markdownTip.show(null);
|
|
85
87
|
setCapabilities(null, null);
|
|
86
88
|
later.composer().newVisit();
|
|
89
|
+
// The reader and the peek are the session's: another one (or none, the
|
|
90
|
+
// start page) never finds them over #center, the composer inert under it.
|
|
91
|
+
if (!keepVerify) wsPreview?.reset();
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
// A session view: its messages, then the turn in progress and the queued
|
|
@@ -92,7 +97,7 @@ export function createSessionView(deps) {
|
|
|
92
97
|
function renderSessionView(data, { kept = [] } = {}) {
|
|
93
98
|
setMarkdownWarning(data.markdown_warning);
|
|
94
99
|
markdownTip.show(data.markdown_tip);
|
|
95
|
-
setCapabilities(data.capabilities, data.editor_url);
|
|
100
|
+
setCapabilities(data.capabilities, data.editor_url, data.viewer);
|
|
96
101
|
// The saved fill (when there is one) goes in here too, so a reloaded or
|
|
97
102
|
// stopped session shows it before its next turn (a running turn's replay
|
|
98
103
|
// below streams over it).
|
|
@@ -195,7 +200,12 @@ export function createSessionView(deps) {
|
|
|
195
200
|
// (its prompt has no drawing yet: a plain row, no miss).
|
|
196
201
|
const historyOpts = (data = {}) => ({ thumbs: (images) => thumbsTemplate(store.selected, images), workingSet: !!workingSet,
|
|
197
202
|
drawings: data.turn_drawings || null, cards: data.cards || null, capabilities: capabilities(),
|
|
198
|
-
runningTurnId: data.current_turn?.turn_id || null });
|
|
203
|
+
runningTurnId: data.current_turn?.turn_id || null, apply: can("apply") });
|
|
204
|
+
|
|
205
|
+
// A read drawn the way a reload draws it: the turn blocks only, nothing
|
|
206
|
+
// placed after them (the opt-in reload check, reload_check.js, compares
|
|
207
|
+
// this with what the page has, offscreen).
|
|
208
|
+
const reloadRender = (data = {}) => turnHistoryNodes(data.messages || [], normalizeTiming(data.timing), historyOpts(data));
|
|
199
209
|
|
|
200
210
|
function renderHistory(items, data = {}) {
|
|
201
211
|
gapClock.seed(null);
|
|
@@ -275,5 +285,5 @@ export function createSessionView(deps) {
|
|
|
275
285
|
if (questionCards.pendingQuestion()) questionCards.resolveQuestion(questionCards.pendingQuestion().id, { cancelled: true, reason: "turn ended" });
|
|
276
286
|
}
|
|
277
287
|
|
|
278
|
-
return { forgetSession, renderSessionView, historyOpts, readTurnEnd, endTurnView };
|
|
288
|
+
return { forgetSession, renderSessionView, historyOpts, reloadRender, readTurnEnd, endTurnView };
|
|
279
289
|
}
|
|
@@ -43,10 +43,12 @@ export function createStageAsking({
|
|
|
43
43
|
// A ref's "show in turn" opened a collapsed stage (not kept).
|
|
44
44
|
let revealed = false;
|
|
45
45
|
// The docked card's fold is +waiting+'s (fold, trackFold): the waiting
|
|
46
|
-
// card the user folded, by its key
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
46
|
+
// card the user folded, by its key — kept for this tab (stage_waiting.js,
|
|
47
|
+
// chi_asking_fold:<session>), so a reload or a resync mid-turn folds that
|
|
48
|
+
// card again. For that card only: the next one arrives open, and when the
|
|
49
|
+
// docked card goes the stage is as the choice and +revealed+ say. One more
|
|
50
|
+
// card waiting since the fold (an approval, a relayed or hook question)
|
|
51
|
+
// drops it, so the stage opens on it.
|
|
50
52
|
|
|
51
53
|
// ── the docked card ──────────────────────────────────────────────────────
|
|
52
54
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// The stage's look while a participant's view of the running turn waits for
|
|
2
|
+
// the owner (the participant main page, scene 3): the status row says
|
|
3
|
+
// "waiting for the owner" and one line under the live slots names what
|
|
4
|
+
// kind of thing they are asked (an approval, a question, a continue
|
|
5
|
+
// offer), never its content; no card. chi web sends the kind alone
|
|
6
|
+
// (ParticipantView.waiting_kind: question_requested / continue_offered's
|
|
7
|
+
// `kind`, the show's current_turn and viewer.waiting_for_owner).
|
|
8
|
+
// stage_view.js draws it at each refresh from its +ownerWait+ hook; the
|
|
9
|
+
// owner's page passes none (its cards are the waiting look).
|
|
10
|
+
|
|
11
|
+
export const OWNER_WAIT_STATUS = "waiting for the owner";
|
|
12
|
+
|
|
13
|
+
// The line per kind; a kind the page doesn't know reads as a question.
|
|
14
|
+
export const OWNER_WAIT_TEXT = Object.freeze({
|
|
15
|
+
approval: "chi wants to run a command that needs the owner's approval. The turn continues when they answer.",
|
|
16
|
+
question: "chi asked the owner a question. The turn continues when they answer.",
|
|
17
|
+
continue: "chi asks the owner whether to go on. The turn continues when they answer.",
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const ownerWaitText = (kind) => OWNER_WAIT_TEXT[kind] || OWNER_WAIT_TEXT.question;
|
|
21
|
+
|
|
22
|
+
// +stage+ the stage's root, +line+ the line's node (stage_view.js makes it
|
|
23
|
+
// after the live slots), +phaseEl+ the status row's phase.
|
|
24
|
+
// @return draw(kind): kind null (not waiting) puts the stage back
|
|
25
|
+
export function createOwnerWait({ stage, line, phaseEl }) {
|
|
26
|
+
line.hidden = true;
|
|
27
|
+
return function draw(kind) {
|
|
28
|
+
stage.classList.toggle("owner-wait", !!kind);
|
|
29
|
+
line.hidden = !kind;
|
|
30
|
+
if (!kind) return;
|
|
31
|
+
phaseEl.textContent = OWNER_WAIT_STATUS;
|
|
32
|
+
const text = ownerWaitText(kind);
|
|
33
|
+
if (line.textContent !== text) line.textContent = text;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -39,6 +39,7 @@ import { ASKS, PENDING } from "./stage_cards.js";
|
|
|
39
39
|
import { createStageAsking } from "./stage_asking.js";
|
|
40
40
|
import { cardIn, createStageWaiting, isApprovalCard, isBlocking, waitingKey, waitingKind } from "./stage_waiting.js";
|
|
41
41
|
import { stampRows } from "./working_set_view.js";
|
|
42
|
+
import { createOwnerWait } from "./stage_owner_wait.js";
|
|
42
43
|
|
|
43
44
|
// The turn-end read (markdown, answer_display) settles within this, or the
|
|
44
45
|
// hand-off stops waiting for it.
|
|
@@ -71,10 +72,14 @@ const OPENING = "[data-turn-id], [data-opening-id]";
|
|
|
71
72
|
// The quiet signal's inputs (stage_stall.js): +clock+() the page's time,
|
|
72
73
|
// +streamLive+() the session's stream is open, +timingNote+() what the turn
|
|
73
74
|
// waits on (live_timing.js note), +serverGen+() a joiner's generation.
|
|
75
|
+
// +canCancel+() whether this turn's Stop turn shows (a participant's own
|
|
76
|
+
// turn only); +ownerWait+() the kind a participant's view waits for the
|
|
77
|
+
// owner on, or null (stage_owner_wait.js).
|
|
74
78
|
export function createStageView({
|
|
75
79
|
historyEl, isNearBottom: historyNearBottom, sawText, sessionId, dockEl, centerEl, taskStop = null,
|
|
76
80
|
renderMarkdown, linkRefs, workingSet = null, onCancel = null, onHandedOff = null,
|
|
77
81
|
clock = () => Date.now(), streamLive = () => true, timingNote = () => "", serverGen = () => null,
|
|
82
|
+
canCancel = () => true, ownerWait = () => null,
|
|
78
83
|
}) {
|
|
79
84
|
// The dock becomes a clear column: the stage's card, then the composer's
|
|
80
85
|
// (its rows moved into .dock-card, which wears the glass and the focus ring).
|
|
@@ -135,6 +140,8 @@ export function createStageView({
|
|
|
135
140
|
headline.tabIndex = 0;
|
|
136
141
|
headline.setAttribute("role", "button");
|
|
137
142
|
headline.title = "Show this step's reasoning";
|
|
143
|
+
// A participant's turn waiting for the owner: one line under the headline, no card.
|
|
144
|
+
const drawOwnerWait = createOwnerWait({ stage, line: node("div", "ts-owner-wait", live), phaseEl });
|
|
138
145
|
const now = node("div", "ts-now", live);
|
|
139
146
|
const trail = node("div", "ts-trail", live);
|
|
140
147
|
// The working set's rail: out of the live slots' flow on their right (a
|
|
@@ -259,8 +266,9 @@ export function createStageView({
|
|
|
259
266
|
});
|
|
260
267
|
|
|
261
268
|
// The cards that wait in the stage, as data (stage_waiting.js): the
|
|
262
|
-
// docked card, its fold, what blocks, the approval's cap read them.
|
|
263
|
-
|
|
269
|
+
// docked card, its fold, what blocks, the approval's cap read them. The
|
|
270
|
+
// fold of the asking card is this tab's own, kept per session.
|
|
271
|
+
const waiting = createStageWaiting({ sessionId });
|
|
264
272
|
let dockedAtLastRefresh = [];
|
|
265
273
|
// The asking form and the stage's fold (stage_asking.js).
|
|
266
274
|
const ask = createStageAsking({
|
|
@@ -415,7 +423,7 @@ export function createStageView({
|
|
|
415
423
|
if (!blocked && wasWaiting) stage.style.setProperty("--live-held", `${live.offsetHeight}px`);
|
|
416
424
|
stage.classList.toggle("plain", plain);
|
|
417
425
|
stage.classList.toggle("ended", !!endKind);
|
|
418
|
-
cancel.hidden = !onCancel || !!endKind;
|
|
426
|
+
cancel.hidden = !onCancel || !canCancel() || !!endKind;
|
|
419
427
|
stage.classList.toggle("waiting", blocked);
|
|
420
428
|
if (!blocked && wasWaiting) settleAnsweredCard();
|
|
421
429
|
if (answeredWithCardLeft && !blocked) releaseHeldWait();
|
|
@@ -427,6 +435,7 @@ export function createStageView({
|
|
|
427
435
|
const asksFront = !!asking && !blocked && !endKind && waiting.get(waiting.front())?.kind === "asks";
|
|
428
436
|
const streaming = asksFront ? liveSlots(turn).phase : "";
|
|
429
437
|
phaseEl.textContent = phaseLabel({ phase: slots.phase, asksFront, streaming });
|
|
438
|
+
drawOwnerWait(endKind ? null : ownerWait());
|
|
430
439
|
stage.classList.toggle("asks-live", asksFront);
|
|
431
440
|
drawAsksText(asksFront && gen?.text && !gen.answer ? String(gen.text).trim() : "");
|
|
432
441
|
|
|
@@ -820,6 +829,9 @@ export function createStageView({
|
|
|
820
829
|
// history is as tall as it gets, room to keep the reader's rows in
|
|
821
830
|
// place): each answered card's clamp says it now, as the turn reloads.
|
|
822
831
|
opener.end();
|
|
832
|
+
// The asking card's fold is this turn's: with the turn handed off, there
|
|
833
|
+
// is nothing for a reload to bring back.
|
|
834
|
+
waiting.clearFold();
|
|
823
835
|
clearStage();
|
|
824
836
|
if (fold.wasAtEnd) easeScrollTo(historyEl, () => historyEl.scrollHeight - historyEl.clientHeight, { reduce: !fold.animate || reducedMotion() });
|
|
825
837
|
// The turn's pieces are in the history now (the opt-in reload check,
|
|
@@ -1100,7 +1112,7 @@ export function createStageView({
|
|
|
1100
1112
|
stage.classList.add("bare");
|
|
1101
1113
|
stage.dataset.phase = "running";
|
|
1102
1114
|
phaseEl.textContent = "running";
|
|
1103
|
-
cancel.hidden = !onCancel;
|
|
1115
|
+
cancel.hidden = !onCancel || !canCancel();
|
|
1104
1116
|
show();
|
|
1105
1117
|
},
|
|
1106
1118
|
// The page's clock ticks (app.js, the live timing's 200 ms / 1 s): the
|
|
@@ -1110,6 +1122,8 @@ export function createStageView({
|
|
|
1110
1122
|
if (holding && asksCard() && liveSlots(view.current().turn).tool?.startedAt != null) scheduleRefresh();
|
|
1111
1123
|
},
|
|
1112
1124
|
callClockActive: () => holding && asksCard() && !!liveSlots(view.current().turn).tool,
|
|
1125
|
+
// An input of the hooks changed (a participant's ownerWait, canCancel).
|
|
1126
|
+
redraw: () => scheduleRefresh(),
|
|
1113
1127
|
handOff,
|
|
1114
1128
|
// A card stopped waiting (stage_waiting.js keys; +only+: when it is that
|
|
1115
1129
|
// kind): question_cards.js's resolve path, cards_place.js upsertCard.
|
|
@@ -25,8 +25,12 @@
|
|
|
25
25
|
// a reload agree whatever order the replay brings them in. One docked card
|
|
26
26
|
// shows at a time (+shown+: the one the pager is on, else the first). The
|
|
27
27
|
// fold (the docked card's own, stage_asking.js) is the folded card's key and
|
|
28
|
-
// how many cards waited when it was folded.
|
|
28
|
+
// how many cards waited when it was folded. That fold is this tab's own: it
|
|
29
|
+
// is kept in sessionStorage (one entry per session) so a reload or a resync
|
|
30
|
+
// mid-turn draws the card as the user left it, and it goes when the card is
|
|
31
|
+
// answered or cancelled, when the fold drops, and when the turn hands off.
|
|
29
32
|
import { ASKS, PENDING, WARN_CARD } from "./stage_cards.js";
|
|
33
|
+
import { readSession, removeSession, writeSession } from "./storage.js";
|
|
30
34
|
|
|
31
35
|
// The kinds: what blocks the turn (BLOCKING), a running turn's card with
|
|
32
36
|
// actions (check-in: "asks"), a plugin's warning ("warn": stays in sight,
|
|
@@ -46,6 +50,20 @@ export const CONTINUE_KEY = "continue";
|
|
|
46
50
|
export const questionKey = (qid) => (qid == null ? null : `q:${qid}`);
|
|
47
51
|
export const pluginCardKey = (id) => (id == null || id === "" ? null : `c:${id}`);
|
|
48
52
|
|
|
53
|
+
// The asking card's fold, kept for this tab (docs/internals/web-frontend.md
|
|
54
|
+
// Storage keys): one entry per session, JSON {key, with}.
|
|
55
|
+
export const FOLD_PREFIX = "chi_asking_fold:";
|
|
56
|
+
export const foldKey = (sessionId) => (sessionId ? `${FOLD_PREFIX}${sessionId}` : null);
|
|
57
|
+
|
|
58
|
+
// The fold's storage: the guarded sessionStorage (storage.js); the specs
|
|
59
|
+
// inject their own, so one test's fold is not another's (they all use session
|
|
60
|
+
// s1 and the same question ids).
|
|
61
|
+
const tabStorage = {
|
|
62
|
+
read: (key) => readSession(key),
|
|
63
|
+
write: (key, value) => writeSession(key, value),
|
|
64
|
+
remove: (key) => removeSession(key),
|
|
65
|
+
};
|
|
66
|
+
|
|
49
67
|
// +el+'s key, null: not a card the stage keeps.
|
|
50
68
|
export function waitingKey(el) {
|
|
51
69
|
if (!el?.matches) return null;
|
|
@@ -79,13 +97,19 @@ export function cardIn(root, key) {
|
|
|
79
97
|
// +kind+'s rank in the stage's order: blocking cards before the rest.
|
|
80
98
|
const rank = (kind) => (BLOCKING.has(kind) ? 0 : 1);
|
|
81
99
|
|
|
82
|
-
|
|
100
|
+
// +sessionId+: the open session (a getter, as app.js and the harnesses pass
|
|
101
|
+
// it); +storage+: the fold's read/write/remove, guarded (the specs inject
|
|
102
|
+
// their own).
|
|
103
|
+
export function createStageWaiting({ sessionId = null, storage = tabStorage } = {}) {
|
|
83
104
|
const cards = new Map();
|
|
84
105
|
let seq = 0;
|
|
85
106
|
let folded = null;
|
|
86
107
|
let foldedWith = 0;
|
|
87
108
|
// The docked card the pager is on (null: the first).
|
|
88
109
|
let paged = null;
|
|
110
|
+
// The key this session's fold is stored under (null: no session, nothing
|
|
111
|
+
// to keep).
|
|
112
|
+
const at = () => foldKey(typeof sessionId === "function" ? sessionId() : sessionId);
|
|
89
113
|
|
|
90
114
|
const entries = () => [...cards.entries()].sort((a, b) => rank(a[1].kind) - rank(b[1].kind) || a[1].order - b[1].order);
|
|
91
115
|
const dockedKeys = () => entries().filter(([, e]) => e.docked).map(([key]) => key);
|
|
@@ -98,6 +122,28 @@ export function createStageWaiting() {
|
|
|
98
122
|
return n;
|
|
99
123
|
}
|
|
100
124
|
|
|
125
|
+
// The fold this tab kept for the open session: {key, with}, or null (no
|
|
126
|
+
// fold, nothing stored, or storage that can't be read).
|
|
127
|
+
function storedFold() {
|
|
128
|
+
const key = at();
|
|
129
|
+
if (!key) return null;
|
|
130
|
+
try {
|
|
131
|
+
const value = JSON.parse(storage.read(key) || "null");
|
|
132
|
+
return typeof value?.key === "string" ? { key: value.key, with: Number(value.with) || 0 } : null;
|
|
133
|
+
} catch (_) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// The stored fold says what the fold is: written with the count it was
|
|
139
|
+
// made at, removed when there is no fold.
|
|
140
|
+
function keepFold() {
|
|
141
|
+
const key = at();
|
|
142
|
+
if (!key) return;
|
|
143
|
+
if (folded) storage.write(key, JSON.stringify({ key: folded, with: foldedWith }));
|
|
144
|
+
else storage.remove(key);
|
|
145
|
+
}
|
|
146
|
+
|
|
101
147
|
return {
|
|
102
148
|
// +key+ came to the stage (again: its kind and flags change, its place in
|
|
103
149
|
// the order stays).
|
|
@@ -117,7 +163,9 @@ export function createStageWaiting() {
|
|
|
117
163
|
cards.delete(key);
|
|
118
164
|
return true;
|
|
119
165
|
},
|
|
120
|
-
// The stage let its turn go: nothing waits, nothing is folded
|
|
166
|
+
// The stage let its turn go: nothing waits, nothing is folded — the
|
|
167
|
+
// stored fold stays: a resync replays the same turn, and the fold is the
|
|
168
|
+
// user's.
|
|
121
169
|
reset() {
|
|
122
170
|
cards.clear();
|
|
123
171
|
seq = 0;
|
|
@@ -161,20 +209,40 @@ export function createStageWaiting() {
|
|
|
161
209
|
// Every entry in the stage's order (the tests' and the harness's read).
|
|
162
210
|
list: () => entries().map(([key, e]) => ({ key, ...e })),
|
|
163
211
|
// The asking form's fold: +key+ folded by hand (null: unfolded), with
|
|
164
|
-
// the blocking cards that wait now.
|
|
212
|
+
// the blocking cards that wait now. Kept for this tab, so the same card
|
|
213
|
+
// is folded again after a reload or a resync.
|
|
165
214
|
fold(key) {
|
|
166
215
|
folded = key ?? null;
|
|
167
216
|
foldedWith = count();
|
|
217
|
+
keepFold();
|
|
168
218
|
},
|
|
169
219
|
// The fold as it stands with +asking+ (the asking form's card key) now:
|
|
170
220
|
// a card answered meanwhile lowers the mark, one more waiting than at
|
|
171
|
-
// the fold drops it, and so does another form. The
|
|
221
|
+
// the fold drops it, and so does another form. The replay drew the card
|
|
222
|
+
// the tab folded: with nothing folded in memory, that fold comes back.
|
|
223
|
+
// The folded key, or null.
|
|
172
224
|
trackFold(asking) {
|
|
173
225
|
const now = count();
|
|
226
|
+
// Nothing folded in memory and the replay drew the card this tab
|
|
227
|
+
// folded (a reload, or a resync mid-turn): take that fold back.
|
|
228
|
+
if (!folded) {
|
|
229
|
+
const stored = storedFold();
|
|
230
|
+
if (stored?.key === asking) { folded = stored.key; foldedWith = stored.with; }
|
|
231
|
+
}
|
|
174
232
|
foldedWith = Math.min(foldedWith, now);
|
|
175
|
-
if (folded && (folded !== asking || now > foldedWith))
|
|
233
|
+
if (folded && (folded !== asking || now > foldedWith)) {
|
|
234
|
+
folded = null;
|
|
235
|
+
// Live unfolded it: a reload must not fold it again.
|
|
236
|
+
keepFold();
|
|
237
|
+
}
|
|
176
238
|
return folded;
|
|
177
239
|
},
|
|
240
|
+
// The folded card is done for good (answered, cancelled, the turn handed
|
|
241
|
+
// off): there is no fold to bring back.
|
|
242
|
+
clearFold() {
|
|
243
|
+
const key = at();
|
|
244
|
+
if (key) storage.remove(key);
|
|
245
|
+
},
|
|
178
246
|
folded: () => folded,
|
|
179
247
|
};
|
|
180
248
|
}
|
|
@@ -4,6 +4,28 @@
|
|
|
4
4
|
// globalThis.localStorage at each call, never at import (the specs install
|
|
5
5
|
// their own).
|
|
6
6
|
|
|
7
|
+
// The same guards on sessionStorage: this tab only, so a reload or a resync
|
|
8
|
+
// of it keeps what is written, and another tab or device never sees it.
|
|
9
|
+
// Reads globalThis.sessionStorage at each call (the specs install their own).
|
|
10
|
+
|
|
11
|
+
// The stored string, or +fallback+ when there is none or it can't be read.
|
|
12
|
+
export function readSession(key, fallback = null) {
|
|
13
|
+
try {
|
|
14
|
+
const value = globalThis.sessionStorage?.getItem(key);
|
|
15
|
+
return value == null ? fallback : value;
|
|
16
|
+
} catch (_) {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function writeSession(key, value) {
|
|
22
|
+
try { globalThis.sessionStorage?.setItem(key, value); } catch (_) {}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function removeSession(key) {
|
|
26
|
+
try { globalThis.sessionStorage?.removeItem(key); } catch (_) {}
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
// The stored string, or +fallback+ when there is none or it can't be read.
|
|
8
30
|
export function readStored(key, fallback = null) {
|
|
9
31
|
try {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
// capabilities() the page's capabilities (card.js shownElsewhere)
|
|
20
20
|
// historyEl the history element (keepFollowing, the failed-turn redraw)
|
|
21
21
|
// liveTurn, turnDom, shownTexts, questionCards, contextChips the page's views
|
|
22
|
+
// (contextChips null for a viewer without them, viewer.js)
|
|
22
23
|
// workingSet the stage view's working set (web.working_set),
|
|
23
24
|
// null when off
|
|
24
25
|
// participantsControl the owner's participants panel (participants_panel.js),
|
|
@@ -370,7 +371,7 @@ export function createStreamHandlers(deps) {
|
|
|
370
371
|
gapBefore(at);
|
|
371
372
|
addNoteBubble({ label: data.label, content: data.text || "", at, objectives: data.objectives });
|
|
372
373
|
// An attached context source's note: its chip changed.
|
|
373
|
-
if (data.context_source) contextChips
|
|
374
|
+
if (data.context_source) contextChips?.refresh();
|
|
374
375
|
},
|
|
375
376
|
// The session's list changed (objectives_strip.js): an edit waiting, a
|
|
376
377
|
// list note in, a turn's moves.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* The web UI's colour tokens,
|
|
1
|
+
/* The web UI's colour tokens, read by the page (index.html, every viewer's): the dark :root (the default) and the light set under prefers-color-scheme: light. Everything else reads them through var(). */
|
|
2
2
|
:root{color-scheme:dark;--bg:#0b0d11;--bg-top:#131d29;--bg-panel:#151821;--bg-inset:#0f1115;--bg-raised:#22262f;--bg-hover:#1b1f27;--border:#1f232b;--border-strong:#2d333f;--text:#e6e8eb;--text-bright:#f0f3f6;--text-2:#c9d1d9;--text-3:#9aa4b2;--text-muted:#8b949e;--text-faint:#6e7681;--accent:#5aa9ff;--accent-strong:#0071e3;--accent-bg:#0f2340;--accent-ring:rgba(90,169,255,.18);--primary:#0071e3;--ok:#3fb950;--ok-text:#7ee787;--ok-bg:#1a2e1a;--warn:#d29922;--warn-text:#e3b341;--warn-bright:#f2cc60;--warn-bg:#2b2108;--warn-border:#6b5416;--danger:#f85149;--danger-bg:#da3633;--danger-text:#ff7b72;--danger-bg-dim:#3d1f1f;--think:#8957e5;--think-text:#d2a8ff;--accent-border:#1a3458;--pick-border:#79c0ff;--accent-tint:color-mix(in srgb,var(--accent) 7%,transparent);--narr:color-mix(in srgb,var(--accent) 38%,var(--text-muted));
|
|
3
3
|
--glass-bg:color-mix(in srgb,var(--bg-panel) 70%,transparent);--glass-border:rgba(255,255,255,.08);--glass-highlight:rgba(255,255,255,.05);--glass-shadow:0 8px 30px rgba(0,0,0,.35);
|
|
4
4
|
--diff-add-bg:rgba(63,185,80,.14);--diff-add-text:#aff5b4;--diff-del-bg:rgba(248,81,73,.14);--diff-del-text:#ffc1bc;
|
|
@@ -978,8 +978,9 @@ function pairStepTexts(els, group, items) {
|
|
|
978
978
|
// turn's footprint on its summary line.
|
|
979
979
|
// +cards+ the snapshot's cards (a drawn card's body), +capabilities+ the
|
|
980
980
|
// page's (a notice shown elsewhere).
|
|
981
|
+
// +apply+: false for a viewer without the owner's Apply (viewer.js): no Apply row.
|
|
981
982
|
export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, now = Date.now(), drawings = null, cards = null, capabilities = {},
|
|
982
|
-
runningTurnId = null }) {
|
|
983
|
+
runningTurnId = null, apply = true }) {
|
|
983
984
|
const timingTemplate = (group) => {
|
|
984
985
|
const record = group.record;
|
|
985
986
|
// Numbered by its place in the turn records, as live (a failed turn
|
|
@@ -1026,7 +1027,7 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
|
|
|
1026
1027
|
blockTurns.push(ordinal);
|
|
1027
1028
|
}
|
|
1028
1029
|
const lineKey = (i) => (items[i]?.source === "delegate_report" ? `${group.turnId}:input:${i - (group.user ?? i)}` : null);
|
|
1029
|
-
const reviewer = group.answer !== null && prompt?.turn_id && prompt.participant_mode !== PAIR ? authorBadge(prompt.author, prompt.author_name) : null;
|
|
1030
|
+
const reviewer = apply && group.answer !== null && prompt?.turn_id && prompt.participant_mode !== PAIR ? authorBadge(prompt.author, prompt.author_name) : null;
|
|
1030
1031
|
const pieces = turnPieces({
|
|
1031
1032
|
prompt: prompt ? (prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt)) : null,
|
|
1032
1033
|
block: drawn.block,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// stay thin and the logic runs under `node --test`.
|
|
3
3
|
|
|
4
4
|
import { authorBadge } from "./participant_names.js";
|
|
5
|
+
import { isViewerClient } from "./viewer_self.js";
|
|
5
6
|
|
|
6
7
|
// Read turn_summary.output; Bridge::ClientEvent omits result for clients.
|
|
7
8
|
export function turnOutput(data) {
|
|
@@ -107,8 +108,10 @@ function writeClientId(storage, id) {
|
|
|
107
108
|
}
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
// This tab's own (+myId+), or the participant viewer's (viewer_self.js:
|
|
112
|
+
// chi web gives their turns one client id, whichever tab sent them).
|
|
110
113
|
export function isOwn(clientId, myId) {
|
|
111
|
-
return clientId != null && clientId === myId;
|
|
114
|
+
return clientId != null && (clientId === myId || isViewerClient(clientId));
|
|
112
115
|
}
|
|
113
116
|
|
|
114
117
|
// As the TUI labels them (Formatting::CLIENT_LABELS); this file's label
|
|
@@ -302,6 +305,9 @@ export function promptOps(event, { myId, known, unmatchedMerge = false, wakeStar
|
|
|
302
305
|
switch (event.type) {
|
|
303
306
|
case "turn_enqueued": {
|
|
304
307
|
const id = event.enqueued_id;
|
|
308
|
+
// Someone else's queued turn as a participant gets it (bare: chi web
|
|
309
|
+
// sends no prompt or id of it): nothing to draw.
|
|
310
|
+
if (!id && !event.prompt) return [];
|
|
305
311
|
// Queued for after the running turn (delivery queue): its bubble says so.
|
|
306
312
|
const delivery = event.delivery ? { delivery: event.delivery } : {};
|
|
307
313
|
if (isOwn(event.client_id, myId)) return [withImages({ op: "tag", enqueuedId: id, prompt: event.prompt, ...delivery }, event.images)];
|
|
@@ -312,10 +318,12 @@ export function promptOps(event, { myId, known, unmatchedMerge = false, wakeStar
|
|
|
312
318
|
if (event.continue || !event.prompt) return [];
|
|
313
319
|
const origin = event.origin || {};
|
|
314
320
|
const id = origin.enqueued_id || null;
|
|
315
|
-
|
|
321
|
+
// A participant's own turn (+mine+: chi web names no origin to them)
|
|
322
|
+
// starts their queued bubble, matched by its text.
|
|
323
|
+
if ((id && known(id)) || isOwn(origin.client_id, myId) || event.mine) {
|
|
316
324
|
// Images too: a resync can have wiped the own echo, and the page then
|
|
317
325
|
// draws the bubble from this.
|
|
318
|
-
return [withImages({ op: "start", enqueuedId: id, prompt: event.prompt }, event.images)];
|
|
326
|
+
return [withImages({ op: "start", enqueuedId: id, prompt: event.prompt, ...(event.mine ? { mine: true } : {}) }, event.images)];
|
|
319
327
|
}
|
|
320
328
|
return [withImages({ op: "add", enqueuedId: id, prompt: event.prompt, state: "started", label: clientLabel(origin.client_id), ...authorOf(event.author || origin.author, event.author_name, event.participant_mode) }, event.images)];
|
|
321
329
|
}
|
|
@@ -349,8 +357,9 @@ export function replaysDrawnTurn(event, turnRecords = []) {
|
|
|
349
357
|
return turnRecords.some((record) => record.id === event.turn_id);
|
|
350
358
|
}
|
|
351
359
|
|
|
352
|
-
// +object+ with `images` when there are any
|
|
353
|
-
// for a turn
|
|
360
|
+
// +object+ with `images` when there are any, refs or a count (another
|
|
361
|
+
// participant's, ParticipantView). The shapes stay as they were for a turn
|
|
362
|
+
// without.
|
|
354
363
|
export function withImages(object, images) {
|
|
355
|
-
return images?.length ? { ...object, images } : object;
|
|
364
|
+
return images?.length || images > 0 ? { ...object, images } : object;
|
|
356
365
|
}
|
|
@@ -13,10 +13,12 @@ import { activityDiffTemplate } from "./diff_view.js";
|
|
|
13
13
|
import { clip, fullTime, steerSender } from "./format.js";
|
|
14
14
|
import { rowTitleRef } from "./refs.js";
|
|
15
15
|
import { readLinesTemplate } from "./read_view.js";
|
|
16
|
+
import { wayInTemplate } from "./ws_way_in.js";
|
|
16
17
|
import { handedOverId } from "./task_stop.js";
|
|
17
18
|
import { STOPPED_LINE_CLASS, cancelLineText, formatDuration } from "./timing.js";
|
|
18
19
|
import { emptyAnswerLine } from "./turn_events.js";
|
|
19
|
-
import { authorBadgeText, authorBadgeTitle } from "./participant_names.js";
|
|
20
|
+
import { authorBadge, authorBadgeText, authorBadgeTitle } from "./participant_names.js";
|
|
21
|
+
import { ownerLabel, viewerSelf } from "./viewer_self.js";
|
|
20
22
|
|
|
21
23
|
// A row's status (live, or a saved tool record's on reload): running,
|
|
22
24
|
// error, stopped (a task_wait the user's Stop ended), blocked (a guardrail
|
|
@@ -183,7 +185,7 @@ export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothi
|
|
|
183
185
|
? html`<div class=${tailMode(row.tool, full) ? "activity-output tail" : "activity-output"} title=${full}>${inlineOutput(row.tool, full)}</div>`
|
|
184
186
|
: nothing;
|
|
185
187
|
const status = statusParts(row.status, { noMatch: !!row.no_match });
|
|
186
|
-
return html`${statusMarkTemplate(status)}<span class="activity-tool">${toolName(row)}</span>${params}${stop}${alias}${statusWordTemplate(status)}${auto}${wrong}${duration ? html`<span class="activity-duration">${duration}</span>` : nothing}${commandBlockTemplate(row.view, row.title)}${llmEditTemplate(row)}${output}${full ? readLinesTemplate(row) : nothing}${row.images?.length ? thumbs(row.images) : nothing}${row.diff ? activityDiffTemplate(row.diff) : nothing}`;
|
|
188
|
+
return html`${statusMarkTemplate(status)}<span class="activity-tool">${toolName(row)}</span>${params}${stop}${alias}${statusWordTemplate(status)}${row.status === "ok" ? wayInTemplate(row) : nothing}${auto}${wrong}${duration ? html`<span class="activity-duration">${duration}</span>` : nothing}${commandBlockTemplate(row.view, row.title)}${llmEditTemplate(row)}${output}${full ? readLinesTemplate(row) : nothing}${row.images?.length ? thumbs(row.images) : nothing}${row.diff ? activityDiffTemplate(row.diff) : nothing}`;
|
|
187
189
|
}
|
|
188
190
|
|
|
189
191
|
// A prompt bubble's states and their badges: queued (waiting for its
|
|
@@ -320,7 +322,12 @@ export function steerRowTemplate({ source, text } = {}, { bubble = false } = {})
|
|
|
320
322
|
// +message.source+ (a delegate report's "delegate_report") is its
|
|
321
323
|
// data-source; +lineKey+, a merged line's fold key (answer_fold.js:
|
|
322
324
|
// data-line-key, the reload's "<turn id>:input:<place>").
|
|
325
|
+
// For a participant looking (viewer_self.js) their own echo carries their
|
|
326
|
+
// badge, and a prompt with neither badge nor label is the owner's ("owner").
|
|
323
327
|
export function userBubbleTemplate(message, { label = null, state = null, step = null, thumbs = nothing, own = false, enqueuedId = null, key = null, delivery = null, author = null, lineKey = null } = {}) {
|
|
328
|
+
const self = viewerSelf();
|
|
329
|
+
if (own && !author && self) author = authorBadge(self.author, self.name, self.mode);
|
|
330
|
+
if (!author && !label) label = ownerLabel();
|
|
324
331
|
const named = BADGES[state] ? state : null;
|
|
325
332
|
const from = author
|
|
326
333
|
? html`<span class="origin-label author-badge" title=${authorBadgeTitle(author.name, author.mode)} data-unnamed=${author.unnamed ? "1" : nothing}>${authorBadgeText(author.name, author.mode)}</span>`
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
// its own: static JS is cached at unversioned URLs, and a new export on an
|
|
10
10
|
// old module would blank the page.
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
// none (its rule passes null).
|
|
12
|
+
// A context note ends a turn.
|
|
14
13
|
export const NOTE_BOUNDARY = ".bubble.note";
|
|
15
14
|
|
|
16
15
|
// The elements of the turn +opening+ starts (the [data-turn-id] element, or
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Where the Verify preset is offered (verify_preset.js)
|
|
1
|
+
// Where the Verify preset is offered (verify_preset.js): only
|
|
2
2
|
// on text in an ended turn's final answer, the one place the server takes a
|
|
3
3
|
// claim from. TurnStages::VerifyAnchors#verified records a Verify turn's card only when
|
|
4
4
|
// its quote is in the last answer of the source turn (AnswerTail.answer?:
|
|
@@ -25,8 +25,6 @@ const PRECEDING = 2;
|
|
|
25
25
|
// a turn shows before its answer); a context note ends a turn's elements
|
|
26
26
|
// (a wake turn starts with one, and its bubble carries no turn id).
|
|
27
27
|
export const OWNER_FINAL = Object.freeze({ answers: ".bubble.output:not(.report-reply)", boundary: NOTE_BOUNDARY });
|
|
28
|
-
// The participant page: a reply's bubble.
|
|
29
|
-
export const PARTICIPANT_FINAL = Object.freeze({ answers: ".p-msg.reply > .p-bubble", boundary: null });
|
|
30
28
|
|
|
31
29
|
// The turn opening +node+ is in (the last [data-turn-id] at or before it in
|
|
32
30
|
// +container+) and whether it is the newest one: {anchor, last}, or null.
|
|
@@ -55,7 +53,7 @@ function finalAnswer(container, anchor, { answers, boundary }) {
|
|
|
55
53
|
// The turn id Verify anchors to for a selection in +root+ (the annotate
|
|
56
54
|
// source's root) in +container+, or null where Verify isn't offered: not in
|
|
57
55
|
// a turn, not in its final answer, or in the running turn. +rule+:
|
|
58
|
-
// OWNER_FINAL
|
|
56
|
+
// OWNER_FINAL. +running+: null when no turn runs, else
|
|
59
57
|
// {turnId} (the running turn's id; null when unknown, then the newest turn
|
|
60
58
|
// counts as the running one) — a running turn's answer isn't final yet.
|
|
61
59
|
export function verifyTurnOf(container, root, rule, { running = null } = {}) {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The viewer: who looks at this page and what they may do on it (chi web's
|
|
2
|
+
// Web::Viewer). chi web stamps it on <body> (data-viewer-kind, and
|
|
3
|
+
// data-viewer-can: the capability names, space-separated), read once here at
|
|
4
|
+
// boot; a session's show carries it as `viewer` too. The page draws a
|
|
5
|
+
// control, and makes a call, only when viewer.can(name) says so: a control
|
|
6
|
+
// the viewer may not use is not created and not wired (the null-control
|
|
7
|
+
// idiom), and its static element stays hidden (control(): hidden and
|
|
8
|
+
// data-withheld, which index.html's CSS never shows). No 403 is a way
|
|
9
|
+
// of gating. See docs/internals/web-frontend.md (The viewer).
|
|
10
|
+
//
|
|
11
|
+
// Not app.js's refEditor() (where this page can open a ref: the editor) nor
|
|
12
|
+
// the show's capabilities (what the page renders): those are per request.
|
|
13
|
+
//
|
|
14
|
+
// A show's viewer narrows the stamp, never widens it: the boot's controls
|
|
15
|
+
// were made from the stamp. Leaving a session (forgetSession) goes back to
|
|
16
|
+
// the stamp. A page with no stamp may do nothing (it fails closed).
|
|
17
|
+
|
|
18
|
+
// The names in a stamp's or a show's list, as a Set.
|
|
19
|
+
function namesOf(list) {
|
|
20
|
+
const names = Array.isArray(list) ? list : String(list || "").split(/\s+/);
|
|
21
|
+
return new Set(names.filter((name) => typeof name === "string" && name));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// @param body the page's <body> (its data-viewer-kind, data-viewer-can)
|
|
25
|
+
// @return {can(name), kind(), update(viewer), reset(), control(el, name), controls(els, name)}
|
|
26
|
+
export function createViewer(body) {
|
|
27
|
+
const data = body?.dataset || {};
|
|
28
|
+
const stamped = { kind: data.viewerKind || null, can: namesOf(data.viewerCan) };
|
|
29
|
+
let current = stamped;
|
|
30
|
+
|
|
31
|
+
const can = (name) => current.can.has(name);
|
|
32
|
+
return {
|
|
33
|
+
can,
|
|
34
|
+
kind: () => current.kind,
|
|
35
|
+
// A show's +viewer+ ({kind, can}); anything else keeps what is there.
|
|
36
|
+
update(viewer) {
|
|
37
|
+
if (!viewer || typeof viewer !== "object" || !Array.isArray(viewer.can)) return;
|
|
38
|
+
const shown = namesOf(viewer.can);
|
|
39
|
+
current = { kind: stamped.kind, can: new Set([...stamped.can].filter((name) => shown.has(name))) };
|
|
40
|
+
},
|
|
41
|
+
reset() {
|
|
42
|
+
current = stamped;
|
|
43
|
+
},
|
|
44
|
+
// +el+ when the viewer may use +name+; else +el+ hidden, and null (the
|
|
45
|
+
// page wires nothing to it). data-withheld keeps it hidden whatever its
|
|
46
|
+
// own CSS display says (index.html's [data-withheld] rule).
|
|
47
|
+
control(el, name) {
|
|
48
|
+
if (can(name)) return el;
|
|
49
|
+
if (el) {
|
|
50
|
+
el.hidden = true;
|
|
51
|
+
el.dataset.withheld = "";
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
},
|
|
55
|
+
controls(els, name) {
|
|
56
|
+
return els.map((el) => this.control(el, name));
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|