samagotchi 0.136.1 → 0.138.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -1
  3. data/docs/architecture.md +2 -2
  4. data/docs/cli.md +15 -7
  5. data/docs/context.md +7 -9
  6. data/docs/guardrails.md +12 -5
  7. data/docs/internals/engine.md +4 -4
  8. data/docs/internals/participants.md +2 -1
  9. data/docs/internals/web-frontend.md +19 -9
  10. data/docs/plugins.md +2 -2
  11. data/docs/sessions.md +8 -8
  12. data/docs/testing.md +2 -1
  13. data/lib/samagotchi/bridge.rb +4 -0
  14. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  15. data/lib/samagotchi/desktop/macos.rb +3 -2
  16. data/lib/samagotchi/engine.rb +6 -1
  17. data/lib/samagotchi/guardrail_wiring.rb +3 -1
  18. data/lib/samagotchi/guardrails/chi_dirs.rb +2 -2
  19. data/lib/samagotchi/guardrails/mise_paths.rb +36 -0
  20. data/lib/samagotchi/guardrails/outside.rb +5 -5
  21. data/lib/samagotchi/guardrails/pair_boundary.rb +11 -4
  22. data/lib/samagotchi/guardrails/pair_command.rb +3 -19
  23. data/lib/samagotchi/guardrails/protected_paths.rb +5 -2
  24. data/lib/samagotchi/guardrails/review_paths.rb +3 -2
  25. data/lib/samagotchi/guardrails/shell_paths.rb +0 -4
  26. data/lib/samagotchi/guardrails/verdict.rb +1 -1
  27. data/lib/samagotchi/version.rb +1 -1
  28. data/lib/samagotchi/web/message_display.rb +6 -3
  29. data/lib/samagotchi/web/participant_review.rb +2 -1
  30. data/lib/samagotchi/web/public/answer_fold.js +24 -6
  31. data/lib/samagotchi/web/public/app.js +24 -19
  32. data/lib/samagotchi/web/public/chains.js +6 -5
  33. data/lib/samagotchi/web/public/chip_popover.js +20 -42
  34. data/lib/samagotchi/web/public/composer.css +15 -11
  35. data/lib/samagotchi/web/public/composer_grip.js +11 -13
  36. data/lib/samagotchi/web/public/composer_mount.js +2 -2
  37. data/lib/samagotchi/web/public/context_chips.js +29 -98
  38. data/lib/samagotchi/web/public/copy.js +8 -5
  39. data/lib/samagotchi/web/public/data.js +2 -0
  40. data/lib/samagotchi/web/public/families_view.js +30 -38
  41. data/lib/samagotchi/web/public/index.html +176 -132
  42. data/lib/samagotchi/web/public/info_bar.js +19 -8
  43. data/lib/samagotchi/web/public/info_bar_model.js +3 -1
  44. data/lib/samagotchi/web/public/lists_view.js +69 -42
  45. data/lib/samagotchi/web/public/participants_panel.js +4 -4
  46. data/lib/samagotchi/web/public/popover_modal.js +57 -0
  47. data/lib/samagotchi/web/public/revise_stack.js +76 -6
  48. data/lib/samagotchi/web/public/revise_switch.js +55 -0
  49. data/lib/samagotchi/web/public/scope.js +3 -3
  50. data/lib/samagotchi/web/public/send_turn.js +4 -2
  51. data/lib/samagotchi/web/public/settings_chip.js +2 -2
  52. data/lib/samagotchi/web/public/stage_view.js +51 -2
  53. data/lib/samagotchi/web/public/stale_params.js +26 -0
  54. data/lib/samagotchi/web/public/start_settings.js +1 -1
  55. data/lib/samagotchi/web/public/start_toolkit.js +1 -1
  56. data/lib/samagotchi/web/public/stream_handlers.js +10 -5
  57. data/lib/samagotchi/web/public/working_set_rail.js +2 -0
  58. data/lib/samagotchi/web/public/working_set_view.js +2 -2
  59. data/lib/samagotchi/web/session_summary.rb +1 -1
  60. metadata +5 -1
