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
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// The stage's waiting cards as data (stage_view.js, stage_asking.js): what
|
|
2
|
+
// waits in the stage, keyed by the card's id, never by its node (card.js
|
|
3
|
+
// drawCard may swap a plugin card's DIV for a DETAILS; a question card is
|
|
4
|
+
// drawn anew on a resync). Pure: no DOM but the key and kind read off a card
|
|
5
|
+
// as it arrives.
|
|
6
|
+
//
|
|
7
|
+
// Its feed: +arrive+ from stage_view.js place (a card the stage keeps),
|
|
8
|
+
// +settle+ from question_cards.js's resolve path (onSettled: answered,
|
|
9
|
+
// cancelled, replaced, removed; the continue card resolved or dropped), from
|
|
10
|
+
// cards_place.js upsertCard for a plugin card redrawn without its actions,
|
|
11
|
+
// and from stage_view.js turnEnded for the warn cards leaving with the
|
|
12
|
+
// block; +reset+ from stage_view.js emptySlots. A second feed only settles:
|
|
13
|
+
// +reconcile+ (stage_asking.js: at each refresh, and before the fold, the
|
|
14
|
+
// blocking read and an opened row) drops an entry whose card
|
|
15
|
+
// is gone or waits no more, for a card answered outside question_cards.js
|
|
16
|
+
// (the stage's unit tests answer a card by its classes alone). It never
|
|
17
|
+
// adds or docks: arrive is the one way in.
|
|
18
|
+
//
|
|
19
|
+
// A card's entry: its kind (KINDS), docked (it shows alone in the box above
|
|
20
|
+
// the composer, its turn opened in the history: stage_asking.js docks; its
|
|
21
|
+
// lead-in bubble is stage_opened.js's, by the same key), whether it is an
|
|
22
|
+
// approval (a delegate's relayed one too: raiseForApproval) and its arrival
|
|
23
|
+
// order. The order the stage shows them in: blocking cards first, then the
|
|
24
|
+
// asking ones (and the warnings), each in arrival order, so a live page and
|
|
25
|
+
// a reload agree whatever order the replay brings them in. One docked card
|
|
26
|
+
// shows at a time (+shown+: the one the pager is on, else the first). The
|
|
27
|
+
// fold (the docked card's own, stage_asking.js) is the folded card's key and
|
|
28
|
+
// how many cards waited when it was folded.
|
|
29
|
+
import { ASKS, PENDING, WARN_CARD } from "./stage_cards.js";
|
|
30
|
+
|
|
31
|
+
// The kinds: what blocks the turn (BLOCKING), a running turn's card with
|
|
32
|
+
// actions (check-in: "asks"), a plugin's warning ("warn": stays in sight,
|
|
33
|
+
// waits for nobody).
|
|
34
|
+
export const KINDS = ["question", "approval", "relay", "hook", "continue", "asks", "warn"];
|
|
35
|
+
const BLOCKING = new Set(["question", "approval", "relay", "hook", "continue"]);
|
|
36
|
+
// +kind+ stops the turn until the user answers.
|
|
37
|
+
export const isBlocking = (kind) => BLOCKING.has(kind);
|
|
38
|
+
// An approval card (question_cards.js data-kind), a delegate's relayed one too.
|
|
39
|
+
const APPROVAL_CARD = '.bubble.question[data-kind="approval"]';
|
|
40
|
+
export const isApprovalCard = (el) => !!el?.matches?.(APPROVAL_CARD);
|
|
41
|
+
|
|
42
|
+
// The old continue card has no id: one at a time.
|
|
43
|
+
export const CONTINUE_KEY = "continue";
|
|
44
|
+
|
|
45
|
+
// A question card's key (its data-qid), a plugin card's (its data-card-id).
|
|
46
|
+
export const questionKey = (qid) => (qid == null ? null : `q:${qid}`);
|
|
47
|
+
export const pluginCardKey = (id) => (id == null || id === "" ? null : `c:${id}`);
|
|
48
|
+
|
|
49
|
+
// +el+'s key, null: not a card the stage keeps.
|
|
50
|
+
export function waitingKey(el) {
|
|
51
|
+
if (!el?.matches) return null;
|
|
52
|
+
if (el.matches(".bubble.question")) return questionKey(el.dataset.qid);
|
|
53
|
+
if (el.matches(".bubble.continue")) return CONTINUE_KEY;
|
|
54
|
+
if (el.matches(".plugin-card")) return pluginCardKey(el.dataset.cardId);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// +el+'s kind (KINDS), null: none (it doesn't wait in the stage).
|
|
59
|
+
export function waitingKind(el) {
|
|
60
|
+
if (!el?.matches) return null;
|
|
61
|
+
if (el.matches(PENDING)) {
|
|
62
|
+
if (el.matches(".bubble.continue, .step-limit") || el.dataset.kind === "continue") return "continue";
|
|
63
|
+
if (el.dataset.relay) return "relay";
|
|
64
|
+
if (el.dataset.kind === "approval") return "approval";
|
|
65
|
+
if (el.dataset.kind === "hook") return "hook";
|
|
66
|
+
return "question";
|
|
67
|
+
}
|
|
68
|
+
if (el.querySelector?.(ASKS)) return "asks";
|
|
69
|
+
if (el.matches(WARN_CARD)) return "warn";
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// +root+'s child card with +key+ (the stage's extras or tail), null: none.
|
|
74
|
+
export function cardIn(root, key) {
|
|
75
|
+
if (!root || !key) return null;
|
|
76
|
+
return [...root.children].find((el) => waitingKey(el) === key) || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// +kind+'s rank in the stage's order: blocking cards before the rest.
|
|
80
|
+
const rank = (kind) => (BLOCKING.has(kind) ? 0 : 1);
|
|
81
|
+
|
|
82
|
+
export function createStageWaiting() {
|
|
83
|
+
const cards = new Map();
|
|
84
|
+
let seq = 0;
|
|
85
|
+
let folded = null;
|
|
86
|
+
let foldedWith = 0;
|
|
87
|
+
// The docked card the pager is on (null: the first).
|
|
88
|
+
let paged = null;
|
|
89
|
+
|
|
90
|
+
const entries = () => [...cards.entries()].sort((a, b) => rank(a[1].kind) - rank(b[1].kind) || a[1].order - b[1].order);
|
|
91
|
+
const dockedKeys = () => entries().filter(([, e]) => e.docked).map(([key]) => key);
|
|
92
|
+
|
|
93
|
+
// Cards that wait for the user: the blocking ones, with +asks+ the asking
|
|
94
|
+
// ones too.
|
|
95
|
+
function count({ asks = false } = {}) {
|
|
96
|
+
let n = 0;
|
|
97
|
+
for (const { kind } of cards.values()) if (BLOCKING.has(kind) || (asks && kind === "asks")) n += 1;
|
|
98
|
+
return n;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
// +key+ came to the stage (again: its kind and flags change, its place in
|
|
103
|
+
// the order stays).
|
|
104
|
+
arrive(key, { kind, docked = false, approval = false } = {}) {
|
|
105
|
+
if (!key || !KINDS.includes(kind)) return;
|
|
106
|
+
const was = cards.get(key);
|
|
107
|
+
// A new card goes in front of what the pager was on.
|
|
108
|
+
if (!was) paged = null;
|
|
109
|
+
cards.set(key, {
|
|
110
|
+
kind, docked: !!docked, approval: !!approval, order: was ? was.order : seq++,
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
// +key+ stopped waiting (+only+: when it is that kind).
|
|
114
|
+
settle(key, { only = null } = {}) {
|
|
115
|
+
const was = cards.get(key);
|
|
116
|
+
if (!was || (only && was.kind !== only)) return false;
|
|
117
|
+
cards.delete(key);
|
|
118
|
+
return true;
|
|
119
|
+
},
|
|
120
|
+
// The stage let its turn go: nothing waits, nothing is folded.
|
|
121
|
+
reset() {
|
|
122
|
+
cards.clear();
|
|
123
|
+
seq = 0;
|
|
124
|
+
folded = null;
|
|
125
|
+
foldedWith = 0;
|
|
126
|
+
paged = null;
|
|
127
|
+
},
|
|
128
|
+
// The second feed: every entry +waits+(key, kind) says no to settles.
|
|
129
|
+
reconcile(waits) {
|
|
130
|
+
for (const [key, { kind }] of [...cards.entries()]) if (!waits(key, kind)) cards.delete(key);
|
|
131
|
+
},
|
|
132
|
+
has: (key) => cards.has(key),
|
|
133
|
+
get: (key) => (cards.has(key) ? { key, ...cards.get(key) } : null),
|
|
134
|
+
// The docked cards' keys, in the stage's order.
|
|
135
|
+
docked: dockedKeys,
|
|
136
|
+
// The docked card the stage shows: the one the pager is on while it is
|
|
137
|
+
// docked, else the first (blocking before asking); null: none.
|
|
138
|
+
shown() {
|
|
139
|
+
const keys = dockedKeys();
|
|
140
|
+
return keys.includes(paged) ? paged : keys[0] ?? null;
|
|
141
|
+
},
|
|
142
|
+
// The pager: the docked card +by+ places from the shown one (clamped).
|
|
143
|
+
// The shown key, or null.
|
|
144
|
+
page(by) {
|
|
145
|
+
const keys = dockedKeys();
|
|
146
|
+
if (!keys.length) return null;
|
|
147
|
+
const at = Math.max(0, keys.indexOf(keys.includes(paged) ? paged : keys[0]));
|
|
148
|
+
paged = keys[Math.min(keys.length - 1, Math.max(0, at + by))];
|
|
149
|
+
return paged;
|
|
150
|
+
},
|
|
151
|
+
// The card in front: the first blocking one, else the first asking one;
|
|
152
|
+
// null: none.
|
|
153
|
+
front() {
|
|
154
|
+
const list = entries();
|
|
155
|
+
const hit = list.find(([, e]) => BLOCKING.has(e.kind)) || list.find(([, e]) => e.kind === "asks");
|
|
156
|
+
return hit ? hit[0] : null;
|
|
157
|
+
},
|
|
158
|
+
count,
|
|
159
|
+
// The blocking cards' keys, in order.
|
|
160
|
+
blocking: () => entries().filter(([, e]) => BLOCKING.has(e.kind)).map(([key]) => key),
|
|
161
|
+
// Every entry in the stage's order (the tests' and the harness's read).
|
|
162
|
+
list: () => entries().map(([key, e]) => ({ key, ...e })),
|
|
163
|
+
// The asking form's fold: +key+ folded by hand (null: unfolded), with
|
|
164
|
+
// the blocking cards that wait now.
|
|
165
|
+
fold(key) {
|
|
166
|
+
folded = key ?? null;
|
|
167
|
+
foldedWith = count();
|
|
168
|
+
},
|
|
169
|
+
// The fold as it stands with +asking+ (the asking form's card key) now:
|
|
170
|
+
// a card answered meanwhile lowers the mark, one more waiting than at
|
|
171
|
+
// the fold drops it, and so does another form. The folded key, or null.
|
|
172
|
+
trackFold(asking) {
|
|
173
|
+
const now = count();
|
|
174
|
+
foldedWith = Math.min(foldedWith, now);
|
|
175
|
+
if (folded && (folded !== asking || now > foldedWith)) folded = null;
|
|
176
|
+
return folded;
|
|
177
|
+
},
|
|
178
|
+
folded: () => folded,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -417,7 +417,7 @@ export function createStreamHandlers(deps) {
|
|
|
417
417
|
const pq = data.pending_question;
|
|
418
418
|
if (!pq || !pq.id) return;
|
|
419
419
|
// A plugin's refs on the reply that asks: on its step before the card
|
|
420
|
-
// draws
|
|
420
|
+
// draws its lead-in bubble.
|
|
421
421
|
if (pq.refs) turnDom.questionRefs(pq.refs);
|
|
422
422
|
questionCards.renderQuestion(pq);
|
|
423
423
|
},
|
|
@@ -176,7 +176,7 @@ export function askingText(turn) {
|
|
|
176
176
|
|
|
177
177
|
// A plugin's refs on the reply that asks (a question's refs, from its
|
|
178
178
|
// pending question): kept on the asking step's gen, the last one, while
|
|
179
|
-
// the turn goes on, for the
|
|
179
|
+
// the turn goes on, for the lead-in bubble (askingRefs) and the
|
|
180
180
|
// step's closed text (turn_block.js). A non-array or an empty list keeps
|
|
181
181
|
// what the gen has. The gen, or null.
|
|
182
182
|
export function questionRefs(turn, refs) {
|
|
@@ -110,16 +110,18 @@ module Samagotchi
|
|
|
110
110
|
# instead of taking the seq as its own. Only the snapshot has it.
|
|
111
111
|
last_event_id: turn_snapshot && turn_snapshot["event_id"],
|
|
112
112
|
timing: timing,
|
|
113
|
-
**objectives_field(id, raw_messages)
|
|
113
|
+
**objectives_field(id, raw_messages, turn_snapshot)
|
|
114
114
|
})
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
# The session's list (the page's strip), and no key with no list: a
|
|
118
118
|
# session without one reads as it always did. A snapshot's entries have
|
|
119
119
|
# string keys (paired_runs reads symbols).
|
|
120
|
-
def objectives_field(id, messages)
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
def objectives_field(id, messages, live)
|
|
121
|
+
view = live_key_or_saved(live, "objectives") do
|
|
122
|
+
symbols = Array(messages).map { |message| message.is_a?(Hash) ? message.transform_keys(&:to_sym) : message }
|
|
123
|
+
Samagotchi::Objectives.view(id, symbols, state_dir: default_state_dir)
|
|
124
|
+
end
|
|
123
125
|
view ? { objectives: view } : {}
|
|
124
126
|
end
|
|
125
127
|
|
|
@@ -180,6 +182,14 @@ module Samagotchi
|
|
|
180
182
|
live ? live[key] : yield
|
|
181
183
|
end
|
|
182
184
|
|
|
185
|
+
# Use the worker's value when its snapshot has the key (nil included);
|
|
186
|
+
# a key missing from an older worker falls back to the saved session.
|
|
187
|
+
def live_key_or_saved(live, key)
|
|
188
|
+
return live[key] if live&.key?(key)
|
|
189
|
+
|
|
190
|
+
yield
|
|
191
|
+
end
|
|
192
|
+
|
|
183
193
|
# The file's question only while a worker owns the session: one a
|
|
184
194
|
# dead worker saved can't be answered (its turn is gone), nor a
|
|
185
195
|
# one-shot's or an older chi's REPL's (they share nothing with the web).
|
data/lib/samagotchi/worker.rb
CHANGED
|
@@ -2042,13 +2042,12 @@ module Samagotchi
|
|
|
2042
2042
|
Log.info(:worker, "idle_exit", idle_s: @idle_exit.idle_seconds.round)
|
|
2043
2043
|
end
|
|
2044
2044
|
|
|
2045
|
-
#
|
|
2046
|
-
#
|
|
2047
|
-
# doesn't either: it is no owner's input.
|
|
2045
|
+
# The owner's input (ClientId.owner?) un-archives the session; a
|
|
2046
|
+
# delegate's, a plugin's, a reminder's or a participant's doesn't.
|
|
2048
2047
|
# It also resets the wakes (budget and pause); input typed in the web or
|
|
2049
2048
|
# a TUI the peer wakes too (chi send doesn't: a peer can run it).
|
|
2050
2049
|
def user_input(client_id)
|
|
2051
|
-
return unless ClientId.
|
|
2050
|
+
return unless ClientId.owner?(client_id)
|
|
2052
2051
|
|
|
2053
2052
|
ArchiveStore.user_input(@session_id, state_dir: @state_dir)
|
|
2054
2053
|
@wakes.human_input!(interactive: ClientId.interactive?(client_id))
|
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.169.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -703,6 +703,7 @@ files:
|
|
|
703
703
|
- lib/samagotchi/session_row.rb
|
|
704
704
|
- lib/samagotchi/session_setting.rb
|
|
705
705
|
- lib/samagotchi/session_setup.rb
|
|
706
|
+
- lib/samagotchi/session_stores.rb
|
|
706
707
|
- lib/samagotchi/sessions_command.rb
|
|
707
708
|
- lib/samagotchi/setting_explained.rb
|
|
708
709
|
- lib/samagotchi/share_command.rb
|
|
@@ -1044,6 +1045,7 @@ files:
|
|
|
1044
1045
|
- lib/samagotchi/web/public/settings_panel.js
|
|
1045
1046
|
- lib/samagotchi/web/public/shell_command.js
|
|
1046
1047
|
- lib/samagotchi/web/public/shown_texts.js
|
|
1048
|
+
- lib/samagotchi/web/public/stage_asking.js
|
|
1047
1049
|
- lib/samagotchi/web/public/stage_cards.js
|
|
1048
1050
|
- lib/samagotchi/web/public/stage_frame.js
|
|
1049
1051
|
- lib/samagotchi/web/public/stage_model.js
|
|
@@ -1051,6 +1053,7 @@ files:
|
|
|
1051
1053
|
- lib/samagotchi/web/public/stage_slots.js
|
|
1052
1054
|
- lib/samagotchi/web/public/stage_stall.js
|
|
1053
1055
|
- lib/samagotchi/web/public/stage_view.js
|
|
1056
|
+
- lib/samagotchi/web/public/stage_waiting.js
|
|
1054
1057
|
- lib/samagotchi/web/public/stage_watch.js
|
|
1055
1058
|
- lib/samagotchi/web/public/stale_params.js
|
|
1056
1059
|
- lib/samagotchi/web/public/start_page.js
|