samagotchi 0.134.0 → 0.136.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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +78 -1
  3. data/bin/chi +2 -3
  4. data/docs/architecture.md +6 -3
  5. data/docs/cli.md +43 -38
  6. data/docs/configuration.md +11 -9
  7. data/docs/guardrails.md +53 -9
  8. data/docs/internals/engine.md +6 -3
  9. data/docs/internals/participants.md +1 -1
  10. data/docs/internals/prompt-caching.md +1 -1
  11. data/docs/internals/web-frontend.md +263 -0
  12. data/docs/internals/working-set.md +2 -2
  13. data/docs/sessions.md +5 -3
  14. data/docs/testing.md +36 -6
  15. data/lib/samagotchi/bridge.rb +11 -6
  16. data/lib/samagotchi/bridge_client.rb +8 -5
  17. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  18. data/lib/samagotchi/config.rb +0 -2
  19. data/lib/samagotchi/engine.rb +69 -12
  20. data/lib/samagotchi/guardrail_wiring.rb +8 -6
  21. data/lib/samagotchi/guardrails/approval.rb +41 -5
  22. data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
  23. data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
  24. data/lib/samagotchi/guardrails/pair_command.rb +387 -78
  25. data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
  26. data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
  27. data/lib/samagotchi/guardrails/shared_git.rb +1 -1
  28. data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
  29. data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
  30. data/lib/samagotchi/guardrails_command.rb +13 -10
  31. data/lib/samagotchi/llm/chat_loop.rb +4 -3
  32. data/lib/samagotchi/message_parts.rb +1 -1
  33. data/lib/samagotchi/revise_anchor.rb +33 -0
  34. data/lib/samagotchi/session_inbox.rb +20 -12
  35. data/lib/samagotchi/session_manager.rb +13 -11
  36. data/lib/samagotchi/system_one/approval_question.rb +21 -16
  37. data/lib/samagotchi/system_one/self_protect.rb +13 -15
  38. data/lib/samagotchi/tools/delegate_relay.rb +6 -5
  39. data/lib/samagotchi/tools/delegate_wait.rb +5 -5
  40. data/lib/samagotchi/tools/execute.rb +6 -6
  41. data/lib/samagotchi/tools/output_guardrails.rb +9 -2
  42. data/lib/samagotchi/tools/read.rb +5 -5
  43. data/lib/samagotchi/turn_anchors.rb +49 -0
  44. data/lib/samagotchi/verify_anchor.rb +5 -5
  45. data/lib/samagotchi/version.rb +1 -1
  46. data/lib/samagotchi/web/app.rb +6 -18
  47. data/lib/samagotchi/web/message_display.rb +33 -1
  48. data/lib/samagotchi/web/participant_access.rb +2 -2
  49. data/lib/samagotchi/web/participant_view.rb +8 -0
  50. data/lib/samagotchi/web/public/activity.js +1 -1
  51. data/lib/samagotchi/web/public/annotations.js +1 -1
  52. data/lib/samagotchi/web/public/answer_fold.js +100 -31
  53. data/lib/samagotchi/web/public/app.js +451 -367
  54. data/lib/samagotchi/web/public/bubbles.js +30 -10
  55. data/lib/samagotchi/web/public/cards_place.js +4 -2
  56. data/lib/samagotchi/web/public/chip_popover.js +2 -2
  57. data/lib/samagotchi/web/public/composer.css +8 -0
  58. data/lib/samagotchi/web/public/context_chips.js +9 -6
  59. data/lib/samagotchi/web/public/data.js +4 -2
  60. data/lib/samagotchi/web/public/dock_reserve.js +13 -15
  61. data/lib/samagotchi/web/public/feature_gates.js +33 -0
  62. data/lib/samagotchi/web/public/history_reveal.js +10 -6
  63. data/lib/samagotchi/web/public/index.html +105 -53
  64. data/lib/samagotchi/web/public/info_bar.js +11 -12
  65. data/lib/samagotchi/web/public/info_bar_model.js +8 -10
  66. data/lib/samagotchi/web/public/latest.js +46 -0
  67. data/lib/samagotchi/web/public/level_chip.js +33 -95
  68. data/lib/samagotchi/web/public/lists_view.js +15 -13
  69. data/lib/samagotchi/web/public/live_timing.js +2 -0
  70. data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
  71. data/lib/samagotchi/web/public/markdown_place.js +1 -1
  72. data/lib/samagotchi/web/public/method_api.js +14 -0
  73. data/lib/samagotchi/web/public/notifications.js +4 -11
  74. data/lib/samagotchi/web/public/participant.js +11 -6
  75. data/lib/samagotchi/web/public/participant_names.js +1 -1
  76. data/lib/samagotchi/web/public/participants_panel.js +4 -2
  77. data/lib/samagotchi/web/public/project_picker.js +18 -22
  78. data/lib/samagotchi/web/public/question_cards.js +2 -2
  79. data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
  80. data/lib/samagotchi/web/public/restore.js +2 -0
  81. data/lib/samagotchi/web/public/revise_stack.js +378 -0
  82. data/lib/samagotchi/web/public/scope.js +12 -20
  83. data/lib/samagotchi/web/public/send_turn.js +29 -16
  84. data/lib/samagotchi/web/public/session_actions.js +15 -14
  85. data/lib/samagotchi/web/public/session_fields.js +152 -0
  86. data/lib/samagotchi/web/public/session_payload.js +35 -0
  87. data/lib/samagotchi/web/public/session_state.js +9 -5
  88. data/lib/samagotchi/web/public/session_store.js +191 -0
  89. data/lib/samagotchi/web/public/settings_chip.js +212 -0
  90. data/lib/samagotchi/web/public/settings_panel.js +121 -0
  91. data/lib/samagotchi/web/public/shell_command.js +26 -0
  92. data/lib/samagotchi/web/public/stage_model.js +4 -4
  93. data/lib/samagotchi/web/public/stage_view.js +11 -16
  94. data/lib/samagotchi/web/public/start_settings.js +25 -88
  95. data/lib/samagotchi/web/public/stream_handlers.js +88 -90
  96. data/lib/samagotchi/web/public/task_stop.js +1 -1
  97. data/lib/samagotchi/web/public/thinking_chip.js +7 -45
  98. data/lib/samagotchi/web/public/time_gaps.js +5 -2
  99. data/lib/samagotchi/web/public/timing.js +15 -5
  100. data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
  101. data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +33 -93
  102. data/lib/samagotchi/web/public/turn_events.js +3 -3
  103. data/lib/samagotchi/web/public/turn_html.js +20 -16
  104. data/lib/samagotchi/web/public/turn_model.js +6 -6
  105. data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
  106. data/lib/samagotchi/web/public/working_set_view.js +3 -1
  107. data/lib/samagotchi/web/server.rb +4 -4
  108. data/lib/samagotchi/web/session_control.rb +2 -2
  109. data/lib/samagotchi/web/session_create.rb +1 -1
  110. data/lib/samagotchi/web/session_show.rb +6 -3
  111. data/lib/samagotchi/worker.rb +9 -9
  112. data/lib/samagotchi/worker_inbound.rb +6 -5
  113. metadata +15 -3
  114. data/lib/samagotchi/web/public/hold.js +0 -78
