samagotchi 0.51.0 → 0.53.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 +72 -1
- data/README.md +1 -1
- data/bin/chi +11 -3
- data/docs/architecture.md +1 -1
- data/docs/cli.md +23 -8
- data/docs/configuration.md +59 -19
- data/docs/memory.md +11 -0
- data/docs/plugins.md +2 -1
- data/docs/releasing.md +6 -3
- data/docs/sessions.md +5 -1
- data/docs/testing.md +8 -1
- data/lib/samagotchi/analytics_file.rb +35 -0
- data/lib/samagotchi/bootstrap/config_writer.rb +6 -37
- data/lib/samagotchi/bridge/ring_buffer.rb +0 -5
- data/lib/samagotchi/bridge.rb +10 -13
- data/lib/samagotchi/broadcast/recipients.rb +1 -2
- data/lib/samagotchi/bundle_command.rb +9 -13
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/children_status.rb +1 -1
- data/lib/samagotchi/commands/registry.rb +2 -1
- data/lib/samagotchi/config.rb +50 -182
- data/lib/samagotchi/config_text_edit.rb +8 -32
- data/lib/samagotchi/context_sources.rb +0 -7
- data/lib/samagotchi/context_window.rb +2 -2
- data/lib/samagotchi/continue_offer.rb +5 -4
- data/lib/samagotchi/cut_policy.rb +0 -1
- data/lib/samagotchi/delivery.rb +23 -0
- data/lib/samagotchi/engine.rb +63 -18
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/hooks/bundle_loader.rb +9 -8
- data/lib/samagotchi/host_config.rb +183 -0
- data/lib/samagotchi/host_registry.rb +18 -10
- data/lib/samagotchi/last_turn.rb +48 -0
- data/lib/samagotchi/live_versions.rb +0 -6
- data/lib/samagotchi/llm/native_tool_normalizer.rb +0 -5
- data/lib/samagotchi/llm_context_override.rb +3 -22
- data/lib/samagotchi/llm_context_strategy.rb +50 -16
- data/lib/samagotchi/memory_bundle/builder.rb +23 -21
- data/lib/samagotchi/memory_bundle/bundle_hook.rb +51 -0
- data/lib/samagotchi/memory_bundle/installer.rb +4 -4
- data/lib/samagotchi/memory_bundle/manifest.rb +6 -28
- data/lib/samagotchi/memory_bundle/placeholder.rb +0 -5
- data/lib/samagotchi/memory_bundle/provenance.rb +8 -17
- data/lib/samagotchi/model_profile.rb +4 -4
- data/lib/samagotchi/model_settings.rb +46 -0
- data/lib/samagotchi/plugin/context.rb +3 -3
- data/lib/samagotchi/plugin/sessions.rb +2 -2
- data/lib/samagotchi/queued_command.rb +35 -0
- data/lib/samagotchi/reminder_store.rb +0 -27
- data/lib/samagotchi/reply_wait.rb +11 -13
- data/lib/samagotchi/sampling_settings.rb +4 -4
- data/lib/samagotchi/self_report.rb +8 -5
- data/lib/samagotchi/send_command.rb +40 -46
- data/lib/samagotchi/session.rb +45 -24
- data/lib/samagotchi/session_commands.rb +68 -11
- data/lib/samagotchi/session_inbox.rb +18 -9
- data/lib/samagotchi/session_manager.rb +44 -11
- data/lib/samagotchi/session_metrics.rb +15 -20
- data/lib/samagotchi/session_setup.rb +30 -0
- data/lib/samagotchi/sessions_command.rb +3 -2
- data/lib/samagotchi/steer_cut.rb +9 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +10 -4
- data/lib/samagotchi/terminal_ui/attached_loop.rb +1 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +26 -4
- data/lib/samagotchi/terminal_ui.rb +21 -4
- data/lib/samagotchi/thinking.rb +67 -7
- data/lib/samagotchi/thinking_command.rb +72 -0
- data/lib/samagotchi/tools/builtins.rb +7 -2
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/task_runtime.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/vision_support.rb +4 -3
- data/lib/samagotchi/web/app.rb +56 -13
- data/lib/samagotchi/web/public/app.js +131 -20
- data/lib/samagotchi/web/public/command_complete.js +7 -2
- data/lib/samagotchi/web/public/data.js +8 -4
- data/lib/samagotchi/web/public/format.js +8 -4
- data/lib/samagotchi/web/public/index.html +8 -5
- data/lib/samagotchi/web/public/kept_commands.js +52 -0
- data/lib/samagotchi/web/public/model_pick.js +3 -1
- data/lib/samagotchi/web/public/thinking_chip.js +179 -0
- data/lib/samagotchi/web/public/timing.js +15 -0
- data/lib/samagotchi/web/public/turn_events.js +23 -0
- data/lib/samagotchi/web/public/turn_view.js +328 -310
- data/lib/samagotchi/web/public/vendor/lit-html.js +8 -0
- data/lib/samagotchi/web/session_summary.rb +1 -1
- data/lib/samagotchi/worker.rb +43 -42
- data/lib/samagotchi/worker_inbound.rb +5 -5
- data/lib/samagotchi/worker_wakes.rb +0 -2
- data/lib/samagotchi/yaml_lines.rb +72 -0
- metadata +13 -1
|
@@ -6,9 +6,21 @@
|
|
|
6
6
|
// markdown, annotate and the turn output dedupe work on a plain bubble.
|
|
7
7
|
// The grouping rules live in turn_model.js.
|
|
8
8
|
//
|
|
9
|
+
// A step is ONE lit-html template (stepTemplate) for the live page and a
|
|
10
|
+
// reload: the live view keeps each step's state (its rows, the ticker's
|
|
11
|
+
// line, the narration's sentences) and renders the template again when an
|
|
12
|
+
// event changed it; lit writes only what changed, so a hand toggle, a
|
|
13
|
+
// selection in the thinking and a row's kept parts stay.
|
|
14
|
+
//
|
|
15
|
+
// lit leaves comment nodes in the DOM (its part markers: <!--?lit$…$-->
|
|
16
|
+
// and <!---->), in a live step and in a reloaded history alike. Code that
|
|
17
|
+
// walks childNodes (or reads firstChild/lastChild) must skip comments;
|
|
18
|
+
// element queries, :empty and textContent are unaffected.
|
|
19
|
+
//
|
|
9
20
|
// A block that holds one gen with no thinking and no tool rows hides its
|
|
10
21
|
// chrome (CSS :has), so a plain answer streams like today's bubble, and is
|
|
11
22
|
// removed at the end: the answer bubble takes its place.
|
|
23
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
12
24
|
import { leavesBlock } from "./card.js";
|
|
13
25
|
import { copySourceAttr } from "./copy.js";
|
|
14
26
|
import { escapeHtml, failedTurnText, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
|
|
@@ -23,6 +35,86 @@ import * as model from "./turn_model.js";
|
|
|
23
35
|
import { createHold } from "./hold.js";
|
|
24
36
|
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
25
37
|
|
|
38
|
+
// ---- The templates: one step, its thinking, its label ----
|
|
39
|
+
|
|
40
|
+
// A closed step's summary (model.genLabel). When its head is the words the
|
|
41
|
+
// body starts with (the narration's first line, the first call's title)
|
|
42
|
+
// and it has calls, the summary is marked .repeats and its parts are
|
|
43
|
+
// spans: an open step shows only its calls (index.html), so it doesn't say
|
|
44
|
+
// the same words twice. Its text stays genLabel's either way.
|
|
45
|
+
function closedLabel(gen) {
|
|
46
|
+
const { head, calls, repeats } = model.genLabelParts(gen);
|
|
47
|
+
if (!repeats || !calls) return { repeats: false, content: model.genLabel(gen) };
|
|
48
|
+
return { repeats: true, content: html`<span class="gen-head">${head}</span><span class="gen-sep"> · </span><span class="gen-calls">${calls}</span>` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// A step's thinking: a collapsed <details> whose summary is "thinking" (a
|
|
52
|
+
// live step's ticker: "thinking · <newest sentence>") over its body; a
|
|
53
|
+
// thinking-only step's body sits bare under the step's own summary (no wrap
|
|
54
|
+
// that would repeat "thinking"). +body+ is the text: a string, or the live
|
|
55
|
+
// step's Text node (appended by delta: a selection in it survives).
|
|
56
|
+
// +cls+ "bubble thinking" is a plain answer's collapsed thinking block.
|
|
57
|
+
export function thinkingTemplate({ body, wrapped = true, line = null, cls = "thinking", on = {} }) {
|
|
58
|
+
const inner = html`<div class="thinking-body">${body}</div>`;
|
|
59
|
+
if (!wrapped) return inner;
|
|
60
|
+
const summary = line === null ? "thinking" : html`<span class="thinking-tag">thinking · </span>${line}`;
|
|
61
|
+
return html`<details class=${cls} @toggle=${on.thinkingToggle ?? nothing}><summary @click=${on.thinkingClick ?? nothing}>${summary}</summary>${inner}</details>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// A step's narration: the live box of complete sentences (one per line,
|
|
65
|
+
// the newest last), the live "…" until the first one, or a closed step's
|
|
66
|
+
// full text. None when there is nothing to say.
|
|
67
|
+
function textTemplate(text, { live, sentences, placeholder }) {
|
|
68
|
+
if (live && sentences?.length) {
|
|
69
|
+
return html`<div class="gen-text streaming"><div class="narration">${sentences.map((s) => html`<div class="sentence">${s}</div>`)}</div></div>`;
|
|
70
|
+
}
|
|
71
|
+
if (live) return placeholder ? html`<div class="gen-text streaming">…</div>` : nothing;
|
|
72
|
+
return text ? html`<div class="gen-text">${text}</div>` : nothing;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// One step, live or reloaded. +gen+: { text, thinking, tools, answer } (a
|
|
76
|
+
// taken answer's text is out of the step). The live view's state: +live+
|
|
77
|
+
// (open, "step N" by its +index+, no failed mark yet), +open+ (lit sets it
|
|
78
|
+
// only when it changes: a hand toggle stays), the +steers+ rows it
|
|
79
|
+
// answers, its +items+ (tool rows, notices, cards: nodes or templates; a
|
|
80
|
+
// reload's are its rows' nodes), the thinking +body+ node, the ticker's
|
|
81
|
+
// +line+, the narration's +sentences+ and "…" +placeholder+, +on+ (the
|
|
82
|
+
// click and toggle handlers).
|
|
83
|
+
export function stepTemplate(gen, st = {}) {
|
|
84
|
+
const { live = false, index = 0, open = live, steers = [], items = [], body = gen.thinking, line = null,
|
|
85
|
+
sentences = null, placeholder = false, on = {} } = st;
|
|
86
|
+
const text = gen.answer ? "" : gen.text || "";
|
|
87
|
+
const label = live ? { repeats: false, content: `step ${index + 1}` } : closedLabel({ ...gen, text });
|
|
88
|
+
const failed = !live && model.failedCount(gen.tools) > 0;
|
|
89
|
+
// A step left with its thinking only reads "thinking" once.
|
|
90
|
+
const wrapped = live || !!text || gen.tools.length > 0;
|
|
91
|
+
return html`<details class=${`gen${live ? " live" : ""}${failed ? " has-failed" : ""}`} .open=${open}><summary class=${label.repeats ? "repeats" : nothing} @click=${on.stepClick ?? nothing}>${label.content}</summary>${steers}${gen.thinking ? thinkingTemplate({ body, wrapped, line, on }) : nothing}${textTemplate(text, { live, sentences, placeholder })}${items.length ? html`<div class="activity-body">${items}</div>` : nothing}</details>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A hook's line (hook_notice) as a step's row. data.line is a whole row of
|
|
95
|
+
// chi's own (an empty-answer retry), shown as is; data.cls a class of its
|
|
96
|
+
// own and data.title its hover (a ✂ row's).
|
|
97
|
+
function noticeTemplate(data) {
|
|
98
|
+
const cls = "hook-notice" + (data.level === "warn" ? " warn" : "") + (data.cls ? ` ${data.cls}` : "");
|
|
99
|
+
return html`<div class=${cls} title=${data.title || nothing}>${data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`}</div>`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// +template+ rendered as detached nodes (a fragment).
|
|
103
|
+
function nodesOf(template) {
|
|
104
|
+
const frag = document.createDocumentFragment();
|
|
105
|
+
render(template, frag);
|
|
106
|
+
return frag;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// +markup+ (any HTML string) as nodes.
|
|
110
|
+
function rawNodes(markup) {
|
|
111
|
+
const t = document.createElement("template");
|
|
112
|
+
t.innerHTML = markup;
|
|
113
|
+
return t.content;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- The live view ----
|
|
117
|
+
|
|
26
118
|
// The parts of a drawn tool row a user may have changed (the command's
|
|
27
119
|
// raw toggle, an opened diff) or that never change once there (the
|
|
28
120
|
// thumbs): a redraw keeps them as they are.
|
|
@@ -33,8 +125,8 @@ const childWith = (el, cls) => [...el.children].find((c) => c.classList.contains
|
|
|
33
125
|
// A live tool row drawn again from its activity row (status, title,
|
|
34
126
|
// output): the reloaded row's markup (toolRowInnerHtml), the kept parts
|
|
35
127
|
// and the stop-task button carried over. +html+ is that markup.
|
|
36
|
-
function redrawRow(el,
|
|
37
|
-
const fresh = htmlElement(`<div>${
|
|
128
|
+
function redrawRow(el, markup) {
|
|
129
|
+
const fresh = htmlElement(`<div>${markup}</div>`);
|
|
38
130
|
for (const cls of KEPT_PARTS) {
|
|
39
131
|
const old = childWith(el, cls);
|
|
40
132
|
const drawn = childWith(fresh, cls);
|
|
@@ -82,7 +174,8 @@ export function createTurnView({
|
|
|
82
174
|
let blockEl = null;
|
|
83
175
|
let blockSummaryEl = null;
|
|
84
176
|
let blockToggled = false;
|
|
85
|
-
// gen →
|
|
177
|
+
// gen → a step's live state (newStep), its rendered nodes between
|
|
178
|
+
// part.startNode and end.
|
|
86
179
|
const genEls = new Map();
|
|
87
180
|
// Steer rows waiting for the step that answers them (steer()).
|
|
88
181
|
let pendingSteers = [];
|
|
@@ -97,11 +190,6 @@ export function createTurnView({
|
|
|
97
190
|
if (hint) hint.remove();
|
|
98
191
|
}
|
|
99
192
|
|
|
100
|
-
// A hand toggle: the code's auto-collapse then leaves it as the user left it.
|
|
101
|
-
function rememberToggle(summary, mark) {
|
|
102
|
-
summary.addEventListener("click", mark);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
193
|
function createBlock() {
|
|
106
194
|
removeHint();
|
|
107
195
|
blockEl = document.createElement("details");
|
|
@@ -110,7 +198,7 @@ export function createTurnView({
|
|
|
110
198
|
blockToggled = false;
|
|
111
199
|
blockSummaryEl = document.createElement("summary");
|
|
112
200
|
blockEl.appendChild(blockSummaryEl);
|
|
113
|
-
|
|
201
|
+
blockSummaryEl.addEventListener("click", () => { blockToggled = true; });
|
|
114
202
|
updateBlockSummary();
|
|
115
203
|
blockHost(blockEl);
|
|
116
204
|
}
|
|
@@ -119,77 +207,89 @@ export function createTurnView({
|
|
|
119
207
|
if (blockSummaryEl && turn) blockSummaryEl.textContent = model.blockSummary(turn, { running: !turn.ended });
|
|
120
208
|
}
|
|
121
209
|
|
|
210
|
+
// The step rendered again from its state (stepTemplate). Each live step
|
|
211
|
+
// is its own lit root, not an item of one block template: newestFirst
|
|
212
|
+
// puts a new step on top and chronological() reorders them, and lit
|
|
213
|
+
// (without the keyed repeat directive, not vendored) matches list items
|
|
214
|
+
// by index, so a block-wide render would hand step N's <details> (and
|
|
215
|
+
// its hand toggle) to another gen. The root's part lives on entry.end
|
|
216
|
+
// (render's renderBefore), so the step's nodes can move anywhere in the
|
|
217
|
+
// page between lit's start marker and that end comment (stepNodes).
|
|
218
|
+
function draw(entry) {
|
|
219
|
+
entry.part = render(stepTemplate(entry.gen, {
|
|
220
|
+
live: !entry.closed, index: entry.index, open: entry.open, steers: entry.steers, items: entry.items,
|
|
221
|
+
body: entry.thinkingText, line: entry.line, sentences: entry.sentences, placeholder: entry.placeholder, on: entry.on,
|
|
222
|
+
}), entry.frag, { renderBefore: entry.end });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// A step's nodes (lit's start marker, the <details>, the end marker), to
|
|
226
|
+
// move or remove them together.
|
|
227
|
+
function stepNodes(entry) {
|
|
228
|
+
const nodes = [];
|
|
229
|
+
for (let n = entry.part.startNode; n; n = n.nextSibling) {
|
|
230
|
+
nodes.push(n);
|
|
231
|
+
if (n === entry.end) break;
|
|
232
|
+
}
|
|
233
|
+
return nodes;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const thinkingWrap = (entry) => entry.el.querySelector(":scope > details.thinking");
|
|
237
|
+
const narrationEl = (entry) => entry.el.querySelector(":scope > .gen-text > .narration");
|
|
238
|
+
|
|
122
239
|
function createGenEl(gen) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
240
|
+
const entry = {
|
|
241
|
+
gen, index: turn.gens.length - 1, frag: document.createDocumentFragment(), end: document.createComment(""),
|
|
242
|
+
part: null, el: null, open: true, toggled: false, closed: false,
|
|
243
|
+
// A plugin's steers merged since the last step: this step answers them.
|
|
244
|
+
steers: pendingSteers, items: [], rows: new Map(),
|
|
245
|
+
// A step after the first shows "…" until its text comes (the first one
|
|
246
|
+
// only for a prompt).
|
|
247
|
+
placeholder: turn.gens.length > 1, sentences: null, feed: createSentenceFeed(),
|
|
248
|
+
thinkingText: null, line: null, ticker: null, tickerTimer: null, thinkingToggled: false, on: null,
|
|
249
|
+
};
|
|
131
250
|
pendingSteers = [];
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
135
|
-
// only
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
251
|
+
// A real click on a summary is a hand toggle: the code's auto-collapse
|
|
252
|
+
// then leaves it as the user left it. A peek at the thinking is per step
|
|
253
|
+
// (the page is for glancing): later steps start closed. The toggle event
|
|
254
|
+
// (queued after <details>.open flips) only scrolls the body to its tail
|
|
255
|
+
// on open.
|
|
256
|
+
entry.on = {
|
|
257
|
+
stepClick: () => { entry.toggled = true; },
|
|
258
|
+
thinkingClick: () => { entry.thinkingToggled = true; },
|
|
259
|
+
thinkingToggle: (e) => {
|
|
260
|
+
const wrap = e.currentTarget;
|
|
261
|
+
const body = wrap.querySelector(".thinking-body");
|
|
262
|
+
if (wrap.open && body) body.scrollTop = body.scrollHeight;
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
entry.frag.append(entry.end);
|
|
142
266
|
genEls.set(gen, entry);
|
|
267
|
+
draw(entry);
|
|
268
|
+
entry.el = entry.end.previousElementSibling || entry.frag.querySelector("details.gen");
|
|
143
269
|
const stick = isNearBottom();
|
|
144
|
-
if (newestFirst) blockEl.insertBefore(
|
|
145
|
-
else blockEl.appendChild(
|
|
270
|
+
if (newestFirst) blockEl.insertBefore(entry.frag, blockSummaryEl.nextSibling);
|
|
271
|
+
else blockEl.appendChild(entry.frag);
|
|
146
272
|
if (stick) follow();
|
|
147
273
|
return entry;
|
|
148
274
|
}
|
|
149
275
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
function thinkingEl(gen) {
|
|
154
|
-
const entry = genEls.get(gen);
|
|
155
|
-
if (!entry.thinking) {
|
|
156
|
-
entry.thinkingWrap = document.createElement("details");
|
|
157
|
-
entry.thinkingWrap.className = "thinking";
|
|
158
|
-
entry.thinkingLine = document.createElement("summary");
|
|
159
|
-
entry.thinkingLine.textContent = "thinking";
|
|
160
|
-
entry.thinkingWrap.appendChild(entry.thinkingLine);
|
|
161
|
-
entry.thinking = document.createElement("div");
|
|
162
|
-
entry.thinking.className = "thinking-body";
|
|
163
|
-
entry.thinkingWrap.appendChild(entry.thinking);
|
|
164
|
-
// A real click on the summary is a hand toggle of *this* step: its wrap
|
|
165
|
-
// then stays as the user left it at the step's end. A peek is per step
|
|
166
|
-
// (the page is for glancing): later steps start closed. The toggle
|
|
167
|
-
// event (queued after <details>.open flips) only scrolls the body to
|
|
168
|
-
// its tail on open.
|
|
169
|
-
rememberToggle(entry.thinkingLine, () => { entry.thinkingToggled = true; });
|
|
170
|
-
entry.thinkingWrap.addEventListener("toggle", () => {
|
|
171
|
-
if (entry.thinkingWrap.open) entry.thinking.scrollTop = entry.thinking.scrollHeight;
|
|
172
|
-
});
|
|
173
|
-
entry.ticker = createTicker({ now: () => Date.now(), dwellMs: 1500 });
|
|
174
|
-
entry.el.insertBefore(entry.thinkingWrap, entry.text);
|
|
175
|
-
}
|
|
176
|
-
return entry.thinking;
|
|
276
|
+
function stopTicker(entry) {
|
|
277
|
+
if (entry.tickerTimer) { clearTimeout(entry.tickerTimer); entry.tickerTimer = null; }
|
|
278
|
+
entry.ticker?.close();
|
|
177
279
|
}
|
|
178
280
|
|
|
179
|
-
// The ticker's newest sentence into the summary
|
|
180
|
-
//
|
|
281
|
+
// The ticker's newest sentence into the thinking's summary, with a subtle
|
|
282
|
+
// fade on change; the class is removed at close (reload parity: the
|
|
283
|
+
// re-rendered summary is plain "thinking").
|
|
181
284
|
function setThinkingLine(entry, sentence) {
|
|
182
|
-
entry.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
entry.thinkingLine.classList.remove("thinking-fade");
|
|
191
|
-
void entry.thinkingLine.offsetWidth;
|
|
192
|
-
entry.thinkingLine.classList.add("thinking-fade");
|
|
285
|
+
entry.line = sentence;
|
|
286
|
+
draw(entry);
|
|
287
|
+
const summary = thinkingWrap(entry)?.querySelector(":scope > summary");
|
|
288
|
+
if (summary) {
|
|
289
|
+
summary.classList.remove("thinking-fade");
|
|
290
|
+
void summary.offsetWidth;
|
|
291
|
+
summary.classList.add("thinking-fade");
|
|
292
|
+
}
|
|
193
293
|
onThinkingLine(sentence);
|
|
194
294
|
}
|
|
195
295
|
|
|
@@ -208,76 +308,46 @@ export function createTurnView({
|
|
|
208
308
|
}
|
|
209
309
|
}
|
|
210
310
|
|
|
211
|
-
//
|
|
212
|
-
//
|
|
213
|
-
//
|
|
214
|
-
|
|
215
|
-
function narrationBox(entry) {
|
|
216
|
-
if (!entry.box) {
|
|
217
|
-
entry.text.textContent = "";
|
|
218
|
-
entry.box = document.createElement("div");
|
|
219
|
-
entry.box.className = "narration";
|
|
220
|
-
entry.text.appendChild(entry.box);
|
|
221
|
-
}
|
|
222
|
-
return entry.box;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Append complete sentences to the box, which follows its own tail unless
|
|
226
|
-
// the user scrolled up inside it to read (decided before the append: a
|
|
227
|
-
// burst lands several lines at once).
|
|
228
|
-
function appendSentences(entry, sentences) {
|
|
311
|
+
// Complete sentences into the narration box, which follows its own tail
|
|
312
|
+
// unless the user scrolled up inside it to read (decided before the
|
|
313
|
+
// append: a burst lands several lines at once).
|
|
314
|
+
function addSentences(entry, sentences) {
|
|
229
315
|
if (!sentences.length) return;
|
|
230
|
-
const box =
|
|
231
|
-
const stick = shouldFollowScroll(box, 24);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
box.appendChild(line);
|
|
237
|
-
}
|
|
238
|
-
if (stick) box.scrollTop = box.scrollHeight;
|
|
316
|
+
const box = narrationEl(entry);
|
|
317
|
+
const stick = !box || shouldFollowScroll(box, 24);
|
|
318
|
+
entry.sentences = [...(entry.sentences || []), ...sentences];
|
|
319
|
+
draw(entry);
|
|
320
|
+
const drawn = narrationEl(entry);
|
|
321
|
+
if (stick && drawn) drawn.scrollTop = drawn.scrollHeight;
|
|
239
322
|
}
|
|
240
323
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
}
|
|
248
|
-
return entry.activity;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// The gen's text and thinking into its elements. The thinking is appended by
|
|
252
|
-
// delta (a selection survives; a full rewrite would drop it) and rewritten
|
|
253
|
-
// only when it shrank (the close's rtrim); the ticker advances from it while
|
|
254
|
-
// the step is live. The live narration goes into the box by complete
|
|
255
|
-
// sentences; a closed gen's text (the close itself, a late chunk via genFor)
|
|
256
|
-
// is written in full. The trailing whitespace is trimmed by the model at
|
|
257
|
-
// close.
|
|
324
|
+
// The gen's text and thinking into its step. The thinking is appended by
|
|
325
|
+
// delta to one Text node (a selection survives; a full rewrite would drop
|
|
326
|
+
// it) and rewritten only when it shrank (the close's rtrim); the ticker
|
|
327
|
+
// advances from it while the step is live. The live narration goes into
|
|
328
|
+
// the box by complete sentences; a closed gen shows its full text (the
|
|
329
|
+
// close itself, a late chunk via genFor).
|
|
258
330
|
function writeGen(gen, { follow = false } = {}) {
|
|
259
331
|
const entry = genEls.get(gen);
|
|
260
332
|
if (!entry) return;
|
|
261
333
|
if (gen.thinking) {
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
if (
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
entry.thinkingWritten = gen.thinking.length;
|
|
268
|
-
}
|
|
269
|
-
} else {
|
|
270
|
-
body.textContent = gen.thinking;
|
|
271
|
-
entry.thinkingWritten = gen.thinking.length;
|
|
334
|
+
const before = entry.thinkingText?.parentNode;
|
|
335
|
+
const stick = follow && before && shouldFollowScroll(before, 24);
|
|
336
|
+
if (!entry.thinkingText) {
|
|
337
|
+
entry.thinkingText = document.createTextNode("");
|
|
338
|
+
entry.ticker = createTicker({ now: () => Date.now(), dwellMs: 1500 });
|
|
272
339
|
}
|
|
273
|
-
|
|
340
|
+
const node = entry.thinkingText;
|
|
341
|
+
if (gen.thinking.length > node.length) node.appendData(gen.thinking.slice(node.length));
|
|
342
|
+
else if (gen.thinking.length < node.length) node.data = gen.thinking;
|
|
343
|
+
draw(entry);
|
|
344
|
+
if (stick) before.scrollTop = before.scrollHeight;
|
|
274
345
|
// Late chunks reach closed gens (genFor); their ticker stays still.
|
|
275
346
|
if (!gen.closed) feedTicker(gen);
|
|
276
347
|
}
|
|
277
|
-
// An empty text keeps the "…" placeholder.
|
|
278
348
|
if (!gen.text) return;
|
|
279
|
-
if (gen.closed) entry
|
|
280
|
-
else
|
|
349
|
+
if (gen.closed) draw(entry);
|
|
350
|
+
else addSentences(entry, entry.feed.feed(gen.text));
|
|
281
351
|
}
|
|
282
352
|
|
|
283
353
|
function flush() {
|
|
@@ -298,68 +368,47 @@ export function createTurnView({
|
|
|
298
368
|
requestAnimationFrame(flush);
|
|
299
369
|
}
|
|
300
370
|
|
|
301
|
-
// A step left with its thinking only would read "thinking" twice (its own
|
|
302
|
-
// summary and its wrap's): the wrap goes and the body sits directly under
|
|
303
|
-
// the step's summary, as turnHistoryHtml draws it.
|
|
304
|
-
function unwrapThinking(entry) {
|
|
305
|
-
if (!entry.thinkingWrap) return;
|
|
306
|
-
if (entry.tickerTimer) { clearTimeout(entry.tickerTimer); entry.tickerTimer = null; }
|
|
307
|
-
entry.ticker.close();
|
|
308
|
-
entry.thinkingWrap.replaceWith(entry.thinking);
|
|
309
|
-
entry.thinkingWrap = null;
|
|
310
|
-
entry.thinkingLine = null;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
371
|
// A closed gen: its final text, collapsed to its label (unless the user
|
|
314
|
-
// opened it by hand), no longer live
|
|
372
|
+
// opened it by hand), no longer live; its thinking's summary back to the
|
|
373
|
+
// plain "thinking", collapsed unless the user opened it.
|
|
315
374
|
function closeGenEl(gen) {
|
|
316
375
|
const entry = genEls.get(gen);
|
|
317
376
|
if (!entry) return;
|
|
318
377
|
dirty.delete(gen);
|
|
319
378
|
writeGen(gen);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
entry.
|
|
323
|
-
entry.
|
|
324
|
-
entry.
|
|
325
|
-
entry
|
|
326
|
-
if (entry.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
entry.thinkingLine.classList.remove("thinking-fade");
|
|
334
|
-
if (!entry.thinkingToggled) entry.thinkingWrap.open = false;
|
|
335
|
-
// A thinking-only middle step (a single-gen turn is promote's plain
|
|
336
|
-
// answer, which reuses the wrap).
|
|
337
|
-
if (!gen.text && !gen.tools.length && turn.gens.length > 1) unwrapThinking(entry);
|
|
379
|
+
stopTicker(entry);
|
|
380
|
+
entry.line = null;
|
|
381
|
+
entry.closed = true;
|
|
382
|
+
entry.sentences = null;
|
|
383
|
+
if (!entry.toggled) entry.open = false;
|
|
384
|
+
const wrap = thinkingWrap(entry);
|
|
385
|
+
if (wrap && !entry.thinkingToggled) wrap.open = false;
|
|
386
|
+
draw(entry);
|
|
387
|
+
// The fade gone without a class="" left behind (the reloaded summary has none).
|
|
388
|
+
const summary = thinkingWrap(entry)?.querySelector(":scope > summary");
|
|
389
|
+
if (summary?.classList.contains("thinking-fade")) {
|
|
390
|
+
summary.classList.remove("thinking-fade");
|
|
391
|
+
if (!summary.classList.length) summary.removeAttribute("class");
|
|
338
392
|
}
|
|
339
|
-
writeClosedLabel(entry, gen);
|
|
340
|
-
if (!entry.toggled) entry.el.open = false;
|
|
341
393
|
// Every generation's text goes into the shown texts (the turn output dedupe).
|
|
342
394
|
const k = normalize(gen.text);
|
|
343
395
|
if (k) sawText(k);
|
|
344
396
|
}
|
|
345
397
|
|
|
346
|
-
// A closed step's label and its failed mark (a red chevron that survives
|
|
347
|
-
// an ellipsed label): at close, and again when a late tool result lands
|
|
348
|
-
// in a step that already closed.
|
|
349
|
-
function writeClosedLabel(entry, gen) {
|
|
350
|
-
setLabel(entry.summary, gen);
|
|
351
|
-
entry.el.classList.toggle("has-failed", model.failedCount(gen.tools) > 0);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
398
|
// A step that closes mid-turn (the next one starts) collapses: a warn card
|
|
355
399
|
// in it would go out of sight until the turn ends. It moves out of the
|
|
356
400
|
// step, right after it in the block (open while the turn runs), where
|
|
357
401
|
// turnEnded still finds it and takes it out of the block (leavesBlock).
|
|
402
|
+
// The card stays in entry.items while it sits outside the step: safe only
|
|
403
|
+
// because the step's items are never removed or reordered after this (a
|
|
404
|
+
// late row is appended, a no-op for the parts before it). A render whose
|
|
405
|
+
// items shifted would make lit insert the card back into the step (or
|
|
406
|
+
// another item into its slot).
|
|
358
407
|
function liftWarnCards(gen) {
|
|
359
408
|
const entry = genEls.get(gen);
|
|
360
409
|
if (!entry) return;
|
|
361
410
|
const cards = [...entry.el.querySelectorAll(".plugin-card.warn")];
|
|
362
|
-
if (cards.length) entry.
|
|
411
|
+
if (cards.length) entry.end.after(...cards);
|
|
363
412
|
}
|
|
364
413
|
|
|
365
414
|
function apply(r) {
|
|
@@ -376,28 +425,34 @@ export function createTurnView({
|
|
|
376
425
|
const rowHtml = new WeakMap();
|
|
377
426
|
const rowThumbs = (images) => thumbsHtml(sessionId(), images);
|
|
378
427
|
|
|
428
|
+
// A row, notice or card into the step's activity; a step that wrote no
|
|
429
|
+
// narration before it drops the "…".
|
|
430
|
+
function addItem(entry, item) {
|
|
431
|
+
if (!entry.gen.text) entry.placeholder = false;
|
|
432
|
+
const stick = isNearBottom();
|
|
433
|
+
entry.items.push(item);
|
|
434
|
+
draw(entry);
|
|
435
|
+
if (stick) follow();
|
|
436
|
+
}
|
|
437
|
+
|
|
379
438
|
function toolEvent(r) {
|
|
380
439
|
apply(r);
|
|
381
440
|
const entry = genEls.get(r.gen);
|
|
382
|
-
// A step that wrote no narration before its calls drops the "…".
|
|
383
|
-
if (!r.gen.text) entry.text.textContent = "";
|
|
384
|
-
const body = activityEl(r.gen);
|
|
385
441
|
let el = entry.rows.get(r.row.key);
|
|
386
|
-
const
|
|
442
|
+
const inner = toolRowInnerHtml(r.row, { thumbs: rowThumbs });
|
|
387
443
|
if (!el) {
|
|
388
444
|
el = htmlElement(toolRowHtml(r.row, { thumbs: rowThumbs }));
|
|
389
|
-
rowHtml.set(el,
|
|
445
|
+
rowHtml.set(el, inner);
|
|
390
446
|
entry.rows.set(r.row.key, el);
|
|
391
|
-
|
|
392
|
-
body.appendChild(el);
|
|
393
|
-
syncTaskStop(el, r.row);
|
|
394
|
-
if (stick) follow();
|
|
447
|
+
addItem(entry, el);
|
|
395
448
|
} else {
|
|
396
|
-
if (
|
|
397
|
-
rowHtml.
|
|
398
|
-
|
|
449
|
+
if (!r.gen.text) entry.placeholder = false;
|
|
450
|
+
if (rowHtml.get(el) !== inner) redrawRow(el, inner);
|
|
451
|
+
rowHtml.set(el, inner);
|
|
452
|
+
// A late result in a closed step: its label and failed mark.
|
|
453
|
+
draw(entry);
|
|
399
454
|
}
|
|
400
|
-
|
|
455
|
+
syncTaskStop(el, r.row);
|
|
401
456
|
updateBlockSummary();
|
|
402
457
|
onChange();
|
|
403
458
|
}
|
|
@@ -421,21 +476,11 @@ export function createTurnView({
|
|
|
421
476
|
|
|
422
477
|
// A hook's line (hook_notice) as a row of the current step, above the row
|
|
423
478
|
// of the call it judged; false with no running turn (app.js shows the
|
|
424
|
-
// bubble then).
|
|
425
|
-
// retry), shown as is; data.cls a class of its own and data.title its
|
|
426
|
-
// hover (a ✂ row's).
|
|
479
|
+
// bubble then).
|
|
427
480
|
function notice(data) {
|
|
428
481
|
const r = turn && model.notice(turn, data);
|
|
429
482
|
if (!r) return false;
|
|
430
|
-
|
|
431
|
-
if (!r.gen.text) entry.text.textContent = "";
|
|
432
|
-
const el = document.createElement("div");
|
|
433
|
-
el.className = "hook-notice" + (data.level === "warn" ? " warn" : "") + (data.cls ? ` ${data.cls}` : "");
|
|
434
|
-
el.textContent = data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`;
|
|
435
|
-
if (data.title) el.title = data.title;
|
|
436
|
-
const stick = isNearBottom();
|
|
437
|
-
activityEl(r.gen).appendChild(el);
|
|
438
|
-
if (stick) follow();
|
|
483
|
+
addItem(genEls.get(r.gen), noticeTemplate(data));
|
|
439
484
|
return true;
|
|
440
485
|
}
|
|
441
486
|
|
|
@@ -446,9 +491,7 @@ export function createTurnView({
|
|
|
446
491
|
// that ends first puts it in its last step). False with no running turn.
|
|
447
492
|
function steer(data) {
|
|
448
493
|
if (!turn || turn.ended) return false;
|
|
449
|
-
|
|
450
|
-
holder.innerHTML = steerRowHtml(data);
|
|
451
|
-
pendingSteers.push(holder.firstChild);
|
|
494
|
+
pendingSteers.push(htmlElement(steerRowHtml(data)));
|
|
452
495
|
return true;
|
|
453
496
|
}
|
|
454
497
|
|
|
@@ -456,7 +499,10 @@ export function createTurnView({
|
|
|
456
499
|
function flushSteers() {
|
|
457
500
|
const gen = turn.gens[turn.gens.length - 1];
|
|
458
501
|
const entry = gen && genEls.get(gen);
|
|
459
|
-
if (entry)
|
|
502
|
+
if (entry) {
|
|
503
|
+
entry.steers = [...entry.steers, ...pendingSteers];
|
|
504
|
+
draw(entry);
|
|
505
|
+
}
|
|
460
506
|
pendingSteers = [];
|
|
461
507
|
}
|
|
462
508
|
|
|
@@ -467,14 +513,11 @@ export function createTurnView({
|
|
|
467
513
|
const gen = turn && !turn.ended ? model.currentGen(turn) : null;
|
|
468
514
|
const entry = gen && genEls.get(gen);
|
|
469
515
|
if (!entry) return false;
|
|
470
|
-
|
|
471
|
-
const stick = isNearBottom();
|
|
472
|
-
activityEl(gen).appendChild(el);
|
|
473
|
-
if (stick) follow();
|
|
516
|
+
addItem(entry, el);
|
|
474
517
|
return true;
|
|
475
518
|
}
|
|
476
519
|
|
|
477
|
-
// The answer: the gen's narration
|
|
520
|
+
// The answer: the gen's narration leaves the block as a bubble. With
|
|
478
521
|
// +heldHeight+ (the live box's height, measured before the step closed) the
|
|
479
522
|
// bubble is held at that height, scrolled to its tail, until answerReady()
|
|
480
523
|
// (the rendered markdown landed) or 1.5 s reveals it.
|
|
@@ -482,63 +525,52 @@ export function createTurnView({
|
|
|
482
525
|
// builds it), placed where the answer bubble would go.
|
|
483
526
|
function promote(gen, { heldHeight = 0, endNote = null } = {}) {
|
|
484
527
|
const entry = genEls.get(gen);
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
const holding = useHold && heldHeight > 0 && !!gen.text && !document.hidden;
|
|
492
|
-
let holdHeight = 0;
|
|
493
|
-
if (holding) {
|
|
494
|
-
// Bubbles are border-box: the box's height plus the bubble's own
|
|
495
|
-
// padding and border keeps the same three lines in view.
|
|
496
|
-
const cs = getComputedStyle(bubble);
|
|
497
|
-
const chrome = ["paddingTop", "paddingBottom", "borderTopWidth", "borderBottomWidth"]
|
|
498
|
-
.reduce((sum, k) => sum + (parseFloat(cs[k]) || 0), 0);
|
|
499
|
-
holdHeight = heldHeight + chrome;
|
|
528
|
+
// No text (an empty answer): no bubble.
|
|
529
|
+
let bubble = null;
|
|
530
|
+
if (gen.text) {
|
|
531
|
+
bubble = document.createElement("div");
|
|
532
|
+
bubble.className = "bubble output";
|
|
533
|
+
bubble.textContent = gen.text;
|
|
500
534
|
}
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
setLabel(entry.summary, { ...gen, text: "" });
|
|
535
|
+
// A hidden tab gets no pop: nobody sees it, and its timers are throttled.
|
|
536
|
+
const holding = useHold && heldHeight > 0 && !!bubble && !document.hidden;
|
|
504
537
|
model.takeAnswer(turn, gen);
|
|
538
|
+
draw(entry);
|
|
505
539
|
// Cards and hook notices in a step that goes (below, or with the whole
|
|
506
540
|
// block for a plain answer) stay: before the answer, out of the block,
|
|
507
541
|
// where a reload puts a turn's cards; a notice as the bubble it is
|
|
508
|
-
// between turns.
|
|
542
|
+
// between turns. These are lit-rendered nodes taken out of the step:
|
|
543
|
+
// safe only because the step is never rendered again with other items
|
|
544
|
+
// or steers (it is removed, its block goes, or it only closes); such a
|
|
545
|
+
// render would move them back in, and a changed class binding on a
|
|
546
|
+
// notice would drop the "bubble" class added here.
|
|
509
547
|
const leaving = !gen.thinking && gen.tools.length === 0;
|
|
510
548
|
const plain = turn.gens.length === 1 && gen.tools.length === 0;
|
|
511
549
|
const cards = leaving || plain ? [...entry.el.querySelectorAll(".plugin-card, .hook-notice, .steer-row")] : [];
|
|
512
550
|
cards.forEach((el) => { if (!el.classList.contains("plugin-card")) el.classList.add("bubble"); });
|
|
513
|
-
if (leaving) entry.
|
|
551
|
+
if (leaving) stepNodes(entry).forEach((n) => n.remove());
|
|
514
552
|
if (plain) {
|
|
515
553
|
// A plain answer: no block, the bubble where it was; its thinking, if
|
|
516
554
|
// any, as a collapsed thinking block above it.
|
|
517
555
|
const pieces = [];
|
|
518
|
-
if (gen.thinking) {
|
|
519
|
-
if (!entry.thinkingWrap) thinkingEl(gen);
|
|
520
|
-
// Reuse the live step's thinking wrap, re-classed as the plain
|
|
521
|
-
// answer's collapsed block (the same element, not a new one): plain
|
|
522
|
-
// summary, no timer, closed.
|
|
523
|
-
if (entry.tickerTimer) { clearTimeout(entry.tickerTimer); entry.tickerTimer = null; }
|
|
524
|
-
entry.ticker.close();
|
|
525
|
-
entry.thinkingWrap.className = "bubble thinking";
|
|
526
|
-
entry.thinkingWrap.open = false;
|
|
527
|
-
entry.thinkingLine.textContent = "thinking";
|
|
528
|
-
pieces.push(entry.thinkingWrap);
|
|
529
|
-
}
|
|
556
|
+
if (gen.thinking) pieces.push(nodesOf(thinkingTemplate({ body: gen.thinking, cls: "bubble thinking" })).firstElementChild);
|
|
530
557
|
pieces.push(...cards);
|
|
531
|
-
if (
|
|
558
|
+
if (bubble) pieces.push(bubble);
|
|
532
559
|
else if (endNote) pieces.push(endNote);
|
|
533
560
|
placeAnswer(pieces, { plain: true, blockEl });
|
|
534
561
|
blockEl = null;
|
|
535
562
|
blockSummaryEl = null;
|
|
536
563
|
} else {
|
|
537
|
-
placeAnswer(
|
|
538
|
-
|
|
539
|
-
|
|
564
|
+
placeAnswer(bubble ? [...cards, bubble] : [...cards, ...(endNote ? [endNote] : [])], { plain: false, blockEl });
|
|
565
|
+
}
|
|
566
|
+
if (holding) {
|
|
567
|
+
// Bubbles are border-box: the box's height plus the bubble's own
|
|
568
|
+
// padding and border keeps the same three lines in view.
|
|
569
|
+
const cs = getComputedStyle(bubble);
|
|
570
|
+
const chrome = ["paddingTop", "paddingBottom", "borderTopWidth", "borderBottomWidth"]
|
|
571
|
+
.reduce((sum, k) => sum + (parseFloat(cs[k]) || 0), 0);
|
|
572
|
+
hold.start(bubble, heldHeight + chrome);
|
|
540
573
|
}
|
|
541
|
-
if (holding) hold.start(bubble, holdHeight);
|
|
542
574
|
}
|
|
543
575
|
|
|
544
576
|
// Returns the cards that left the block (card.js leavesBlock), detached:
|
|
@@ -557,7 +589,8 @@ export function createTurnView({
|
|
|
557
589
|
// still open: the apply below swaps it for the full text and collapses
|
|
558
590
|
// the step. Only a completed turn's answer pops; a canceled or failed
|
|
559
591
|
// one shows at once.
|
|
560
|
-
const
|
|
592
|
+
const lastEntry = promoting && kind === "completed" ? genEls.get(last) : null;
|
|
593
|
+
const box = lastEntry ? narrationEl(lastEntry) : null;
|
|
561
594
|
const heldHeight = box ? box.getBoundingClientRect().height : 0;
|
|
562
595
|
if (pendingSteers.length) flushSteers();
|
|
563
596
|
apply(model.turnEnded(turn));
|
|
@@ -586,9 +619,7 @@ export function createTurnView({
|
|
|
586
619
|
blockSummaryEl = null;
|
|
587
620
|
blockToggled = false;
|
|
588
621
|
// Clear every ticker timer before the map goes (a live step mid-dwell).
|
|
589
|
-
for (const entry of genEls.values())
|
|
590
|
-
if (entry.tickerTimer) { clearTimeout(entry.tickerTimer); entry.tickerTimer = null; }
|
|
591
|
-
}
|
|
622
|
+
for (const entry of genEls.values()) stopTicker(entry);
|
|
592
623
|
genEls.clear();
|
|
593
624
|
pendingSteers = [];
|
|
594
625
|
dirty.clear();
|
|
@@ -602,7 +633,11 @@ export function createTurnView({
|
|
|
602
633
|
createBlock();
|
|
603
634
|
const r = model.turnStarted(turn);
|
|
604
635
|
apply(r);
|
|
605
|
-
if (data.prompt)
|
|
636
|
+
if (data.prompt) {
|
|
637
|
+
const entry = genEls.get(r.gen);
|
|
638
|
+
entry.placeholder = true;
|
|
639
|
+
draw(entry);
|
|
640
|
+
}
|
|
606
641
|
},
|
|
607
642
|
generationStarted(data) {
|
|
608
643
|
if (!turn) this.turnStarted({});
|
|
@@ -624,14 +659,10 @@ export function createTurnView({
|
|
|
624
659
|
const entry = r && genEls.get(r.gen);
|
|
625
660
|
if (!entry) return;
|
|
626
661
|
dirty.delete(r.gen);
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
}
|
|
632
|
-
Object.assign(entry, { thinking: null, thinkingWrap: null, thinkingLine: null, ticker: null, thinkingWritten: 0,
|
|
633
|
-
box: null, feed: createSentenceFeed() });
|
|
634
|
-
entry.text.textContent = "…";
|
|
662
|
+
stopTicker(entry);
|
|
663
|
+
Object.assign(entry, { thinkingText: null, line: null, ticker: null, sentences: null, placeholder: true,
|
|
664
|
+
feed: createSentenceFeed() });
|
|
665
|
+
draw(entry);
|
|
635
666
|
onChange();
|
|
636
667
|
},
|
|
637
668
|
generationCompleted() {
|
|
@@ -644,7 +675,7 @@ export function createTurnView({
|
|
|
644
675
|
// The remainder is the step's last line: narration without a final
|
|
645
676
|
// period ("Let me check the shell first") would otherwise never show.
|
|
646
677
|
const entry = genEls.get(r.gen);
|
|
647
|
-
if (entry && r.gen.text && !r.gen.closed)
|
|
678
|
+
if (entry && r.gen.text && !r.gen.closed) addSentences(entry, entry.feed.flush(r.gen.text));
|
|
648
679
|
},
|
|
649
680
|
toolStarted(data) {
|
|
650
681
|
if (!turn) this.turnStarted({});
|
|
@@ -669,7 +700,9 @@ export function createTurnView({
|
|
|
669
700
|
containsLive(el) {
|
|
670
701
|
const gen = turn && model.currentGen(turn);
|
|
671
702
|
const entry = gen && genEls.get(gen);
|
|
672
|
-
|
|
703
|
+
if (!entry) return false;
|
|
704
|
+
const text = entry.el.querySelector(":scope > .gen-text");
|
|
705
|
+
return !!text?.contains(el) || !!entry.thinkingText?.parentNode?.contains(el);
|
|
673
706
|
},
|
|
674
707
|
reset,
|
|
675
708
|
// For a view that composes this one (the stage): the turn's model and
|
|
@@ -678,13 +711,13 @@ export function createTurnView({
|
|
|
678
711
|
current() { return { turn, blockEl }; },
|
|
679
712
|
step(i) {
|
|
680
713
|
const entry = turn && genEls.get(turn.gens[i]);
|
|
681
|
-
return entry ? { el: entry.el, summary: entry.summary, thinkingWrap: entry
|
|
714
|
+
return entry ? { el: entry.el, summary: entry.el.querySelector(":scope > summary"), thinkingWrap: thinkingWrap(entry) } : null;
|
|
682
715
|
},
|
|
683
716
|
chronological() {
|
|
684
717
|
if (!blockEl || !turn) return;
|
|
685
718
|
for (const gen of turn.gens) {
|
|
686
|
-
const
|
|
687
|
-
if (el
|
|
719
|
+
const entry = genEls.get(gen);
|
|
720
|
+
if (entry?.el.parentNode === blockEl) blockEl.append(...stepNodes(entry));
|
|
688
721
|
}
|
|
689
722
|
},
|
|
690
723
|
};
|
|
@@ -699,28 +732,7 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
699
732
|
};
|
|
700
733
|
}
|
|
701
734
|
|
|
702
|
-
// A
|
|
703
|
-
// body starts with (the narration's first line, the first call's title)
|
|
704
|
-
// and it has calls, the summary is marked .repeats and its parts are
|
|
705
|
-
// spans: an open step shows only its calls (index.html), so it doesn't say
|
|
706
|
-
// the same words twice. Its text stays genLabel's either way.
|
|
707
|
-
// { repeats, html } (the summary's inside).
|
|
708
|
-
function labelParts(gen) {
|
|
709
|
-
const { head, calls, repeats } = model.genLabelParts(gen);
|
|
710
|
-
if (!repeats || !calls) return { repeats: false, html: escapeHtml(model.genLabel(gen)) };
|
|
711
|
-
return { repeats: true, html: `<span class="gen-head">${escapeHtml(head)}</span><span class="gen-sep"> · </span><span class="gen-calls">${escapeHtml(calls)}</span>` };
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
function labelHtml(gen) {
|
|
715
|
-
const { repeats, html } = labelParts(gen);
|
|
716
|
-
return `<summary${repeats ? ' class="repeats"' : ""}>${html}</summary>`;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function setLabel(summary, gen) {
|
|
720
|
-
const { repeats, html } = labelParts(gen);
|
|
721
|
-
summary.classList.toggle("repeats", repeats);
|
|
722
|
-
summary.innerHTML = html;
|
|
723
|
-
}
|
|
735
|
+
// ---- A reload ----
|
|
724
736
|
|
|
725
737
|
// A re-rendered failed turn's line (a .bubble.cancel, as live's
|
|
726
738
|
// addEndBubble), in turn_failed's words (failedTurnText) from what the turn
|
|
@@ -733,13 +745,28 @@ function failedLineHtml(record) {
|
|
|
733
745
|
return `<div class="bubble cancel">${escapeHtml(failedTurnText(failure))}</div>`;
|
|
734
746
|
}
|
|
735
747
|
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
748
|
+
// Parts (HTML strings and templates, in order) as one fragment.
|
|
749
|
+
function fragmentOf(parts) {
|
|
750
|
+
const out = document.createDocumentFragment();
|
|
751
|
+
let markup = "";
|
|
752
|
+
for (const part of parts) {
|
|
753
|
+
if (typeof part === "string") { markup += part; continue; }
|
|
754
|
+
if (markup) out.append(rawNodes(markup));
|
|
755
|
+
markup = "";
|
|
756
|
+
out.append(nodesOf(part));
|
|
757
|
+
}
|
|
758
|
+
if (markup) out.append(rawNodes(markup));
|
|
759
|
+
return out;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// A reloaded history in the turn view (renderHistory's object path), as
|
|
763
|
+
// nodes: per turn the prompt, a collapsed block of its steps when it had
|
|
764
|
+
// any (stepTemplate, the live page's: texts, and with the server's parts
|
|
765
|
+
// the thinking, params and output, from the saved messages; status and
|
|
766
|
+
// duration from the timing's tool records), then the answer and the turn's
|
|
767
|
+
// timing line under it.
|
|
741
768
|
// +thumbs(images)+ draws a prompt's or a tool row's image thumbs.
|
|
742
|
-
export function
|
|
769
|
+
export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
743
770
|
const timingHtml = (group) => {
|
|
744
771
|
const record = group.record;
|
|
745
772
|
// Numbered by its place in the turn records, as live (a failed turn
|
|
@@ -773,40 +800,31 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
773
800
|
return `<div class="bubble output report-reply${renderedMarkdown ? " markdown" : ""}"${copySourceAttr(items[j], renderedMarkdown)}>${body}</div>`;
|
|
774
801
|
};
|
|
775
802
|
const steerHtml = (i, bubble) => steerRowHtml({ source: items[i].source, text: items[i].content }, { bubble });
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
if (group.kind === "
|
|
779
|
-
if (group.kind === "
|
|
803
|
+
const parts = [];
|
|
804
|
+
for (const group of turnGroups(items, timing)) {
|
|
805
|
+
if (group.kind === "note") { parts.push(`<div class="bubble note">${noteHtml(items[group.i])}</div>`); continue; }
|
|
806
|
+
if (group.kind === "steer") { parts.push(steerHtml(group.i, true)); continue; }
|
|
807
|
+
if (group.kind === "answer") { parts.push(answerHtml(items[group.answer], "")); continue; }
|
|
780
808
|
const prompt = items[group.user];
|
|
781
809
|
// A context wake turn starts with its note.
|
|
782
|
-
|
|
783
|
-
? `<div class="bubble note">${noteHtml(prompt)}</div>`
|
|
784
|
-
: userHtml(prompt)];
|
|
810
|
+
parts.push(prompt.role === "note" ? `<div class="bubble note">${noteHtml(prompt)}</div>` : userHtml(prompt));
|
|
785
811
|
const [only] = group.steps;
|
|
786
812
|
const ended = group.answer !== null || group.emptyAnswer != null;
|
|
787
813
|
const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
|
|
788
814
|
// A lone thinking step before an answer (or the notice of a turn with
|
|
789
|
-
// none): no block,
|
|
815
|
+
// none): no block, the plain answer's collapsed thinking block.
|
|
790
816
|
const plainThinking = group.steps.length === 1 && ended && textless && !only.tools.length && only.thinking;
|
|
791
817
|
const noBlock = !group.steps.length || plainThinking;
|
|
792
818
|
if (plainThinking) {
|
|
793
|
-
|
|
794
|
-
parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
|
|
819
|
+
parts.push(thinkingTemplate({ body: only.thinking, cls: "bubble thinking" }));
|
|
795
820
|
} else if (group.steps.length) {
|
|
796
821
|
const gens = group.steps.map((step) => ({ text: step.i === null ? "" : String(items[step.i].content ?? ""), thinking: step.thinking || "", tools: step.tools }));
|
|
797
|
-
const
|
|
798
|
-
// A thinking-only step: the body directly under the step's own
|
|
799
|
-
// "thinking" summary (no wrap that would repeat it), as live.
|
|
800
|
-
const body = gen.thinking ? `<div class="thinking-body">${escapeHtml(gen.thinking)}</div>` : "";
|
|
801
|
-
const thinking = body && (gen.text || gen.tools.length) ? `<details class="thinking"><summary>thinking</summary>${body}</details>` : body;
|
|
802
|
-
const text = gen.text ? `<div class="gen-text">${escapeHtml(gen.text)}</div>` : "";
|
|
822
|
+
const steps = gens.map((gen, k) => stepTemplate(gen, {
|
|
803
823
|
// The steers the step answered first, where the live rows went.
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
});
|
|
809
|
-
parts.push(`<details class="turn-work done"><summary>${escapeHtml(model.blockSummary({ gens, ended: true }))}</summary>${rows.join("")}</details>`);
|
|
824
|
+
steers: (group.steps[k].steers || []).map((i) => rawNodes(steerHtml(i, false))),
|
|
825
|
+
items: gen.tools.length ? [rawNodes(gen.tools.map((r) => toolRowHtml(r, { thumbs })).join(""))] : [],
|
|
826
|
+
}));
|
|
827
|
+
parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
|
|
810
828
|
}
|
|
811
829
|
// Steers with no step to sit in, or in a turn that shows no block: on
|
|
812
830
|
// their own after the prompt.
|
|
@@ -831,6 +849,6 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
831
849
|
// stayed with its failure line, as the live view left them.
|
|
832
850
|
parts.push(cancelLineHtml(group.record, escapeHtml));
|
|
833
851
|
parts.push(failedLineHtml(group.record));
|
|
834
|
-
|
|
835
|
-
|
|
852
|
+
}
|
|
853
|
+
return fragmentOf(parts);
|
|
836
854
|
}
|