samagotchi 0.71.0 → 0.73.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 +70 -1
- data/docs/architecture.md +15 -9
- data/docs/cli.md +12 -5
- data/docs/configuration.md +23 -13
- data/docs/hooks.md +5 -3
- data/docs/internals/anthropic.md +1 -1
- data/docs/internals/background-tasks.md +2 -2
- data/docs/internals/engine.md +38 -13
- data/docs/internals/llm-context-live.md +1 -1
- data/docs/internals/prompt-caching.md +6 -0
- data/docs/plugins.md +18 -7
- data/docs/sessions.md +5 -3
- data/docs/testing.md +10 -1
- data/lib/samagotchi/bootstrap/config_writer.rb +2 -3
- data/lib/samagotchi/bridge.rb +5 -2
- data/lib/samagotchi/broadcast/scope_card.rb +2 -2
- data/lib/samagotchi/bundles/claude-peer/manifest.yml +2 -7
- data/lib/samagotchi/bundles/claude-peer/plugin.rb +2 -2
- data/lib/samagotchi/bundles/codex-peer/manifest.yml +2 -6
- data/lib/samagotchi/bundles/codex-peer/plugin.rb +2 -2
- data/lib/samagotchi/bundles/coordinator/manifest.yml +8 -6
- data/lib/samagotchi/bundles/coordinator/plugin.rb +10 -0
- data/lib/samagotchi/bundles/coordinator/skill_coordinator.md +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/config.rb +7 -25
- data/lib/samagotchi/context_note.rb +2 -6
- data/lib/samagotchi/effective_model.rb +1 -1
- data/lib/samagotchi/engine.rb +91 -213
- data/lib/samagotchi/engine_peers.rb +2 -0
- data/lib/samagotchi/event_routes.rb +83 -0
- data/lib/samagotchi/hook_routes.rb +16 -37
- data/lib/samagotchi/host_config.rb +1 -1
- data/lib/samagotchi/host_keys.rb +0 -2
- data/lib/samagotchi/paths.rb +13 -0
- data/lib/samagotchi/plugin/api.rb +11 -5
- data/lib/samagotchi/plugin/peer.rb +5 -8
- data/lib/samagotchi/prompt_files.rb +2 -4
- data/lib/samagotchi/session.rb +7 -1
- data/lib/samagotchi/session_chain.rb +24 -0
- data/lib/samagotchi/session_continue.rb +3 -1
- data/lib/samagotchi/session_manager.rb +6 -16
- data/lib/samagotchi/session_model_settings.rb +227 -0
- data/lib/samagotchi/session_setup.rb +2 -1
- data/lib/samagotchi/system_prompt.rb +18 -13
- data/lib/samagotchi/terminal_ui/attached_loop.rb +30 -14
- data/lib/samagotchi/terminal_ui/formatting.rb +4 -0
- data/lib/samagotchi/tool_activity.rb +1 -0
- data/lib/samagotchi/tool_declarations.rb +4 -0
- data/lib/samagotchi/toolkit.rb +55 -24
- data/lib/samagotchi/toolkit_binding.rb +7 -5
- data/lib/samagotchi/toolkit_command.rb +28 -5
- data/lib/samagotchi/tools/builtins.rb +2 -1
- data/lib/samagotchi/tools/delegate.rb +32 -6
- data/lib/samagotchi/tools/execute.rb +1 -15
- data/lib/samagotchi/tools/list_sessions.rb +2 -2
- data/lib/samagotchi/tools/peers.rb +5 -2
- data/lib/samagotchi/tools/register_reminder.rb +1 -0
- data/lib/samagotchi/tools/registry.rb +1 -1
- data/lib/samagotchi/tools/task_runtime.rb +69 -6
- data/lib/samagotchi/tools/tool_index.rb +15 -4
- data/lib/samagotchi/turn_note.rb +4 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +40 -13
- data/lib/samagotchi/web/public/app.js +53 -28
- data/lib/samagotchi/web/public/bubbles.js +5 -3
- data/lib/samagotchi/web/public/cards_place.js +6 -10
- data/lib/samagotchi/web/public/chains.js +22 -2
- data/lib/samagotchi/web/public/chip_popover.js +3 -1
- data/lib/samagotchi/web/public/composer.js +13 -6
- data/lib/samagotchi/web/public/context_chips.js +25 -12
- data/lib/samagotchi/web/public/data.js +14 -1
- data/lib/samagotchi/web/public/format.js +5 -0
- data/lib/samagotchi/web/public/index.html +39 -2
- data/lib/samagotchi/web/public/notice_links.js +55 -0
- data/lib/samagotchi/web/public/question_cards.js +33 -1
- data/lib/samagotchi/web/public/sessions_list.js +2 -3
- data/lib/samagotchi/web/public/stage_frame.js +1 -1
- data/lib/samagotchi/web/public/stage_slots.js +35 -3
- data/lib/samagotchi/web/public/stage_view.js +17 -6
- data/lib/samagotchi/web/public/stream_handlers.js +6 -3
- data/lib/samagotchi/web/public/timing.js +1 -1
- data/lib/samagotchi/web/public/toolkit_chip.js +3 -1
- data/lib/samagotchi/web/public/turn_view.js +3 -2
- data/lib/samagotchi/web/session_hub.rb +8 -0
- metadata +4 -1
|
@@ -168,6 +168,11 @@ export function goneSessionNotice(id, why) {
|
|
|
168
168
|
return `Session ${String(id).slice(0, 8)} was ${why === "deleted" ? "deleted" : "not found"}.`;
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
// A failed action's toast: what failed and why (api()'s error says why).
|
|
172
|
+
export function cantText(what, error) {
|
|
173
|
+
return `Can't ${what}: ${String(error?.message ?? error)}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
// A session in the list after its live status changed (a turn started or
|
|
172
177
|
// ended): that is activity now, so its card's "N min ago" says so too, and
|
|
173
178
|
// the change came from a worker's stream, so the card says "live".
|
|
@@ -326,6 +326,22 @@ 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
|
+
/* Its markdown once the card is first opened (question_cards.js renderSaidOnOpen), as compact as the stage's. */
|
|
330
|
+
.question-said-full.markdown{white-space:normal}
|
|
331
|
+
.question-said-full.markdown :is(h1,h2,h3,h4,h5,h6){font-size:13.5px;font-weight:600;line-height:1.4;margin:8px 0 2px;color:var(--text-2)}
|
|
332
|
+
.question-said-full.markdown :is(p,ul,ol,blockquote,table,pre){margin:0 0 6px}
|
|
333
|
+
.question-said-full.markdown>:first-child{margin-top:0}
|
|
334
|
+
.question-said-full.markdown>:last-child{margin-bottom:0}
|
|
335
|
+
.question-said-full.markdown :is(ul,ol){padding-left:1.3em}
|
|
336
|
+
.question-said-full.markdown li>:is(ul,ol){margin:0}
|
|
337
|
+
.question-said-full.markdown hr{border:0;border-top:1px solid var(--border);margin:8px 0}
|
|
338
|
+
.question-said-full.markdown blockquote{border-left:3px solid var(--border-strong);padding-left:10px}
|
|
339
|
+
.question-said-full.markdown code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.88em}
|
|
340
|
+
.question-said-full.markdown :not(pre)>code{background:var(--bg-inset);border:1px solid var(--border);border-radius:4px;padding:0 4px}
|
|
341
|
+
.question-said-full.markdown pre{overflow:auto;padding:6px 8px;border-radius:6px;background:var(--bg-inset);border:1px solid var(--border);font-size:12px;line-height:1.45;white-space:pre}
|
|
342
|
+
.question-said-full.markdown table{border-collapse:collapse;display:block;max-width:100%;overflow:auto}
|
|
343
|
+
.question-said-full.markdown :is(th,td){border:1px solid var(--border-strong);padding:3px 7px;font-size:12.5px}
|
|
344
|
+
.question-said-full.markdown th{background:var(--bg-inset);font-weight:600}
|
|
329
345
|
.bubble.command{border-left:3px solid var(--text-faint);background:var(--bg-panel);font-size:13px}
|
|
330
346
|
.bubble.note{border-left:3px dotted var(--text-faint);background:transparent;font-size:12px;color:var(--text-muted);opacity:.85}
|
|
331
347
|
.note-line{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-faint);margin-bottom:2px}
|
|
@@ -438,6 +454,8 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
438
454
|
.gen .activity-body>.hook-notice:first-child{border-top:none}
|
|
439
455
|
.gen .hook-notice.warn{border-left-color:var(--warn);color:var(--warn-text)}
|
|
440
456
|
.hook-notice.llm-context{border-left-style:dashed;cursor:help}
|
|
457
|
+
.hook-notice a{color:inherit;text-decoration:underline;text-underline-offset:2px}
|
|
458
|
+
.hook-notice a:hover{text-decoration-thickness:2px}
|
|
441
459
|
/* A plugin's steer (a nudge into the running turn): a row of its step, or a bubble; its text collapsed. */
|
|
442
460
|
.steer-row{font-size:13px;color:var(--text-3);border-left:2px solid var(--accent-border)}
|
|
443
461
|
.steer-row>summary{cursor:pointer;list-style:none;font-style:italic}
|
|
@@ -708,9 +726,28 @@ body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-
|
|
|
708
726
|
.ts-headline.thinking{font-size:13.5px;font-style:italic;color:var(--text-muted)}
|
|
709
727
|
.ts-think-tag{font-style:normal;font-size:12.5px;color:var(--think-text);margin-right:6px}
|
|
710
728
|
.ts-headline.fresh{animation:ts-fade .3s ease}
|
|
711
|
-
/* While a question or approval waits: the whole reply that asked it, raw as a closed step's
|
|
712
|
-
|
|
729
|
+
/* While a question or approval waits: the whole reply that asked it, in the headline's place; raw as a closed step's
|
|
730
|
+
text, then its markdown when web.markdown is on (.markdown, stage_slots.js drawAsking); capped so the card keeps
|
|
731
|
+
its room, read from the top. */
|
|
713
732
|
.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}
|
|
714
751
|
.turn-stage.asking .ts-headline{display:none}
|
|
715
752
|
@keyframes ts-fade{from{opacity:.35}}
|
|
716
753
|
.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}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// A hook notice line's http(s) URLs as links (a source-links `sources:`
|
|
2
|
+
// line with markdown off): the terminals need the line as text, the web can
|
|
3
|
+
// click it. Only http(s) URLs that refs.js httpUrl accepts (no userinfo, not
|
|
4
|
+
// over-long) are linked; everything else stays text, escaped by lit (no
|
|
5
|
+
// unsafeHTML). A link opens in a new tab: refs.js installRefClicks leaves a
|
|
6
|
+
// plain <a> (no data-ref-kind) to the browser.
|
|
7
|
+
import { html } from "./vendor/lit-html.js";
|
|
8
|
+
import { httpUrl } from "./refs.js";
|
|
9
|
+
|
|
10
|
+
// A candidate: the scheme, then anything up to a space or an angle bracket
|
|
11
|
+
// (`<https://x/a>` links https://x/a).
|
|
12
|
+
const CANDIDATE = /https?:\/\/[^\s<>]+/gi;
|
|
13
|
+
const TRAILING = /[.,;:!?'"]+$/;
|
|
14
|
+
|
|
15
|
+
// +url+ cut at its first ) that no ( inside it balances: `(https://x/a)`
|
|
16
|
+
// ends before the ), `…/Foo_(bar)` keeps it (source_links.rb trim_url_end).
|
|
17
|
+
function cutUnbalancedParen(url) {
|
|
18
|
+
let depth = 0;
|
|
19
|
+
for (let i = 0; i < url.length; i++) {
|
|
20
|
+
if (url[i] === "(") depth++;
|
|
21
|
+
else if (url[i] === ")") {
|
|
22
|
+
if (depth === 0) return url.slice(0, i);
|
|
23
|
+
depth--;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return url;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A candidate without what ends the sentence around it: trailing
|
|
30
|
+
// punctuation (the sources line's ", "), an unbalanced ), a ] no [ opens.
|
|
31
|
+
function trimUrl(candidate) {
|
|
32
|
+
let url = cutUnbalancedParen(candidate.replace(TRAILING, ""));
|
|
33
|
+
if (url.endsWith("]") && !url.includes("[")) url = url.replace(/\]+$/, "");
|
|
34
|
+
return url.replace(TRAILING, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// +text+ with each http(s) URL a link (a lit template's array of strings
|
|
38
|
+
// and <a> parts); +text+ itself when it links nothing, so a URL-less line
|
|
39
|
+
// renders exactly as before.
|
|
40
|
+
export function linkedLine(text) {
|
|
41
|
+
if (typeof text !== "string" || !text) return text;
|
|
42
|
+
const parts = [];
|
|
43
|
+
let last = 0;
|
|
44
|
+
for (const match of text.matchAll(CANDIDATE)) {
|
|
45
|
+
const shown = trimUrl(match[0]);
|
|
46
|
+
const href = httpUrl(shown);
|
|
47
|
+
if (!href) continue;
|
|
48
|
+
if (match.index > last) parts.push(text.slice(last, match.index));
|
|
49
|
+
parts.push(html`<a href=${href} target="_blank" rel="noopener noreferrer">${shown}</a>`);
|
|
50
|
+
last = match.index + shown.length;
|
|
51
|
+
}
|
|
52
|
+
if (!parts.length) return text;
|
|
53
|
+
if (last < text.length) parts.push(text.slice(last));
|
|
54
|
+
return parts;
|
|
55
|
+
}
|
|
@@ -31,6 +31,9 @@ import { continueLine } from "./commands.js";
|
|
|
31
31
|
// @param removeHintIfEmpty () drops the history's placeholder hint
|
|
32
32
|
// @param askingText () => the running turn's asking reply (turn_model.js
|
|
33
33
|
// askingText via the page's view), kept by a question or own approval card
|
|
34
|
+
// @param renderMarkdown (text) => Promise<html|null>: the kept reply's
|
|
35
|
+
// markdown (the page's POST /api/render; null keeps the raw text)
|
|
36
|
+
// @param linkRefs (el) the page's path:line ref pass on it
|
|
34
37
|
// @param api {sendAnswer, dismissQuestion, sendCommand} (data.js)
|
|
35
38
|
// @param doc the page's document
|
|
36
39
|
// @return {renderQuestion(pq, {said}), resolveQuestion(id, opts), removeQuestion(),
|
|
@@ -38,6 +41,7 @@ import { continueLine } from "./commands.js";
|
|
|
38
41
|
// renderContinue(offer), resolveContinue(data), forgetContinue()}
|
|
39
42
|
export function createQuestionCards({
|
|
40
43
|
sessionId, clientId, place, isNearBottom, scrollToEnd, removeHintIfEmpty, askingText = () => "",
|
|
44
|
+
renderMarkdown = () => Promise.resolve(null), linkRefs = () => {},
|
|
41
45
|
api = { sendAnswer, dismissQuestion, sendCommand }, doc = globalThis.document,
|
|
42
46
|
}) {
|
|
43
47
|
// Live ask_user_question card, keyed by pending question id. Re-rendered from
|
|
@@ -403,13 +407,14 @@ export function createQuestionCards({
|
|
|
403
407
|
// the toggle's label), and whole, selectable, at the top of the body
|
|
404
408
|
// when it is open (a drag in the summary would toggle the card).
|
|
405
409
|
const asked = saidOf.get(questionEl);
|
|
410
|
+
let full = null;
|
|
406
411
|
if (summary && asked) {
|
|
407
412
|
const line = doc.createElement("div");
|
|
408
413
|
line.className = "question-said";
|
|
409
414
|
line.setAttribute("aria-hidden", "true");
|
|
410
415
|
line.textContent = asked;
|
|
411
416
|
summary.appendChild(line);
|
|
412
|
-
|
|
417
|
+
full = doc.createElement("div");
|
|
413
418
|
full.className = "question-said-full";
|
|
414
419
|
full.textContent = asked;
|
|
415
420
|
questionEl.insertBefore(full, summary.nextSibling);
|
|
@@ -421,9 +426,36 @@ export function createQuestionCards({
|
|
|
421
426
|
const keepNote = questionEl.dataset.toggled || (!cancelled && deniedReason(pending, answer));
|
|
422
427
|
if (summary && !keepNote) note.classList.add("hidden");
|
|
423
428
|
if (!questionEl.dataset.toggled) questionEl.open = false;
|
|
429
|
+
// After the collapse: a card left open by hand renders at once.
|
|
430
|
+
if (full) renderSaidOnOpen(questionEl, full, asked);
|
|
424
431
|
pending = null;
|
|
425
432
|
}
|
|
426
433
|
|
|
434
|
+
// The body's copy as markdown, from the card's first open on (a reload
|
|
435
|
+
// draws every card in the history: rendering them all would ask once per
|
|
436
|
+
// card). Raw until then, and when the render answers null (then the next
|
|
437
|
+
// open asks again).
|
|
438
|
+
function renderSaidOnOpen(card, full, asked) {
|
|
439
|
+
let state = "raw";
|
|
440
|
+
const draw = () => {
|
|
441
|
+
if (!card.open || state !== "raw") return;
|
|
442
|
+
state = "asking";
|
|
443
|
+
Promise.resolve()
|
|
444
|
+
.then(() => renderMarkdown(asked))
|
|
445
|
+
.catch(() => null)
|
|
446
|
+
.then((html) => {
|
|
447
|
+
if (!html) { state = "raw"; return; }
|
|
448
|
+
state = "done";
|
|
449
|
+
// Sanitised on the server (POST /api/render).
|
|
450
|
+
full.innerHTML = html;
|
|
451
|
+
full.classList.add("markdown");
|
|
452
|
+
linkRefs(full);
|
|
453
|
+
});
|
|
454
|
+
};
|
|
455
|
+
card.addEventListener("toggle", draw);
|
|
456
|
+
draw();
|
|
457
|
+
}
|
|
458
|
+
|
|
427
459
|
function markQuestionSelection(sel) {
|
|
428
460
|
if (!questionEl || !sel.length) return;
|
|
429
461
|
questionEl.querySelectorAll(".question-choice").forEach((button) => {
|
|
@@ -264,10 +264,9 @@ export function batchTargets(picked, shownIds, archive, sessionsById) {
|
|
|
264
264
|
});
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
-
// A refusal's reason as the card shows it: the server's words
|
|
268
|
-
// the api() status suffix.
|
|
267
|
+
// A refusal's reason as the card shows it: the server's words.
|
|
269
268
|
function refusalText(error) {
|
|
270
|
-
return String(error?.message || "refused")
|
|
269
|
+
return String(error?.message || "refused");
|
|
271
270
|
}
|
|
272
271
|
|
|
273
272
|
// Runs +act+ over +ids+ one at a time (a live session's stop waits on the
|
|
@@ -13,7 +13,7 @@ import { easeScrollTo } from "./scroll.js";
|
|
|
13
13
|
const GROW_MS = 180;
|
|
14
14
|
const GROW_EASE = "cubic-bezier(.2,.7,.3,1)";
|
|
15
15
|
// The room between the stage and the composer card (index.html's margin-bottom).
|
|
16
|
-
const STAGE_GAP = 10;
|
|
16
|
+
export const STAGE_GAP = 10;
|
|
17
17
|
|
|
18
18
|
export function reducedMotion() {
|
|
19
19
|
return matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
@@ -19,10 +19,19 @@ export function node(tag, cls, parent = null) {
|
|
|
19
19
|
// +taskStop+: the page's stopper (task_stop.js), for the stop-task button
|
|
20
20
|
// on a running task_wait; +gens()+ the running turn's gens (the button's
|
|
21
21
|
// command, read at its click); +onChange+ redraws (the stage's
|
|
22
|
-
// scheduleRefresh).
|
|
23
|
-
|
|
22
|
+
// scheduleRefresh). +renderMarkdown(text)+: the asking text's HTML
|
|
23
|
+
// (Promise<string|null>; null keeps the raw text: markdown off, a failed
|
|
24
|
+
// read); +linkRefs(el)+: the page's path:line ref pass on the slot.
|
|
25
|
+
export function createStageSlots({
|
|
26
|
+
headline, asking, now, trail, ticks, taskStop = null, gens, onChange,
|
|
27
|
+
renderMarkdown = () => Promise.resolve(null), linkRefs = () => {},
|
|
28
|
+
}) {
|
|
24
29
|
// What each slot shows, so an unchanged slot is left alone.
|
|
25
30
|
const shown = { headline: null, now: null, trail: null, asking: null };
|
|
31
|
+
// The last asking text sent to render and its answer (in flight or
|
|
32
|
+
// done, HTML only): the same question drawn again (after a reset, a
|
|
33
|
+
// blank between) asks no second time.
|
|
34
|
+
let rendering = { text: null, html: null };
|
|
26
35
|
|
|
27
36
|
function drawHeadline({ text, thinking }) {
|
|
28
37
|
const key = `${thinking ? "t" : "n"}|${text}`;
|
|
@@ -42,13 +51,35 @@ export function createStageSlots({ headline, asking, now, trail, ticks, taskStop
|
|
|
42
51
|
|
|
43
52
|
// Patched only when the text changes (a late chunk to the closed gen
|
|
44
53
|
// rewrites the same text): a selection in it survives. A new text reads
|
|
45
|
-
// from its top
|
|
54
|
+
// from its top: drawn raw at once, then swapped for its markdown when
|
|
55
|
+
// that comes back while the slot still shows the same text (the text is
|
|
56
|
+
// final: it shows only once the call that asks runs).
|
|
46
57
|
function drawAsking(text) {
|
|
47
58
|
asking.hidden = !text;
|
|
48
59
|
if (shown.asking === text) return;
|
|
49
60
|
shown.asking = text;
|
|
61
|
+
asking.classList.remove("markdown");
|
|
50
62
|
asking.textContent = text;
|
|
51
63
|
asking.scrollTop = 0;
|
|
64
|
+
if (!text) return;
|
|
65
|
+
linkRefs(asking);
|
|
66
|
+
if (rendering.text !== text) {
|
|
67
|
+
let html;
|
|
68
|
+
try { html = Promise.resolve(renderMarkdown(text)).catch(() => null); } catch (_) { html = Promise.resolve(null); }
|
|
69
|
+
rendering = { text, html };
|
|
70
|
+
}
|
|
71
|
+
const pending = rendering;
|
|
72
|
+
pending.html.then((html) => {
|
|
73
|
+
// A null isn't kept: the next draw of this text asks again.
|
|
74
|
+
if (!html && rendering === pending) rendering = { text: null, html: null };
|
|
75
|
+
// Swapped once: a blank-then-same draw while in flight waits twice.
|
|
76
|
+
if (!html || shown.asking !== text || asking.classList.contains("markdown")) return;
|
|
77
|
+
// Sanitised on the server (POST /api/render), as an answer's HTML is.
|
|
78
|
+
asking.innerHTML = html;
|
|
79
|
+
asking.classList.add("markdown");
|
|
80
|
+
asking.scrollTop = 0;
|
|
81
|
+
linkRefs(asking);
|
|
82
|
+
});
|
|
52
83
|
}
|
|
53
84
|
|
|
54
85
|
function drawNow(tool, quiet) {
|
|
@@ -123,6 +154,7 @@ export function createStageSlots({ headline, asking, now, trail, ticks, taskStop
|
|
|
123
154
|
function reset() {
|
|
124
155
|
for (const slot of [headline, asking, now, trail, ticks]) slot.replaceChildren();
|
|
125
156
|
asking.hidden = true;
|
|
157
|
+
asking.classList.remove("markdown");
|
|
126
158
|
shown.headline = shown.now = shown.trail = shown.asking = null;
|
|
127
159
|
}
|
|
128
160
|
|
|
@@ -48,8 +48,12 @@ function readCollapsed() {
|
|
|
48
48
|
// the history only for a reader at its end). +dockEl+, +centerEl+ and
|
|
49
49
|
// +composerEl+ size the stage's cap.
|
|
50
50
|
// +taskStop+: the page's stopper (task_stop.js), for the stop-task button
|
|
51
|
-
// on a running task_wait.
|
|
52
|
-
|
|
51
|
+
// on a running task_wait. +renderMarkdown+ and +linkRefs+: the asking
|
|
52
|
+
// slot's markdown read and ref pass (stage_slots.js).
|
|
53
|
+
export function createStageView({
|
|
54
|
+
historyEl, appendToHistory, isNearBottom: historyNearBottom, sawText, sessionId, dockEl, centerEl, composerEl, taskStop = null,
|
|
55
|
+
renderMarkdown, linkRefs,
|
|
56
|
+
}) {
|
|
53
57
|
// The dock becomes a clear column: the stage's card, then the composer's
|
|
54
58
|
// (its rows moved into .dock-card, which wears the glass and the focus ring).
|
|
55
59
|
document.body.classList.add("stage-view");
|
|
@@ -82,7 +86,8 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
82
86
|
headline.setAttribute("role", "button");
|
|
83
87
|
headline.title = "Show this step's reasoning";
|
|
84
88
|
// While a question or approval waits: the whole reply that asked it, in
|
|
85
|
-
// the headline's place (the card says what to answer, this says why)
|
|
89
|
+
// the headline's place (the card says what to answer, this says why),
|
|
90
|
+
// as markdown when web.markdown is on.
|
|
86
91
|
const asking = node("div", "ts-asking", live);
|
|
87
92
|
asking.hidden = true;
|
|
88
93
|
const now = node("div", "ts-now", live);
|
|
@@ -111,7 +116,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
111
116
|
// "queued"), each to run as a turn of its own; they hand off last.
|
|
112
117
|
const queuedSlot = node("div", "ts-queued", stage);
|
|
113
118
|
const slotsView = createStageSlots({
|
|
114
|
-
headline, asking, now, trail, ticks, taskStop,
|
|
119
|
+
headline, asking, now, trail, ticks, taskStop, renderMarkdown, linkRefs,
|
|
115
120
|
gens: () => view.current().turn?.gens,
|
|
116
121
|
onChange: () => scheduleRefresh(),
|
|
117
122
|
});
|
|
@@ -127,6 +132,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
127
132
|
let endedAt = 0;
|
|
128
133
|
let thinkingLine = null; // { step, text } from the live step's ticker
|
|
129
134
|
let flash = null; // { kind, text, until } a notice or nudge in the trail
|
|
135
|
+
let flashTimer = null; // its end's redraw
|
|
130
136
|
let collapsed = readCollapsed();
|
|
131
137
|
let cloudOpen = false;
|
|
132
138
|
// A ref's "show in turn" opened a collapsed stage (not kept).
|
|
@@ -252,7 +258,8 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
252
258
|
if (!what) return;
|
|
253
259
|
flash = { ...what, until: Date.now() + FLASH_MS };
|
|
254
260
|
scheduleRefresh();
|
|
255
|
-
|
|
261
|
+
clearTimeout(flashTimer);
|
|
262
|
+
flashTimer = setTimeout(scheduleRefresh, FLASH_MS + 50);
|
|
256
263
|
}
|
|
257
264
|
|
|
258
265
|
// The answer reads from its top (no tail pin in the stage): scrolled to
|
|
@@ -408,6 +415,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
408
415
|
promptSlot.classList.remove("full");
|
|
409
416
|
thinkingLine = null;
|
|
410
417
|
flash = null;
|
|
418
|
+
clearTimeout(flashTimer);
|
|
411
419
|
setCloud(false);
|
|
412
420
|
stage.classList.remove("plain", "ended", "waiting", "held-wait", "asking", "in-use");
|
|
413
421
|
stage.style.removeProperty("--live-held");
|
|
@@ -590,8 +598,11 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
|
|
|
590
598
|
// composer's delivery line).
|
|
591
599
|
current: () => view.current(),
|
|
592
600
|
phase: () => liveSlots(view.current().turn, { pending: pendingCard() || asksCard() }).phase,
|
|
601
|
+
// Another session opens, or this one renders again: a turn still
|
|
602
|
+
// sliding away is dropped (its history is about to be drawn anew),
|
|
603
|
+
// never moved into whatever history shows now.
|
|
593
604
|
reset() {
|
|
594
|
-
|
|
605
|
+
folding = null;
|
|
595
606
|
release();
|
|
596
607
|
},
|
|
597
608
|
// The stage's extras while it holds a turn (a card, a prompt for its
|
|
@@ -150,12 +150,15 @@ export function createStreamHandlers(deps) {
|
|
|
150
150
|
// An after_turn hook presented the answer (AnswerDisplay) or attached
|
|
151
151
|
// refs to it (AnswerRefs: refs, a count; display null when only refs):
|
|
152
152
|
// it comes after turn_completed, so re-read the answer once that read
|
|
153
|
-
// is done
|
|
153
|
+
// is done: the session's open then, so a switch meanwhile leaves the
|
|
154
|
+
// other session's answers alone.
|
|
154
155
|
answer_display: async (data) => {
|
|
156
|
+
const id = selected();
|
|
155
157
|
try {
|
|
156
158
|
await liveTurn.endRead;
|
|
157
|
-
if (!selected() || (!data.display && !data.refs)) return;
|
|
158
|
-
const d = await getSession(
|
|
159
|
+
if (!id || id !== selected() || (!data.display && !data.refs)) return;
|
|
160
|
+
const d = await getSession(id, { tail: true, recent: true });
|
|
161
|
+
if (id !== selected()) return;
|
|
159
162
|
if (Array.isArray(d.messages)) applyFinalMarkdown(d.messages);
|
|
160
163
|
} catch (_) {
|
|
161
164
|
} finally {
|
|
@@ -278,7 +278,7 @@ function noMatch(record) {
|
|
|
278
278
|
// every assistant message is one step. Its calls take the turn's records by
|
|
279
279
|
// tool, in call order (each the next record of its tool, consumed with the
|
|
280
280
|
// ones it skipped), not by iteration: an empty retry spends an iteration
|
|
281
|
-
// that left no message (as the TUI's join, attached_loop.rb
|
|
281
|
+
// that left no message (as the TUI's join, attached_loop.rb join_record).
|
|
282
282
|
// The last message is the answer when it has text and made no call (and the
|
|
283
283
|
// turn is no canceled multi-step one); its thinking stays behind as a step,
|
|
284
284
|
// as it does live.
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// worker's snapshot or a command_ran; the server's own reading of the
|
|
6
6
|
// session file has no lists). Left out is reachable (find_tools and
|
|
7
7
|
// call_tool reach them) and removed (only /toolkit add brings one back).
|
|
8
|
-
// Changing it is /toolkit (no popover yet).
|
|
8
|
+
// Changing it is /toolkit (no popover yet). A kit with a prompt text has
|
|
9
|
+
// it on the summary too (prompt), and the tooltip ends with it.
|
|
9
10
|
|
|
10
11
|
import { html, nothing } from "./vendor/lit-html.js";
|
|
11
12
|
|
|
@@ -24,6 +25,7 @@ export function chipModel(summary) {
|
|
|
24
25
|
parts.push(summary.left_out.length ? `Not loaded: ${summary.left_out.join(", ")}.` : "Nothing left out.");
|
|
25
26
|
}
|
|
26
27
|
parts.push("/toolkit lists them; /toolkit NAME, add TOOL or remove TOOL changes them from the next turn.");
|
|
28
|
+
if (summary.prompt) parts.push(`Prompt: ${summary.prompt}`);
|
|
27
29
|
return { text: `tools ${label}${summary.fallback ? " ?" : ""}`, title: parts.join(" ") };
|
|
28
30
|
}
|
|
29
31
|
|
|
@@ -37,6 +37,7 @@ import { createSentenceFeed } from "./sentences.js";
|
|
|
37
37
|
import { createTicker } from "./thinking_ticker.js";
|
|
38
38
|
import * as model from "./turn_model.js";
|
|
39
39
|
import { createHold } from "./hold.js";
|
|
40
|
+
import { linkedLine } from "./notice_links.js";
|
|
40
41
|
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
41
42
|
|
|
42
43
|
// ---- The templates: one step, its thinking, its label ----
|
|
@@ -99,10 +100,10 @@ export function stepTemplate(gen, st = {}) {
|
|
|
99
100
|
|
|
100
101
|
// A hook's line (hook_notice) as a step's row. data.line is a whole row of
|
|
101
102
|
// chi's own (an empty-answer retry), shown as is; data.cls a class of its
|
|
102
|
-
// own and data.title its hover (a ✂ row's).
|
|
103
|
+
// own and data.title its hover (a ✂ row's). Its http(s) URLs are links.
|
|
103
104
|
function noticeTemplate(data) {
|
|
104
105
|
const cls = "hook-notice" + (data.level === "warn" ? " warn" : "") + (data.cls ? ` ${data.cls}` : "");
|
|
105
|
-
return html`<div class=${cls} title=${data.title || nothing}>${data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`}</div>`;
|
|
106
|
+
return html`<div class=${cls} title=${data.title || nothing}>${linkedLine(data.line || `${hookNoticeLabel(data.hook)}: ${data.text || ""}`)}</div>`;
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
// +template+ rendered as detached nodes (a fragment).
|
|
@@ -172,6 +172,14 @@ module Samagotchi
|
|
|
172
172
|
end
|
|
173
173
|
end
|
|
174
174
|
|
|
175
|
+
# A session's folder as the projection holds it (its file's, read at
|
|
176
|
+
# the last scan or touch): what a live read that loads no session
|
|
177
|
+
# file checks refs against. nil for a session the hub hasn't seen.
|
|
178
|
+
# @return [String, nil]
|
|
179
|
+
def working_directory(id)
|
|
180
|
+
@monitor.synchronize { @sessions[id]&.session&.working_directory }
|
|
181
|
+
end
|
|
182
|
+
|
|
175
183
|
# Register a sink (#call(event)). With +snapshot:+ the current
|
|
176
184
|
# projection comes back with the handle, taken in the same step, so
|
|
177
185
|
# every event after it is newer than it.
|
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.73.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -279,6 +279,7 @@ files:
|
|
|
279
279
|
- lib/samagotchi/empty_answer_retry.rb
|
|
280
280
|
- lib/samagotchi/engine.rb
|
|
281
281
|
- lib/samagotchi/engine_peers.rb
|
|
282
|
+
- lib/samagotchi/event_routes.rb
|
|
282
283
|
- lib/samagotchi/events.rb
|
|
283
284
|
- lib/samagotchi/extension_load.rb
|
|
284
285
|
- lib/samagotchi/file_ref.rb
|
|
@@ -459,6 +460,7 @@ files:
|
|
|
459
460
|
- lib/samagotchi/session_inbox.rb
|
|
460
461
|
- lib/samagotchi/session_manager.rb
|
|
461
462
|
- lib/samagotchi/session_metrics.rb
|
|
463
|
+
- lib/samagotchi/session_model_settings.rb
|
|
462
464
|
- lib/samagotchi/session_observer.rb
|
|
463
465
|
- lib/samagotchi/session_retention.rb
|
|
464
466
|
- lib/samagotchi/session_setting.rb
|
|
@@ -612,6 +614,7 @@ files:
|
|
|
612
614
|
- lib/samagotchi/web/public/llm_context_chip.js
|
|
613
615
|
- lib/samagotchi/web/public/model_pick.js
|
|
614
616
|
- lib/samagotchi/web/public/model_picker.js
|
|
617
|
+
- lib/samagotchi/web/public/notice_links.js
|
|
615
618
|
- lib/samagotchi/web/public/notifications.js
|
|
616
619
|
- lib/samagotchi/web/public/notify.js
|
|
617
620
|
- lib/samagotchi/web/public/prompt_history.js
|