samagotchi 0.5.0 → 0.6.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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +84 -1
  3. data/bin/chi +32 -891
  4. data/docs/architecture.md +8 -1
  5. data/docs/cli.md +12 -13
  6. data/docs/configuration.md +4 -1
  7. data/docs/desktop.md +3 -1
  8. data/docs/hooks.md +69 -4
  9. data/docs/plugins.md +98 -3
  10. data/docs/sessions.md +3 -3
  11. data/lib/samagotchi/bridge/card_store.rb +30 -5
  12. data/lib/samagotchi/bridge/turn_accumulator.rb +7 -4
  13. data/lib/samagotchi/bridge/turn_notice.rb +37 -0
  14. data/lib/samagotchi/bundle_command.rb +652 -0
  15. data/lib/samagotchi/bundles/loop-guard/manifest.yml +4 -4
  16. data/lib/samagotchi/bundles/loop-guard/plugin.rb +214 -2
  17. data/lib/samagotchi/bundles/system/config_modification_protocol.md +3 -0
  18. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  19. data/lib/samagotchi/bundles/system/self_map.md +2 -1
  20. data/lib/samagotchi/cancellation_controller.rb +42 -1
  21. data/lib/samagotchi/config.rb +2 -2
  22. data/lib/samagotchi/desktop/macos/ChiRunner.swift +20 -5
  23. data/lib/samagotchi/desktop/macos/Panel.swift +31 -3
  24. data/lib/samagotchi/engine.rb +138 -618
  25. data/lib/samagotchi/guardrail_wiring.rb +238 -0
  26. data/lib/samagotchi/hooks/registry.rb +11 -3
  27. data/lib/samagotchi/hooks/stream_watch.rb +93 -0
  28. data/lib/samagotchi/hooks.rb +3 -0
  29. data/lib/samagotchi/kernel_loop.rb +65 -6
  30. data/lib/samagotchi/llm/chat_loop.rb +73 -11
  31. data/lib/samagotchi/llm/openai_chat.rb +16 -4
  32. data/lib/samagotchi/log_subscriber.rb +10 -4
  33. data/lib/samagotchi/plugin/api.rb +8 -0
  34. data/lib/samagotchi/plugin/context.rb +12 -3
  35. data/lib/samagotchi/plugin_tasks.rb +229 -0
  36. data/lib/samagotchi/question_desk.rb +280 -0
  37. data/lib/samagotchi/recap_setup.rb +128 -0
  38. data/lib/samagotchi/reply_wait.rb +9 -0
  39. data/lib/samagotchi/send_command.rb +3 -6
  40. data/lib/samagotchi/session.rb +9 -1
  41. data/lib/samagotchi/sessions_command.rb +255 -0
  42. data/lib/samagotchi/steer.rb +19 -2
  43. data/lib/samagotchi/terminal_ui/attached_loop.rb +18 -5
  44. data/lib/samagotchi/terminal_ui/formatting.rb +4 -2
  45. data/lib/samagotchi/tool_declarations.rb +2 -2
  46. data/lib/samagotchi/tools/delegate.rb +6 -2
  47. data/lib/samagotchi/tools/delegate_wait.rb +26 -8
  48. data/lib/samagotchi/turn_note.rb +20 -1
  49. data/lib/samagotchi/version.rb +1 -1
  50. data/lib/samagotchi/web/app.rb +28 -10
  51. data/lib/samagotchi/web/public/app.js +94 -52
  52. data/lib/samagotchi/web/public/card.js +8 -1
  53. data/lib/samagotchi/web/public/format.js +1 -1
  54. data/lib/samagotchi/web/public/index.html +2 -5
  55. data/lib/samagotchi/web/public/stage_model.js +44 -0
  56. data/lib/samagotchi/web/public/stage_view.js +26 -17
  57. data/lib/samagotchi/web/public/timing.js +9 -3
  58. data/lib/samagotchi/web/public/turn_events.js +18 -5
  59. data/lib/samagotchi/web/public/turn_view.js +80 -16
  60. data/lib/samagotchi/web/server.rb +4 -3
  61. metadata +9 -2
  62. data/lib/samagotchi/web/public/chat_view.js +0 -376
@@ -11,8 +11,7 @@
11
11
  // reload. Slots are patched in place, never rebuilt from HTML: a selection
