samagotchi 0.75.0 → 0.76.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 +47 -1
- data/docs/architecture.md +11 -4
- data/docs/cli.md +13 -1
- data/docs/configuration.md +4 -3
- data/docs/guardrails.md +9 -0
- data/docs/internals/anthropic.md +3 -1
- data/docs/internals/bundles.md +20 -7
- data/docs/internals/context-telemetry.md +22 -0
- data/docs/internals/engine.md +14 -3
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/memory.md +34 -0
- data/docs/plugins.md +1 -1
- data/docs/releasing.md +6 -2
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/bundle_command.rb +52 -39
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -2
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +19 -2
- data/lib/samagotchi/bundles/core/manifest.yml +2 -2
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/desktop/macos.rb +1 -2
- data/lib/samagotchi/effective_model.rb +5 -0
- data/lib/samagotchi/engine.rb +84 -4
- data/lib/samagotchi/kernel_loop.rb +13 -10
- data/lib/samagotchi/llm_context_override.rb +8 -6
- data/lib/samagotchi/memory_bundle/asset_installer.rb +34 -5
- data/lib/samagotchi/memory_bundle/builder.rb +11 -14
- data/lib/samagotchi/memory_bundle/installer.rb +122 -5
- data/lib/samagotchi/memory_bundle/manifest.rb +20 -1
- data/lib/samagotchi/memory_bundle/memory_files.rb +24 -6
- data/lib/samagotchi/memory_bundle/profile.rb +1 -1
- data/lib/samagotchi/memory_bundle/shipped_update.rb +2 -1
- data/lib/samagotchi/memory_bundle/source.rb +66 -3
- data/lib/samagotchi/memory_bundle/system_bundle.rb +4 -2
- data/lib/samagotchi/model_price.rb +4 -3
- data/lib/samagotchi/paths.rb +15 -1
- data/lib/samagotchi/session_commands.rb +2 -0
- data/lib/samagotchi/session_manager.rb +1 -2
- data/lib/samagotchi/session_metrics/model_segment.rb +109 -0
- data/lib/samagotchi/session_metrics/totals.rb +127 -0
- data/lib/samagotchi/session_metrics.rb +118 -83
- data/lib/samagotchi/terminal_ui/formatting.rb +127 -10
- data/lib/samagotchi/terminal_ui/status_row.rb +3 -1
- data/lib/samagotchi/toolkit.rb +9 -8
- data/lib/samagotchi/toolkit_command.rb +2 -2
- data/lib/samagotchi/tools/builtins.rb +1 -5
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +3 -0
- data/lib/samagotchi/web/public/app.js +4 -3
- data/lib/samagotchi/web/public/chains.js +1 -1
- data/lib/samagotchi/web/public/composer.js +9 -5
- data/lib/samagotchi/web/public/ctx.js +73 -11
- data/lib/samagotchi/web/public/index.html +30 -35
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/question_cards.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +3 -0
- data/lib/samagotchi/web/public/stage_slots.js +3 -3
- data/lib/samagotchi/web/public/stream_handlers.js +32 -1
- data/lib/samagotchi/web/public/timing.js +11 -2
- metadata +3 -1
data/lib/samagotchi/toolkit.rb
CHANGED
|
@@ -77,16 +77,17 @@ module Samagotchi
|
|
|
77
77
|
super
|
|
78
78
|
end
|
|
79
79
|
|
|
80
|
-
# What the prompt carries
|
|
81
|
-
# and removed ones (the not-loaded line, find_tools'
|
|
82
|
-
# either list has any the kit name the line gives
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def prompt_key
|
|
86
|
-
[declared.map(&:name), reachable, removed, reachable.empty? && removed.empty? ? nil : resolved.name,
|
|
87
|
-
resolved.prompt]
|
|
80
|
+
# What the prompt carries before the kit's prompt text: the declared
|
|
81
|
+
# names, reachable and removed ones (the not-loaded line, find_tools'
|
|
82
|
+
# chi line), and, when either list has any, the kit name the line gives.
|
|
83
|
+
def tools_key
|
|
84
|
+
[declared.map(&:name), reachable, removed, reachable.empty? && removed.empty? ? nil : resolved.name]
|
|
88
85
|
end
|
|
89
86
|
|
|
87
|
+
# What the prompt carries of it, including the kit's prompt text. Two
|
|
88
|
+
# equal keys build the same prompt (/toolkit's "no change", its cost).
|
|
89
|
+
def prompt_key = [*tools_key, resolved.prompt]
|
|
90
|
+
|
|
90
91
|
# What the UIs show (the snapshot, command_ran): nil when it declares
|
|
91
92
|
# every tool on full (full, full +delegate), which has no chip.
|
|
92
93
|
# +left_out+ is +reachable+ and +removed+ together; +prompt+ (the
|
|
@@ -129,9 +129,9 @@ module Samagotchi
|
|
|
129
129
|
|
|
130
130
|
# What the confirm says is read again: from the tools on (the front of
|
|
131
131
|
# every request), or from the kit's prompt text on when only that
|
|
132
|
-
# changes (Explained#
|
|
132
|
+
# changes (Explained#tools_key is unchanged: only a kit's prompt text).
|
|
133
133
|
def reread_line(before, after)
|
|
134
|
-
if before.
|
|
134
|
+
if before.tools_key == after.tools_key
|
|
135
135
|
"The kit's prompt text is in the system prompt: the prompt is read again from it on."
|
|
136
136
|
else
|
|
137
137
|
"The tools are the front of every request: the whole prompt is read again from them on."
|
|
@@ -144,11 +144,7 @@ module Samagotchi
|
|
|
144
144
|
|
|
145
145
|
# The XDG dirs set in +env+, made absolute (nothing for an unset one).
|
|
146
146
|
def xdg_absolute(env = ENV)
|
|
147
|
-
|
|
148
|
-
dirs = {}
|
|
149
|
-
dirs["XDG_STATE_HOME"] = Paths.state_home(env: env) if set.call("XDG_STATE_HOME")
|
|
150
|
-
dirs["XDG_CONFIG_HOME"] = Paths.config_home(env: env) if set.call("XDG_CONFIG_HOME")
|
|
151
|
-
dirs
|
|
147
|
+
Paths.absolute_xdg(env: env)
|
|
152
148
|
end
|
|
153
149
|
|
|
154
150
|
# The unsets that keep a `chi --model X` worker's default from passing
|
data/lib/samagotchi/version.rb
CHANGED
data/lib/samagotchi/web/app.rb
CHANGED
|
@@ -1794,6 +1794,9 @@ module Samagotchi
|
|
|
1794
1794
|
# before the next turn streams (live over saved).
|
|
1795
1795
|
context: source["context"],
|
|
1796
1796
|
tokens: source["tokens"],
|
|
1797
|
+
# The tokens, cost and speeds per model (SessionMetrics'
|
|
1798
|
+
# model_segments; none with one model, or from an older file).
|
|
1799
|
+
model_segments: Array(source["model_segments"]),
|
|
1797
1800
|
# The memory indexes the session's prompt holds (the ctx tooltip).
|
|
1798
1801
|
memory_index: source["memory_index"]
|
|
1799
1802
|
}
|
|
@@ -1475,7 +1475,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1475
1475
|
// The chips stay (uploaded ones keep their ref, to sentTo) for another try: an
|
|
1476
1476
|
// image refused (bad_image, too_large, bad_images) says why below.
|
|
1477
1477
|
if (e.code === "owned_by_tui") {
|
|
1478
|
-
if (sentTo !== selected) return
|
|
1478
|
+
if (sentTo !== selected) return showToast(cantText(`send to ${sentTo.slice(0, 8)}`, e), null, { sticky: true });
|
|
1479
1479
|
current.owner = "tui";
|
|
1480
1480
|
updateComposerMode();
|
|
1481
1481
|
return;
|
|
@@ -1485,7 +1485,7 @@ async function handleSendTurn({ delivery: chosen = null } = {}) {
|
|
|
1485
1485
|
if (e.code === "not_found") return leaveGoneSession(sentTo, "not_found");
|
|
1486
1486
|
// A /queue to a worker that can't queue (it would steer): its words, the text kept.
|
|
1487
1487
|
if (e.code === "delivery_unavailable" && sentTo === selected) return addCommandBubble({ label: null, line, text: e.body?.detail || e.message, failed: true });
|
|
1488
|
-
|
|
1488
|
+
showToast(cantText("send", e), null, { sticky: true });
|
|
1489
1489
|
} finally {
|
|
1490
1490
|
actionBtn.disabled = current.owner === "tui";
|
|
1491
1491
|
promptEl.focus();
|
|
@@ -1502,13 +1502,14 @@ const composerCommands = createComposerCommands({
|
|
|
1502
1502
|
refreshHistory: () => refreshHistory(),
|
|
1503
1503
|
failed: (line, text) => addCommandBubble({ label: null, line, text, failed: true }),
|
|
1504
1504
|
// As handleSendTurn says a turn's failure after a switch.
|
|
1505
|
-
failedElsewhere: (line, text) =>
|
|
1505
|
+
failedElsewhere: (line, text, id) => showToast(cantText(`run ${line} in ${id.slice(0, 8)}`, text), null, { sticky: true }),
|
|
1506
1506
|
ownedByTui: () => {
|
|
1507
1507
|
current.owner = "tui";
|
|
1508
1508
|
updateComposerMode();
|
|
1509
1509
|
},
|
|
1510
1510
|
composer: {
|
|
1511
1511
|
busy: () => { actionBtn.disabled = true; },
|
|
1512
|
+
text: () => promptEl.value,
|
|
1512
1513
|
clear: () => clearComposer(),
|
|
1513
1514
|
done: () => {
|
|
1514
1515
|
actionBtn.disabled = current.owner === "tui";
|
|
@@ -230,7 +230,7 @@ export function chainRowsTemplate(f, { activeId = null, now = Date.now() } = {})
|
|
|
230
230
|
const ago = relativeTime(s.updated_at || s.created_at || "", now);
|
|
231
231
|
const tip = `${s.id} · ${s.status || ""}${ago ? ` · updated ${ago}` : ""}`;
|
|
232
232
|
return html`<div class=${cls.join(" ")} data-id=${s.id} style=${"--depth:1"} role=${"listitem"}><button type="button" class="fr-open"
|
|
233
|
-
title=${tip}>${rowDotTemplate(s, waiting)}<span class="cr-
|
|
233
|
+
title=${tip}>${rowDotTemplate(s, waiting)}<span class="cr-n">#${n}</span>${
|
|
234
234
|
date ? html`<span class=${date.length > 10 ? "cr-date timed" : "cr-date"}>${date}</span>` : nothing}<span class="fr-preview">${s.recap || chainTitle(s, byId)}</span>${
|
|
235
235
|
rowBadgesTemplate(waiting, stopped)}${s.archived ? archivedBadge : nothing}</button></div>`;
|
|
236
236
|
});
|
|
@@ -60,9 +60,10 @@ export function routeSessionLine(prompt, { commands = [], images = 0, delivery =
|
|
|
60
60
|
// followWokenWorker() the command woke the worker: follow its stream
|
|
61
61
|
// refreshHistory()
|
|
62
62
|
// failed(line, text) a failed command's bubble
|
|
63
|
-
// failedElsewhere(line, text) a command to
|
|
63
|
+
// failedElsewhere(line, text, id) a command to session id, no longer open, failed
|
|
64
64
|
// ownedByTui() a plain terminal holds the session
|
|
65
|
-
// composer: {busy(), clear(), done()} the composer's send button
|
|
65
|
+
// composer: {busy(), text(), clear(), done()} the composer's send button
|
|
66
|
+
// and text (text(): what it holds now)
|
|
66
67
|
export function createComposerCommands(deps) {
|
|
67
68
|
const { session, clientId, sendCommand, followWokenWorker, refreshHistory, failed, failedElsewhere, ownedByTui,
|
|
68
69
|
composer } = deps;
|
|
@@ -72,14 +73,17 @@ export function createComposerCommands(deps) {
|
|
|
72
73
|
// worker, owner and history are left alone then.
|
|
73
74
|
async function send(line) {
|
|
74
75
|
const id = session();
|
|
76
|
+
// The composer is shared: a draft typed in a session opened meanwhile
|
|
77
|
+
// stays when the answer comes; only the text sent goes.
|
|
78
|
+
const sent = composer.text();
|
|
75
79
|
composer.busy();
|
|
76
80
|
try {
|
|
77
81
|
await sendCommand(id, line, { clientId });
|
|
78
|
-
composer.clear();
|
|
82
|
+
if (composer.text() === sent) composer.clear();
|
|
79
83
|
refreshHistory();
|
|
80
84
|
if (session() === id) followWokenWorker();
|
|
81
85
|
} catch (e) {
|
|
82
|
-
if (session() !== id) return failedElsewhere(line, e.message);
|
|
86
|
+
if (session() !== id) return failedElsewhere(line, e.message, id);
|
|
83
87
|
if (e.code === "owned_by_tui") {
|
|
84
88
|
ownedByTui();
|
|
85
89
|
return;
|
|
@@ -103,7 +107,7 @@ export function createComposerCommands(deps) {
|
|
|
103
107
|
return true;
|
|
104
108
|
} catch (e) {
|
|
105
109
|
if (session() === id) failed(line, e.message);
|
|
106
|
-
else failedElsewhere(line, e.message);
|
|
110
|
+
else failedElsewhere(line, e.message, id);
|
|
107
111
|
return false;
|
|
108
112
|
}
|
|
109
113
|
}
|
|
@@ -133,14 +133,23 @@ export function ctxBarText(pct, windowSource = null) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// The info bar's / the card's ctx tooltip: the window line under the title
|
|
136
|
-
// when there's a number (tokensTipText), or why there's none
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
136
|
+
// when there's a number (tokensTipText), or why there's none when the page
|
|
137
|
+
// has no percent: "No request in this session yet" before any request (the
|
|
138
|
+
// page isn't told context.status is off, so "?" never means 0); after one,
|
|
139
|
+
// "Context window unknown" over the session's tokens and per-model rows with
|
|
140
|
+
// no window line (a window lookup that failed; a model list without a context
|
|
141
|
+
// length still falls back to context.window_tokens or chi's default). +window.afterSwitch+ (the info
|
|
142
|
+
// bar's; a /model whose new window didn't resolve, no request since:
|
|
143
|
+
// Engine#announce_switched_context! sent usage null) adds "until the next
|
|
144
|
+
// request", which asks for it again.
|
|
145
|
+
export function ctxBarTip(tokens, memoryIndex, window, segments = null) {
|
|
146
|
+
if (num(window?.pct) === null) {
|
|
147
|
+
if (!num(tokens?.prompt_sum) && !num(tokens?.completion_sum)) return "No request in this session yet";
|
|
148
|
+
const why = window?.afterSwitch ? "Context window unknown until the next request" : "Context window unknown";
|
|
149
|
+
return tokensTipText(tokens, why, memoryIndex, null, segments);
|
|
150
|
+
}
|
|
142
151
|
|
|
143
|
-
return tokensTipText(tokens, undefined, memoryIndex, window);
|
|
152
|
+
return tokensTipText(tokens, undefined, memoryIndex, window, segments);
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
// The ctx tooltip's window line ({tokens:, source:, pct:, budget:} the
|
|
@@ -166,14 +175,67 @@ export function ctxWindowText(window) {
|
|
|
166
175
|
return `context: ${used}${countK(tokens)} tokens${sourceText}`;
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
// ── A session's rows per model (the timing's modelSegments) ──────────────
|
|
179
|
+
// The words /stats' models: block shares (Formatting#model_segment_*;
|
|
180
|
+
// spec/shared/labels_matrix.json model_segments).
|
|
181
|
+
|
|
182
|
+
// A row's model: its ref; the rows folded past the cap ("3 earlier
|
|
183
|
+
// segments", told by folded_segments); a session's turns from before turns
|
|
184
|
+
// named their model ("earlier turns").
|
|
185
|
+
export function modelSegmentLabel(row) {
|
|
186
|
+
const folded = num(row?.folded_segments);
|
|
187
|
+
if (folded !== null && folded > 0) return `${folded} earlier segments`;
|
|
188
|
+
return row?.model_ref ? String(row.model_ref) : "earlier turns";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// "6.0k in / 40 out".
|
|
192
|
+
export function modelSegmentTokensText(row) {
|
|
193
|
+
return `${countK(num(row?.prompt_sum) || 0)} in / ${countK(num(row?.completion_sum) || 0)} out`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// "$0.42", "~$0.12", "$0.42 + ~$0.12"; "" for none.
|
|
197
|
+
export function modelSegmentCostText(row) {
|
|
198
|
+
return [costText(row?.cost_sum), costText(row?.cost_estimate_sum, { estimate: true })].filter(Boolean).join(" + ");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// "switch prefill 3.0k uncached prompt ≈$0.0060": what the switch to the
|
|
202
|
+
// row's model prefilled (its cost an estimate from the configured price,
|
|
203
|
+
// left out without one); "" for none.
|
|
204
|
+
export function modelSegmentPrefillText(row) {
|
|
205
|
+
const tokens = num(row?.prefill_tokens) || 0;
|
|
206
|
+
if (tokens <= 0) return "";
|
|
207
|
+
const cost = costText(row?.prefill_cost_estimate);
|
|
208
|
+
return `switch prefill ${countK(tokens)} uncached prompt${cost ? ` ≈${cost}` : ""}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// The tooltip's rows per model, with ≥ 2 (one model: none): "model ·
|
|
212
|
+
// turns · in / out · cost", a switch's prefill under its row.
|
|
213
|
+
function modelSegmentLines(segments) {
|
|
214
|
+
if (!Array.isArray(segments) || segments.length < 2) return [];
|
|
215
|
+
const lines = ["per model:"];
|
|
216
|
+
for (const row of segments) {
|
|
217
|
+
const turns = num(row.turns) || 0;
|
|
218
|
+
const parts = [modelSegmentLabel(row), `${turns} ${turns === 1 ? "turn" : "turns"}`, modelSegmentTokensText(row)];
|
|
219
|
+
const cost = modelSegmentCostText(row);
|
|
220
|
+
if (cost) parts.push(cost);
|
|
221
|
+
lines.push(` ${parts.join(" · ")}`);
|
|
222
|
+
const prefill = modelSegmentPrefillText(row);
|
|
223
|
+
if (prefill) lines.push(` └ ${prefill}`);
|
|
224
|
+
}
|
|
225
|
+
return lines;
|
|
226
|
+
}
|
|
227
|
+
|
|
169
228
|
// The ctx tooltip (info bar and card): +title+, then the session's tokens
|
|
170
229
|
// (in summed over every request, cached with its share, out with the
|
|
171
230
|
// reasoning part) and its cost, when it has any (an estimated part with
|
|
172
231
|
// "~", costLine), and the memory indexes
|
|
173
232
|
// its prompt holds (+memoryIndex+, the snapshot's memory_index); with
|
|
174
|
-
// +window+ (ctxWindowText's) the window line under the title
|
|
175
|
-
//
|
|
176
|
-
|
|
233
|
+
// +window+ (ctxWindowText's) the window line under the title; with
|
|
234
|
+
// +segments+ (the timing's modelSegments, ≥ 2) the rows per model above the
|
|
235
|
+
// totals. A delegated session keeps its own metrics, so these are this
|
|
236
|
+
// session's only.
|
|
237
|
+
export function tokensTipText(tokens, title = "Context used after the last turn", memoryIndex = null, window = null,
|
|
238
|
+
segments = null) {
|
|
177
239
|
const memory = memoryIndexText(memoryIndex);
|
|
178
240
|
const memoryLine = memory ? `\nmemory index: ${memory}` : "";
|
|
179
241
|
const windowText = ctxWindowText(window);
|
|
@@ -186,7 +248,7 @@ export function tokensTipText(tokens, title = "Context used after the last turn"
|
|
|
186
248
|
const parts = [`in ${count(prompt)}`];
|
|
187
249
|
if (cached > 0 && prompt > 0) parts.push(`cached ${count(cached)} (${Math.round((cached * 100) / prompt)}%)`);
|
|
188
250
|
parts.push(`out ${count(completion)}${reasoning > 0 ? ` (reasoning ${count(reasoning)})` : ""}`);
|
|
189
|
-
const lines = [title, `tokens: ${parts.join(" · ")}`];
|
|
251
|
+
const lines = [title, ...modelSegmentLines(segments), `tokens: ${parts.join(" · ")}`];
|
|
190
252
|
const cost = costLine(tokens);
|
|
191
253
|
if (cost) lines.push(cost);
|
|
192
254
|
lines.push("this session only, all requests");
|
|
@@ -326,22 +326,35 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
326
326
|
.bubble.question>summary .question-said{max-width:72ch;margin:2px 0 1px 1.05em;font-size:12.5px;font-weight:400;line-height:1.5;color:var(--text-muted);white-space:normal;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;line-clamp:2;overflow:hidden}
|
|
327
327
|
.bubble.question[open]>summary .question-said{display:none}
|
|
328
328
|
.question-said-full{margin:4px 0 6px;font-size:13px;line-height:1.55;color:var(--text-muted);white-space:pre-wrap;overflow-wrap:anywhere}
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.
|
|
336
|
-
.
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
.question-said-full
|
|
342
|
-
.
|
|
343
|
-
.
|
|
344
|
-
.
|
|
329
|
+
.question-said-full.md-compact{
|
|
330
|
+
--md-heading-size:13.5px;--md-heading-color:var(--text-2);
|
|
331
|
+
--md-subheading-size:13.5px;--md-subheading-color:var(--text-2);
|
|
332
|
+
--md-strong-color:currentColor;--md-blockquote-color:currentColor;--md-code-color:currentColor;
|
|
333
|
+
--md-pre-size:12px;--md-cell-size:12.5px
|
|
334
|
+
}
|
|
335
|
+
.ts-asking.md-compact{
|
|
336
|
+
--md-heading-size:14.5px;--md-heading-color:var(--text-bright);
|
|
337
|
+
--md-subheading-size:14px;--md-subheading-color:var(--text-2);
|
|
338
|
+
--md-strong-color:var(--text-bright);--md-blockquote-color:var(--text-muted);--md-code-color:var(--text);
|
|
339
|
+
--md-pre-size:12.5px;--md-cell-size:13px
|
|
340
|
+
}
|
|
341
|
+
.md-compact:is(.ts-asking,.question-said-full){white-space:normal}
|
|
342
|
+
.md-compact :is(h1,h2,h3,h4,h5,h6){font-size:var(--md-heading-size);font-weight:600;line-height:1.4;margin:8px 0 2px;color:var(--md-heading-color)}
|
|
343
|
+
.md-compact :is(h4,h5,h6){font-size:var(--md-subheading-size);color:var(--md-subheading-color)}
|
|
344
|
+
.md-compact :is(p,ul,ol,blockquote,table,pre){margin:0 0 6px}
|
|
345
|
+
.md-compact>:first-child{margin-top:0}
|
|
346
|
+
.md-compact>:last-child{margin-bottom:0}
|
|
347
|
+
.md-compact :is(ul,ol){padding-left:1.3em}
|
|
348
|
+
.md-compact li>:is(ul,ol){margin:0}
|
|
349
|
+
.md-compact hr{border:0;border-top:1px solid var(--border);margin:8px 0}
|
|
350
|
+
.md-compact blockquote{border-left:3px solid var(--border-strong);padding-left:10px;color:var(--md-blockquote-color)}
|
|
351
|
+
.md-compact code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
352
|
+
.md-compact :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:0 4px;color:var(--md-code-color)}
|
|
353
|
+
.md-compact strong{color:var(--md-strong-color)}
|
|
354
|
+
.md-compact pre{overflow:auto;padding:6px 8px;border-radius:6px;background:var(--bg-inset);border:1px solid var(--border);font-size:var(--md-pre-size);line-height:1.45;white-space:pre}
|
|
355
|
+
.md-compact table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
|
|
356
|
+
.md-compact :is(th,td){border:1px solid var(--border-strong);padding:3px 7px;font-size:var(--md-cell-size)}
|
|
357
|
+
.md-compact th{background:var(--bg-inset);font-weight:600}
|
|
345
358
|
.bubble.command{border-left:3px solid var(--text-faint);background:var(--bg-panel);font-size:13px}
|
|
346
359
|
.bubble.note{border-left:3px dotted var(--text-faint);background:transparent;font-size:12px;color:var(--text-muted);opacity:.85}
|
|
347
360
|
.note-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);margin-bottom:2px}
|
|
@@ -730,24 +743,6 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
730
743
|
text, then its markdown when web.markdown is on (.markdown, stage_slots.js drawAsking); capped so the card keeps
|
|
731
744
|
its room, read from the top. */
|
|
732
745
|
.ts-asking{max-height:calc(var(--stage-max,60vh) * .35);overflow:auto;scrollbar-width:thin;white-space:pre-wrap;overflow-wrap:anywhere;font-size:14px;line-height:1.55;color:var(--narr);border-left:2px solid var(--border-strong);padding-left:10px}
|
|
733
|
-
/* Its markdown, compact: about the raw text's metrics, so the swap barely moves the slot. */
|
|
734
|
-
.ts-asking.markdown{white-space:normal}
|
|
735
|
-
.ts-asking.markdown :is(h1,h2,h3,h4,h5,h6){font-size:14.5px;font-weight:600;line-height:1.4;margin:8px 0 2px;color:var(--text-bright)}
|
|
736
|
-
.ts-asking.markdown :is(h4,h5,h6){font-size:14px;color:var(--text-2)}
|
|
737
|
-
.ts-asking.markdown :is(p,ul,ol,blockquote,table,pre){margin:0 0 6px}
|
|
738
|
-
.ts-asking.markdown>:first-child{margin-top:0}
|
|
739
|
-
.ts-asking.markdown>:last-child{margin-bottom:0}
|
|
740
|
-
.ts-asking.markdown :is(ul,ol){padding-left:1.3em}
|
|
741
|
-
.ts-asking.markdown li>:is(ul,ol){margin:0}
|
|
742
|
-
.ts-asking.markdown strong{color:var(--text-bright)}
|
|
743
|
-
.ts-asking.markdown hr{border:0;border-top:1px solid var(--border);margin:8px 0}
|
|
744
|
-
.ts-asking.markdown blockquote{border-left:3px solid var(--border-strong);padding-left:10px;color:var(--text-muted)}
|
|
745
|
-
.ts-asking.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
746
|
-
.ts-asking.markdown :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:0 4px;color:var(--text)}
|
|
747
|
-
.ts-asking.markdown pre{overflow:auto;padding:6px 8px;border-radius:6px;background:var(--bg-inset);border:1px solid var(--border);font-size:12.5px;line-height:1.45;white-space:pre}
|
|
748
|
-
.ts-asking.markdown table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
|
|
749
|
-
.ts-asking.markdown :is(th,td){border:1px solid var(--border-strong);padding:3px 7px;font-size:13px}
|
|
750
|
-
.ts-asking.markdown th{background:var(--bg-inset);font-weight:600}
|
|
751
746
|
.turn-stage.asking .ts-headline{display:none}
|
|
752
747
|
@keyframes ts-fade{from{opacity:.35}}
|
|
753
748
|
.ts-now{height:var(--ts-now-h);display:flex;align-items:center;gap:8px;min-width:0;padding:0 9px;border-radius:8px;background:var(--accent-tint);font-size:13px}
|
|
@@ -1031,7 +1026,7 @@ body.no-glass :is(#familyPop,#chainPop){background:var(--bg-panel);-webkit-backd
|
|
|
1031
1026
|
.chain-chip .cc-arrow{display:inline-block;margin-right:4px}
|
|
1032
1027
|
.chain-chip .live-dot{display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--ok);margin-right:3px;vertical-align:1px}
|
|
1033
1028
|
.chain-chip .cc-waiting{font-family:system-ui,sans-serif;font-weight:600}
|
|
1034
|
-
.chain-row .cr-
|
|
1029
|
+
.chain-row .cr-n{font-family:ui-monospace,monospace;font-size:11px;color:var(--accent);white-space:nowrap}
|
|
1035
1030
|
.chain-row .cr-date{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
1036
1031
|
/* A row with its time (chainStamp): every row's date as wide, so the previews line up. */
|
|
1037
1032
|
.chain-rows:has(.cr-date.timed) .cr-date{min-width:16ch}
|
|
@@ -67,7 +67,8 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
67
67
|
const tokens = state.timing.tokens;
|
|
68
68
|
const ctxTip = ctxBarTip(tokens, state.timing.memoryIndex,
|
|
69
69
|
{ tokens: state.ctxWindow, source: state.ctxWindowSource, pct: state.ctxPct,
|
|
70
|
-
budget: state.llmContext?.budget_tokens }
|
|
70
|
+
budget: state.llmContext?.budget_tokens, afterSwitch: state.ctxUnknownAfterSwitch },
|
|
71
|
+
state.timing.modelSegments);
|
|
71
72
|
const speedTextNow = lastSpeedText(tokens);
|
|
72
73
|
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
73
74
|
const sessionDuration = sessionDurationNow(state.timing, running);
|
|
@@ -450,7 +450,7 @@ export function createQuestionCards({
|
|
|
450
450
|
state = "done";
|
|
451
451
|
// Sanitised on the server (POST /api/render).
|
|
452
452
|
full.innerHTML = html;
|
|
453
|
-
full.classList.add("markdown");
|
|
453
|
+
full.classList.add("markdown", "md-compact");
|
|
454
454
|
linkRefs(full);
|
|
455
455
|
});
|
|
456
456
|
};
|
|
@@ -31,6 +31,9 @@ export function emptySessionState() {
|
|
|
31
31
|
ctxWindow: null,
|
|
32
32
|
// Where it came from ("server", "config", … "default": chi's guess).
|
|
33
33
|
ctxWindowSource: null,
|
|
34
|
+
// A /model's new window didn't resolve and no request has started since
|
|
35
|
+
// (stream_handlers.js context_status): the ctx tooltip says it waits.
|
|
36
|
+
ctxUnknownAfterSwitch: false,
|
|
34
37
|
status: "",
|
|
35
38
|
model: "",
|
|
36
39
|
// What the worker's server said it served for a model name: [served, asked].
|
|
@@ -58,7 +58,7 @@ export function createStageSlots({
|
|
|
58
58
|
asking.hidden = !text;
|
|
59
59
|
if (shown.asking === text) return;
|
|
60
60
|
shown.asking = text;
|
|
61
|
-
asking.classList.remove("markdown");
|
|
61
|
+
asking.classList.remove("markdown", "md-compact");
|
|
62
62
|
asking.textContent = text;
|
|
63
63
|
asking.scrollTop = 0;
|
|
64
64
|
if (!text) return;
|
|
@@ -76,7 +76,7 @@ export function createStageSlots({
|
|
|
76
76
|
if (!html || shown.asking !== text || asking.classList.contains("markdown")) return;
|
|
77
77
|
// Sanitised on the server (POST /api/render), as an answer's HTML is.
|
|
78
78
|
asking.innerHTML = html;
|
|
79
|
-
asking.classList.add("markdown");
|
|
79
|
+
asking.classList.add("markdown", "md-compact");
|
|
80
80
|
asking.scrollTop = 0;
|
|
81
81
|
linkRefs(asking);
|
|
82
82
|
});
|
|
@@ -154,7 +154,7 @@ export function createStageSlots({
|
|
|
154
154
|
function reset() {
|
|
155
155
|
for (const slot of [headline, asking, now, trail, ticks]) slot.replaceChildren();
|
|
156
156
|
asking.hidden = true;
|
|
157
|
-
asking.classList.remove("markdown");
|
|
157
|
+
asking.classList.remove("markdown", "md-compact");
|
|
158
158
|
shown.headline = shown.now = shown.trail = shown.asking = null;
|
|
159
159
|
}
|
|
160
160
|
|
|
@@ -73,6 +73,9 @@ export function createStreamHandlers(deps) {
|
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
75
|
generation_started: (data) => {
|
|
76
|
+
// The request after a /model's unresolved window started: whatever it
|
|
77
|
+
// brings, the ctx tooltip no longer waits for it (ctxBarTip).
|
|
78
|
+
state().ctxUnknownAfterSwitch = false;
|
|
76
79
|
if (data?.context_window_tokens) {
|
|
77
80
|
const current = state();
|
|
78
81
|
current.ctxWindow = data.context_window_tokens;
|
|
@@ -91,6 +94,10 @@ export function createStreamHandlers(deps) {
|
|
|
91
94
|
if (data?.served_model) current.served = [data.served_model, data.requested_model, data.served_expected_by || null];
|
|
92
95
|
// The generation's speed and the session's token totals, from the server.
|
|
93
96
|
current.timing.tokens = generationTokens(current.timing.tokens, data);
|
|
97
|
+
// The rows per model, the server's whole list (the open one with this
|
|
98
|
+
// generation in); an event without them (a replay, an older worker)
|
|
99
|
+
// keeps the page's.
|
|
100
|
+
if (Array.isArray(data?.model_segments)) current.timing.modelSegments = data.model_segments;
|
|
94
101
|
if (data?.served_model || data?.tokens) updateInfoBar();
|
|
95
102
|
turnDom.generationCompleted();
|
|
96
103
|
},
|
|
@@ -185,6 +192,7 @@ export function createStreamHandlers(deps) {
|
|
|
185
192
|
// then show the prompt once more, marked failed (with why). This tab's own
|
|
186
193
|
// prompt goes back into an empty composer.
|
|
187
194
|
prompt_restored: async (data) => {
|
|
195
|
+
const id = selected();
|
|
188
196
|
keepEarlyRestore(data, earlyRestores, { myId: clientId, sentIds });
|
|
189
197
|
const { refill, label, images: restoredImages } = restoreAction(data, { myId: clientId, sentIds, composerEmpty: composer.empty() });
|
|
190
198
|
if (refill) composer.refill(refill);
|
|
@@ -192,6 +200,9 @@ export function createStreamHandlers(deps) {
|
|
|
192
200
|
const failedText = liveTurn.failedText;
|
|
193
201
|
liveTurn.failedText = null;
|
|
194
202
|
await resync();
|
|
203
|
+
// Another session was opened during the re-read: its history isn't
|
|
204
|
+
// this one's (the re-read drew nothing either).
|
|
205
|
+
if (selected() !== id) return;
|
|
195
206
|
// The re-render drew it from the session (failed_turn): drawn again
|
|
196
207
|
// below with this event's label and images.
|
|
197
208
|
const drawn = historyEl.querySelector(".bubble.user[data-failed-turn]");
|
|
@@ -207,6 +218,7 @@ export function createStreamHandlers(deps) {
|
|
|
207
218
|
// A session command ran in the worker (any client's). One that changed
|
|
208
219
|
// the conversation (!rollback, !cmd, a continue answered no) re-reads it.
|
|
209
220
|
command_ran: async (data) => {
|
|
221
|
+
const id = selected();
|
|
210
222
|
const view = commandView(data, clientId);
|
|
211
223
|
if (view.modelName && view.modelName !== state().model) {
|
|
212
224
|
state().model = view.modelName;
|
|
@@ -240,7 +252,11 @@ export function createStreamHandlers(deps) {
|
|
|
240
252
|
state().toolkit = data.toolkit || null;
|
|
241
253
|
updateInfoBar();
|
|
242
254
|
}
|
|
243
|
-
if (view.resync)
|
|
255
|
+
if (view.resync) {
|
|
256
|
+
await resync();
|
|
257
|
+
// Another session was opened during the re-read: the bubble isn't its.
|
|
258
|
+
if (selected() !== id) return;
|
|
259
|
+
}
|
|
244
260
|
// A card's action that isn't anytime (the step-limit question's
|
|
245
261
|
// /continue) has no bubble: nothing to remove. An anytime or queued
|
|
246
262
|
// one's bubble was drawn at its command_queued: filled in here. One
|
|
@@ -286,6 +302,21 @@ export function createStreamHandlers(deps) {
|
|
|
286
302
|
},
|
|
287
303
|
context_status: (data) => {
|
|
288
304
|
const current = state();
|
|
305
|
+
// After /model (Engine#announce_switched_context!): the new model's
|
|
306
|
+
// window, and usage null when it didn't resolve (the % clears, "ctx ?").
|
|
307
|
+
// Only that event carries the key: a null drops the old model's window,
|
|
308
|
+
// so the chunks before the next window count no % against it.
|
|
309
|
+
if (data && "context_window_tokens" in data) {
|
|
310
|
+
current.ctxWindow = data.context_window_tokens || null;
|
|
311
|
+
current.ctxWindowSource = data.context_window_tokens ? data.context_window_source || null : null;
|
|
312
|
+
// The ctx tooltip's "until the next request" (ctxBarTip).
|
|
313
|
+
current.ctxUnknownAfterSwitch = !data.context_window_tokens;
|
|
314
|
+
}
|
|
315
|
+
if (data && "usage" in data && data.usage === null) {
|
|
316
|
+
current.ctxPct = null;
|
|
317
|
+
updateInfoBar();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
289
320
|
const pct = extractCtxPct(data, current.ctxWindow, current.llmContext?.budget_tokens);
|
|
290
321
|
if (pct !== null) {
|
|
291
322
|
current.ctxPct = pct;
|
|
@@ -81,6 +81,10 @@ export function normalizeTiming(data = {}) {
|
|
|
81
81
|
// The memory indexes the session's prompt holds (SessionMetrics'
|
|
82
82
|
// memory_index), for the ctx tooltip.
|
|
83
83
|
memoryIndex: validRecord(data.memory_index) ? data.memory_index : null,
|
|
84
|
+
// The session's tokens, cost and speeds per model after a /model
|
|
85
|
+
// (SessionMetrics' model_segments; [] with one model), for the ctx
|
|
86
|
+
// tooltip.
|
|
87
|
+
modelSegments: Array.isArray(data.model_segments) ? data.model_segments.filter(validRecord) : [],
|
|
84
88
|
};
|
|
85
89
|
}
|
|
86
90
|
|
|
@@ -91,7 +95,7 @@ export function normalizeTiming(data = {}) {
|
|
|
91
95
|
// dates keep arrival order), an unknown tool record at the end (they come
|
|
92
96
|
// grouped by turn, in call order). startedAt, sessionDurationMs and
|
|
93
97
|
// activeTurn are the reply's, and so are its tokens (a reply without them
|
|
94
|
-
// keeps the page's). `short`: the reply's turn_count says the page
|
|
98
|
+
// keeps the page's) and its model_segments (present: authoritative). `short`: the reply's turn_count says the page
|
|
95
99
|
// misses a turn (it then reads the whole timing). A reply without
|
|
96
100
|
// turn_count (an older server: the whole timing) replaces, as before; more
|
|
97
101
|
// records than the count (a worker died before saving a turn the page saw)
|
|
@@ -117,7 +121,12 @@ export function mergeTiming(timing, data = {}) {
|
|
|
117
121
|
});
|
|
118
122
|
const tokens = fresh.tokens || timing?.tokens || null;
|
|
119
123
|
const memoryIndex = fresh.memoryIndex || timing?.memoryIndex || null;
|
|
120
|
-
|
|
124
|
+
// A reply that has the rows is the answer, an empty list too (one model
|
|
125
|
+
// now): a stale list never survives it; one without them (an older
|
|
126
|
+
// server) keeps the page's.
|
|
127
|
+
const modelSegments = data && "model_segments" in data ? fresh.modelSegments : (timing?.modelSegments || []);
|
|
128
|
+
return { timing: { ...fresh, turnRecords, toolRecords, tokens, memoryIndex, modelSegments },
|
|
129
|
+
short: turnRecords.length < count };
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
function replaceById(records, record) {
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.76.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -461,6 +461,8 @@ files:
|
|
|
461
461
|
- lib/samagotchi/session_inbox.rb
|
|
462
462
|
- lib/samagotchi/session_manager.rb
|
|
463
463
|
- lib/samagotchi/session_metrics.rb
|
|
464
|
+
- lib/samagotchi/session_metrics/model_segment.rb
|
|
465
|
+
- lib/samagotchi/session_metrics/totals.rb
|
|
464
466
|
- lib/samagotchi/session_model_settings.rb
|
|
465
467
|
- lib/samagotchi/session_observer.rb
|
|
466
468
|
- lib/samagotchi/session_retention.rb
|