samagotchi 0.135.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 +30 -1
- data/docs/cli.md +4 -4
- data/docs/configuration.md +10 -7
- data/docs/internals/prompt-caching.md +1 -1
- data/docs/internals/web-frontend.md +9 -8
- data/docs/internals/working-set.md +2 -2
- data/docs/sessions.md +3 -3
- data/docs/testing.md +11 -6
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +4 -4
- data/lib/samagotchi/web/message_display.rb +16 -0
- data/lib/samagotchi/web/public/answer_fold.js +98 -29
- data/lib/samagotchi/web/public/app.js +17 -40
- data/lib/samagotchi/web/public/bubbles.js +27 -7
- data/lib/samagotchi/web/public/chip_popover.js +2 -2
- data/lib/samagotchi/web/public/history_reveal.js +2 -2
- data/lib/samagotchi/web/public/index.html +67 -44
- 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/level_chip.js +33 -95
- data/lib/samagotchi/web/public/llm_context_chip.js +13 -144
- data/lib/samagotchi/web/public/reasoning_history_chip.js +8 -34
- data/lib/samagotchi/web/public/revise_stack.js +9 -3
- data/lib/samagotchi/web/public/session_fields.js +4 -4
- data/lib/samagotchi/web/public/session_state.js +7 -5
- data/lib/samagotchi/web/public/session_store.js +1 -1
- 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/start_settings.js +25 -88
- 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 +13 -3
- data/lib/samagotchi/web/public/toolkit_chip.js +37 -21
- data/lib/samagotchi/web/public/turn_block.js +6 -1
- data/lib/samagotchi/web/public/turn_events.js +1 -1
- data/lib/samagotchi/web/public/turn_html.js +16 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +6 -3
- metadata +4 -1
|
@@ -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,7 +43,9 @@ export const FOLD_PREFIX = "chi_answer_fold:";
|
|
|
36
43
|
const LABEL_CHARS = 160;
|
|
37
44
|
|
|
38
45
|
const ROW_CLASS = "answer-fold";
|
|
39
|
-
|
|
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}`;
|
|
40
49
|
// Not done yet: still streaming.
|
|
41
50
|
const UNFINISHED = ".streaming";
|
|
42
51
|
|
|
@@ -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 };
|
|
@@ -35,10 +35,8 @@ import {
|
|
|
35
35
|
import { authProbe, createAuthLost } from "./auth_lost.js";
|
|
36
36
|
import { REPLACE_SESSION_TEXT, fragmentCode, tradeFragmentCode } from "./token_fragment.js";
|
|
37
37
|
import { createContextChips } from "./context_chips.js";
|
|
38
|
-
import { createLlmContextControl } from "./llm_context_chip.js";
|
|
39
38
|
import { createParticipantsControl } from "./participants_panel.js";
|
|
40
|
-
import {
|
|
41
|
-
import { createReasoningHistoryControl } from "./reasoning_history_chip.js";
|
|
39
|
+
import { createSettingsControl } from "./settings_chip.js";
|
|
42
40
|
import { renderInfoBar } from "./info_bar.js";
|
|
43
41
|
import { sessionBar, workerBadgeFor } from "./info_bar_model.js";
|
|
44
42
|
import { createShownTexts } from "./shown_texts.js";
|
|
@@ -155,29 +153,13 @@ const contextChips = createContextChips({
|
|
|
155
153
|
api: { list: listContext, show: getContext, detach: detachContext, add: addContext },
|
|
156
154
|
toast: (text) => showToast(text),
|
|
157
155
|
});
|
|
158
|
-
// The open session's
|
|
159
|
-
//
|
|
160
|
-
|
|
156
|
+
// The open session's ⚙ chip in the info bar, and its panel (thinking,
|
|
157
|
+
// reasoning history, LLM context): a pick runs its command in the session's
|
|
158
|
+
// worker.
|
|
159
|
+
const settingsControl = createSettingsControl({
|
|
161
160
|
el: infoTextEl,
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
running: () => liveTurn.running,
|
|
165
|
-
run: (line) => runSessionCommand(line),
|
|
166
|
-
});
|
|
167
|
-
// The open session's thinking chip, beside the llm ctx one: /thinking runs
|
|
168
|
-
// in the session's worker.
|
|
169
|
-
const thinkingControl = createThinkingControl({
|
|
170
|
-
el: infoTextEl,
|
|
171
|
-
summary: () => (store.selected ? store.current.thinking : null),
|
|
172
|
-
session: () => store.selected,
|
|
173
|
-
running: () => liveTurn.running,
|
|
174
|
-
run: (line) => runSessionCommand(line),
|
|
175
|
-
});
|
|
176
|
-
// The open session's reasoning history chip, after the thinking one (a
|
|
177
|
-
// chat host's only): /reasoning-history runs in the session's worker.
|
|
178
|
-
const reasoningHistoryControl = createReasoningHistoryControl({
|
|
179
|
-
el: infoTextEl,
|
|
180
|
-
summary: () => (store.selected ? store.current.reasoningHistory : null),
|
|
161
|
+
summaries: () => (store.selected ? { thinking: store.current.thinking, reasoningHistory: store.current.reasoningHistory,
|
|
162
|
+
llmContext: store.current.llmContext } : null),
|
|
181
163
|
session: () => store.selected,
|
|
182
164
|
running: () => liveTurn.running,
|
|
183
165
|
run: (line) => runSessionCommand(line),
|
|
@@ -1191,14 +1173,15 @@ function renderSessionView(data, { kept = [] } = {}) {
|
|
|
1191
1173
|
const loadWarned = (label) => (data.cards || []).some((c) => c?.type === "guardrail_warning" && (c.label || "guardrails") === label);
|
|
1192
1174
|
if (!loadWarned("guardrails")) showGuardrailWarning(data.guardrail_warning);
|
|
1193
1175
|
if (!loadWarned("plugins")) showGuardrailWarning(data.plugin_warning, "plugins");
|
|
1194
|
-
// The recap saved with the session (also a stopped one's), else a live
|
|
1195
|
-
// worker's from before saved_recap existed.
|
|
1196
|
-
if (data.saved_recap) showRecap(data.saved_recap.text, data.saved_recap.turns_since, data.saved_recap.created_at || null);
|
|
1197
|
-
else showRecap(data.recap);
|
|
1198
1176
|
// A live worker's last cards and between-turns notices, where they
|
|
1199
1177
|
// arrived; the running turn's go into its step once it is drawn.
|
|
1200
1178
|
const snapshotCards = splitSnapshotCards(data.cards, pageCapabilities);
|
|
1201
1179
|
placeSnapshotCards(snapshotCards.placed);
|
|
1180
|
+
// The recap saved with the session (also a stopped one's), else a live
|
|
1181
|
+
// worker's from before saved_recap existed. After the cards: the last
|
|
1182
|
+
// turn's check-in notice goes to the end, and the recap closes the page.
|
|
1183
|
+
if (data.saved_recap) showRecap(data.saved_recap.text, data.saved_recap.turns_since, data.saved_recap.created_at || null);
|
|
1184
|
+
else showRecap(data.recap);
|
|
1202
1185
|
const replay = snapshotEvents({
|
|
1203
1186
|
current_turn: data.current_turn,
|
|
1204
1187
|
queued: data.queued,
|
|
@@ -1373,8 +1356,6 @@ function endTurnView(kind, endText = null, { emptyAnswer = null, stopped = false
|
|
|
1373
1356
|
if (questionCards.pendingQuestion()) questionCards.resolveQuestion(questionCards.pendingQuestion().id, { cancelled: true, reason: "turn ended" });
|
|
1374
1357
|
}
|
|
1375
1358
|
|
|
1376
|
-
// The info bar's "copy chi --attach": the full id, which the page shows
|
|
1377
|
-
// only short; "copied ✓" for 2 s (current.attachCopiedAt: that session's).
|
|
1378
1359
|
infoTextEl.addEventListener("click", (e) => {
|
|
1379
1360
|
if (e.target.closest(".worker-restart")) restartWorker();
|
|
1380
1361
|
// Pushed as openAllView does, so the view's back returns here.
|
|
@@ -1383,12 +1364,10 @@ infoTextEl.addEventListener("click", (e) => {
|
|
|
1383
1364
|
openAllView();
|
|
1384
1365
|
}
|
|
1385
1366
|
});
|
|
1367
|
+
// The info bar's id: a click copies the full id, which the bar shows only short.
|
|
1386
1368
|
infoTextEl.addEventListener("click", async (e) => {
|
|
1387
|
-
if (!e.target.closest(".
|
|
1388
|
-
|
|
1389
|
-
if (!(await copyText(`chi --attach ${id}`)) || id !== store.selected) return;
|
|
1390
|
-
store.update({ current: { attachCopiedAt: Date.now() } });
|
|
1391
|
-
setTimeout(() => store.touch("infoBar"), 2000);
|
|
1369
|
+
if (!e.target.closest(".id") || !store.selected) return;
|
|
1370
|
+
showToast((await copyText(store.selected)) ? "Session id copied" : "Couldn't copy the session id");
|
|
1392
1371
|
});
|
|
1393
1372
|
|
|
1394
1373
|
// Apply (review_apply.js) under a participant's review turn that just
|
|
@@ -1503,7 +1482,7 @@ function updateInfoBar() {
|
|
|
1503
1482
|
const { buttons, bar } = sessionBar({
|
|
1504
1483
|
id: store.selected, state: store.current, sessions: store.sessions, versions, cardTitle: (s) => lists.cardTitle(s),
|
|
1505
1484
|
continuing: sessionActions.continuing, restarting: restartingIds, streaming: !!streamControl, running: liveTurn.running,
|
|
1506
|
-
|
|
1485
|
+
workingSet: workingSet ? workingSet.chip() : undefined, participants: participantsControl.chip(),
|
|
1507
1486
|
});
|
|
1508
1487
|
const stopBtn = document.getElementById("infoStopBtn");
|
|
1509
1488
|
const deleteBtn = document.getElementById("infoDeleteBtn");
|
|
@@ -2193,9 +2172,7 @@ document.addEventListener("keydown", (e) => {
|
|
|
2193
2172
|
// before it (none at load) would redraw nothing.
|
|
2194
2173
|
store.on("lists", () => lists.render());
|
|
2195
2174
|
store.on("paintActive", () => lists.paintActive());
|
|
2196
|
-
store.on("
|
|
2197
|
-
store.on("thinkingChip", () => thinkingControl.refresh());
|
|
2198
|
-
store.on("reasoningChip", () => reasoningHistoryControl.refresh());
|
|
2175
|
+
store.on("settingsPanel", () => settingsControl.refresh());
|
|
2199
2176
|
// The working set's list offers pin only under forget.
|
|
2200
2177
|
store.on("workingSetStrategy", () => workingSet?.strategyChanged());
|
|
2201
2178
|
store.on("featureGates", () => featureGates.check(store.current));
|
|
@@ -14,6 +14,11 @@ import { hookNoticeLabel, isReportClient, promptOps } from "./turn_events.js";
|
|
|
14
14
|
import { drawCommandBubble } from "./commands.js";
|
|
15
15
|
import { TIME_GAP_CLASS, timeGapEl } from "./time_gaps.js";
|
|
16
16
|
|
|
17
|
+
// What a session's load places at a turn's prompt from the snapshot
|
|
18
|
+
// (cards_place.js placeSnapshotCards): its cards, answered questions,
|
|
19
|
+
// notices and warnings.
|
|
20
|
+
const SNAPSHOT_PLACED = ".plugin-card, .bubble.question, .bubble.hook-notice, .bubble.guardrail-warning";
|
|
21
|
+
|
|
17
22
|
export function removeQueuedBubble(bubble) {
|
|
18
23
|
if (!bubble) return;
|
|
19
24
|
const slot = bubble.parentElement?.classList.contains("ts-queued") ? bubble.parentElement : null;
|
|
@@ -159,10 +164,11 @@ export function createBubbles({
|
|
|
159
164
|
|
|
160
165
|
// The bubble a reload draws (turn_html.js), with this page's own fields.
|
|
161
166
|
// +delivery+: how it went into a running turn (next_step, cut, queue): its
|
|
162
|
-
// queued badge says what it waits for.
|
|
163
|
-
|
|
167
|
+
// queued badge says what it waits for. +source+: a delegate report's
|
|
168
|
+
// "delegate_report" (its fold row, answer_fold.js).
|
|
169
|
+
function makeUserBubble(prompt, { state = null, label = null, enqueuedId = null, own = false, images = null, delivery = null, author = null, turnId = null, source = null } = {}) {
|
|
164
170
|
removeHint();
|
|
165
|
-
const div = elementOf(userBubbleTemplate({ content: prompt, turn_id: turnId }, {
|
|
171
|
+
const div = elementOf(userBubbleTemplate({ content: prompt, turn_id: turnId, source }, {
|
|
166
172
|
label, state, own, enqueuedId, delivery, author, key: normalize(prompt), thumbs: thumbsTemplate(selected(), images),
|
|
167
173
|
}));
|
|
168
174
|
return placeFollowing(div);
|
|
@@ -182,10 +188,16 @@ export function createBubbles({
|
|
|
182
188
|
return echoes.find((el) => el.dataset.userContent === text) || echoes[0] || null;
|
|
183
189
|
}
|
|
184
190
|
|
|
191
|
+
// The running wake turn's id (a turn chi ran for reports), for its report's bubble.
|
|
192
|
+
let wakeTurnId = null;
|
|
193
|
+
|
|
185
194
|
// Returns the bubble of the turn a turn_started starts (the stage takes it
|
|
186
195
|
// into its prompt line), else null.
|
|
187
196
|
function applyPromptOps(event) {
|
|
188
|
-
if (event.type === "turn_started")
|
|
197
|
+
if (event.type === "turn_started") {
|
|
198
|
+
liveTurn.wakeStart = !!event.continue && isReportClient(event.origin?.client_id);
|
|
199
|
+
wakeTurnId = liveTurn.wakeStart ? event.turn_id || null : null;
|
|
200
|
+
}
|
|
189
201
|
const ops = promptOps(event, { myId: clientId, known: (id) => !!bubbleById(id), unmatchedMerge: liveTurn.unmatchedMerge, wakeStart: liveTurn.wakeStart });
|
|
190
202
|
if (event.type === "input_merged") liveTurn.unmatchedMerge = false;
|
|
191
203
|
if (event.type === "pending_input_merged") liveTurn.unmatchedMerge = false;
|
|
@@ -194,7 +206,10 @@ export function createBubbles({
|
|
|
194
206
|
if (op.op === "report") {
|
|
195
207
|
liveTurn.unmatchedMerge = "report";
|
|
196
208
|
} else if (op.op === "add") {
|
|
197
|
-
|
|
209
|
+
// A report that opens a wake turn carries that turn's id, as a
|
|
210
|
+
// reload's does (its fold key); a merged one has none until reload.
|
|
211
|
+
const turnId = op.source && !op.state && liveTurn.wakeStart ? wakeTurnId : null;
|
|
212
|
+
const bubble = makeUserBubble(op.prompt, { state: op.state, label: op.label, enqueuedId: op.enqueuedId, images: op.images, delivery: op.delivery, author: op.author, source: op.source, turnId });
|
|
198
213
|
if (op.state === "started") started = bubble;
|
|
199
214
|
if (event.type === "pending_input_merged") liveTurn.wakeStart = false;
|
|
200
215
|
} else if (op.op === "tag") {
|
|
@@ -241,8 +256,13 @@ export function createBubbles({
|
|
|
241
256
|
// Never in the stage: a recap is between turns.
|
|
242
257
|
turnDom.settleFold();
|
|
243
258
|
const prompt = recapPlace(turnPromptBubbles(historyEl), turnsSince);
|
|
244
|
-
// Above that turn's gap divider too: the recap was written before the quiet stretch ended.
|
|
245
|
-
|
|
259
|
+
// Above that turn's gap divider too: the recap was written before the quiet stretch ended. The
|
|
260
|
+
// snapshot's cards placed at that prompt (placed first) sit between the divider and the prompt.
|
|
261
|
+
// A delegate report's fold row (answer_fold.js) heads its prompt.
|
|
262
|
+
const head = prompt?.previousElementSibling?.matches(".answer-fold") ? prompt.previousElementSibling : prompt;
|
|
263
|
+
let back = head?.previousElementSibling;
|
|
264
|
+
while (back?.matches(SNAPSHOT_PLACED)) back = back.previousElementSibling;
|
|
265
|
+
const before = back?.matches(`.${TIME_GAP_CLASS}`) ? back : head;
|
|
246
266
|
keepFollowing(historyEl, () => (before ? historyEl.insertBefore(details, before) : appendAboveLiveTiming(historyEl, details, liveTurn.timing.el)));
|
|
247
267
|
}
|
|
248
268
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// The popover shell a session-bar chip opens (
|
|
2
|
-
//
|
|
1
|
+
// The popover shell a session-bar chip opens (settings_chip.js' ⚙ panel,
|
|
2
|
+
// the working set's, the start page's panels): a div.ctx-popover on the body, placed
|
|
3
3
|
// at the chip, opened by a click on it (delegated on +el+) or by open()
|
|
4
4
|
// (another opener: the working set's rail and footprint), closed by a second
|
|
5
5
|
// click, its × (data-act="close"), a click outside or Esc; focus goes back
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The one way a jump into the history shows what it lands on: whatever
|
|
2
2
|
// hides +el+ unfolds: a closed <details> around it (a turn's steps block, a
|
|
3
|
-
// step, a diff), an answer
|
|
3
|
+
// step, a diff), an answer or a delegate report folded (answer_fold.js: its row follows
|
|
4
4
|
// the bubble's class; the jump's unfold isn't saved), a superseded version
|
|
5
5
|
// of a Revise stack (revise_stack.js: its stub opens, not saved). Every
|
|
6
6
|
// jump goes through it (claim_link.js showClaim, refs.js showRow and so the
|
|
@@ -19,7 +19,7 @@ import { openStubOver } from "./revise_stack.js";
|
|
|
19
19
|
export function revealInHistory(el) {
|
|
20
20
|
if (!el) return null;
|
|
21
21
|
openStubOver(el);
|
|
22
|
-
el.closest?.(".bubble.
|
|
22
|
+
el.closest?.(".bubble.folded")?.classList.remove("folded");
|
|
23
23
|
for (let details = el.parentElement?.closest("details"); details; details = details.parentElement?.closest("details")) {
|
|
24
24
|
details.open = true;
|
|
25
25
|
}
|