samagotchi 0.61.0 → 0.63.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 +91 -1
- data/README.md +4 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +3 -0
- data/docs/cli.md +26 -9
- data/docs/configuration.md +33 -20
- data/docs/context.md +21 -3
- data/docs/guardrails.md +2 -1
- data/docs/hooks.md +14 -3
- data/docs/internals/engine.md +24 -11
- data/docs/internals/prompt-caching.md +6 -1
- data/docs/memory.md +1 -1
- data/docs/plugins.md +131 -5
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/docs/sub-agent.md +6 -0
- data/docs/testing.md +10 -4
- data/lib/samagotchi/answer_refs.rb +18 -26
- data/lib/samagotchi/bridge.rb +23 -1
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +22 -0
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +406 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/chi-peer-SKILL.md +104 -0
- data/lib/samagotchi/bundles/claude-peer/scripts/pair +66 -0
- data/lib/samagotchi/bundles/claude-peer/skill_claude_peer.md +60 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
- data/lib/samagotchi/bundles/github-pr/plugin.rb +70 -14
- data/lib/samagotchi/bundles/loop-guard/manifest.yml +2 -2
- data/lib/samagotchi/bundles/loop-guard/plugin.rb +17 -6
- data/lib/samagotchi/bundles/slow-commands/manifest.yml +2 -2
- data/lib/samagotchi/bundles/slow-commands/plugin.rb +5 -1
- data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +58 -3
- data/lib/samagotchi/bundles/source-links/manifest.yml +4 -4
- data/lib/samagotchi/bundles/source-links/source_links.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/memory_guide.md +1 -1
- data/lib/samagotchi/context_absorber.rb +25 -6
- data/lib/samagotchi/engine.rb +9 -0
- data/lib/samagotchi/engine_peers.rb +1 -0
- data/lib/samagotchi/idle_recap.rb +2 -3
- data/lib/samagotchi/image_store.rb +8 -0
- data/lib/samagotchi/kernel_loop.rb +7 -16
- data/lib/samagotchi/llm/chat_loop.rb +23 -15
- data/lib/samagotchi/plugin/api.rb +36 -18
- data/lib/samagotchi/plugin/attached_context.rb +7 -4
- data/lib/samagotchi/reasoning_history.rb +22 -4
- data/lib/samagotchi/recap_setup.rb +3 -3
- data/lib/samagotchi/row_refs.rb +125 -0
- data/lib/samagotchi/session.rb +12 -0
- data/lib/samagotchi/session_metrics.rb +24 -2
- data/lib/samagotchi/terminal_ui/attach_launcher.rb +6 -3
- data/lib/samagotchi/terminal_ui/attached_loop.rb +26 -9
- data/lib/samagotchi/terminal_ui/formatting.rb +1 -1
- data/lib/samagotchi/terminal_ui.rb +57 -14
- data/lib/samagotchi/tool_activity.rb +8 -4
- data/lib/samagotchi/tool_response.rb +5 -4
- data/lib/samagotchi/tool_row_fields.rb +4 -2
- data/lib/samagotchi/tool_runner.rb +16 -3
- data/lib/samagotchi/tools/builtins.rb +9 -2
- data/lib/samagotchi/tools/execute.rb +37 -7
- data/lib/samagotchi/tools/peers.rb +3 -2
- data/lib/samagotchi/tools/registry.rb +5 -3
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +20 -5
- data/lib/samagotchi/web/message_parts.rb +17 -9
- data/lib/samagotchi/web/public/activity.js +6 -3
- data/lib/samagotchi/web/public/app.js +106 -106
- data/lib/samagotchi/web/public/card.js +73 -23
- data/lib/samagotchi/web/public/chains.js +24 -24
- data/lib/samagotchi/web/public/command_view.js +39 -35
- data/lib/samagotchi/web/public/context_chips.js +92 -64
- data/lib/samagotchi/web/public/copy.js +7 -15
- data/lib/samagotchi/web/public/diff_view.js +20 -23
- data/lib/samagotchi/web/public/families_view.js +51 -39
- data/lib/samagotchi/web/public/format.js +0 -48
- data/lib/samagotchi/web/public/images.js +12 -16
- data/lib/samagotchi/web/public/index.html +61 -10
- data/lib/samagotchi/web/public/keep_focus.js +24 -0
- data/lib/samagotchi/web/public/lists_view.js +13 -13
- data/lib/samagotchi/web/public/llm_context_chip.js +55 -34
- data/lib/samagotchi/web/public/question_card.js +11 -0
- data/lib/samagotchi/web/public/question_cards.js +63 -7
- data/lib/samagotchi/web/public/read_view.js +10 -9
- data/lib/samagotchi/web/public/ref_menu.js +18 -5
- data/lib/samagotchi/web/public/refs.js +113 -15
- data/lib/samagotchi/web/public/scope.js +23 -0
- data/lib/samagotchi/web/public/scroll.js +42 -0
- data/lib/samagotchi/web/public/stage_model.js +22 -3
- data/lib/samagotchi/web/public/stage_view.js +299 -70
- data/lib/samagotchi/web/public/timing.js +1 -8
- data/lib/samagotchi/web/public/turn_html.js +122 -61
- data/lib/samagotchi/web/public/turn_model.js +49 -0
- data/lib/samagotchi/web/public/turn_view.js +167 -116
- data/lib/samagotchi/web/public/vendor/directive.js +8 -0
- data/lib/samagotchi/web/public/vendor/directives/unsafe-html.js +8 -0
- data/lib/samagotchi/worker.rb +49 -32
- data/lib/samagotchi/worker_inbound.rb +17 -14
- metadata +10 -1
|
@@ -1,15 +1,17 @@
|
|
|
1
|
-
// The turn's pieces as
|
|
2
|
-
// a tool row, a prompt bubble
|
|
3
|
-
//
|
|
4
|
-
// (
|
|
1
|
+
// The turn's pieces as lit-html templates, one for the live page and a
|
|
2
|
+
// reload: a tool row, a prompt bubble, a note, a steer, the notice of a
|
|
3
|
+
// turn with no answer. The live page renders the same template a reload
|
|
4
|
+
// does (elementOf, or its own lit root for a tool row), so the two can't
|
|
5
|
+
// drift apart. `node --test` covers it (on the happy-dom shim).
|
|
5
6
|
import { toolName } from "./activity.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
7
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
8
|
+
import { commandBlockTemplate, rowHover } from "./command_view.js";
|
|
9
|
+
import { copySource } from "./copy.js";
|
|
10
|
+
import { activityDiffTemplate } from "./diff_view.js";
|
|
11
|
+
import { steerSender } from "./format.js";
|
|
12
|
+
import { rowTitleRef } from "./refs.js";
|
|
13
|
+
import { readLinesTemplate } from "./read_view.js";
|
|
14
|
+
import { STOPPED_LINE_CLASS, cancelLineText, formatDuration } from "./timing.js";
|
|
13
15
|
import { emptyAnswerLine } from "./turn_events.js";
|
|
14
16
|
|
|
15
17
|
// A row's status (live, or a saved tool record's on reload): running,
|
|
@@ -31,12 +33,14 @@ export function statusParts(status, { noMatch = false } = {}) {
|
|
|
31
33
|
return { cls, label, word: cls === "ok" || cls === "running" ? "" : label };
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
// role is bound too: lit sets bound attributes after the static ones, and
|
|
37
|
+
// the markup keeps the order a reload wrote before.
|
|
38
|
+
function statusMarkTemplate({ cls, label }) {
|
|
39
|
+
return html`<span class="activity-status ${cls}" role=${"img"} aria-label=${label} title=${label}></span>`;
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
function
|
|
39
|
-
return word ? `<span class="activity-state ${wordCls || cls}">${word}</span>` :
|
|
42
|
+
function statusWordTemplate({ cls, word, wordCls }) {
|
|
43
|
+
return word ? html`<span class="activity-state ${wordCls || cls}">${word}</span>` : nothing;
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
// A reloaded row's ✂ mark: what an LLM context edit saved on its output
|
|
@@ -62,11 +66,11 @@ function compactTokens(n) {
|
|
|
62
66
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}k tokens` : `${n} tokens`;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
|
-
function
|
|
69
|
+
function llmEditTemplate(row) {
|
|
66
70
|
const line = llmEditLine(row.edit);
|
|
67
|
-
if (!line) return
|
|
71
|
+
if (!line) return nothing;
|
|
68
72
|
const hover = `${row.tool_id ? `${row.tool_id}: ` : ""}${line}\nThe model is sent this instead of the output; the session keeps it.`;
|
|
69
|
-
return `<div class="activity-edit" title
|
|
73
|
+
return html`<div class="activity-edit" title=${hover}>${line}</div>`;
|
|
70
74
|
}
|
|
71
75
|
|
|
72
76
|
// A tool row: its status mark, tool, title (else params; the full command,
|
|
@@ -76,30 +80,35 @@ function llmEditHtml(row) {
|
|
|
76
80
|
// the rest on hover; a read of the file as-is also its numbered lines,
|
|
77
81
|
// closed: read_view.js), its images (+thumbs+(images) draws them) and an
|
|
78
82
|
// edit's collapsed diff, and a reloaded row's ✂ mark above its output. A
|
|
79
|
-
// part with nothing to say is left out.
|
|
80
|
-
|
|
81
|
-
|
|
83
|
+
// part with nothing to say is left out. A reload renders toolRowTemplate;
|
|
84
|
+
// the live view renders toolRowInnerTemplate into its own row element
|
|
85
|
+
// (turn_view.js), again on each event: lit writes only what changed, so a
|
|
86
|
+
// part the user toggled (the raw command, the diff, the read lines) stays.
|
|
87
|
+
// +stop+: the live row's stop-task button (a node), after the title.
|
|
88
|
+
export function toolRowTemplate(row, opts = {}) {
|
|
89
|
+
return html`<div class="activity-row" data-key=${row.key}>${toolRowInnerTemplate(row, opts)}</div>`;
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
export function
|
|
92
|
+
export function toolRowInnerTemplate(row, { thumbs = () => nothing, stop = nothing } = {}) {
|
|
85
93
|
const shown = row.title || row.params;
|
|
86
|
-
const hover = rowHover(row)
|
|
87
|
-
// A row with a ref (a file tool's file
|
|
88
|
-
// opens it, and its diff lines open it at
|
|
89
|
-
|
|
90
|
-
const
|
|
94
|
+
const hover = rowHover(row);
|
|
95
|
+
// A row with a ref (a file tool's file, else a plugin tool's first ref)
|
|
96
|
+
// carries it on its title, which opens it, and its diff lines open it at
|
|
97
|
+
// their line (refs.js; bindRowRefs binds a plugin's data to the title).
|
|
98
|
+
const ref = shown ? rowTitleRef(row) : null;
|
|
99
|
+
const attr = (value) => (value ? String(value) : nothing);
|
|
100
|
+
const params = shown
|
|
101
|
+
? html`<span class=${`activity-params${ref?.kind ? " ref" : ""}`} title=${hover || nothing} data-ref-kind=${attr(ref?.kind)} data-ref-path=${attr(ref?.kind && ref.path)} data-ref-line=${attr(ref?.kind && ref.line)} data-ref-end-line=${attr(ref?.kind && ref.end_line)}>${shown}</span>`
|
|
102
|
+
: nothing;
|
|
91
103
|
// The name the model called the tool by when it was an alias's, dim.
|
|
92
|
-
const alias = row.called_as ? `<span class="activity-alias">(called as ${
|
|
104
|
+
const alias = row.called_as ? html`<span class="activity-alias">(called as ${row.called_as})</span>` : nothing;
|
|
93
105
|
const duration = formatDuration(row.duration_ms);
|
|
94
106
|
const full = row.status === "running" ? "" : String(row.output || "").replace(/^\[[^\]]*\]\s*/, "");
|
|
95
107
|
const output = full
|
|
96
|
-
? `<div class="activity-output" title
|
|
97
|
-
:
|
|
108
|
+
? html`<div class="activity-output" title=${full}>${full.length > 300 ? `${full.slice(0, 300)}…` : full}</div>`
|
|
109
|
+
: nothing;
|
|
98
110
|
const status = statusParts(row.status, { noMatch: !!row.no_match });
|
|
99
|
-
return `${
|
|
100
|
-
`${duration ? `<span class="activity-duration">${escapeHtml(duration)}</span>` : ""}` +
|
|
101
|
-
`${commandBlockHtml(row.view, row.title)}${llmEditHtml(row)}${output}${output ? readLinesHtml(row) : ""}` +
|
|
102
|
-
`${row.images?.length ? thumbs(row.images) : ""}${row.diff ? activityDiffHtml(row.diff) : ""}`;
|
|
111
|
+
return html`${statusMarkTemplate(status)}<span class="activity-tool">${toolName(row)}</span>${params}${stop}${alias}${statusWordTemplate(status)}${duration ? html`<span class="activity-duration">${duration}</span>` : nothing}${commandBlockTemplate(row.view, row.title)}${llmEditTemplate(row)}${output}${full ? readLinesTemplate(row) : nothing}${row.images?.length ? thumbs(row.images) : nothing}${row.diff ? activityDiffTemplate(row.diff) : nothing}`;
|
|
103
112
|
}
|
|
104
113
|
|
|
105
114
|
// A prompt bubble's states and their badges: queued (waiting for its
|
|
@@ -107,8 +116,8 @@ export function toolRowInnerHtml(row, { thumbs = () => "" } = {}) {
|
|
|
107
116
|
// gave it back). Any other state ("started", none) is a plain bubble.
|
|
108
117
|
export const BADGES = { queued: "queued", steered: "steered", failed: "failed" };
|
|
109
118
|
|
|
110
|
-
export function
|
|
111
|
-
return BADGES[state] ? `<span class="state-badge">${
|
|
119
|
+
export function stateBadgeTemplate(state) {
|
|
120
|
+
return BADGES[state] ? html`<span class="state-badge">${BADGES[state]}</span>` : nothing;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
// A `!cmd`'s saved user message (the worker appends "!(<command>)\n<output>"
|
|
@@ -127,45 +136,97 @@ export function shellCommandMessage(content) {
|
|
|
127
136
|
// A `!cmd`'s history bubble: the same markup the live page draws for a
|
|
128
137
|
// command (app.js's drawCommandBubble): the line, the output in a
|
|
129
138
|
// pre.command-output.
|
|
130
|
-
export function
|
|
131
|
-
const body = text ? `<pre class="command-output">${
|
|
132
|
-
return `<div class="bubble command"><div class="command-line">${
|
|
139
|
+
export function commandBubbleTemplate({ line, text }) {
|
|
140
|
+
const body = text ? html`<pre class="command-output">${text}</pre>` : nothing;
|
|
141
|
+
return html`<div class="bubble command"><div class="command-line">${line}</div>${body}</div>`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// A user message's body: runs of `>` lines become <blockquote>s, the rest
|
|
145
|
+
// stays text. The bubble is pre-wrap, so the blank lines around a quote
|
|
146
|
+
// are dropped (the blockquote's margin spaces it instead).
|
|
147
|
+
export function userBodyTemplate(content) {
|
|
148
|
+
const text = String(content ?? "").replace(/\r\n?/g, "\n");
|
|
149
|
+
const blocks = [];
|
|
150
|
+
for (const line of text.split("\n")) {
|
|
151
|
+
const m = /^ {0,3}> ?(.*)$/.exec(line);
|
|
152
|
+
const kind = m ? "quote" : "text";
|
|
153
|
+
const last = blocks[blocks.length - 1];
|
|
154
|
+
if (last?.kind === kind) last.lines.push(m ? m[1] : line);
|
|
155
|
+
else blocks.push({ kind, lines: [m ? m[1] : line] });
|
|
156
|
+
}
|
|
157
|
+
if (!blocks.some((b) => b.kind === "quote")) return String(content ?? "");
|
|
158
|
+
return blocks.map((b) => {
|
|
159
|
+
if (b.kind === "quote") return html`<blockquote>${b.lines.join("\n")}</blockquote>`;
|
|
160
|
+
const body = b.lines.join("\n").replace(/^\s*\n/, "").replace(/\n\s*$/, "");
|
|
161
|
+
return body.trim() ? body : nothing;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// An answer's bubble (+cls+: a report reply's; +at+: its message's index,
|
|
166
|
+
// data-answer-at): an assistant's rendered markdown (the server's html,
|
|
167
|
+
// sanitized there) set as it is, as the live turn end sets it
|
|
168
|
+
// (answer_refs.js applyFinalAnswer), else its text, set whole (no lit
|
|
169
|
+
// markers inside: the bubble's nodes are the live one's). Its markdown
|
|
170
|
+
// source is kept for the copy button when it shows rendered html.
|
|
171
|
+
export function answerBubbleTemplate(message, { cls = "", at = null } = {}) {
|
|
172
|
+
const rendered = message?.role === "assistant" && typeof message.html === "string";
|
|
173
|
+
const user = !rendered && message?.role === "user";
|
|
174
|
+
const klass = `bubble output${cls}${rendered ? " markdown" : ""}`;
|
|
175
|
+
const answerAt = at ?? nothing;
|
|
176
|
+
const source = rendered || user ? copySource(message) : nothing;
|
|
177
|
+
if (rendered) return html`<div class=${klass} data-answer-at=${answerAt} data-copy-source=${source} .innerHTML=${message.html}></div>`;
|
|
178
|
+
if (user) return html`<div class=${klass} data-answer-at=${answerAt} data-copy-source=${source}>${userBodyTemplate(message.content)}</div>`;
|
|
179
|
+
return html`<div class=${klass} data-answer-at=${answerAt} data-copy-source=${source} .textContent=${String(message?.content ?? "")}></div>`;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// A context note's bubble (background the user or another session pushed
|
|
183
|
+
// in; the model reads it, nobody asked it anything): who sent it, then its
|
|
184
|
+
// text.
|
|
185
|
+
export function noteBubbleTemplate(note) {
|
|
186
|
+
const from = note?.label ? `note from ${note.label}` : "note";
|
|
187
|
+
return html`<div class="bubble note"><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// A plugin's steer (a nudge put into the running turn): one line that
|
|
191
|
+
// names who nudged the model, its text collapsed under it. +bubble+: on its
|
|
192
|
+
// own between bubbles (no step holds it), not a row of a step.
|
|
193
|
+
export function steerRowTemplate({ source, text } = {}, { bubble = false } = {}) {
|
|
194
|
+
const sender = steerSender(source);
|
|
195
|
+
const who = sender ? `${sender} nudged the model` : "nudged the model";
|
|
196
|
+
return html`<details class=${`${bubble ? "bubble " : ""}steer-row`}><summary>${who}</summary><div class="steer-text">${text ?? ""}</div></details>`;
|
|
133
197
|
}
|
|
134
198
|
|
|
135
199
|
// A user's line: who sent it (+label+, a delegate report's or another
|
|
136
200
|
// client's), its body (`>` lines as quotes) and +thumbs+ (its images'
|
|
137
|
-
//
|
|
201
|
+
// template), its state's badge. +message+: { content, display } (the copy
|
|
138
202
|
// source keeps the text as written). +step+: the step that read a line
|
|
139
203
|
// merged mid-turn (a reload's; a resolved question goes before it).
|
|
140
204
|
// The live page's own: +own+ (this tab sent it), +enqueuedId+ and +key+
|
|
141
205
|
// (its normalized text: an echo waiting for its id is matched by it).
|
|
142
|
-
export function
|
|
206
|
+
export function userBubbleTemplate(message, { label = null, state = null, step = null, thumbs = nothing, own = false, enqueuedId = null, key = null } = {}) {
|
|
143
207
|
const named = BADGES[state] ? state : null;
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
named ? `data-user-state="${named}"` : "",
|
|
147
|
-
step ? `data-step="${Number(step)}"` : "",
|
|
148
|
-
].filter(Boolean).join(" ");
|
|
149
|
-
const live = [
|
|
150
|
-
key !== null ? ` data-user-content="${escapeHtml(key)}"` : "",
|
|
151
|
-
own ? ' data-own="1"' : "",
|
|
152
|
-
enqueuedId ? ` data-enqueued-id="${escapeHtml(enqueuedId)}"` : "",
|
|
153
|
-
].join("");
|
|
154
|
-
const from = label ? `<span class="origin-label">${escapeHtml(label)}</span>` : "";
|
|
155
|
-
return `<div ${attrs}${copySourceAttr({ ...message, role: "user" }, false)}${live}>${from}` +
|
|
156
|
-
`<div class="user-message">${userBodyHtml(message?.content)}${thumbs}</div>${stateBadgeHtml(named)}</div>`;
|
|
208
|
+
const from = label ? html`<span class="origin-label">${label}</span>` : nothing;
|
|
209
|
+
return html`<div class=${`bubble user${named ? ` ${named}` : ""}`} data-user-state=${named || nothing} data-step=${step ? Number(step) : nothing} data-copy-source=${copySource({ ...message, role: "user" })} data-user-content=${key !== null ? key : nothing} data-own=${own ? "1" : nothing} data-enqueued-id=${enqueuedId || nothing}>${from}<div class="user-message">${userBodyTemplate(message?.content)}${thumbs}</div>${stateBadgeTemplate(named)}</div>`;
|
|
157
210
|
}
|
|
158
211
|
|
|
159
212
|
// The notice of a turn that ended with no answer (the server's
|
|
160
213
|
// empty_answer item, turn_summary.empty_answer live): a muted row like the
|
|
161
214
|
// loop's retry row, where the answer would be.
|
|
162
|
-
export function
|
|
163
|
-
return `<div class="bubble hook-notice empty-answer">${
|
|
215
|
+
export function emptyAnswerTemplate(item) {
|
|
216
|
+
return html`<div class="bubble hook-notice empty-answer">${emptyAnswerLine(item)}</div>`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A re-rendered turn's cancel line (a .bubble.cancel.stopped, as live's
|
|
220
|
+
// addEndBubble), nothing when the turn record is not a canceled one.
|
|
221
|
+
export function cancelLineTemplate(record) {
|
|
222
|
+
if (record?.status !== "canceled") return nothing;
|
|
223
|
+
return html`<div class=${STOPPED_LINE_CLASS}>${cancelLineText(record.cancellation_reason, record.cancelled_by)}</div>`;
|
|
164
224
|
}
|
|
165
225
|
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
226
|
+
// +template+'s one top-level element, rendered once (the live page's
|
|
227
|
+
// bubbles: their classes and badge change by hand after this).
|
|
228
|
+
export function elementOf(template) {
|
|
229
|
+
const frag = document.createDocumentFragment();
|
|
230
|
+
render(template, frag);
|
|
231
|
+
return frag.firstElementChild;
|
|
171
232
|
}
|
|
@@ -152,6 +152,20 @@ export function notice(turn, { hook, text, level } = {}) {
|
|
|
152
152
|
return { gen };
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// The whole text of the reply that asked the user (a question or an
|
|
156
|
+
// approval blocks its call): the last gen's text, trimmed, while one of its
|
|
157
|
+
// calls runs and the turn goes on; else "". Only that step's text: a hook's
|
|
158
|
+
// question (asked before its call's row, or between steps) has no running
|
|
159
|
+
// row there, so it gets none rather than another step's (or the answer's)
|
|
160
|
+
// text. The stage shows it while the card waits; the card keeps it once
|
|
161
|
+
// answered.
|
|
162
|
+
export function askingText(turn) {
|
|
163
|
+
if (!turn || turn.ended) return "";
|
|
164
|
+
const gen = turn.gens[turn.gens.length - 1];
|
|
165
|
+
if (!gen?.text || !gen.tools.some((row) => row.status === "running")) return "";
|
|
166
|
+
return gen.text.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
155
169
|
// The turn is over (completed, canceled, failed, the worker gone).
|
|
156
170
|
export function turnEnded(turn) {
|
|
157
171
|
const closed = closeCurrent(turn);
|
|
@@ -215,6 +229,41 @@ export function genLabelParts(gen) {
|
|
|
215
229
|
return { head: "thinking", calls: "", repeats: false };
|
|
216
230
|
}
|
|
217
231
|
|
|
232
|
+
// A step that said something (its text is not the turn's answer): a row of
|
|
233
|
+
// its own in a finished block's outline.
|
|
234
|
+
export function isNarrated(gen) {
|
|
235
|
+
return !gen.answer && !!String(gen.text || "").trim();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// A finished block's outline (+gens+: its steps in order, as the block holds
|
|
239
|
+
// them): each narrated step a row, each run of the others between (tool-only,
|
|
240
|
+
// thinking-only) one fold ({fold: [indices]}), so every mid-turn text's first
|
|
241
|
+
// line reads without a click and the steps that said nothing take one line.
|
|
242
|
+
// [] when no step said anything: the block stays closed, as before.
|
|
243
|
+
// A last run of one thinking-only step stays a row (no "· 1 step" at the end).
|
|
244
|
+
export function outlineRuns(gens) {
|
|
245
|
+
if (!gens.some(isNarrated)) return [];
|
|
246
|
+
const runs = [];
|
|
247
|
+
gens.forEach((gen, i) => {
|
|
248
|
+
if (isNarrated(gen)) return runs.push({ step: i });
|
|
249
|
+
const last = runs[runs.length - 1];
|
|
250
|
+
if (last?.fold) last.fold.push(i);
|
|
251
|
+
else runs.push({ fold: [i] });
|
|
252
|
+
});
|
|
253
|
+
// A turn doesn't end on a "1 step" fold for a lone thinking-only step
|
|
254
|
+
// (the answer's own thinking): it stays a row, its "thinking" label.
|
|
255
|
+
const last = runs[runs.length - 1];
|
|
256
|
+
const lone = last.fold?.length === 1 && gens[last.fold[0]];
|
|
257
|
+
if (lone && lone.tools.length === 0) runs[runs.length - 1] = { step: last.fold[0] };
|
|
258
|
+
return runs;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// A finished turn's steps as its block holds them: the answer's step left it
|
|
262
|
+
// when the answer was all it had (turn_view.js promote).
|
|
263
|
+
export function blockGens(turn) {
|
|
264
|
+
return turn.gens.filter((gen) => !(gen.answer && !gen.thinking && gen.tools.length === 0));
|
|
265
|
+
}
|
|
266
|
+
|
|
218
267
|
// The block's summary line: "working · 3 steps · 8 tool calls · execute ×7 · …"
|
|
219
268
|
// while it runs, without the prefix after; the tally (tally.js) from 3 calls.
|
|
220
269
|
export function blockSummary(turn, { running = false } = {}) {
|