samagotchi 0.168.0 → 0.169.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 +38 -1
- data/docs/architecture.md +1 -1
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +2 -2
- data/docs/hooks.md +2 -2
- data/docs/internals/block-renderers.md +1 -1
- data/docs/internals/bridge.md +16 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +2 -1
- data/docs/internals/remote-sessions.md +1 -1
- data/docs/internals/web-frontend.md +62 -22
- data/docs/plugins.md +1 -1
- data/docs/releasing.md +10 -6
- data/docs/testing.md +12 -0
- data/lib/samagotchi/bridge.rb +12 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/client_id.rb +3 -0
- data/lib/samagotchi/command_intake.rb +7 -7
- data/lib/samagotchi/compat.rb +14 -5
- data/lib/samagotchi/engine/objectives.rb +10 -1
- data/lib/samagotchi/guardrails/approvals.rb +8 -3
- data/lib/samagotchi/session_continue.rb +8 -11
- data/lib/samagotchi/session_manager/removal.rb +3 -3
- data/lib/samagotchi/session_stores.rb +79 -0
- data/lib/samagotchi/turn_stages/objectives.rb +6 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +3 -1
- data/lib/samagotchi/web/public/block_watch.js +1 -1
- data/lib/samagotchi/web/public/cards_place.js +5 -1
- data/lib/samagotchi/web/public/index.html +58 -39
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/question_cards.js +20 -6
- data/lib/samagotchi/web/public/stage_asking.js +283 -0
- data/lib/samagotchi/web/public/stage_frame.js +7 -11
- data/lib/samagotchi/web/public/stage_model.js +37 -11
- data/lib/samagotchi/web/public/stage_opened.js +28 -18
- data/lib/samagotchi/web/public/stage_slots.js +13 -89
- data/lib/samagotchi/web/public/stage_view.js +202 -160
- data/lib/samagotchi/web/public/stage_waiting.js +180 -0
- data/lib/samagotchi/web/public/stream_handlers.js +1 -1
- data/lib/samagotchi/web/public/turn_model.js +1 -1
- data/lib/samagotchi/web/session_show.rb +14 -4
- data/lib/samagotchi/worker.rb +3 -4
- metadata +4 -1
|
@@ -10,33 +10,36 @@
|
|
|
10
10
|
// newest step first while the turn runs. When the turn has ended and the stage is not
|
|
11
11
|
// in use, everything moves into the history in the turn block's order (the
|
|
12
12
|
// hand-off), so a handed-off turn reads like its reload. Slots are patched in place, never rebuilt from HTML: a selection
|
|
13
|
-
// and annotate survive. When the
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// so far go there
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
13
|
+
// and annotate survive. When a card waits for the user (a question, an
|
|
14
|
+
// approval, a delegate's relayed card, a hook's question, check-in), the
|
|
15
|
+
// turn opens in the history (stage_opened.js): the prompt, the block and the
|
|
16
|
+
// rows so far go there (chi's line before its own question or approval as a
|
|
17
|
+
// lead-in bubble), and the stage docks the card alone (the asking form)
|
|
18
|
+
// until the answer. The step limit's card goes under its ended turn. The
|
|
19
|
+
// rules are pure in stage_model.js; the live slots are drawn by
|
|
20
|
+
// stage_slots.js, the cap and the held height are stage_frame.js's, the
|
|
21
|
+
// in-use watch that times the hand-off is stage_watch.js's, the docked card
|
|
22
|
+
// and the fold are stage_asking.js's, and the waiting cards as data are
|
|
23
|
+
// stage_waiting.js's.
|
|
21
24
|
import { createTurnBlock } from "./turn_block.js";
|
|
22
25
|
import { openAfterEnd } from "./block_open.js";
|
|
23
|
-
import { chipLabelText, flashOf, handOffDue, handOffOrder, headlineOf, isPlain, liveSlots, MARKS,
|
|
26
|
+
import { chipLabelText, flashOf, handOffDue, handOffOrder, headlineOf, isPlain, liveSlots, MARKS, foldedRowText, phaseLabel, placeFor, placeKind, QUEUED_CHILDREN, stepSlotText } from "./stage_model.js";
|
|
24
27
|
import { createStageSlots, node } from "./stage_slots.js";
|
|
25
28
|
import { createStageFrame, reducedMotion } from "./stage_frame.js";
|
|
26
29
|
import { createInUseWatch } from "./stage_watch.js";
|
|
27
30
|
import { createStageStall, GRACE_MS, stallState } from "./stage_stall.js";
|
|
28
31
|
import { formatDuration } from "./timing.js";
|
|
29
32
|
import { easeScrollTo } from "./scroll.js";
|
|
30
|
-
import { readStored, writeStored } from "./storage.js";
|
|
31
33
|
import { createWorkingSetRail } from "./working_set_rail.js";
|
|
32
34
|
import { moveToHistoryEnd } from "./history_end.js";
|
|
33
35
|
import { turnElements } from "./turn_range.js";
|
|
34
36
|
import { createOpenedTurn } from "./stage_opened.js";
|
|
35
37
|
import { askingRefs } from "./turn_model.js";
|
|
36
|
-
import { ASKS, PENDING
|
|
38
|
+
import { ASKS, PENDING } from "./stage_cards.js";
|
|
39
|
+
import { createStageAsking } from "./stage_asking.js";
|
|
40
|
+
import { createStageWaiting, isApprovalCard, isBlocking, waitingKey, waitingKind } from "./stage_waiting.js";
|
|
37
41
|
import { stampRows } from "./working_set_view.js";
|
|
38
42
|
|
|
39
|
-
const COLLAPSE_KEY = "chi_stage_collapsed";
|
|
40
43
|
// The turn-end read (markdown, answer_display) settles within this, or the
|
|
41
44
|
// hand-off stops waiting for it.
|
|
42
45
|
const SETTLE_CAP_MS = 8000;
|
|
@@ -51,26 +54,15 @@ const ANSWER_CUT = 12;
|
|
|
51
54
|
const ANSWERED_LINE = 40;
|
|
52
55
|
// That line brought back sits this far under the scroller's edge.
|
|
53
56
|
const ANSWERED_GAP = 8;
|
|
54
|
-
// The model's own question (question_cards.js data-kind; not an approval,
|
|
55
|
-
// the step limit, a hook's question or a delegate's relayed one): the one
|
|
56
|
-
// card whose asking text opens the turn in the history (stage_opened.js).
|
|
57
|
-
const ASK_CARD = '.bubble.question[data-kind="question"]:not([data-relay])';
|
|
58
|
-
// A blocking approval, a delegate's relayed one too: too tall for the stage's
|
|
59
|
-
// view, it gets a raised cap (raiseForApproval).
|
|
60
|
-
const APPROVAL_CARD = '.bubble.question[data-kind="approval"]';
|
|
61
57
|
// What opens a turn in the history (reload_check.js's OPENING).
|
|
62
58
|
const OPENING = "[data-turn-id], [data-opening-id]";
|
|
63
59
|
|
|
64
|
-
function readCollapsed() {
|
|
65
|
-
return readStored(COLLAPSE_KEY) === "1";
|
|
66
|
-
}
|
|
67
|
-
|
|
68
60
|
// +historyNearBottom+: the history's own follow check (the hand-off scrolls
|
|
69
61
|
// the history only for a reader at its end). +dockEl+ and +centerEl+ size
|
|
70
62
|
// the stage's cap.
|
|
71
63
|
// +taskStop+: the page's stopper (task_stop.js), for the stop-task button
|
|
72
|
-
// on a running task_wait. +renderMarkdown+ and +linkRefs+:
|
|
73
|
-
//
|
|
64
|
+
// on a running task_wait. +renderMarkdown+ and +linkRefs+: a lead-in
|
|
65
|
+
// bubble's markdown read and ref pass (stage_opened.js). +workingSet+: the
|
|
74
66
|
// page's working set (working_set_view.js, web.working_set; null: off): its
|
|
75
67
|
// rail in the live slots, its footprint at the hand-off. +onCancel+: the
|
|
76
68
|
// status row's Stop turn (a promise of the cancel request; the button is
|
|
@@ -99,6 +91,18 @@ export function createStageView({
|
|
|
99
91
|
const status = node("div", "ts-status", stage);
|
|
100
92
|
const mark = node("span", "ts-mark", status);
|
|
101
93
|
const phaseEl = node("span", "ts-phase", status);
|
|
94
|
+
// Two cards docked: one at a time, "‹ 1 of 2 ›" (stage_waiting.js shown).
|
|
95
|
+
const pager = node("span", "ts-pager", status);
|
|
96
|
+
pager.hidden = true;
|
|
97
|
+
const pagePrev = node("button", "ts-page", pager);
|
|
98
|
+
pagePrev.type = "button";
|
|
99
|
+
pagePrev.textContent = "‹";
|
|
100
|
+
pagePrev.setAttribute("aria-label", "The card before");
|
|
101
|
+
const pageLabel = node("span", "ts-page-label", pager);
|
|
102
|
+
const pageNext = node("button", "ts-page", pager);
|
|
103
|
+
pageNext.type = "button";
|
|
104
|
+
pageNext.textContent = "›";
|
|
105
|
+
pageNext.setAttribute("aria-label", "The next card");
|
|
102
106
|
const stepEl = node("span", "ts-step", status);
|
|
103
107
|
const timingSlot = node("span", "ts-timing", status);
|
|
104
108
|
const miniTool = node("span", "ts-mini", status);
|
|
@@ -131,11 +135,6 @@ export function createStageView({
|
|
|
131
135
|
headline.tabIndex = 0;
|
|
132
136
|
headline.setAttribute("role", "button");
|
|
133
137
|
headline.title = "Show this step's reasoning";
|
|
134
|
-
// While a question or approval waits: the whole reply that asked it, in
|
|
135
|
-
// the headline's place (the card says what to answer, this says why),
|
|
136
|
-
// as markdown when web.markdown is on.
|
|
137
|
-
const asking = node("div", "ts-asking", live);
|
|
138
|
-
asking.hidden = true;
|
|
139
138
|
const now = node("div", "ts-now", live);
|
|
140
139
|
const trail = node("div", "ts-trail", live);
|
|
141
140
|
// The working set's rail: out of the live slots' flow on their right (a
|
|
@@ -149,6 +148,11 @@ export function createStageView({
|
|
|
149
148
|
workingSet.onServer(() => scheduleRefresh());
|
|
150
149
|
}
|
|
151
150
|
const extras = node("div", "ts-extras", scroller);
|
|
151
|
+
// While check-in's card is docked: the live step's text, streamed under
|
|
152
|
+
// the card (the steps are in the history then). The answer's own slot
|
|
153
|
+
// takes it over at the turn's end (placeAnswer, in one task).
|
|
154
|
+
const asksText = node("div", "ts-asks-text", scroller);
|
|
155
|
+
asksText.hidden = true;
|
|
152
156
|
const answer = node("div", "ts-answer", scroller);
|
|
153
157
|
const tail = node("div", "ts-tail", scroller);
|
|
154
158
|
// The cloud's steps, the scroller's last slot, right above the chip.
|
|
@@ -174,7 +178,7 @@ export function createStageView({
|
|
|
174
178
|
// The quiet signal: its span in the now row, its head after the timing line.
|
|
175
179
|
const stall = createStageStall({ stage, nowEl: now, timingSlot });
|
|
176
180
|
const slotsView = createStageSlots({
|
|
177
|
-
headline,
|
|
181
|
+
headline, now, trail, ticks, taskStop,
|
|
178
182
|
gens: () => view.current().turn?.gens,
|
|
179
183
|
onChange: () => scheduleRefresh(),
|
|
180
184
|
});
|
|
@@ -191,10 +195,7 @@ export function createStageView({
|
|
|
191
195
|
let thinkingLine = null; // { step, text } from the live step's ticker
|
|
192
196
|
let flash = null; // { kind, text, until } a notice or nudge in the trail
|
|
193
197
|
let flashTimer = null; // its end's redraw
|
|
194
|
-
let collapsed = readCollapsed();
|
|
195
198
|
let cloudOpen = false;
|
|
196
|
-
// A ref's "show in turn" opened a collapsed stage (not kept).
|
|
197
|
-
let revealed = false;
|
|
198
199
|
let rafPending = false;
|
|
199
200
|
// A turn the page knows runs (the session says running) with none in the
|
|
200
201
|
// stage to show (a join without its current_turn): the status row alone,
|
|
@@ -244,6 +245,7 @@ export function createStageView({
|
|
|
244
245
|
blockHost: (el) => cloudBody.appendChild(el),
|
|
245
246
|
placeAnswer: (pieces, { plain, blockEl }) => {
|
|
246
247
|
if (plain) blockEl.remove();
|
|
248
|
+
drawAsksText("");
|
|
247
249
|
answer.append(...pieces);
|
|
248
250
|
showAnswerTop();
|
|
249
251
|
},
|
|
@@ -256,6 +258,19 @@ export function createStageView({
|
|
|
256
258
|
},
|
|
257
259
|
});
|
|
258
260
|
|
|
261
|
+
// 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
|
+
const waiting = createStageWaiting();
|
|
264
|
+
// The asking form and the stage's fold (stage_asking.js).
|
|
265
|
+
const ask = createStageAsking({
|
|
266
|
+
stage, extras, tail, scroller, fold, view, opener, frame: () => frame, isOpened: () => opened, setOpened,
|
|
267
|
+
isEnded: () => !!endKind, promptEl: () => promptEl, setCloud, scheduleRefresh, pendingCard, waiting,
|
|
268
|
+
});
|
|
269
|
+
const {
|
|
270
|
+
askingGen, asksOf, askedFor, askedText, docks, leadInText, openTurn, settleCards, askingCard, cardOf, foldedText, markOwnToggles,
|
|
271
|
+
isCollapsed, drawCollapsed, applyCollapsed, setCollapsed,
|
|
272
|
+
} = ask;
|
|
273
|
+
|
|
259
274
|
// ── slots ────────────────────────────────────────────────────────────────
|
|
260
275
|
|
|
261
276
|
// "history" (an opened turn's), "cloud", "answer" (a plain turn's in
|
|
@@ -272,8 +287,12 @@ export function createStageView({
|
|
|
272
287
|
requestAnimationFrame(refresh);
|
|
273
288
|
}
|
|
274
289
|
|
|
290
|
+
// A card that blocks the turn waits (stage_waiting.js, checked against
|
|
291
|
+
// the slots first: a replay answers a card and starts the next step in one
|
|
292
|
+
// task, before the refresh).
|
|
275
293
|
function pendingCard() {
|
|
276
|
-
|
|
294
|
+
ask.reconcileWaiting();
|
|
295
|
+
return waiting.count() > 0;
|
|
277
296
|
}
|
|
278
297
|
|
|
279
298
|
// A blocking approval too tall for the stage's view: a raised cap, the
|
|
@@ -282,75 +301,64 @@ export function createStageView({
|
|
|
282
301
|
// fits never gets here.
|
|
283
302
|
function raiseForApproval(blocked) {
|
|
284
303
|
if (!blocked || roomCap) return;
|
|
285
|
-
const
|
|
286
|
-
|
|
304
|
+
const front = waiting.get(waiting.front());
|
|
305
|
+
const card = front?.approval ? cardOf(front.key) : null;
|
|
306
|
+
if (!card || frame.fitsCap(card)) return;
|
|
287
307
|
roomCap = true;
|
|
288
308
|
frame.fitCap();
|
|
289
309
|
}
|
|
290
310
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
// (TurnAccumulator.join_events), so the last step when its last call is
|
|
295
|
-
// ask_user_question and no card of this page took its reply yet (+asked+:
|
|
296
|
-
// a later card, another kind's, never reuses it). null: none.
|
|
297
|
-
function askingGen() {
|
|
298
|
-
const { turn } = view.current();
|
|
299
|
-
const gen = turn && !turn.ended ? turn.gens[turn.gens.length - 1] : null;
|
|
300
|
-
if (!gen?.text) return null;
|
|
301
|
-
if (view.askingText()) return gen;
|
|
302
|
-
return gen.tools[gen.tools.length - 1]?.tool === "ask_user_question" && !asked.has(gen) ? gen : null;
|
|
303
|
-
}
|
|
304
|
-
// The steps whose reply a question card took (place: question_cards.js
|
|
305
|
-
// reads askingText right before), for askingGen's replay rule.
|
|
306
|
-
const asked = new WeakSet();
|
|
307
|
-
function askedFor() {
|
|
308
|
-
const gen = askingGen();
|
|
309
|
-
if (gen) asked.add(gen);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// The reply that asked the question, "" for none.
|
|
313
|
-
function askedText() {
|
|
314
|
-
return String(askingGen()?.text || "").trim();
|
|
311
|
+
// An asking card (check-in) waits while the turn runs.
|
|
312
|
+
function asksCard() {
|
|
313
|
+
return !endKind && waiting.count({ asks: true }) > waiting.count();
|
|
315
314
|
}
|
|
316
315
|
|
|
317
|
-
//
|
|
318
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
316
|
+
// One docked card at a time: the others wait behind it (.ts-behind) and
|
|
317
|
+
// the pager says where the shown one is.
|
|
318
|
+
function drawDocked(shownCard) {
|
|
319
|
+
const keys = waiting.docked();
|
|
320
|
+
for (const key of keys) cardOf(key)?.classList.toggle("ts-behind", !!shownCard && cardOf(key) !== shownCard);
|
|
321
|
+
for (const el of extras.querySelectorAll(":scope > .ts-behind")) if (!keys.includes(waitingKey(el))) el.classList.remove("ts-behind");
|
|
322
|
+
// Focus in a card that went behind (a new card came in front of it):
|
|
323
|
+
// to the card that shows now.
|
|
324
|
+
const focused = document.activeElement;
|
|
325
|
+
if (shownCard && focused && [...extras.querySelectorAll(":scope > .ts-behind")].some((el) => el.contains(focused))) focusIn(shownCard);
|
|
326
|
+
const at = shownCard ? keys.indexOf(waitingKey(shownCard)) : -1;
|
|
327
|
+
pager.hidden = keys.length < 2 || at < 0;
|
|
328
|
+
if (pager.hidden) return;
|
|
329
|
+
const label = `${at + 1} of ${keys.length}`;
|
|
330
|
+
if (pageLabel.textContent !== label) pageLabel.textContent = label;
|
|
331
|
+
pagePrev.disabled = at === 0;
|
|
332
|
+
pageNext.disabled = at === keys.length - 1;
|
|
333
|
+
// A pager button the page just disabled keeps no focus: the other one has it.
|
|
334
|
+
if (document.activeElement === pagePrev && pagePrev.disabled) pageNext.focus();
|
|
335
|
+
else if (document.activeElement === pageNext && pageNext.disabled) pagePrev.focus();
|
|
323
336
|
}
|
|
324
337
|
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (blockEl) blockEl.open = false;
|
|
334
|
-
const rows = [...extras.children].filter((el) => !waitsInStage(el));
|
|
335
|
-
setOpened(true);
|
|
336
|
-
opener.open(handOffOrder({ prompt: promptEl, block: blockEl, extras: rows }));
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// An opened turn's cards that stopped waiting go into the history: a
|
|
340
|
-
// promoted one under its reason (also after the turn's end, so a card
|
|
341
|
-
// cancelled by Stop turn sits there), another one at the history's end
|
|
342
|
-
// while the turn runs.
|
|
343
|
-
function settleCards() {
|
|
344
|
-
if (!opened) return;
|
|
345
|
-
for (const el of [...extras.children]) {
|
|
346
|
-
if (waitsInStage(el)) continue;
|
|
347
|
-
if (opener.has(el)) opener.settle(el);
|
|
348
|
-
else if (!endKind) opener.add(el);
|
|
338
|
+
// +card+'s first control takes the focus (its summary is out of the tab
|
|
339
|
+
// order while it waits), else the card.
|
|
340
|
+
function focusIn(card) {
|
|
341
|
+
const control = card.querySelector('input:not([type="hidden"]), textarea, select, button:not([disabled]), a[href]');
|
|
342
|
+
if (control) control.focus();
|
|
343
|
+
else {
|
|
344
|
+
if (!card.hasAttribute("tabindex")) card.tabIndex = -1;
|
|
345
|
+
card.focus();
|
|
349
346
|
}
|
|
350
347
|
}
|
|
348
|
+
for (const [button, by] of [[pagePrev, -1], [pageNext, 1]]) {
|
|
349
|
+
button.addEventListener("click", (e) => {
|
|
350
|
+
e.stopPropagation();
|
|
351
|
+
waiting.page(by);
|
|
352
|
+
refresh();
|
|
353
|
+
const shown = askingCard();
|
|
354
|
+
if (shown) frame.afterGrowth(() => frame.reveal(shown.parentElement));
|
|
355
|
+
});
|
|
356
|
+
}
|
|
351
357
|
|
|
352
|
-
|
|
353
|
-
|
|
358
|
+
// The text under a docked check-in card, patched only when it changes.
|
|
359
|
+
function drawAsksText(text) {
|
|
360
|
+
if (asksText.textContent !== text) asksText.textContent = text;
|
|
361
|
+
asksText.hidden = !text;
|
|
354
362
|
}
|
|
355
363
|
// A card answered or redrawn in place (its buttons gone) changes the
|
|
356
364
|
// status without a turn event: at once (a microtask), so the slots swap in
|
|
@@ -377,13 +385,21 @@ export function createStageView({
|
|
|
377
385
|
const thinking = thinkingLine && thinkingLine.step === turn.gens.length ? thinkingLine.text : "";
|
|
378
386
|
// A question blocks the turn: its card takes the live slots' room. An
|
|
379
387
|
// asking card's turn goes on: the status waits, the slots stay.
|
|
388
|
+
ask.reconcileWaiting();
|
|
380
389
|
settleCards();
|
|
390
|
+
markOwnToggles();
|
|
381
391
|
const blocked = pendingCard();
|
|
382
|
-
// The asking form: a
|
|
383
|
-
// its
|
|
384
|
-
const
|
|
392
|
+
// The asking form: a docked card waits (the card alone in the stage;
|
|
393
|
+
// its turn is in the history, a reply before it the lead-in bubble).
|
|
394
|
+
const asking = askingCard();
|
|
395
|
+
const askingForm = !!asking;
|
|
385
396
|
stage.classList.toggle("asking-form", askingForm);
|
|
386
|
-
|
|
397
|
+
// Its fold (stage_asking.js trackFold): that card's only.
|
|
398
|
+
ask.trackFold();
|
|
399
|
+
drawDocked(asking);
|
|
400
|
+
const collapsed = isCollapsed();
|
|
401
|
+
drawCollapsed();
|
|
402
|
+
const slots = liveSlots(turn, { narration, thinking, pending: blocked || asksCard(), ended: endKind, emptyAnswer });
|
|
387
403
|
|
|
388
404
|
// The live slots keep their height while the turn waits and until its
|
|
389
405
|
// next step (held-wait, set with waiting): the card under them never
|
|
@@ -398,13 +414,18 @@ export function createStageView({
|
|
|
398
414
|
cancel.hidden = !onCancel || !!endKind;
|
|
399
415
|
stage.classList.toggle("waiting", blocked);
|
|
400
416
|
if (!blocked && wasWaiting) settleAnsweredCard();
|
|
401
|
-
stage.classList.toggle("asking", !!slots.asking);
|
|
402
417
|
stage.dataset.phase = slots.phase;
|
|
403
418
|
mark.textContent = MARKS[slots.phase] || "";
|
|
404
|
-
|
|
419
|
+
// check-in docked, nothing blocking: the row says what chi does now (the
|
|
420
|
+
// running call, "writing" while text streams) and the text streams
|
|
421
|
+
// under the card.
|
|
422
|
+
const asksFront = !!asking && !blocked && !endKind && waiting.get(waiting.front())?.kind === "asks";
|
|
423
|
+
const streaming = asksFront ? liveSlots(turn).phase : "";
|
|
424
|
+
phaseEl.textContent = phaseLabel({ phase: slots.phase, asksFront, streaming });
|
|
425
|
+
stage.classList.toggle("asks-live", asksFront);
|
|
426
|
+
drawAsksText(asksFront && gen?.text && !gen.answer ? String(gen.text).trim() : "");
|
|
405
427
|
|
|
406
428
|
slotsView.drawHeadline(slots.headline);
|
|
407
|
-
slotsView.drawAsking(askingForm ? "" : slots.asking, slots.askingRefs);
|
|
408
429
|
raiseForApproval(blocked);
|
|
409
430
|
slotsView.drawNow(slots.tool, slots.quiet);
|
|
410
431
|
drawStall(clock(), blocked);
|
|
@@ -422,15 +443,19 @@ export function createStageView({
|
|
|
422
443
|
// that line is elsewhere (a turn the stage didn't hold from its start).
|
|
423
444
|
const elapsed = endKind && !timingSlot.firstChild ? formatDuration(endedAt - startedAt) : "";
|
|
424
445
|
chipLabel.textContent = chipLabelText({ steps: slots.step, calls, elapsed });
|
|
425
|
-
|
|
446
|
+
// Folded, the row says what the docked card asks (live and after a
|
|
447
|
+
// reload alike: from the card), else the running call.
|
|
448
|
+
const mini = foldedRowText({ collapsed, question: foldedText(asking), tool: slots.tool, running: asksFront });
|
|
426
449
|
miniTool.textContent = mini.text;
|
|
427
450
|
miniTool.title = mini.title;
|
|
451
|
+
miniTool.classList.toggle("ts-mini-question", mini.question);
|
|
428
452
|
|
|
429
453
|
// An ended turn's live slots go, unless something under them would move
|
|
430
454
|
// (a card or a row in the extras): then they stay as they were last.
|
|
431
455
|
live.hidden = plain || (!!endKind && !extras.firstChild);
|
|
432
|
-
// The chip: a block in the cloud
|
|
433
|
-
|
|
456
|
+
// The chip: a block in the cloud. An opened turn's steps line in the
|
|
457
|
+
// history says the count: none here.
|
|
458
|
+
cloud.hidden = place !== "cloud";
|
|
434
459
|
cloudBody.hidden = !cloudOpen || cloud.hidden;
|
|
435
460
|
// Running: the step it is on. Ended: the count, unless the chip says it.
|
|
436
461
|
stepEl.textContent = stepSlotText({ step: slots.step, ended: !!endKind, chipHidden: cloud.hidden });
|
|
@@ -529,25 +554,6 @@ export function createStageView({
|
|
|
529
554
|
|
|
530
555
|
// ── collapse, cloud, peek ────────────────────────────────────────────────
|
|
531
556
|
|
|
532
|
-
function applyCollapsed() {
|
|
533
|
-
stage.classList.toggle("collapsed", collapsed);
|
|
534
|
-
scroller.hidden = collapsed;
|
|
535
|
-
fold.textContent = collapsed ? "▴" : "▾";
|
|
536
|
-
fold.setAttribute("aria-label", collapsed ? "Expand the running turn" : "Collapse the running turn to its status");
|
|
537
|
-
fold.setAttribute("aria-expanded", String(!collapsed));
|
|
538
|
-
frame.fitCap();
|
|
539
|
-
scheduleRefresh();
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function setCollapsed(value) {
|
|
543
|
-
collapsed = value;
|
|
544
|
-
revealed = false;
|
|
545
|
-
writeStored(COLLAPSE_KEY, value ? "1" : "0");
|
|
546
|
-
applyCollapsed();
|
|
547
|
-
// Opened on a waiting card: it shows as it does on arrival.
|
|
548
|
-
if (!value && pendingCard()) frame.afterGrowth(() => frame.reveal(tail.querySelector(PENDING) ? tail : extras));
|
|
549
|
-
}
|
|
550
|
-
|
|
551
557
|
function setCloud(open) {
|
|
552
558
|
cloudOpen = open;
|
|
553
559
|
cloudBody.hidden = !open || cloud.hidden;
|
|
@@ -574,7 +580,7 @@ export function createStageView({
|
|
|
574
580
|
opener.reveal(step.el);
|
|
575
581
|
return;
|
|
576
582
|
}
|
|
577
|
-
if (
|
|
583
|
+
if (isCollapsed()) setCollapsed(false);
|
|
578
584
|
setCloud(true);
|
|
579
585
|
if (!step.el.open) step.summary.click();
|
|
580
586
|
const wrap = step.thinkingWrap;
|
|
@@ -589,16 +595,14 @@ export function createStageView({
|
|
|
589
595
|
|
|
590
596
|
fold.addEventListener("click", (e) => {
|
|
591
597
|
e.stopPropagation();
|
|
592
|
-
setCollapsed(!
|
|
598
|
+
setCollapsed(!isCollapsed());
|
|
593
599
|
});
|
|
594
600
|
status.addEventListener("click", (e) => {
|
|
595
|
-
if (
|
|
601
|
+
if (isCollapsed() && !e.target.closest("button, a")) setCollapsed(false);
|
|
596
602
|
});
|
|
597
603
|
chip.addEventListener("click", (e) => {
|
|
598
604
|
const tick = e.target.closest(".ts-tick");
|
|
599
605
|
if (tick) openStep(Number(tick.dataset.step));
|
|
600
|
-
// An opened turn's block is in the history: the chip shows it there.
|
|
601
|
-
else if (opened) opener.reveal(view.current().blockEl);
|
|
602
606
|
else setCloud(!cloudOpen);
|
|
603
607
|
});
|
|
604
608
|
headline.addEventListener("click", peek);
|
|
@@ -613,11 +617,7 @@ export function createStageView({
|
|
|
613
617
|
if (!cloudBody.contains(e.target)) return;
|
|
614
618
|
if (folding) return finishFold();
|
|
615
619
|
if (!holding) return;
|
|
616
|
-
if (
|
|
617
|
-
collapsed = false;
|
|
618
|
-
revealed = true;
|
|
619
|
-
applyCollapsed();
|
|
620
|
-
}
|
|
620
|
+
if (isCollapsed()) ask.reveal();
|
|
621
621
|
setCloud(true);
|
|
622
622
|
if (e.detail) e.detail.until = new Promise((resolve) => frame.afterGrowth(resolve));
|
|
623
623
|
});
|
|
@@ -636,13 +636,13 @@ export function createStageView({
|
|
|
636
636
|
// The cap and the frame (stage_frame.js): the stage's height, held while
|
|
637
637
|
// it holds a turn, and its own scrolls.
|
|
638
638
|
const frame = createStageFrame({
|
|
639
|
-
stage, scroller, status, cloud, queuedSlot, holdEl,
|
|
640
|
-
observed: [status, promptSlot, live, extras, answer, tail, cloudBody, cloud, queuedSlot],
|
|
639
|
+
stage, scroller, status, cloud, queuedSlot, holdEl, extras, dockEl, centerEl,
|
|
640
|
+
observed: [status, promptSlot, live, extras, asksText, answer, tail, cloudBody, cloud, queuedSlot],
|
|
641
641
|
isHolding: () => holding,
|
|
642
|
-
isCollapsed
|
|
642
|
+
isCollapsed,
|
|
643
643
|
isCloudOpen: () => cloudOpen,
|
|
644
644
|
isRoomCap: () => roomCap,
|
|
645
|
-
waitingCard: () => (stage.classList.contains("waiting") ?
|
|
645
|
+
waitingCard: () => (stage.classList.contains("waiting") ? cardOf(waiting.front()) : null),
|
|
646
646
|
});
|
|
647
647
|
|
|
648
648
|
// The in-use watch (stage_watch.js): an ended turn hands off once the
|
|
@@ -683,6 +683,7 @@ export function createStageView({
|
|
|
683
683
|
|
|
684
684
|
function emptySlots() {
|
|
685
685
|
for (const slot of [promptSlot, extras, answer, tail, cloudBody, queuedSlot, timingSlot]) slot.replaceChildren();
|
|
686
|
+
drawAsksText("");
|
|
686
687
|
setOpened(false);
|
|
687
688
|
promptEl = null;
|
|
688
689
|
// The turn left the stage: the status row and the chip say nothing until
|
|
@@ -703,9 +704,13 @@ export function createStageView({
|
|
|
703
704
|
flash = null;
|
|
704
705
|
clearTimeout(flashTimer);
|
|
705
706
|
roomCap = false;
|
|
707
|
+
markOwnToggles();
|
|
706
708
|
opener.reset();
|
|
709
|
+
waiting.reset();
|
|
710
|
+
miniTool.classList.remove("ts-mini-question");
|
|
707
711
|
setCloud(false);
|
|
708
|
-
stage.classList.remove("plain", "ended", "waiting", "held-wait", "
|
|
712
|
+
stage.classList.remove("plain", "ended", "waiting", "held-wait", "in-use", "asking-form", "asks-live");
|
|
713
|
+
pager.hidden = true;
|
|
709
714
|
stage.style.removeProperty("--live-held");
|
|
710
715
|
}
|
|
711
716
|
|
|
@@ -731,11 +736,7 @@ export function createStageView({
|
|
|
731
736
|
emptySlots();
|
|
732
737
|
stage.hidden = true;
|
|
733
738
|
frame.frameFree();
|
|
734
|
-
|
|
735
|
-
revealed = false;
|
|
736
|
-
collapsed = readCollapsed();
|
|
737
|
-
applyCollapsed();
|
|
738
|
-
}
|
|
739
|
+
ask.unreveal();
|
|
739
740
|
}
|
|
740
741
|
|
|
741
742
|
function release() {
|
|
@@ -760,6 +761,8 @@ export function createStageView({
|
|
|
760
761
|
const { blockEl } = view.current();
|
|
761
762
|
view.chronological();
|
|
762
763
|
const place = blockPlace(blockEl);
|
|
764
|
+
// A finished turn's block draws as a reload draws it.
|
|
765
|
+
if (blockEl) delete blockEl.dataset.openedTurn;
|
|
763
766
|
// The block moves with the pieces from the cloud; an opened turn's is
|
|
764
767
|
// in the history already.
|
|
765
768
|
const block = place === "cloud" ? blockEl : null;
|
|
@@ -772,13 +775,13 @@ export function createStageView({
|
|
|
772
775
|
// The prompt still in its slot (an opened turn's is in the history).
|
|
773
776
|
prompt: !opened && promptEl?.parentNode === promptSlot ? promptEl : null,
|
|
774
777
|
block,
|
|
775
|
-
extras: [...extras.children],
|
|
778
|
+
extras: [...extras.children].map((el) => { el.classList.remove("ts-behind"); return el; }),
|
|
776
779
|
answer: [...answer.children],
|
|
777
780
|
timing: timingSlot.firstElementChild,
|
|
778
781
|
kept: [...tail.children],
|
|
779
782
|
queued: [...queuedSlot.children],
|
|
780
783
|
});
|
|
781
|
-
const animate = !immediate && !reducedMotion() && !document.hidden && !
|
|
784
|
+
const animate = !immediate && !reducedMotion() && !document.hidden && !isCollapsed();
|
|
782
785
|
folding = { pieces, block: blockEl, endKind, wasAtEnd: historyNearBottom(), animate, immediate, opened, prompt: promptEl };
|
|
783
786
|
releaseTurn();
|
|
784
787
|
if (!animate) return finishFold();
|
|
@@ -795,6 +798,9 @@ export function createStageView({
|
|
|
795
798
|
// The stage held it open; here it takes its place in the history:
|
|
796
799
|
// closed, unless the turn was cut short with an outline (block_open.js).
|
|
797
800
|
if (fold.block) fold.block.open = openAfterEnd(fold.endKind, fold.block.classList.contains("outline"));
|
|
801
|
+
// A piece removed while it slid (the old continue card the step-limit
|
|
802
|
+
// question replaces) stays gone.
|
|
803
|
+
fold.pieces = fold.pieces.filter((el) => el.isConnected);
|
|
798
804
|
moveToHistoryEnd(historyEl, fold.pieces, { animate: fold.animate });
|
|
799
805
|
// An opened turn's reasons go in the pieces' task (after them: the
|
|
800
806
|
// history is as tall as it gets, room to keep the reader's rows in
|
|
@@ -903,7 +909,10 @@ export function createStageView({
|
|
|
903
909
|
// The warn cards the extras kept in sight leave the turn with the
|
|
904
910
|
// block's (card.js leavesBlock): after the end line, as a reload has them.
|
|
905
911
|
const warn = [...extras.querySelectorAll(":scope > .plugin-card.warn")];
|
|
906
|
-
warn.forEach((el) =>
|
|
912
|
+
warn.forEach((el) => {
|
|
913
|
+
el.remove();
|
|
914
|
+
waiting.settle(waitingKey(el), { only: "warn" });
|
|
915
|
+
});
|
|
907
916
|
kept.push(...warn);
|
|
908
917
|
view.chronological();
|
|
909
918
|
endKind = args.kind;
|
|
@@ -946,8 +955,8 @@ export function createStageView({
|
|
|
946
955
|
containsLive: (el) => view.containsLive(el),
|
|
947
956
|
// The reply that asked (question_cards.js keeps it for the answered
|
|
948
957
|
// card's two lines), a replayed question's too.
|
|
949
|
-
askingText: () => askedText(),
|
|
950
|
-
//
|
|
958
|
+
askingText: (pq) => askedText(pq),
|
|
959
|
+
// A lead-in bubble drawn after this pairs them (askingRefs).
|
|
951
960
|
questionRefs(refs) {
|
|
952
961
|
alive();
|
|
953
962
|
view.questionRefs(refs);
|
|
@@ -956,7 +965,9 @@ export function createStageView({
|
|
|
956
965
|
// The turn model and its live phase as the status row reads it (the
|
|
957
966
|
// composer's delivery line).
|
|
958
967
|
current: () => view.current(),
|
|
959
|
-
|
|
968
|
+
// Only a card that blocks says "goes in once the card above is
|
|
969
|
+
// answered": check-in leaves the composer free.
|
|
970
|
+
phase: () => liveSlots(view.current().turn, { pending: pendingCard() }).phase,
|
|
960
971
|
// Another session opens, or this one renders again: a turn still
|
|
961
972
|
// sliding away is dropped (its history is about to be drawn anew),
|
|
962
973
|
// never moved into whatever history shows now.
|
|
@@ -989,37 +1000,62 @@ export function createStageView({
|
|
|
989
1000
|
scheduleRefresh();
|
|
990
1001
|
return true;
|
|
991
1002
|
}
|
|
992
|
-
// An opened turn's row: the history's end, in arrival order
|
|
1003
|
+
// An opened turn's row: the history's end, in arrival order; a card
|
|
1004
|
+
// that stopped waiting before it (a replay answers it and steers in
|
|
1005
|
+
// one task, before the refresh) goes there first.
|
|
993
1006
|
if (where === "opened") {
|
|
1007
|
+
ask.reconcileWaiting();
|
|
1008
|
+
settleCards();
|
|
994
1009
|
opener.add(el);
|
|
995
1010
|
scheduleRefresh();
|
|
996
1011
|
return true;
|
|
997
1012
|
}
|
|
998
|
-
//
|
|
999
|
-
// history,
|
|
1000
|
-
const
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1013
|
+
// A card that waits for the user docks: the turn opens in the
|
|
1014
|
+
// history, a lead-in lands there in this task, the card stays here.
|
|
1015
|
+
const docked = docks(el);
|
|
1016
|
+
const key = waitingKey(el);
|
|
1017
|
+
// A card drawn again with its key (the same question) keeps its lead-in.
|
|
1018
|
+
const kept = docked && opener.has(key);
|
|
1019
|
+
const reason = docked && !kept ? leadInText(el) : "";
|
|
1020
|
+
// Read before the turn's pieces make the history taller: with no
|
|
1021
|
+
// lead-in to read, a reader at the end follows the turn's move.
|
|
1022
|
+
const atEnd = docked ? historyNearBottom() : false;
|
|
1023
|
+
if (docked) openTurn({ follow: atEnd && !reason });
|
|
1004
1024
|
const slot = endKind ? tail : extras;
|
|
1005
1025
|
const stick = slot.scrollHeight - slot.scrollTop - slot.clientHeight < 24;
|
|
1006
|
-
|
|
1026
|
+
// The stage's order (stage_waiting.js): a card that blocks goes before
|
|
1027
|
+
// the asking ones, as a reload places them.
|
|
1028
|
+
const behind = isBlocking(waitingKind(el)) ? [...slot.children].find((x) => waitingKind(x) === "asks") : null;
|
|
1029
|
+
slot.insertBefore(el, behind || null);
|
|
1007
1030
|
slot.hidden = false;
|
|
1031
|
+
// The card is the stage's to keep (stage_waiting.js): docked when it
|
|
1032
|
+
// waits for the user (its lead-in is the opener's, by the same key).
|
|
1033
|
+
const waitKind = waitingKind(el);
|
|
1034
|
+
if (waitKind) waiting.arrive(key, { kind: waitKind, docked, approval: isApprovalCard(el) });
|
|
1008
1035
|
if (stick || waits) slot.scrollTop = slot.scrollHeight;
|
|
1036
|
+
if (kept) opener.relink(key, el);
|
|
1009
1037
|
if (reason) {
|
|
1010
1038
|
const { turn } = view.current();
|
|
1011
1039
|
opener.ask({
|
|
1012
|
-
card: el, text: reason, refs: askingRefs(turn) || askingGen()?.refs || null, step: turn?.gens.length || 0,
|
|
1040
|
+
card: el, key, text: reason, refs: askingRefs(turn) || askingGen(asksOf(el))?.refs || null, step: turn?.gens.length || 0,
|
|
1013
1041
|
turnId: promptEl?.dataset.turnId || null, questionId: el.dataset.qid ?? null, follow: atEnd,
|
|
1014
1042
|
});
|
|
1015
1043
|
}
|
|
1016
|
-
//
|
|
1044
|
+
// The step limit: its card goes under its ended turn in the
|
|
1045
|
+
// conversation at once, as a reload draws it (the hand-off: its fold,
|
|
1046
|
+
// the history's follow and the reload check as for any finished turn;
|
|
1047
|
+
// the in-use watch and the end read don't hold it).
|
|
1048
|
+
if (endKind && waitKind === "continue") {
|
|
1049
|
+
handOff();
|
|
1050
|
+
return true;
|
|
1051
|
+
}
|
|
1052
|
+
// A waiting card and the asking form (refresh) land in one task.
|
|
1017
1053
|
if (waits) refresh();
|
|
1018
1054
|
else scheduleRefresh();
|
|
1019
1055
|
// The asking form: the stage shrinks to the card (one of its two
|
|
1020
1056
|
// allowed shrinks, stage_frame.js; the other is releaseHeldWait).
|
|
1021
|
-
if (
|
|
1022
|
-
if (waits && el.matches?.(".bubble.question")) askedFor();
|
|
1057
|
+
if (docked) frame.forgetContent();
|
|
1058
|
+
if (waits && el.matches?.(".bubble.question")) askedFor(el);
|
|
1023
1059
|
// A card that waits for the user is brought into the stage's view: in
|
|
1024
1060
|
// a short window the prompt and headline fill it and the card sat
|
|
1025
1061
|
// below, out of sight (once the stage has grown for it).
|
|
@@ -1059,6 +1095,12 @@ export function createStageView({
|
|
|
1059
1095
|
drawStall(nowMs);
|
|
1060
1096
|
},
|
|
1061
1097
|
handOff,
|
|
1098
|
+
// A card stopped waiting (stage_waiting.js keys; +only+: when it is that
|
|
1099
|
+
// kind): question_cards.js's resolve path, cards_place.js upsertCard.
|
|
1100
|
+
// The stage's redraw comes from the card's own change.
|
|
1101
|
+
settleWaiting: (key, opts) => waiting.settle(key, opts),
|
|
1102
|
+
// The waiting cards as data (the tests' and the harness's read).
|
|
1103
|
+
waiting: { list: () => waiting.list(), front: () => waiting.front(), docked: () => waiting.docked(), folded: () => waiting.folded(), count: (o) => waiting.count(o) },
|
|
1062
1104
|
root: stage,
|
|
1063
1105
|
scrollEl: scroller,
|
|
1064
1106
|
};
|