samagotchi 0.143.0 → 0.145.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 +71 -1
- data/docs/bundle-authoring.md +1 -1
- data/docs/cli.md +21 -4
- data/docs/configuration.md +1 -1
- data/docs/guardrails.md +6 -3
- data/docs/hooks.md +5 -1
- data/docs/internals/bridge.md +2 -1
- data/docs/internals/bundles.md +1 -1
- data/docs/internals/engine.md +1 -1
- data/docs/internals/web-frontend.md +59 -2
- data/docs/plugins.md +4 -2
- data/docs/releasing.md +3 -2
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bridge/sse_writer.rb +11 -1
- data/lib/samagotchi/bundle_command/check.rb +13 -2
- data/lib/samagotchi/bundle_settings_check.rb +34 -1
- data/lib/samagotchi/bundles/check-in/manifest.yml +4 -4
- data/lib/samagotchi/bundles/check-in/plugin.rb +13 -6
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/cost-cap/manifest.yml +4 -4
- data/lib/samagotchi/bundles/cost-cap/plugin.rb +4 -1
- data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
- data/lib/samagotchi/bundles/github-pr/plugin.rb +4 -2
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +4 -2
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/bundles/work-cap/manifest.yml +4 -4
- data/lib/samagotchi/bundles/work-cap/plugin.rb +17 -8
- data/lib/samagotchi/context_status.rb +1 -1
- data/lib/samagotchi/engine.rb +17 -7
- data/lib/samagotchi/kernel_loop.rb +7 -0
- data/lib/samagotchi/live_versions.rb +28 -2
- data/lib/samagotchi/llm/chat_loop.rb +5 -0
- data/lib/samagotchi/llm/utf8_scrub.rb +5 -3
- data/lib/samagotchi/plugin/api.rb +2 -2
- data/lib/samagotchi/plugin/context.rb +3 -2
- data/lib/samagotchi/plugin/peer.rb +1 -1
- data/lib/samagotchi/plugin_session_state.rb +6 -3
- data/lib/samagotchi/self_report.rb +15 -13
- data/lib/samagotchi/session_access/local.rb +7 -10
- data/lib/samagotchi/session_commands.rb +2 -1
- data/lib/samagotchi/system_one/answer.rb +8 -5
- data/lib/samagotchi/system_one/client.rb +17 -0
- data/lib/samagotchi/system_one/http_backend.rb +23 -10
- data/lib/samagotchi/testing/boundary.rb +76 -0
- data/lib/samagotchi/testing/env_not_shown.rb +17 -0
- data/lib/samagotchi/testing.rb +1 -0
- data/lib/samagotchi/update_command.rb +8 -7
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/annotate_bar.js +1 -1
- data/lib/samagotchi/web/public/app.js +15 -3
- data/lib/samagotchi/web/public/chip_popover.js +14 -4
- data/lib/samagotchi/web/public/composer_fold.js +5 -2
- data/lib/samagotchi/web/public/composer_grip.js +45 -2
- data/lib/samagotchi/web/public/copy.js +1 -1
- data/lib/samagotchi/web/public/index.html +79 -40
- data/lib/samagotchi/web/public/info_bar_model.js +6 -4
- data/lib/samagotchi/web/public/more_sheet.js +212 -0
- data/lib/samagotchi/web/public/question_cards.js +26 -3
- data/lib/samagotchi/web/public/ref_scan.js +4 -2
- data/lib/samagotchi/web/public/stage_frame.js +58 -13
- data/lib/samagotchi/web/public/stage_model.js +2 -1
- data/lib/samagotchi/web/public/stage_view.js +52 -20
- data/lib/samagotchi/web/public/turn_block.js +43 -9
- data/lib/samagotchi/worker.rb +21 -4
- metadata +3 -1
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// The stage view's cap and frame (stage_view.js): the one place that
|
|
2
2
|
// writes the stage's height, and the stage's own scrolls. The cap: at most
|
|
3
|
-
// ~60% of the page (~80% with the cloud open, 85%
|
|
4
|
-
//
|
|
5
|
-
// panel. The frame: held and grow-only while the
|
|
6
|
-
// (stage_model.js frameTarget), its growth eased
|
|
3
|
+
// ~60% of the page (~80% with the cloud open, 85% from a card that needs the
|
|
4
|
+
// room until the next step: a long reply's question, a tall approval), and
|
|
5
|
+
// never above the session panel. The frame: held and grow-only while the
|
|
6
|
+
// stage holds a turn (stage_model.js frameTarget), its growth eased; the next
|
|
7
|
+
// step after an answered card lets it go once (forgetContent). The stage's state comes
|
|
7
8
|
// in as getters (+isHolding+, +isCollapsed+, +isCloudOpen+, +isRoomCap+);
|
|
8
9
|
// what the stage changes of the frame's goes through resetHeld and
|
|
9
|
-
// forgetContent.
|
|
10
|
+
// forgetContent. +waitingCard+: the card blocking the turn, or null.
|
|
10
11
|
import { frameTarget } from "./stage_model.js";
|
|
11
12
|
import { easeScrollTo } from "./scroll.js";
|
|
12
13
|
|
|
@@ -21,6 +22,11 @@ export const STAGE_GAP = 10;
|
|
|
21
22
|
const SHARE = 0.6;
|
|
22
23
|
const CLOUD_SHARE = 0.8;
|
|
23
24
|
const ROOM_SHARE = 0.85;
|
|
25
|
+
// A waiting card read from its top sits this far under the scroller's edge
|
|
26
|
+
// (the scroller's own top padding).
|
|
27
|
+
const CARD_TOP_GAP = 8;
|
|
28
|
+
// A card this much in the stage's view counts as in sight.
|
|
29
|
+
const CARD_SEEN = 40;
|
|
24
30
|
|
|
25
31
|
export function reducedMotion() {
|
|
26
32
|
return matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -28,7 +34,7 @@ export function reducedMotion() {
|
|
|
28
34
|
|
|
29
35
|
// +observed+: the elements whose size changes refit the frame (the stage's
|
|
30
36
|
// slots, in the order they are observed).
|
|
31
|
-
export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, holdEl, asking, extras, dockEl, centerEl, observed, isHolding, isCollapsed, isCloudOpen, isRoomCap = () => false }) {
|
|
37
|
+
export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, holdEl, asking, extras, dockEl, centerEl, observed, isHolding, isCollapsed, isCloudOpen, isRoomCap = () => false, waitingCard = () => null }) {
|
|
32
38
|
// The frame (fitFrame): the height held while the turn runs and the
|
|
33
39
|
// tallest content seen.
|
|
34
40
|
let held = null;
|
|
@@ -53,7 +59,7 @@ export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, h
|
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
// ── the cap: at most ~60% of the page (~80% with the cloud open, 85%
|
|
56
|
-
//
|
|
62
|
+
// while a card has the room), and never above the session
|
|
57
63
|
// panel (the dock is absolute: a tall composer or a phone's keyboard
|
|
58
64
|
// would push the stage's top out) ──────────────────────────────────────
|
|
59
65
|
|
|
@@ -62,11 +68,16 @@ export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, h
|
|
|
62
68
|
// (a waiting card) fit together (the reply reads from its start, the card
|
|
63
69
|
// still in sight). +fromStart+ (the question room's arrival): the asking
|
|
64
70
|
// text's top even when they don't (the card is reached by the stage's
|
|
65
|
-
// jump pill, stage_view.js).
|
|
71
|
+
// jump pill, stage_view.js). A waiting card in +slot+ taller than the
|
|
72
|
+
// view reads from its top, never from its options: the question and what
|
|
73
|
+
// it approves are what the answer is about; the options are a scroll away.
|
|
74
|
+
// null: nothing to show.
|
|
66
75
|
function revealTop(slot, { fromStart = false } = {}) {
|
|
67
76
|
if (stage.hidden || slot.hidden) return null;
|
|
68
77
|
let top = Math.max(scroller.scrollTop, slot.offsetTop + slot.offsetHeight - scroller.clientHeight);
|
|
69
78
|
if (!asking.hidden && asking.offsetTop < top && (fromStart || slot.offsetTop + slot.offsetHeight - asking.offsetTop <= scroller.clientHeight)) top = asking.offsetTop;
|
|
79
|
+
const card = waitingCard();
|
|
80
|
+
if (card && slot.contains(card)) top = Math.min(top, Math.max(0, card.offsetTop - CARD_TOP_GAP));
|
|
70
81
|
return top;
|
|
71
82
|
}
|
|
72
83
|
|
|
@@ -81,6 +92,36 @@ export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, h
|
|
|
81
92
|
else easeStage(top);
|
|
82
93
|
}
|
|
83
94
|
|
|
95
|
+
// Some of the waiting card shows in the stage's view (no card: nothing
|
|
96
|
+
// says otherwise).
|
|
97
|
+
function cardInSight() {
|
|
98
|
+
const card = waitingCard();
|
|
99
|
+
if (!card) return true;
|
|
100
|
+
const top = scroller.scrollTop;
|
|
101
|
+
return card.offsetTop + CARD_SEEN <= top + scroller.clientHeight && card.offsetTop + card.offsetHeight - CARD_SEEN >= top;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// The cap dropped under a waiting card in sight (a phone's keyboard, a
|
|
105
|
+
// rotation): it stays in sight. Someone typing in it keeps their field;
|
|
106
|
+
// while some of it still shows, the reader's scroll is left alone.
|
|
107
|
+
function keepCardInSight() {
|
|
108
|
+
const card = waitingCard();
|
|
109
|
+
const field = document.activeElement;
|
|
110
|
+
if (card?.contains(field) && field.matches('textarea, input:not([type="radio"], [type="checkbox"])')) {
|
|
111
|
+
field.scrollIntoView({ block: "nearest" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!card || !cardInSight()) reveal(card?.parentElement ?? extras, { now: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// +card+ fits the stage's view at the cap now: it, the scroller's padding
|
|
118
|
+
// and the stage's rows around the scroller.
|
|
119
|
+
function fitsCap(card) {
|
|
120
|
+
const css = getComputedStyle(scroller);
|
|
121
|
+
const around = stage.offsetHeight - scroller.offsetHeight + (parseFloat(css.paddingTop) || 0) + (parseFloat(css.paddingBottom) || 0);
|
|
122
|
+
return card.offsetHeight + around <= capPx();
|
|
123
|
+
}
|
|
124
|
+
|
|
84
125
|
function fitCap() {
|
|
85
126
|
if (stage.hidden) return;
|
|
86
127
|
// The composer card and the gap above it (the stage floats over them,
|
|
@@ -91,14 +132,15 @@ export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, h
|
|
|
91
132
|
const cap = Math.max(96, Math.round(Math.min(share, room)));
|
|
92
133
|
const was = capPx();
|
|
93
134
|
if (cap === was) return;
|
|
135
|
+
// Read before the cap changes the view. A reader who scrolled away from
|
|
136
|
+
// the card isn't brought back to it.
|
|
137
|
+
const keep = cap < was && stage.classList.contains("waiting") && cardInSight();
|
|
94
138
|
stage.style.setProperty("--stage-max", `${cap}px`);
|
|
95
139
|
fitFrame();
|
|
96
140
|
// A smaller cap (a phone's keyboard came up) keeps a waiting card in
|
|
97
141
|
// sight: the asking text above it would push it out. A larger one
|
|
98
142
|
// leaves the reader's scroll alone.
|
|
99
|
-
if (
|
|
100
|
-
requestAnimationFrame(() => reveal(extras, { now: true }));
|
|
101
|
-
}
|
|
143
|
+
if (keep) requestAnimationFrame(keepCardInSight);
|
|
102
144
|
}
|
|
103
145
|
// The whole dock, not only its composer card: the info bar under it
|
|
104
146
|
// fills and wraps after a load (the session's facts arrive), and a cap
|
|
@@ -111,7 +153,8 @@ export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, h
|
|
|
111
153
|
// Nothing in the stage view jumps. The rules: the stage is out of the
|
|
112
154
|
// dock's flow (index.html), so its height never moves the composer; while
|
|
113
155
|
// it holds a turn its height is held and grow-only (frameTarget), so a
|
|
114
|
-
// content change happens inside it
|
|
156
|
+
// content change happens inside it (the next step after an answered card
|
|
157
|
+
// lets it go once: forgetContent); growth eases (frameTo); the chip is a
|
|
115
158
|
// footer no content can push; the hand-off releases the turn at once
|
|
116
159
|
// (releaseTurn) and empties the stage only after it slid away under the
|
|
117
160
|
// composer (a transform, then finishFold/clearStage).
|
|
@@ -182,6 +225,7 @@ export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, h
|
|
|
182
225
|
|
|
183
226
|
return {
|
|
184
227
|
fitCap,
|
|
228
|
+
fitsCap,
|
|
185
229
|
frameFree,
|
|
186
230
|
afterGrowth,
|
|
187
231
|
easeStage,
|
|
@@ -189,7 +233,8 @@ export function createStageFrame({ stage, scroller, status, cloud, queuedSlot, h
|
|
|
189
233
|
// A new turn, or none: no height held yet.
|
|
190
234
|
resetHeld: () => { held = null; },
|
|
191
235
|
// The tallest content seen and the height held for it are forgotten (a
|
|
192
|
-
// new turn, the cloud closed by hand
|
|
236
|
+
// new turn, the cloud closed by hand, the next step after an answered
|
|
237
|
+
// card): the frame refits to what shows now.
|
|
193
238
|
forgetContent: () => {
|
|
194
239
|
maxContent = 0;
|
|
195
240
|
held = null;
|
|
@@ -198,7 +198,8 @@ export function liveSlots(turn, { narration = "", thinking = "", pending = false
|
|
|
198
198
|
|
|
199
199
|
// The stage's frame height (px) while it holds a turn: grow-only, so a
|
|
200
200
|
// content change inside it (the plain flip, an answered card, a short
|
|
201
|
-
// answer) never moves its top edge. +
|
|
201
|
+
// answer) never moves its top edge. (Its one shrink forgets +held+: the next
|
|
202
|
+
// step after an answered card, stage_view.js releaseHeldWait.) +natural+: its content's own height;
|
|
202
203
|
// +held+: the height it holds (null: none yet); +cap+: --stage-max. A lower
|
|
203
204
|
// cap (a phone's keyboard) clamps it at once and the clamp is the new held
|
|
204
205
|
// height: a cap that rises again doesn't bring the old one back, only new
|
|
@@ -36,6 +36,10 @@ const FLASH_MS = 4000;
|
|
|
36
36
|
const ANSWER_SEEN = 72;
|
|
37
37
|
// A short answer cut by less than this at its foot isn't scrolled to.
|
|
38
38
|
const ANSWER_CUT = 12;
|
|
39
|
+
// An answered card's one line.
|
|
40
|
+
const ANSWERED_LINE = 40;
|
|
41
|
+
// That line brought back sits this far under the scroller's edge.
|
|
42
|
+
const ANSWERED_GAP = 8;
|
|
39
43
|
const PENDING = ".bubble.question:not(.answered):not(.cancelled), .bubble.continue:not(.resolved)";
|
|
40
44
|
// A running turn's card with actions (check-in's Nudge / Keep going / Stop)
|
|
41
45
|
// asks the user too, but the turn goes on: it waits only while the turn runs
|
|
@@ -45,6 +49,9 @@ const ASKS = ".plugin-card[data-in-turn] .card-action";
|
|
|
45
49
|
// the step limit, a hook's question or a delegate's relayed one): the one
|
|
46
50
|
// card whose long asking text gets the question room.
|
|
47
51
|
const ASK_CARD = '.bubble.question[data-kind="question"]:not([data-relay])';
|
|
52
|
+
// A blocking approval, a delegate's relayed one too: too tall for the stage's
|
|
53
|
+
// view, it gets the question room's raised cap (raiseForApproval).
|
|
54
|
+
const APPROVAL_CARD = '.bubble.question[data-kind="approval"]';
|
|
48
55
|
|
|
49
56
|
function readCollapsed() {
|
|
50
57
|
return readStored(COLLAPSE_KEY) === "1";
|
|
@@ -188,8 +195,9 @@ export function createStageView({
|
|
|
188
195
|
let bare = false;
|
|
189
196
|
let foldTimer = null;
|
|
190
197
|
// The question room (drawRoom): the pending ask card whose asking text
|
|
191
|
-
// overflowed its cap
|
|
192
|
-
//
|
|
198
|
+
// overflowed its cap; and the raised cap (stage_frame.js), which an
|
|
199
|
+
// approval too tall for the stage gets too (raiseForApproval), kept until
|
|
200
|
+
// the next step (releaseHeldWait).
|
|
193
201
|
let roomCard = null;
|
|
194
202
|
let roomCap = false;
|
|
195
203
|
// What the overflow was measured for ({ card, text }): measured once the
|
|
@@ -256,8 +264,8 @@ export function createStageView({
|
|
|
256
264
|
// The question room: while a model's own question waits and its asking
|
|
257
265
|
// text didn't fit its cap, the slot loses its cap and the stage's one
|
|
258
266
|
// scroll reads the whole reply (index.html .question-room); the cap rises
|
|
259
|
-
//
|
|
260
|
-
//
|
|
267
|
+
// until the next step. The card keeps the room until it stops pending
|
|
268
|
+
// (+text+: the asking text drawn).
|
|
261
269
|
function drawRoom(blocked, text) {
|
|
262
270
|
const ask = blocked ? askCard() : null;
|
|
263
271
|
if (roomCard && ask === roomCard) return;
|
|
@@ -290,6 +298,18 @@ export function createStageView({
|
|
|
290
298
|
}
|
|
291
299
|
}
|
|
292
300
|
|
|
301
|
+
// A blocking approval too tall for the stage's view: the same raised cap,
|
|
302
|
+
// the stage takes what the page has. The card still opens at its question
|
|
303
|
+
// and command (stage_frame.js revealTop), so no room class and no pill. The
|
|
304
|
+
// cap is a ceiling: a card that fits never gets here.
|
|
305
|
+
function raiseForApproval(blocked) {
|
|
306
|
+
if (!blocked || roomCap) return;
|
|
307
|
+
const card = extras.querySelector(PENDING) || tail.querySelector(PENDING);
|
|
308
|
+
if (!card?.matches(APPROVAL_CARD) || frame.fitsCap(card)) return;
|
|
309
|
+
roomCap = true;
|
|
310
|
+
frame.fitCap();
|
|
311
|
+
}
|
|
312
|
+
|
|
293
313
|
// The pill: the room's card out of the stage's view, once its arrival
|
|
294
314
|
// reveal has run, the stage open.
|
|
295
315
|
function drawPill() {
|
|
@@ -338,13 +358,13 @@ export function createStageView({
|
|
|
338
358
|
const wasWaiting = stage.classList.contains("waiting");
|
|
339
359
|
if (blocked && !wasWaiting) {
|
|
340
360
|
stage.classList.add("held-wait");
|
|
341
|
-
live.style.minHeight = "";
|
|
342
361
|
}
|
|
343
362
|
if (!blocked && wasWaiting) stage.style.setProperty("--live-held", `${live.offsetHeight}px`);
|
|
344
363
|
stage.classList.toggle("plain", plain);
|
|
345
364
|
stage.classList.toggle("ended", !!endKind);
|
|
346
365
|
cancel.hidden = !onCancel || !!endKind;
|
|
347
366
|
stage.classList.toggle("waiting", blocked);
|
|
367
|
+
if (!blocked && wasWaiting) settleAnsweredCard();
|
|
348
368
|
stage.classList.toggle("asking", !!slots.asking);
|
|
349
369
|
stage.dataset.phase = slots.phase;
|
|
350
370
|
mark.textContent = MARKS[slots.phase] || "";
|
|
@@ -353,6 +373,7 @@ export function createStageView({
|
|
|
353
373
|
slotsView.drawHeadline(slots.headline);
|
|
354
374
|
slotsView.drawAsking(slots.asking, slots.askingRefs);
|
|
355
375
|
drawRoom(blocked, slots.asking);
|
|
376
|
+
raiseForApproval(blocked);
|
|
356
377
|
drawPill();
|
|
357
378
|
slotsView.drawNow(slots.tool, slots.quiet);
|
|
358
379
|
slotsView.drawTrail(slots.trail, flash);
|
|
@@ -386,6 +407,20 @@ export function createStageView({
|
|
|
386
407
|
tail.hidden = !tail.firstChild;
|
|
387
408
|
}
|
|
388
409
|
|
|
410
|
+
// A card stopped waiting and its slot has its cap and own scroll again: the
|
|
411
|
+
// slot's end shows (the answered card's line). The stage's scroll stays, so
|
|
412
|
+
// nothing moves at the tap (the frame holds, and the blank goes at the next
|
|
413
|
+
// step), unless the reader had scrolled down the tall card past its line:
|
|
414
|
+
// then the line comes back, to the view's top and no further.
|
|
415
|
+
function settleAnsweredCard() {
|
|
416
|
+
const slot = tail.firstChild ? tail : extras;
|
|
417
|
+
if (!slot.firstChild) return;
|
|
418
|
+
slot.scrollTop = slot.scrollHeight;
|
|
419
|
+
const end = slot.offsetTop + slot.offsetHeight;
|
|
420
|
+
if (scroller.scrollTop <= end - ANSWERED_LINE) return;
|
|
421
|
+
scroller.scrollTop = Math.max(0, Math.min(slot.lastElementChild.offsetTop, end - ANSWERED_LINE) - ANSWERED_GAP);
|
|
422
|
+
}
|
|
423
|
+
|
|
389
424
|
// The prompt's first words (the working set's session tags say whose
|
|
390
425
|
// turn a step was).
|
|
391
426
|
function promptWords() {
|
|
@@ -393,23 +428,18 @@ export function createStageView({
|
|
|
393
428
|
return text.length > 60 ? `${text.slice(0, 59)}\u2026` : text;
|
|
394
429
|
}
|
|
395
430
|
|
|
396
|
-
// The next step
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
431
|
+
// The next step after a blocking card's answer: the one time a turn's
|
|
432
|
+
// frame shrinks (stage_frame.js forgetContent; grow-only otherwise). What
|
|
433
|
+
// the wait held goes together: the live slots' waiting height, a raised
|
|
434
|
+
// cap, the tallest content seen. +held-wait+ says it is due, so a later
|
|
435
|
+
// step, or a generation with the card still waiting, changes nothing.
|
|
401
436
|
function releaseHeldWait() {
|
|
402
|
-
|
|
403
|
-
const before = held ? live.offsetHeight : 0;
|
|
404
|
-
const top = scroller.scrollTop;
|
|
437
|
+
if (!stage.classList.contains("held-wait") || stage.classList.contains("waiting")) return;
|
|
405
438
|
stage.classList.remove("held-wait");
|
|
406
439
|
stage.style.removeProperty("--live-held");
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const absorbed = Math.min(Math.max(0, before - after), top);
|
|
411
|
-
if (before - absorbed > after) live.style.minHeight = `${before - absorbed}px`;
|
|
412
|
-
if (absorbed) scroller.scrollTop = top - absorbed;
|
|
440
|
+
roomCap = false;
|
|
441
|
+
frame.fitCap();
|
|
442
|
+
frame.forgetContent();
|
|
413
443
|
}
|
|
414
444
|
|
|
415
445
|
// One line in the trail for FLASH_MS, ahead of the calls (a newer flash
|
|
@@ -451,6 +481,8 @@ export function createStageView({
|
|
|
451
481
|
revealed = false;
|
|
452
482
|
writeStored(COLLAPSE_KEY, value ? "1" : "0");
|
|
453
483
|
applyCollapsed();
|
|
484
|
+
// Opened on a waiting card: it shows as it does on arrival.
|
|
485
|
+
if (!value && pendingCard()) frame.afterGrowth(() => frame.reveal(tail.querySelector(PENDING) ? tail : extras));
|
|
454
486
|
}
|
|
455
487
|
|
|
456
488
|
function setCloud(open) {
|
|
@@ -546,6 +578,7 @@ export function createStageView({
|
|
|
546
578
|
isCollapsed: () => collapsed,
|
|
547
579
|
isCloudOpen: () => cloudOpen,
|
|
548
580
|
isRoomCap: () => roomCap,
|
|
581
|
+
waitingCard: () => (stage.classList.contains("waiting") ? extras.querySelector(PENDING) || tail.querySelector(PENDING) : null),
|
|
549
582
|
});
|
|
550
583
|
|
|
551
584
|
// The in-use watch (stage_watch.js): an ended turn hands off once the
|
|
@@ -608,7 +641,6 @@ export function createStageView({
|
|
|
608
641
|
setCloud(false);
|
|
609
642
|
stage.classList.remove("plain", "ended", "waiting", "held-wait", "asking", "in-use", "question-room");
|
|
610
643
|
stage.style.removeProperty("--live-held");
|
|
611
|
-
live.style.minHeight = "";
|
|
612
644
|
}
|
|
613
645
|
|
|
614
646
|
// The turn leaves the stage's care (the hand-off's start, a reset, a new
|
|
@@ -45,6 +45,9 @@ import { groupGaps, timeGapTemplate } from "./time_gaps.js";
|
|
|
45
45
|
|
|
46
46
|
// ---- The templates: one step, its thinking, its label ----
|
|
47
47
|
|
|
48
|
+
// Streaming text reaches the page at most this often (docs/internals/web-frontend.md, Frames).
|
|
49
|
+
const FLUSH_MS = 100;
|
|
50
|
+
|
|
48
51
|
// A closed step's summary (model.genLabel). When its head is the words the
|
|
49
52
|
// body starts with (the narration's first line, the first call's title)
|
|
50
53
|
// and it has calls, the summary is marked .repeats and its parts are
|
|
@@ -218,7 +221,11 @@ export function createTurnBlock({
|
|
|
218
221
|
// Steer rows waiting for the step that answers them (steer()).
|
|
219
222
|
let pendingSteers = [];
|
|
220
223
|
const dirty = new Set();
|
|
224
|
+
// A flush is pending: a timer running (flushTimer) or a frame requested.
|
|
221
225
|
let rafPending = false;
|
|
226
|
+
let flushTimer = null;
|
|
227
|
+
// When the last flush wrote something (performance.now()).
|
|
228
|
+
let lastFlushAt = -Infinity;
|
|
222
229
|
|
|
223
230
|
function removeHint() {
|
|
224
231
|
const hint = historyEl.querySelector(".hint");
|
|
@@ -388,9 +395,26 @@ export function createTurnBlock({
|
|
|
388
395
|
else addSentences(entry, entry.feed.feed(gen.text));
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
function cancelTimer() {
|
|
399
|
+
clearTimeout(flushTimer);
|
|
400
|
+
flushTimer = null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Nothing pending from here on. A frame already requested can't be taken
|
|
404
|
+
// back this way: it runs flush with nothing to write.
|
|
405
|
+
function cancelFlush() {
|
|
406
|
+
cancelTimer();
|
|
407
|
+
dirty.clear();
|
|
408
|
+
rafPending = false;
|
|
409
|
+
}
|
|
410
|
+
|
|
391
411
|
function flush() {
|
|
392
412
|
rafPending = false;
|
|
393
|
-
|
|
413
|
+
cancelTimer();
|
|
414
|
+
// A late frame with nothing to write (a direct flush, a turn's end came
|
|
415
|
+
// first) doesn't count: it would hold back the next first token.
|
|
416
|
+
if (dirty.size) lastFlushAt = performance.now();
|
|
417
|
+
// Runs up to every FLUSH_MS while streaming — must not yank a user who
|
|
394
418
|
// scrolled up to read; only follow when they were already near the bottom.
|
|
395
419
|
const stick = isNearBottom();
|
|
396
420
|
for (const gen of dirty) writeGen(gen, { follow: true });
|
|
@@ -403,7 +427,16 @@ export function createTurnBlock({
|
|
|
403
427
|
dirty.add(gen);
|
|
404
428
|
if (rafPending) return;
|
|
405
429
|
rafPending = true;
|
|
406
|
-
|
|
430
|
+
const wait = lastFlushAt + FLUSH_MS - performance.now();
|
|
431
|
+
if (wait <= 0) {
|
|
432
|
+
requestAnimationFrame(flush);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
// Still on a frame after the timer: a hidden tab's flush pauses, as it always did.
|
|
436
|
+
flushTimer = setTimeout(() => {
|
|
437
|
+
flushTimer = null;
|
|
438
|
+
requestAnimationFrame(flush);
|
|
439
|
+
}, wait);
|
|
407
440
|
}
|
|
408
441
|
|
|
409
442
|
// A closed gen: its final text, collapsed to its label (unless the user
|
|
@@ -626,8 +659,7 @@ export function createTurnBlock({
|
|
|
626
659
|
}
|
|
627
660
|
kept.forEach((el) => el.remove());
|
|
628
661
|
}
|
|
629
|
-
|
|
630
|
-
rafPending = false;
|
|
662
|
+
cancelFlush();
|
|
631
663
|
onChange();
|
|
632
664
|
return kept;
|
|
633
665
|
}
|
|
@@ -655,8 +687,8 @@ export function createTurnBlock({
|
|
|
655
687
|
for (const entry of genEls.values()) stopTicker(entry);
|
|
656
688
|
genEls.clear();
|
|
657
689
|
pendingSteers = [];
|
|
658
|
-
|
|
659
|
-
|
|
690
|
+
cancelFlush();
|
|
691
|
+
lastFlushAt = -Infinity;
|
|
660
692
|
}
|
|
661
693
|
|
|
662
694
|
return {
|
|
@@ -682,7 +714,9 @@ export function createTurnBlock({
|
|
|
682
714
|
if (!text && !thinking) return;
|
|
683
715
|
if (!turn) this.turnStarted({});
|
|
684
716
|
const r = model.chunk(turn, { text, thinking, iteration, now });
|
|
685
|
-
|
|
717
|
+
// A step's edge shows now; the text, and the stage's refresh that goes
|
|
718
|
+
// with it, wait for the flush (apply's onChange would refresh per frame).
|
|
719
|
+
if (r.opened || r.closed) apply(r);
|
|
686
720
|
scheduleFlush(r.gen);
|
|
687
721
|
},
|
|
688
722
|
// A dropped stream's step asked again (generation_retrying with
|
|
@@ -704,8 +738,8 @@ export function createTurnBlock({
|
|
|
704
738
|
if (!turn) return;
|
|
705
739
|
const r = model.generationCompleted(turn);
|
|
706
740
|
if (!r) return;
|
|
707
|
-
// The text lands
|
|
708
|
-
//
|
|
741
|
+
// The text lands at the next flush; a generation can end before one
|
|
742
|
+
// runs (inside FLUSH_MS, a hidden tab).
|
|
709
743
|
writeGen(r.gen);
|
|
710
744
|
// The remainder is the step's last line: narration without a final
|
|
711
745
|
// period ("Let me check the shell first") would otherwise never show.
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -81,8 +81,11 @@ module Samagotchi
|
|
|
81
81
|
# gets it over the Bridge a moment after it starts, and that message's
|
|
82
82
|
# turn goes first (the reports join it).
|
|
83
83
|
WAKE_START_GRACE = 2.0
|
|
84
|
-
# How much of a command's output goes into its :command_ran.
|
|
85
|
-
|
|
84
|
+
# How much of a command's output goes into its :command_ran, in chars.
|
|
85
|
+
# Room for a real listing (/guardrails with a bundle's rules, /models):
|
|
86
|
+
# the event lives only in the Bridge's ring (256 events, in memory) and
|
|
87
|
+
# on the streams, a session file doesn't hold it.
|
|
88
|
+
COMMAND_OUTPUT_LIMIT = 64 * 1024
|
|
86
89
|
BUSY_OUTPUT = "busy: wait for the turn to end"
|
|
87
90
|
# /exit held by background tasks a finish wake waits for: how to end them.
|
|
88
91
|
TASK_STOP_HINT = "stop one with the web's stop-task button or by asking for task_stop"
|
|
@@ -1211,8 +1214,10 @@ module Samagotchi
|
|
|
1211
1214
|
# line that applies it (the TUI's prefill, the web's Apply button)
|
|
1212
1215
|
def announce_command(command, status:, output:, changed:, anytime: false, confirm: nil)
|
|
1213
1216
|
TurnCheckpoint.record_command_outcome(@session_dir, command.enqueued_id, status: status)
|
|
1214
|
-
|
|
1215
|
-
|
|
1217
|
+
# Any bytes (a !cmd's): one that JSON can't take would end every stream
|
|
1218
|
+
# that reaches this event, for as long as the ring holds it.
|
|
1219
|
+
text = output.to_s.scrub("?")
|
|
1220
|
+
event = { type: :command_ran, **command.event_fields, status: status, output: command_output_shown(text),
|
|
1216
1221
|
changed: changed.map(&:to_s), model_name: @engine.effective_model_name }
|
|
1217
1222
|
event[:anytime] = true if anytime
|
|
1218
1223
|
# The session's title after /title (nil: cleared), for the TUIs' row.
|
|
@@ -1228,6 +1233,18 @@ module Samagotchi
|
|
|
1228
1233
|
@engine.announce(event)
|
|
1229
1234
|
end
|
|
1230
1235
|
|
|
1236
|
+
# +text+ as its :command_ran carries it: over the limit, its head to a
|
|
1237
|
+
# line's end (one long line is cut where it must be), then a line that
|
|
1238
|
+
# says so: every UI prints this text as it is.
|
|
1239
|
+
def command_output_shown(text)
|
|
1240
|
+
return text if text.length <= COMMAND_OUTPUT_LIMIT
|
|
1241
|
+
|
|
1242
|
+
kept = text[0, COMMAND_OUTPUT_LIMIT]
|
|
1243
|
+
line_end = kept.rindex("\n")
|
|
1244
|
+
kept = kept[0, line_end] if line_end && line_end > COMMAND_OUTPUT_LIMIT / 2
|
|
1245
|
+
"#{kept}\n[cut: #{kept.length} of #{text.length} chars: a command's output stops here]"
|
|
1246
|
+
end
|
|
1247
|
+
|
|
1231
1248
|
# One Engine turn, the same for a prompt, a reminder and a continue:
|
|
1232
1249
|
# the session shows as running to readers of the file (the web's
|
|
1233
1250
|
# session list; the Engine resets it to idle when it ends), commands
|
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.145.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -682,6 +682,7 @@ files:
|
|
|
682
682
|
- lib/samagotchi/testing.rb
|
|
683
683
|
- lib/samagotchi/testing/boundary.rb
|
|
684
684
|
- lib/samagotchi/testing/bundle.rb
|
|
685
|
+
- lib/samagotchi/testing/env_not_shown.rb
|
|
685
686
|
- lib/samagotchi/testing/fake_executable.rb
|
|
686
687
|
- lib/samagotchi/testing/fake_executable_launcher
|
|
687
688
|
- lib/samagotchi/testing/fake_http.rb
|
|
@@ -865,6 +866,7 @@ files:
|
|
|
865
866
|
- lib/samagotchi/web/public/method_api.js
|
|
866
867
|
- lib/samagotchi/web/public/model_pick.js
|
|
867
868
|
- lib/samagotchi/web/public/model_picker.js
|
|
869
|
+
- lib/samagotchi/web/public/more_sheet.js
|
|
868
870
|
- lib/samagotchi/web/public/notice_links.js
|
|
869
871
|
- lib/samagotchi/web/public/notifications.js
|
|
870
872
|
- lib/samagotchi/web/public/notify.js
|