samagotchi 0.29.0 → 0.31.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 +99 -1
- data/docs/cli.md +4 -4
- data/docs/configuration.md +4 -0
- data/docs/hooks.md +1 -1
- data/docs/internals/engine.md +1 -1
- data/docs/plugins.md +1 -1
- data/docs/sessions.md +11 -4
- data/lib/samagotchi/bridge/card_store.rb +1 -1
- data/lib/samagotchi/bridge/turn_notice.rb +1 -1
- data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
- data/lib/samagotchi/bundles/mcp/plugin.rb +4 -3
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +17 -4
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/delegated.md +1 -0
- data/lib/samagotchi/bundles/system/manifest.yml +3 -3
- data/lib/samagotchi/bundles/system/self_map.md +2 -1
- data/lib/samagotchi/child_reports.rb +214 -0
- data/lib/samagotchi/client.rb +35 -4
- data/lib/samagotchi/command_steps.rb +35 -15
- data/lib/samagotchi/config.rb +5 -0
- data/lib/samagotchi/engine.rb +2 -0
- data/lib/samagotchi/host_registry.rb +4 -0
- data/lib/samagotchi/idle_recap.rb +4 -0
- data/lib/samagotchi/kernel_loop.rb +63 -11
- data/lib/samagotchi/llm/chat_loop.rb +14 -7
- data/lib/samagotchi/llm/errors.rb +9 -0
- data/lib/samagotchi/log_subscriber.rb +1 -1
- data/lib/samagotchi/model_list_store.rb +117 -0
- data/lib/samagotchi/model_profile.rb +43 -0
- data/lib/samagotchi/prompt_warmup.rb +30 -5
- data/lib/samagotchi/reply_wait.rb +39 -8
- data/lib/samagotchi/send_command.rb +5 -3
- data/lib/samagotchi/session.rb +31 -5
- data/lib/samagotchi/session_inbox.rb +34 -1
- data/lib/samagotchi/session_manager.rb +41 -5
- data/lib/samagotchi/session_metrics.rb +6 -3
- data/lib/samagotchi/steer.rb +39 -7
- data/lib/samagotchi/terminal_ui/attached_loop.rb +4 -2
- data/lib/samagotchi/terminal_ui/event_renderer.rb +3 -1
- data/lib/samagotchi/terminal_ui/formatting.rb +38 -8
- data/lib/samagotchi/terminal_ui.rb +44 -5
- data/lib/samagotchi/tool_activity.rb +6 -2
- data/lib/samagotchi/tool_call_parser.rb +40 -0
- data/lib/samagotchi/tool_declarations.rb +7 -7
- data/lib/samagotchi/tools/delegate.rb +26 -13
- data/lib/samagotchi/tools/delegate_cursor.rb +91 -0
- data/lib/samagotchi/tools/delegate_wait.rb +50 -31
- data/lib/samagotchi/tools/execute.rb +3 -1
- data/lib/samagotchi/tools/task_runtime.rb +21 -2
- data/lib/samagotchi/tools/task_wait.rb +2 -2
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +8 -2
- data/lib/samagotchi/web/public/activity.js +2 -1
- data/lib/samagotchi/web/public/app.js +21 -11
- data/lib/samagotchi/web/public/command_view.js +19 -5
- data/lib/samagotchi/web/public/format.js +8 -1
- data/lib/samagotchi/web/public/index.html +14 -6
- data/lib/samagotchi/web/public/notify.js +4 -2
- data/lib/samagotchi/web/public/stage_model.js +18 -0
- data/lib/samagotchi/web/public/stage_view.js +16 -11
- data/lib/samagotchi/web/public/task_stop.js +11 -2
- data/lib/samagotchi/web/public/timing.js +12 -7
- data/lib/samagotchi/web/public/turn_events.js +29 -9
- data/lib/samagotchi/web/public/turn_model.js +16 -11
- data/lib/samagotchi/web/public/turn_view.js +49 -7
- data/lib/samagotchi/worker.rb +240 -23
- metadata +4 -1
|
@@ -19,17 +19,26 @@ export function stopTarget(row, features) {
|
|
|
19
19
|
return row.view?.task_id || null;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
// A task_wait row's title without its wait ("npm test · up to 600s").
|
|
23
|
+
const WAIT_SUFFIX = / · up to \d+s$/;
|
|
24
|
+
|
|
22
25
|
// The command a task_create of this turn started +taskId+ with (its row's
|
|
23
|
-
// view),
|
|
26
|
+
// view), else a task_wait row's title for it (the server titles one by its
|
|
27
|
+
// task's command, also for a task from an earlier turn: ToolActivity.
|
|
28
|
+
// task_title), or null.
|
|
24
29
|
export function taskCommand(gens, taskId) {
|
|
30
|
+
let titled = null;
|
|
25
31
|
for (const gen of gens || []) {
|
|
26
32
|
for (const row of gen.tools || []) {
|
|
33
|
+
if (row.tool === "task_wait" && row.view?.task_id === taskId && row.title) {
|
|
34
|
+
titled ||= row.title.replace(WAIT_SUFFIX, "");
|
|
35
|
+
}
|
|
27
36
|
if (row.tool !== "task_create") continue;
|
|
28
37
|
const id = /(?:^|\n)task_id: (\S+)/.exec(row.output || "")?.[1];
|
|
29
38
|
if (id === taskId) return row.view?.command || null;
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
|
-
return
|
|
41
|
+
return titled;
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
export function confirmText(taskId, command) {
|
|
@@ -24,20 +24,25 @@ export function turnTimingText(number, durationMs, { running = false, canceled =
|
|
|
24
24
|
// CANCEL_REASONS); anything else as it is. Both sides: spec/shared/labels_matrix.json.
|
|
25
25
|
const CANCEL_REASONS = { ctrl_c: "Ctrl-C", user: "stopped", hook: "by a hook" };
|
|
26
26
|
|
|
27
|
-
// The line a canceled turn ends with: "
|
|
27
|
+
// The line a canceled turn ends with: "■ canceled (stopped)", live from
|
|
28
28
|
// turn_canceled's reason and after a re-render from the turn record's
|
|
29
29
|
// cancellation_reason (records saved before it existed: no reason). A
|
|
30
|
-
// hook's stop that names who (+by+, cancelled_by): "
|
|
30
|
+
// hook's stop that names who (+by+, cancelled_by): "■ stopped by loop-guard".
|
|
31
|
+
// Every cancel is a stop someone chose (the user, Ctrl-C, a hook), not a
|
|
32
|
+
// failure: the ■ mark and the stopped colours of a call the Stop cut
|
|
33
|
+
// (STOPPED_LINE_CLASS), not the ✕ of an error.
|
|
34
|
+
export const STOP_MARK = "\u25A0";
|
|
35
|
+
export const STOPPED_LINE_CLASS = "bubble cancel stopped";
|
|
31
36
|
export function cancelLineText(reason, by) {
|
|
32
|
-
if (reason === "hook" && by) return
|
|
33
|
-
return
|
|
37
|
+
if (reason === "hook" && by) return `${STOP_MARK} stopped by ${by}`;
|
|
38
|
+
return `${STOP_MARK} canceled` + (reason ? ` (${CANCEL_REASONS[reason] || reason})` : "");
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
// A re-rendered turn's cancel line (a .bubble.cancel), or "" when
|
|
37
|
-
// record is not a canceled one.
|
|
41
|
+
// A re-rendered turn's cancel line (a .bubble.cancel.stopped), or "" when
|
|
42
|
+
// the turn record is not a canceled one.
|
|
38
43
|
export function cancelLineHtml(record, escape) {
|
|
39
44
|
if (record?.status !== "canceled") return "";
|
|
40
|
-
return `<div class="
|
|
45
|
+
return `<div class="${STOPPED_LINE_CLASS}">${escape(cancelLineText(record.cancellation_reason, record.cancelled_by))}</div>`;
|
|
41
46
|
}
|
|
42
47
|
|
|
43
48
|
export function elapsedSince(startedAt, now = Date.now()) {
|
|
@@ -14,10 +14,12 @@ export function turnOutput(data) {
|
|
|
14
14
|
|
|
15
15
|
// The end line for a turn whose worker went away mid-turn (the footer's
|
|
16
16
|
// stop, a crash): no turn_canceled comes, so the page ends the turn itself.
|
|
17
|
+
// The footer's stop is the user's, drawn as a cancel is (\u25A0, timing.js
|
|
18
|
+
// cancelLineText); a worker that exited on its own is a failure (\u2715).
|
|
17
19
|
// null when no turn was running.
|
|
18
20
|
export function workerGoneText({ turnRunning, stopped = false }) {
|
|
19
21
|
if (!turnRunning) return null;
|
|
20
|
-
return
|
|
22
|
+
return stopped ? "\u25A0 canceled (session stopped)" : "\u2715 canceled (worker exited)";
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
// How long a turn's answer waits for the after_turn hooks' answer_display
|
|
@@ -91,7 +93,13 @@ export function isOwn(clientId, myId) {
|
|
|
91
93
|
|
|
92
94
|
// As the TUI labels them (Formatting::CLIENT_LABELS); this file's label
|
|
93
95
|
// helpers and the TUI's agree per spec/shared/labels_matrix.json.
|
|
94
|
-
const CLIENT_LABELS = Object.freeze({ tui: "tui", web: "web", system: "reminder", delegate: "delegate" });
|
|
96
|
+
const CLIENT_LABELS = Object.freeze({ tui: "tui", web: "web", system: "reminder", delegate: "delegate", child: "delegate report" });
|
|
97
|
+
|
|
98
|
+
// A delegate child's report comes in as client child:<id8> (ChildReports).
|
|
99
|
+
const REPORT_CLIENT = "child:";
|
|
100
|
+
export function isReportClient(clientId) {
|
|
101
|
+
return String(clientId || "").startsWith(REPORT_CLIENT);
|
|
102
|
+
}
|
|
95
103
|
|
|
96
104
|
// The label on another client's prompt, by its client_id prefix.
|
|
97
105
|
export function clientLabel(clientId) {
|
|
@@ -117,9 +125,10 @@ export function hookNoticeLabel(hook) {
|
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
// The row a step gets when the loop asks again after an empty answer, or
|
|
120
|
-
// after a plugin cut the generation (stopped_by: the bundle)
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
// after a plugin cut the generation (stopped_by: the bundle), or after a
|
|
129
|
+
// corrupt native generation (malformed).
|
|
130
|
+
export function emptyRetryLine({ attempt, of, stopped_by: stoppedBy, malformed } = {}) {
|
|
131
|
+
const what = stoppedBy ? `cut by ${stoppedBy}` : malformed ? "malformed answer" : "empty answer";
|
|
123
132
|
return `↻ ${what}, asking again (${attempt}/${of})`;
|
|
124
133
|
}
|
|
125
134
|
|
|
@@ -379,9 +388,14 @@ export function composerAfterAck({ sent = "", current = "", refill = "" } = {})
|
|
|
379
388
|
// tag — this tab's own local echo gets its enqueued_id
|
|
380
389
|
// start — the bubble's turn started (drops the "queued" badge)
|
|
381
390
|
// steer — the bubble was merged into the running turn
|
|
391
|
+
// report — the merge brought a delegate child's report (origin
|
|
392
|
+
// child:<id8>, no bubble anywhere): its text gets a labelled one
|
|
382
393
|
// `known(enqueuedId)` says whether a bubble carries that id already;
|
|
383
|
-
// `unmatchedMerge` whether the last input_merged had an origin with no
|
|
384
|
-
|
|
394
|
+
// `unmatchedMerge` whether the last input_merged had an origin with no
|
|
395
|
+
// bubble ("report": a delegate report's); `wakeStart` whether this is the
|
|
396
|
+
// first merge of a turn chi ran for reports (its bubble starts the turn,
|
|
397
|
+
// no "steered" badge, as a reload shows it).
|
|
398
|
+
export function promptOps(event, { myId, known, unmatchedMerge = false, wakeStart = false }) {
|
|
385
399
|
if (!event) return [];
|
|
386
400
|
switch (event.type) {
|
|
387
401
|
case "turn_enqueued": {
|
|
@@ -401,12 +415,18 @@ export function promptOps(event, { myId, known, unmatchedMerge = false }) {
|
|
|
401
415
|
}
|
|
402
416
|
return [withImages({ op: "add", enqueuedId: id, prompt: event.prompt, state: "started", label: clientLabel(origin.client_id) }, event.images)];
|
|
403
417
|
}
|
|
404
|
-
case "input_merged":
|
|
405
|
-
|
|
418
|
+
case "input_merged": {
|
|
419
|
+
const ops = (event.origins || []).filter((o) => o && o.enqueued_id).map((o) =>
|
|
406
420
|
known(o.enqueued_id) ? { op: "steer", enqueuedId: o.enqueued_id } : { op: "steer", enqueuedId: o.enqueued_id, unmatched: true },
|
|
407
421
|
);
|
|
422
|
+
if ((event.origins || []).some((o) => o && !o.enqueued_id && isReportClient(o.client_id))) ops.push({ op: "report" });
|
|
423
|
+
return ops;
|
|
424
|
+
}
|
|
408
425
|
case "pending_input_merged":
|
|
409
426
|
if (!unmatchedMerge || !event.content) return [];
|
|
427
|
+
if (unmatchedMerge === "report") {
|
|
428
|
+
return [{ op: "add", enqueuedId: null, prompt: event.content, state: wakeStart ? null : "steered", label: clientLabel(REPORT_CLIENT) }];
|
|
429
|
+
}
|
|
410
430
|
return [{ op: "add", enqueuedId: null, prompt: event.content, state: "steered", label: null }];
|
|
411
431
|
default:
|
|
412
432
|
return [];
|
|
@@ -176,21 +176,26 @@ export function failedCount(rows) {
|
|
|
176
176
|
// call's title ("edit lib/a.rb"), else the tools it worked with, else
|
|
177
177
|
// "thinking"; then its call count and how many failed.
|
|
178
178
|
export function genLabel(gen) {
|
|
179
|
+
const { head, calls } = genLabelParts(gen);
|
|
180
|
+
return calls ? `${head} · ${calls}` : head;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// genLabel in its parts: +head+, +calls+ ("3 tool calls (1 failed)", or ""
|
|
184
|
+
// without calls) and +repeats+: the head is words the open step's body
|
|
185
|
+
// starts with (its narration, its first call's title), so an open step's
|
|
186
|
+
// summary can say only its calls (turn_view.js labelHtml).
|
|
187
|
+
export function genLabelParts(gen) {
|
|
179
188
|
const line = String(gen.text || "").trim().split("\n")[0].trim();
|
|
180
|
-
const calls = gen.tools.length;
|
|
189
|
+
const calls = gen.tools.length ? toolCallsText(gen.tools) : "";
|
|
181
190
|
const first = gen.tools[0];
|
|
182
|
-
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
} else if (first?.title) {
|
|
186
|
-
head = cut(`${toolName(first)} ${first.title}`);
|
|
187
|
-
} else if (calls) {
|
|
191
|
+
if (line) return { head: cut(line), calls, repeats: true };
|
|
192
|
+
if (first?.title) return { head: cut(`${toolName(first)} ${first.title}`), calls, repeats: true };
|
|
193
|
+
if (gen.tools.length) {
|
|
188
194
|
const names = [...new Set(gen.tools.map(toolName))];
|
|
189
|
-
head = `working with ${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
|
|
190
|
-
|
|
191
|
-
return "thinking";
|
|
195
|
+
const head = `working with ${names.slice(0, LABEL_TOOLS).join(", ")}${names.length > LABEL_TOOLS ? ", …" : ""}`;
|
|
196
|
+
return { head, calls, repeats: false };
|
|
192
197
|
}
|
|
193
|
-
return
|
|
198
|
+
return { head: "thinking", calls: "", repeats: false };
|
|
194
199
|
}
|
|
195
200
|
|
|
196
201
|
// The block's summary line: "working · 3 steps · 8 tool calls · execute ×7 · …"
|
|
@@ -14,7 +14,7 @@ import { leavesBlock } from "./card.js";
|
|
|
14
14
|
import { copySourceAttr } from "./copy.js";
|
|
15
15
|
import { commandBlockHtml, rowHover } from "./command_view.js";
|
|
16
16
|
import { activityDiffHtml } from "./diff_view.js";
|
|
17
|
-
import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
|
|
17
|
+
import { escapeHtml, messageBodyHtml, noteHtml, normalize, sourceLabel, steerRowHtml } from "./format.js";
|
|
18
18
|
import { thumbsHtml } from "./images.js";
|
|
19
19
|
import { shouldFollowScroll } from "./scroll.js";
|
|
20
20
|
import { emptyAnswerLine, hookNoticeLabel } from "./turn_events.js";
|
|
@@ -399,7 +399,7 @@ export function createTurnView({
|
|
|
399
399
|
// an ellipsed label): at close, and again when a late tool result lands
|
|
400
400
|
// in a step that already closed.
|
|
401
401
|
function writeClosedLabel(entry, gen) {
|
|
402
|
-
entry.summary
|
|
402
|
+
setLabel(entry.summary, gen);
|
|
403
403
|
entry.el.classList.toggle("has-failed", model.failedCount(gen.tools) > 0);
|
|
404
404
|
}
|
|
405
405
|
|
|
@@ -551,7 +551,7 @@ export function createTurnView({
|
|
|
551
551
|
}
|
|
552
552
|
entry.text = document.createElement("div");
|
|
553
553
|
entry.text.className = "gen-text";
|
|
554
|
-
entry.summary
|
|
554
|
+
setLabel(entry.summary, { ...gen, text: "" });
|
|
555
555
|
model.takeAnswer(turn, gen);
|
|
556
556
|
// Cards and hook notices in a step that goes (below, or with the whole
|
|
557
557
|
// block for a plain answer) stay: before the answer, out of the block,
|
|
@@ -731,6 +731,29 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
731
731
|
};
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
// A closed step's summary (model.genLabel). When its head is the words the
|
|
735
|
+
// body starts with (the narration's first line, the first call's title)
|
|
736
|
+
// and it has calls, the summary is marked .repeats and its parts are
|
|
737
|
+
// spans: an open step shows only its calls (index.html), so it doesn't say
|
|
738
|
+
// the same words twice. Its text stays genLabel's either way.
|
|
739
|
+
// { repeats, html } (the summary's inside).
|
|
740
|
+
function labelParts(gen) {
|
|
741
|
+
const { head, calls, repeats } = model.genLabelParts(gen);
|
|
742
|
+
if (!repeats || !calls) return { repeats: false, html: escapeHtml(model.genLabel(gen)) };
|
|
743
|
+
return { repeats: true, html: `<span class="gen-head">${escapeHtml(head)}</span><span class="gen-sep"> · </span><span class="gen-calls">${escapeHtml(calls)}</span>` };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function labelHtml(gen) {
|
|
747
|
+
const { repeats, html } = labelParts(gen);
|
|
748
|
+
return `<summary${repeats ? ' class="repeats"' : ""}>${html}</summary>`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function setLabel(summary, gen) {
|
|
752
|
+
const { repeats, html } = labelParts(gen);
|
|
753
|
+
summary.classList.toggle("repeats", repeats);
|
|
754
|
+
summary.innerHTML = html;
|
|
755
|
+
}
|
|
756
|
+
|
|
734
757
|
// A reloaded tool row, the live row's markup (fillActivityRow): status, tool, params, the full command (an
|
|
735
758
|
// execute/task_create's view), output (its "[tool]" tag dropped, 300 chars, the rest on hover), plus the call's
|
|
736
759
|
// duration from its record.
|
|
@@ -782,13 +805,28 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
782
805
|
const { body, renderedMarkdown } = messageBodyHtml(m);
|
|
783
806
|
return `<div class="bubble output${renderedMarkdown ? " markdown" : ""}"${copySourceAttr(m, renderedMarkdown)}>${body}</div>${timing}`;
|
|
784
807
|
};
|
|
808
|
+
// The live bubble's origin label (app.js makeUserBubble), for a line
|
|
809
|
+
// chi brought in: a delegate report.
|
|
810
|
+
const originLabelHtml = (m) => {
|
|
811
|
+
const label = sourceLabel(m?.source);
|
|
812
|
+
return label ? `<span class="origin-label">${escapeHtml(label)}</span>` : "";
|
|
813
|
+
};
|
|
814
|
+
// The first text after a delegate report merged mid-turn, unless it is
|
|
815
|
+
// the turn's answer (shown anyway).
|
|
816
|
+
const reportReplyHtml = (i, group) => {
|
|
817
|
+
if (items[i]?.source !== "delegate_report") return "";
|
|
818
|
+
const j = items.findIndex((m, k) => k > i && m?.role === "assistant" && String(m.content ?? "").trim());
|
|
819
|
+
if (j < 0 || j === group.answer || items.slice(i + 1, j).some((m) => m?.role === "user" && !m.merged)) return "";
|
|
820
|
+
const { body, renderedMarkdown } = messageBodyHtml(items[j]);
|
|
821
|
+
return `<div class="bubble output report-reply${renderedMarkdown ? " markdown" : ""}"${copySourceAttr(items[j], renderedMarkdown)}>${body}</div>`;
|
|
822
|
+
};
|
|
785
823
|
const steerHtml = (i, bubble) => steerRowHtml({ source: items[i].source, text: items[i].content }, { bubble });
|
|
786
824
|
return turnGroups(items, timing).map((group) => {
|
|
787
825
|
if (group.kind === "note") return `<div class="bubble note">${noteHtml(items[group.i])}</div>`;
|
|
788
826
|
if (group.kind === "steer") return steerHtml(group.i, true);
|
|
789
827
|
if (group.kind === "answer") return answerHtml(items[group.answer], "");
|
|
790
828
|
const prompt = items[group.user];
|
|
791
|
-
const parts = [`<div class="bubble user"${copySourceAttr(prompt, false)}
|
|
829
|
+
const parts = [`<div class="bubble user"${copySourceAttr(prompt, false)}>${originLabelHtml(prompt)}<div class="user-message">${messageBodyHtml(prompt).body}${thumbs(prompt.images)}</div></div>`];
|
|
792
830
|
const [only] = group.steps;
|
|
793
831
|
const ended = group.answer !== null || group.emptyAnswer != null;
|
|
794
832
|
const textless = only && (only.i === null || !String(items[only.i].content ?? ""));
|
|
@@ -811,7 +849,7 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
811
849
|
const steers = (group.steps[k].steers || []).map((i) => steerHtml(i, false)).join("");
|
|
812
850
|
const tools = gen.tools.length ? `<div class="activity-body">${gen.tools.map((r) => reloadRowHtml(r, thumbs)).join("")}</div>` : "";
|
|
813
851
|
const failed = model.failedCount(gen.tools) ? " has-failed" : "";
|
|
814
|
-
return `<details class="gen${failed}"
|
|
852
|
+
return `<details class="gen${failed}">${labelHtml(gen)}${steers}${thinking}${text}${tools}</details>`;
|
|
815
853
|
});
|
|
816
854
|
parts.push(`<details class="turn-work done"><summary>${escapeHtml(model.blockSummary({ gens, ended: true }))}</summary>${rows.join("")}</details>`);
|
|
817
855
|
}
|
|
@@ -820,9 +858,13 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
|
|
|
820
858
|
const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
|
|
821
859
|
parts.push(loose.map((i) => steerHtml(i, true)).join(""));
|
|
822
860
|
// The user's lines merged into the turn: the live "steered" bubbles,
|
|
823
|
-
// after its steps and before its answer.
|
|
861
|
+
// after its steps and before its answer. A delegate report's is
|
|
862
|
+
// followed by the text that answered it when that isn't the answer: a
|
|
863
|
+
// small model tells its user about a report before its next tool call,
|
|
864
|
+
// and that step's text would stay folded in the block.
|
|
824
865
|
parts.push((group.merged || []).map((i) =>
|
|
825
|
-
`<div class="bubble user steered" data-user-state="steered"${items[i].step ? ` data-step="${Number(items[i].step)}"` : ""}${copySourceAttr(items[i], false)}
|
|
866
|
+
`<div class="bubble user steered" data-user-state="steered"${items[i].step ? ` data-step="${Number(items[i].step)}"` : ""}${copySourceAttr(items[i], false)}>${originLabelHtml(items[i])}<div class="user-message">${messageBodyHtml(items[i]).body}${thumbs(items[i].images)}</div><span class="state-badge">steered</span></div>` +
|
|
867
|
+
reportReplyHtml(i, group)).join(""));
|
|
826
868
|
// A turn that ended with no answer: chi's muted notice where the answer
|
|
827
869
|
// would be, before the timing, as live.
|
|
828
870
|
if (group.emptyAnswer != null) parts.push(`${emptyAnswerHtml(items[group.emptyAnswer])}${timingHtml(group)}`);
|