samagotchi 0.154.0 → 0.155.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.
Files changed (138) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -1
  3. data/docs/architecture.md +3 -2
  4. data/docs/bundles.md +21 -0
  5. data/docs/cli.md +10 -8
  6. data/docs/guardrails.md +2 -2
  7. data/docs/hooks.md +1 -1
  8. data/docs/internals/anthropic.md +4 -4
  9. data/docs/internals/attached-context.md +8 -6
  10. data/docs/internals/block-renderers.md +8 -1
  11. data/docs/internals/bridge.md +18 -5
  12. data/docs/internals/engine.md +221 -100
  13. data/docs/internals/inline-widgets.md +136 -0
  14. data/docs/internals/objectives.md +8 -6
  15. data/docs/internals/participants.md +5 -5
  16. data/docs/internals/prompt-caching.md +12 -6
  17. data/docs/internals/web-frontend.md +2 -2
  18. data/docs/internals/working-set.md +2 -1
  19. data/docs/plugins.md +2 -0
  20. data/docs/releasing.md +1 -1
  21. data/docs/sessions.md +3 -3
  22. data/docs/testing.md +16 -1
  23. data/lib/samagotchi/answer_text.rb +1 -2
  24. data/lib/samagotchi/block_renderers.rb +5 -0
  25. data/lib/samagotchi/bridge/card_places.rb +56 -7
  26. data/lib/samagotchi/bridge/card_store.rb +24 -9
  27. data/lib/samagotchi/bridge.rb +9 -0
  28. data/lib/samagotchi/bridge_client.rb +9 -5
  29. data/lib/samagotchi/bundle_command/check.rb +1 -1
  30. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  31. data/lib/samagotchi/bundles/btw/manifest.yml +2 -2
  32. data/lib/samagotchi/bundles/btw/plugin.rb +1 -1
  33. data/lib/samagotchi/bundles/coordinator/manifest.yml +2 -2
  34. data/lib/samagotchi/bundles/coordinator/plugin.rb +3 -2
  35. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  36. data/lib/samagotchi/bundles/github-pr/manifest.yml +2 -2
  37. data/lib/samagotchi/bundles/github-pr/plugin.rb +3 -3
  38. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  39. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  40. data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
  41. data/lib/samagotchi/bundles/skills/plugin.rb +3 -2
  42. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  43. data/lib/samagotchi/desktop/macos/ChiRunner.swift +1 -1
  44. data/lib/samagotchi/engine/context_judge.rb +19 -125
  45. data/lib/samagotchi/engine/guardrails.rb +4 -2
  46. data/lib/samagotchi/engine/messages.rb +9 -6
  47. data/lib/samagotchi/engine/objectives.rb +22 -90
  48. data/lib/samagotchi/engine/participants.rb +15 -10
  49. data/lib/samagotchi/engine/plugins.rb +3 -1
  50. data/lib/samagotchi/engine/questions.rb +1 -1
  51. data/lib/samagotchi/engine/side_requests.rb +5 -2
  52. data/lib/samagotchi/engine/toolkit.rb +33 -190
  53. data/lib/samagotchi/engine/turn_end.rb +1 -1
  54. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  55. data/lib/samagotchi/engine/verify.rb +7 -143
  56. data/lib/samagotchi/engine.rb +162 -99
  57. data/lib/samagotchi/engine_peers.rb +1 -1
  58. data/lib/samagotchi/engine_snapshots.rb +9 -8
  59. data/lib/samagotchi/event_fanout.rb +18 -12
  60. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  61. data/lib/samagotchi/guardrails/gate.rb +41 -40
  62. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  63. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  64. data/lib/samagotchi/guardrails/targets.rb +6 -4
  65. data/lib/samagotchi/guardrails_command.rb +1 -1
  66. data/lib/samagotchi/idle_recap.rb +3 -1
  67. data/lib/samagotchi/inline_widgets.rb +325 -0
  68. data/lib/samagotchi/kernel_loop.rb +194 -216
  69. data/lib/samagotchi/llm/backend.rb +3 -1
  70. data/lib/samagotchi/llm/chat_loop.rb +66 -42
  71. data/lib/samagotchi/llm/native_backend.rb +3 -2
  72. data/lib/samagotchi/llm_context_forget.rb +5 -3
  73. data/lib/samagotchi/model_binding.rb +3 -3
  74. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  75. data/lib/samagotchi/question_desk.rb +1 -1
  76. data/lib/samagotchi/revise_anchor.rb +1 -1
  77. data/lib/samagotchi/session_manager.rb +8 -13
  78. data/lib/samagotchi/session_retention.rb +24 -4
  79. data/lib/samagotchi/steer.rb +12 -1
  80. data/lib/samagotchi/stream_event.rb +22 -0
  81. data/lib/samagotchi/system_prompt.rb +84 -61
  82. data/lib/samagotchi/terminal_ui/attached_view.rb +0 -9
  83. data/lib/samagotchi/terminal_ui/input_support.rb +16 -15
  84. data/lib/samagotchi/terminal_ui/join_replay.rb +16 -12
  85. data/lib/samagotchi/terminal_ui/reline_seam.rb +0 -10
  86. data/lib/samagotchi/terminal_ui/status_row.rb +0 -7
  87. data/lib/samagotchi/testing/boundary.rb +7 -0
  88. data/lib/samagotchi/testing/bundle.rb +7 -5
  89. data/lib/samagotchi/testing/install.rb +3 -2
  90. data/lib/samagotchi/tool_activity.rb +8 -6
  91. data/lib/samagotchi/tool_declarations.rb +4 -4
  92. data/lib/samagotchi/tool_response.rb +4 -3
  93. data/lib/samagotchi/tool_runner.rb +93 -101
  94. data/lib/samagotchi/toolkit_binding.rb +40 -14
  95. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  96. data/lib/samagotchi/tools/registry.rb +43 -40
  97. data/lib/samagotchi/tools/tool_index.rb +135 -88
  98. data/lib/samagotchi/tools/tool_surface.rb +173 -0
  99. data/lib/samagotchi/turn_context.rb +67 -0
  100. data/lib/samagotchi/turn_flow.rb +7 -2
  101. data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
  102. data/lib/samagotchi/turn_stages/objectives.rb +146 -0
  103. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
  104. data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
  105. data/lib/samagotchi/turn_stages/warmup.rb +83 -0
  106. data/lib/samagotchi/turn_state.rb +46 -4
  107. data/lib/samagotchi/version.rb +1 -1
  108. data/lib/samagotchi/web/app.rb +3 -1
  109. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  110. data/lib/samagotchi/web/message_display.rb +8 -1
  111. data/lib/samagotchi/web/public/app.js +5 -1
  112. data/lib/samagotchi/web/public/block_host.js +74 -199
  113. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  114. data/lib/samagotchi/web/public/block_source.js +7 -4
  115. data/lib/samagotchi/web/public/block_watch.js +256 -0
  116. data/lib/samagotchi/web/public/card_stamps.js +27 -0
  117. data/lib/samagotchi/web/public/cards_place.js +5 -3
  118. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  119. data/lib/samagotchi/web/public/composer_mode.js +1 -1
  120. data/lib/samagotchi/web/public/format.js +3 -2
  121. data/lib/samagotchi/web/public/index.html +28 -9
  122. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  123. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  124. data/lib/samagotchi/web/public/participant.js +1 -1
  125. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  126. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  127. data/lib/samagotchi/web/public/text_roots.js +1 -1
  128. data/lib/samagotchi/web/public/turn_html.js +8 -4
  129. data/lib/samagotchi/web/public/verify_target.js +1 -1
  130. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  131. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  132. data/lib/samagotchi/web/public/widgets.css +70 -0
  133. data/lib/samagotchi/web/session_access_routes.rb +8 -0
  134. data/lib/samagotchi/web/session_show.rb +4 -0
  135. data/lib/samagotchi/worker.rb +8 -5
  136. data/lib/samagotchi.rb +0 -1
  137. metadata +19 -2
  138. data/lib/samagotchi/pending_input_queue.rb +0 -55
