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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -1
- data/bin/chi +2 -3
- data/docs/architecture.md +6 -3
- data/docs/cli.md +43 -38
- data/docs/configuration.md +11 -9
- data/docs/guardrails.md +53 -9
- data/docs/internals/engine.md +6 -3
- data/docs/internals/participants.md +1 -1
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/web-frontend.md +263 -0
- data/docs/internals/working-set.md +2 -2
- data/docs/sessions.md +5 -3
- data/docs/testing.md +36 -6
- data/lib/samagotchi/bridge.rb +11 -6
- data/lib/samagotchi/bridge_client.rb +8 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +0 -2
- data/lib/samagotchi/engine.rb +69 -12
- data/lib/samagotchi/guardrail_wiring.rb +8 -6
- data/lib/samagotchi/guardrails/approval.rb +41 -5
- data/lib/samagotchi/guardrails/implicit_writers.rb +19 -4
- data/lib/samagotchi/guardrails/pair_boundary.rb +5 -1
- data/lib/samagotchi/guardrails/pair_command.rb +387 -78
- data/lib/samagotchi/guardrails/parent_approvals.rb +12 -15
- data/lib/samagotchi/guardrails/read_only_shell.rb +24 -2
- data/lib/samagotchi/guardrails/shared_git.rb +1 -1
- data/lib/samagotchi/guardrails/shell_git_dirs.rb +1 -1
- data/lib/samagotchi/guardrails/shell_lex.rb +22 -0
- data/lib/samagotchi/guardrails_command.rb +13 -10
- data/lib/samagotchi/llm/chat_loop.rb +4 -3
- data/lib/samagotchi/message_parts.rb +1 -1
- data/lib/samagotchi/revise_anchor.rb +33 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager.rb +13 -11
- data/lib/samagotchi/system_one/approval_question.rb +21 -16
- data/lib/samagotchi/system_one/self_protect.rb +13 -15
- data/lib/samagotchi/tools/delegate_relay.rb +6 -5
- data/lib/samagotchi/tools/delegate_wait.rb +5 -5
- data/lib/samagotchi/tools/execute.rb +6 -6
- data/lib/samagotchi/tools/output_guardrails.rb +9 -2
- data/lib/samagotchi/tools/read.rb +5 -5
- data/lib/samagotchi/turn_anchors.rb +49 -0
- data/lib/samagotchi/verify_anchor.rb +5 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +6 -18
- data/lib/samagotchi/web/message_display.rb +33 -1
- data/lib/samagotchi/web/participant_access.rb +2 -2
- data/lib/samagotchi/web/participant_view.rb +8 -0
- data/lib/samagotchi/web/public/activity.js +1 -1
- data/lib/samagotchi/web/public/annotations.js +1 -1
- data/lib/samagotchi/web/public/answer_fold.js +100 -31
- data/lib/samagotchi/web/public/app.js +451 -367
- data/lib/samagotchi/web/public/bubbles.js +30 -10
- data/lib/samagotchi/web/public/cards_place.js +4 -2
- data/lib/samagotchi/web/public/chip_popover.js +2 -2
- data/lib/samagotchi/web/public/composer.css +8 -0
- data/lib/samagotchi/web/public/context_chips.js +9 -6
- data/lib/samagotchi/web/public/data.js +4 -2
- data/lib/samagotchi/web/public/dock_reserve.js +13 -15
- data/lib/samagotchi/web/public/feature_gates.js +33 -0
- data/lib/samagotchi/web/public/history_reveal.js +10 -6
- data/lib/samagotchi/web/public/index.html +105 -53
- data/lib/samagotchi/web/public/info_bar.js +11 -12
- data/lib/samagotchi/web/public/info_bar_model.js +8 -10
- data/lib/samagotchi/web/public/latest.js +46 -0
- data/lib/samagotchi/web/public/level_chip.js +33 -95
- data/lib/samagotchi/web/public/lists_view.js +15 -13
- data/lib/samagotchi/web/public/live_timing.js +2 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
- data/lib/samagotchi/web/public/markdown_place.js +1 -1
- data/lib/samagotchi/web/public/method_api.js +14 -0
- data/lib/samagotchi/web/public/notifications.js +4 -11
- data/lib/samagotchi/web/public/participant.js +11 -6
- data/lib/samagotchi/web/public/participant_names.js +1 -1
- data/lib/samagotchi/web/public/participants_panel.js +4 -2
- data/lib/samagotchi/web/public/project_picker.js +18 -22
- data/lib/samagotchi/web/public/question_cards.js +2 -2
- data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
- data/lib/samagotchi/web/public/restore.js +2 -0
- data/lib/samagotchi/web/public/revise_stack.js +378 -0
- data/lib/samagotchi/web/public/scope.js +12 -20
- data/lib/samagotchi/web/public/send_turn.js +29 -16
- data/lib/samagotchi/web/public/session_actions.js +15 -14
- data/lib/samagotchi/web/public/session_fields.js +152 -0
- data/lib/samagotchi/web/public/session_payload.js +35 -0
- data/lib/samagotchi/web/public/session_state.js +9 -5
- data/lib/samagotchi/web/public/session_store.js +191 -0
- data/lib/samagotchi/web/public/settings_chip.js +212 -0
- data/lib/samagotchi/web/public/settings_panel.js +121 -0
- data/lib/samagotchi/web/public/shell_command.js +26 -0
- data/lib/samagotchi/web/public/stage_model.js +4 -4
- data/lib/samagotchi/web/public/stage_view.js +11 -16
- data/lib/samagotchi/web/public/start_settings.js +25 -88
- data/lib/samagotchi/web/public/stream_handlers.js +88 -90
- data/lib/samagotchi/web/public/task_stop.js +1 -1
- data/lib/samagotchi/web/public/thinking_chip.js +7 -45
- data/lib/samagotchi/web/public/time_gaps.js +5 -2
- data/lib/samagotchi/web/public/timing.js +15 -5
- data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
- data/lib/samagotchi/web/public/{turn_view.js → turn_block.js} +33 -93
- data/lib/samagotchi/web/public/turn_events.js +3 -3
- data/lib/samagotchi/web/public/turn_html.js +20 -16
- data/lib/samagotchi/web/public/turn_model.js +6 -6
- data/lib/samagotchi/web/public/verdict_fresh.js +2 -0
- data/lib/samagotchi/web/public/working_set_view.js +3 -1
- data/lib/samagotchi/web/server.rb +4 -4
- data/lib/samagotchi/web/session_control.rb +2 -2
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +6 -3
- data/lib/samagotchi/worker.rb +9 -9
- data/lib/samagotchi/worker_inbound.rb +6 -5
- metadata +15 -3
- 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
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
|
12
|
-
// last FOLD_KEEP of
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
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
|
|
75
|
-
|
|
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
|
|
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 || !
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
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
|
-
|
|
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
|
|
223
|
-
const stored = () => (
|
|
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
|
|
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)
|
|
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)
|
|
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 (
|
|
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
|
-
|
|
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 };
|