samagotchi 0.100.1 → 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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -1
  3. data/bin/chi +1 -0
  4. data/docs/architecture.md +6 -0
  5. data/docs/cli.md +55 -0
  6. data/docs/configuration.md +1 -0
  7. data/docs/internals/engine.md +1 -0
  8. data/docs/internals/working-set.md +130 -0
  9. data/docs/testing.md +1 -1
  10. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  11. data/lib/samagotchi/config.rb +3 -0
  12. data/lib/samagotchi/engine.rb +30 -0
  13. data/lib/samagotchi/host_keys.rb +35 -18
  14. data/lib/samagotchi/testing/fake_executable.rb +6 -16
  15. data/lib/samagotchi/testing/fake_executable_launcher +6 -0
  16. data/lib/samagotchi/tool_response.rb +10 -6
  17. data/lib/samagotchi/tool_runner.rb +25 -6
  18. data/lib/samagotchi/version.rb +1 -1
  19. data/lib/samagotchi/web/app.rb +49 -1
  20. data/lib/samagotchi/web/public/app.js +33 -3
  21. data/lib/samagotchi/web/public/chip_popover.js +56 -17
  22. data/lib/samagotchi/web/public/data.js +6 -0
  23. data/lib/samagotchi/web/public/index.html +127 -1
  24. data/lib/samagotchi/web/public/info_bar.js +16 -4
  25. data/lib/samagotchi/web/public/info_bar_model.js +7 -3
  26. data/lib/samagotchi/web/public/refs.js +10 -1
  27. data/lib/samagotchi/web/public/stage_model.js +6 -5
  28. data/lib/samagotchi/web/public/stage_view.js +34 -2
  29. data/lib/samagotchi/web/public/stream_handlers.js +6 -1
  30. data/lib/samagotchi/web/public/turn_view.js +16 -4
  31. data/lib/samagotchi/web/public/working_set.js +520 -0
  32. data/lib/samagotchi/web/public/working_set_rail.js +278 -0
  33. data/lib/samagotchi/web/public/working_set_view.js +426 -0
  34. data/lib/samagotchi/web/server.rb +6 -4
  35. data/lib/samagotchi/working_set.rb +200 -0
  36. 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
 
@@ -840,8 +841,11 @@ function failedLineTemplate(record) {
840
841
  // duration from the timing's tool records), then the answer and the turn's
841
842
  // timing line under it.
842
843
  // +thumbs(images)+ draws a prompt's or a tool row's image thumbs (a
843
- // template, images.js thumbsTemplate).
844
- export function turnHistoryNodes(items, timing, { thumbs }) {
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 }) {
845
849
  const timingTemplate = (group) => {
846
850
  const record = group.record;
847
851
  // Numbered by its place in the turn records, as live (a failed turn
@@ -875,12 +879,16 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
875
879
  };
876
880
  const steerTemplate = (i, bubble) => steerRowTemplate({ source: items[i].source, text: items[i].content }, { bubble });
877
881
  const parts = [];
878
- // 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).
879
884
  const outlines = [];
885
+ const blockTurns = [];
886
+ let turnOrdinal = -1;
880
887
  for (const group of turnGroups(items, timing)) {
881
888
  if (group.kind === "note") { parts.push(noteBubbleTemplate(items[group.i])); continue; }
882
889
  if (group.kind === "steer") { parts.push(steerTemplate(group.i, true)); continue; }
883
890
  if (group.kind === "answer") { parts.push(answerTemplate(group.answer, nothing)); continue; }
891
+ turnOrdinal += 1;
884
892
  const prompt = items[group.user];
885
893
  // A context wake turn starts with its note.
886
894
  parts.push(prompt.role === "note" ? noteBubbleTemplate(prompt) : userTemplate(prompt));
@@ -909,6 +917,8 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
909
917
  }));
910
918
  parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
911
919
  outlines.push(gens);
920
+ blockTurns.push(turnOrdinal);
921
+ if (workingSet) parts.push(footprintTemplate(groupItems(group, turnOrdinal), turnOrdinal));
912
922
  }
913
923
  // Steers with no step to sit in, or in a turn that shows no block: on
914
924
  // their own after the prompt.
@@ -935,7 +945,9 @@ export function turnHistoryNodes(items, timing, { thumbs }) {
935
945
  }
936
946
  const out = nodesOf(parts);
937
947
  // A finished turn's block as the live end leaves it: its outline, open.
938
- [...out.children].filter((el) => el.matches(".turn-work")).forEach((block, i) => outlineBlock(block, outlines[i] || []));
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);
939
951
  // Each answer bubble and step text knows its message's refs
940
952
  // (answer_refs.js; the page pairs them after the render), the index
941
953
  // stays out of the DOM.