@@ -285,9 +285,11 @@ export function noteBubbleTemplate(note) {
285
285
  if (note?.objectives) return objectivesEntryTemplate(note, timeTitle(note.at));
286
286
  const from = note?.label ? `note from ${note.label}` : "note";
287
287
  // A wake turn's note opens that turn (TURN_PROMPT, bubbles.js); a class, not
288
- // data-turn-id, which marks a claim's anchor (claim_link.js).
288
+ // data-turn-id, which marks a claim's anchor (claim_link.js). Its turn's id
289
+ // is its data-opening-id: a card's stamp finds it (card_stamps.js).
289
290
  const cls = note?.turn_start ? "bubble note turn-start" : "bubble note";
290
- return html`<div class=${cls} title=${timeTitle(note?.at)}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
291
+ const opening = note?.turn_start && note?.turn_id ? String(note.turn_id) : nothing;
292
+ return html`<div class=${cls} title=${timeTitle(note?.at)} data-opening-id=${opening}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
291
293
  }
292
294
 
293
295
  // A plugin's steer (a nudge put into the running turn): one line that
@@ -311,7 +313,9 @@ export function steerRowTemplate({ source, text } = {}, { bubble = false } = {})
311
313
  // participant's badge (participant_names.js authorBadge: their name and
312
314
  // colour) in place of +label+. A turn's opening carries its Revise stamp
313
315
  // (+message.revises+: data-revises, revise_stack.js) and whether it was a
314
- // Verify turn (+message.verify+: data-verify, no Revise on its answer).
316
+ // Verify turn (+message.verify+: data-verify, no Revise on its answer). An
317
+ // opening no turn ran (/continue no's reason) has +message.opening_id+
318
+ // instead of a turn id: data-opening-id, a card's stamp (card_stamps.js).
315
319
  // +message.source+ (a delegate report's "delegate_report") is its
316
320
  // data-source; +lineKey+, a merged line's fold key (answer_fold.js:
317
321
  // data-line-key, the reload's "<turn id>:input:<place>").
