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
@@ -3,13 +3,20 @@
3
3
  // gets a fold row: a sibling just before the bubble, never inside it (the
4
4
  // bubble's copy text, its claims and a live end's innerHTML stay the
5
5
  // bubble's own). Folded, the bubble has the class `.folded` (CSS hides it;
6
- // no inline style: hold.js strips that) and the row reads its first line
7
- // and its size, "‹first line› · N lines ▸"; unfolded, the row stays as a
8
- // slim "▾ N lines" header.
6
+ // no inline style); the row reads its first line and its size, "▸
7
+ // ‹first line› · N lines", folded or not: unfolded it is the bubble's header,
8
+ // flush on top of it, its bar the bubble's.
9
+ //
10
+ // A delegate report (a user line chi brought in, data-source
11
+ // "delegate_report") gets the same row, folded by default (it is written
12
+ // for the model; the reply under it is the user's summary): the row sits
13
+ // where the report sits and reads "delegate report · ‹first line› · N
14
+ // lines". Its key: a merged one's data-line-key, a wake turn's opening
15
+ // "<turn id>:prompt". A live merged report has no key until a reload.
9
16
  //
10
17
  // Which answers are folded is this browser's choice (localStorage through
11
- // storage.js): one key per session holding the folded answers' keys, the
12
- // last FOLD_KEEP of them. An answer's key is its turn's id
18
+ // storage.js): one key per session holding the folded answers' keys and
19
+ // the unfolded reports' keys, the last FOLD_KEEP of each. An answer's key is its turn's id
13
20
  // (data-answer-turn), a delegate report's reply its own (data-reply-key);
14
21
  // a bubble with neither (an answer before any prompt, a canceled turn's
15
22
  // partial) gets no row. The keys of sessions gone from the session list go
@@ -36,9 +43,11 @@ export const FOLD_PREFIX = "chi_answer_fold:";
36
43
  const LABEL_CHARS = 160;
37
44
 
38
45
  const ROW_CLASS = "answer-fold";
39
- const ANSWERS = ".bubble.output";
40
- // Not done yet: still streaming, or held by the answer pop (hold.js).
41
- const UNFINISHED = ".streaming, .boxed";
46
+ // What gets a row: answers and delegate reports (user lines chi brought in).
47
+ const REPORTS = '.bubble.user[data-source="delegate_report"]';
48
+ const ANSWERS = `.bubble.output, ${REPORTS}`;
49
+ // Not done yet: still streaming.
50
+ const UNFINISHED = ".streaming";
42
51
 
43
52
  // +text+'s lines (a trailing newline ends the last one, it adds none).
