samagotchi 0.75.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +72 -1
- data/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 +15 -4
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/memory.md +34 -0
- data/docs/plugins.md +43 -1
- data/docs/releasing.md +6 -2
- data/docs/sessions.md +3 -1
- data/lib/samagotchi/answer_command.rb +5 -5
- data/lib/samagotchi/bridge.rb +3 -3
- 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/source-links/hooks/source_links.rb +12 -10
- data/lib/samagotchi/bundles/source-links/manifest.yml +2 -2
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/child_reports.rb +1 -1
- data/lib/samagotchi/desktop/macos.rb +1 -2
- data/lib/samagotchi/effective_model.rb +5 -0
- data/lib/samagotchi/engine.rb +105 -16
- data/lib/samagotchi/guardrails/parent_approvals.rb +4 -4
- data/lib/samagotchi/guardrails/parent_continue.rb +2 -2
- 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 +13 -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/message_parts.rb +272 -0
- data/lib/samagotchi/model_price.rb +4 -3
- data/lib/samagotchi/parent_report.rb +30 -22
- data/lib/samagotchi/paths.rb +15 -1
- data/lib/samagotchi/pending_question.rb +49 -0
- data/lib/samagotchi/question_desk.rb +39 -32
- data/lib/samagotchi/relay.rb +8 -9
- data/lib/samagotchi/relay_verifier.rb +2 -2
- data/lib/samagotchi/relay_watcher.rb +2 -2
- data/lib/samagotchi/reply_wait.rb +4 -4
- data/lib/samagotchi/row_refs.rb +1 -1
- data/lib/samagotchi/session.rb +22 -14
- data/lib/samagotchi/session_commands.rb +5 -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/attach_launcher.rb +3 -2
- data/lib/samagotchi/terminal_ui/attached_loop.rb +2 -2
- data/lib/samagotchi/terminal_ui/formatting.rb +24 -10
- data/lib/samagotchi/terminal_ui/model_segments_text.rb +118 -0
- data/lib/samagotchi/terminal_ui/status_row.rb +3 -1
- data/lib/samagotchi/terminal_ui.rb +5 -3
- data/lib/samagotchi/tool_output_cut.rb +1 -1
- data/lib/samagotchi/tool_response.rb +13 -13
- data/lib/samagotchi/tool_row_fields.rb +1 -1
- data/lib/samagotchi/tool_runner.rb +16 -9
- 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/tools/delegate_relay.rb +7 -7
- data/lib/samagotchi/tools/delegate_wait.rb +1 -1
- data/lib/samagotchi/tools/web_fetch.rb +3 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +5 -2
- data/lib/samagotchi/web/public/app.js +21 -58
- 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 +84 -11
- data/lib/samagotchi/web/public/index.html +32 -36
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +53 -0
- 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 +38 -3
- data/lib/samagotchi/web/public/timing.js +11 -2
- data/lib/samagotchi/web/public/toasts.js +69 -0
- metadata +8 -2
- data/lib/samagotchi/web/message_parts.rb +0 -274
|
@@ -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,78 @@ 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
|
+
// A ref past +max+ characters with its middle cut to one "…":
|
|
212
|
+
// the tooltip is a native title, which grows as wide as its longest line.
|
|
213
|
+
// The host's start and the model's end stay; the model chip has the full ref.
|
|
214
|
+
const TIP_REF_MAX = 40;
|
|
215
|
+
export function shortRef(text, max = TIP_REF_MAX) {
|
|
216
|
+
const chars = [...text];
|
|
217
|
+
if (chars.length <= max) return text;
|
|
218
|
+
const head = Math.floor((max - 1) / 3);
|
|
219
|
+
return `${chars.slice(0, head).join("")}…${chars.slice(chars.length - (max - 1 - head)).join("")}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// The tooltip's rows per model, with ≥ 2 (one model: none): "model ·
|
|
223
|
+
// turns · in / out · cost", a switch's prefill under its row.
|
|
224
|
+
function modelSegmentLines(segments) {
|
|
225
|
+
if (!Array.isArray(segments) || segments.length < 2) return [];
|
|
226
|
+
const lines = ["per model:"];
|
|
227
|
+
for (const row of segments) {
|
|
228
|
+
const turns = num(row.turns) || 0;
|
|
229
|
+
const parts = [shortRef(modelSegmentLabel(row)), `${turns} ${turns === 1 ? "turn" : "turns"}`, modelSegmentTokensText(row)];
|
|
230
|
+
const cost = modelSegmentCostText(row);
|
|
231
|
+
if (cost) parts.push(cost);
|
|
232
|
+
lines.push(` ${parts.join(" · ")}`);
|
|
233
|
+
const prefill = modelSegmentPrefillText(row);
|
|
234
|
+
if (prefill) lines.push(` └ ${prefill}`);
|
|
235
|
+
}
|
|
236
|
+
return lines;
|
|
237
|
+
}
|
|
238
|
+
|
|
169
239
|
// The ctx tooltip (info bar and card): +title+, then the session's tokens
|
|
170
240
|
// (in summed over every request, cached with its share, out with the
|
|
171
241
|
// reasoning part) and its cost, when it has any (an estimated part with
|
|
172
242
|
// "~", costLine), and the memory indexes
|
|
173
243
|
// its prompt holds (+memoryIndex+, the snapshot's memory_index); with
|
|
174
|
-
// +window+ (ctxWindowText's) the window line under the title
|
|
175
|
-
//
|
|
176
|
-
|
|
244
|
+
// +window+ (ctxWindowText's) the window line under the title; with
|
|
245
|
+
// +segments+ (the timing's modelSegments, ≥ 2) the rows per model above the
|
|
246
|
+
// totals. A delegated session keeps its own metrics, so these are this
|
|
247
|
+
// session's only.
|
|
248
|
+
export function tokensTipText(tokens, title = "Context used after the last turn", memoryIndex = null, window = null,
|
|
249
|
+
segments = null) {
|
|
177
250
|
const memory = memoryIndexText(memoryIndex);
|
|
178
251
|
const memoryLine = memory ? `\nmemory index: ${memory}` : "";
|
|
179
252
|
const windowText = ctxWindowText(window);
|
|
@@ -186,7 +259,7 @@ export function tokensTipText(tokens, title = "Context used after the last turn"
|
|
|
186
259
|
const parts = [`in ${count(prompt)}`];
|
|
187
260
|
if (cached > 0 && prompt > 0) parts.push(`cached ${count(cached)} (${Math.round((cached * 100) / prompt)}%)`);
|
|
188
261
|
parts.push(`out ${count(completion)}${reasoning > 0 ? ` (reasoning ${count(reasoning)})` : ""}`);
|
|
189
|
-
const lines = [title, `tokens: ${parts.join(" · ")}`];
|
|
262
|
+
const lines = [title, ...modelSegmentLines(segments), `tokens: ${parts.join(" · ")}`];
|
|
190
263
|
const cost = costLine(tokens);
|
|
191
264
|
if (cost) lines.push(cost);
|
|
192
265
|
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}
|
|
@@ -1126,8 +1121,9 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
1126
1121
|
.card.archived:hover{opacity:.85}
|
|
1127
1122
|
.card .archived-badge,.family-row .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
1128
1123
|
/* bottom: above the session's composer card when one shows (--toast-bottom, app.js placeToast). */
|
|
1129
|
-
#toast{position:fixed;left:50%;bottom:var(--toast-bottom,24px);transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;
|
|
1124
|
+
#toast{position:fixed;left:50%;bottom:var(--toast-bottom,24px);transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;flex-direction:column;align-items:center;gap:8px}
|
|
1130
1125
|
#toast[hidden]{display:none}
|
|
1126
|
+
#toast .toast-item{display:flex;gap:12px;align-items:center;width:max-content;max-width:100%;box-sizing:border-box;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
1131
1127
|
#toast .toast-action{background:transparent;border:0;color:var(--accent);font-weight:600;cursor:pointer;padding:2px 4px}
|
|
1132
1128
|
#toast .toast-close{background:transparent;border:0;color:var(--text-muted);cursor:pointer;padding:2px 4px;margin-left:-6px}
|
|
1133
1129
|
#toast .toast-close:hover{color:var(--text)}
|
|
@@ -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);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Where the page's rendered markdown and its refs go (moved out of app.js):
|
|
2
|
+
// a text as its markdown HTML, the `path:line` refs in one rendered text,
|
|
3
|
+
// a final answer's rendering and refs, and the refs across the page. The
|
|
4
|
+
// page's state comes in as getters (capabilities, roots, currentDir), read
|
|
5
|
+
// at each call.
|
|
6
|
+
export function createMarkdownPlace({
|
|
7
|
+
capabilities, renderText, roots, touchedFiles, currentDir, linkRefs, applyFinalAnswer, findInTurnRoots, normalize,
|
|
8
|
+
pairAnswerRefs,
|
|
9
|
+
}) {
|
|
10
|
+
// A text the page already shows as its markdown HTML (POST /api/render,
|
|
11
|
+
// sanitised there), or null: markdown off here (no request), or a failed
|
|
12
|
+
// read (the raw text stays).
|
|
13
|
+
function renderMarkdown(text) {
|
|
14
|
+
if (capabilities().display !== true) return Promise.resolve(null);
|
|
15
|
+
return renderText(text)
|
|
16
|
+
.then((d) => (typeof d?.html === "string" && d.html ? d.html : null))
|
|
17
|
+
.catch(() => null);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The `path:line` refs in one rendered text (the stage's asking slot, a
|
|
21
|
+
// kept reply), as linkTextRefs links the answers'.
|
|
22
|
+
function linkRefsIn(el) {
|
|
23
|
+
linkRefs(el, touchedFiles(roots()), currentDir());
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The turn's answer as its rendered markdown and with its plugin refs,
|
|
27
|
+
// once it is final (the turn-end read, an answer_display re-read:
|
|
28
|
+
// answer_refs.js applyFinalAnswer); then the refs.
|
|
29
|
+
function applyFinalMarkdown(messages) {
|
|
30
|
+
applyFinalAnswer(messages, findInTurnRoots(".bubble.output"), normalize);
|
|
31
|
+
linkTextRefs();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The refs on the answers' and closed steps' texts, on this page: first
|
|
35
|
+
// each answer's own (a plugin's, answer_refs.js), then the `path:line`
|
|
36
|
+
// refs that name a file the session's rows (and cards) touched
|
|
37
|
+
// (ref_scan.js), which leave a plugin's alone. Not mid-stream: at a
|
|
38
|
+
// turn's end and after a reload's render. A ref already made stays as it
|
|
39
|
+
// is.
|
|
40
|
+
function linkTextRefs() {
|
|
41
|
+
const pageRoots = roots();
|
|
42
|
+
const touched = touchedFiles(pageRoots);
|
|
43
|
+
for (const root of pageRoots) {
|
|
44
|
+
for (const el of root.querySelectorAll(".bubble.output")) pairAnswerRefs(el);
|
|
45
|
+
}
|
|
46
|
+
if (!touched.size) return;
|
|
47
|
+
for (const root of pageRoots) {
|
|
48
|
+
for (const el of root.querySelectorAll(".bubble.output, .gen:not(.live) > .gen-text")) linkRefs(el, touched, currentDir());
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return { renderMarkdown, linkRefsIn, applyFinalMarkdown, linkTextRefs };
|
|
53
|
+
}
|
|
@@ -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,11 +73,18 @@ 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
|
+
const current = state();
|
|
79
|
+
let contextChanged = current.ctxUnknownAfterSwitch !== false;
|
|
80
|
+
current.ctxUnknownAfterSwitch = false;
|
|
76
81
|
if (data?.context_window_tokens) {
|
|
77
|
-
const
|
|
82
|
+
const source = data.context_window_source || null;
|
|
83
|
+
contextChanged ||= current.ctxWindow !== data.context_window_tokens || current.ctxWindowSource !== source;
|
|
78
84
|
current.ctxWindow = data.context_window_tokens;
|
|
79
|
-
current.ctxWindowSource =
|
|
85
|
+
current.ctxWindowSource = source;
|
|
80
86
|
}
|
|
87
|
+
if (contextChanged) updateInfoBar();
|
|
81
88
|
liveTurn.timing.setNote("");
|
|
82
89
|
// The step's clock (the composer's delivery line): the page's own at a
|
|
83
90
|
// live start; a replay's comes from the snapshot's generation age,
|
|
@@ -91,6 +98,10 @@ export function createStreamHandlers(deps) {
|
|
|
91
98
|
if (data?.served_model) current.served = [data.served_model, data.requested_model, data.served_expected_by || null];
|
|
92
99
|
// The generation's speed and the session's token totals, from the server.
|
|
93
100
|
current.timing.tokens = generationTokens(current.timing.tokens, data);
|
|
101
|
+
// The rows per model, the server's whole list (the open one with this
|
|
102
|
+
// generation in); an event without them (a replay, an older worker)
|
|
103
|
+
// keeps the page's.
|
|
104
|
+
if (Array.isArray(data?.model_segments)) current.timing.modelSegments = data.model_segments;
|
|
94
105
|
if (data?.served_model || data?.tokens) updateInfoBar();
|
|
95
106
|
turnDom.generationCompleted();
|
|
96
107
|
},
|
|
@@ -185,6 +196,7 @@ export function createStreamHandlers(deps) {
|
|
|
185
196
|
// then show the prompt once more, marked failed (with why). This tab's own
|
|
186
197
|
// prompt goes back into an empty composer.
|
|
187
198
|
prompt_restored: async (data) => {
|
|
199
|
+
const id = selected();
|
|
188
200
|
keepEarlyRestore(data, earlyRestores, { myId: clientId, sentIds });
|
|
189
201
|
const { refill, label, images: restoredImages } = restoreAction(data, { myId: clientId, sentIds, composerEmpty: composer.empty() });
|
|
190
202
|
if (refill) composer.refill(refill);
|
|
@@ -192,6 +204,9 @@ export function createStreamHandlers(deps) {
|
|
|
192
204
|
const failedText = liveTurn.failedText;
|
|
193
205
|
liveTurn.failedText = null;
|
|
194
206
|
await resync();
|
|
207
|
+
// Another session was opened during the re-read: its history isn't
|
|
208
|
+
// this one's (the re-read drew nothing either).
|
|
209
|
+
if (selected() !== id) return;
|
|
195
210
|
// The re-render drew it from the session (failed_turn): drawn again
|
|
196
211
|
// below with this event's label and images.
|
|
197
212
|
const drawn = historyEl.querySelector(".bubble.user[data-failed-turn]");
|
|
@@ -207,6 +222,7 @@ export function createStreamHandlers(deps) {
|
|
|
207
222
|
// A session command ran in the worker (any client's). One that changed
|
|
208
223
|
// the conversation (!rollback, !cmd, a continue answered no) re-reads it.
|
|
209
224
|
command_ran: async (data) => {
|
|
225
|
+
const id = selected();
|
|
210
226
|
const view = commandView(data, clientId);
|
|
211
227
|
if (view.modelName && view.modelName !== state().model) {
|
|
212
228
|
state().model = view.modelName;
|
|
@@ -240,7 +256,11 @@ export function createStreamHandlers(deps) {
|
|
|
240
256
|
state().toolkit = data.toolkit || null;
|
|
241
257
|
updateInfoBar();
|
|
242
258
|
}
|
|
243
|
-
if (view.resync)
|
|
259
|
+
if (view.resync) {
|
|
260
|
+
await resync();
|
|
261
|
+
// Another session was opened during the re-read: the bubble isn't its.
|
|
262
|
+
if (selected() !== id) return;
|
|
263
|
+
}
|
|
244
264
|
// A card's action that isn't anytime (the step-limit question's
|
|
245
265
|
// /continue) has no bubble: nothing to remove. An anytime or queued
|
|
246
266
|
// one's bubble was drawn at its command_queued: filled in here. One
|
|
@@ -286,6 +306,21 @@ export function createStreamHandlers(deps) {
|
|
|
286
306
|
},
|
|
287
307
|
context_status: (data) => {
|
|
288
308
|
const current = state();
|
|
309
|
+
// After /model (Engine#announce_switched_context!): the new model's
|
|
310
|
+
// window, and usage null when it didn't resolve (the % clears, "ctx ?").
|
|
311
|
+
// Only that event carries the key: a null drops the old model's window,
|
|
312
|
+
// so the chunks before the next window count no % against it.
|
|
313
|
+
if (data && "context_window_tokens" in data) {
|
|
314
|
+
current.ctxWindow = data.context_window_tokens || null;
|
|
315
|
+
current.ctxWindowSource = data.context_window_tokens ? data.context_window_source || null : null;
|
|
316
|
+
// The ctx tooltip's "until the next request" (ctxBarTip).
|
|
317
|
+
current.ctxUnknownAfterSwitch = !data.context_window_tokens;
|
|
318
|
+
}
|
|
319
|
+
if (data && "usage" in data && data.usage === null) {
|
|
320
|
+
current.ctxPct = null;
|
|
321
|
+
updateInfoBar();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
289
324
|
const pct = extractCtxPct(data, current.ctxWindow, current.llmContext?.budget_tokens);
|
|
290
325
|
if (pct !== null) {
|
|
291
326
|
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) {
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { html, nothing, render } from "./vendor/lit-html.js";
|
|
2
|
+
|
|
3
|
+
// The page's toasts (app.js showToast): a stack in #toast, newest last, at
|
|
4
|
+
// most MAX_TOASTS. Each item has its own 6 s timer, or none when sticky: a
|
|
5
|
+
// sticky one stays until its action, its ✕ or a link in it is clicked, so a
|
|
6
|
+
// later notice can't hide an error. +text+: words, or a template
|
|
7
|
+
// (chains.js openChildrenTemplate: a link in it opens that session and hides
|
|
8
|
+
// the toast). The same text again while it shows reuses its item, moved to
|
|
9
|
+
// the end with a fresh timer. When a 4th comes, the oldest non-sticky goes
|
|
10
|
+
// (the oldest of all if every one is sticky).
|
|
11
|
+
const TOAST_MS = 6000;
|
|
12
|
+
const MAX_TOASTS = 3;
|
|
13
|
+
|
|
14
|
+
export function createToasts({ el, onChange = () => {}, doc = globalThis.document }) {
|
|
15
|
+
const entries = [];
|
|
16
|
+
|
|
17
|
+
function sync() {
|
|
18
|
+
el.hidden = entries.length === 0;
|
|
19
|
+
onChange();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function remove(entry, notify = true) {
|
|
23
|
+
const index = entries.indexOf(entry);
|
|
24
|
+
if (index < 0) return;
|
|
25
|
+
if (entry.timer !== null) clearTimeout(entry.timer);
|
|
26
|
+
entry.timer = null;
|
|
27
|
+
entries.splice(index, 1);
|
|
28
|
+
entry.el.remove();
|
|
29
|
+
if (notify) sync();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function draw(entry, text, action, sticky) {
|
|
33
|
+
render(html`<span class="toast-message" @click=${(e) => { if (e.target.closest?.("a[href]")) remove(entry); }}>${text}</span>${action
|
|
34
|
+
? html`<button type="button" class="toast-action" @click=${() => { remove(entry); action.run(); }}>${action.label}</button>`
|
|
35
|
+
: nothing}${sticky ? html`<button type="button" class="toast-close" title="Dismiss" aria-label="Dismiss" @click=${() => remove(entry)}>✕</button>` : nothing}`,
|
|
36
|
+
entry.el);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function show(text, action = null, { sticky = false } = {}) {
|
|
40
|
+
const candidate = { el: doc.createElement("div"), timer: null, sticky, key: null };
|
|
41
|
+
candidate.el.className = "toast-item";
|
|
42
|
+
draw(candidate, text, action, sticky);
|
|
43
|
+
candidate.key = candidate.el.querySelector(".toast-message").textContent;
|
|
44
|
+
|
|
45
|
+
const duplicate = entries.find((entry) => entry.key === candidate.key);
|
|
46
|
+
if (duplicate) {
|
|
47
|
+
if (duplicate.timer !== null) clearTimeout(duplicate.timer);
|
|
48
|
+
duplicate.timer = null;
|
|
49
|
+
duplicate.sticky = sticky;
|
|
50
|
+
duplicate.key = candidate.key;
|
|
51
|
+
draw(duplicate, text, action, sticky);
|
|
52
|
+
entries.push(...entries.splice(entries.indexOf(duplicate), 1));
|
|
53
|
+
el.appendChild(duplicate.el);
|
|
54
|
+
if (!sticky) duplicate.timer = setTimeout(() => remove(duplicate), TOAST_MS);
|
|
55
|
+
sync();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (entries.length === MAX_TOASTS) {
|
|
60
|
+
remove(entries.find((entry) => !entry.sticky) || entries[0], false);
|
|
61
|
+
}
|
|
62
|
+
entries.push(candidate);
|
|
63
|
+
el.appendChild(candidate.el);
|
|
64
|
+
if (!sticky) candidate.timer = setTimeout(() => remove(candidate), TOAST_MS);
|
|
65
|
+
sync();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { show };
|
|
69
|
+
}
|
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.77.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -393,6 +393,7 @@ files:
|
|
|
393
393
|
- lib/samagotchi/memory_bundle/trash.rb
|
|
394
394
|
- lib/samagotchi/memory_bundle/uninstaller.rb
|
|
395
395
|
- lib/samagotchi/memory_paths.rb
|
|
396
|
+
- lib/samagotchi/message_parts.rb
|
|
396
397
|
- lib/samagotchi/model_catalog.rb
|
|
397
398
|
- lib/samagotchi/model_list_store.rb
|
|
398
399
|
- lib/samagotchi/model_match.rb
|
|
@@ -412,6 +413,7 @@ files:
|
|
|
412
413
|
- lib/samagotchi/paths.rb
|
|
413
414
|
- lib/samagotchi/peer_sources.rb
|
|
414
415
|
- lib/samagotchi/pending_input_queue.rb
|
|
416
|
+
- lib/samagotchi/pending_question.rb
|
|
415
417
|
- lib/samagotchi/plugin/api.rb
|
|
416
418
|
- lib/samagotchi/plugin/attached_context.rb
|
|
417
419
|
- lib/samagotchi/plugin/context.rb
|
|
@@ -461,6 +463,8 @@ files:
|
|
|
461
463
|
- lib/samagotchi/session_inbox.rb
|
|
462
464
|
- lib/samagotchi/session_manager.rb
|
|
463
465
|
- lib/samagotchi/session_metrics.rb
|
|
466
|
+
- lib/samagotchi/session_metrics/model_segment.rb
|
|
467
|
+
- lib/samagotchi/session_metrics/totals.rb
|
|
464
468
|
- lib/samagotchi/session_model_settings.rb
|
|
465
469
|
- lib/samagotchi/session_observer.rb
|
|
466
470
|
- lib/samagotchi/session_retention.rb
|
|
@@ -483,6 +487,7 @@ files:
|
|
|
483
487
|
- lib/samagotchi/terminal_ui/input_support.rb
|
|
484
488
|
- lib/samagotchi/terminal_ui/line_reader.rb
|
|
485
489
|
- lib/samagotchi/terminal_ui/live_region.rb
|
|
490
|
+
- lib/samagotchi/terminal_ui/model_segments_text.rb
|
|
486
491
|
- lib/samagotchi/terminal_ui/one_shot_sink.rb
|
|
487
492
|
- lib/samagotchi/terminal_ui/plain_surface.rb
|
|
488
493
|
- lib/samagotchi/terminal_ui/question_prompt.rb
|
|
@@ -571,7 +576,6 @@ files:
|
|
|
571
576
|
- lib/samagotchi/web/info_probe.rb
|
|
572
577
|
- lib/samagotchi/web/lan.rb
|
|
573
578
|
- lib/samagotchi/web/markdown_renderer.rb
|
|
574
|
-
- lib/samagotchi/web/message_parts.rb
|
|
575
579
|
- lib/samagotchi/web/public/activity.js
|
|
576
580
|
- lib/samagotchi/web/public/annotate_actions.js
|
|
577
581
|
- lib/samagotchi/web/public/annotate_bar.js
|
|
@@ -613,6 +617,7 @@ files:
|
|
|
613
617
|
- lib/samagotchi/web/public/live_timing.js
|
|
614
618
|
- lib/samagotchi/web/public/live_turn.js
|
|
615
619
|
- lib/samagotchi/web/public/llm_context_chip.js
|
|
620
|
+
- lib/samagotchi/web/public/markdown_place.js
|
|
616
621
|
- lib/samagotchi/web/public/model_pick.js
|
|
617
622
|
- lib/samagotchi/web/public/model_picker.js
|
|
618
623
|
- lib/samagotchi/web/public/notice_links.js
|
|
@@ -649,6 +654,7 @@ files:
|
|
|
649
654
|
- lib/samagotchi/web/public/thinking_ticker.js
|
|
650
655
|
- lib/samagotchi/web/public/timing.js
|
|
651
656
|
- lib/samagotchi/web/public/toast_place.js
|
|
657
|
+
- lib/samagotchi/web/public/toasts.js
|
|
652
658
|
- lib/samagotchi/web/public/toolkit_chip.js
|
|
653
659
|
- lib/samagotchi/web/public/turn_events.js
|
|
654
660
|
- lib/samagotchi/web/public/turn_html.js
|