samagotchi 0.154.1 → 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 (99) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -1
  3. data/docs/architecture.md +3 -2
  4. data/docs/bundles.md +21 -0
  5. data/docs/cli.md +1 -1
  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/engine.md +220 -99
  12. data/docs/internals/inline-widgets.md +136 -0
  13. data/docs/internals/objectives.md +8 -6
  14. data/docs/internals/participants.md +5 -5
  15. data/docs/internals/prompt-caching.md +9 -5
  16. data/docs/internals/web-frontend.md +1 -1
  17. data/docs/internals/working-set.md +2 -1
  18. data/docs/plugins.md +2 -0
  19. data/docs/releasing.md +1 -1
  20. data/docs/sessions.md +2 -2
  21. data/docs/testing.md +1 -1
  22. data/lib/samagotchi/block_renderers.rb +5 -0
  23. data/lib/samagotchi/bundle_command/check.rb +1 -1
  24. data/lib/samagotchi/bundles/dev/manifest.yml +3 -2
  25. data/lib/samagotchi/bundles/inline-widgets/manifest.yml +11 -0
  26. data/lib/samagotchi/bundles/inline-widgets/plugin.rb +76 -0
  27. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  28. data/lib/samagotchi/engine/context_judge.rb +19 -125
  29. data/lib/samagotchi/engine/guardrails.rb +4 -2
  30. data/lib/samagotchi/engine/messages.rb +9 -6
  31. data/lib/samagotchi/engine/objectives.rb +22 -90
  32. data/lib/samagotchi/engine/participants.rb +15 -10
  33. data/lib/samagotchi/engine/plugins.rb +3 -1
  34. data/lib/samagotchi/engine/questions.rb +1 -1
  35. data/lib/samagotchi/engine/side_requests.rb +5 -2
  36. data/lib/samagotchi/engine/toolkit.rb +33 -190
  37. data/lib/samagotchi/engine/turn_end.rb +1 -1
  38. data/lib/samagotchi/engine/turn_helpers.rb +16 -39
  39. data/lib/samagotchi/engine/verify.rb +7 -143
  40. data/lib/samagotchi/engine.rb +162 -99
  41. data/lib/samagotchi/engine_peers.rb +1 -1
  42. data/lib/samagotchi/event_fanout.rb +18 -12
  43. data/lib/samagotchi/guardrail_wiring.rb +83 -82
  44. data/lib/samagotchi/guardrails/gate.rb +41 -40
  45. data/lib/samagotchi/guardrails/pair_boundary.rb +2 -1
  46. data/lib/samagotchi/guardrails/review_boundary.rb +2 -1
  47. data/lib/samagotchi/guardrails/targets.rb +6 -4
  48. data/lib/samagotchi/guardrails_command.rb +1 -1
  49. data/lib/samagotchi/inline_widgets.rb +325 -0
  50. data/lib/samagotchi/kernel_loop.rb +190 -213
  51. data/lib/samagotchi/llm/backend.rb +3 -1
  52. data/lib/samagotchi/llm/chat_loop.rb +63 -41
  53. data/lib/samagotchi/llm/native_backend.rb +3 -2
  54. data/lib/samagotchi/llm_context_forget.rb +5 -3
  55. data/lib/samagotchi/model_binding.rb +3 -3
  56. data/lib/samagotchi/plugin/prompt_sections.rb +7 -0
  57. data/lib/samagotchi/question_desk.rb +1 -1
  58. data/lib/samagotchi/revise_anchor.rb +1 -1
  59. data/lib/samagotchi/system_prompt.rb +37 -30
  60. data/lib/samagotchi/testing/boundary.rb +7 -0
  61. data/lib/samagotchi/testing/bundle.rb +7 -5
  62. data/lib/samagotchi/testing/install.rb +3 -2
  63. data/lib/samagotchi/tool_activity.rb +8 -6
  64. data/lib/samagotchi/tool_declarations.rb +4 -4
  65. data/lib/samagotchi/tool_response.rb +4 -3
  66. data/lib/samagotchi/tool_runner.rb +91 -100
  67. data/lib/samagotchi/toolkit_binding.rb +40 -14
  68. data/lib/samagotchi/tools/deferred_tools.rb +17 -21
  69. data/lib/samagotchi/tools/registry.rb +43 -40
  70. data/lib/samagotchi/tools/tool_index.rb +135 -88
  71. data/lib/samagotchi/tools/tool_surface.rb +173 -0
  72. data/lib/samagotchi/turn_context.rb +67 -0
  73. data/lib/samagotchi/turn_stages/context_judge.rb +170 -0
  74. data/lib/samagotchi/turn_stages/objectives.rb +146 -0
  75. data/lib/samagotchi/turn_stages/toolkit_suggestion.rb +261 -0
  76. data/lib/samagotchi/turn_stages/verify_anchors.rb +169 -0
  77. data/lib/samagotchi/turn_stages/warmup.rb +83 -0
  78. data/lib/samagotchi/turn_state.rb +46 -4
  79. data/lib/samagotchi/version.rb +1 -1
  80. data/lib/samagotchi/web/markdown_renderer.rb +39 -14
  81. data/lib/samagotchi/web/message_display.rb +1 -1
  82. data/lib/samagotchi/web/public/app.js +5 -1
  83. data/lib/samagotchi/web/public/block_host.js +74 -199
  84. data/lib/samagotchi/web/public/block_renderers.css +2 -0
  85. data/lib/samagotchi/web/public/block_source.js +7 -4
  86. data/lib/samagotchi/web/public/block_watch.js +256 -0
  87. data/lib/samagotchi/web/public/composer_fold.js +3 -0
  88. data/lib/samagotchi/web/public/index.html +28 -9
  89. data/lib/samagotchi/web/public/inline_widgets.js +146 -0
  90. data/lib/samagotchi/web/public/owner_verify.js +1 -1
  91. data/lib/samagotchi/web/public/participant.js +1 -1
  92. data/lib/samagotchi/web/public/revise_stack.js +1 -1
  93. data/lib/samagotchi/web/public/stage_watch.js +6 -2
  94. data/lib/samagotchi/web/public/text_roots.js +1 -1
  95. data/lib/samagotchi/web/public/verify_target.js +1 -1
  96. data/lib/samagotchi/web/public/widget_picks.js +130 -0
  97. data/lib/samagotchi/web/public/widget_walk.js +230 -0
  98. data/lib/samagotchi/web/public/widgets.css +70 -0
  99. metadata +17 -1
@@ -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}}
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.1
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
@@ -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,6 +876,7 @@ 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
870
882
  - lib/samagotchi/web/public/card_stamps.js
@@ -907,6 +919,7 @@ files:
907
919
  - lib/samagotchi/web/public/info_bar.js
908
920
  - lib/samagotchi/web/public/info_bar_model.js
909
921
  - lib/samagotchi/web/public/init_row.js
922
+ - lib/samagotchi/web/public/inline_widgets.js
910
923
  - lib/samagotchi/web/public/keep_focus.js
911
924
  - lib/samagotchi/web/public/kept_commands.js
912
925
  - lib/samagotchi/web/public/latest.js
@@ -1024,6 +1037,9 @@ files:
1024
1037
  - lib/samagotchi/web/public/verify_preset.js
1025
1038
  - lib/samagotchi/web/public/verify_target.js
1026
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
1027
1043
  - lib/samagotchi/web/public/worker_features.js
1028
1044
  - lib/samagotchi/web/public/working_set.js
1029
1045
  - lib/samagotchi/web/public/working_set_rail.js