samagotchi 0.149.0 → 0.151.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -1
  3. data/README.md +4 -2
  4. data/docs/architecture.md +4 -4
  5. data/docs/bundle-authoring.md +1 -1
  6. data/docs/cli.md +59 -24
  7. data/docs/configuration.md +6 -6
  8. data/docs/internals/background-tasks.md +1 -1
  9. data/docs/internals/block-renderers.md +96 -0
  10. data/docs/internals/bridge.md +53 -9
  11. data/docs/internals/bundles.md +43 -14
  12. data/docs/internals/objectives.md +7 -2
  13. data/docs/internals/web-frontend.md +2 -2
  14. data/docs/internals/web-push.md +2 -2
  15. data/docs/internals/working-set.md +5 -2
  16. data/docs/memory.md +48 -10
  17. data/docs/plugins.md +49 -3
  18. data/docs/sessions.md +5 -4
  19. data/docs/testing.md +5 -0
  20. data/lib/samagotchi/atomic_file.rb +3 -0
  21. data/lib/samagotchi/block_renderers.rb +115 -0
  22. data/lib/samagotchi/bootstrap_command.rb +5 -2
  23. data/lib/samagotchi/bridge/card_places.rb +93 -0
  24. data/lib/samagotchi/bridge/card_store.rb +224 -82
  25. data/lib/samagotchi/bridge.rb +12 -4
  26. data/lib/samagotchi/bundle_command/try/trace.rb +1 -1
  27. data/lib/samagotchi/bundle_command.rb +91 -68
  28. data/lib/samagotchi/bundler_env.rb +24 -0
  29. data/lib/samagotchi/bundles/dev/manifest.yml +2 -2
  30. data/lib/samagotchi/bundles/github-pr/manifest.yml +3 -3
  31. data/lib/samagotchi/bundles/github-pr/plugin.rb +2 -1
  32. data/lib/samagotchi/bundles/known-names/hooks/known_names.rb +3 -1
  33. data/lib/samagotchi/bundles/known-names/manifest.yml +3 -2
  34. data/lib/samagotchi/bundles/source-links/hooks/source_links.rb +2 -2
  35. data/lib/samagotchi/bundles/source-links/manifest.yml +3 -3
  36. data/lib/samagotchi/bundles/system/manifest.yml +3 -3
  37. data/lib/samagotchi/bundles/system/memory_guide.md +5 -5
  38. data/lib/samagotchi/bundles/system/self_map.md +1 -1
  39. data/lib/samagotchi/chi_home.rb +4 -2
  40. data/lib/samagotchi/chi_shim.rb +2 -1
  41. data/lib/samagotchi/child_env.rb +10 -3
  42. data/lib/samagotchi/engine.rb +19 -1
  43. data/lib/samagotchi/git_env.rb +43 -0
  44. data/lib/samagotchi/guardrails/context.rb +2 -0
  45. data/lib/samagotchi/guardrails/targets.rb +4 -1
  46. data/lib/samagotchi/memory_bundle/asset_installer.rb +23 -3
  47. data/lib/samagotchi/memory_bundle/builder.rb +5 -3
  48. data/lib/samagotchi/memory_bundle/file_standing.rb +167 -0
  49. data/lib/samagotchi/memory_bundle/installed_bundle.rb +70 -6
  50. data/lib/samagotchi/memory_bundle/installed_copy.rb +1 -0
  51. data/lib/samagotchi/memory_bundle/installer.rb +162 -52
  52. data/lib/samagotchi/memory_bundle/listing.rb +4 -3
  53. data/lib/samagotchi/memory_bundle/manifest.rb +23 -2
  54. data/lib/samagotchi/memory_bundle/merger.rb +209 -8
  55. data/lib/samagotchi/memory_bundle/placeholder.rb +2 -1
  56. data/lib/samagotchi/memory_bundle/profile.rb +7 -3
  57. data/lib/samagotchi/memory_bundle/provenance.rb +182 -22
  58. data/lib/samagotchi/memory_bundle/remerge.rb +167 -0
  59. data/lib/samagotchi/memory_bundle/shipped_history.rb +79 -0
  60. data/lib/samagotchi/memory_bundle/shipped_update.rb +89 -24
  61. data/lib/samagotchi/memory_bundle/source.rb +3 -3
  62. data/lib/samagotchi/memory_bundle/status.rb +21 -3
  63. data/lib/samagotchi/memory_bundle/system_bundle.rb +98 -33
  64. data/lib/samagotchi/memory_bundle/text_merge.rb +399 -0
  65. data/lib/samagotchi/memory_bundle/trash.rb +58 -2
  66. data/lib/samagotchi/memory_bundle/uninstaller.rb +26 -6
  67. data/lib/samagotchi/objectives_check.rb +37 -7
  68. data/lib/samagotchi/project_env.rb +3 -2
  69. data/lib/samagotchi/self_report.rb +2 -0
  70. data/lib/samagotchi/session_chain.rb +2 -7
  71. data/lib/samagotchi/session_commands.rb +15 -9
  72. data/lib/samagotchi/session_manager.rb +10 -7
  73. data/lib/samagotchi/steer.rb +17 -0
  74. data/lib/samagotchi/terminal_ui.rb +4 -4
  75. data/lib/samagotchi/testing/boundary.rb +1 -0
  76. data/lib/samagotchi/testing/minitest.rb +3 -0
  77. data/lib/samagotchi/testing/rspec.rb +3 -0
  78. data/lib/samagotchi/testing/sandbox.rb +3 -1
  79. data/lib/samagotchi/tools/delegate.rb +2 -1
  80. data/lib/samagotchi/tools/task_runtime.rb +13 -10
  81. data/lib/samagotchi/turn_checkpoint.rb +11 -5
  82. data/lib/samagotchi/turn_flow.rb +2 -1
  83. data/lib/samagotchi/update_command.rb +76 -28
  84. data/lib/samagotchi/update_hint.rb +4 -1
  85. data/lib/samagotchi/version.rb +1 -1
  86. data/lib/samagotchi/web/app.rb +22 -12
  87. data/lib/samagotchi/web/auto_update.rb +8 -8
  88. data/lib/samagotchi/web/auto_update_record.rb +6 -4
  89. data/lib/samagotchi/web/block_renderer_routes.rb +32 -0
  90. data/lib/samagotchi/web/markdown_renderer.rb +39 -6
  91. data/lib/samagotchi/web/message_display.rb +3 -3
  92. data/lib/samagotchi/web/public/app.js +7 -3
  93. data/lib/samagotchi/web/public/block_frame.js +202 -0
  94. data/lib/samagotchi/web/public/block_host.js +502 -0
  95. data/lib/samagotchi/web/public/block_open.js +13 -0
  96. data/lib/samagotchi/web/public/block_renderers.css +20 -0
  97. data/lib/samagotchi/web/public/block_source.js +37 -0
  98. data/lib/samagotchi/web/public/bubbles.js +11 -5
  99. data/lib/samagotchi/web/public/card.js +17 -10
  100. data/lib/samagotchi/web/public/cards_place.js +10 -5
  101. data/lib/samagotchi/web/public/claim_link.js +4 -2
  102. data/lib/samagotchi/web/public/details_motion.js +2 -1
  103. data/lib/samagotchi/web/public/history_reveal.js +4 -1
  104. data/lib/samagotchi/web/public/index.html +20 -14
  105. data/lib/samagotchi/web/public/markdown_place.js +2 -3
  106. data/lib/samagotchi/web/public/stage_model.js +5 -6
  107. data/lib/samagotchi/web/public/stage_view.js +8 -7
  108. data/lib/samagotchi/web/public/text_roots.js +6 -0
  109. data/lib/samagotchi/web/public/turn_block.js +34 -19
  110. data/lib/samagotchi/web/public/turn_html.js +4 -1
  111. data/lib/samagotchi/web/public/turn_model.js +1 -1
  112. data/lib/samagotchi/web/public/update_sheet.js +8 -3
  113. data/lib/samagotchi/web/public/working_set_view.js +10 -7
  114. data/lib/samagotchi/web/push/notifier.rb +7 -9
  115. data/lib/samagotchi/web/session_hub.rb +2 -1
  116. data/lib/samagotchi/web/session_show.rb +1 -1
  117. data/lib/samagotchi/web/update_ops.rb +2 -1
  118. data/lib/samagotchi/worker.rb +4 -2
  119. metadata +17 -1