@@ -320,7 +324,7 @@ export function userBubbleTemplate(message, { label = null, state = null, step =
320
324
  const from = author
321
325
  ? html`<span class="origin-label author-badge" title=${authorBadgeTitle(author.name, author.mode)} data-unnamed=${author.unnamed ? "1" : nothing}>${authorBadgeText(author.name, author.mode)}</span>`
322
326
  : label ? html`<span class="origin-label">${label}</span>` : nothing;
323
- return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-participant-mode=${author?.mode || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-turn-id=${message?.turn_id || nothing} data-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing} data-source=${message?.source || nothing} data-line-key=${lineKey || nothing}>${from}${userMessageTemplate(message, thumbs)}${stateBadgeTemplate(named, delivery)}</div>`;
327
+ return html`<div class=${`bubble user${named ? ` ${named}` : ""}${author ? " by-participant" : ""}`} title=${timeTitle(message?.at)} style=${author ? `--author-color:${author.color}` : nothing} data-author=${author?.author || nothing} data-participant-mode=${author?.mode || nothing} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-turn-id=${message?.turn_id || nothing} data-opening-id=${(!message?.turn_id && message?.opening_id) || nothing} data-revises=${(message?.turn_id && message?.revises?.turn_id) || nothing} data-verify=${message?.turn_id && message?.verify ? "1" : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing} data-source=${message?.source || nothing} data-line-key=${lineKey || nothing}>${from}${userMessageTemplate(message, thumbs)}${stateBadgeTemplate(named, delivery)}</div>`;
324
328
  }
325
329
 
326
330
  // A user line's text: plain (its quotes set apart), or the server's
@@ -1,6 +1,6 @@
1
1
  // Where the Verify preset is offered (verify_preset.js), on both pages: only
2
2
  // on text in an ended turn's final answer, the one place the server takes a
3
- // claim from. Engine#verified_anchor records a Verify turn's card only when
3
+ // claim from. TurnStages::VerifyAnchors#verified records a Verify turn's card only when
4
4
  // its quote is in the last answer of the source turn (AnswerTail.answer?:
5
5
  // the turn's last assistant message with text, from its prompt up to the
6
6
  // next prompt, merged lines and steers inside); anything else (an earlier
@@ -0,0 +1,130 @@
1
+ // A chi-ui picks (docs/internals/inline-widgets.md): rows of things to decide,
2
+ // each with its options as chips, the recommended one marked and picked to
3
+ // start with, a "+ note" per row, and one "Fill reply" that puts the picks in
4
+ // the composer as plain text ("1: fix now (today) · D4: B"). It never sends:
5
+ // the composer text is the widget's only way out, and the owner reads it
6
+ // before sending. Ported from eval 0013's viewer.
7
+ //
8
+ // The data is P1a's normalised JSON (InlineWidgets). Its strings are the
9
+ // model's: they go in as textContent or attributes only, never markup. The
10
+ // picks live here, in the widget: a rewrite of the bubble starts over.
11
+ //
12
+ // A module of its own: static JS is cached at unversioned URLs.
13
+
14
+ // What a row starts with: its recommended option picked, no note.
15
+ export function initialPicks(rows) {
16
+ return (rows || []).map((r) => ({ pick: r.recommended != null ? String(r.recommended) : null, note: "" }));
17
+ }
18
+
19
+ // The reply the picks make: "id: option (note)" per picked row, " · " between
20
+ // them; a row with no pick is left out.
21
+ export function replyText(rows, picks) {
22
+ return (rows || [])
23
+ .map((r, k) => {
24
+ const { pick, note } = picks[k] || {};
25
+ if (!pick) return null;
26
+ const said = String(note ?? "").replace(/\s+/g, " ").trim();
27
+ return `${r.id ?? k + 1}: ${pick}${said ? ` (${said})` : ""}`;
28
+ })
29
+ .filter(Boolean)
30
+ .join(" · ");
31
+ }
32
+
33
+ // The composer's text with +reply+: after a draft on a line of its own, the
34
+ // draft kept as typed.
35
+ export function withDraft(draft, reply) {
36
+ const typed = String(draft ?? "").trimEnd();
37
+ return typed ? `${typed}\n${reply}` : reply;
38
+ }
39
+
40
+ function el(doc, tag, className, text) {
41
+ const node = doc.createElement(tag);
42
+ if (className) node.className = className;
43
+ if (text != null) node.textContent = String(text);
44
+ return node;
45
+ }
46
+
47
+ function button(doc, className, text) {
48
+ const node = el(doc, "button", className, text);
49
+ node.type = "button";
50
+ return node;
51
+ }
52
+
53
+ // The picks as an element, for a widget host's shadow root.
54
+ // @param fill (reply) => puts +reply+ in the composer (never sends)
55
+ export function drawPicks(picks, { fill, doc = document }) {
56
+ const rows = picks.rows || [];
57
+ const state = initialPicks(rows);
58
+
59
+ const box = el(doc, "div", "w picks");
60
+ box.setAttribute("role", "group");
61
+ box.setAttribute("aria-label", `Picks: ${picks.title || "choices"}`);
62
+ const head = el(doc, "div", "w-head");
63
+ const count = el(doc, "span", "w-count");
64
+ head.append(el(doc, "span", "w-kind", "Picks"), el(doc, "span", "w-title", picks.title || ""), count);
65
+ const body = el(doc, "div", "w-body");
66
+ box.append(head, body);
67
+
68
+ const list = el(doc, "div", "wp-list");
69
+ const chips = rows.map((r, k) => {
70
+ const id = String(r.id ?? k + 1);
71
+ const row = el(doc, "div", "wp-row");
72
+ const text = el(doc, "div", "wp-text");
73
+ text.append(el(doc, "div", null, r.text || ""));
74
+ if (r.why) text.append(el(doc, "div", "wp-why", r.why));
75
+ const add = button(doc, "wp-addnote", "+ note");
76
+ add.addEventListener("click", () => {
77
+ const input = el(doc, "input", "wp-note");
78
+ input.type = "text";
79
+ input.placeholder = "a word for this row";
80
+ input.setAttribute("aria-label", `Note for ${id}`);
81
+ input.addEventListener("input", () => { state[k].note = input.value; });
82
+ add.replaceWith(input);
83
+ input.focus();
84
+ });
85
+ text.append(add);
86
+ const opts = el(doc, "div", "wp-opts");
87
+ opts.setAttribute("role", "group");
88
+ opts.setAttribute("aria-label", `Options for ${id}`);
89
+ const own = (r.options || []).map((option) => {
90
+ const o = String(option);
91
+ const b = button(doc, "wp-opt", o);
92
+ if (r.recommended != null && String(r.recommended) === o) {
93
+ b.append(el(doc, "span", "rec", "rec"));
94
+ b.title = "recommended";
95
+ }
96
+ b.addEventListener("click", () => {
97
+ state[k].pick = state[k].pick === o ? null : o;
98
+ paint();
99
+ });
100
+ opts.append(b);
101
+ return [o, b];
102
+ });
103
+ row.append(el(doc, "span", "wp-id", id), text, opts);
104
+ list.append(row);
105
+ return own;
106
+ });
107
+
108
+ const foot = el(doc, "div", "wp-foot");
109
+ const fillBtn = button(doc, "wp-fill", "Fill reply");
110
+ fillBtn.title = "Put the picks in the composer; you send it";
111
+ foot.append(fillBtn, el(doc, "span", "hint", "fills the composer, never sends"));
112
+ body.append(list, foot);
113
+
114
+ fillBtn.addEventListener("click", () => {
115
+ const reply = replyText(rows, state);
116
+ if (reply) fill(reply);
117
+ });
118
+
119
+ function paint() {
120
+ chips.forEach((own, k) => {
121
+ for (const [o, b] of own) b.setAttribute("aria-pressed", String(state[k].pick === o));
122
+ });
123
+ const picked = state.filter((s) => s.pick).length;
124
+ count.textContent = `${picked} of ${rows.length} picked`;
125
+ fillBtn.disabled = picked === 0;
126
+ }
127
+
128
+ paint();
129
+ return box;
130
+ }
@@ -0,0 +1,230 @@
1
+ // A chi-ui walk (docs/internals/inline-widgets.md): a scenario stepped through
2
+ // one step at a time. On top a swimlane, one column per actor and one row per
3
+ // step (the current, the seen and a case's target marked; a click jumps); under
4
+ // it the step's card (the actor, what it does, "You see", the dashed "Behind
5
+ // it"); a fork asks "Which case?" instead of Next; Back, Next and the path
6
+ // taken, and the arrow keys while the widget has the focus. Ported from eval
7
+ // 0013's viewer.
8
+ //
9
+ // The data is P1a's normalised JSON (InlineWidgets): ids are strings, every
10
+ // next and goto names a step, round 1's branches are cases. Its strings are
11
+ // the model's: they go in as textContent or attributes only, never markup.
12
+ // The position lives here, in the widget: a rewrite of the bubble starts over.
13
+ //
14
+ // A module of its own: static JS is cached at unversioned URLs.
15
+
16
+ // A case that is no condition: round 1 wrote the usual path as "always".
17
+ export const ALWAYS = /^(always|otherwise|then|next)$/i;
18
+ export const USUAL = "otherwise (the usual path)";
19
+ // A step's `next: end` (InlineWidgets::END_NEXT): the walk ends there, a
20
+ // branch that ends early.
21
+ export const END = "end";
22
+
23
+ const isAlways = (when) => ALWAYS.test(String(when ?? "").trim());
24
+
25
+ // Where each step of +walk+ goes, by index.
26
+ // - nextOf(k): the step after k when it isn't a fork: its next, else a case
27
+ // that is no condition, else the step below; null at the end, and at a
28
+ // step whose next is `end`.
29
+ // - casesOf(k): a fork's cases, [{when, to}] (to null when the goto names no
30
+ // step), or null when k is no fork. A case that is no condition reads as
31
+ // the usual path, and a step that names a next as well offers it first when
32
+ // no case goes there (round 1's next + branches).
33
+ export function walkRoutes(walk) {
34
+ const steps = walk?.steps || [];
35
+ const byId = new Map(steps.map((s, k) => [String(s.id), k]));
36
+ const idx = (id) => (id == null ? null : byId.get(String(id)) ?? null);
37
+ const casesIn = (k) => (Array.isArray(steps[k]?.cases) ? steps[k].cases : []);
38
+ const isFork = (k) => casesIn(k).some((c) => !isAlways(c.when));
39
+
40
+ function nextOf(k) {
41
+ const step = steps[k];
42
+ if (!step || step.next === END) return null;
43
+ const named = idx(step.next);
44
+ if (named != null) return named;
45
+ const always = casesIn(k).find((c) => isAlways(c.when) && idx(c.goto) != null);
46
+ if (always && !isFork(k)) return idx(always.goto);
47
+ return k + 1 < steps.length ? k + 1 : null;
48
+ }
49
+
50
+ function casesOf(k) {
51
+ if (!isFork(k)) return null;
52
+ const cases = casesIn(k).map((c) => ({ when: isAlways(c.when) ? USUAL : String(c.when ?? ""), to: idx(c.goto) }));
53
+ const named = idx(steps[k].next);
54
+ if (named != null && !cases.some((c) => c.to === named)) cases.unshift({ when: USUAL, to: named });
55
+ return cases;
56
+ }
57
+
58
+ return { steps, nextOf, casesOf };
59
+ }
60
+
61
+ // The swimlane's cells: one per step, in its actor's column (1-based, after
62
+ // the number column) and its own row (after the actors' row), with a tag that
63
+ // says where it leads when that isn't the step below: "N cases", "→ n", "end".
64
+ export function swimlaneCells(walk, routes = walkRoutes(walk)) {
65
+ const actors = (walk?.actors || []).map(String);
66
+ const { steps, nextOf, casesOf } = routes;
67
+ return steps.map((s, k) => {
68
+ const cases = casesOf(k);
69
+ const next = nextOf(k);
70
+ let tag = null;
71
+ if (cases) tag = { text: `${cases.length} cases`, fork: true };
72
+ else if (next != null && next !== k + 1) tag = { text: `→ ${next + 1}`, fork: false };
73
+ else if (next == null && k < steps.length - 1) tag = { text: "end", fork: false };
74
+ return { k, column: Math.max(0, actors.indexOf(String(s.actor))) + 2, row: k + 2, text: String(s.does || s.sees || ""), tag };
75
+ });
76
+ }
77
+
78
+ // Where the walker is: the path taken (step indexes) and the steps seen.
79
+ // next() at the end starts over.
80
+ export function createWalker(routes) {
81
+ let path = [0];
82
+ const seen = new Set([0]);
83
+ return {
84
+ get at() { return path[path.length - 1]; },
85
+ get path() { return [...path]; },
86
+ seen: (k) => seen.has(k),
87
+ go(k) {
88
+ if (k == null || !routes.steps[k] || k === this.at) return;
89
+ path.push(k);
90
+ seen.add(k);
91
+ },
92
+ back() { if (path.length > 1) path.pop(); },
93
+ next() {
94
+ const n = routes.nextOf(this.at);
95
+ if (n != null) { this.go(n); return; }
96
+ path = [0];
97
+ seen.clear();
98
+ seen.add(0);
99
+ },
100
+ };
101
+ }
102
+
103
+ // The path's words under the card, empty before the first move.
104
+ export function trailText(path) {
105
+ return path.length > 1 ? `path ${path.map((k) => k + 1).join(" → ")}` : "";
106
+ }
107
+
108
+ function el(doc, tag, className, text) {
109
+ const node = doc.createElement(tag);
110
+ if (className) node.className = className;
111
+ if (text != null) node.textContent = String(text);
112
+ return node;
113
+ }
114
+
115
+ function button(doc, className, text) {
116
+ const node = el(doc, "button", className, text);
117
+ node.type = "button";
118
+ return node;
119
+ }
120
+
121
+ // The walk as an element, for a widget host's shadow root.
122
+ export function drawWalk(walk, doc = document) {
123
+ const routes = walkRoutes(walk);
124
+ const { steps, nextOf, casesOf } = routes;
125
+ const actors = (walk.actors || []).map(String);
126
+ const walker = createWalker(routes);
127
+
128
+ const box = el(doc, "div", "w walk");
129
+ box.tabIndex = 0;
130
+ box.setAttribute("role", "group");
131
+ box.setAttribute("aria-label", `Walk: ${walk.title || "a scenario"}`);
132
+ const head = el(doc, "div", "w-head");
133
+ const count = el(doc, "span", "w-count");
134
+ head.append(el(doc, "span", "w-kind", "Walk"), el(doc, "span", "w-title", walk.title || ""), count);
135
+ const body = el(doc, "div", "w-body");
136
+ box.append(head, body);
137
+
138
+ const lane = el(doc, "div", "ww-swim");
139
+ lane.style.setProperty("--actors", String(actors.length));
140
+ lane.append(el(doc, "span"));
141
+ for (const actor of actors) lane.append(el(doc, "span", "ww-swim-actor", actor));
142
+ const cells = swimlaneCells(walk, routes).map((cell) => {
143
+ const num = el(doc, "span", "ww-swim-num", cell.k + 1);
144
+ num.style.gridRow = String(cell.row);
145
+ const node = button(doc, "ww-cell");
146
+ node.style.gridColumn = String(cell.column);
147
+ node.style.gridRow = String(cell.row);
148
+ node.append(el(doc, "span", "ww-cell-t", cell.text));
149
+ if (cell.tag) node.append(el(doc, "span", cell.tag.fork ? "ww-tag fork" : "ww-tag", cell.tag.text));
150
+ const s = steps[cell.k];
151
+ node.title = `${s.does || ""}${s.does && s.sees ? "\n" : ""}${s.sees ? `sees: ${s.sees}` : ""}`;
152
+ node.setAttribute("aria-label", `Step ${cell.k + 1}, ${s.actor}: ${cell.text}`);
153
+ node.addEventListener("click", () => { walker.go(cell.k); render(); });
154
+ lane.append(num, node);
155
+ return node;
156
+ });
157
+
158
+ const card = el(doc, "div", "ww-step");
159
+ card.setAttribute("aria-live", "polite");
160
+ const nav = el(doc, "div", "ww-nav");
161
+ const prev = button(doc, "btn", "← Back");
162
+ const next = button(doc, "btn primary", "Next →");
163
+ const trail = el(doc, "span", "ww-trail");
164
+ nav.append(prev, trail, next);
165
+ body.append(lane, card, nav);
166
+
167
+ prev.addEventListener("click", () => { walker.back(); render(); });
168
+ next.addEventListener("click", () => { walker.next(); render(); });
169
+ box.addEventListener("keydown", (e) => {
170
+ if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return;
171
+ if (e.key === "ArrowRight" && !next.hidden) { e.preventDefault(); next.click(); }
172
+ if (e.key === "ArrowLeft" && !prev.disabled) { e.preventDefault(); prev.click(); }
173
+ });
174
+
175
+ const peek = (k, on) => cells[k]?.classList.toggle("peek", on);
176
+
177
+ function pane(className, label, text) {
178
+ const node = el(doc, "div", `ww-pane ${className}`);
179
+ node.append(el(doc, "span", "eyebrow", label), el(doc, "span", null, text));
180
+ return node;
181
+ }
182
+
183
+ // A keyboard move can remove, hide or disable the focused button (a case,
184
+ // Next at a fork, Back at the start): the focus stays in the widget.
185
+ function render() {
186
+ const focused = box.getRootNode()?.activeElement;
187
+ const hadFocus = !!focused && box.contains(focused);
188
+ const k = walker.at;
189
+ const s = steps[k] || {};
190
+ card.replaceChildren();
191
+ const top = el(doc, "div", "ww-step-top");
192
+ top.append(el(doc, "span", "ww-actor", s.actor || ""), el(doc, "span", "ww-stepno", `${k + 1} / ${steps.length}`));
193
+ card.append(top);
194
+ if (s.does) card.append(el(doc, "div", "ww-does", s.does));
195
+ if (s.sees) card.append(pane("ww-sees", "You see", s.sees));
196
+ if (s.behind) card.append(pane("ww-behind", "Behind it", s.behind));
197
+ const cases = casesOf(k);
198
+ if (cases) {
199
+ const fork = el(doc, "div", "ww-fork");
200
+ fork.append(el(doc, "span", "eyebrow", "Which case?"));
201
+ for (const c of cases) {
202
+ const b = button(doc, "ww-case");
203
+ b.append(el(doc, "span", null, c.when), el(doc, "span", "ww-case-to", c.to != null ? `step ${c.to + 1} →` : "?"));
204
+ b.disabled = c.to == null;
205
+ b.addEventListener("click", () => { walker.go(c.to); render(); });
206
+ for (const [type, on] of [["mouseenter", true], ["mouseleave", false], ["focus", true], ["blur", false]]) {
207
+ b.addEventListener(type, () => peek(c.to, on));
208
+ }
209
+ fork.append(b);
210
+ }
211
+ card.append(fork);
212
+ }
213
+ cells.forEach((cell, j) => {
214
+ cell.classList.toggle("cur", j === k);
215
+ cell.classList.toggle("seen", walker.seen(j) && j !== k);
216
+ cell.classList.remove("peek");
217
+ if (j === k) cell.setAttribute("aria-current", "step");
218
+ else cell.removeAttribute("aria-current");
219
+ });
220
+ count.textContent = `step ${k + 1} of ${steps.length}`;
221
+ prev.disabled = walker.path.length < 2;
222
+ next.hidden = !!cases;
223
+ next.textContent = nextOf(k) == null ? "Start over" : "Next →";
224
+ trail.textContent = trailText(walker.path);
225
+ if (hadFocus && (!focused.isConnected || focused.hidden || focused.disabled)) box.focus({ preventScroll: true });
226
+ }
227
+
228
+ render();
229
+ return box;
230
+ }
@@ -0,0 +1,70 @@
1
+ /* A chi-ui widget (inline_widgets.js), inside its host's shadow root: chi's :root tokens (tokens.css) reach it as inherited custom properties, so light and dark follow the page. Ported from eval 0013's viewer. At a phone's width the swimlane scrolls sideways inside the widget; the page never does. */
2
+ :host{display:block;min-width:0}
3
+ *{box-sizing:border-box}
4
+ [hidden]{display:none!important}
5
+ button{font:inherit;color:inherit;cursor:pointer;margin:0}
6
+ button:disabled{cursor:default}
7
+ button:focus-visible,input:focus-visible,.w:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
8
+ .eyebrow{font-size:10px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted)}
9
+
10
+ /* The frame both share. The head leaves room on the right for the figure's source toggle and a code block's copy button (block_renderers.css). */
11
+ .w{border:1px solid var(--accent-border);border-radius:10px;background:var(--bg-inset);overflow:hidden;font-size:14px;line-height:1.5;color:var(--text-2)}
12
+ .w-head{display:flex;gap:8px;align-items:baseline;padding:8px 68px 8px 12px;border-bottom:1px solid var(--border);flex-wrap:wrap}
13
+ .w-kind{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);font-weight:600}
14
+ .w-title{font-weight:600;color:var(--text-bright);min-width:0;overflow-wrap:anywhere}
15
+ .w-count{margin-left:auto;color:var(--text-muted);font-size:12px;font-variant-numeric:tabular-nums}
16
+ .w-body{padding:12px;display:grid;gap:12px;min-width:0}
17
+
18
+ /* walk */
19
+ .ww-swim{display:grid;grid-template-columns:22px repeat(var(--actors,2),minmax(84px,1fr));gap:3px 6px;align-items:stretch;font-size:12px;overflow-x:auto;padding-bottom:2px}
20
+ .ww-swim-actor{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--text-muted);text-align:center;padding-bottom:2px;border-bottom:1px solid var(--border-strong);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
21
+ .ww-swim-num{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);font-size:11px;align-self:center;text-align:right}
22
+ .ww-cell{display:flex;gap:6px;align-items:center;justify-content:space-between;text-align:left;min-width:0;border:1px solid var(--border);background:var(--bg-panel);border-radius:6px;padding:3px 8px;color:var(--text-3);transition:background .15s,border-color .15s}
23
+ .ww-cell:hover{border-color:var(--border-strong)}
24
+ .ww-cell-t{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
25
+ .ww-cell.seen{border-color:var(--accent-border);color:var(--text-2)}
26
+ .ww-cell.cur{background:var(--accent);border-color:var(--accent);color:var(--on-solid)}
27
+ .ww-cell.peek{border-color:var(--warn);box-shadow:0 0 0 2px var(--warn-bg)}
28
+ .ww-tag{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px;color:var(--text-faint)}
29
+ .ww-tag.fork{color:var(--warn)}
30
+ .ww-cell.cur .ww-tag{color:var(--on-solid);opacity:.85}
31
+ .ww-step{display:grid;gap:8px;min-height:120px;border-top:1px solid var(--border);padding-top:12px;align-content:start}
32
+ .ww-step-top{display:flex;justify-content:space-between;align-items:baseline;gap:8px}
33
+ .ww-actor{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--accent);font-size:12px}
34
+ .ww-stepno{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);font-size:11px}
35
+ .ww-does{font-size:16px;color:var(--text-bright);font-weight:500;text-wrap:pretty;overflow-wrap:anywhere}
36
+ .ww-pane{border-radius:8px;padding:8px 10px;display:grid;gap:2px;overflow-wrap:anywhere}
37
+ .ww-sees{background:var(--bg-panel);border:1px solid var(--border);color:var(--text)}
38
+ .ww-behind{background:transparent;border:1px dashed var(--border-strong);color:var(--text-3);font-size:13px}
39
+ .ww-fork{display:grid;gap:6px}
40
+ .ww-case{display:flex;gap:10px;justify-content:space-between;align-items:center;text-align:left;border:1px solid var(--border-strong);background:var(--bg-panel);border-radius:8px;padding:7px 10px;overflow-wrap:anywhere}
41
+ .ww-case:hover:not(:disabled),.ww-case:focus-visible{border-color:var(--warn)}
42
+ .ww-case:disabled{opacity:.5}
43
+ .ww-case-to{flex:none;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--warn)}
44
+ .ww-nav{display:flex;gap:10px;align-items:center;justify-content:space-between}
45
+ .ww-nav .btn{flex:none;border:1px solid var(--border-strong);background:var(--bg-panel);border-radius:8px;padding:4px 12px}
46
+ .ww-nav .btn.primary{background:var(--accent);border-color:var(--accent);color:var(--on-solid)}
47
+ .ww-nav .btn:disabled{opacity:.4}
48
+ .ww-trail{font-size:12px;color:var(--text-faint);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;overflow-wrap:anywhere;text-align:center;flex:1;min-width:0}
49
+ @media (prefers-reduced-motion:reduce){.ww-cell{transition:none}}
50
+
51
+ /* picks */
52
+ .wp-row{display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:10px;align-items:start;padding:8px 0;border-bottom:1px solid var(--border)}
53
+ .wp-row:first-child{padding-top:0}
54
+ .wp-row:last-child{border-bottom:0}
55
+ .wp-id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;color:var(--text-muted);padding-top:2px;min-width:24px}
56
+ .wp-text{color:var(--text);min-width:0;overflow-wrap:anywhere}
57
+ .wp-why{color:var(--text-muted);font-size:12px}
58
+ .wp-note{display:block;margin-top:4px;width:100%;background:var(--bg-panel);border:1px solid var(--border-strong);border-radius:6px;color:var(--text);padding:3px 6px;font:inherit;font-size:12px}
59
+ .wp-opts{display:flex;flex-wrap:wrap;gap:4px;justify-content:flex-end;max-width:320px}
60
+ .wp-opt{border:1px solid var(--border-strong);background:var(--bg-panel);border-radius:6px;padding:2px 10px;font-size:12px;overflow-wrap:anywhere;text-align:left}
61
+ .wp-opt:hover{border-color:var(--accent-border)}
62
+ .wp-opt[aria-pressed="true"]{background:var(--accent);border-color:var(--accent);color:var(--on-solid)}
63
+ .wp-opt .rec{font-size:9px;letter-spacing:.06em;text-transform:uppercase;margin-left:5px;opacity:.75}
64
+ .wp-addnote{background:none;border:0;color:var(--text-faint);font-size:12px;padding:2px 0}
65
+ .wp-addnote:hover{color:var(--text-2)}
66
+ .wp-foot{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
67
+ .wp-fill{background:var(--accent);border:1px solid var(--accent);color:var(--on-solid);border-radius:8px;padding:4px 14px}
68
+ .wp-fill:disabled{opacity:.4}
69
+ .hint{color:var(--text-faint);font-size:12px}
70
+ @media (max-width:560px){.wp-row{grid-template-columns:auto minmax(0,1fr)}.wp-opts{grid-column:2;justify-content:flex-start;max-width:none}}
@@ -54,6 +54,14 @@ module Samagotchi
54
54
  json_response(status, { error: code, detail: detail, message: error.message, **fields }.compact)
55
55
  end
56
56
 
57
+ # 409 scratch for a chi scratch session +id+, which only its terminal
58
+ # uses (Session::SCRATCH_REFUSED): the session page's read and stream
59
+ # (a tab opened at its id by hand: the hub never lists it), as
60
+ # POST …/wake refuses it.
61
+ def scratch_refused(id)
62
+ refusal_response(409, "scratch", SessionManager::ScratchSession.new(id), session_id: id)
63
+ end
64
+
57
65
  # The body as a Hash ({} when there is none), or nil when it isn't one.
58
66
  def body_hash(req)
59
67
  raw = request_body(req)
@@ -40,6 +40,10 @@ module Samagotchi
40
40
  return handle_brief_read(id) if req.params["brief"] == "1"
41
41
 
42
42
  session = @session_class.load(id, state_dir: default_state_dir)
43
+ # A tab opened at a chi scratch session's id: its terminal's alone.
44
+ # (The light re-reads come after a full read only.)
45
+ return scratch_refused(id) if session.scratch
46
+
43
47
  # Read-only preview: selecting a session never spawns a worker.
44
48
  # The worker is woken only on POST /turn (handle_turn) via
45
49
  # SessionManager.resume_session + write_turn_input. This avoids
@@ -1608,14 +1608,17 @@ module Samagotchi
1608
1608
  SCRATCH_GONE_AFTER = 2 * BridgeClient::EventStream::REDISCOVER_DELAYS.sum
1609
1609
 
1610
1610
  # Whether this is a `chi scratch` session's worker whose terminal is
1611
- # gone: no stream open for SCRATCH_GONE_AFTER, after one was (a killed
1612
- # TUI's closes as its writer reads the hang-up), or none came within
1613
- # SCRATCH_JOIN_GRACE. Its terminal deletes it on every way out it sees;
1614
- # this is for the ones it doesn't (kill -9, a closed terminal window).
1611
+ # gone: no terminal stream (a tui: client id's) open for
1612
+ # SCRATCH_GONE_AFTER, after a stream was (a killed TUI's closes as its
1613
+ # writer reads the hang-up), or none came within SCRATCH_JOIN_GRACE.
1614
+ # Any other stream (a web tab's, which chi web refuses for a scratch
1615
+ # session anyway) never keeps it. Its terminal deletes it on every way
1616
+ # out it sees; this is for the ones it doesn't (kill -9, a closed
1617
+ # terminal window).
1615
1618
  def scratch_unattended?
1616
1619
  return false unless @session&.scratch && @bridge
1617
1620
 
1618
- if @bridge.open_streams.positive?
1621
+ if @bridge.open_streams_of(ClientId::TUI_PREFIX).positive?
1619
1622
  @unattended_since = nil
1620
1623
  return false
1621
1624
  end
data/lib/samagotchi.rb CHANGED
@@ -7,7 +7,6 @@ require_relative "samagotchi/prompt"
7
7
  require_relative "samagotchi/client"
8
8
  require_relative "samagotchi/debug_log"
9
9
  require_relative "samagotchi/delivery"
10
- require_relative "samagotchi/pending_input_queue"
11
10
  require_relative "samagotchi/kernel_loop"
12
11
  require_relative "samagotchi/session"
13
12
  require_relative "samagotchi/terminal_ui"
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.154.0
4
+ version: 0.155.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -161,6 +161,7 @@ files:
161
161
  - docs/internals/context-telemetry.md
162
162
  - docs/internals/engine.md
163
163
  - docs/internals/gemma4-contract.md
164
+ - docs/internals/inline-widgets.md
164
165
  - docs/internals/llm-context-bench.md
165
166
  - docs/internals/llm-context-forget.md
166
167
  - docs/internals/llm-context-live.md
@@ -264,6 +265,8 @@ files:
264
265
  - lib/samagotchi/bundles/guardrails/guardrails/rules.yml
265
266
  - lib/samagotchi/bundles/guardrails/guardrails/small-models.yml
266
267
  - lib/samagotchi/bundles/guardrails/manifest.yml
268
+ - lib/samagotchi/bundles/inline-widgets/manifest.yml
269
+ - lib/samagotchi/bundles/inline-widgets/plugin.rb
267
270
  - lib/samagotchi/bundles/known-names/hooks/known_names.rb
268
271
  - lib/samagotchi/bundles/known-names/known_names.md
269
272
  - lib/samagotchi/bundles/known-names/manifest.yml
@@ -445,6 +448,7 @@ files:
445
448
  - lib/samagotchi/idle_scheduler.rb
446
449
  - lib/samagotchi/idle_target.rb
447
450
  - lib/samagotchi/image_store.rb
451
+ - lib/samagotchi/inline_widgets.rb
448
452
  - lib/samagotchi/installed_gem.rb
449
453
  - lib/samagotchi/installed_versions.rb
450
454
  - lib/samagotchi/interrupted_turn.rb
@@ -563,7 +567,6 @@ files:
563
567
  - lib/samagotchi/participants_watch.rb
564
568
  - lib/samagotchi/paths.rb
565
569
  - lib/samagotchi/peer_sources.rb
566
- - lib/samagotchi/pending_input_queue.rb
567
570
  - lib/samagotchi/pending_question.rb
568
571
  - lib/samagotchi/plugin/api.rb
569
572
  - lib/samagotchi/plugin/attached_context.rb
@@ -670,6 +673,7 @@ files:
670
673
  - lib/samagotchi/steer.rb
671
674
  - lib/samagotchi/steer_cut.rb
672
675
  - lib/samagotchi/steer_cut_after.rb
676
+ - lib/samagotchi/stream_event.rb
673
677
  - lib/samagotchi/system_one.rb
674
678
  - lib/samagotchi/system_one/answer.rb
675
679
  - lib/samagotchi/system_one/approval_label.rb
@@ -808,14 +812,21 @@ files:
808
812
  - lib/samagotchi/tools/task_wait.rb
809
813
  - lib/samagotchi/tools/tool_index.rb
810
814
  - lib/samagotchi/tools/tool_path.rb
815
+ - lib/samagotchi/tools/tool_surface.rb
811
816
  - lib/samagotchi/tools/web_fetch.rb
812
817
  - lib/samagotchi/tools/write.rb
813
818
  - lib/samagotchi/turn_anchors.rb
814
819
  - lib/samagotchi/turn_checkpoint.rb
820
+ - lib/samagotchi/turn_context.rb
815
821
  - lib/samagotchi/turn_endings.rb
816
822
  - lib/samagotchi/turn_flow.rb
817
823
  - lib/samagotchi/turn_note.rb
818
824
  - lib/samagotchi/turn_origin.rb
825
+ - lib/samagotchi/turn_stages/context_judge.rb
826
+ - lib/samagotchi/turn_stages/objectives.rb
827
+ - lib/samagotchi/turn_stages/toolkit_suggestion.rb
828
+ - lib/samagotchi/turn_stages/verify_anchors.rb
829
+ - lib/samagotchi/turn_stages/warmup.rb
819
830
  - lib/samagotchi/turn_state.rb
820
831
  - lib/samagotchi/turn_tally.rb
821
832
  - lib/samagotchi/update_command.rb
@@ -865,8 +876,10 @@ files:
865
876
  - lib/samagotchi/web/public/block_open.js
866
877
  - lib/samagotchi/web/public/block_renderers.css
867
878
  - lib/samagotchi/web/public/block_source.js
879
+ - lib/samagotchi/web/public/block_watch.js
868
880
  - lib/samagotchi/web/public/bubbles.js
869
881
  - lib/samagotchi/web/public/card.js
882
+ - lib/samagotchi/web/public/card_stamps.js
870
883
  - lib/samagotchi/web/public/cards_place.js
871
884
  - lib/samagotchi/web/public/chains.js
872
885
  - lib/samagotchi/web/public/chip_popover.js
@@ -906,6 +919,7 @@ files:
906
919
  - lib/samagotchi/web/public/info_bar.js
907
920
  - lib/samagotchi/web/public/info_bar_model.js
908
921
  - lib/samagotchi/web/public/init_row.js
922
+ - lib/samagotchi/web/public/inline_widgets.js
909
923
  - lib/samagotchi/web/public/keep_focus.js
910
924
  - lib/samagotchi/web/public/kept_commands.js
911
925
  - lib/samagotchi/web/public/latest.js
@@ -1023,6 +1037,9 @@ files:
1023
1037
  - lib/samagotchi/web/public/verify_preset.js
1024
1038
  - lib/samagotchi/web/public/verify_target.js
1025
1039
  - lib/samagotchi/web/public/version_notices.js
1040
+ - lib/samagotchi/web/public/widget_picks.js
1041
+ - lib/samagotchi/web/public/widget_walk.js
1042
+ - lib/samagotchi/web/public/widgets.css
1026
1043
  - lib/samagotchi/web/public/worker_features.js
1027
1044
  - lib/samagotchi/web/public/working_set.js
1028
1045
  - lib/samagotchi/web/public/working_set_rail.js