samagotchi 0.136.0 → 0.137.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 +42 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +15 -6
- data/docs/guardrails.md +12 -5
- data/docs/internals/engine.md +4 -4
- data/docs/internals/web-frontend.md +20 -10
- data/docs/sessions.md +1 -1
- data/docs/testing.md +2 -1
- data/lib/samagotchi/bridge.rb +4 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos.rb +3 -2
- data/lib/samagotchi/engine.rb +6 -1
- data/lib/samagotchi/guardrail_wiring.rb +3 -1
- data/lib/samagotchi/guardrails/chi_dirs.rb +2 -2
- data/lib/samagotchi/guardrails/outside.rb +5 -5
- data/lib/samagotchi/guardrails/pair_boundary.rb +11 -4
- data/lib/samagotchi/guardrails/pair_command.rb +3 -19
- data/lib/samagotchi/guardrails/protected_paths.rb +5 -2
- data/lib/samagotchi/guardrails/review_paths.rb +32 -2
- data/lib/samagotchi/guardrails/shell_paths.rb +0 -4
- data/lib/samagotchi/guardrails/verdict.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/message_display.rb +6 -3
- data/lib/samagotchi/web/participant_review.rb +2 -1
- data/lib/samagotchi/web/public/answer_fold.js +44 -15
- data/lib/samagotchi/web/public/app.js +12 -1
- data/lib/samagotchi/web/public/chip_popover.js +20 -42
- data/lib/samagotchi/web/public/context_chips.js +16 -6
- data/lib/samagotchi/web/public/copy.js +8 -5
- data/lib/samagotchi/web/public/details_motion.js +74 -0
- data/lib/samagotchi/web/public/families_view.js +15 -6
- data/lib/samagotchi/web/public/index.html +67 -49
- data/lib/samagotchi/web/public/info_bar.js +1 -1
- data/lib/samagotchi/web/public/lists_view.js +1 -0
- data/lib/samagotchi/web/public/participants_panel.js +1 -1
- data/lib/samagotchi/web/public/popover_modal.js +57 -0
- data/lib/samagotchi/web/public/revise_stack.js +76 -6
- data/lib/samagotchi/web/public/revise_switch.js +55 -0
- data/lib/samagotchi/web/public/scope.js +3 -3
- data/lib/samagotchi/web/public/send_turn.js +4 -2
- data/lib/samagotchi/web/public/settings_chip.js +2 -2
- data/lib/samagotchi/web/public/stale_params.js +26 -0
- data/lib/samagotchi/web/public/start_settings.js +1 -1
- data/lib/samagotchi/web/public/start_toolkit.js +1 -1
- data/lib/samagotchi/web/public/stream_handlers.js +10 -5
- data/lib/samagotchi/web/public/working_set_rail.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +2 -2
- metadata +5 -1
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
// verdict cards, the recap. A time-gap divider between two folded stubs
|
|
12
12
|
// folds into the later stub (it shows the divider's words);
|
|
13
13
|
// - the top answer gets a version chip, "v3 · 2 earlier ▾", listing the
|
|
14
|
-
// versions by their prompts; picking one reveals its stub;
|
|
14
|
+
// versions by their prompts; picking one reveals its stub; and a ‹ 3/3 ›
|
|
15
|
+
// switcher before it shows an earlier version's answer in the top
|
|
16
|
+
// answer's place (revise_switch.js; view only, not stored);
|
|
15
17
|
// - the final answer of an ended turn (verify_target.js's rule, a delegate
|
|
16
18
|
// report's reply excluded), unstacked or a stack's top, and not a Verify
|
|
17
19
|
// turn's, gets a Revise button: it arms the composer (app.js).
|
|
@@ -28,6 +30,7 @@ import { bubbleCopyText } from "./copy.js";
|
|
|
28
30
|
import { foldLabel, keepRowInView, lineCount, readFolded, writeFold } from "./answer_fold.js";
|
|
29
31
|
import { clip } from "./format.js";
|
|
30
32
|
import { NOTE_BOUNDARY } from "./turn_range.js";
|
|
33
|
+
import { SWAPPED_CLASS, VIEW_CLASS, stepVersion, switcherTemplate, viewOf } from "./revise_switch.js";
|
|
31
34
|
|
|
32
35
|
// The classes and attributes this module puts on the history.
|
|
33
36
|
export const HIDDEN_CLASS = "stack-hidden";
|
|
@@ -146,7 +149,7 @@ export function historyTurns(historyEl) {
|
|
|
146
149
|
while (turn.els.at(-1)?.classList.contains("time-gap")) turn.els.pop();
|
|
147
150
|
};
|
|
148
151
|
for (const el of historyEl.children) {
|
|
149
|
-
if (el.classList.contains(STUB_CLASS) || el.classList.contains(ROW_CLASS)) continue;
|
|
152
|
+
if (el.classList.contains(STUB_CLASS) || el.classList.contains(ROW_CLASS) || el.classList.contains(VIEW_CLASS)) continue;
|
|
150
153
|
if (el.classList.contains(FOLD_ROW) && el.nextElementSibling?.matches(OPENING)) continue;
|
|
151
154
|
const opening = el.matches(OPENING) ? el : el.querySelector(OPENING);
|
|
152
155
|
if (opening || el.matches(NOTE_BOUNDARY)) {
|
|
@@ -187,6 +190,10 @@ export function installReviseStacks({
|
|
|
187
190
|
const rows = new Map();
|
|
188
191
|
// Each row's answer bubble (its label at a click).
|
|
189
192
|
const rowAnswer = new WeakMap();
|
|
193
|
+
// The switcher: each stack's version on view (0-based, by its top's id;
|
|
194
|
+
// none: the latest), and the copy shown for it ({el, source, hidden}).
|
|
195
|
+
const shown = new Map();
|
|
196
|
+
const views = new Map();
|
|
190
197
|
let observer = null;
|
|
191
198
|
|
|
192
199
|
function stubFor(id, opening, stored) {
|
|
@@ -234,7 +241,8 @@ export function installReviseStacks({
|
|
|
234
241
|
: nothing;
|
|
235
242
|
const button = html`<button type="button" class=${isArmed ? "revise-btn armed" : "revise-btn"} data-revise=${id} ?disabled=${off} title=${off ? REVISE_UNAVAILABLE : "Rewrite this answer: your next message revises it, and the earlier version folds away"}>${isArmed ? "Revising…" : "Revise"}</button>`;
|
|
236
243
|
row.classList.toggle("stacked", !!chain);
|
|
237
|
-
|
|
244
|
+
const at = chain ? (shown.get(id) ?? chain.length - 1) : 0;
|
|
245
|
+
render(html`${chain ? switcherTemplate(id, at, chain.length) : nothing}${versions}${button}`, row);
|
|
238
246
|
rowAnswer.set(row, answer);
|
|
239
247
|
}
|
|
240
248
|
|
|
@@ -287,6 +295,7 @@ export function installReviseStacks({
|
|
|
287
295
|
keepRows.add(turn.id);
|
|
288
296
|
drawRow(row, { id: turn.id, answer, chain: tops.get(turn.id) || null, openings });
|
|
289
297
|
}
|
|
298
|
+
syncViews(tops, keepRows, new Map(turns.filter((t) => t.id).map((t) => [t.id, t.answer])));
|
|
290
299
|
// A divider between two folded stubs folds into the later one.
|
|
291
300
|
for (const turn of turns) {
|
|
292
301
|
if (!turn.stub) continue;
|
|
@@ -307,13 +316,67 @@ export function installReviseStacks({
|
|
|
307
316
|
for (const [id, stub] of stubs) if (!keepStubs.has(id) || stub.parentElement !== historyEl) { stub.remove(); stubs.delete(id); }
|
|
308
317
|
for (const [id, row] of rows) if (!keepRows.has(id) || row.parentElement !== historyEl) { row.remove(); rows.delete(id); }
|
|
309
318
|
// A stale row left by a re-render (its map entry gone with the nodes).
|
|
310
|
-
|
|
311
|
-
|
|
319
|
+
const viewEls = new Set([...views.values()].map((v) => v.el));
|
|
320
|
+
for (const el of historyEl.querySelectorAll(`:scope > .${STUB_CLASS}, :scope > .${ROW_CLASS}, :scope > .${VIEW_CLASS}`)) {
|
|
321
|
+
if (stubs.get(el.dataset.stackStub) !== el && rows.get(el.dataset.reviseFor) !== el && !viewEls.has(el)) el.remove();
|
|
312
322
|
}
|
|
313
323
|
// What we changed isn't news.
|
|
314
324
|
observer?.takeRecords();
|
|
315
325
|
}
|
|
316
326
|
|
|
327
|
+
// The switcher's copies: a stack on an earlier version (its top's row
|
|
328
|
+
// drawn, that version's answer there) shows a copy of that answer just
|
|
329
|
+
// before the top answer and its fold row, which hide; any other stack's
|
|
330
|
+
// copy goes and its top shows again.
|
|
331
|
+
function syncViews(tops, keepRows, answers) {
|
|
332
|
+
const wanted = (id) => {
|
|
333
|
+
const chain = tops.get(id);
|
|
334
|
+
const k = shown.get(id);
|
|
335
|
+
if (!chain || k === undefined || k >= chain.length - 1 || !keepRows.has(id)) return null;
|
|
336
|
+
const source = answers.get(chain[k]);
|
|
337
|
+
const top = answers.get(id);
|
|
338
|
+
return source && top ? { source, top, version: k + 1 } : null;
|
|
339
|
+
};
|
|
340
|
+
for (const [id, view] of views) {
|
|
341
|
+
const want = wanted(id);
|
|
342
|
+
if (want && view.source === want.source && view.top === want.top && view.el.isConnected) continue;
|
|
343
|
+
view.el.remove();
|
|
344
|
+
for (const el of view.hidden) el.classList.remove(SWAPPED_CLASS);
|
|
345
|
+
views.delete(id);
|
|
346
|
+
}
|
|
347
|
+
for (const id of [...shown.keys()]) {
|
|
348
|
+
const want = wanted(id);
|
|
349
|
+
if (!want) {
|
|
350
|
+
shown.delete(id);
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
let view = views.get(id);
|
|
354
|
+
if (!view) {
|
|
355
|
+
view = { el: viewOf(want.source, want.version), source: want.source, top: want.top, hidden: [] };
|
|
356
|
+
views.set(id, view);
|
|
357
|
+
}
|
|
358
|
+
const head = want.top.previousElementSibling?.classList.contains(FOLD_ROW) ? want.top.previousElementSibling : null;
|
|
359
|
+
for (const el of view.hidden) if (el !== want.top && el !== head) el.classList.remove(SWAPPED_CLASS);
|
|
360
|
+
view.hidden = head ? [want.top, head] : [want.top];
|
|
361
|
+
for (const el of view.hidden) el.classList.add(SWAPPED_CLASS);
|
|
362
|
+
const before = head || want.top;
|
|
363
|
+
if (before.previousElementSibling !== view.el) historyEl.insertBefore(view.el, before);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// The switcher's ‹ or › on its row's stack.
|
|
368
|
+
function step(row, delta) {
|
|
369
|
+
const id = row.dataset.reviseFor;
|
|
370
|
+
const count = row.querySelectorAll("[data-stack-pick]").length;
|
|
371
|
+
if (!id || count < 2) return;
|
|
372
|
+
const was = shown.get(id) ?? count - 1;
|
|
373
|
+
const k = stepVersion(was, count, delta);
|
|
374
|
+
if (k === was) return;
|
|
375
|
+
if (k === count - 1) shown.delete(id);
|
|
376
|
+
else shown.set(id, k);
|
|
377
|
+
keepRowInView(historyEl, row, decorate);
|
|
378
|
+
}
|
|
379
|
+
|
|
317
380
|
function setOpen(stub, open, { save }) {
|
|
318
381
|
keepRowInView(historyEl, stub, () => {
|
|
319
382
|
stub.classList.toggle("open", open);
|
|
@@ -326,7 +389,7 @@ export function installReviseStacks({
|
|
|
326
389
|
// answer's key or its end (its class).
|
|
327
390
|
observer = new MutationObserver((records) => {
|
|
328
391
|
const news = records.some((r) => (r.type === "childList" ? r.target === historyEl
|
|
329
|
-
&& [...r.addedNodes, ...r.removedNodes].some((n) => n.nodeType === 1 && !
|
|
392
|
+
&& [...r.addedNodes, ...r.removedNodes].some((n) => n.nodeType === 1 && ![STUB_CLASS, ROW_CLASS, VIEW_CLASS].some((c) => n.classList.contains(c)))
|
|
330
393
|
: r.attributeName !== "class" || r.target.matches?.(`.bubble.output, .${STUB_CLASS}`)));
|
|
331
394
|
if (news) decorate();
|
|
332
395
|
});
|
|
@@ -340,6 +403,13 @@ export function installReviseStacks({
|
|
|
340
403
|
setOpen(stub, !stub.classList.contains("open"), { save: true });
|
|
341
404
|
return;
|
|
342
405
|
}
|
|
406
|
+
const stepper = e.target.closest?.("[data-stack-step]");
|
|
407
|
+
if (stepper) {
|
|
408
|
+
e.preventDefault();
|
|
409
|
+
const row = stepper.closest(`.${ROW_CLASS}`);
|
|
410
|
+
if (row && stepper.getAttribute("aria-disabled") !== "true") step(row, Number(stepper.dataset.stackStep));
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
343
413
|
const pick = e.target.closest?.("[data-stack-pick]");
|
|
344
414
|
if (pick) {
|
|
345
415
|
e.preventDefault();
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// The ‹ N/M › switcher on a Revise stack's top answer (revise_stack.js draws
|
|
2
|
+
// it in the top answer's row, before the version chip): the owner reads an
|
|
3
|
+
// earlier version's answer in place of the top one. View only: no server
|
|
4
|
+
// call, nothing stored (a reload shows the latest again).
|
|
5
|
+
//
|
|
6
|
+
// An earlier version is shown as a copy of its answer bubble (the bubble
|
|
7
|
+
// itself stays hidden in its superseded turn): the copy goes just before the
|
|
8
|
+
// top answer (its fold row, answer_fold.js, kept with it), and the top answer
|
|
9
|
+
// and its fold row are hidden by a class while it shows. The copy keeps its
|
|
10
|
+
// kept source (data-copy-source) and its copy buttons, so Copy copies the
|
|
11
|
+
// version on view; it has no answer key (data-answer-turn), so no fold row,
|
|
12
|
+
// claim key or Revise row is drawn for it, and the top answer stays its
|
|
13
|
+
// turn's last answer (verify_target.js, cards_place.js).
|
|
14
|
+
//
|
|
15
|
+
// A module of its own: static JS is cached at unversioned URLs, and a new
|
|
16
|
+
// export on an old module would blank the page.
|
|
17
|
+
import { html } from "./vendor/lit-html.js";
|
|
18
|
+
|
|
19
|
+
// The copy of an earlier answer on view, and what hides the top one.
|
|
20
|
+
export const VIEW_CLASS = "stack-view";
|
|
21
|
+
export const SWAPPED_CLASS = "stack-swapped";
|
|
22
|
+
|
|
23
|
+
// The switcher's words for version +k+ (0-based) of +count+: the counter
|
|
24
|
+
// ("2/3") and its spoken label ("version 2 of 3").
|
|
25
|
+
export function switchText(k, count) {
|
|
26
|
+
return { text: `${k + 1}/${count}`, label: `version ${k + 1} of ${count}` };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Version +k+ moved by +delta+, kept in 0…count-1.
|
|
30
|
+
export function stepVersion(k, count, delta) {
|
|
31
|
+
return Math.min(Math.max(k + delta, 0), Math.max(count - 1, 0));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The switcher for the stack +id+ (its top turn's id) showing version +k+
|
|
35
|
+
// of +count+. The buttons stay focusable at either end (aria-disabled, not
|
|
36
|
+
// disabled: a press there keeps the focus where it is).
|
|
37
|
+
export function switcherTemplate(id, k, count) {
|
|
38
|
+
const { text, label } = switchText(k, count);
|
|
39
|
+
const first = k <= 0;
|
|
40
|
+
const last = k >= count - 1;
|
|
41
|
+
return html`<span class="stack-switch" role="group" aria-label="Answer versions" data-stack-switch=${id}><button type="button" class="stack-switch-btn" data-stack-step="-1" aria-label="Previous version" title="Read the previous version" aria-disabled=${first ? "true" : "false"}>‹</button><span class="stack-switch-n" title=${label}><span aria-hidden="true">${text}</span><span class="stack-switch-sr" aria-live="polite">${label}</span></span><button type="button" class="stack-switch-btn" data-stack-step="1" aria-label="Next version" title="Read the next version" aria-disabled=${last ? "true" : "false"}>›</button></span>`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// A copy of +answer+ (an earlier version's answer bubble) to show in the top
|
|
45
|
+
// answer's place, titled as version +version+: no answer key,
|
|
46
|
+
// none of the history's state classes.
|
|
47
|
+
export function viewOf(answer, version) {
|
|
48
|
+
const view = answer.cloneNode(true);
|
|
49
|
+
for (const attr of ["id", "data-answer-turn", "data-reply-key", "data-stack-of", "data-line-key"]) view.removeAttribute(attr);
|
|
50
|
+
view.classList.remove("stack-hidden", "folded", SWAPPED_CLASS);
|
|
51
|
+
view.classList.add(VIEW_CLASS);
|
|
52
|
+
view.dataset.stackView = String(version);
|
|
53
|
+
for (const btn of view.querySelectorAll(".copy-btn[data-copied]")) delete btn.dataset.copied;
|
|
54
|
+
return view;
|
|
55
|
+
}
|
|
@@ -50,13 +50,13 @@ export function scopeTemplate(scope, { error = null, where = () => ({ hash: "" }
|
|
|
50
50
|
const at = where();
|
|
51
51
|
const allHref = () => allScopeHref(where().hash, scope.dir);
|
|
52
52
|
const all = html`<a class="scope-all" href=${allScopeHref(at.hash, scope.dir)} title=${"Sessions of every project"} @click=${(e) => { e.currentTarget.href = allHref(); }}>all</a>`;
|
|
53
|
-
if (error) return html`<button id="scopePick" class="scope-name error" type="button" aria-haspopup="dialog" title=${error}>folder not found</button>${all}`;
|
|
54
|
-
if (scope.projectName) return html`<button id="scopePick" class="scope-name" type="button" aria-haspopup="dialog" title=${`Sessions of ${scope.projectDir}`}>${scope.projectName}</button>${all}`;
|
|
53
|
+
if (error) return html`<button id="scopePick" class="scope-name error" type="button" aria-haspopup="dialog" aria-expanded="false" title=${error}>folder not found</button>${all}`;
|
|
54
|
+
if (scope.projectName) return html`<button id="scopePick" class="scope-name" type="button" aria-haspopup="dialog" aria-expanded="false" title=${`Sessions of ${scope.projectDir}`}>${scope.projectName}</button>${all}`;
|
|
55
55
|
const dir = scope.dir || scope.serverDir;
|
|
56
56
|
const backHref = () => projectScopeHref(scope.backDir, where().hash);
|
|
57
57
|
const back = scope.backName
|
|
58
58
|
? html`<a class="scope-all scope-back" href=${projectScopeHref(scope.backDir, at.hash)} title=${`Sessions of ${scope.backName} only`} @click=${(e) => { e.currentTarget.href = backHref(); }}>← ${scope.backName}</a>`
|
|
59
59
|
: nothing;
|
|
60
60
|
const newSession = dir ? html`<span class="scope-where" title=${`New sessions start in ${dir}`}>new session in<span class="path"><bdi>${dir}</bdi></span></span>` : nothing;
|
|
61
|
-
return html`<button id="scopePick" class="scope-name" type="button" aria-haspopup="dialog" title="Sessions of every project">all projects</button>${back}${newSession}`;
|
|
61
|
+
return html`<button id="scopePick" class="scope-name" type="button" aria-haspopup="dialog" aria-expanded="false" title="Sessions of every project">all projects</button>${back}${newSession}`;
|
|
62
62
|
}
|
|
@@ -121,8 +121,10 @@ export function createSendTurn({
|
|
|
121
121
|
turnDom.handOff({ immediate: true });
|
|
122
122
|
const sending = composer.chips().slice();
|
|
123
123
|
// Into a running turn the echo says what it waits for (its delivery); a
|
|
124
|
-
// line with images always waits for the turn's
|
|
125
|
-
|
|
124
|
+
// line with images, or a Verify or a Revise, always waits for the turn's
|
|
125
|
+
// end (the worker queues it).
|
|
126
|
+
const anchored = !!verify() || !!revise();
|
|
127
|
+
const shownDelivery = isRunning ? (sending.length || anchored ? "queue" : delivery || "next_step") : null;
|
|
126
128
|
const echo = makeUserBubble(prompt, { state: "queued", own: true, images: sending, delivery: shownDelivery });
|
|
127
129
|
if (!turnDom.holds()) historyEl.scrollTop = historyEl.scrollHeight;
|
|
128
130
|
if (!store.current.firstPreview || store.current.firstPreview === "—") {
|
|
@@ -77,7 +77,7 @@ export function chipTemplate(summaries) {
|
|
|
77
77
|
const chip = summaries && chipModel(summaries);
|
|
78
78
|
if (!chip) return nothing;
|
|
79
79
|
return html`<button type="button" class=${`model settings-chip${chip.own ? " own" : ""}`} title=${chip.title}
|
|
80
|
-
aria-label=${chip.label} aria-haspopup="dialog" data-settings-chip><span class="chip-icon sc-gear" aria-hidden="true">⚙</span>${
|
|
80
|
+
aria-label=${chip.label} aria-haspopup="dialog" aria-expanded="false" data-settings-chip><span class="chip-icon sc-gear" aria-hidden="true">⚙</span>${
|
|
81
81
|
chip.text ? html`<span class="sc-own"> ${chip.text}</span>` : nothing}</button>`;
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -197,7 +197,7 @@ export function createSettingsControl({ el, summaries, session = () => null, run
|
|
|
197
197
|
};
|
|
198
198
|
const shell = createChipPopover({
|
|
199
199
|
id, className: "ctx-popover start-settings session-settings", width: 440, el, anchor, summary: present, session,
|
|
200
|
-
show, sheet: true, sheetShare: SHEET_TALL,
|
|
200
|
+
show, sheet: true, sheetShare: SHEET_TALL, label: PANEL_LABEL, onOpen: focusInside,
|
|
201
201
|
onClose: () => { shown = null; error = null; }, onSubmit: submit, onAct: act, doc, win,
|
|
202
202
|
});
|
|
203
203
|
// An edit makes the disclosure Set's error stale.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Query parameters the page no longer reads, dropped from the address bar
|
|
2
|
+
// as it loads (app.js, history.replaceState), so a bookmark or a link kept
|
|
3
|
+
// from an older chi stops carrying them: ?view= (the turn view, removed in
|
|
4
|
+
// 0.135.0). The rest of the query stays as written (?dir='s readable
|
|
5
|
+
// slashes too), in its order.
|
|
6
|
+
//
|
|
7
|
+
// A module of its own: static JS is cached at unversioned URLs, and a new
|
|
8
|
+
// export on an old module would blank the page.
|
|
9
|
+
|
|
10
|
+
export const STALE_PARAMS = ["view"];
|
|
11
|
+
|
|
12
|
+
// A query part's name ("view" of "view=turn"), decoded as URLSearchParams
|
|
13
|
+
// reads it.
|
|
14
|
+
function nameOf(part) {
|
|
15
|
+
const raw = part.split("=", 1)[0].replace(/\+/g, " ");
|
|
16
|
+
try { return decodeURIComponent(raw); } catch { return raw; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// +search+ (a location.search) without the stale parameters, "?…" or "";
|
|
20
|
+
// null when it has none (nothing to replace).
|
|
21
|
+
export function withoutStaleParams(search) {
|
|
22
|
+
const parts = String(search || "").replace(/^\?/, "").split("&").filter(Boolean);
|
|
23
|
+
const kept = parts.filter((part) => !STALE_PARAMS.includes(nameOf(part)));
|
|
24
|
+
if (kept.length === parts.length) return null;
|
|
25
|
+
return kept.length ? `?${kept.join("&")}` : "";
|
|
26
|
+
}
|
|
@@ -190,7 +190,7 @@ export function createStartSettings({ el, button, openers = `#${button.id}`, thi
|
|
|
190
190
|
id: "startSettingsPopover", className: "ctx-popover start-settings", width: 440, el, anchor: `#${button.id}`, openers,
|
|
191
191
|
// Taller than a plain sheet: the Apply and budget fields or the review
|
|
192
192
|
// form would push the note under the edge of a 60 % one.
|
|
193
|
-
summary: () => ({}), sheet: true, sheetShare: SHEET_TALL,
|
|
193
|
+
summary: () => ({}), sheet: true, sheetShare: SHEET_TALL, label: PANEL_LABEL, doc, win,
|
|
194
194
|
show: (_data, opening) => {
|
|
195
195
|
if (opening) {
|
|
196
196
|
const choice = llmContext.choice();
|
|
@@ -109,7 +109,7 @@ export function createStartToolkit({ el, button, openers = `#${button.id}`, tool
|
|
|
109
109
|
|
|
110
110
|
const shell = createChipPopover({
|
|
111
111
|
id: "startToolkitPopover", className: "ctx-popover start-toolkit", width: 380, el, anchor: `#${button.id}`, openers,
|
|
112
|
-
summary: () => toolkits(), sheet: true,
|
|
112
|
+
summary: () => toolkits(), sheet: true, label: LIST_LABEL, doc, win,
|
|
113
113
|
show: () => draw(),
|
|
114
114
|
onOpen: focusInside,
|
|
115
115
|
onClose: () => { error = null; },
|
|
@@ -185,7 +185,8 @@ export function createStreamHandlers(deps) {
|
|
|
185
185
|
liveTurn.timing.start(data.started_at, data.turn_id || null);
|
|
186
186
|
}),
|
|
187
187
|
turn_completed: async (data) => {
|
|
188
|
-
// The turn's id: the event names
|
|
188
|
+
// The turn's id: the event names it (an older worker names
|
|
189
|
+
// none: its turn_started did).
|
|
189
190
|
const turnId = data.turn_id || liveTurn.timing.turnId || null;
|
|
190
191
|
activityEnded();
|
|
191
192
|
// A participant's turn: their spend and turns in the panel moved.
|
|
@@ -194,11 +195,15 @@ export function createStreamHandlers(deps) {
|
|
|
194
195
|
const out = turnOutput(data);
|
|
195
196
|
if (out) appendChunk(out);
|
|
196
197
|
if (data.verdict) deps.placeVerdict?.(data.verdict);
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
198
|
+
// Its prompt takes the turn's stamps, as a reload draws them
|
|
199
|
+
// (revise_stack.js follows the DOM): a Revise turn that revised
|
|
200
|
+
// (the stack folds), and a Verify turn (no Revise on its answer, for
|
|
201
|
+
// every viewer, verdict or not; the sender's echo had it already).
|
|
202
|
+
if ((data.revises?.turn_id || data.verify) && turnId) {
|
|
200
203
|
for (const prompt of findInTurnRoots(".bubble.user[data-turn-id]")) {
|
|
201
|
-
if (prompt.dataset.turnId
|
|
204
|
+
if (prompt.dataset.turnId !== turnId) continue;
|
|
205
|
+
if (data.revises?.turn_id) prompt.dataset.revises = data.revises.turn_id;
|
|
206
|
+
if (data.verify) prompt.dataset.verify = "1";
|
|
202
207
|
}
|
|
203
208
|
}
|
|
204
209
|
// after_turn hooks are running and may present the answer: its
|
|
@@ -100,6 +100,8 @@ export function createRing(parent) {
|
|
|
100
100
|
export function createWorkingSetRail({ live, stage, onOpen, onChange }) {
|
|
101
101
|
const rail = el("div", "ts-ws", live);
|
|
102
102
|
rail.setAttribute("role", "button");
|
|
103
|
+
rail.setAttribute("aria-haspopup", "dialog");
|
|
104
|
+
rail.setAttribute("aria-expanded", "false");
|
|
103
105
|
rail.tabIndex = 0;
|
|
104
106
|
const head = el("div", "ws-head", rail);
|
|
105
107
|
const name = el("span", "ws-name", head);
|
|
@@ -105,7 +105,7 @@ export function footprintTemplate(items, ordinal) {
|
|
|
105
105
|
const sep = html` <span class="sep">·</span> `;
|
|
106
106
|
const parts = fp.labels.map((l, i) => html`${i ? sep : nothing}<span class=${`l ${l.kind}${l.state && l.state !== "live" ? ` ${l.state}` : ""}`} title=${labelTitle(l)}>${l.glyph ? html`<span class="k">${l.glyph}</span> ` : nothing}${labelText({ ...l, glyph: "" })}</span>${footTag(l)}`);
|
|
107
107
|
const text = footprintText(items, BUDGETS.footprint);
|
|
108
|
-
return html`<div class="ws-foot" role="button" tabindex="0" data-ws-turn=${ordinal} data-ws-sig=${footSig(items)} aria-label=${`Working set of this turn: ${text.replace(/^touched /, "")}. Open the list.`}><span class="w">touched</span> ${parts}${fp.tail ? html`${sep}<span class="w">${fp.tail}</span>` : nothing}${fp.edited ? html`${sep}<span class="e">edited ${fp.edited}</span>` : nothing}${fp.stale ? html`${sep}<span class="s">${fp.stale} stale</span>` : nothing}${fp.forgotten ? html`${sep}<span class="f">${fp.forgotten} forgotten</span>` : nothing}${fp.dropped ? html`${sep}<span class="f">${fp.dropped} dropped</span>` : nothing}</div>`;
|
|
108
|
+
return html`<div class="ws-foot" role="button" tabindex="0" aria-haspopup="dialog" aria-expanded="false" data-ws-turn=${ordinal} data-ws-sig=${footSig(items)} aria-label=${`Working set of this turn: ${text.replace(/^touched /, "")}. Open the list.`}><span class="w">touched</span> ${parts}${fp.tail ? html`${sep}<span class="w">${fp.tail}</span>` : nothing}${fp.edited ? html`${sep}<span class="e">edited ${fp.edited}</span>` : nothing}${fp.stale ? html`${sep}<span class="s">${fp.stale} stale</span>` : nothing}${fp.forgotten ? html`${sep}<span class="f">${fp.forgotten} forgotten</span>` : nothing}${fp.dropped ? html`${sep}<span class="f">${fp.dropped} dropped</span>` : nothing}</div>`;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
// What a footprint shows that can change after it is drawn: its items'
|
|
@@ -186,7 +186,7 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
|
|
|
186
186
|
|
|
187
187
|
const popover = createChipPopover({
|
|
188
188
|
id: "workingSetPopover", className: "ctx-popover ws-popover", width: 420, el: infoEl, anchor: ".ws-chip",
|
|
189
|
-
openers: ".ws-chip, .ts-ws, .ws-foot", sheet: true,
|
|
189
|
+
openers: ".ws-chip, .ts-ws, .ws-foot", sheet: true, label: "Working set",
|
|
190
190
|
// The chip opens the session's scope; the rail and a footprint set
|
|
191
191
|
// theirs before they open it.
|
|
192
192
|
summary: (anchor) => {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.137.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -802,6 +802,7 @@ files:
|
|
|
802
802
|
- lib/samagotchi/web/public/ctx.js
|
|
803
803
|
- lib/samagotchi/web/public/data.js
|
|
804
804
|
- lib/samagotchi/web/public/delivery_line.js
|
|
805
|
+
- lib/samagotchi/web/public/details_motion.js
|
|
805
806
|
- lib/samagotchi/web/public/diff_view.js
|
|
806
807
|
- lib/samagotchi/web/public/dock_reserve.js
|
|
807
808
|
- lib/samagotchi/web/public/families_view.js
|
|
@@ -842,6 +843,7 @@ files:
|
|
|
842
843
|
- lib/samagotchi/web/public/participant_names.js
|
|
843
844
|
- lib/samagotchi/web/public/participants_panel.js
|
|
844
845
|
- lib/samagotchi/web/public/plain_line.js
|
|
846
|
+
- lib/samagotchi/web/public/popover_modal.js
|
|
845
847
|
- lib/samagotchi/web/public/project_picker.js
|
|
846
848
|
- lib/samagotchi/web/public/prompt_history.js
|
|
847
849
|
- lib/samagotchi/web/public/push.js
|
|
@@ -858,6 +860,7 @@ files:
|
|
|
858
860
|
- lib/samagotchi/web/public/review_history.js
|
|
859
861
|
- lib/samagotchi/web/public/review_start.js
|
|
860
862
|
- lib/samagotchi/web/public/revise_stack.js
|
|
863
|
+
- lib/samagotchi/web/public/revise_switch.js
|
|
861
864
|
- lib/samagotchi/web/public/route.js
|
|
862
865
|
- lib/samagotchi/web/public/scope.js
|
|
863
866
|
- lib/samagotchi/web/public/scroll.js
|
|
@@ -880,6 +883,7 @@ files:
|
|
|
880
883
|
- lib/samagotchi/web/public/stage_slots.js
|
|
881
884
|
- lib/samagotchi/web/public/stage_view.js
|
|
882
885
|
- lib/samagotchi/web/public/stage_watch.js
|
|
886
|
+
- lib/samagotchi/web/public/stale_params.js
|
|
883
887
|
- lib/samagotchi/web/public/start_page.js
|
|
884
888
|
- lib/samagotchi/web/public/start_settings.js
|
|
885
889
|
- lib/samagotchi/web/public/start_toolkit.js
|