@@ -1,6 +1,6 @@
1
- // The turn view of a running turn (web.view: turn, and the stage's block):
2
- // one <details class="turn-work"> per turn holding one <details
3
- // class="gen"> per generation, the current one live (open, at the bottom),
1
+ // The turn block of a running turn (the stage's cloud, stage_view.js): one
2
+ // <details class="turn-work"> per turn holding one <details class="gen">
3
+ // per generation, the current one live (open, on top while the turn runs),
4
4
  // the earlier ones collapsed to one line each. At the turn's end the last
5
5
  // generation's narration moves out of the block as the answer bubble, so
6
6
  // markdown, annotate and the turn output dedupe work on a plain bubble.
@@ -38,7 +38,6 @@ import { formatDuration, turnGroups, turnTimingText } from "./timing.js";
38
38
  import { createSentenceFeed } from "./sentences.js";
39
39
  import { createTicker } from "./thinking_ticker.js";
40
40
  import * as model from "./turn_model.js";
41
- import { createHold } from "./hold.js";
42
41
  import { linkedLine } from "./notice_links.js";
43
42
  import { taskCommand, taskStopButton } from "./task_stop.js";
44
43
  import { footprintTemplate, groupItems, stampHistory } from "./working_set_view.js";
@@ -188,32 +187,23 @@ function rowNodes(rows, thumbs) {
188
187
  return nodes;
189
188
  }
190
189
 
191
- // ---- The live view ----
190
+ // ---- The live block ----
192
191
 
193
- // The container seams (the stage view composes this view, stage_view.js):
194
- // blockHost(el) puts the turn's block in the page (default: the history);
195
- // follow() keeps the live part in view (default: the history's
196
- // tail), called only when isNearBottom() said so before
197
- // the change;
192
+ // The container seams (the stage composes the block, stage_view.js):
193
+ // blockHost(el) puts the turn's block in the page (the cloud);
194
+ // follow() keeps the live part in view, called only when
195
+ // isNearBottom() said so before the change;
198
196
  // placeAnswer(pieces, { plain, blockEl }) puts the promoted answer (a
199
- // plain answer's thinking, cards, bubble) in the page
200
- // (default: in the block's place for a plain answer,
201
- // else after it in the history);
202
- // useHold the answer's pop at the narration box's height (hold.js);
203
- // newestFirst a new step goes on top of the block while the turn runs
204
- // (chronological() restores the order);
197
+ // plain answer's thinking, cards, bubble) in the page;
205
198
  // onChange() after anything in the turn changed (the stage redraws
206
199
  // its slots from current());
207
200
  // onThinkingLine(sentence) the live step's ticker showed a new line;
208
201
  // taskStop the page's stopper (task_stop.js): a running task_wait
209
202
  // row gets its stop-task button (none: no button).