44
53
  export function lineCount(text) {
@@ -71,12 +80,12 @@ export function foldLabel(text, { display = false } = {}) {
71
80
  return clip(plainLine(heading ?? texts[0] ?? ""), LABEL_CHARS);
72
81
  }
73
82
 
74
- // The row's words: folded, the label and the size; unfolded, the size.
75
- export function foldRowText(text, { display = false, folded = true } = {}) {
83
+ // The row's words, folded or not: the label and the size (an open row is
84
+ // the answer's header, as an open steps block keeps its summary).
85
+ // +label+: the row's own (a report's, reportLabel), else the answer's.
86
+ export function foldRowText(text, { display = false, label = foldLabel(text, { display }) } = {}) {
76
87
  const n = lineCount(text);
77
88
  const size = `${n} line${n === 1 ? "" : "s"}`;
78
- if (!folded) return { label: "", size };
79
- const label = foldLabel(text, { display });
80
89
  return { label, size: label ? `\u00a0\u00b7 ${size}` : size };
81
90
  }
82
91
 
@@ -86,11 +95,17 @@ export function foldStoreKey(sessionId) {
86
95
  return sessionId ? `${FOLD_PREFIX}${sessionId}` : null;
87
96
  }
88
97
 
98
+ // A session's entry: {root, turns, open}. `turns` holds the answers folded
99
+ // by hand (and Revise stubs opened, revise_stack.js); `open` the delegate
100
+ // reports unfolded by hand (a report folds by default). An entry written
101
+ // before `open` reads as none unfolded.
102
+ const strings = (list) => (Array.isArray(list) ? list.filter((t) => typeof t === "string") : []);
103
+
89
104
  function readEntry(key) {
90
105
  try {
91
106
  const entry = JSON.parse(readStored(key, "null"));
92
- if (!entry || !Array.isArray(entry.turns)) return null;
93
- return { root: typeof entry.root === "string" ? entry.root : null, turns: entry.turns.filter((t) => typeof t === "string") };
107
+ if (!entry || (!Array.isArray(entry.turns) && !Array.isArray(entry.open))) return null;
108
+ return { root: typeof entry.root === "string" ? entry.root : null, turns: strings(entry.turns), open: strings(entry.open) };
94
109
  } catch (_) {
95
110
  return null;
96
111
  }
@@ -103,6 +118,12 @@ export function readFolded(sessionId) {
103
118
  return (key && readEntry(key)?.turns) || [];
104
119
  }
105
120
 
121
+ // The session's delegate reports unfolded by hand, oldest first.
122
+ export function readOpened(sessionId) {
123
+ const key = foldStoreKey(sessionId);
124
+ return (key && readEntry(key)?.open) || [];
125
+ }
126
+
106
127
  // +list+ with +answerKey+ folded (moved to the end) or not, at most
107
128
  // FOLD_KEEP long. Pure.
108
129
  export function withFold(list, answerKey, folded) {
@@ -113,11 +134,23 @@ export function withFold(list, answerKey, folded) {
113
134
  // Saves +answerKey+ folded or not for +sessionId+. +root+: the session's
114
135
  // project root, kept for the sweep of a project's list.
115
136
  export function writeFold(sessionId, answerKey, folded, { root = null } = {}) {
137
+ writeList(sessionId, "turns", answerKey, folded, root);
138
+ }
139
+
140
+ // Saves the report +reportKey+ unfolded (+open+) or not for +sessionId+.
141
+ export function writeOpened(sessionId, reportKey, open, { root = null } = {}) {
142
+ writeList(sessionId, "open", reportKey, open, root);
143
+ }
144
+
145
+ // +member+ in or out of the entry's +list+ ("turns" or "open"), the other
146
+ // list kept; the key goes when both are empty.
147
+ function writeList(sessionId, list, member, inList, root) {
116
148
  const key = foldStoreKey(sessionId);
117
- if (!key || !answerKey) return;
118
- const turns = withFold(readFolded(sessionId), answerKey, folded);
119
- if (!turns.length) removeStored(key);
120
- else writeStored(key, JSON.stringify({ root, turns }));
149
+ if (!key || !member) return;
150
+ const entry = readEntry(key) || { turns: [], open: [] };
151
+ const next = { turns: entry.turns, open: entry.open, [list]: withFold(entry[list], member, inList) };
152
+ if (!next.turns.length && !next.open.length) removeStored(key);
153
+ else writeStored(key, JSON.stringify({ root, turns: next.turns, ...(next.open.length ? { open: next.open } : {}) }));
121
154
  }
122
155
 
123
156
  // The session list loaded (+sessions+, each {id, project_root}): the fold
@@ -137,8 +170,17 @@ export function sweepAnswerFolds(sessions, { scoped = false } = {}) {
137
170
 
138
171
  // ---- The rows ----
139
172
 
140
- // The key an answer bubble is folded by, or null.
173
+ const isReport = (bubble) => !!bubble?.matches?.(REPORTS);
174
+
175
+ // The key an answer bubble is folded by, or null. A delegate report's: a
176
+ // merged one's line key (data-line-key, "<turn id>:input:<place>"), one
177
+ // that opened a wake turn its turn's id and ":prompt" (never the answer's
178
+ // key, the bare id).
141
179
  export function answerKey(bubble) {
180
+ if (isReport(bubble)) {
181
+ const turn = bubble.getAttribute("data-turn-id");
182
+ return bubble.getAttribute("data-line-key") || (turn ? `${turn}:prompt` : null);
183
+ }
142
184
  return bubble?.getAttribute?.("data-answer-turn") || bubble?.getAttribute?.("data-reply-key") || null;
143
185
  }
144
186
 
@@ -146,13 +188,30 @@ function answerText(bubble) {
146
188
  return bubbleCopyText(bubble);
147
189
  }
148
190
 
191
+ // A delegate report's lines before its body: "session: <id>",
192
+ // "status: <s>", "---".
193
+ const REPORT_HEADER = /^(?:(?:session|status):\s|---\s*$)/;
194
+
195
+ // A report row's label: its origin label (the bubble's) and its first
196
+ // heading or first line after the header.
197
+ export function reportLabel(text, origin = "") {
198
+ const body = String(text ?? "").replace(/\r\n?/g, "\n").split("\n").filter((l) => !REPORT_HEADER.test(l.trim())).join("\n");
199
+ const first = foldLabel(body);
200
+ return clip([origin.trim(), first].filter(Boolean).join(" · "), LABEL_CHARS);
201
+ }
202
+
149
203
  function drawRow(row, bubble) {
150
204
  const folded = bubble.classList.contains("folded");
151
205
  const text = answerText(bubble);
152
- const { label, size } = foldRowText(text, { display: bubble.hasAttribute("data-answer-display"), folded });
206
+ const report = isReport(bubble);
207
+ const own = report ? { label: reportLabel(text, bubble.querySelector(":scope > .origin-label")?.textContent || "") } : {};
208
+ const { label, size } = foldRowText(text, { display: bubble.hasAttribute("data-answer-display"), ...own });
209
+ const what = report ? "report" : "answer";
153
210
  row.setAttribute("aria-expanded", folded ? "false" : "true");
154
- row.title = folded ? "Show the answer" : "Fold the answer";
211
+ row.title = folded ? `Show the ${what}` : `Fold the ${what}`;
155
212
  row.classList.toggle("folded", folded);
213
+ row.classList.toggle("report", report);
214
+ row.classList.toggle("steered", report && bubble.classList.contains("steered"));
156
215
  const [labelEl, sizeEl] = row.children;
157
216
  if (labelEl.textContent !== label) labelEl.textContent = label;
158
217
  if (sizeEl.textContent !== size) sizeEl.textContent = size;
@@ -202,14 +261,18 @@ export function installAnswerFold({ historyEl, sessionId, projectRoot = () => nu
202
261
  rows.set(bubble, row);
203
262
  bubbleOf.set(row, bubble);
204
263
  // The stored choice, once: after that the bubble's class is the state
205
- // (a jump that unfolds it, history_reveal.js, isn't saved).
206
- if (stored().includes(key)) bubble.classList.add("folded");
264
+ // (a jump that unfolds it, history_reveal.js, isn't saved). An answer
265
+ // is open unless folded by hand, a report folded unless opened.
266
+ const folded = isReport(bubble) ? !stored().open.includes(key) : stored().turns.includes(key);
267
+ if (folded) bubble.classList.add("folded");
207
268
  }
208
269
  if (bubble.previousElementSibling !== row) historyEl.insertBefore(row, bubble);
209
270
  drawRow(row, bubble);
210
271
  }
211
272
 
212
- // Rows whose bubble left (a re-render, a replaced bubble) go with it.
273
+ // Rows whose bubble left (a re-render, a replaced bubble) go with it; a
274
+ // row something was placed after (a card or a recap placed before a
275
+ // report that opens its turn) goes back just before its bubble.
213
276
  function dropOrphans() {
214
277
  for (const row of historyEl.querySelectorAll(`:scope > .${ROW_CLASS}`)) {
215
278
  const bubble = bubbleOf.get(row);
@@ -219,8 +282,8 @@ export function installAnswerFold({ historyEl, sessionId, projectRoot = () => nu
219
282
  }
220
283
 
221
284
  function decorate(root) {
222
- let folded = null;
223
- const stored = () => (folded ??= readFolded(sessionId()));
285
+ let entry = null;
286
+ const stored = () => (entry ??= { turns: readFolded(sessionId()), open: readOpened(sessionId()) });
224
287
  if (root.matches?.(ANSWERS)) decorateBubble(root, stored);
225
288
  for (const bubble of root.querySelectorAll?.(ANSWERS) || []) decorateBubble(bubble, stored);
226
289
  }
@@ -229,18 +292,22 @@ export function installAnswerFold({ historyEl, sessionId, projectRoot = () => nu
229
292
  // the same frame as its bubble, so nothing below it shifts after a paint.
230
293
  new MutationObserver((records) => {
231
294
  const roots = new Set();
232
- let removed = false;
295
+ let settle = false;
233
296
  for (const r of records) {
234
297
  if (r.type === "attributes") {
235
298
  if (r.target.matches?.(ANSWERS)) roots.add(r.target);
236
299
  continue;
237
300
  }
238
- if (r.removedNodes.length) removed = true;
301
+ if (r.removedNodes.length) settle = true;
239
302
  const answer = r.target.closest?.(ANSWERS);
240
303
  if (answer) roots.add(answer);
241
- for (const n of r.addedNodes) if (n.nodeType === 1 && !n.classList.contains(ROW_CLASS)) roots.add(n);
304
+ for (const n of r.addedNodes) {
305
+ if (n.nodeType !== 1 || n.classList.contains(ROW_CLASS)) continue;
306
+ roots.add(n);
307
+ if (r.target === historyEl) settle = true;
308
+ }
242
309
  }
243
- if (removed) dropOrphans();
310
+ if (settle) dropOrphans();
244
311
  for (const el of roots) if (el.isConnected) decorate(el);
245
312
  }).observe(historyEl, { childList: true, subtree: true, attributes: true, attributeFilter: ["class"] });
246
313
  decorate(historyEl);
@@ -255,7 +322,9 @@ export function installAnswerFold({ historyEl, sessionId, projectRoot = () => nu
255
322
  bubble.classList.toggle("folded", folded);
256
323
  drawRow(row, bubble);
257
324
  });
258
- writeFold(sessionId(), answerKey(bubble), folded, { root: projectRoot() });
325
+ // An answer's choice is its fold, a report's its unfold (D2: the default differs).
326
+ if (isReport(bubble)) writeOpened(sessionId(), answerKey(bubble), !folded, { root: projectRoot() });
327
+ else writeFold(sessionId(), answerKey(bubble), folded, { root: projectRoot() });
259
328
  });
260
329
 
261
330
  return { decorate };