samagotchi 0.100.0 → 0.101.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 +39 -1
- data/bin/chi +1 -0
- data/docs/architecture.md +10 -1
- data/docs/cli.md +55 -0
- data/docs/configuration.md +1 -0
- data/docs/hooks.md +2 -0
- data/docs/internals/engine.md +1 -0
- data/docs/internals/working-set.md +130 -0
- data/docs/sessions.md +1 -1
- data/docs/testing.md +1 -1
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/bundles/work-cap/manifest.yml +2 -2
- data/lib/samagotchi/bundles/work-cap/plugin.rb +26 -16
- data/lib/samagotchi/config.rb +3 -0
- data/lib/samagotchi/engine.rb +30 -0
- data/lib/samagotchi/hooks/loader.rb +17 -2
- data/lib/samagotchi/host_keys.rb +35 -18
- data/lib/samagotchi/testing/fake_executable.rb +6 -16
- data/lib/samagotchi/testing/fake_executable_launcher +6 -0
- data/lib/samagotchi/tool_response.rb +10 -6
- data/lib/samagotchi/tool_runner.rb +25 -6
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +49 -1
- data/lib/samagotchi/web/public/app.js +77 -11
- data/lib/samagotchi/web/public/chains.js +24 -0
- data/lib/samagotchi/web/public/chip_popover.js +56 -17
- data/lib/samagotchi/web/public/data.js +6 -0
- data/lib/samagotchi/web/public/index.html +134 -2
- data/lib/samagotchi/web/public/info_bar.js +16 -4
- data/lib/samagotchi/web/public/info_bar_model.js +7 -3
- data/lib/samagotchi/web/public/markdown_place.js +47 -1
- data/lib/samagotchi/web/public/refs.js +10 -1
- data/lib/samagotchi/web/public/send_turn.js +6 -2
- data/lib/samagotchi/web/public/session_actions.js +10 -5
- data/lib/samagotchi/web/public/stage_model.js +6 -5
- data/lib/samagotchi/web/public/stage_view.js +34 -2
- data/lib/samagotchi/web/public/stream_handlers.js +6 -1
- data/lib/samagotchi/web/public/turn_view.js +37 -11
- data/lib/samagotchi/web/public/working_set.js +520 -0
- data/lib/samagotchi/web/public/working_set_rail.js +278 -0
- data/lib/samagotchi/web/public/working_set_view.js +426 -0
- data/lib/samagotchi/web/server.rb +6 -4
- data/lib/samagotchi/working_set.rb +200 -0
- metadata +7 -1
|
@@ -23,6 +23,8 @@ import { createInUseWatch } from "./stage_watch.js";
|
|
|
23
23
|
import { formatDuration } from "./timing.js";
|
|
24
24
|
import { easeScrollTo } from "./scroll.js";
|
|
25
25
|
import { readStored, writeStored } from "./storage.js";
|
|
26
|
+
import { createWorkingSetRail } from "./working_set_rail.js";
|
|
27
|
+
import { stampRows } from "./working_set_view.js";
|
|
26
28
|
|
|
27
29
|
const COLLAPSE_KEY = "chi_stage_collapsed";
|
|
28
30
|
// The turn-end read (markdown, answer_display) settles within this, or the
|
|
@@ -54,10 +56,12 @@ function readCollapsed() {
|
|
|
54
56
|
// +composerEl+ size the stage's cap.
|
|
55
57
|
// +taskStop+: the page's stopper (task_stop.js), for the stop-task button
|
|
56
58
|
// on a running task_wait. +renderMarkdown+ and +linkRefs+: the asking
|
|
57
|
-
// slot's markdown read and ref pass (stage_slots.js).
|
|
59
|
+
// slot's markdown read and ref pass (stage_slots.js). +workingSet+: the
|
|
60
|
+
// page's working set (working_set_view.js, web.working_set; null: off): its
|
|
61
|
+
// rail in the live slots, its footprint at the hand-off.
|
|
58
62
|
export function createStageView({
|
|
59
63
|
historyEl, appendToHistory, isNearBottom: historyNearBottom, sawText, sessionId, dockEl, centerEl, composerEl, taskStop = null,
|
|
60
|
-
renderMarkdown, linkRefs,
|
|
64
|
+
renderMarkdown, linkRefs, workingSet = null,
|
|
61
65
|
}) {
|
|
62
66
|
// The dock becomes a clear column: the stage's card, then the composer's
|
|
63
67
|
// (its rows moved into .dock-card, which wears the glass and the focus ring).
|
|
@@ -97,6 +101,16 @@ export function createStageView({
|
|
|
97
101
|
asking.hidden = true;
|
|
98
102
|
const now = node("div", "ts-now", live);
|
|
99
103
|
const trail = node("div", "ts-trail", live);
|
|
104
|
+
// The working set's rail: out of the live slots' flow on their right (a
|
|
105
|
+
// row under the trail in a narrow stage), so it never sets their height.
|
|
106
|
+
const rail = workingSet
|
|
107
|
+
? createWorkingSetRail({ live, stage, onOpen: (el) => workingSet.openTurn(el), onChange: () => scheduleRefresh() })
|
|
108
|
+
: null;
|
|
109
|
+
if (rail) {
|
|
110
|
+
workingSet.setSeen(rail.seenAt);
|
|
111
|
+
// The server's states change the lines (never the fold): a redraw.
|
|
112
|
+
workingSet.onServer(() => scheduleRefresh());
|
|
113
|
+
}
|
|
100
114
|
const extras = node("div", "ts-extras", scroller);
|
|
101
115
|
const answer = node("div", "ts-answer", scroller);
|
|
102
116
|
const tail = node("div", "ts-tail", scroller);
|
|
@@ -324,6 +338,11 @@ export function createStageView({
|
|
|
324
338
|
slotsView.drawNow(slots.tool, slots.quiet);
|
|
325
339
|
slotsView.drawTrail(slots.trail, flash);
|
|
326
340
|
slotsView.drawTicks(slots.ticks);
|
|
341
|
+
if (rail) {
|
|
342
|
+
const { items, ordinal } = workingSet.liveItems(turn, promptWords());
|
|
343
|
+
stampRows(blockEl, turn, ordinal);
|
|
344
|
+
rail.draw(items, { ctxPct: workingSet.ctxPct() });
|
|
345
|
+
}
|
|
327
346
|
const calls = slots.ticks.length;
|
|
328
347
|
// An ended turn says each thing once: its steps in the chip (the label
|
|
329
348
|
// of what the cloud holds), its time in the status row's timing line
|
|
@@ -348,6 +367,13 @@ export function createStageView({
|
|
|
348
367
|
tail.hidden = !tail.firstChild;
|
|
349
368
|
}
|
|
350
369
|
|
|
370
|
+
// The prompt's first words (the working set's session tags say whose
|
|
371
|
+
// turn a step was).
|
|
372
|
+
function promptWords() {
|
|
373
|
+
const text = (promptSlot.querySelector(".user-message")?.textContent || "").replace(/\s+/g, " ").trim();
|
|
374
|
+
return text.length > 60 ? `${text.slice(0, 59)}\u2026` : text;
|
|
375
|
+
}
|
|
376
|
+
|
|
351
377
|
// The next step: the live slots' waiting height is let go. In a turn
|
|
352
378
|
// with the question room that height was the whole reply's: it goes as
|
|
353
379
|
// far as the stage's scroll absorbs it (the scroll moves up by as much,
|
|
@@ -539,6 +565,7 @@ export function createStageView({
|
|
|
539
565
|
function emptySlots() {
|
|
540
566
|
for (const slot of [promptSlot, extras, answer, tail, cloudBody, queuedSlot, timingSlot]) slot.replaceChildren();
|
|
541
567
|
slotsView.reset();
|
|
568
|
+
rail?.reset();
|
|
542
569
|
promptSlot.classList.remove("full");
|
|
543
570
|
thinkingLine = null;
|
|
544
571
|
flash = null;
|
|
@@ -584,6 +611,7 @@ export function createStageView({
|
|
|
584
611
|
}
|
|
585
612
|
|
|
586
613
|
function release() {
|
|
614
|
+
workingSet?.release();
|
|
587
615
|
releaseTurn();
|
|
588
616
|
clearStage();
|
|
589
617
|
}
|
|
@@ -604,9 +632,13 @@ export function createStageView({
|
|
|
604
632
|
const { blockEl } = view.current();
|
|
605
633
|
view.chronological();
|
|
606
634
|
const block = blockEl && blockEl.parentNode === cloudBody ? blockEl : null;
|
|
635
|
+
// The turn's set joins the page's turns (a plain one's too: the turns
|
|
636
|
+
// keep their places); its footprint goes under the block.
|
|
637
|
+
const footprint = workingSet ? workingSet.handOff(view.current().turn, promptWords()) : null;
|
|
607
638
|
const pieces = handOffOrder({
|
|
608
639
|
prompt: promptSlot.firstElementChild,
|
|
609
640
|
block,
|
|
641
|
+
footprint: block ? footprint : null,
|
|
610
642
|
extras: [...extras.children],
|
|
611
643
|
answer: [...answer.children],
|
|
612
644
|
timing: timingSlot.firstElementChild,
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
// liveTurn, turnDom, shownTexts, questionCards, contextChips,
|
|
18
18
|
// llmContextControl, thinkingControl,
|
|
19
19
|
// reasoningHistoryControl the page's views
|
|
20
|
+
// workingSet the stage view's working set (web.working_set),
|
|
21
|
+
// null when off
|
|
20
22
|
// sentIds, earlyRestores restore.js's bookkeeping
|
|
21
23
|
// stream: {takeGuess(), restart()} the stream was opened from a
|
|
22
24
|
// guess (takeGuess: whether, and forget it); restart:
|
|
@@ -51,7 +53,7 @@ export function createStreamHandlers(deps) {
|
|
|
51
53
|
const {
|
|
52
54
|
selected, state, clientId, capabilities, historyEl,
|
|
53
55
|
liveTurn, turnDom, shownTexts, questionCards, contextChips, llmContextControl, thinkingControl, reasoningHistoryControl,
|
|
54
|
-
sentIds, earlyRestores, stream, composer, getSession,
|
|
56
|
+
workingSet = null, sentIds, earlyRestores, stream, composer, getSession,
|
|
55
57
|
resync, updateInfoBar, applyPromptOps, showSteers, markRecapStale, setTurnRunning, setLiveStatus, endTurnView,
|
|
56
58
|
appendChunk, readTurnEnd, applyFinalMarkdown, refreshTimingFromSession, makeUserBubble, addEndBubble,
|
|
57
59
|
commandBubble, addCommandBubble, addNoteBubble, findInTurnRoots, setUsedMemories, showRecap,
|
|
@@ -307,6 +309,9 @@ export function createStreamHandlers(deps) {
|
|
|
307
309
|
used_memories_updated: (data) => {
|
|
308
310
|
if (Array.isArray(data.used_memory_names)) setUsedMemories(data.used_memory_names);
|
|
309
311
|
},
|
|
312
|
+
// The server's working set (WorkingSet), at a request and at turn end;
|
|
313
|
+
// ignored with web.working_set off.
|
|
314
|
+
working_set_updated: (data) => { workingSet?.update({ items: data.items, at: data.at }); },
|
|
310
315
|
context_status: (data) => {
|
|
311
316
|
const current = state();
|
|
312
317
|
// After /model (Engine#announce_switched_context!): the new model's
|
|
@@ -39,6 +39,7 @@ import * as model from "./turn_model.js";
|
|
|
39
39
|
import { createHold } from "./hold.js";
|
|
40
40
|
import { linkedLine } from "./notice_links.js";
|
|
41
41
|
import { taskCommand, taskStopButton } from "./task_stop.js";
|
|
42
|
+
import { footprintTemplate, groupItems, stampHistory } from "./working_set_view.js";
|
|
42
43
|
|
|
43
44
|
// ---- The templates: one step, its thinking, its label ----
|
|
44
45
|
|
|
@@ -71,13 +72,21 @@ function thinkingTemplate({ body, wrapped = true, line = null, cls = "thinking",
|
|
|
71
72
|
// full text. None when there is nothing to say. A closed step's text is
|
|
72
73
|
// set whole (.textContent): its refs (ref_scan.js) split it into spans
|
|
73
74
|
// that a text part's update would leave behind. +at+: a reloaded step's
|
|
74
|
-
// message index, for its refs (data-answer-at, as an answer's).
|
|
75
|
-
|
|
75
|
+
// message index, for its refs (data-answer-at, as an answer's). A
|
|
76
|
+
// reloaded step's +rendered+ markdown (its message's html, sanitised on the
|
|
77
|
+
// server, as an answer's) shows in place of the text, which it keeps as
|
|
78
|
+
// its source (stepText); a live step's renders once it closed
|
|
79
|
+
// (markdown_place.js renderStepTexts).
|
|
80
|
+
function textTemplate(text, { live, sentences, placeholder, at = null, rendered = null }) {
|
|
76
81
|
if (live && sentences?.length) {
|
|
77
82
|
return html`<div class="gen-text streaming"><div class="narration">${sentences.map((s) => html`<div class="sentence">${s}</div>`)}</div></div>`;
|
|
78
83
|
}
|
|
79
84
|
if (live) return placeholder ? html`<div class="gen-text streaming">…</div>` : nothing;
|
|
80
|
-
|
|
85
|
+
if (!text) return nothing;
|
|
86
|
+
if (typeof rendered === "string" && rendered) {
|
|
87
|
+
return html`<div class="gen-text markdown md-compact" data-answer-at=${at ?? nothing} data-copy-source=${text} .innerHTML=${rendered}></div>`;
|
|
88
|
+
}
|
|
89
|
+
return html`<div class="gen-text" data-answer-at=${at ?? nothing} .textContent=${text}></div>`;
|
|
81
90
|
}
|
|
82
91
|
|
|
83
92
|
// One step, live or reloaded. +gen+: { text, thinking, tools, answer } (a
|
|
@@ -97,7 +106,7 @@ export function stepTemplate(gen, st = {}) {
|
|
|
97
106
|
const failed = !live && model.failedCount(gen.tools) > 0;
|
|
98
107
|
// A step left with its thinking only reads "thinking" once.
|
|
99
108
|
const wrapped = live || !!text || gen.tools.length > 0;
|
|
100
|
-
return html`<details class=${`gen${live ? " live" : ""}${failed ? " has-failed" : ""}`} .open=${open}><summary class=${label.repeats ? "repeats" : nothing} @click=${on.stepClick ?? nothing}>${label.content}</summary>${steers}${gen.thinking ? thinkingTemplate({ body, wrapped, line, on }) : nothing}${textTemplate(text, { live, sentences, placeholder, at })}${items.length ? html`<div class="activity-body">${items}</div>` : nothing}</details>`;
|
|
109
|
+
return html`<details class=${`gen${live ? " live" : ""}${failed ? " has-failed" : ""}`} .open=${open}><summary class=${label.repeats ? "repeats" : nothing} @click=${on.stepClick ?? nothing}>${label.content}</summary>${steers}${gen.thinking ? thinkingTemplate({ body, wrapped, line, on }) : nothing}${textTemplate(text, { live, sentences, placeholder, at, rendered: gen.html })}${items.length ? html`<div class="activity-body">${items}</div>` : nothing}</details>`;
|
|
101
110
|
}
|
|
102
111
|
|
|
103
112
|
// A hook's line (hook_notice) as a step's row. data.line is a whole row of
|
|
@@ -807,10 +816,11 @@ function defaultPlaceAnswer(appendToHistory) {
|
|
|
807
816
|
|
|
808
817
|
// The text of a reloaded block's step +iteration+ (1-based, as the snapshot's
|
|
809
818
|
// cards count), "" when it has none: a reloaded question card's asking
|
|
810
|
-
// reply.
|
|
819
|
+
// reply, as written (a rendered step keeps it as data-copy-source).
|
|
811
820
|
export function stepText(block, iteration) {
|
|
812
821
|
const step = block?.querySelectorAll(":scope > .gen")[Number(iteration) - 1];
|
|
813
|
-
|
|
822
|
+
const text = step?.querySelector(":scope > .gen-text");
|
|
823
|
+
return text?.getAttribute("data-copy-source") ?? text?.textContent ?? "";
|
|
814
824
|
}
|
|
815
825
|
|
|
816
826
|
// A re-rendered failed turn's line (a .bubble.cancel, as live's
|
|
@@ -831,8 +841,11 @@ function failedLineTemplate(record) {
|
|
|
831
841
|
// duration from the timing's tool records), then the answer and the turn's
|
|
832
842
|
// timing line under it.
|
|
833
843
|
// +thumbs(images)+ draws a prompt's or a tool row's image thumbs (a
|
|
834
|
-
// template, images.js thumbsTemplate).
|
|
835
|
-
|
|
844
|
+
// template, images.js thumbsTemplate). +workingSet+ (the stage view's
|
|
845
|
+
// web.working_set, never the turn view's): each block stamped with its
|
|
846
|
+
// turn's ordinal and its rows with theirs (working_set_view.js), and the
|
|
847
|
+
// turn's footprint line after it.
|
|
848
|
+
export function turnHistoryNodes(items, timing, { thumbs, workingSet = false }) {
|
|
836
849
|
const timingTemplate = (group) => {
|
|
837
850
|
const record = group.record;
|
|
838
851
|
// Numbered by its place in the turn records, as live (a failed turn
|
|
@@ -866,12 +879,16 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
866
879
|
};
|
|
867
880
|
const steerTemplate = (i, bubble) => steerRowTemplate({ source: items[i].source, text: items[i].content }, { bubble });
|
|
868
881
|
const parts = [];
|
|
869
|
-
// Each block's steps, in order: its outline once the nodes exist
|
|
882
|
+
// Each block's steps, in order: its outline once the nodes exist; and
|
|
883
|
+
// its turn's place among the turns (the working set's stamp).
|
|
870
884
|
const outlines = [];
|
|
885
|
+
const blockTurns = [];
|
|
886
|
+
let turnOrdinal = -1;
|
|
871
887
|
for (const group of turnGroups(items, timing)) {
|
|
872
888
|
if (group.kind === "note") { parts.push(noteBubbleTemplate(items[group.i])); continue; }
|
|
873
889
|
if (group.kind === "steer") { parts.push(steerTemplate(group.i, true)); continue; }
|
|
874
890
|
if (group.kind === "answer") { parts.push(answerTemplate(group.answer, nothing)); continue; }
|
|
891
|
+
turnOrdinal += 1;
|
|
875
892
|
const prompt = items[group.user];
|
|
876
893
|
// A context wake turn starts with its note.
|
|
877
894
|
parts.push(prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt));
|
|
@@ -885,7 +902,12 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
885
902
|
if (plainThinking) {
|
|
886
903
|
parts.push(thinkingTemplate({ body: only.thinking, cls: "bubble thinking" }));
|
|
887
904
|
} else if (group.steps.length) {
|
|
888
|
-
|
|
905
|
+
// A step's text with its message's rendered markdown (html: web.markdown on).
|
|
906
|
+
const gens = group.steps.map((step) => ({
|
|
907
|
+
text: step.i === null ? "" : String(items[step.i].content ?? ""),
|
|
908
|
+
html: step.i === null ? null : items[step.i].html ?? null,
|
|
909
|
+
thinking: step.thinking || "", tools: step.tools,
|
|
910
|
+
}));
|
|
889
911
|
const steps = gens.map((gen, k) => stepTemplate(gen, {
|
|
890
912
|
// The steers the step answered first, where the live rows went.
|
|
891
913
|
steers: (group.steps[k].steers || []).map((i) => steerTemplate(i, false)),
|
|
@@ -895,6 +917,8 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
895
917
|
}));
|
|
896
918
|
parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
|
|
897
919
|
outlines.push(gens);
|
|
920
|
+
blockTurns.push(turnOrdinal);
|
|
921
|
+
if (workingSet) parts.push(footprintTemplate(groupItems(group, turnOrdinal), turnOrdinal));
|
|
898
922
|
}
|
|
899
923
|
// Steers with no step to sit in, or in a turn that shows no block: on
|
|
900
924
|
// their own after the prompt.
|
|
@@ -921,7 +945,9 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
|
|
|
921
945
|
}
|
|
922
946
|
const out = nodesOf(parts);
|
|
923
947
|
// A finished turn's block as the live end leaves it: its outline, open.
|
|
924
|
-
[...out.children].filter((el) => el.matches(".turn-work"))
|
|
948
|
+
const blocks = [...out.children].filter((el) => el.matches(".turn-work"));
|
|
949
|
+
blocks.forEach((block, i) => outlineBlock(block, outlines[i] || []));
|
|
950
|
+
if (workingSet) stampHistory(blocks, blockTurns);
|
|
925
951
|
// Each answer bubble and step text knows its message's refs
|
|
926
952
|
// (answer_refs.js; the page pairs them after the render), the index
|
|
927
953
|
// stays out of the DOM.
|