210
- export function createTurnView({
211
- historyEl, appendToHistory, isNearBottom, sawText, sessionId,
212
- blockHost = appendToHistory,
213
- follow = () => { historyEl.scrollTop = historyEl.scrollHeight; },
214
- placeAnswer = defaultPlaceAnswer(appendToHistory),
215
- useHold = true,
216
- newestFirst = false,
203
+ // A new step goes on top of the block while the turn runs (chronological()
204
+ // restores the order at its end).
205
+ export function createTurnBlock({
206
+ historyEl, isNearBottom, sawText, sessionId, blockHost, follow, placeAnswer,
217
207
  onChange = () => {},
218
208
  onThinkingLine = () => {},
219
209
  taskStop = null,
@@ -229,9 +219,6 @@ export function createTurnView({
229
219
  let pendingSteers = [];
230
220
  const dirty = new Set();
231
221
  let rafPending = false;
232
- // The promoted answer bubble while it is held at the box's height (the
233
- // pop, D6).
234
- const hold = createHold({ isNearBottom, follow });
235
222
 
236
223
  function removeHint() {
237
224
  const hint = historyEl.querySelector(".hint");
@@ -256,8 +243,8 @@ export function createTurnView({
256
243
  }
257
244
 
258
245
  // The step rendered again from its state (stepTemplate). Each live step
259
- // is its own lit root, not an item of one block template: newestFirst
260
- // puts a new step on top and chronological() reorders them, and lit
246
+ // is its own lit root, not an item of one block template: a new step
247
+ // goes on top and chronological() reorders them, and lit
261
248
  // (without the keyed repeat directive, not vendored) matches list items
262
249
  // by index, so a block-wide render would hand step N's <details> (and
263
250
  // its hand toggle) to another gen. The root's part lives on entry.end
@@ -319,8 +306,7 @@ export function createTurnView({
319
306
  draw(entry);
320
307
  entry.el = entry.end.previousElementSibling || entry.frag.querySelector("details.gen");
321
308
  const stick = isNearBottom();
322
- if (newestFirst) blockEl.insertBefore(entry.frag, blockSummaryEl.nextSibling);
323
- else blockEl.appendChild(entry.frag);
309
+ blockEl.insertBefore(entry.frag, blockSummaryEl.nextSibling);
324
310
  if (stick) follow();
325
311
  return entry;
326
312
  }
@@ -447,25 +433,8 @@ export function createTurnView({
447
433
  if (k) sawText(k);
448
434
  }
449
435
 
450
- // A step that closes mid-turn (the next one starts) collapses: a warn card
451
- // in it would go out of sight until the turn ends. It moves out of the
452
- // step, right after it in the block (open while the turn runs), where
453
- // turnEnded still finds it and takes it out of the block (leavesBlock).
454
- // The card stays in entry.items while it sits outside the step: safe only
455
- // because the step's items are never removed or reordered after this (a
456
- // late row is appended, a no-op for the parts before it). A render whose
457
- // items shifted would make lit insert the card back into the step (or
458
- // another item into its slot).
459
- function liftWarnCards(gen) {
460
- const entry = genEls.get(gen);
461
- if (!entry) return;
462
- const cards = [...entry.el.querySelectorAll(".plugin-card.warn")];
463
- if (cards.length) entry.end.after(...cards);
464
- }
465
-
466
436
  function apply(r) {
467
437
  if (!r) return;
468
- if (r.closed && r.opened) liftWarnCards(r.closed);
469
438
  if (r.closed) closeGenEl(r.closed);
470
439
  if (r.opened) createGenEl(r.gen);
471
440
  if (r.closed || r.opened) updateBlockSummary();
@@ -579,14 +548,11 @@ export function createTurnView({
579
548
  return true;
580
549
  }
581
550
 
582
- // The answer: the gen's narration leaves the block as a bubble. With
583
- // +heldHeight+ (the live box's height, measured before the step closed) the
584
- // bubble is held at that height, scrolled to its tail, until answerReady()
585
- // (the rendered markdown landed) or 1.5 s reveals it.
551
+ // The answer: the gen's narration leaves the block as a bubble.
586
552
  // +endNote+: the notice of a turn that ended with no answer (app.js
587
553
  // builds it), placed where the answer bubble would go. +turnId+: a
588
554
  // completed turn's id, the bubble's data-answer-turn as a reload's.
589
- function promote(gen, { heldHeight = 0, endNote = null, turnId = null } = {}) {
555
+ function promote(gen, { endNote = null, turnId = null } = {}) {
590
556
  const entry = genEls.get(gen);
591
557
  // No text (an empty answer): no bubble.
592
558
  let bubble = null;
@@ -596,8 +562,6 @@ export function createTurnView({
596
562
  bubble.textContent = gen.text;
597
563
  if (turnId) bubble.dataset.answerTurn = turnId;
598
564
  }
599
- // A hidden tab gets no pop: nobody sees it, and its timers are throttled.
600
- const holding = useHold && heldHeight > 0 && !!bubble && !document.hidden;
601
565
  model.takeAnswer(turn, gen);
602
566
  draw(entry);
603
567
  // Cards and hook notices in a step that goes (below, or with the whole
@@ -627,14 +591,6 @@ export function createTurnView({
627
591
  } else {
628
592
  placeAnswer(bubble ? [...cards, bubble] : [...cards, ...(endNote ? [endNote] : [])], { plain: false, blockEl });
629
593
  }
630
- if (holding) {
631
- // Bubbles are border-box: the box's height plus the bubble's own
632
- // padding and border keeps the same three lines in view.
633
- const cs = getComputedStyle(bubble);
634
- const chrome = ["paddingTop", "paddingBottom", "borderTopWidth", "borderBottomWidth"]
635
- .reduce((sum, k) => sum + (parseFloat(cs[k]) || 0), 0);
636
- hold.start(bubble, heldHeight + chrome);
637
- }
638
594
  }
639
595
 
640
596
  // Returns the cards that left the block (card.js leavesBlock), detached:
@@ -652,16 +608,9 @@ export function createTurnView({
652
608
  // turn without tools leaves the block in any case (canceled, it is the
653
609
  // partial text bubble).
654
610
  const promoting = last && (kind === "completed" || (turn.gens.length === 1 && last.tools.length === 0));
655
- // The box's height, measured while it is still in the DOM and the step
656
- // still open: the apply below swaps it for the full text and collapses
657
- // the step. Only a completed turn's answer pops; a canceled or failed
658
- // one shows at once.
659
- const lastEntry = promoting && kind === "completed" ? genEls.get(last) : null;
660
- const box = lastEntry ? narrationEl(lastEntry) : null;
661
- const heldHeight = box ? box.getBoundingClientRect().height : 0;
662
611
  if (pendingSteers.length) flushSteers();
663
612
  apply(model.turnEnded(turn));
664
- if (promoting) promote(last, { heldHeight, endNote, turnId: kind === "completed" ? turnId : null });
613
+ if (promoting) promote(last, { endNote, turnId: kind === "completed" ? turnId : null });
665
614
  else if (endNote) placeAnswer([endNote], { plain: false, blockEl });
666
615
  const kept = [];
667
616
  if (blockEl) {
@@ -669,7 +618,7 @@ export function createTurnView({
669
618
  blockEl.classList.add("done");
670
619
  // The outline (turn_model.js outlineRuns): open on its narrated steps'
671
620
  // rows, as a reload draws it; a block with nothing said closes.
672
- if (newestFirst) chronological();
621
+ chronological();
673
622
  const outlined = outlineBlock(blockEl, model.blockGens(turn));
674
623
  if (!blockToggled) blockEl.open = outlined;
675
624
  for (const el of blockEl.querySelectorAll(".plugin-card")) {
@@ -683,8 +632,8 @@ export function createTurnView({
683
632
  return kept;
684
633
  }
685
634
 
686
- // The steps back in chronological order after newestFirst (a no-op
687
- // otherwise); a fold (outlineBlock) stays before its run's first step.
635
+ // The steps back in chronological order (they ran newest first); a fold
636
+ // (outlineBlock) stays before its run's first step.
688
637
  function chronological() {
689
638
  if (!blockEl || !turn) return;
690
639
  for (const gen of turn.gens) {
@@ -698,7 +647,6 @@ export function createTurnView({
698
647
  }
699
648
 
700
649
  function reset() {
701
- hold.finish();
702
650
  turn = null;
703
651
  blockEl = null;
704
652
  blockSummaryEl = null;
@@ -777,9 +725,6 @@ export function createTurnView({
777
725
  steer,
778
726
  card,
779
727
  flush,
780
- // The rendered answer landed (or the re-read failed): let the held
781
- // bubble pop. A no-op with nothing held.
782
- answerReady() { hold.reveal(); },
783
728
  // The live gen's narration box shows complete sentences, its thinking is
784
729
  // appended by delta (the ticker); a selection in either is refused while
785
730
  // the step is live — the quote lands once it closes (then on the full
@@ -792,9 +737,8 @@ export function createTurnView({
792
737
  return !!text?.contains(el) || !!entry.thinkingText?.parentNode?.contains(el);
793
738
  },
794
739
  reset,
795
- // For a view that composes this one (the stage): the turn's model and
796
- // block, a step's elements by index, and the steps back in
797
- // chronological order after newestFirst.
740
+ // For the stage that composes the block: the turn's model and block, a
741
+ // step's elements by index, and the steps back in chronological order.
798
742
  current() { return { turn, blockEl }; },
799
743
  // The running turn's asking reply (turn_model.js askingText): what a
800
744
  // question or approval card keeps once answered.
@@ -811,15 +755,6 @@ export function createTurnView({
811
755
  };
812
756
  }
813
757
 
814
- // The turn view's answer placement: a plain answer's pieces in its block's
815
- // place (the block goes), else after the block in the history.
816
- function defaultPlaceAnswer(appendToHistory) {
817
- return (pieces, { plain, blockEl }) => {
818
- if (plain) blockEl.replaceWith(...pieces);
819
- else pieces.forEach((el) => appendToHistory(el));
820
- };
821
- }
822
-
823
758
  // ---- A reload ----
824
759
 
825
760
  // The text of a reloaded block's step +iteration+ (1-based, as the snapshot's
@@ -842,15 +777,15 @@ function failedLineTemplate(record) {
842
777
  return html`<div class="bubble cancel">${failedTurnText(failure)}</div>`;
843
778
  }
844
779
 
845
- // A reloaded history in the turn view (renderHistory's object path), as
780
+ // A reloaded history as turn blocks (renderHistory's object path), as
846
781
  // nodes: per turn the prompt, a collapsed block of its steps when it had
847
782
  // any (stepTemplate, the live page's: texts, and with the server's parts
848
783
  // the thinking, params and output, from the saved messages; status and
849
784
  // duration from the timing's tool records), then the answer and the turn's
850
785
  // timing line under it.
851
786
  // +thumbs(images)+ draws a prompt's or a tool row's image thumbs (a
852
- // template, images.js thumbsTemplate). +workingSet+ (the stage view's
853
- // web.working_set, never the turn view's): each block stamped with its
787
+ // template, images.js thumbsTemplate). +workingSet+ (the stage's
788
+ // web.working_set): each block stamped with its
854
789
  // turn's ordinal and its rows with theirs (working_set_view.js), and the
855
790
  // turn's footprint line after it.
856
791
  export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, now = Date.now() }) {
@@ -905,6 +840,8 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
905
840
  if (group.kind === "note") { parts.push(noteBubbleTemplate(items[group.i])); continue; }
906
841
  if (group.kind === "steer") { parts.push(steerTemplate(group.i, true)); continue; }
907
842
  if (group.kind === "answer") { parts.push(answerTemplate(group.answer, nothing)); continue; }
843
+ // A `!cmd`'s record: its command bubble, no steps, no timing.
844
+ if (group.kind === "command") { parts.push(userTemplate(items[group.i])); continue; }
908
845
  turnOrdinal += 1;
909
846
  const prompt = items[group.user];
910
847
  // A context wake turn starts with its note.
@@ -946,8 +883,11 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
946
883
  // followed by the text that answered it when that isn't the answer: a
947
884
  // small model tells its user about a report before its next tool call,
948
885
  // and that step's text would stay folded in the block.
886
+ // A report's fold key (answer_fold.js), as its reply's: the turn's id
887
+ // and its place after the opening; none in a turn saved before turn ids.
888
+ const lineKey = (i) => (group.turnId && items[i]?.source === "delegate_report" ? `${group.turnId}:input:${i - group.user}` : null);
949
889
  parts.push((group.merged || []).map((i) =>
950
- html`${userTemplate(items[i], { state: "steered", step: items[i].step })}${reportReplyTemplate(i, group)}`));
890
+ html`${userTemplate(items[i], { state: "steered", step: items[i].step, lineKey: lineKey(i) })}${reportReplyTemplate(i, group)}`));
951
891
  // A turn that ended with no answer: chi's muted notice where the answer
952
892
  // would be, before the timing, as live.
953
893
  if (group.emptyAnswer != null) parts.push(html`${emptyAnswerTemplate(items[group.emptyAnswer])}${timingTemplate(group)}`);
@@ -21,7 +21,7 @@ export function workerGoneText({ turnRunning, stopped = false }) {
21
21
  return stopped ? "\u25A0 canceled (session stopped)" : "\u2715 canceled (worker exited)";
22
22
  }
23
23
 
24
- // Whether the turn view ends now, or waits for the session re-read: a stream
24
+ // Whether the running turn's DOM ends now, or waits for the session re-read: a stream
25
25
  // that was open and dropped with no stop (a phone briefly offline) says
26
26
  // nothing about the worker, and `resync` re-renders the turn and adds the end
27
27
  // line only when no worker took it — ending the view first is a wrong flash.
@@ -328,7 +328,7 @@ export function promptOps(event, { myId, known, unmatchedMerge = false, wakeStar
328
328
  case "pending_input_merged":
329
329
  if (!unmatchedMerge || !event.content) return [];
330
330
  if (unmatchedMerge === "report") {
331
- return [{ op: "add", enqueuedId: null, prompt: event.content, state: wakeStart ? null : "steered", label: clientLabel(REPORT_CLIENT) }];
331
+ return [{ op: "add", enqueuedId: null, prompt: event.content, state: wakeStart ? null : "steered", label: clientLabel(REPORT_CLIENT), source: "delegate_report" }];
332
332
  }
333
333
  return [{ op: "add", enqueuedId: null, prompt: event.content, state: "steered", label: null }];
334
334
  default:
@@ -380,7 +380,7 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], queued_
380
380
  closeText();
381
381
  events.push({ type: "generation_started", iteration: part.iteration ?? null });
382
382
  break;
383
- // The chunks carry the part's iteration, so the turn view groups a
383
+ // The chunks carry the part's iteration, so the turn block groups a
384
384
  // join like the live stream (turn_model.js).
385
385
  case "thinking":
386
386
  events.push({ type: "generation_chunk", text: "", thinking: part.text, iteration: part.iteration ?? null });
@@ -141,7 +141,7 @@ function tailClip(text) {
141
141
  // edit's collapsed diff, and a reloaded row's ✂ mark above its output. A
142
142
  // part with nothing to say is left out. A reload renders toolRowTemplate;
143
143
  // the live view renders toolRowInnerTemplate into its own row element
144
- // (turn_view.js), again on each event: lit writes only what changed, so a
144
+ // (turn_block.js), again on each event: lit writes only what changed, so a
145
145
  // part the user toggled (the raw command, the diff, the read lines) stays.
146
146
  // +stop+: the live row's stop-task button (a node), after the title.
147
147
  export function toolRowTemplate(row, opts = {}) {
@@ -214,18 +214,8 @@ export function stateBadgeTemplate(state, delivery = null) {
214
214
  return html`<span class="state-badge">${text}${remove}</span>`;
215
215
  }
216
216
 
217
- // A `!cmd`'s saved user message (the worker appends "!(<command>)\n<output>"
218
- // for the next turn): its command line and its output, or null when the
219
- // content is any other message. The command sits on the first line inside
220
- // `!(` `)`; the output is everything after that first newline. A message
221
- // that merely starts with "!" or "!(" without the newline shape is a plain
222
- // message.
223
- export function shellCommandMessage(content) {
224
- const text = String(content ?? "");
225
- const match = text.match(/^!\((.*)\)\n([\s\S]*)$/);
226
- if (!match) return null;
227
- return { line: `!${match[1]}`, text: match[2] };
228
- }
217
+ // A `!cmd`'s saved user message as its line and output (shell_command.js).
218
+ export { shellCommandMessage } from "./shell_command.js";
229
219
 
230
220
  // A `!cmd`'s history bubble: the same markup the live page draws for a
231
221
  // command (app.js's drawCommandBubble): the line, the output in a
@@ -313,13 +303,27 @@ export function steerRowTemplate({ source, text } = {}, { bubble = false } = {})
313
303
  // +delivery+: how it was sent into a running turn (next_step, cut, queue):
314
304
  // its data-delivery and its queued badge's words. +author+: a
315
305
  // participant's badge (participant_names.js authorBadge: their name and
316
- // colour) in place of +label+.
317
- export function userBubbleTemplate(message, { label = null, state = null, step = null, thumbs = nothing, own = false, enqueuedId = null, key = null, delivery = null, author = null } = {}) {
306
+ // colour) in place of +label+. A turn's opening carries its Revise stamp
307
+ // (+message.revises+: data-revises, revise_stack.js) and whether it was a
308
+ // Verify turn (+message.verify+: data-verify, no Revise on its answer).
309
+ // +message.source+ (a delegate report's "delegate_report") is its
310
+ // data-source; +lineKey+, a merged line's fold key (answer_fold.js:
311
+ // data-line-key, the reload's "<turn id>:input:<place>").
312
+ export function userBubbleTemplate(message, { label = null, state = null, step = null, thumbs = nothing, own = false, enqueuedId = null, key = null, delivery = null, author = null, lineKey = null } = {}) {
318
313
  const named = BADGES[state] ? state : null;
319
314
  const from = author
320
315
  ? 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>`
321
316
  : label ? html`<span class="origin-label">${label}</span>` : nothing;
322
- 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-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing} data-delivery=${delivery || nothing}>${from}<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>${stateBadgeTemplate(named, delivery)}</div>`;
317
+ 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>`;
318
+ }
319
+
320
+ // A user line's text: plain (its quotes set apart), or the server's
321
+ // rendered markdown when the line has it (a delegate report's,
322
+ // message_display.rb; a live line has none until a reload). Its copy
323
+ // source stays the raw text either way (data-copy-source).
324
+ function userMessageTemplate(message, thumbs) {
325
+ if (typeof message?.html === "string") return html`<div class="user-message markdown" .innerHTML=${message.html}></div>`;
326
+ return html`<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>`;
323
327
  }
324
328
 
325
329
  // The notice of a turn that ended with no answer (the server's
@@ -1,8 +1,8 @@
1
- // The generations of one turn, as the turn view shows them: a list of gens
1
+ // The generations of one turn, as the turn block shows them: a list of gens
2
2
  // (one per model generation: its thinking, its narration, its tool rows),
3
3
  // the last one live until the next opens or the turn ends. Pure: fed by the
4
4
  // same events live and on a join (turn_events.js snapshotEvents), no DOM, so
5
- // `node --test` covers the grouping rules. turn_view.js draws it.
5
+ // `node --test` covers the grouping rules. turn_block.js draws it.
6
6
  //
7
7
  // Rules:
8
8
  // - turn_started opens the first gen (no iteration yet).
@@ -177,7 +177,7 @@ export function askingText(turn) {
177
177
  // A plugin's refs on the reply that asks (a question's refs, from its
178
178
  // pending question): kept on the asking step's gen, the last one, while
179
179
  // the turn goes on, for the stage's asking slot (askingRefs) and the
180
- // step's closed text (turn_view.js). A non-array or an empty list keeps
180
+ // step's closed text (turn_block.js). A non-array or an empty list keeps
181
181
  // what the gen has. The gen, or null.
182
182
  export function questionRefs(turn, refs) {
183
183
  if (!turn || turn.ended || !Array.isArray(refs) || !refs.length) return null;
@@ -202,7 +202,7 @@ export function turnEnded(turn) {
202
202
  return { gen: null, opened: false, closed };
203
203
  }
204
204
 
205
- // The turn's answer is +gen+'s text, moved out of the block (turn_view.js
205
+ // The turn's answer is +gen+'s text, moved out of the block (turn_block.js
206
206
  // promote): a gen left with no thinking and no tool rows is no step then,
207
207
  // as it is no row, and as a reload counts it (turnGroups).
208
208
  export function takeAnswer(turn, gen) {
@@ -244,7 +244,7 @@ export function genLabel(gen) {
244
244
  // genLabel in its parts: +head+, +calls+ ("3 tool calls (1 failed)", or ""
245
245
  // without calls) and +repeats+: the head is words the open step's body
246
246
  // starts with (its narration, its first call's title), so an open step's
247
- // summary can say only its calls (turn_view.js labelHtml).
247
+ // summary can say only its calls (turn_block.js labelHtml).
248
248
  export function genLabelParts(gen) {
249
249
  const line = String(gen.text || "").split("\n").map(plainLine).find(Boolean) || "";
250
250
  const calls = gen.tools.length ? toolCallsText(gen.tools) : "";
@@ -299,7 +299,7 @@ export function outlineRuns(gens) {
299
299
  }
300
300
 
301
301
  // A finished turn's steps as its block holds them: the answer's step left it
302
- // when the answer was all it had (turn_view.js promote).
302
+ // when the answer was all it had (turn_block.js promote).
303
303
  export function blockGens(turn) {
304
304
  return turn.gens.filter((gen) => !(gen.answer && !gen.thinking && gen.tools.length === 0));
305
305
  }
@@ -132,6 +132,8 @@ async function startFresh(card, button, fresh) {
132
132
  }
133
133
  if (result?.ok) {
134
134
  if (fresh.errors) delete fresh.errors[of];
135
+ // In place, on purpose: on the owner page fresh.running is the session
136
+ // store's verifyFresh (session_fields.js, D8).
135
137
  if (fresh.running && !fresh.running.some((entry) => runningOf(entry) === of)) {
136
138
  fresh.running.push(result.child_id ? { of, child_id: result.child_id } : of);
137
139
  }
@@ -39,6 +39,7 @@
39
39
  // finds the row by them, never by a row key.
40
40
  import { html, nothing, render, svg } from "./vendor/lit-html.js";
41
41
  import { createChipPopover } from "./chip_popover.js";
42
+ import { stillOn } from "./latest.js";
42
43
  import { showRow } from "./refs.js";
43
44
  import {
44
45
  addedItems, addLine, BUDGETS, dropRefusal, fold, footprint, footprintText, forgetOn, GLYPHS, INVERSE, isFresh, itemsOfTurn, labelText,
@@ -487,9 +488,10 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
487
488
  const id = session();
488
489
  if (!getWorkingSet || !id) return;
489
490
  const asked = events;
491
+ const still = stillOn(session, id);
490
492
  Promise.resolve()
491
493
  .then(() => getWorkingSet(id))
492
- .then((snapshot) => { if (session() === id) takeServer(snapshot, { overtaken: events !== asked }); })
494
+ .then((snapshot) => { if (still()) takeServer(snapshot, { overtaken: events !== asked }); })
493
495
  .catch(() => {});
494
496
  }
495
497
 
@@ -92,7 +92,7 @@ module Samagotchi
92
92
  # on while the server runs
93
93
  # @return [Integer] the exit status
94
94
  def self.launch(port: nil, host: nil, scope: "project", start_dir: nil, open_browser: false, markdown: false,
95
- view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
95
+ annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
96
96
  editor: Config::BY_KEY["web.editor"].default, new_token: false,
97
97
  working_set: Config::BY_KEY["web.working_set"].default, tunnel: nil, start_dir_from: :flag,
98
98
  restarted: false, restart_argv: ["web"], exec: nil)
@@ -152,7 +152,7 @@ module Samagotchi
152
152
  # would refuse, or a key store others can read, stops it here,
153
153
  # before any tunnel goes up.
154
154
  Push.preflight!(public_url)
155
- options = { port: port, host: host, url: url, open_browser: open_browser, markdown: markdown, view: view,
155
+ options = { port: port, host: host, url: url, open_browser: open_browser, markdown: markdown,
156
156
  annotate_presets: annotate_presets, editor: editor, lan: lan, working_set: working_set,
157
157
  public_url: public_url, start_dir: start_dir }
158
158
  started = if plan
@@ -345,7 +345,7 @@ module Samagotchi
345
345
  # @return [Boolean, :restart] false when the port was taken (said so on stderr);
346
346
  # :restart when the owner requested an in-place process replacement
347
347
  def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
348
- view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
348
+ annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
349
349
  editor: Config::BY_KEY["web.editor"].default, token_path: Token.path,
350
350
  working_set: Config::BY_KEY["web.working_set"].default, public_url: nil, public_notes: [],
351
351
  on_bound: nil, start_dir: Dir.pwd, update_check: nil)
@@ -362,7 +362,7 @@ module Samagotchi
362
362
  lan_option = { ip: lan.ip, token: Token::Source.new(token_path) } if lan
363
363
  public_token = Token::Source.new(token_path) if public_url
364
364
  push = push_runtime(public_url, state_dir)
365
- app = App.new(manager: manager, state_dir: state_dir, markdown: markdown, view: view,
365
+ app = App.new(manager: manager, state_dir: state_dir, markdown: markdown,
366
366
  annotate_presets: annotate_presets, editor: editor, hub: hub, lan: lan_option, working_set: working_set,
367
367
  public_url: public_url, public_token: public_token, push: push, start_dir: start_dir)
368
368
  webrick = nil
@@ -196,11 +196,11 @@ module Samagotchi
196
196
 
197
197
  # no_interrupt: an attached TUI's --no-interrupt (chi --remote --attach).
198
198
  # author: the principal's (nil for the owner), never the body's.
199
- verify = VerifyAnchor.from(body["verify"])
199
+ # anchors: a Verify turn's claim, a Revise turn's draft (TurnAnchors).
200
200
  result = SessionManager.deliver_turn(id, prompt: prompt.to_s, client_id: client_id, images: images,
201
201
  delivery: delivery, no_interrupt: body["no_interrupt"] == true,
202
202
  author: principal(req).author,
203
- verify: verify,
203
+ anchors: TurnAnchors.from(body),
204
204
  state_dir: @state_dir, manager: @manager,
205
205
  bridge: -> { live_bridge_client(id) })
206
206
  case result.status
@@ -69,7 +69,7 @@ module Samagotchi
69
69
  return error if error
70
70
 
71
71
  # llm_context: the session's own LLM context values before its first
72
- # turn (the start page's llm ctx chip; chi --llm-context), read
72
+ # turn (the start page's ⚙ panel; chi --llm-context), read
73
73
  # before anything spawns so a bad one spawns nothing.
74
74
  llm_context, error = request_llm_context(body["llm_context"])
75
75
  return error if error
@@ -13,6 +13,7 @@ require_relative "../session_commands"
13
13
  require_relative "../session_metrics"
14
14
  require_relative "../session_setting"
15
15
  require_relative "../system_one/log"
16
+ require_relative "../toolkit"
16
17
 
17
18
  module Samagotchi
18
19
  module Web
@@ -151,14 +152,16 @@ module Samagotchi
151
152
  else
152
153
  session_json = session_json.merge(saved_served_model(session, persisted))
153
154
  end
154
- # The per-session settings' chips (the info bar's llm ctx, think,
155
- # reasoning and tools): the worker's, else worked out here from the
155
+ # The per-session settings' chips (the info bar's ⚙ chip: llm ctx, think
156
+ # and reasoning; the tools chip): the worker's, else worked out here from the
156
157
  # session file (SessionSetting#snapshot_value, #saved_summary).
157
158
  SessionSetting::ALL.each do |setting|
158
159
  chip = setting.snapshot_value(snapshot) { setting.saved_summary(session, registry: host_registry) }
159
160
  session_json = session_json.merge(setting.key => chip)
160
161
  end
161
- session_json
162
+ # The 🧰 chip reads the session's toolkit against config's default:
163
+ # on it, the chip is 🧰 alone (a toolkit summary is nil on full only).
164
+ session_json.merge(toolkit_default: Toolkit.configured.name)
162
165
  end
163
166
 
164
167
  # ?cards=1: the page's re-read when a card arrives, for its rendered
@@ -678,7 +678,7 @@ module Samagotchi
678
678
  @initial_turn = true
679
679
  fresh = @session.verify_fresh
680
680
  run_prompt(WorkerInbound::Prompt.new(text: prompt, origin: TurnOrigin.new(enqueued_id: @session.first_turn_id),
681
- verify: fresh&.anchor))
681
+ anchors: { verify: fresh&.anchor }))
682
682
  ensure
683
683
  @initial_turn = false
684
684
  end
@@ -705,10 +705,10 @@ module Samagotchi
705
705
  @continue_offer.drop(origin)
706
706
  @turn_flow.before_prompt_turn
707
707
  run_engine_turn(prompt.text, origin: origin, max_iterations: max_iterations(prompt.no_interrupt),
708
- images: prompt.images, **(prompt.verify ? { verify: prompt.verify } : {})) do |result, error|
708
+ images: prompt.images, **prompt.anchors.kwargs) do |result, error|
709
709
  if error
710
710
  # The Engine announced :turn_failed (with the error's one line).
711
- restore_failed_turn([[prompt.text, origin, prompt.images, prompt.verify], *@merged_this_turn], error: error)
711
+ restore_failed_turn([[prompt.text, origin, prompt.images, prompt.anchors], *@merged_this_turn], error: error)
712
712
  else
713
713
  @continue_offer.after_turn(result, no_interrupt: prompt.no_interrupt)
714
714
  end
@@ -1395,10 +1395,10 @@ module Samagotchi
1395
1395
  # or the restored ones, never the one without the other. A turn that
1396
1396
  # got somewhere stays instead (TurnFlow#prompt_turn_failed), with the
1397
1397
  # prompts it took: nothing goes back.
1398
- # @param prompts [Array<Array(String, TurnOrigin|nil, Array|nil, VerifyAnchor|nil)>]
1399
- # [prompt, origin, images, verify] (a web client gets its image chips
1400
- # back, and a Verify turn's claim, so sending it again still records
1401
- # a verdict)
1398
+ # @param prompts [Array<Array(String, TurnOrigin|nil, Array|nil, TurnAnchors|nil)>]
1399
+ # [prompt, origin, images, anchors] (a web client gets its image chips
1400
+ # back, a Verify turn's claim, so sending it again still records a
1401
+ # verdict, and a Revise turn's draft, so it still stacks)
1402
1402
  # @param error [Exception, nil] what failed: its one line stays in the
1403
1403
  # conversation as a turn note
1404
1404
  # Not saved here: run_engine_turn saves after its block.
@@ -1406,10 +1406,10 @@ module Samagotchi
1406
1406
  @engine.synchronize_events do
1407
1407
  next if @turn_flow.prompt_turn_failed(error: error) == :kept
1408
1408
 
1409
- prompts.each do |prompt, origin, images, verify|
1409
+ prompts.each do |prompt, origin, images, anchors|
1410
1410
  restored = { type: :prompt_restored, prompt: prompt, origin: origin.wire }
1411
1411
  restored[:images] = images unless Array(images).empty?
1412
- restored[:verify] = verify.to_h if verify
1412
+ restored.merge!(anchors.to_h) if anchors
1413
1413
  @engine.announce(restored)
1414
1414
  end
1415
1415
  end
@@ -39,10 +39,11 @@ module Samagotchi
39
39
  # +no_interrupt+: an offer its turn makes keeps it for its continue
40
40
  # turn; +images+: its image refs ({file:, name:}); +delivery+: the wire
41
41
  # value the sender asked for ("queue", or nil for the default
42
- # next_step); +origin+: a TurnOrigin (a Hash is read as one).
43
- Prompt = Data.define(:text, :origin, :no_interrupt, :images, :delivery, :verify) do
44
- def initialize(text:, origin: nil, no_interrupt: false, images: [], delivery: nil, verify: nil)
45
- super(text:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:, verify: VerifyAnchor.from(verify))
42
+ # next_step); +origin+: a TurnOrigin (a Hash is read as one); +anchors+:
43
+ # a Verify turn's claim, a Revise turn's draft (TurnAnchors).
44
+ Prompt = Data.define(:text, :origin, :no_interrupt, :images, :delivery, :anchors) do
45
+ def initialize(text:, origin: nil, no_interrupt: false, images: [], delivery: nil, anchors: nil)
46
+ super(text:, origin: TurnOrigin.of(origin), no_interrupt:, images:, delivery:, anchors: TurnAnchors.from(anchors))
46
47
  end
47
48
  end
48
49
 
@@ -215,7 +216,7 @@ module Samagotchi
215
216
  return if input.images.empty? && queue_as_command(input.prompt, input.origin, File.basename(input_file))
216
217
 
217
218
  yield Prompt.new(text: input.prompt, origin: input.origin, no_interrupt: input.no_interrupt, images: input.images,
218
- delivery: input.delivery, verify: input.verify)
219
+ delivery: input.delivery, anchors: input.anchors)
219
220
  ensure
220
221
  FileUtils.rm_f(claimed_file)
221
222
  end