12
12
  // and annotate survive. The rules are pure in stage_model.js.
13
13
  import { createTurnView } from "./turn_view.js";
14
- import { handOffDue, handOffOrder, inUseFrom, isPlain, liveSlots, placeFor } from "./stage_model.js";
15
- import { splitSentences } from "./sentences.js";
14
+ import { handOffDue, handOffOrder, headlineOf, inUseFrom, isPlain, liveSlots, placeFor } from "./stage_model.js";
16
15
  import { formatDuration } from "./timing.js";
17
16
 
18
17
  const COLLAPSE_KEY = "chi_stage_collapsed";
@@ -23,6 +22,10 @@ const SETTLE_CAP_MS = 8000;
23
22
  const FOLD_MS = 420;
24
23
  const FLASH_MS = 4000;
25
24
  const PENDING = ".bubble.question:not(.answered):not(.cancelled), .bubble.continue:not(.resolved)";
25
+ // A running turn's card with actions (check-in's Nudge / Keep going / Stop)
26
+ // asks the user too, but the turn goes on: it waits only while the turn runs
27
+ // and never holds the hand-off (a turn cut short can leave it open).
28
+ const ASKS = ".plugin-card[data-in-turn] .card-action";
26
29
  const MARKS = { answered: "✓", canceled: "■", failed: "✕" };
27
30
 
28
31
  function node(tag, cls, parent = null) {
@@ -32,16 +35,6 @@ function node(tag, cls, parent = null) {
32
35
  return el;
33
36
  }
34
37
 
35
- // The step's newest narration sentence: the end of a finished text is a
36
- // boundary (its last line may have no period), a live one's not yet.
37
- function headlineOf(text, done) {
38
- const { sentences, rest } = splitSentences(text, { atEnd: true });
39
- const fragment = rest.replace(/\s+/g, " ").trim();
40
- if (done && fragment) return fragment;
41
- if (sentences.length) return sentences[sentences.length - 1];
42
- return fragment.length > 160 ? fragment : "";
43
- }
44
-
45
38
  function readCollapsed() {
46
39
  try { return localStorage.getItem(COLLAPSE_KEY) === "1"; } catch (_) { return false; }
47
40
  }
@@ -153,6 +146,13 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
153
146
  return !!(extras.querySelector(PENDING) || tail.querySelector(PENDING));
154
147
  }
155
148
 