@@ -121,8 +121,10 @@ export function createSendTurn({
121
121
  turnDom.handOff({ immediate: true });
122
122
  const sending = composer.chips().slice();
123
123
  // Into a running turn the echo says what it waits for (its delivery); a
124
- // line with images always waits for the turn's end (the worker queues it).
125
- const shownDelivery = isRunning ? (sending.length ? "queue" : delivery || "next_step") : null;
124
+ // line with images, or a Verify or a Revise, always waits for the turn's
125
+ // end (the worker queues it).
126
+ const anchored = !!verify() || !!revise();
127
+ const shownDelivery = isRunning ? (sending.length || anchored ? "queue" : delivery || "next_step") : null;
126
128
  const echo = makeUserBubble(prompt, { state: "queued", own: true, images: sending, delivery: shownDelivery });
127
129
  if (!turnDom.holds()) historyEl.scrollTop = historyEl.scrollHeight;
128
130
  if (!store.current.firstPreview || store.current.firstPreview === "—") {
@@ -77,7 +77,7 @@ export function chipTemplate(summaries) {
77
77
  const chip = summaries && chipModel(summaries);
78
78
  if (!chip) return nothing;
79
79
  return html`<button type="button" class=${`model settings-chip${chip.own ? " own" : ""}`} title=${chip.title}
80
- aria-label=${chip.label} aria-haspopup="dialog" data-settings-chip><span class="chip-icon sc-gear" aria-hidden="true">⚙</span>${
80
+ aria-label=${chip.label} aria-haspopup="dialog" aria-expanded="false" data-settings-chip><span class="chip-icon sc-gear" aria-hidden="true">⚙</span>${
81
81
  chip.text ? html`<span class="sc-own"> ${chip.text}</span>` : nothing}</button>`;
82
82
  }
83
83
 
@@ -197,7 +197,7 @@ export function createSettingsControl({ el, summaries, session = () => null, run
197
197
  };
198
198
  const shell = createChipPopover({
199
199
  id, className: "ctx-popover start-settings session-settings", width: 440, el, anchor, summary: present, session,
200
- show, sheet: true, sheetShare: SHEET_TALL, modal: true, label: PANEL_LABEL, onOpen: focusInside,
200
+ show, sheet: true, sheetShare: SHEET_TALL, label: PANEL_LABEL, onOpen: focusInside,
201
201
  onClose: () => { shown = null; error = null; }, onSubmit: submit, onAct: act, doc, win,
202
202
  });
203
203
  // An edit makes the disclosure Set's error stale.
@@ -57,10 +57,12 @@ function readCollapsed() {
57
57
  // on a running task_wait. +renderMarkdown+ and +linkRefs+: the asking
58
58
  // slot's markdown read and ref pass (stage_slots.js). +workingSet+: the
59
59
  // page's working set (working_set_view.js, web.working_set; null: off): its
60
- // rail in the live slots, its footprint at the hand-off.
60
+ // rail in the live slots, its footprint at the hand-off. +onCancel+: the
61
+ // status row's Stop turn (a promise of the cancel request; the button is
62
+ // disabled until it settles); null: no Stop turn.
61
63
  export function createStageView({
62
64
  historyEl, isNearBottom: historyNearBottom, sawText, sessionId, dockEl, centerEl, taskStop = null,
63
- renderMarkdown, linkRefs, workingSet = null,
65
+ renderMarkdown, linkRefs, workingSet = null, onCancel = null,
64
66
  }) {
65
67
  // The dock becomes a clear column: the stage's card, then the composer's
66
68
  // (its rows moved into .dock-card, which wears the glass and the focus ring).
@@ -82,6 +84,22 @@ export function createStageView({
82
84
  const miniTool = node("span", "ts-mini", status);
83
85
  const defer = node("span", "ts-defer", status);
84
86
  defer.textContent = "moves up when you leave";
87
+ // Stop turn, at the row's right edge beside the fold: while the turn
88
+ // hasn't ended (refresh), in a collapsed stage too (the row stays).
89
+ const cancel = node("button", "ts-cancel", status);
90
+ cancel.type = "button";
91
+ cancel.title = "Cancel the running turn, whoever sent it";
92
+ cancel.textContent = "Stop turn";
93
+ cancel.hidden = true;
94
+ cancel.addEventListener("click", async () => {
95
+ if (!onCancel || cancel.disabled) return;
96
+ cancel.disabled = true;
97
+ try {
98
+ await onCancel();
99
+ } finally {
100
+ cancel.disabled = false;
101
+ }
102
+ });
85
103
  const fold = node("button", "ts-fold", status);
86
104
  fold.type = "button";
87
105
 
@@ -164,6 +182,10 @@ export function createStageView({
164
182
  // A ref's "show in turn" opened a collapsed stage (not kept).
165
183
  let revealed = false;
166
184
  let rafPending = false;
185
+ // A turn the page knows runs (the session says running) with none in the
186
+ // stage to show (a join without its current_turn): the status row alone,
187
+ // so Stop turn stays in reach (runningWithoutTurn).
188
+ let bare = false;
167
189
  let foldTimer = null;
168
190
  // The question room (drawRoom): the pending ask card whose asking text
169
191
  // overflowed its cap, and the turn's raised cap (stage_frame.js; kept
@@ -321,6 +343,7 @@ export function createStageView({
321
343
  if (!blocked && wasWaiting) stage.style.setProperty("--live-held", `${live.offsetHeight}px`);
322
344
  stage.classList.toggle("plain", plain);
323
345
  stage.classList.toggle("ended", !!endKind);
346
+ cancel.hidden = !onCancel || !!endKind;
324
347
  stage.classList.toggle("waiting", blocked);
325
348
  stage.classList.toggle("asking", !!slots.asking);
326
349
  stage.dataset.phase = slots.phase;
@@ -568,6 +591,7 @@ export function createStageView({
568
591
  phaseEl.textContent = "";
569
592
  stepEl.textContent = "";
570
593
  miniTool.textContent = "";
594
+ cancel.hidden = true;
571
595
  chipLabel.textContent = "";
572
596
  slotsView.reset();
573
597
  rail?.reset();
@@ -604,6 +628,7 @@ export function createStageView({
604
628
 
605
629
  // The stage empty and hidden, its frame free.
606
630
  function clearStage() {
631
+ dropBare();
607
632
  cancelFold();
608
633
  emptySlots();
609
634
  stage.hidden = true;
@@ -679,11 +704,19 @@ export function createStageView({
679
704
  if (fold.wasAtEnd) easeScrollTo(historyEl, () => historyEl.scrollHeight - historyEl.clientHeight, { reduce: !fold.animate || reducedMotion() });
680
705
  }
681
706
 
707
+ function dropBare() {
708
+ if (!bare) return;
709
+ bare = false;
710
+ stage.classList.remove("bare");
711
+ delete stage.dataset.phase;
712
+ }
713
+
682
714
  // ── the view interface (app.js) ──────────────────────────────────────────
683
715
 
684
716
  return {
685
717
  turnStarted(data) {
686
718
  if (folding) finishFold();
719
+ dropBare();
687
720
  // A turn that never ended (released, not handed off) leaves its queued
688
721
  // lines waiting still: they stay.
689
722
  const waiting = [...queuedSlot.children].filter((el) => el !== data?.promptBubble);
@@ -829,6 +862,22 @@ export function createStageView({
829
862
  // A turn is in the stage (running, or ended and not handed off yet):
830
863
  // nothing the turn does touches the history's scroll.
831
864
  holds: () => holding,
865
+ // The page's turn runs but the stage holds none (+on+); false: it ended.
866
+ // A turn the stage holds draws its own row: nothing to do then.
867
+ runningWithoutTurn(on) {
868
+ if (holding || folding) return;
869
+ if (!on) {
870
+ if (bare) clearStage();
871
+ return;
872
+ }
873
+ emptySlots();
874
+ bare = true;
875
+ stage.classList.add("bare");
876
+ stage.dataset.phase = "running";
877
+ phaseEl.textContent = "running";
878
+ cancel.hidden = !onCancel;
879
+ show();
880
+ },
832
881
  handOff,
833
882
  root: stage,
834
883
  scrollEl: scroller,
@@ -0,0 +1,26 @@
1
+ // Query parameters the page no longer reads, dropped from the address bar
2
+ // as it loads (app.js, history.replaceState), so a bookmark or a link kept
3
+ // from an older chi stops carrying them: ?view= (the turn view, removed in
4
+ // 0.135.0). The rest of the query stays as written (?dir='s readable
5
+ // slashes too), in its order.
6
+ //
7
+ // A module of its own: static JS is cached at unversioned URLs, and a new
8
+ // export on an old module would blank the page.
9
+
10
+ export const STALE_PARAMS = ["view"];
11
+
12
+ // A query part's name ("view" of "view=turn"), decoded as URLSearchParams
13
+ // reads it.
14
+ function nameOf(part) {
15
+ const raw = part.split("=", 1)[0].replace(/\+/g, " ");
16
+ try { return decodeURIComponent(raw); } catch { return raw; }
17
+ }
18
+
19
+ // +search+ (a location.search) without the stale parameters, "?…" or "";
20
+ // null when it has none (nothing to replace).
21
+ export function withoutStaleParams(search) {
22
+ const parts = String(search || "").replace(/^\?/, "").split("&").filter(Boolean);
23
+ const kept = parts.filter((part) => !STALE_PARAMS.includes(nameOf(part)));
24
+ if (kept.length === parts.length) return null;
25
+ return kept.length ? `?${kept.join("&")}` : "";
26
+ }
@@ -190,7 +190,7 @@ export function createStartSettings({ el, button, openers = `#${button.id}`, thi
190
190
  id: "startSettingsPopover", className: "ctx-popover start-settings", width: 440, el, anchor: `#${button.id}`, openers,
191
191
  // Taller than a plain sheet: the Apply and budget fields or the review
192
192
  // form would push the note under the edge of a 60 % one.
193
- summary: () => ({}), sheet: true, sheetShare: SHEET_TALL, expands: true, modal: true, label: PANEL_LABEL, doc, win,
193
+ summary: () => ({}), sheet: true, sheetShare: SHEET_TALL, label: PANEL_LABEL, doc, win,
194
194
  show: (_data, opening) => {
195
195
  if (opening) {
196
196
  const choice = llmContext.choice();
@@ -109,7 +109,7 @@ export function createStartToolkit({ el, button, openers = `#${button.id}`, tool
109
109
 
110
110
  const shell = createChipPopover({
111
111
  id: "startToolkitPopover", className: "ctx-popover start-toolkit", width: 380, el, anchor: `#${button.id}`, openers,
112
- summary: () => toolkits(), sheet: true, expands: true, modal: true, label: LIST_LABEL, doc, win,
112
+ summary: () => toolkits(), sheet: true, label: LIST_LABEL, doc, win,
113
113
  show: () => draw(),
114
114
  onOpen: focusInside,
115
115
  onClose: () => { error = null; },
@@ -185,7 +185,8 @@ export function createStreamHandlers(deps) {
185
185
  liveTurn.timing.start(data.started_at, data.turn_id || null);
186
186
  }),
187
187
  turn_completed: async (data) => {
188
- // The turn's id: the event names none, its turn_started did.
188
+ // The turn's id: the event names it (an older worker names
189
+ // none: its turn_started did).
189
190
  const turnId = data.turn_id || liveTurn.timing.turnId || null;
190
191
  activityEnded();
191
192
  // A participant's turn: their spend and turns in the panel moved.
@@ -194,11 +195,15 @@ export function createStreamHandlers(deps) {
194
195
  const out = turnOutput(data);
195
196
  if (out) appendChunk(out);
196
197
  if (data.verdict) deps.placeVerdict?.(data.verdict);
197
- // A Revise turn that revised: its prompt takes the stamp, as a reload
198
- // draws it, and the stack folds (revise_stack.js follows the DOM).
199
- if (data.revises?.turn_id && turnId) {
198
+ // Its prompt takes the turn's stamps, as a reload draws them
199
+ // (revise_stack.js follows the DOM): a Revise turn that revised
200
+ // (the stack folds), and a Verify turn (no Revise on its answer, for
201
+ // every viewer, verdict or not; the sender's echo had it already).
202
+ if ((data.revises?.turn_id || data.verify) && turnId) {
200
203
  for (const prompt of findInTurnRoots(".bubble.user[data-turn-id]")) {
201
- if (prompt.dataset.turnId === turnId) prompt.dataset.revises = data.revises.turn_id;
204
+ if (prompt.dataset.turnId !== turnId) continue;
205
+ if (data.revises?.turn_id) prompt.dataset.revises = data.revises.turn_id;
206
+ if (data.verify) prompt.dataset.verify = "1";
202
207
  }
203
208
  }
204
209
  // after_turn hooks are running and may present the answer: its
@@ -100,6 +100,8 @@ export function createRing(parent) {
100
100
  export function createWorkingSetRail({ live, stage, onOpen, onChange }) {
101
101
  const rail = el("div", "ts-ws", live);
102
102
  rail.setAttribute("role", "button");
103
+ rail.setAttribute("aria-haspopup", "dialog");
104
+ rail.setAttribute("aria-expanded", "false");
103
105
  rail.tabIndex = 0;
104
106
  const head = el("div", "ws-head", rail);
105
107
  const name = el("span", "ws-name", head);
@@ -105,7 +105,7 @@ export function footprintTemplate(items, ordinal) {
105
105
  const sep = html` <span class="sep">·</span> `;
106
106
  const parts = fp.labels.map((l, i) => html`${i ? sep : nothing}<span class=${`l ${l.kind}${l.state && l.state !== "live" ? ` ${l.state}` : ""}`} title=${labelTitle(l)}>${l.glyph ? html`<span class="k">${l.glyph}</span> ` : nothing}${labelText({ ...l, glyph: "" })}</span>${footTag(l)}`);
107
107
  const text = footprintText(items, BUDGETS.footprint);
108
- return html`<div class="ws-foot" role="button" tabindex="0" data-ws-turn=${ordinal} data-ws-sig=${footSig(items)} aria-label=${`Working set of this turn: ${text.replace(/^touched /, "")}. Open the list.`}><span class="w">touched</span> ${parts}${fp.tail ? html`${sep}<span class="w">${fp.tail}</span>` : nothing}${fp.edited ? html`${sep}<span class="e">edited ${fp.edited}</span>` : nothing}${fp.stale ? html`${sep}<span class="s">${fp.stale} stale</span>` : nothing}${fp.forgotten ? html`${sep}<span class="f">${fp.forgotten} forgotten</span>` : nothing}${fp.dropped ? html`${sep}<span class="f">${fp.dropped} dropped</span>` : nothing}</div>`;
108
+ return html`<div class="ws-foot" role="button" tabindex="0" aria-haspopup="dialog" aria-expanded="false" data-ws-turn=${ordinal} data-ws-sig=${footSig(items)} aria-label=${`Working set of this turn: ${text.replace(/^touched /, "")}. Open the list.`}><span class="w">touched</span> ${parts}${fp.tail ? html`${sep}<span class="w">${fp.tail}</span>` : nothing}${fp.edited ? html`${sep}<span class="e">edited ${fp.edited}</span>` : nothing}${fp.stale ? html`${sep}<span class="s">${fp.stale} stale</span>` : nothing}${fp.forgotten ? html`${sep}<span class="f">${fp.forgotten} forgotten</span>` : nothing}${fp.dropped ? html`${sep}<span class="f">${fp.dropped} dropped</span>` : nothing}</div>`;
109
109
  }
110
110
 
111
111
  // What a footprint shows that can change after it is drawn: its items'
@@ -186,7 +186,7 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
186
186
 
187
187
  const popover = createChipPopover({
188
188
  id: "workingSetPopover", className: "ctx-popover ws-popover", width: 420, el: infoEl, anchor: ".ws-chip",
189
- openers: ".ws-chip, .ts-ws, .ws-foot", sheet: true,
189
+ openers: ".ws-chip, .ts-ws, .ws-foot", sheet: true, label: "Working set",
190
190
  // The chip opens the session's scope; the rail and a footprint set
191
191
  // theirs before they open it.
192
192
  summary: (anchor) => {
@@ -94,7 +94,7 @@ module Samagotchi
94
94
  memory_index: saved&.memory_index,
95
95
  # Its side requests, apart from the tokens (the tooltip's own line).
96
96
  side: saved&.side,
97
- # Hidden from the strip and the list unless "include archived".
97
+ # Hidden from the strip and the list unless the all view's "archived" toggle is on.
98
98
  archived: ArchiveStore.archived?(session_dir),
99
99
  project_root: session.project_root(cache: root_cache),
100
100
  # A worker is reachable: WorkerLink answers Live (the lock held by
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.136.1
4
+ version: 0.138.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -353,6 +353,7 @@ files:
353
353
  - lib/samagotchi/guardrails/gate.rb
354
354
  - lib/samagotchi/guardrails/implicit_writers.rb
355
355
  - lib/samagotchi/guardrails/load_failures.rb
356
+ - lib/samagotchi/guardrails/mise_paths.rb
356
357
  - lib/samagotchi/guardrails/model_size.rb
357
358
  - lib/samagotchi/guardrails/outside.rb
358
359
  - lib/samagotchi/guardrails/pair_boundary.rb
@@ -843,6 +844,7 @@ files:
843
844
  - lib/samagotchi/web/public/participant_names.js
844
845
  - lib/samagotchi/web/public/participants_panel.js
845
846
  - lib/samagotchi/web/public/plain_line.js
847
+ - lib/samagotchi/web/public/popover_modal.js
846
848
  - lib/samagotchi/web/public/project_picker.js
847
849
  - lib/samagotchi/web/public/prompt_history.js
848
850
  - lib/samagotchi/web/public/push.js
@@ -859,6 +861,7 @@ files:
859
861
  - lib/samagotchi/web/public/review_history.js
860
862
  - lib/samagotchi/web/public/review_start.js
861
863
  - lib/samagotchi/web/public/revise_stack.js
864
+ - lib/samagotchi/web/public/revise_switch.js
862
865
  - lib/samagotchi/web/public/route.js
863
866
  - lib/samagotchi/web/public/scope.js
864
867
  - lib/samagotchi/web/public/scroll.js
@@ -881,6 +884,7 @@ files:
881
884
  - lib/samagotchi/web/public/stage_slots.js
882
885
  - lib/samagotchi/web/public/stage_view.js
883
886
  - lib/samagotchi/web/public/stage_watch.js
887
+ - lib/samagotchi/web/public/stale_params.js
884
888
  - lib/samagotchi/web/public/start_page.js
885
889
  - lib/samagotchi/web/public/start_settings.js
886
890
  - lib/samagotchi/web/public/start_toolkit.js