samagotchi 0.134.0 → 0.136.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 +78 -1
- data/bin/chi +2 -3
- data/docs/architecture.md +6 -3
- data/docs/cli.md +43 -38
- data/docs/configuration.md +11 -9
- data/docs/guardrails.md +53 -9
- data/docs/internals/engine.md +6 -3
- data/docs/internals/participants.md +1 -1
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/web-frontend.md +263 -0
- data/docs/internals/working-set.md +2 -2
- data/docs/sessions.md +5 -3
- data/docs/testing.md +36 -6
- data/lib/samagotchi/bridge.rb +11 -6
- data/lib/samagotchi/bridge_client.rb +8 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +0 -2
- data/lib/samagotchi/engine.rb +69 -12
- data/lib/samagotchi/guardrail_wiring.rb +8 -6
- data/lib/samagotchi/guardrails/approval.rb +41 -5
- data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
- data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
- data/lib/samagotchi/guardrails/pair_command.rb +387 -78
- data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
- data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
- data/lib/samagotchi/guardrails/shared_git.rb +1 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
- data/lib/samagotchi/guardrails_command.rb +13 -10
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/message_parts.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +33 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager.rb +13 -11
- data/lib/samagotchi/system_one/approval_question.rb +21 -16
- data/lib/samagotchi/system_one/self_protect.rb +13 -15
- data/lib/samagotchi/tools/delegate_relay.rb +6 -5
- data/lib/samagotchi/tools/delegate_wait.rb +5 -5
- data/lib/samagotchi/tools/execute.rb +6 -6
- data/lib/samagotchi/tools/output_guardrails.rb +9 -2
- data/lib/samagotchi/tools/read.rb +5 -5
- data/lib/samagotchi/turn_anchors.rb +49 -0
- data/lib/samagotchi/verify_anchor.rb +5 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -18
- data/lib/samagotchi/web/message_display.rb +33 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_view.rb +8 -0
- data/lib/samagotchi/web/public/activity.js +1 -1
- data/lib/samagotchi/web/public/annotations.js +1 -1
- data/lib/samagotchi/web/public/answer_fold.js +100 -31
- data/lib/samagotchi/web/public/app.js +451 -367
- data/lib/samagotchi/web/public/bubbles.js +30 -10
- data/lib/samagotchi/web/public/cards_place.js +4 -2
- data/lib/samagotchi/web/public/chip_popover.js +2 -2
- data/lib/samagotchi/web/public/composer.css +8 -0
- data/lib/samagotchi/web/public/context_chips.js +9 -6
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/dock_reserve.js +13 -15
- data/lib/samagotchi/web/public/feature_gates.js +33 -0
- data/lib/samagotchi/web/public/history_reveal.js +10 -6
- data/lib/samagotchi/web/public/index.html +105 -53
- data/lib/samagotchi/web/public/info_bar.js +11 -12
- data/lib/samagotchi/web/public/info_bar_model.js +8 -10
- data/lib/samagotchi/web/public/latest.js +46 -0
- data/lib/samagotchi/web/public/level_chip.js +33 -95
- data/lib/samagotchi/web/public/lists_view.js +15 -13
- data/lib/samagotchi/web/public/live_timing.js +2 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/method_api.js +14 -0
- data/lib/samagotchi/web/public/notifications.js +4 -11
- data/lib/samagotchi/web/public/participant.js +11 -6
- data/lib/samagotchi/web/public/participant_names.js +1 -1
- data/lib/samagotchi/web/public/participants_panel.js +4 -2
- data/lib/samagotchi/web/public/project_picker.js +18 -22
- data/lib/samagotchi/web/public/question_cards.js +2 -2
- data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
- data/lib/samagotchi/web/public/restore.js +2 -0
- data/lib/samagotchi/web/public/revise_stack.js +378 -0
- data/lib/samagotchi/web/public/scope.js +12 -20
- data/lib/samagotchi/web/public/send_turn.js +29 -16
- data/lib/samagotchi/web/public/session_actions.js +15 -14
- data/lib/samagotchi/web/public/session_fields.js +152 -0
- data/lib/samagotchi/web/public/session_payload.js +35 -0
- data/lib/samagotchi/web/public/session_state.js +9 -5
- data/lib/samagotchi/web/public/session_store.js +191 -0
- data/lib/samagotchi/web/public/settings_chip.js +212 -0
- data/lib/samagotchi/web/public/settings_panel.js +121 -0
- data/lib/samagotchi/web/public/shell_command.js +26 -0
- data/lib/samagotchi/web/public/stage_model.js +4 -4
- data/lib/samagotchi/web/public/stage_view.js +11 -16
- data/lib/samagotchi/web/public/start_settings.js +25 -88
- data/lib/samagotchi/web/public/stream_handlers.js +88 -90
- data/lib/samagotchi/web/public/task_stop.js +1 -1
- data/lib/samagotchi/web/public/thinking_chip.js +7 -45
- data/lib/samagotchi/web/public/time_gaps.js +5 -2
- data/lib/samagotchi/web/public/timing.js +15 -5
- data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
- data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +33 -93
- data/lib/samagotchi/web/public/turn_events.js +3 -3
- data/lib/samagotchi/web/public/turn_html.js +20 -16
- data/lib/samagotchi/web/public/turn_model.js +6 -6
- data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +3 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/web/session_control.rb +2 -2
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +6 -3
- data/lib/samagotchi/worker.rb +9 -9
- data/lib/samagotchi/worker_inbound.rb +6 -5
- metadata +15 -3
- data/lib/samagotchi/web/public/hold.js +0 -78
|
@@ -14,6 +14,11 @@ import { hookNoticeLabel, isReportClient, promptOps } from "./turn_events.js";
|
|
|
14
14
|
import { drawCommandBubble } from "./commands.js";
|
|
15
15
|
import { TIME_GAP_CLASS, timeGapEl } from "./time_gaps.js";
|
|
16
16
|
|
|
17
|
+
// What a session's load places at a turn's prompt from the snapshot
|
|
18
|
+
// (cards_place.js placeSnapshotCards): its cards, answered questions,
|
|
19
|
+
// notices and warnings.
|
|
20
|
+
const SNAPSHOT_PLACED = ".plugin-card, .bubble.question, .bubble.hook-notice, .bubble.guardrail-warning";
|
|
21
|
+
|
|
17
22
|
export function removeQueuedBubble(bubble) {
|
|
18
23
|
if (!bubble) return;
|
|
19
24
|
const slot = bubble.parentElement?.classList.contains("ts-queued") ? bubble.parentElement : null;
|
|
@@ -105,7 +110,7 @@ export function setBubbleDelivery(bubble, delivery) {
|
|
|
105
110
|
// @param appendToHistory (el) puts +el+ at the history's end (or the stage's,
|
|
106
111
|
// while it holds a turn), above the live timing line
|
|
107
112
|
// @param isNearBottom () => whether the history follows its end
|
|
108
|
-
// @param turnDom the running turn's
|
|
113
|
+
// @param turnDom the running turn's DOM (stage_view.js)
|
|
109
114
|
// @param findInTurnRoots (selector) => the matching elements in the history
|
|
110
115
|
// and the stage
|
|
111
116
|
// @param shownTexts the running turn's texts on the page (shown_texts.js)
|
|
@@ -149,7 +154,7 @@ export function createBubbles({
|
|
|
149
154
|
|
|
150
155
|
function addEndBubble(text, { stopped = false } = {}) {
|
|
151
156
|
appendToHistory(elementOf(endBubbleTemplate(text, { stopped })));
|
|
152
|
-
if (!turnDom.holds
|
|
157
|
+
if (!turnDom.holds()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
153
158
|
}
|
|
154
159
|
|
|
155
160
|
// ── Prompt bubbles ───────────────────────────────────────────────────────
|
|
@@ -159,10 +164,11 @@ export function createBubbles({
|
|
|
159
164
|
|
|
160
165
|
// The bubble a reload draws (turn_html.js), with this page's own fields.
|
|
161
166
|
// +delivery+: how it went into a running turn (next_step, cut, queue): its
|
|
162
|
-
// queued badge says what it waits for.
|
|
163
|
-
|
|
167
|
+
// queued badge says what it waits for. +source+: a delegate report's
|
|
168
|
+
// "delegate_report" (its fold row, answer_fold.js).
|
|
169
|
+
function makeUserBubble(prompt, { state = null, label = null, enqueuedId = null, own = false, images = null, delivery = null, author = null, turnId = null, source = null } = {}) {
|
|
164
170
|
removeHint();
|
|
165
|
-
const div = elementOf(userBubbleTemplate({ content: prompt, turn_id: turnId }, {
|
|
171
|
+
const div = elementOf(userBubbleTemplate({ content: prompt, turn_id: turnId, source }, {
|
|
166
172
|
label, state, own, enqueuedId, delivery, author, key: normalize(prompt), thumbs: thumbsTemplate(selected(), images),
|
|
167
173
|
}));
|
|
168
174
|
return placeFollowing(div);
|
|
@@ -182,10 +188,16 @@ export function createBubbles({
|
|
|
182
188
|
return echoes.find((el) => el.dataset.userContent === text) || echoes[0] || null;
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
// The running wake turn's id (a turn chi ran for reports), for its report's bubble.
|
|
192
|
+
let wakeTurnId = null;
|
|
193
|
+
|
|
185
194
|
// Returns the bubble of the turn a turn_started starts (the stage takes it
|
|
186
195
|
// into its prompt line), else null.
|
|
187
196
|
function applyPromptOps(event) {
|
|
188
|
-
if (event.type === "turn_started")
|
|
197
|
+
if (event.type === "turn_started") {
|
|
198
|
+
liveTurn.wakeStart = !!event.continue && isReportClient(event.origin?.client_id);
|
|
199
|
+
wakeTurnId = liveTurn.wakeStart ? event.turn_id || null : null;
|
|
200
|
+
}
|
|
189
201
|
const ops = promptOps(event, { myId: clientId, known: (id) => !!bubbleById(id), unmatchedMerge: liveTurn.unmatchedMerge, wakeStart: liveTurn.wakeStart });
|
|
190
202
|
if (event.type === "input_merged") liveTurn.unmatchedMerge = false;
|
|
191
203
|
if (event.type === "pending_input_merged") liveTurn.unmatchedMerge = false;
|
|
@@ -194,7 +206,10 @@ export function createBubbles({
|
|
|
194
206
|
if (op.op === "report") {
|
|
195
207
|
liveTurn.unmatchedMerge = "report";
|
|
196
208
|
} else if (op.op === "add") {
|
|
197
|
-
|
|
209
|
+
// A report that opens a wake turn carries that turn's id, as a
|
|
210
|
+
// reload's does (its fold key); a merged one has none until reload.
|
|
211
|
+
const turnId = op.source && !op.state && liveTurn.wakeStart ? wakeTurnId : null;
|
|
212
|
+
const bubble = makeUserBubble(op.prompt, { state: op.state, label: op.label, enqueuedId: op.enqueuedId, images: op.images, delivery: op.delivery, author: op.author, source: op.source, turnId });
|
|
198
213
|
if (op.state === "started") started = bubble;
|
|
199
214
|
if (event.type === "pending_input_merged") liveTurn.wakeStart = false;
|
|
200
215
|
} else if (op.op === "tag") {
|
|
@@ -239,10 +254,15 @@ export function createBubbles({
|
|
|
239
254
|
const details = elementOf(recapTemplate(text, turnsSince, createdAt));
|
|
240
255
|
// A stale one sits where it was written, above the turns it doesn't cover.
|
|
241
256
|
// Never in the stage: a recap is between turns.
|
|
242
|
-
turnDom.settleFold
|
|
257
|
+
turnDom.settleFold();
|
|
243
258
|
const prompt = recapPlace(turnPromptBubbles(historyEl), turnsSince);
|
|
244
|
-
// Above that turn's gap divider too: the recap was written before the quiet stretch ended.
|
|
245
|
-
|
|
259
|
+
// Above that turn's gap divider too: the recap was written before the quiet stretch ended. The
|
|
260
|
+
// snapshot's cards placed at that prompt (placed first) sit between the divider and the prompt.
|
|
261
|
+
// A delegate report's fold row (answer_fold.js) heads its prompt.
|
|
262
|
+
const head = prompt?.previousElementSibling?.matches(".answer-fold") ? prompt.previousElementSibling : prompt;
|
|
263
|
+
let back = head?.previousElementSibling;
|
|
264
|
+
while (back?.matches(SNAPSHOT_PLACED)) back = back.previousElementSibling;
|
|
265
|
+
const before = back?.matches(`.${TIME_GAP_CLASS}`) ? back : head;
|
|
246
266
|
keepFollowing(historyEl, () => (before ? historyEl.insertBefore(details, before) : appendAboveLiveTiming(historyEl, details, liveTurn.timing.el)));
|
|
247
267
|
}
|
|
248
268
|
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
import { getSession, sendCommand } from "./data.js";
|
|
11
11
|
import { cardPlace, drawCard, isCard, isNoticeCard, refreshCardBody, turnNoticePlace } from "./card.js";
|
|
12
12
|
import { elementOf } from "./turn_html.js";
|
|
13
|
-
import { stepText } from "./
|
|
13
|
+
import { stepText } from "./turn_block.js";
|
|
14
14
|
import { llmContextNotice, noticeLine } from "./turn_events.js";
|
|
15
15
|
import { hookNoticeTemplate, TURN_PROMPT, turnPromptBubbles } from "./bubbles.js";
|
|
16
|
+
import { stillOn } from "./latest.js";
|
|
16
17
|
|
|
17
18
|
// The elements of the reloaded turn that ends before +before+ (the next
|
|
18
19
|
// turn's prompt, or null for the last turn), its prompt first.
|
|
@@ -145,10 +146,11 @@ export function createCardsPlace({
|
|
|
145
146
|
function refreshCardBodies() {
|
|
146
147
|
clearTimeout(cardBodiesTimer);
|
|
147
148
|
const id = selected();
|
|
149
|
+
const still = stillOn(selected, id);
|
|
148
150
|
cardBodiesTimer = setTimeout(async () => {
|
|
149
151
|
try {
|
|
150
152
|
const data = await api.getSession(id, { cards: true });
|
|
151
|
-
if (
|
|
153
|
+
if (!still()) return;
|
|
152
154
|
for (const card of data.cards || []) {
|
|
153
155
|
const el = card.type === "card" && cardElement(card.id);
|
|
154
156
|
if (el) refreshCardBody(el, card);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The popover shell a session-bar chip opens (
|
|
2
|
-
//
|
|
1
|
+
// The popover shell a session-bar chip opens (settings_chip.js' ⚙ panel,
|
|
2
|
+
// the working set's, the start page's panels): a div.ctx-popover on the body, placed
|
|
3
3
|
// at the chip, opened by a click on it (delegated on +el+) or by open()
|
|
4
4
|
// (another opener: the working set's rail and footprint), closed by a second
|
|
5
5
|
// click, its × (data-act="close"), a click outside or Esc; focus goes back
|
|
@@ -24,6 +24,14 @@ body.no-glass .composer-card{background:var(--bg-panel);-webkit-backdrop-filter:
|
|
|
24
24
|
body.has-session #composer{flex-wrap:wrap;row-gap:6px}
|
|
25
25
|
#chips{display:flex;flex-wrap:wrap;gap:6px}
|
|
26
26
|
#chips[hidden]{display:none}
|
|
27
|
+
/* The armed Revise (revise_stack.js reviseChipTemplate): "Revising: ‹first line› ✕". */
|
|
28
|
+
#reviseChip{display:flex;min-width:0}
|
|
29
|
+
#reviseChip[hidden]{display:none}
|
|
30
|
+
.revise-chip{display:inline-flex;align-items:center;gap:6px;max-width:100%;min-width:0;padding:2px 4px 2px 10px;border-radius:999px;border:1px solid var(--accent-border);background:var(--accent-bg);color:var(--text-2);font-size:12px;line-height:18px}
|
|
31
|
+
.revise-chip.refused{border-color:var(--warn-border);background:var(--warn-bg);color:var(--warn-text)}
|
|
32
|
+
.revise-chip-text{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
33
|
+
.revise-chip-clear{flex:none;display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;min-height:0;padding:0;border:0;border-radius:999px;background:transparent;color:var(--text-muted);font-size:11px;cursor:pointer}
|
|
34
|
+
.revise-chip-clear:hover{background:var(--bg-raised);color:var(--text-bright);filter:none}
|
|
27
35
|
.chip{display:inline-flex;align-items:center;gap:6px;background:var(--accent-bg);border:1px solid var(--border-strong);border-radius:8px;padding:3px 4px 3px 3px;font-size:12px;max-width:220px}
|
|
28
36
|
.chip img{width:32px;height:32px;object-fit:cover;border-radius:5px}
|
|
29
37
|
.chip .chip-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
@@ -11,6 +11,7 @@ import { html, nothing, render } from "./vendor/lit-html.js";
|
|
|
11
11
|
import { keepFocus } from "./keep_focus.js";
|
|
12
12
|
import { popoverPlace } from "./chip_popover.js";
|
|
13
13
|
import { cantText } from "./format.js";
|
|
14
|
+
import { createLatest, stillOn } from "./latest.js";
|
|
14
15
|
|
|
15
16
|
// "now", "3m", "2h", "4d" since an ISO time; "" for none.
|
|
16
17
|
export function ageText(iso, now = Date.now()) {
|
|
@@ -106,7 +107,8 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
106
107
|
let rows = [];
|
|
107
108
|
let canAdd = false;
|
|
108
109
|
let openName = null;
|
|
109
|
-
|
|
110
|
+
// Only the newest list read for the open session is drawn (latest.js).
|
|
111
|
+
const latest = createLatest(() => sessionId);
|
|
110
112
|
// The open row's text once read (view text), and the "+ URL" form's
|
|
111
113
|
// state ({error, busy}).
|
|
112
114
|
let text = null;
|
|
@@ -140,15 +142,15 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
140
142
|
async function refresh() {
|
|
141
143
|
const id = sessionId;
|
|
142
144
|
if (!id) return;
|
|
143
|
-
const
|
|
145
|
+
const fresh = latest.begin();
|
|
144
146
|
try {
|
|
145
147
|
const data = await api.list(id);
|
|
146
|
-
if (
|
|
148
|
+
if (!fresh()) return;
|
|
147
149
|
rows = data.sources || [];
|
|
148
150
|
canAdd = data.can_add_url === true;
|
|
149
151
|
} catch {
|
|
150
152
|
// A session the server doesn't know (gone, or an older chi web): no chips.
|
|
151
|
-
if (
|
|
153
|
+
if (!fresh()) return;
|
|
152
154
|
rows = [];
|
|
153
155
|
canAdd = false;
|
|
154
156
|
}
|
|
@@ -234,15 +236,16 @@ export function createContextChips({ el, api, toast = () => {}, doc = document,
|
|
|
234
236
|
const url = form.elements.url.value.trim();
|
|
235
237
|
const why = form.elements.why.value.trim();
|
|
236
238
|
if (!id || !url) return;
|
|
239
|
+
const still = stillOn(() => sessionId);
|
|
237
240
|
drawAdd({ busy: true });
|
|
238
241
|
try {
|
|
239
242
|
const data = await api.add(id, { url, why: why || undefined });
|
|
240
|
-
if (
|
|
243
|
+
if (!still()) return;
|
|
241
244
|
close();
|
|
242
245
|
await refresh();
|
|
243
246
|
toast(`Attached ${data.name}: the agent gets a note before your next message`);
|
|
244
247
|
} catch (e) {
|
|
245
|
-
if (
|
|
248
|
+
if (!still() || openName !== ADD) return;
|
|
246
249
|
// The same form again: what the user typed stays; the URL gets focus.
|
|
247
250
|
drawAdd({ busy: false, error: e.message });
|
|
248
251
|
pop.querySelector('input[name="url"]')?.focus();
|
|
@@ -130,7 +130,7 @@ export function uploadImage(id, file, name, opts = {}) {
|
|
|
130
130
|
// (mergeTiming), and opts.turnId asks for that turn's answer.
|
|
131
131
|
// opts.timing: the whole timing alone (when the merge came up short).
|
|
132
132
|
// opts.parts: each saved message with what it did (thinking, tool calls
|
|
133
|
-
// and their output), for the turn
|
|
133
|
+
// and their output), for the turn block's reload.
|
|
134
134
|
export function getSession(id, { tail, recent, turnId, timing, parts, cards, ...opts } = {}) {
|
|
135
135
|
let query = cards ? "?cards=1" : tail ? "?tail=1" : timing ? "?timing=1" : parts ? "?parts=1" : "";
|
|
136
136
|
if (tail && recent) query += "&recent=1";
|
|
@@ -144,12 +144,14 @@ export function getSession(id, { tail, recent, turnId, timing, parts, cards, ...
|
|
|
144
144
|
// image-only send, whose text is only its [image: …] placeholders).
|
|
145
145
|
// delivery: what the message does to a running turn ("cut": /cut TEXT);
|
|
146
146
|
// none = the server's default (the next step).
|
|
147
|
-
export function sendTurn(id, prompt, { clientId, images, history = true, delivery = null, verify = null, ...opts } = {}) {
|
|
147
|
+
export function sendTurn(id, prompt, { clientId, images, history = true, delivery = null, verify = null, revises = null, ...opts } = {}) {
|
|
148
148
|
const body = clientId ? { prompt, client_id: clientId } : { prompt };
|
|
149
149
|
if (images?.length) body.images = images;
|
|
150
150
|
if (delivery) body.delivery = delivery;
|
|
151
151
|
if (history === false) body.history = false;
|
|
152
152
|
if (verify) body.verify = verify;
|
|
153
|
+
// A Revise turn's draft: {turn_id} of the answer it rewrites.
|
|
154
|
+
if (revises) body.revises = revises;
|
|
153
155
|
return api(`/api/sessions/${id}/turn`, {
|
|
154
156
|
method: "POST",
|
|
155
157
|
body: JSON.stringify(body),
|
|
@@ -21,7 +21,7 @@ import { toastBottom } from "./toast_place.js";
|
|
|
21
21
|
// model panel (toast_place.js); with no card in sight (the start page, all
|
|
22
22
|
// sessions) the CSS's 24 px from the bottom (--toast-bottom unset).
|
|
23
23
|
//
|
|
24
|
-
// +stageEl+: the stage (
|
|
24
|
+
// +stageEl+: the stage (stage_view.js). +atHistoryEnd()+:
|
|
25
25
|
// the reader is at the history's end (app.js, scroll.js's threshold).
|
|
26
26
|
// +reduce()+ / +ease+: prefers-reduced-motion and the eased follow
|
|
27
27
|
// (scroll.js), defaults the page's.
|
|
@@ -37,7 +37,7 @@ export function createDockReserve({
|
|
|
37
37
|
const win = doc.defaultView;
|
|
38
38
|
|
|
39
39
|
function dockReserve() {
|
|
40
|
-
const shown =
|
|
40
|
+
const shown = !stageEl.hidden && !stageEl.classList.contains("leaving");
|
|
41
41
|
return dockEl.offsetHeight + (shown ? stageEl.offsetHeight + STAGE_GAP : 0);
|
|
42
42
|
}
|
|
43
43
|
const historyEnd = () => historyEl.scrollHeight - historyEl.clientHeight;
|
|
@@ -48,7 +48,7 @@ export function createDockReserve({
|
|
|
48
48
|
// counts as at the end.
|
|
49
49
|
function updateReserve({ fromStage = false } = {}) {
|
|
50
50
|
const follow = atHistoryEnd() || historyEl.dataset.following !== undefined;
|
|
51
|
-
const leaving =
|
|
51
|
+
const leaving = stageEl.classList.contains("leaving");
|
|
52
52
|
centerEl.classList.toggle("reserve-easing", leaving);
|
|
53
53
|
const reserve = `${dockReserve()}px`;
|
|
54
54
|
const changed = centerEl.style.getPropertyValue("--dock-h") !== reserve;
|
|
@@ -83,18 +83,16 @@ export function createDockReserve({
|
|
|
83
83
|
|
|
84
84
|
const dockObserver = new win.ResizeObserver((entries) => updateReserve({ fromStage: entries.some((e) => e.target === stageEl) }));
|
|
85
85
|
dockObserver.observe(dockEl);
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}).observe(stageEl, { attributes: true, attributeFilter: ["class", "hidden"] });
|
|
97
|
-
}
|
|
86
|
+
dockObserver.observe(stageEl);
|
|
87
|
+
// Only showing, hiding and leaving change the reserve (not the stage's
|
|
88
|
+
// other classes: waiting, in-use…).
|
|
89
|
+
let stageShown = "";
|
|
90
|
+
new win.MutationObserver(() => {
|
|
91
|
+
const shown = `${stageEl.hidden}|${stageEl.classList.contains("leaving")}`;
|
|
92
|
+
if (shown === stageShown) return;
|
|
93
|
+
stageShown = shown;
|
|
94
|
+
updateReserve({ fromStage: true });
|
|
95
|
+
}).observe(stageEl, { attributes: true, attributeFilter: ["class", "hidden"] });
|
|
98
96
|
win.addEventListener("resize", () => placeToast());
|
|
99
97
|
// The model panel opening, closing or changing height (its list filters).
|
|
100
98
|
new win.ResizeObserver(() => placeToast()).observe(modelPanelEl);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// The open session's feature gates: a page action that its worker must
|
|
2
|
+
// support (its sidecar features, a live bridge), and the redraw that
|
|
3
|
+
// follows when that flips. Registered with the session store as its
|
|
4
|
+
// `featureGates` redraw (session_store.js ORDER): it runs whenever a field
|
|
5
|
+
// it reads changes, and a gate's own redraw only when its answer flipped.
|
|
6
|
+
//
|
|
7
|
+
// A session switch (forgetSession) and a full load (renderSessionView) draw
|
|
8
|
+
// every card with the gate's answer already: they prime() the gates, so the
|
|
9
|
+
// redraw that follows their write sees no flip. A write anywhere else (a
|
|
10
|
+
// restart's hub event bringing the feature, the worker going) fires it.
|
|
11
|
+
//
|
|
12
|
+
// A new module: the page's modules are cached at unversioned URLs, and a
|
|
13
|
+
// new export of an old module would leave a cached copy without it.
|
|
14
|
+
|
|
15
|
+
// @param entries [{name, unavailable(current) → bool, redraw()}]
|
|
16
|
+
// @return {prime(current), check(current)}: prime takes each gate's answer
|
|
17
|
+
// without redrawing; check redraws the gates whose answer flipped since
|
|
18
|
+
export function createFeatureGates(entries) {
|
|
19
|
+
const last = new Map();
|
|
20
|
+
return {
|
|
21
|
+
prime(current) {
|
|
22
|
+
for (const gate of entries) last.set(gate.name, !!gate.unavailable(current));
|
|
23
|
+
},
|
|
24
|
+
check(current) {
|
|
25
|
+
for (const gate of entries) {
|
|
26
|
+
const now = !!gate.unavailable(current);
|
|
27
|
+
const flipped = last.has(gate.name) && last.get(gate.name) !== now;
|
|
28
|
+
last.set(gate.name, now);
|
|
29
|
+
if (flipped) gate.redraw();
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
// The one way a jump into the history shows what it lands on: whatever
|
|
2
2
|
// hides +el+ unfolds: a closed <details> around it (a turn's steps block, a
|
|
3
|
-
// step, a diff), an answer
|
|
4
|
-
// the bubble's class; the jump's unfold isn't saved)
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
3
|
+
// step, a diff), an answer or a delegate report folded (answer_fold.js: its row follows
|
|
4
|
+
// the bubble's class; the jump's unfold isn't saved), a superseded version
|
|
5
|
+
// of a Revise stack (revise_stack.js: its stub opens, not saved). Every
|
|
6
|
+
// jump goes through it (claim_link.js showClaim, refs.js showRow and so the
|
|
7
|
+
// working set's step tags, verdict_card.js "View check"), so a new way to
|
|
8
|
+
// hide history is opened in one place.
|
|
8
9
|
//
|
|
9
10
|
// Pure DOM, so it runs under `node --test` with happy-dom. A module of its
|
|
10
11
|
// own: static JS is cached at unversioned URLs, and a new export on an old
|
|
11
12
|
// module would blank the page.
|
|
12
13
|
|
|
14
|
+
import { openStubOver } from "./revise_stack.js";
|
|
15
|
+
|
|
13
16
|
// Opens each closed <details> that holds +el+ (not +el+ itself: a fold the
|
|
14
17
|
// caller means to open it opens itself) and unfolds the folded answer that
|
|
15
18
|
// is or holds it. +el+, or null.
|
|
16
19
|
export function revealInHistory(el) {
|
|
17
20
|
if (!el) return null;
|
|
18
|
-
el
|
|
21
|
+
openStubOver(el);
|
|
22
|
+
el.closest?.(".bubble.folded")?.classList.remove("folded");
|
|
19
23
|
for (let details = el.parentElement?.closest("details"); details; details = details.parentElement?.closest("details")) {
|
|
20
24
|
details.open = true;
|
|
21
25
|
}
|