149
+ function asksCard() {
150
+ return !endKind && !!extras.querySelector(ASKS);
151
+ }
152
+ // A card answered or redrawn in place (its buttons gone) changes the
153
+ // status without a turn event.
154
+ new MutationObserver(() => scheduleRefresh()).observe(extras, { childList: true, subtree: true });
155
+
156
156
  function refresh() {
157
157
  rafPending = false;
158
158
  const { turn, blockEl } = view.current();
@@ -169,12 +169,14 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
169
169
  if (blockEl && blockEl.parentNode === cloudBody) blockEl.open = true;
170
170
  const narration = gen?.text ? headlineOf(gen.text, gen.textDone || gen.closed) : "";
171
171
  const thinking = thinkingLine && thinkingLine.step === turn.gens.length ? thinkingLine.text : "";
172
- const pending = pendingCard();
173
- const slots = liveSlots(turn, { narration, thinking, pending, ended: endKind });
172
+ // A question blocks the turn: its card takes the live slots' room. An
173
+ // asking card's turn goes on: the status waits, the slots stay.
174
+ const blocked = pendingCard();
175
+ const slots = liveSlots(turn, { narration, thinking, pending: blocked || asksCard(), ended: endKind });
174
176
 
175
177
  stage.classList.toggle("plain", plain);
176
178
  stage.classList.toggle("ended", !!endKind);
177
- stage.classList.toggle("waiting", pending);
179
+ stage.classList.toggle("waiting", blocked);
178
180
  stage.dataset.phase = slots.phase;
179
181
  mark.textContent = MARKS[slots.phase] || "";
180
182
  phaseEl.textContent = slots.phase;
@@ -521,6 +523,11 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
521
523
  turnEnded(args) {
522
524
  const kept = view.turnEnded(args) || [];
523
525
  if (!holding) return kept;
526
+ // The warn cards the extras kept in sight leave the turn with the
527
+ // block's (card.js leavesBlock): after the end line, as a reload has them.
528
+ const warn = [...extras.querySelectorAll(":scope > .plugin-card.warn")];
529
+ warn.forEach((el) => el.remove());
530
+ kept.push(...warn);
524
531
  view.chronological();
525
532
  endKind = args.kind;
526
533
  endedAt = Date.now();
@@ -542,7 +549,9 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
542
549
  return placed;
543
550
  },
544
551
  steer: (data) => view.steer(data),
545
- card: (el) => view.card(el),
552
+ // A warn card (a plugin's warning) stays in sight: false puts it in the
553
+ // extras (place), not in its step inside the closed cloud.
554
+ card: (el) => !el.classList.contains("warn") && view.card(el),
546
555
  flush: () => view.flush(),
547
556
  answerReady() {
548
557
  settled = true;
@@ -564,7 +573,7 @@ export function createStageView({ historyEl, appendToHistory, isNearBottom: hist
564
573
  const stick = slot.scrollHeight - slot.scrollTop - slot.clientHeight < 24;
565
574
  slot.appendChild(el);
566
575
  slot.hidden = false;
567
- if (stick || el.matches?.(PENDING)) slot.scrollTop = slot.scrollHeight;
576
+ if (stick || el.matches?.(PENDING) || el.querySelector?.(ASKS)) slot.scrollTop = slot.scrollHeight;
568
577
  scheduleRefresh();
569
578
  return true;
570
579
  },
@@ -88,7 +88,9 @@ export function timedTurnIndexes(items) {
88
88
  // the last, in order; a text-less iteration leaves no message, so a text
89
89
  // can sit one step early: inspection, not truth) and its answer (the last
90
90
  // assistant message). Assistant messages before any prompt are answers on
91
- // their own. The turn index counts prompts, as timedTurnIndexes does.
91
+ // their own. The turn index counts prompts, as timedTurnIndexes does. The
92
+ // user's lines merged into the running turn (`merged`, the server's
93
+ // Steer::INPUT_KIND) stay in it: the turn's `merged` list.
92
94
  export function turnGroups(items, timing) {
93
95
  const groups = [];
94
96
  let turnIndex = -1;
@@ -99,9 +101,11 @@ export function turnGroups(items, timing) {
99
101
  } else if (item?.role === "steer") {
100
102
  if (turn) turn.steers.push(i);
101
103
  else groups.push({ kind: "steer", i });
104
+ } else if (item?.role === "user" && item.merged && turn) {
105
+ turn.merged.push(i);
102
106
  } else if (item?.role === "user") {
103
107
  turnIndex += 1;
104
- turn = { kind: "turn", turnIndex, user: i, texts: [], steers: [] };
108
+ turn = { kind: "turn", turnIndex, user: i, texts: [], steers: [], merged: [] };
105
109
  groups.push(turn);
106
110
  } else if (item?.role === "assistant") {
107
111
  if (turn) turn.texts.push(i);
@@ -110,7 +114,9 @@ export function turnGroups(items, timing) {
110
114
  });
111
115
  return groups.map((g) => {
112
116
  if (g.kind !== "turn") return g;
113
- return withSteers(turnGroup(g, items, timing), g.steers, items);
117
+ const group = withSteers(turnGroup(g, items, timing), g.steers, items);
118
+ // A turn with no merged lines keeps its shape.
119
+ return g.merged.length ? { ...group, merged: g.merged } : group;
114
120
  });
115
121
  }
116
122
 
@@ -55,9 +55,18 @@ export function hookNoticeLabel(hook) {
55
55
  return /^[a-z][a-z0-9-]*$/.test(name) ? name : "hook";
56
56
  }
57
57
 
58
- // The row a step gets when the loop asks again after an empty answer.
59
- export function emptyRetryLine({ attempt, of } = {}) {
60
- return `↻ empty answer, asking again (${attempt}/${of})`;
58
+ // The row a step gets when the loop asks again after an empty answer, or
59
+ // after a plugin cut the generation (stopped_by: the bundle).
60
+ export function emptyRetryLine({ attempt, of, stopped_by: stoppedBy } = {}) {
61
+ const what = stoppedBy ? `cut by ${stoppedBy}` : "empty answer";
62
+ return `↻ ${what}, asking again (${attempt}/${of})`;
63
+ }
64
+
65
+ // The text of a turn's row from a snapshot entry (CardStore): a hook's
66
+ // notice as "<label>: text", the loop's asking-again row as its line.
67
+ export function noticeLine(entry = {}) {
68
+ if (entry.type === "empty_answer_retry") return emptyRetryLine(entry);
69
+ return `${hookNoticeLabel(entry.hook)}: ${entry.text || ""}`;
61
70
  }
62
71
 
63
72
  // The live turn's status while the provider is asked again after an error
@@ -211,6 +220,7 @@ export function promptOps(event, { myId, known, unmatchedMerge = false }) {
211
220
  // parts in order), then the prompts queued behind it. app.js feeds these to
212
221
  // its stream handlers, so a join renders exactly like watching live.
213
222
  // `merged_input` is the one synthetic type: a prompt merged into the turn.
223
+ // A `notice` part carries its event as it came (TurnNotice).
214
224
  export function snapshotEvents({ current_turn: turn = null, queued = [], started_at = undefined } = {}) {
215
225
  const events = [];
216
226
  if (turn) {
@@ -256,9 +266,12 @@ export function snapshotEvents({ current_turn: turn = null, queued = [], started
256
266
  closeText();
257
267
  events.push({ type: "reminder_injected", reminders: part.reminders || [] });
258
268
  break;
259
- case "hook_notice":
269
+ // One of the turn's rows (a hook's notice, an empty-answer retry, a
270
+ // question and its answer): the event itself, which the live
271
+ // handler draws where it drew it live.
272
+ case "notice":
260
273
  closeText();
261
- events.push({ type: "hook_notice", hook: part.hook, text: part.text, level: part.level });
274
+ if (part.event?.type) events.push({ ...part.event });
262
275
  break;
263
276
  // A plugin's steer: a steer-only merge, as live.
264
277
  case "steer":
@@ -1,20 +1,20 @@
1
- // The turn view of a running turn (web.turn_view): one <details
2
- // class="turn-work"> per turn holding one <details class="gen"> per
3
- // generation, the current one live (open, at the bottom), the earlier ones
4
- // collapsed to one line each. At the turn's end the last generation's
5
- // narration moves out of the block as the answer bubble, so markdown,
6
- // annotate and the turn output dedupe work as in the chat view. The same
7
- // interface as chat_view.js; the grouping rules live in turn_model.js.
1
+ // The turn view of a running turn (web.view: turn, and the stage's block):
2
+ // one <details class="turn-work"> per turn holding one <details
3
+ // class="gen"> per generation, the current one live (open, at the bottom),
4
+ // the earlier ones collapsed to one line each. At the turn's end the last
5
+ // generation's narration moves out of the block as the answer bubble, so
6
+ // markdown, annotate and the turn output dedupe work on a plain bubble.
7
+ // The grouping rules live in turn_model.js.
8
8
  //
9
9
  // A block that holds one gen with no thinking and no tool rows hides its
10
10
  // chrome (CSS :has), so a plain answer streams like today's bubble, and is
11
11
  // removed at the end: the answer bubble takes its place.
12
12
  import { toolName } from "./activity.js";
13
13
  import { leavesBlock } from "./card.js";
14
- import { activityStatusClass, activityStatusLabel, fillActivityRow } from "./chat_view.js";
15
14
  import { copySourceAttr } from "./copy.js";
16
15
  import { activityDiffHtml } from "./diff_view.js";
17
16
  import { escapeHtml, messageBodyHtml, noteHtml, normalize, steerRowHtml } from "./format.js";
17
+ import { thumbsHtml } from "./images.js";
18
18
  import { shouldFollowScroll } from "./scroll.js";
19
19
  import { hookNoticeLabel } from "./turn_events.js";
20
20
  import { cancelLineHtml, formatDuration, turnGroups, turnTimingText } from "./timing.js";
@@ -23,6 +23,55 @@ import { createTicker } from "./thinking_ticker.js";
23
23
  import * as model from "./turn_model.js";
24
24
  import { createHold } from "./hold.js";
25
25
 
26
+ // A row's status (live, or a saved tool record's on reload): running,
27
+ // error, stopped (a task_wait the user's Stop ended), else ok ("done").
28
+ function activityStatusClass(status) {
29
+ if (status === "running") return "running";
30
+ if (status === "error") return "error";
31
+ if (status === "stopped") return "stopped";
32
+ return "ok";
33
+ }
34
+
35
+ function activityStatusLabel(status) {
36
+ if (status === "running") return "running";
37
+ if (status === "error") return "error";
38
+ if (status === "stopped") return "stopped";
39
+ return "done";
40
+ }
41
+
42
+ // A live tool row filled from its activity row (status, title, output).
43
+ function fillActivityRow(el, row, sessionId) {
44
+ const statusEl = el.querySelector(".activity-status");
45
+ statusEl.className = `activity-status ${activityStatusClass(row.status)}`;
46
+ statusEl.textContent = activityStatusLabel(row.status);
47
+ const toolEl = el.querySelector(".activity-tool");
48
+ if (toolEl) toolEl.textContent = toolName(row);
49
+ const paramsEl = el.querySelector(".activity-params");
50
+ if (paramsEl) {
51
+ // The title in place of the params; the full params line on hover.
52
+ paramsEl.textContent = row.title || row.params || "";
53
+ paramsEl.title = row.title && row.params ? row.params : "";
54
+ }
55
+ const outEl = el.querySelector(".activity-output");
56
+ if (outEl) {
57
+ if (row.status === "running") {
58
+ outEl.style.display = "none";
59
+ outEl.textContent = "";
60
+ outEl.title = "";
61
+ } else {
62
+ // dispatch prefixes output with "[tool]"; strip that noise for display.
63
+ const full = (row.output || "").replace(/^\[[^\]]*\]\s*/, "");
64
+ outEl.title = full;
65
+ outEl.textContent = full.length > 300 ? `${full.slice(0, 300)}…` : full;
66
+ outEl.style.display = full ? "" : "none";
67
+ }
68
+ }
69
+ // The images a tool returned (a reloaded row has them too, reloadRowHtml).
70
+ if (row.images?.length && !el.querySelector(".thumbs")) el.insertAdjacentHTML("beforeend", thumbsHtml(sessionId, row.images));
71
+ // An edit/write's change, collapsed: "diff +3 −1".
72
+ if (row.diff && !el.querySelector(".activity-diff")) el.insertAdjacentHTML("beforeend", activityDiffHtml(row.diff));
73
+ }
74
+
26
75
  // The container seams (the stage view composes this view, stage_view.js):
27
76
  // blockHost(el) puts the turn's block in the page (default: the history);
28
77
  // follow() keeps the live part in view (default: the history's
@@ -102,7 +151,7 @@ export function createTurnView({
102
151
  const text = document.createElement("div");
103
152
  text.className = "gen-text streaming";
104
153
  // A step after the first shows "…" until its text comes (the first one
105
- // only for a prompt, as the chat view's bubble).
154
+ // only for a prompt).
106
155
  if (turn.gens.length > 1) text.textContent = "…";
107
156
  el.appendChild(text);
108
157
  const entry = { el, summary, thinking: null, text, activity: null, rows: new Map(), toggled: false,
@@ -308,13 +357,25 @@ export function createTurnView({
308
357
  }
309
358
  entry.summary.textContent = model.genLabel(gen);
310
359
  if (!entry.toggled) entry.el.open = false;
311
- // The chat view puts every generation's text in seenContent.
360
+ // Every generation's text goes into seenContent (the turn output dedupe).
312
361
  const k = normalize(gen.text);
313
362
  if (k) sawText(k);
314
363
  }
315
364
 
365
+ // A step that closes mid-turn (the next one starts) collapses: a warn card
366
+ // in it would go out of sight until the turn ends. It moves out of the
367
+ // step, right after it in the block (open while the turn runs), where
368
+ // turnEnded still finds it and takes it out of the block (leavesBlock).
369
+ function liftWarnCards(gen) {
370
+ const entry = genEls.get(gen);
371
+ if (!entry) return;
372
+ const cards = [...entry.el.querySelectorAll(".plugin-card.warn")];
373
+ if (cards.length) entry.el.after(...cards);
374
+ }
375
+
316
376
  function apply(r) {
317
377
  if (!r) return;
378
+ if (r.closed && r.opened) liftWarnCards(r.closed);
318
379
  if (r.closed) closeGenEl(r.closed);
319
380
  if (r.opened) createGenEl(r.gen);
320
381
  if (r.closed || r.opened) updateBlockSummary();
@@ -437,8 +498,8 @@ export function createTurnView({
437
498
  cards.forEach((el) => { if (!el.classList.contains("plugin-card")) el.classList.add("bubble"); });
438
499
  if (leaving) entry.el.remove();
439
500
  if (plain) {
440
- // A plain answer: no block, the bubble where it was (as the chat
441
- // view); its thinking, if any, as the chat view's collapsed block.
501
+ // A plain answer: no block, the bubble where it was; its thinking, if
502
+ // any, as a collapsed thinking block above it.
442
503
  const pieces = [];
443
504
  if (gen.thinking) {
444
505
  if (!entry.thinkingWrap) thinkingEl(gen);
@@ -473,7 +534,7 @@ export function createTurnView({
473
534
  const last = model.currentGen(turn);
474
535
  // A completed turn's answer is its last generation's text; a one-step
475
536
  // turn without tools leaves the block in any case (canceled, it is the
476
- // partial text bubble the chat view leaves).
537
+ // partial text bubble).
477
538
  const promoting = last && (kind === "completed" || (turn.gens.length === 1 && last.tools.length === 0));
478
539
  // The box's height, measured while it is still in the DOM and the step
479
540
  // still open: the apply below swaps it for the full text and collapses
@@ -601,8 +662,7 @@ function defaultPlaceAnswer(appendToHistory) {
601
662
  };
602
663
  }
603
664
 
604
- // A reloaded tool row, the live row's markup (chat_view.js
605
- // fillActivityRow): status, tool, params, output (its "[tool]" tag dropped,
665
+ // A reloaded tool row, the live row's markup (fillActivityRow): status, tool, params, output (its "[tool]" tag dropped,
606
666
  // 300 chars, the rest on hover), plus the call's duration from its record.
607
667
  // Params, output and the call's images come from the saved messages'
608
668
  // parts; without them the row has the record's name, status and duration
@@ -651,7 +711,7 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
651
711
  const noBlock = !group.steps.length ||
652
712
  (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking);
653
713
  if (group.steps.length === 1 && group.answer !== null && only.i === null && !only.tools.length && only.thinking) {
654
- // A plain answer with thinking: the chat view's collapsed block, as live.
714
+ // A plain answer with thinking: the collapsed thinking block, as live.
655
715
  parts.push(`<details class="bubble thinking"><summary>thinking</summary><div class="thinking-body">${escapeHtml(only.thinking)}</div></details>`);
656
716
  } else if (group.steps.length) {
657
717
  const gens = group.steps.map((step) => ({ text: step.i === null ? "" : String(items[step.i].content ?? ""), thinking: step.thinking || "", tools: step.tools }));
@@ -672,6 +732,10 @@ export function turnHistoryHtml(items, timing, { thumbs }) {
672
732
  // their own after the prompt.
673
733
  const loose = noBlock ? [...group.steps.flatMap((step) => step.steers || []), ...(group.steers || [])] : group.steers || [];
674
734
  parts.push(loose.map((i) => steerHtml(i, true)).join(""));
735
+ // The user's lines merged into the turn: the live "steered" bubbles,
736
+ // after its steps and before its answer.
737
+ parts.push((group.merged || []).map((i) =>
738
+ `<div class="bubble user steered" data-user-state="steered"${items[i].step ? ` data-step="${Number(items[i].step)}"` : ""}${copySourceAttr(items[i], false)}><div class="user-message">${messageBodyHtml(items[i]).body}${thumbs(items[i].images)}</div><span class="state-badge">steered</span></div>`).join(""));
675
739
  // A turn with no answer (canceled, or running: no record yet) shows its
676
740
  // timing after its prompt and steps, where the live view leaves it.
677
741
  if (group.answer === null) parts.push(timingHtml(group));
@@ -59,8 +59,9 @@ module Samagotchi
59
59
  # @param new_token [Boolean] replace the LAN access token first
60
60
  # (--new-token): a running server takes the new one at once
61
61
  # @return [Integer] the exit status
62
- def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false, view: "turn",
63
- annotate_presets: Config::BY_KEY["web.annotate_presets"].default, new_token: false)
62
+ def self.launch(port: nil, host: nil, scope: "project", dir: Dir.pwd, open_browser: false, markdown: false,
63
+ view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default,
64
+ new_token: false)
64
65
  rotate_token if new_token
65
66
  setting = host_setting(host)
66
67
  lan = Lan.wanted?(setting) ? Lan.choose(setting) : nil
@@ -108,7 +109,7 @@ module Samagotchi
108
109
  # @param token_path [String] the LAN access token's file
109
110
  # @return [Boolean] false when the port was taken (said so on stderr)
110
111
  def self.start(port: nil, host: nil, url: nil, open_browser: false, state_dir: nil, manager: nil, markdown: false,
111
- view: "turn", annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
112
+ view: Config::BY_KEY["web.view"].default, annotate_presets: Config::BY_KEY["web.annotate_presets"].default, hub: nil, lan: nil,
112
113
  token_path: Token.path)
113
114
  port = resolve_port(port)
114
115
  host = resolve_host(host)
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.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -150,9 +150,11 @@ files:
150
150
  - lib/samagotchi/bridge/ring_buffer.rb
151
151
  - lib/samagotchi/bridge/sse_writer.rb
152
152
  - lib/samagotchi/bridge/turn_accumulator.rb
153
+ - lib/samagotchi/bridge/turn_notice.rb
153
154
  - lib/samagotchi/bridge_client.rb
154
155
  - lib/samagotchi/bridge_client/event_stream.rb
155
156
  - lib/samagotchi/bridge_client/sse_parser.rb
157
+ - lib/samagotchi/bundle_command.rb
156
158
  - lib/samagotchi/bundle_needs.rb
157
159
  - lib/samagotchi/bundles/btw/manifest.yml
158
160
  - lib/samagotchi/bundles/btw/plugin.rb
@@ -201,6 +203,7 @@ files:
201
203
  - lib/samagotchi/empty_answer_retry.rb
202
204
  - lib/samagotchi/engine.rb
203
205
  - lib/samagotchi/gem_update.rb
206
+ - lib/samagotchi/guardrail_wiring.rb
204
207
  - lib/samagotchi/guardrails.rb
205
208
  - lib/samagotchi/guardrails/approval.rb
206
209
  - lib/samagotchi/guardrails/approvals.rb
@@ -216,6 +219,7 @@ files:
216
219
  - lib/samagotchi/hooks/bundle_loader.rb
217
220
  - lib/samagotchi/hooks/loader.rb
218
221
  - lib/samagotchi/hooks/registry.rb
222
+ - lib/samagotchi/hooks/stream_watch.rb
219
223
  - lib/samagotchi/host_registry.rb
220
224
  - lib/samagotchi/idle_client.rb
221
225
  - lib/samagotchi/idle_recap.rb
@@ -269,9 +273,12 @@ files:
269
273
  - lib/samagotchi/plugin/sessions.rb
270
274
  - lib/samagotchi/plugin/side_question.rb
271
275
  - lib/samagotchi/plugin/tool_result.rb
276
+ - lib/samagotchi/plugin_tasks.rb
272
277
  - lib/samagotchi/project_scope.rb
273
278
  - lib/samagotchi/prompt.rb
274
279
  - lib/samagotchi/prompt_literal_guard.rb
280
+ - lib/samagotchi/question_desk.rb
281
+ - lib/samagotchi/recap_setup.rb
275
282
  - lib/samagotchi/recap_store.rb
276
283
  - lib/samagotchi/reminder_store.rb
277
284
  - lib/samagotchi/reply_wait.rb
@@ -286,6 +293,7 @@ files:
286
293
  - lib/samagotchi/session_manager.rb
287
294
  - lib/samagotchi/session_metrics.rb
288
295
  - lib/samagotchi/session_observer.rb
296
+ - lib/samagotchi/sessions_command.rb
289
297
  - lib/samagotchi/steer.rb
290
298
  - lib/samagotchi/system_prompt.rb
291
299
  - lib/samagotchi/terminal_ui.rb
@@ -360,7 +368,6 @@ files:
360
368
  - lib/samagotchi/web/public/annotations.js
361
369
  - lib/samagotchi/web/public/app.js
362
370
  - lib/samagotchi/web/public/card.js
363
- - lib/samagotchi/web/public/chat_view.js
364
371
  - lib/samagotchi/web/public/chunk_router.js
365
372
  - lib/samagotchi/web/public/command_complete.js
366
373
  - lib/samagotchi/web/public/composer_size.js