samagotchi 0.53.0 → 0.55.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 +52 -1
- data/README.md +1 -1
- data/bin/chi +3 -2
- data/docs/architecture.md +94 -54
- data/docs/configuration.md +30 -2
- data/docs/context.md +104 -9
- data/docs/internals/background-tasks.md +1 -1
- data/docs/internals/bundles.md +73 -0
- data/docs/internals/context-telemetry.md +27 -6
- data/docs/internals/engine.md +55 -24
- data/docs/internals/tool-guardrails.md +8 -6
- data/docs/plugins.md +2 -1
- data/docs/sessions.md +3 -3
- data/docs/testing.md +23 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -6
- data/lib/samagotchi/bundle_command.rb +31 -36
- data/lib/samagotchi/bundle_needs.rb +4 -5
- data/lib/samagotchi/bundles/skills/manifest.yml +2 -2
- data/lib/samagotchi/bundles/skills/plugin.rb +2 -1
- data/lib/samagotchi/bundles/system/config_modification_protocol.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +2 -2
- data/lib/samagotchi/child_move.rb +6 -4
- data/lib/samagotchi/config.rb +2 -1
- data/lib/samagotchi/context_command.rb +1 -1
- data/lib/samagotchi/context_providers.rb +11 -14
- data/lib/samagotchi/context_status.rb +10 -18
- data/lib/samagotchi/engine.rb +117 -134
- data/lib/samagotchi/extension_load.rb +102 -0
- data/lib/samagotchi/file_ref.rb +59 -0
- data/lib/samagotchi/guardrail_wiring.rb +16 -16
- data/lib/samagotchi/kernel_loop.rb +5 -3
- data/lib/samagotchi/llm/chat_loop.rb +5 -4
- data/lib/samagotchi/memory_bundle/asset_installer.rb +302 -0
- data/lib/samagotchi/memory_bundle/builder.rb +170 -178
- data/lib/samagotchi/memory_bundle/installed_bundle.rb +90 -0
- data/lib/samagotchi/memory_bundle/installer.rb +252 -488
- data/lib/samagotchi/memory_bundle/listing.rb +6 -6
- data/lib/samagotchi/memory_bundle/manifest.rb +4 -2
- data/lib/samagotchi/memory_bundle/plugin_ref.rb +27 -0
- data/lib/samagotchi/memory_bundle/profile.rb +21 -21
- data/lib/samagotchi/memory_bundle/provenance.rb +38 -40
- data/lib/samagotchi/memory_bundle/shipped_update.rb +24 -26
- data/lib/samagotchi/memory_bundle/status.rb +22 -24
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -5
- data/lib/samagotchi/memory_bundle/uninstaller.rb +8 -10
- data/lib/samagotchi/plugin/loader.rb +8 -8
- data/lib/samagotchi/self_report.rb +2 -2
- data/lib/samagotchi/send_command.rb +1 -1
- data/lib/samagotchi/session_continue.rb +275 -0
- data/lib/samagotchi/session_manager.rb +9 -253
- data/lib/samagotchi/session_metrics.rb +3 -1
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -1
- data/lib/samagotchi/terminal_ui.rb +12 -2
- data/lib/samagotchi/tool_activity.rb +84 -14
- data/lib/samagotchi/tool_row_fields.rb +46 -0
- data/lib/samagotchi/tool_runner.rb +7 -11
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +26 -3
- data/lib/samagotchi/web/capabilities.rb +13 -9
- data/lib/samagotchi/web/editor.rb +38 -0
- data/lib/samagotchi/web/message_parts.rb +5 -10
- data/lib/samagotchi/web/public/activity.js +35 -18
- data/lib/samagotchi/web/public/app.js +240 -1136
- data/lib/samagotchi/web/public/commands.js +236 -0
- data/lib/samagotchi/web/public/composer.js +101 -0
- data/lib/samagotchi/web/public/ctx.js +18 -4
- data/lib/samagotchi/web/public/diff_view.js +30 -4
- data/lib/samagotchi/web/public/index.html +9 -2
- data/lib/samagotchi/web/public/info_bar.js +78 -0
- data/lib/samagotchi/web/public/lists_view.js +725 -0
- data/lib/samagotchi/web/public/llm_context_chip.js +9 -6
- data/lib/samagotchi/web/public/question_card.js +9 -1
- data/lib/samagotchi/web/public/question_cards.js +7 -4
- data/lib/samagotchi/web/public/refs.js +134 -0
- data/lib/samagotchi/web/public/restore.js +81 -0
- data/lib/samagotchi/web/public/session_state.js +52 -0
- data/lib/samagotchi/web/public/stream_handlers.js +324 -0
- data/lib/samagotchi/web/public/thinking_chip.js +6 -7
- data/lib/samagotchi/web/public/timing.js +8 -2
- data/lib/samagotchi/web/public/turn_events.js +17 -258
- data/lib/samagotchi/web/public/turn_html.js +15 -7
- data/lib/samagotchi/web/server.rb +4 -4
- metadata +17 -2
- data/lib/samagotchi/web/public/stable_html.js +0 -18
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
// The open session's stream event handlers (GET /stream's typed events, by
|
|
2
|
+
// type); renderSessionView replays a snapshot through them too. Split out
|
|
3
|
+
// of app.js; no markup of its own: the page passes its views and hooks.
|
|
4
|
+
//
|
|
5
|
+
// Everything that changes while the page runs is read through deps at the
|
|
6
|
+
// event, never captured at creation: the open session (selected()), its
|
|
7
|
+
// fields (state(): resetSessionState replaces the object), the page's
|
|
8
|
+
// capabilities, the composer's text. A session switch mid-stream is handled
|
|
9
|
+
// as before: the handlers act on whatever is open when the event comes.
|
|
10
|
+
//
|
|
11
|
+
// deps:
|
|
12
|
+
// selected() the open session's id, or null
|
|
13
|
+
// state() the open session's fields (session_state.js)
|
|
14
|
+
// clientId this tab's id (own prompts and commands)
|
|
15
|
+
// capabilities() the page's capabilities (card.js shownElsewhere)
|
|
16
|
+
// historyEl the history element (keepFollowing, the failed-turn redraw)
|
|
17
|
+
// liveTurn, turnDom, shownTexts, questionCards, contextChips,
|
|
18
|
+
// llmContextControl, thinkingControl the page's views
|
|
19
|
+
// sentIds, earlyRestores restore.js's bookkeeping
|
|
20
|
+
// stream: {takeGuess(), restart()} the stream was opened from a
|
|
21
|
+
// guess (takeGuess: whether, and forget it); restart:
|
|
22
|
+
// close the stream and resync
|
|
23
|
+
// composer: {empty(), refill(text), setChips(chips)}
|
|
24
|
+
// getSession(id, opts) data.js
|
|
25
|
+
// resync(), updateInfoBar(), applyPromptOps(data), showSteers(steers),
|
|
26
|
+
// markRecapStale(), setTurnRunning(running), setLiveStatus(status),
|
|
27
|
+
// endTurnView(kind, endText, opts), appendChunk(text), readTurnEnd(),
|
|
28
|
+
// applyFinalMarkdown(messages), refreshTimingFromSession(),
|
|
29
|
+
// makeUserBubble(prompt, opts), addEndBubble(text), commandBubble(id),
|
|
30
|
+
// addCommandBubble(view), addNoteBubble(note), findInTurnRoots(selector),
|
|
31
|
+
// setUsedMemories(names), showRecap(text), showGuardrailWarning(text, label),
|
|
32
|
+
// showHookNotice(data), onInitEvent(type, data), upsertCard(card),
|
|
33
|
+
// refreshCardBodies() the page's hooks
|
|
34
|
+
|
|
35
|
+
import { routeChunk } from "./chunk_router.js";
|
|
36
|
+
import { extractCtxPct, generationTokens } from "./ctx.js";
|
|
37
|
+
import { failedTurnText } from "./format.js";
|
|
38
|
+
import { cancelLineText } from "./timing.js";
|
|
39
|
+
import { keepFollowing } from "./scroll.js";
|
|
40
|
+
import { shownElsewhere } from "./card.js";
|
|
41
|
+
import { restoredChips } from "./images.js";
|
|
42
|
+
import {
|
|
43
|
+
clientLabel, displayWait, emptyRetryLine, initWaitLine, isOwn, llmContextNotice, reminderText, replaysDrawnTurn,
|
|
44
|
+
retryStatusLine, steerCutLine, turnOutput,
|
|
45
|
+
} from "./turn_events.js";
|
|
46
|
+
import { commandView, drawCommandBubble, historyHasCommandBubble } from "./commands.js";
|
|
47
|
+
import { keepEarlyRestore, restoreAction } from "./restore.js";
|
|
48
|
+
|
|
49
|
+
export function createStreamHandlers(deps) {
|
|
50
|
+
const {
|
|
51
|
+
selected, state, clientId, capabilities, historyEl,
|
|
52
|
+
liveTurn, turnDom, shownTexts, questionCards, contextChips, llmContextControl, thinkingControl,
|
|
53
|
+
sentIds, earlyRestores, stream, composer, getSession,
|
|
54
|
+
resync, updateInfoBar, applyPromptOps, showSteers, markRecapStale, setTurnRunning, setLiveStatus, endTurnView,
|
|
55
|
+
appendChunk, readTurnEnd, applyFinalMarkdown, refreshTimingFromSession, makeUserBubble, addEndBubble,
|
|
56
|
+
commandBubble, addCommandBubble, addNoteBubble, findInTurnRoots, setUsedMemories, showRecap,
|
|
57
|
+
showGuardrailWarning, showHookNotice, onInitEvent, upsertCard, refreshCardBodies,
|
|
58
|
+
} = deps;
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
generation_chunk: (data) => {
|
|
62
|
+
// Route the chunk's bytes to the text bubble and the thinking block (see
|
|
63
|
+
// chunk_router.js). Raw `content` is only a fallback for an older server.
|
|
64
|
+
const { text, thinking } = routeChunk(data);
|
|
65
|
+
liveTurn.timing.setNote("");
|
|
66
|
+
turnDom.chunk({ text, thinking, iteration: data.iteration ?? null });
|
|
67
|
+
const current = state();
|
|
68
|
+
const pct = extractCtxPct(data, current.ctxWindow, current.llmContext?.budget_tokens);
|
|
69
|
+
if (pct !== null && !Number.isNaN(pct)) {
|
|
70
|
+
current.ctxPct = pct;
|
|
71
|
+
updateInfoBar();
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
generation_started: (data) => {
|
|
75
|
+
if (data?.context_window_tokens) {
|
|
76
|
+
const current = state();
|
|
77
|
+
current.ctxWindow = data.context_window_tokens;
|
|
78
|
+
current.ctxWindowSource = data.context_window_source || null;
|
|
79
|
+
}
|
|
80
|
+
liveTurn.timing.setNote("");
|
|
81
|
+
turnDom.generationStarted(data || {});
|
|
82
|
+
},
|
|
83
|
+
generation_completed: (data) => {
|
|
84
|
+
liveTurn.timing.setNote("");
|
|
85
|
+
const current = state();
|
|
86
|
+
if (data?.served_model) current.served = [data.served_model, data.requested_model, data.served_expected_by || null];
|
|
87
|
+
// The generation's speed and the session's token totals, from the server.
|
|
88
|
+
current.timing.tokens = generationTokens(current.timing.tokens, data);
|
|
89
|
+
if (data?.served_model || data?.tokens) updateInfoBar();
|
|
90
|
+
turnDom.generationCompleted();
|
|
91
|
+
},
|
|
92
|
+
// Prompts sent while a turn ran merge into it at an iteration boundary:
|
|
93
|
+
// input_merged names them (their bubbles turn "steered"), then
|
|
94
|
+
// pending_input_merged carries the merged text, shown only for prompts
|
|
95
|
+
// this tab has no bubble for. Its `answer` (the answer the merge
|
|
96
|
+
// superseded) is the TUI's line: here it streamed as that step's text.
|
|
97
|
+
input_merged: (data) => applyPromptOps(data),
|
|
98
|
+
pending_input_merged: (data) => {
|
|
99
|
+
applyPromptOps(data);
|
|
100
|
+
showSteers(data.steers);
|
|
101
|
+
},
|
|
102
|
+
turn_enqueued: (data) => applyPromptOps(data),
|
|
103
|
+
turn_started: (data) => keepFollowing(historyEl, () => {
|
|
104
|
+
if (stream.takeGuess()) {
|
|
105
|
+
// A replay of a turn the render drew: re-read and stream from there.
|
|
106
|
+
if (replaysDrawnTurn(data, state().timing.turnRecords)) {
|
|
107
|
+
stream.restart();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// A finished turn still in the stage goes first: the start op looks
|
|
112
|
+
// for a queued prompt among its rows.
|
|
113
|
+
turnDom.handOff?.({ immediate: true });
|
|
114
|
+
markRecapStale();
|
|
115
|
+
shownTexts.turnStarted();
|
|
116
|
+
const promptBubble = applyPromptOps(data);
|
|
117
|
+
setTurnRunning(true);
|
|
118
|
+
setLiveStatus("running");
|
|
119
|
+
liveTurn.timing.detach();
|
|
120
|
+
turnDom.turnStarted({ ...data, promptBubble });
|
|
121
|
+
liveTurn.timing.start(data.started_at, data.turn_id || null);
|
|
122
|
+
}),
|
|
123
|
+
turn_completed: async (data) => {
|
|
124
|
+
endTurnView("completed", null, { emptyAnswer: data.turn_summary?.empty_answer || null });
|
|
125
|
+
const out = turnOutput(data);
|
|
126
|
+
if (out) appendChunk(out);
|
|
127
|
+
// after_turn hooks are running and may present the answer: its
|
|
128
|
+
// answer_display (display: null when they didn't) comes next. A worker
|
|
129
|
+
// that dies before it never sends one: workerGone releases the wait, and
|
|
130
|
+
// displayWait's timeout ends it whatever happens (4.14).
|
|
131
|
+
const display = data.display_pending ? displayWait() : null;
|
|
132
|
+
if (display) liveTurn.displayPending = display;
|
|
133
|
+
liveTurn.endRead = readTurnEnd();
|
|
134
|
+
await liveTurn.endRead;
|
|
135
|
+
// The answer bubble the turn view held at its box's height pops now
|
|
136
|
+
// that the rendered markdown is in it (or the re-read failed: then the
|
|
137
|
+
// plain text pops), and the display if one was coming, so the links
|
|
138
|
+
// never swap in after the pop. The hold reveals by itself after
|
|
139
|
+
// HOLD_MS whatever comes. The stage has no hold.
|
|
140
|
+
if (display) await display.promise;
|
|
141
|
+
turnDom.answerReady?.();
|
|
142
|
+
},
|
|
143
|
+
// An after_turn hook presented the answer (AnswerDisplay): it comes after
|
|
144
|
+
// turn_completed, so re-read the answer once that read is done.
|
|
145
|
+
answer_display: async (data) => {
|
|
146
|
+
try {
|
|
147
|
+
await liveTurn.endRead;
|
|
148
|
+
if (!selected() || !data.display) return;
|
|
149
|
+
const d = await getSession(selected(), { tail: true, recent: true });
|
|
150
|
+
if (Array.isArray(d.messages)) applyFinalMarkdown(d.messages);
|
|
151
|
+
} catch (_) {
|
|
152
|
+
} finally {
|
|
153
|
+
liveTurn.releaseDisplay();
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
turn_canceled: (data) => {
|
|
157
|
+
endTurnView("canceled", cancelLineText(data.cancellation_reason || "", data.cancelled_by), { stopped: true });
|
|
158
|
+
setLiveStatus("idle");
|
|
159
|
+
refreshTimingFromSession();
|
|
160
|
+
},
|
|
161
|
+
// The turn raised (e.g. the model server stayed unreachable). The worker
|
|
162
|
+
// stays up: it rolls the turn back and hands the prompt back
|
|
163
|
+
// (prompt_restored), and the prompts queued behind it run as usual. A turn
|
|
164
|
+
// that got to tool steps stays as it is (kept_steps; no prompt_restored).
|
|
165
|
+
turn_failed: (data) => {
|
|
166
|
+
const text = failedTurnText(data);
|
|
167
|
+
// Only a rolled-back turn's line waits for its prompt_restored.
|
|
168
|
+
liveTurn.failedText = Number.isInteger(data.kept_steps) ? null : text;
|
|
169
|
+
endTurnView("failed", text);
|
|
170
|
+
setLiveStatus("idle");
|
|
171
|
+
},
|
|
172
|
+
// The rolled-back turn left the session: render it from the server again,
|
|
173
|
+
// then show the prompt once more, marked failed (with why). This tab's own
|
|
174
|
+
// prompt goes back into an empty composer.
|
|
175
|
+
prompt_restored: async (data) => {
|
|
176
|
+
keepEarlyRestore(data, earlyRestores, { myId: clientId, sentIds });
|
|
177
|
+
const { refill, label, images: restoredImages } = restoreAction(data, { myId: clientId, sentIds, composerEmpty: composer.empty() });
|
|
178
|
+
if (refill) composer.refill(refill);
|
|
179
|
+
if (restoredImages?.length) composer.setChips(restoredChips(selected(), restoredImages));
|
|
180
|
+
const failedText = liveTurn.failedText;
|
|
181
|
+
liveTurn.failedText = null;
|
|
182
|
+
await resync();
|
|
183
|
+
// The re-render drew it from the session (failed_turn): drawn again
|
|
184
|
+
// below with this event's label and images.
|
|
185
|
+
const drawn = historyEl.querySelector(".bubble.user[data-failed-turn]");
|
|
186
|
+
if (drawn) {
|
|
187
|
+
if (drawn.nextElementSibling?.classList.contains("cancel")) drawn.nextElementSibling.remove();
|
|
188
|
+
drawn.remove();
|
|
189
|
+
}
|
|
190
|
+
// Not an echo waiting for its turn_enqueued: it keeps its enqueued_id, so
|
|
191
|
+
// a resend's echo isn't matched to it.
|
|
192
|
+
makeUserBubble(data.prompt || "", { state: "failed", label, enqueuedId: data.origin?.enqueued_id || `failed:${Date.now()}`, images: data.images });
|
|
193
|
+
if (failedText) addEndBubble(failedText);
|
|
194
|
+
},
|
|
195
|
+
// A session command ran in the worker (any client's). One that changed
|
|
196
|
+
// the conversation (!rollback, !cmd, a continue answered no) re-reads it.
|
|
197
|
+
command_ran: async (data) => {
|
|
198
|
+
const view = commandView(data, clientId);
|
|
199
|
+
if (view.modelName && view.modelName !== state().model) {
|
|
200
|
+
state().model = view.modelName;
|
|
201
|
+
state().served = null;
|
|
202
|
+
updateInfoBar();
|
|
203
|
+
}
|
|
204
|
+
// /llm-context changed what the next turn runs under.
|
|
205
|
+
if (data.llm_context) {
|
|
206
|
+
state().llmContext = data.llm_context;
|
|
207
|
+
updateInfoBar();
|
|
208
|
+
llmContextControl.refresh();
|
|
209
|
+
}
|
|
210
|
+
// /thinking (or /model) changed the level the next turn runs at.
|
|
211
|
+
if (data.thinking) {
|
|
212
|
+
state().thinking = data.thinking;
|
|
213
|
+
updateInfoBar();
|
|
214
|
+
thinkingControl.refresh();
|
|
215
|
+
}
|
|
216
|
+
if (view.resync) await resync();
|
|
217
|
+
// A card's action that isn't anytime (the step-limit question's
|
|
218
|
+
// /continue) has no bubble: nothing to remove. An anytime or queued
|
|
219
|
+
// one's bubble was drawn at its command_queued: filled in here. One
|
|
220
|
+
// queued for after the turn ran after it: its bubble moves from among
|
|
221
|
+
// the turn's rows to after its answer and timing line.
|
|
222
|
+
const queued = view.anytime || view.queued ? commandBubble(view.commandId) : null;
|
|
223
|
+
if (view.hidden) queued?.remove();
|
|
224
|
+
else if (queued && view.queued && !view.anytime) {
|
|
225
|
+
queued.remove();
|
|
226
|
+
addCommandBubble(view);
|
|
227
|
+
} else if (queued) drawCommandBubble(queued, view);
|
|
228
|
+
// A command that changed the conversation (!cmd): the redrawn history
|
|
229
|
+
// ends with its command bubble already (the saved "!(…)\n<output>"
|
|
230
|
+
// message renders as one) — no second bubble, or the output shows twice.
|
|
231
|
+
else if (view.resync && historyHasCommandBubble(findInTurnRoots(".bubble.command"), view.line)) { /* drawn by the resync */ }
|
|
232
|
+
else addCommandBubble(view);
|
|
233
|
+
},
|
|
234
|
+
// An anytime command (/help, a plugin's /btw) runs at once, beside a
|
|
235
|
+
// turn: its bubble goes here, so the cards it shows come after it. One
|
|
236
|
+
// queued for after the turn (/model X mid-turn) shows waiting here.
|
|
237
|
+
command_queued: (data) => {
|
|
238
|
+
// A card's action waits for its command_ran: shown only if it answers.
|
|
239
|
+
if ((data.anytime || data.waits) && !data.card && !commandBubble(data.command_id)) addCommandBubble(commandView(data, clientId));
|
|
240
|
+
},
|
|
241
|
+
// Due reminders went into the turn (a reminder turn has no prompt bubble).
|
|
242
|
+
reminder_injected: (data) => addCommandBubble({ label: null, line: reminderText(data), text: "" }),
|
|
243
|
+
// A context note joined the conversation (between turns; no turn runs).
|
|
244
|
+
context_added: (data) => {
|
|
245
|
+
addNoteBubble({ label: data.label, content: data.text || "" });
|
|
246
|
+
// An attached context source's note: its chip changed.
|
|
247
|
+
if (data.context_source) contextChips.refresh();
|
|
248
|
+
},
|
|
249
|
+
continue_offered: (data) => questionCards.renderContinue(data),
|
|
250
|
+
continue_resolved: (data) => questionCards.resolveContinue(data),
|
|
251
|
+
tool_call_started: (data) => turnDom.toolStarted(data),
|
|
252
|
+
tool_call_completed: (data) => turnDom.toolCompleted(data),
|
|
253
|
+
// The engine's list, after every memory read and at a turn's end.
|
|
254
|
+
used_memories_updated: (data) => {
|
|
255
|
+
if (Array.isArray(data.used_memory_names)) setUsedMemories(data.used_memory_names);
|
|
256
|
+
},
|
|
257
|
+
context_status: (data) => {
|
|
258
|
+
const current = state();
|
|
259
|
+
const pct = extractCtxPct(data, current.ctxWindow, current.llmContext?.budget_tokens);
|
|
260
|
+
if (pct !== null) {
|
|
261
|
+
current.ctxPct = pct;
|
|
262
|
+
updateInfoBar();
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
question_requested: (data) => {
|
|
266
|
+
const pq = data.pending_question;
|
|
267
|
+
if (pq && pq.id) questionCards.renderQuestion(pq);
|
|
268
|
+
},
|
|
269
|
+
question_answered: (data) => {
|
|
270
|
+
questionCards.resolveQuestion(data.id, { answer: data.answer });
|
|
271
|
+
},
|
|
272
|
+
// A delegate's question relayed to its parent's card, or no longer.
|
|
273
|
+
question_relay: (data) => questionCards.markRelayed(data.id, data.relayed_to),
|
|
274
|
+
question_cancelled: (data) => {
|
|
275
|
+
questionCards.resolveQuestion(data.id, { cancelled: true, reason: data.reason });
|
|
276
|
+
},
|
|
277
|
+
// One collected just as a turn started describes the chat before it.
|
|
278
|
+
recap_ready: (data) => {
|
|
279
|
+
// After the turn it covers, never above it.
|
|
280
|
+
turnDom.handOff?.({ immediate: true });
|
|
281
|
+
if (!liveTurn.running) showRecap(data.recap);
|
|
282
|
+
},
|
|
283
|
+
guardrail_warning: (data) => showGuardrailWarning(data.message, data.label || "guardrails"),
|
|
284
|
+
// A notice during a turn is a row of its current step. One the page
|
|
285
|
+
// shows elsewhere (a hook's links in the rendered answer) isn't drawn,
|
|
286
|
+
// not even in the stage trail; a joined turn's notices come here too.
|
|
287
|
+
hook_notice: (data) => {
|
|
288
|
+
if (shownElsewhere(data, capabilities())) return;
|
|
289
|
+
if (!turnDom.notice(data)) showHookNotice(data);
|
|
290
|
+
},
|
|
291
|
+
// The loop asks again after an empty answer: a row of the empty step
|
|
292
|
+
// (a reload puts it back from the snapshot's cards).
|
|
293
|
+
empty_answer_retry: (data) => { turnDom.notice({ line: emptyRetryLine(data) }); },
|
|
294
|
+
steer_cut: (data) => { turnDom.notice({ line: steerCutLine(data), cls: "steer-cut" }); },
|
|
295
|
+
// A batch of LLM context edits the server applied: a ✂ row of the
|
|
296
|
+
// current step, its hover listing the outputs.
|
|
297
|
+
llm_context_edited: (data) => { turnDom.notice(llmContextNotice(data)); },
|
|
298
|
+
// The provider is asked again after an error, or the turn waits for
|
|
299
|
+
// plugins' setup: a note on the live timing line until it streams.
|
|
300
|
+
generation_retrying: (data) => {
|
|
301
|
+
// A dropped stream's step asked again: what it streamed is void.
|
|
302
|
+
if (data?.restarted) turnDom.generationRestarted(data);
|
|
303
|
+
liveTurn.timing.setNote(retryStatusLine(data));
|
|
304
|
+
},
|
|
305
|
+
plugin_init_wait: (data) => liveTurn.timing.setNote(initWaitLine(data)),
|
|
306
|
+
plugin_init_started: (data) => onInitEvent("plugin_init_started", data),
|
|
307
|
+
plugin_init_finished: (data) => onInitEvent("plugin_init_finished", data),
|
|
308
|
+
// A plugin's card, or a new version of one (the same id).
|
|
309
|
+
card: (data) => {
|
|
310
|
+
upsertCard(data);
|
|
311
|
+
if (data.body) refreshCardBodies();
|
|
312
|
+
},
|
|
313
|
+
// The stream's cursor could not be replayed: render from scratch.
|
|
314
|
+
reset: () => resync(),
|
|
315
|
+
// Synthetic (snapshotEvents): a prompt merged into the turn in progress.
|
|
316
|
+
merged_input: (data) => {
|
|
317
|
+
const origin = (data.origins || [])[0] || {};
|
|
318
|
+
const own = isOwn(origin.client_id, clientId);
|
|
319
|
+
makeUserBubble(data.content, {
|
|
320
|
+
state: "steered", own, enqueuedId: origin.enqueued_id || null, label: own ? null : clientLabel(origin.client_id),
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
@@ -37,13 +37,12 @@ export function chipModel(summary, { kind = SESSION } = {}) {
|
|
|
37
37
|
return { text: `think ${summary.level}${own ? ` · ${kind}` : ""}`, title, own };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// The session bar's chip
|
|
41
|
-
//
|
|
42
|
-
export function
|
|
40
|
+
// The session bar's chip (a lit-html template; info_bar.js), nothing
|
|
41
|
+
// without a summary.
|
|
42
|
+
export function chipTemplate(summary) {
|
|
43
43
|
const chip = chipModel(summary);
|
|
44
|
-
if (!chip) return
|
|
45
|
-
return `<button type="button" class
|
|
46
|
-
`data-think-chip aria-haspopup="dialog">${escapeHtml(chip.text)}</button>`;
|
|
44
|
+
if (!chip) return nothing;
|
|
45
|
+
return html`<button type="button" class=${`model think-chip${chip.own ? " own" : ""}`} title=${chip.title} data-think-chip aria-haspopup="dialog">${chip.text}</button>`;
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
// The /thinking line for a select value ("default" unsets the session's
|
|
@@ -81,7 +80,7 @@ export function formTemplate(summary, { running = false, kind = SESSION, choice
|
|
|
81
80
|
}
|
|
82
81
|
|
|
83
82
|
// The chip's popover, as createLlmContextControl's: el is where the chip
|
|
84
|
-
// is drawn (clicks are delegated: #infoText
|
|
83
|
+
// is drawn (clicks are delegated: #infoText, info_bar.js);
|
|
85
84
|
// summary(): the open session's thinking; session(): its id; running():
|
|
86
85
|
// a turn runs; run(line): sends the command. The start page's (kind
|
|
87
86
|
// NEW_CHAT): summary() is the picked model's, choice() the pending level
|
|
@@ -264,14 +264,20 @@ function canceledSteps(g, record, tools) {
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
// A call's row from its tool record alone (no saved parts): the record's
|
|
267
|
-
// called_as too, so "(called as bash)" shows after a reload, as live
|
|
267
|
+
// called_as too, so "(called as bash)" shows after a reload, as live, and
|
|
268
|
+
// its no_match ("no match" on a grep that found nothing).
|
|
268
269
|
function recordRows(tools, iteration) {
|
|
269
270
|
return tools.filter((r) => Number(r.iteration) === iteration).map((r) => ({
|
|
270
271
|
key: `${iteration}:${r.call_index ?? 0}`, tool: r.tool, status: r.status, duration_ms: r.duration_ms,
|
|
271
272
|
...(r.called_as ? { called_as: r.called_as } : {}),
|
|
273
|
+
...noMatch(r),
|
|
272
274
|
}));
|
|
273
275
|
}
|
|
274
276
|
|
|
277
|
+
function noMatch(record) {
|
|
278
|
+
return record?.no_match ? { no_match: true } : {};
|
|
279
|
+
}
|
|
280
|
+
|
|
275
281
|
// A turn whose saved messages carry their parts (the server's ?parts=1):
|
|
276
282
|
// every assistant message is one step. Its calls take the turn's records by
|
|
277
283
|
// tool, in call order (each the next record of its tool, consumed with the
|
|
@@ -292,7 +298,7 @@ function partsTurn(g, items, record, tools, stepsOnly = false) {
|
|
|
292
298
|
const rows = Array.isArray(parts.tools)
|
|
293
299
|
? parts.tools.map((call, j) => {
|
|
294
300
|
const r = takeRecord(pool, call.tool);
|
|
295
|
-
return { key: `${iteration}:${j + 1}`, status: r?.status || "ok", duration_ms: r?.duration_ms, ...call };
|
|
301
|
+
return { key: `${iteration}:${j + 1}`, status: r?.status || "ok", duration_ms: r?.duration_ms, ...noMatch(r), ...call };
|
|
296
302
|
})
|
|
297
303
|
: [];
|
|
298
304
|
const thinking = String(parts.thinking || "");
|