@@ -46,7 +46,7 @@ export function turnAnswerEl(turnEls) {
46
46
  // (bubbles.js createBubbles)
47
47
  // @param followWokenWorker () follows the worker a card's command woke
48
48
  // @param api {getSession, sendCommand} (data.js)
49
- // @return {cardElement(id), upsertCard(card, {before}), placeSnapshotCards(entries),
49
+ // @return {cardElement(id), upsertCard(card, {before}), placeSnapshotCards(entries, {before}),
50
50
  // refreshCardBodies(), onCardAction(event)}
51
51
  const HELD_UNTIL_ANSWERED = /^\/objectives --card /;
52
52
 
@@ -63,9 +63,12 @@ export function createCardsPlace({
63
63
  // running turn's card as a row of its step; else before +before+ or at the
64
64
  // end of the history). A running turn's card with actions asks the user
65
65
  // something (check-in's Nudge / Keep going / Stop): it goes under the turn's
66
- // block, not into a step that collapses when the next one starts.
66
+ // block, not into a step that collapses when the next one starts. A turn
67
+ // still sliding away arrives first: its list of pieces holds this card's
68
+ // element, and an update may swap that (drawCard, a notice to a card).
67
69
  function upsertCard(card, { before = null } = {}) {
68
70
  if (!card || !card.id) return;
71
+ turnDom.settleFold?.();
69
72
  const existing = cardElement(card.id);
70
73
  if (existing) {
71
74
  const drawn = drawCard(existing, card);
@@ -81,11 +84,13 @@ export function createCardsPlace({
81
84
  }
82
85
 
83
86
  // A snapshot's cards and notices, each above the first turn after it; a
84
- // turn's own notice where it showed live (placeTurnNotice).
85
- function placeSnapshotCards(entries) {
87
+ // turn's own notice where it showed live (placeTurnNotice). +end+: what
88
+ // closes the history below them (a failed turn's prompt), for those with
89
+ // no turn after them; one shown after that prompt went back stays below it.
90
+ function placeSnapshotCards(entries, { before: end = null } = {}) {
86
91
  const users = turnPromptBubbles(historyEl);
87
92
  for (const entry of entries) {
88
- const before = cardPlace(users, entry);
93
+ const before = cardPlace(users, entry) || (entry.after_failed ? null : end);
89
94
  if (isCard(entry)) upsertCard(entry, { before });
90
95
  else if (entry.type === "question") placeResolvedQuestion(entry, before);
91
96
  else if (entry.type === "guardrail_warning") bubbles.showGuardrailWarning(entry.message, entry.label || "guardrails", { before });
@@ -325,12 +325,14 @@ export function showClaim(container, claim, { roots: rootSelector, opts = {}, do
325
325
  const hit = findClaim(texts.map((t) => t.text), claim, opts);
326
326
  if (!hit && !turn) return null;
327
327
  const el = hit ? roots[hit.at] : turn.anchor;
328
+ const start = hit && pointAt(texts[hit.at], hit.start);
329
+ const end = hit && pointAt(texts[hit.at], hit.end, true);
328
330
  revealInHistory(el);
331
+ // The text can be somewhere its root isn't: a drawn block's hidden source.
332
+ if (start) revealInHistory(start.node.parentElement);
329
333
  el.classList.add(HIT_CLASS);
330
334
  el.scrollIntoView?.({ block: "center" });
331
335
  if (hit) {
332
- const start = pointAt(texts[hit.at], hit.start);
333
- const end = pointAt(texts[hit.at], hit.end, true);
334
336
  const highlights = win?.CSS?.highlights;
335
337
  if (start && end && highlights && typeof win.Highlight === "function") {
336
338
  const range = doc.createRange();
@@ -56,7 +56,8 @@ export function installDetailsMotion({ root, selector, motion = canAnimate }) {
56
56
  const summary = e.target.closest?.("summary");
57
57
  const details = summary?.parentElement;
58
58
  if (!details || !details.matches?.(selector) || summary.parentElement !== details) return;
59
- if (e.target.closest("a, button, input, label") !== null && e.target !== summary) return;
59
+ // A live part of the summary (a steps block's footprint opens its own view).
60
+ if (e.target.closest("a, button, input, label, .ws-foot") !== null && e.target !== summary) return;
60
61
  if (!motion(details)) return;
61
62
  e.preventDefault();
62
63
  if (busy.has(details)) return;
@@ -2,7 +2,8 @@
2
2
  // hides +el+ unfolds: a closed <details> around it (a turn's steps block, a
3
3
  // step, a diff), an answer or a delegate report folded (answer_fold.js: its row follows
4
4
  // the bubble's class; the jump's unfold isn't saved), a superseded version
5
- // of a Revise stack (revise_stack.js: its stub opens, not saved). Every
5
+ // of a Revise stack (revise_stack.js: its stub opens, not saved), a drawn
6
+ // block's source (block_source.js). Every
6
7
  // jump goes through it (claim_link.js showClaim, refs.js showRow and so the
7
8
  // working set's step tags, verdict_card.js "View check"), so a new way to
8
9
  // hide history is opened in one place.
@@ -11,6 +12,7 @@
11
12
  // own: static JS is cached at unversioned URLs, and a new export on an old
12
13
  // module would blank the page.
13
14
 
15
+ import { revealBlockSource } from "./block_source.js";
14
16
  import { openStubOver } from "./revise_stack.js";
15
17
 
16
18
  // Opens each closed <details> that holds +el+ (not +el+ itself: a fold the
@@ -19,6 +21,7 @@ import { openStubOver } from "./revise_stack.js";
19
21
  export function revealInHistory(el) {
20
22
  if (!el) return null;
21
23
  openStubOver(el);
24
+ revealBlockSource(el);
22
25
  el.closest?.(".bubble.folded")?.classList.remove("folded");
23
26
  for (let details = el.parentElement?.closest("details"); details; details = details.parentElement?.closest("details")) {
24
27
  details.open = true;
@@ -15,6 +15,7 @@
15
15
  <link rel="stylesheet" href="/assets/composer.css">
16
16
  <link rel="stylesheet" href="/assets/forms.css">
17
17
  <link rel="stylesheet" href="/assets/objectives.css">
18
+ <link rel="stylesheet" href="/assets/block_renderers.css">
18
19
  <style>
19
20
  /* Icon masks shared by the phone info bar and the all view's cards (the stroke only shapes the mask; the icon takes currentColor); the image icon is composer.css's. */
20
21
  :root{--icon-archive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M6.5 9h3'/%3E%3C/svg%3E");--icon-unarchive:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2' y='2.75' width='12' height='3.5' rx='1'/%3E%3Cpath d='M3.25 6.25v6a1 1 0 0 0 1 1h7.5a1 1 0 0 0 1-1v-6M8 11.5V8.25M6.5 9.75 8 8.25l1.5 1.5'/%3E%3C/svg%3E")}
@@ -346,10 +347,16 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
346
347
  .thinking-body{margin-top:6px;font-size:13px;color:var(--text-3);white-space:pre-wrap;word-break:break-word;font-style:italic;max-height:240px;overflow:auto}
347
348
  /* The turn block (the stage's cloud, the history): one block per turn, its steps as collapsed rows, the live one open on top. */
348
349
  .turn-work{border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;background:var(--bg-panel);padding:8px 12px;font-size:14px}
349
- .turn-work>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
350
+ .turn-work>summary{position:relative;padding-left:13px;cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;user-select:none;list-style:none}
350
351
  .turn-work>summary::-webkit-details-marker{display:none}
351
- .turn-work>summary::before{content:"▸ ";color:var(--accent)}
352
- .turn-work[open]>summary::before{content:"▾ "}
352
+ .turn-work>summary::before{content:"▸";position:absolute;left:0;color:var(--accent)}
353
+ /* The summary's line: flex on this span, not on <summary> (WebKit's flex on a summary has had quirks). The chevron and the steps toggle the block, the footprint opens the turn's working set (working_set_view.js footClick). */
354
+ .tw-sum{display:flex;align-items:baseline;gap:10px;min-width:0}
355
+ /* The count gives way before the footprint, which is what the line is for. */
356
+ .ws-foot~.turn-steps{margin-left:auto;flex:0 100 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:12px;font-weight:500;color:var(--text-muted);white-space:nowrap}
357
+ /* A phone has no count to tap, and the chevron is then the only way to the steps: its hit area spreads over the block's padding, the glyph stays where it is. */
358
+ @media (max-width:600px){.ws-foot~.turn-steps{display:none}.turn-work>summary::before{left:-12px;top:-8px;width:36px;box-sizing:border-box;padding:8px 0 8px 12px}}
359
+ .turn-work[open]>summary::before{content:"▾"}
353
360
  /* Closed, a block keeps its box (the bar, the border, the width): a click changes its height only (details_motion.js); thinking and the recap the same. */
354
361
  /* A step's body (thinking, narration, rows, cards) sits --step-indent right of its summary, so the thinking
355
362
  chevron and the row marks share one column under the step's own chevron. The summary takes it back with a
@@ -399,11 +406,11 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
399
406
  .gen:not(.live) .gen-text{font-size:14px;color:var(--text-2)}
400
407
  .gen-text:empty{display:none}
401
408
  .gen .activity-body{margin-top:6px}
402
- /* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. */
403
- .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)){border:0;padding:0;background:transparent}
404
- .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body))>summary,.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen>summary{display:none}
405
- .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen{margin:0;padding:0;border:0;background:transparent}
406
- .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body)) .gen-text{margin:0;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:12px 14px;opacity:.95}
409
+ /* One gen, no thinking, no tool rows: nothing to stack yet, so the narration streams like a plain answer bubble. A step that closed with text (a line steered in after it, its answer gone from the block) is a row: its summary is the way to its text. */
410
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)){border:0;padding:0;background:transparent}
411
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text))>summary,.turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen>summary{display:none}
412
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen{margin:0;padding:0;border:0;background:transparent}
413
+ .turn-work:not(:has(.gen~.gen,.thinking-body,.activity-body,.gen:not(.live)>.gen-text)) .gen-text{margin:0;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent);border-radius:12px;padding:12px 14px;opacity:.95}
407
414
  @media(max-width:600px){.gen{--step-indent:8px}}
408
415
  /* A question card asks for input like the composer card, so it wears the same glass (the recipe above); only its warm tint is its own. */
409
416
  .bubble.question{border-left:3px solid var(--warn);background:color-mix(in srgb,var(--warn) 7%,var(--glass-bg));padding:10px 12px}
@@ -603,6 +610,7 @@ button.system-one-wrong,.system-one-wrong.marked{flex:none;padding:1px 6px;borde
603
610
  .card-head{display:flex;align-items:baseline;gap:8px;margin-bottom:4px}
604
611
  .card-title{font-size:14px;font-weight:600;color:var(--text-bright)}
605
612
  .card-source{font-size:11px;color:var(--text-faint);font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
613
+ .card-label{font-size:11px;color:var(--text-faint)}
606
614
  .card-body{font-size:13.5px;color:var(--text-2);white-space:normal;line-height:1.5}
607
615
  .card-body p{margin:0 0 6px}.card-body p:last-child{margin-bottom:0}
608
616
  .card-body ul,.card-body ol{margin:0 0 6px 20px}
@@ -978,11 +986,11 @@ body.working-set .ts-live{position:relative;padding-right:264px}
978
986
  }
979
987
  body.working-set .turn-stage.waiting .ts-live{min-height:calc(var(--ts-live-h) + var(--ws-row-h,0px))}
980
988
  body.working-set .turn-stage.held-wait:not(.waiting) .ts-live{min-height:max(calc(var(--ts-live-h) + var(--ws-row-h,0px)),var(--live-held,0px))}
981
- /* A finished turn's footprint, under its closed block (working_set_view.js): one faint line; a click opens its list. */
982
- .ws-foot{display:block;width:fit-content;max-width:calc(100% - 13px);box-sizing:border-box;margin:-4px 0 0 13px;padding:0 6px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11.5px;line-height:18px;color:var(--text-faint);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
983
- @media(hover:hover){.ws-foot:hover{color:var(--text-muted);background:var(--bg-hover)}}
989
+ /* A finished turn's footprint, its block's summary line (working_set_view.js): a click opens its list. */
990
+ .ws-foot{flex:0 1 auto;min-width:0;padding:0 6px;border-radius:6px;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12.5px;font-weight:500;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer}
991
+ @media(hover:hover){.ws-foot:hover{background:var(--bg-hover)}}
984
992
  .ws-foot:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
985
- .ws-foot .w{font-family:ui-sans-serif,system-ui,sans-serif}
993
+ .ws-foot .w{font-family:ui-sans-serif,system-ui,sans-serif;font-weight:600;color:var(--text-muted)}
986
994
  .ws-foot .sep{color:var(--border-strong)}
987
995
  .ws-foot .e{color:var(--accent)}
988
996
  .ws-foot .k{color:var(--think-text)}
@@ -1327,8 +1335,6 @@ body.has-session.composer-folded #infoBar{border-top-color:transparent}
1327
1335
  .us-row-head span{flex:1 1 0;min-width:0;text-align:right;color:var(--text-2);font-size:12px;overflow-wrap:anywhere}
1328
1336
  .us-detail{margin-top:4px;color:var(--text-muted);font-size:12px;overflow-wrap:anywhere}
1329
1337
  .us-kept{margin-top:6px;color:var(--warn-text);font-size:12px}
1330
- .us-kept ul{margin:3px 0 0;padding-left:18px;color:var(--text-2)}
1331
- .us-kept li{overflow-wrap:anywhere}
1332
1338
  .us-foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:10px}
1333
1339
  @media(max-width:600px){#updateSheet{position:fixed;inset:auto 8px 8px;width:auto;max-width:none;max-height:min(80dvh,calc(100dvh - 16px));margin:0}}
1334
1340
  .ctx-pop-kind{font-size:11px;color:var(--text-muted)}
@@ -4,9 +4,8 @@
4
4
  // page's state comes in as getters (capabilities, roots, currentDir), read
5
5
  // at each call.
6
6
 
7
- // The texts whose refs linkTextRefs links: the answers and the closed
8
- // steps' texts.
9
- const TEXTS = ".bubble.output, .gen:not(.live) > .gen-text";
7
+ import { TEXTS } from "./text_roots.js";
8
+
10
9
  // The closed steps' texts still shown raw: a live turn's (a reload's come
11
10
  // rendered, turn_block.js textTemplate).
12
11
  const RAW_STEPS = ".gen:not(.live) > .gen-text:not(.markdown)";
@@ -226,12 +226,11 @@ export function handOffDue({ ended, pending, inUse, quietSinceMs, nowMs }) {
226
226
  }
227
227
 
228
228
  // A handed-off turn's nodes in the order the turn block leaves them: the
229
- // prompt bubble (none for a reminder turn), the block, its working set's
230
- // footprint line (web.working_set), the extras in arrival order, the
231
- // answer's pieces, the timing line, the end line, the kept cards, then the
232
- // lines queued for after the turn (they run next).
233
- export function handOffOrder({ prompt = null, block = null, footprint = null, extras = [], answer = [], timing = null, end = null, kept = [], queued = [] }) {
234
- return [prompt, block, footprint, ...extras, ...answer, timing, end, ...kept, ...queued].filter(Boolean);
229
+ // prompt bubble (none for a reminder turn), the block, the extras in arrival
230
+ // order, the answer's pieces, the timing line, the end line, the kept cards,
231
+ // then the lines queued for after the turn (they run next).
232
+ export function handOffOrder({ prompt = null, block = null, extras = [], answer = [], timing = null, end = null, kept = [], queued = [] }) {
233
+ return [prompt, block, ...extras, ...answer, timing, end, ...kept, ...queued].filter(Boolean);
235
234
  }
236
235
 
237
236
  // The rows that wait for the running turn's end to run as their own: a
@@ -15,6 +15,7 @@
15
15
  // stage_frame.js's, and the in-use watch that times the hand-off is
16
16
  // stage_watch.js's.
17
17
  import { createTurnBlock } from "./turn_block.js";
18
+ import { openAfterEnd } from "./block_open.js";
18
19
  import { chipLabelText, flashOf, handOffDue, handOffOrder, headlineOf, isPlain, liveSlots, MARKS, miniToolText, placeFor, placeKind, QUEUED_CHILDREN, stepSlotText } from "./stage_model.js";
19
20
  import { createStageSlots, node } from "./stage_slots.js";
20
21
  import { createStageFrame, reducedMotion } from "./stage_frame.js";
@@ -687,7 +688,7 @@ export function createStageView({
687
688
  // the pieces arrive in the history and it follows a reader who was at its
688
689
  // end. Not with +immediate+ (another turn takes the stage at once), a
689
690
  // reduced motion, a hidden page or a collapsed stage: all at once.
690
- let folding = null; // { pieces, block, wasAtEnd, animate } while it slides
691
+ let folding = null; // { pieces, block, endKind, wasAtEnd, animate } while it slides
691
692
  function handOff({ immediate = false } = {}) {
692
693
  if (folding && immediate) finishFold();
693
694
  if (!holding || !endKind) return;
@@ -695,12 +696,12 @@ export function createStageView({
695
696
  view.chronological();
696
697
  const block = blockEl && blockEl.parentNode === cloudBody ? blockEl : null;
697
698
  // The turn's set joins the page's turns (a plain one's too: the turns
698
- // keep their places); its footprint goes under the block.
699
+ // keep their places); its footprint takes the block's summary line.
699
700
  const footprint = workingSet ? workingSet.handOff(view.current().turn, promptWords()) : null;
701
+ if (block && footprint) block.querySelector(":scope > summary > .tw-sum")?.prepend(footprint);
700
702
  const pieces = handOffOrder({
701
703
  prompt: promptSlot.firstElementChild,
702
704
  block,
703
- footprint: block ? footprint : null,
704
705
  extras: [...extras.children],
705
706
  answer: [...answer.children],
706
707
  timing: timingSlot.firstElementChild,
@@ -708,7 +709,7 @@ export function createStageView({
708
709
  queued: [...queuedSlot.children],
709
710
  });
710
711
  const animate = !immediate && !reducedMotion() && !document.hidden && !collapsed;
711
- folding = { pieces, block: blockEl, wasAtEnd: historyNearBottom(), animate };
712
+ folding = { pieces, block: blockEl, endKind, wasAtEnd: historyNearBottom(), animate };
712
713
  releaseTurn();
713
714
  if (!animate) return finishFold();
714
715
  stage.classList.add("leaving");
@@ -721,9 +722,9 @@ export function createStageView({
721
722
  const fold = folding;
722
723
  if (!fold) return;
723
724
  folding = null;
724
- // Closed, unless it is an outline (turn_block.js outlineBlock): the
725
- // history reads its narrated steps without a click.
726
- if (fold.block) fold.block.open = fold.block.classList.contains("outline");
725
+ // The stage held it open; here it takes its place in the history:
726
+ // closed, unless the turn was cut short with an outline (block_open.js).
727
+ if (fold.block) fold.block.open = openAfterEnd(fold.endKind, fold.block.classList.contains("outline"));
727
728
  historyEl.querySelector(":scope > .hint")?.remove();
728
729
  for (const el of fold.pieces) {
729
730
  historyEl.appendChild(el);
@@ -0,0 +1,6 @@
1
+ // The texts the page treats as text it wrote out of a model's words: the
2
+ // answers and the closed steps' texts. Cards, question replies, the asking
3
+ // slot and delegate reports aren't among them. markdown_place.js links refs on
4
+ // these; block_host.js draws in these. A module of its own: static JS is cached
5
+ // at unversioned URLs, and a new export on an old module would blank the page.
6
+ export const TEXTS = ".bubble.output, .gen:not(.live) > .gen-text";
@@ -17,14 +17,17 @@
17
17
  // walks childNodes (or reads firstChild/lastChild) must skip comments;
18
18
  // element queries, :empty and textContent are unaffected.
19
19
  //
20
- // A block that holds one gen with no thinking and no tool rows hides its
20
+ // A block that holds one live gen with no thinking and no tool rows hides its
21
21
  // chrome (CSS :has), so a plain answer streams like today's bubble, and is
22
- // removed at the end: the answer bubble takes its place.
22
+ // removed at the end: the answer bubble takes its place. A lone step that
23
+ // closed with text keeps its chrome: a line steered in after it left it alone
24
+ // in the block when the answer went, and its summary is the way to its text.
23
25
  import { authorBadge, PAIR } from "./participant_names.js";
24
26
  import { applyRowTemplate } from "./review_apply.js";
25
27
  import { html, nothing, render } from "./vendor/lit-html.js";
26
28
  import { answerRefsOf, setAnswerRefs } from "./answer_refs.js";
27
29
  import { bindRowRefs } from "./refs.js";
30
+ import { openAfterEnd } from "./block_open.js";
28
31
  import { leavesBlock } from "./card.js";
29
32
  import { failedTurnText, normalize, sourceLabel } from "./format.js";
30
33
  import { thumbsTemplate } from "./images.js";
@@ -132,7 +135,7 @@ function nodesOf(template) {
132
135
  // ---- A finished block's outline ----
133
136
 
134
137
  // A finished block as its outline (turn_model.js outlineRuns of +gens+, its
135
- // steps in order): open, each narrated step a one-line row as before, each
138
+ // steps in order): each narrated step a one-line row as before, each
136
139
  // run of steps that said nothing hidden behind one "N steps" fold that
137
140
  // shows them. The live turn's end and a reload call it alike. The folds sit
138
141
  // between the steps, not in them (`:scope > .gen` still counts steps), each
@@ -178,7 +181,6 @@ export function outlineBlock(blockEl, gens) {
178
181
  const step = e.target.closest?.(".gen.folded");
179
182
  if (step?.parentElement === blockEl) unfold.get(step)?.();
180
183
  });
181
- blockEl.open = true;
182
184
  return true;
183
185
  }
184
186
 
@@ -214,7 +216,7 @@ export function createTurnBlock({
214
216
  let turn = null;
215
217
  let blockEl = null;
216
218
  let blockSummaryEl = null;
217
- let blockToggled = false;
219
+ let blockStepsEl = null;
218
220
  // gen → a step's live state (newStep), its rendered nodes between
219
221
  // part.startNode and end.
220
222
  const genEls = new Map();
@@ -237,16 +239,20 @@ export function createTurnBlock({
237
239
  blockEl = document.createElement("details");
238
240
  blockEl.className = "turn-work";
239
241
  blockEl.open = true;
240
- blockToggled = false;
241
242
  blockSummaryEl = document.createElement("summary");
243
+ const line = document.createElement("span");
244
+ line.className = "tw-sum";
245
+ blockStepsEl = document.createElement("span");
246
+ blockStepsEl.className = "turn-steps";
247
+ line.appendChild(blockStepsEl);
248
+ blockSummaryEl.appendChild(line);
242
249
  blockEl.appendChild(blockSummaryEl);
243
- blockSummaryEl.addEventListener("click", () => { blockToggled = true; });
244
250
  updateBlockSummary();
245
251
  blockHost(blockEl);
246
252
  }
247
253
 
248
254
  function updateBlockSummary() {
249
- if (blockSummaryEl && turn) blockSummaryEl.textContent = model.blockSummary(turn, { running: !turn.ended });
255
+ if (blockStepsEl && turn) blockStepsEl.textContent = model.blockSummary(turn, { running: !turn.ended });
250
256
  }
251
257
 
252
258
  // The step rendered again from its state (stepTemplate). Each live step
@@ -621,6 +627,7 @@ export function createTurnBlock({
621
627
  placeAnswer(pieces, { plain: true, blockEl });
622
628
  blockEl = null;
623
629
  blockSummaryEl = null;
630
+ blockStepsEl = null;
624
631
  } else {
625
632
  placeAnswer(bubble ? [...cards, bubble] : [...cards, ...(endNote ? [endNote] : [])], { plain: false, blockEl });
626
633
  }
@@ -649,11 +656,12 @@ export function createTurnBlock({
649
656
  if (blockEl) {
650
657
  updateBlockSummary();
651
658
  blockEl.classList.add("done");
652
- // The outline (turn_model.js outlineRuns): open on its narrated steps'
653
- // rows, as a reload draws it; a block with nothing said closes.
659
+ // The outline (turn_model.js outlineRuns), for when it is opened. Closed
660
+ // as a reload draws it, unless the turn was cut short (block_open.js).
661
+ // The stage decides again as the block reaches the history.
654
662
  chronological();
655
663
  const outlined = outlineBlock(blockEl, model.blockGens(turn));
656
- if (!blockToggled) blockEl.open = outlined;
664
+ blockEl.open = openAfterEnd(kind, outlined);
657
665
  for (const el of blockEl.querySelectorAll(".plugin-card")) {
658
666
  if (leavesBlock({ warn: el.classList.contains("warn"), kind })) kept.push(el);
659
667
  }
@@ -682,7 +690,7 @@ export function createTurnBlock({
682
690
  turn = null;
683
691
  blockEl = null;
684
692
  blockSummaryEl = null;
685
- blockToggled = false;
693
+ blockStepsEl = null;
686
694
  // Clear every ticker timer before the map goes (a live step mid-dwell).
687
695
  for (const entry of genEls.values()) stopTicker(entry);
688
696
  genEls.clear();
@@ -821,7 +829,7 @@ function failedLineTemplate(record) {
821
829
  // template, images.js thumbsTemplate). +workingSet+ (the stage's
822
830
  // web.working_set): each block stamped with its
823
831
  // turn's ordinal and its rows with theirs (working_set_view.js), and the
824
- // turn's footprint line after it.
832
+ // turn's footprint on its summary line.
825
833
  export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, now = Date.now() }) {
826
834
  const timingTemplate = (group) => {
827
835
  const record = group.record;
@@ -861,9 +869,11 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
861
869
  };
862
870
  const steerTemplate = (i, bubble) => steerRowTemplate({ source: items[i].source, text: items[i].content }, { bubble });
863
871
  const parts = [];
864
- // Each block's steps, in order: its outline once the nodes exist; and
865
- // its turn's place among the turns (the working set's stamp).
872
+ // Each block's steps, in order: its outline once the nodes exist; how its
873
+ // turn ended (whether it stays open); and its turn's place among the turns
874
+ // (the working set's stamp).
866
875
  const outlines = [];
876
+ const blockEnds = [];
867
877
  const blockTurns = [];
868
878
  let turnOrdinal = -1;
869
879
  const groups = turnGroups(items, timing);
@@ -903,10 +913,14 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
903
913
  // Its text knows its message's refs (a question's), as an answer does.
904
914
  at: group.steps[k].i,
905
915
  }));
906
- parts.push(html`<details class="turn-work done"><summary>${model.blockSummary({ gens, ended: true })}</summary>${steps}</details>`);
916
+ // Its summary line: the working set's footprint, when the turn touched
917
+ // something, then the steps (the live block's, stage_view.js handOff).
918
+ const foot = workingSet ? footprintTemplate(groupItems(group, turnOrdinal), turnOrdinal) : nothing;
919
+ parts.push(html`<details class="turn-work done"><summary><span class="tw-sum">${foot}<span class="turn-steps">${model.blockSummary({ gens, ended: true })}</span></span></summary>${steps}</details>`);
907
920
  outlines.push(gens);
921
+ // No record: a turn saved before them, or running at the reload.
922
+ blockEnds.push(group.record?.status ?? "completed");
908
923
  blockTurns.push(turnOrdinal);
909
- if (workingSet) parts.push(footprintTemplate(groupItems(group, turnOrdinal), turnOrdinal));
910
924
  }
911
925
  // Steers with no step to sit in, or in a turn that shows no block: on
912
926
  // their own after the prompt.
@@ -941,9 +955,10 @@ export function turnHistoryNodes(items, timing, { thumbs, workingSet = false, no
941
955
  parts.push(failedLineTemplate(group.record));
942
956
  }
943
957
  const out = nodesOf(parts);
944
- // A finished turn's block as the live end leaves it: its outline, open.
958
+ // A finished turn's block as the live end leaves it: its outline, closed
959
+ // unless the turn was cut short.
945
960
  const blocks = [...out.children].filter((el) => el.matches(".turn-work"));
946
- blocks.forEach((block, i) => outlineBlock(block, outlines[i] || []));
961
+ blocks.forEach((block, i) => { block.open = openAfterEnd(blockEnds[i], outlineBlock(block, outlines[i] || [])); });
947
962
  if (workingSet) stampHistory(blocks, blockTurns);
948
963
  // Each answer bubble and step text knows its message's refs
949
964
  // (answer_refs.js; the page pairs them after the render), the index
@@ -284,7 +284,10 @@ export function answerBubbleTemplate(message, { cls = "", at = null, turnId = nu
284
284
  export function noteBubbleTemplate(note) {
285
285
  if (note?.objectives) return objectivesEntryTemplate(note, timeTitle(note.at));
286
286
  const from = note?.label ? `note from ${note.label}` : "note";
287
- return html`<div class="bubble note" title=${timeTitle(note?.at)}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
287
+ // A wake turn's note opens that turn (TURN_PROMPT, bubbles.js); a class, not
288
+ // data-turn-id, which marks a claim's anchor (claim_link.js).
289
+ const cls = note?.turn_start ? "bubble note turn-start" : "bubble note";
290
+ return html`<div class=${cls} title=${timeTitle(note?.at)}><div class="note-line">${from}</div><div class="note-text">${note?.content ?? ""}</div></div>`;
288
291
  }
289
292
 
290
293
  // A plugin's steer (a nudge put into the running turn): one line that
@@ -279,7 +279,7 @@ export function isNarrated(gen) {
279
279
  // them): each narrated step a row, each run of the others between (tool-only,
280
280
  // thinking-only) one fold ({fold: [indices]}), so every mid-turn text's first
281
281
  // line reads without a click and the steps that said nothing take one line.
282
- // [] when no step said anything: the block stays closed, as before.
282
+ // [] when no step said anything: nothing to outline.
283
283
  // A last run of one thinking-only step stays a row (no "· 1 step" at the end).
284
284
  export function outlineRuns(gens) {
285
285
  if (!gens.some(isNarrated)) return [];
@@ -1,12 +1,17 @@
1
1
  import { html, nothing, render } from "./vendor/lit-html.js";
2
2
 
3
+ // chi update's own words for the row (FileStanding owns what it says of a file).
4
+ // One that speaks of edited files runs long, so it gets the row's whole width.
3
5
  function installRow(row) {
4
- const kept = Array.isArray(row.kept) ? row.kept.map(String) : [];
6
+ const count = (list) => (Array.isArray(list) ? list.length : 0);
7
+ const behind = count(row.kept) > 0;
8
+ const long = behind || count(row.merged) > 0 || count(row.earlier) > 0;
9
+ const status = row.status || "working";
5
10
  const versions = [row.from, row.to].filter(Boolean).join(" → ");
6
11
  return html`<li class="us-row">
7
- <div class="us-row-head"><strong>${row.component || "chi update"}</strong><span>${row.status || "working"}</span></div>
12
+ <div class="us-row-head"><strong>${row.component || "chi update"}</strong>${long ? nothing : html`<span>${status}</span>`}</div>
8
13
  ${versions ? html`<div class="us-detail">${versions}</div>` : nothing}
9
- ${kept.length ? html`<div class="us-kept"><strong>Kept edited files:</strong><ul>${kept.map((file) => html`<li>${file}</li>`)}</ul></div>` : nothing}
14
+ ${long ? html`<div class=${behind ? "us-kept" : "us-detail"}>${status}</div>` : nothing}
10
15
  </li>`;
11
16
  }
12
17
 
@@ -95,17 +95,18 @@ function footTag(label) {
95
95
  return html` <button type="button" class="ws-step p" data-ws="jump" data-at=${atAttr(first.at)} title=${title}>${text}</button>`;
96
96
  }
97
97
 
98
- // The footprint line of a finished turn's +items+ (turn +ordinal+), nothing
99
- // for a turn that touched nothing: "touched web/public/ · 9 files · ◆ 3
100
- // memories s1–5 · +1 url · edited 2". A click opens the expanded view at
101
- // that turn; a step tag jumps to its call.
98
+ // The footprint of a finished turn's +items+ (turn +ordinal+), nothing for a
99
+ // turn that touched nothing: "touched web/public/ · 9 files · ◆ 3 memories
100
+ // s1–5 · +1 url · edited 2". It sits in the turn block's summary line, so a
101
+ // span (phrasing content). A click opens the expanded view at that turn; a
102
+ // step tag jumps to its call.
102
103
  export function footprintTemplate(items, ordinal) {
103
104
  const fp = footprint(items, BUDGETS.footprint);
104
105
  if (!fp) return nothing;
105
106
  const sep = html` <span class="sep">·</span> `;
106
107
  const parts = fp.labels.map((l, i) => html`${i ? sep : nothing}<span class=${`l ${l.kind}${l.state && l.state !== "live" ? ` ${l.state}` : ""}`} title=${labelTitle(l)}>${l.glyph ? html`<span class="k">${l.glyph}</span> ` : nothing}${labelText({ ...l, glyph: "" })}</span>${footTag(l)}`);
107
108
  const text = footprintText(items, BUDGETS.footprint);
108
- return html`<div class="ws-foot" role="button" tabindex="0" aria-haspopup="dialog" aria-expanded="false" data-ws-turn=${ordinal} data-ws-sig=${footSig(items)} aria-label=${`Working set of this turn: ${text.replace(/^touched /, "")}. Open the list.`}><span class="w">touched</span> ${parts}${fp.tail ? html`${sep}<span class="w">${fp.tail}</span>` : nothing}${fp.edited ? html`${sep}<span class="e">edited ${fp.edited}</span>` : nothing}${fp.stale ? html`${sep}<span class="s">${fp.stale} stale</span>` : nothing}${fp.forgotten ? html`${sep}<span class="f">${fp.forgotten} forgotten</span>` : nothing}${fp.dropped ? html`${sep}<span class="f">${fp.dropped} dropped</span>` : nothing}</div>`;
109
+ return html`<span class="ws-foot" role="button" tabindex="0" aria-haspopup="dialog" aria-expanded="false" data-ws-turn=${ordinal} data-ws-sig=${footSig(items)} aria-label=${`Working set of this turn: ${text.replace(/^touched /, "")}. Open the list.`}><span class="w">touched</span> ${parts}${fp.tail ? html`${sep}<span class="w">${fp.tail}</span>` : nothing}${fp.edited ? html`${sep}<span class="e">edited ${fp.edited}</span>` : nothing}${fp.stale ? html`${sep}<span class="s">${fp.stale} stale</span>` : nothing}${fp.forgotten ? html`${sep}<span class="f">${fp.forgotten} forgotten</span>` : nothing}${fp.dropped ? html`${sep}<span class="f">${fp.dropped} dropped</span>` : nothing}</span>`;
109
110
  }
110
111
 
111
112
  // What a footprint shows that can change after it is drawn: its items'
@@ -418,10 +419,12 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
418
419
  }
419
420
  });
420
421
 
421
- // A footprint: its step tag jumps, the rest opens that turn's view.
422
+ // A footprint: its step tag jumps, the rest opens that turn's view. Inside
423
+ // a <summary>, the click is cancelled so it doesn't toggle the block.
422
424
  function footClick(e) {
423
425
  const foot = e.target.closest?.(".ws-foot");
424
426
  if (!foot) return;
427
+ if (foot.closest("summary")) e.preventDefault();
425
428
  const tag = e.target.closest(".ws-step[data-at]");
426
429
  if (tag) {
427
430
  const at = parseAt(tag.dataset.at);
@@ -563,7 +566,7 @@ export function createWorkingSet({ historyEl, stageRoot = () => null, state, ses
563
566
  // The rail's first-sight times, for the popover's fresh rows.
564
567
  setSeen(fn) { seenAt = fn; },
565
568
  // The running turn hands off: its set joins the turns; its footprint
566
- // line (null: it touched nothing) goes after its block.
569
+ // (null: it touched nothing) goes into its block's summary.
567
570
  handOff(turn, prompt = "") {
568
571
  const ordinal = turns.length;
569
572
  const items = itemsOfTurn(turn, ordinal);
@@ -5,6 +5,7 @@ require "uri"
5
5
  require "time"
6
6
  require_relative "attention"
7
7
  require_relative "../../log"
8
+ require_relative "../../memory_bundle/file_standing"
8
9
 
9
10
  module Samagotchi
10
11
  module Web
@@ -52,10 +53,10 @@ module Samagotchi
52
53
  AUTO_NOTICES = {
53
54
  "installed" => lambda do |version, notes|
54
55
  ["installed", "chi #{version} is installed",
55
- "chi web and its sessions switch over when you restart them.#{kept_words(notes[:kept])}"]
56
+ "chi web and its sessions switch over when you restart them.#{file_words(notes)}"]
56
57
  end,
57
58
  "updated" => lambda do |version, notes|
58
- ["updated", "chi updated to #{version}", "#{restarted_words(notes[:restarted], notes[:left])}#{kept_words(notes[:kept])}"]
59
+ ["updated", "chi updated to #{version}", "#{restarted_words(notes[:restarted], notes[:left])}#{file_words(notes)}"]
59
60
  end,
60
61
  "restart_failed" => lambda do |version, notes|
61
62
  ["restart_failed", "chi #{version} is installed; chi web didn't restart",
@@ -79,13 +80,10 @@ module Samagotchi
79
80
  "#{words}."
80
81
  end
81
82
 
82
- # " Kept your edits in 2 files." ("" for none).
83
- def self.kept_words(count)
84
- if count.to_i.positive?
85
- " Kept your edits in #{count} #{count == 1 ? "file" : "files"}."
86
- else
87
- ""
88
- end
83
+ # " Merged the update into 1 file you edited. 2 files stay behind." ("" for none).
84
+ def self.file_words(notes)
85
+ words = MemoryBundle::FileStanding.counts_sentence(merged: notes[:merged].to_i, behind: notes[:kept].to_i)
86
+ words.empty? ? "" : " #{words}"
89
87
  end
90
88
 
91
89
  # A show carries +open+ only while its JSON stays this small: one
@@ -216,7 +216,8 @@ module Samagotchi
216
216
  # `chi_update` frames are the page's), and not kept for a snapshot.
217
217
  # @param kind [String] installed, updated or failed
218
218
  # @param version [String] the release it is about
219
- # @param notes [Hash] detail:, kept: (a count), restarted:, left:
219
+ # @param notes [Hash] detail:, kept:, merged: (counts of edited files that
220
+ # stayed behind and that took the update), restarted:, left:
220
221
  def publish_notice(kind:, version:, **notes)
221
222
  @monitor.synchronize { emit("chi_notice", { kind: kind, version: version, **notes }) }
222
223
  nil
@@ -80,7 +80,7 @@ module Samagotchi
80
80
  plugin_warning: turn_snapshot && turn_snapshot["plugin_warning"],
81
81
  # Plugins' slow setup still running (chi.init): the page's init row.
82
82
  init_tasks: snapshot_list(turn_snapshot, "init_tasks"),
83
- cards: cards_for_display(live_or_saved(turn_snapshot, "cards") { saved_cards(id) }, cwd: session.working_directory),
83
+ cards: cards_for_display(live_or_saved(turn_snapshot, "cards") { saved_cards(id, messages: raw_messages) }, cwd: session.working_directory),
84
84
  # The composer's / autocomplete; the built-ins until a worker
85
85
  # names its plugins' too.
86
86
  commands: turn_snapshot&.fetch("commands", nil) || SessionCommands.builtin_registry.listing,
@@ -175,7 +175,8 @@ module Samagotchi
175
175
  def perform_install(version, auto)
176
176
  argv = update_argv(version)
177
177
  said = nil
178
- output, success = @runner.call(argv, env: ChildEnv.unbundled,
178
+ env = auto ? ChildEnv.unbundled.merge(ChildEnv::UNATTENDED_UPDATE => "1") : ChildEnv.unbundled
179
+ output, success = @runner.call(argv, env: env,
179
180
  progress: lambda { |line|
180
181
  said = line unless line.strip.empty?
181
182
  @hub.publish_update(state: "running", rows: [], progress: line,