samagotchi 0.136.0 → 0.136.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 12638807efa522a91eb43ef7934f29aa0a4473d48da39897d6244b755eafdfdc
4
- data.tar.gz: e4dbe2698ef9517d9ccbacbd6e216720d8f0ce8e75bef8974f05951b7e1d2bfd
3
+ metadata.gz: a5dbc0c078bedf4518127ba59fef2277480193f98de99dcbd8983bb79a3802d2
4
+ data.tar.gz: e352d50d3b09a5e2f2a3ddd8dda3b79461e76c7e4743c306fc885e78fa670100
5
5
  SHA512:
6
- metadata.gz: 605c9b52f0d2b949e2f875ef939ba01401def71fb879ef78affe5e97b9f04ba2c761e48e17c801525617a87cfb4f7bed2d5b64045080f6408974e97a7e8f3f07
7
- data.tar.gz: c8d922fa4080f5b098ad953859506cfded71dc2be9038f836e88f53c288abca31265df0acdda01d3b945e43107d5c41b6ab9d5e2c3fa9196d4be284654e9d8e9
6
+ metadata.gz: c48493b22b5916b579d1f1fd386f0169f8a26ea71a29f5c77a567045e422f115b8b3ec421cb6f0499e0fc584da7e08840eeebf2650ad5d9bd75a61c41dc34c89
7
+ data.tar.gz: b9e9e2c78d869fe803ac6dbf54fcacc305b60c4b4ccd354d78b0967d340ad54cbf3f18357bb24c26050d96e4cff3899e90755857979fd9ec6680cc9fd730f46a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.136.1] - 2026-10-30
11
+
12
+ ### Changed
13
+
14
+ - Web: collapsibles keep their shape. A steps block, a thinking block, the recap and a long answer's fold are the
15
+ same box open or closed (the bar, the border, the width); a click changes the height, animated unless the system
16
+ prefers reduced motion. A closed answer fold is its box's one line, "▸ first line · N lines"; open, the row is the
17
+ box's top edge, "▾ N lines", and no longer repeats the answer's first line above the answer (a delegate report's
18
+ likewise, in the report's tint).
19
+
20
+ Update with `chi update`, then restart `chi web`. `chi home doctor` says what else to do.
21
+
10
22
  ## [0.136.0] - 2026-10-30
11
23
 
12
24
  ### Changed
@@ -5218,7 +5230,8 @@ and long-lived sessions.
5218
5230
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5219
5231
  hotkey panel that send selected text or the clipboard to your sessions.
5220
5232
 
5221
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.136.0...HEAD
5233
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.136.1...HEAD
5234
+ [0.136.1]: https://github.com/dm1try/samagotchi/compare/v0.136.0...v0.136.1
5222
5235
  [0.136.0]: https://github.com/dm1try/samagotchi/compare/v0.135.0...v0.136.0
5223
5236
  [0.135.0]: https://github.com/dm1try/samagotchi/compare/v0.134.0...v0.135.0
5224
5237
  [0.134.0]: https://github.com/dm1try/samagotchi/compare/v0.133.0...v0.134.0
data/docs/cli.md CHANGED
@@ -1026,8 +1026,9 @@ copies its Markdown source, not the rendered text; a code block copies just
1026
1026
  its code; a prompt copies the text as you typed it.
1027
1027
 
1028
1028
  A long answer (over 25 lines or 2,000 characters of its text) can be folded
1029
- by hand: a slim "▾ N lines" row sits just above it once its turn has ended.
1030
- Click it and the answer folds to one row, "▸ its first line · N lines" (the
1029
+ by hand: a "▾ N lines" row is its top edge once its turn has ended. Click
1030
+ it and the answer closes to one line in its own box,
1031
+ "▸ its first line · N lines" (the
1031
1032
  first line of what a hook presented in its place, else its first heading,
1032
1033
  else its first line); click again to unfold. A delegate report's reply
1033
1034
  folds the same way. Folding is this browser's choice, kept per session in
@@ -17,7 +17,7 @@ on an existing module, imported by a new version of another, then finds the old
17
17
  page. A factory dep a new version stops passing is the same hazard the other way round: the store's redraws took
18
18
  `updateInfoBar` and the chip controls out of `stream_handlers.js`'s deps (and `updateInfoBar`/`updateComposerMode`
19
19
  out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
20
- until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`,
20
+ until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
21
21
  `turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
22
22
  `session_fields.js`, `feature_gates.js`, `revise_stack.js` and `shell_command.js` are new modules for that reason.
23
23
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.136.0
3
+ version: 0.136.1
4
4
  scope: system
5
5
  description: Default system memories — identity, self map, config modification protocol, memory guide and the delegated-session rules
6
6
  files:
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Samagotchi
4
- VERSION = "0.136.0"
4
+ VERSION = "0.136.1"
5
5
  # Sent as the User-Agent of every HTTP request chi makes.
6
6
  USER_AGENT = "chi/#{VERSION}"
7
7
  end
@@ -3,9 +3,11 @@
3
3
  // gets a fold row: a sibling just before the bubble, never inside it (the
4
4
  // bubble's copy text, its claims and a live end's innerHTML stay the
5
5
  // bubble's own). Folded, the bubble has the class `.folded` (CSS hides it;
6
- // no inline style); the row reads its first line and its size, "▸
7
- // ‹first line› · N lines", folded or not: unfolded it is the bubble's header,
8
- // flush on top of it, its bar the bubble's.
6
+ // no inline style) and the row is the answer's box closed: "▸ ‹first
7
+ // line› · N lines" in the bubble's shape; unfolded, the row is that box's
8
+ // top edge, "▾ N lines", the bubble its rest (CSS joins the two). The box
9
+ // never changes shape, a click changes its height, animated
10
+ // (details_motion.js animateHeight) unless the viewer prefers no motion.
9
11
  //
10
12
  // A delegate report (a user line chi brought in, data-source
11
13
  // "delegate_report") gets the same row, folded by default (it is written
@@ -30,6 +32,7 @@
30
32
  // A module of its own: static JS is cached at unversioned URLs, and a new
31
33
  // export on an old module would blank the page.
32
34
  import { bubbleCopyText } from "./copy.js";
35
+ import { animateHeight, canAnimate } from "./details_motion.js";
33
36
  import { clip } from "./format.js";
34
37
  import { readStored, removeStored, storedKeys, writeStored } from "./storage.js";
35
38
 
@@ -80,12 +83,13 @@ export function foldLabel(text, { display = false } = {}) {
80
83
  return clip(plainLine(heading ?? texts[0] ?? ""), LABEL_CHARS);
81
84
  }
82
85
 
83
- // The row's words, folded or not: the label and the size (an open row is
84
- // the answer's header, as an open steps block keeps its summary).
85
- // +label+: the row's own (a report's, reportLabel), else the answer's.
86
- export function foldRowText(text, { display = false, label = foldLabel(text, { display }) } = {}) {
86
+ // The row's words: folded, the label and the size; unfolded, the size
87
+ // alone (the content is right under it). +label+: the row's own (a
88
+ // report's, reportLabel), else the answer's.
89
+ export function foldRowText(text, { display = false, folded = true, label = foldLabel(text, { display }) } = {}) {
87
90
  const n = lineCount(text);
88
91
  const size = `${n} line${n === 1 ? "" : "s"}`;
92
+ if (!folded) return { label: "", size };
89
93
  return { label, size: label ? `\u00a0\u00b7 ${size}` : size };
90
94
  }
91
95
 
@@ -205,7 +209,7 @@ function drawRow(row, bubble) {
205
209
  const text = answerText(bubble);
206
210
  const report = isReport(bubble);
207
211
  const own = report ? { label: reportLabel(text, bubble.querySelector(":scope > .origin-label")?.textContent || "") } : {};
208
- const { label, size } = foldRowText(text, { display: bubble.hasAttribute("data-answer-display"), ...own });
212
+ const { label, size } = foldRowText(text, { display: bubble.hasAttribute("data-answer-display"), folded, ...own });
209
213
  const what = report ? "report" : "answer";
210
214
  row.setAttribute("aria-expanded", folded ? "false" : "true");
211
215
  row.title = folded ? `Show the ${what}` : `Fold the ${what}`;
@@ -245,8 +249,9 @@ export function keepRowInView(scroller, row, mutate) {
245
249
  // @param historyEl the history (#history), also its scroller
246
250
  // @param sessionId () => the open session's id
247
251
  // @param projectRoot () => the open session's project root, or null
252
+ // @param motion (bubble) => whether a fold or unfold animates (canAnimate)
248
253
  // @return {decorate(root)} (the observer runs it; specs call it)
249
- export function installAnswerFold({ historyEl, sessionId, projectRoot = () => null }) {
254
+ export function installAnswerFold({ historyEl, sessionId, projectRoot = () => null, motion = canAnimate }) {
250
255
  // Each bubble's row, and each row's bubble.
251
256
  const rows = new WeakMap();
252
257
  const bubbleOf = new WeakMap();
@@ -318,10 +323,16 @@ export function installAnswerFold({ historyEl, sessionId, projectRoot = () => nu
318
323
  if (!bubble) return;
319
324
  e.preventDefault();
320
325
  const folded = !bubble.classList.contains("folded");
321
- keepRowInView(historyEl, row, () => {
326
+ const settle = () => keepRowInView(historyEl, row, () => {
322
327
  bubble.classList.toggle("folded", folded);
323
328
  drawRow(row, bubble);
324
329
  });
330
+ if (!motion(bubble)) settle();
331
+ else if (folded) animateHeight(bubble, bubble.offsetHeight, 0, { fade: true }).then(settle);
332
+ else {
333
+ settle();
334
+ animateHeight(bubble, 0, bubble.offsetHeight, { fade: true });
335
+ }
325
336
  // An answer's choice is its fold, a report's its unfold (D2: the default differs).
326
337
  if (isReport(bubble)) writeOpened(sessionId(), answerKey(bubble), !folded, { root: projectRoot() });
327
338
  else writeFold(sessionId(), answerKey(bubble), folded, { root: projectRoot() });
@@ -72,6 +72,7 @@ import { OWNER_FINAL, verifyTurnOf } from "./verify_target.js";
72
72
  import { actionLine, annotationActions, composerWithAction } from "./annotate_actions.js";
73
73
  import { copyText, installCopy } from "./copy.js";
74
74
  import { installAnswerFold, sweepAnswerFolds } from "./answer_fold.js";
75
+ import { installDetailsMotion } from "./details_motion.js";
75
76
  import { answerLabel, installReviseStacks, reviseChipTemplate, reviseWorkerUnavailable } from "./revise_stack.js";
76
77
  import { createQuestionCards } from "./question_cards.js";
77
78
  import { splitSnapshotCards } from "./card.js";
@@ -2085,6 +2086,10 @@ historyEl.addEventListener("click", (e) => {
2085
2086
  });
2086
2087
  turnDom.scrollEl.addEventListener("scroll", annotateBar.schedule, true);
2087
2088
  turnRoots().forEach((root) => installCopy(root));
2089
+ // A steps block, a thinking block and the recap open and close by hand
2090
+ // with their height animated (details_motion.js), in the history and on
2091
+ // the stage.
2092
+ installDetailsMotion({ root: document.body, selector: ".turn-work, .bubble.thinking, .bubble.recap" });
2088
2093
  // Long answers fold by hand (answer_fold.js), per viewer and session.
2089
2094
  installAnswerFold({
2090
2095
  historyEl,
@@ -0,0 +1,74 @@
1
+ // Collapsibles keep their shape: a steps block, a thinking block, the recap
2
+ // and the answer fold are the same box open or closed (the bar, the border,
3
+ // the width), and a click only changes the height. This module is the
4
+ // motion: a click on one of their summaries animates the height between the
5
+ // two states (Web Animations, MOTION_MS) instead of snapping, and the
6
+ // answer fold borrows animateHeight for its bubble. Nothing moves when the
7
+ // viewer prefers reduced motion or the element cannot animate (an old
8
+ // engine, a spec's DOM): the click is the native toggle then.
9
+ //
10
+ // Only a hand's click animates: the page's own opens and closes (a turn's
11
+ // end closing a block, a reload drawing the outline) stay instant, as they
12
+ // are not a change the viewer watches.
13
+ //
14
+ // A module of its own: static JS is cached at unversioned URLs, and a new
15
+ // export on an old module would blank the page.
16
+
17
+ export const MOTION_MS = 220;
18
+
19
+ // Whether +el+ can animate for this viewer.
20
+ export function canAnimate(el) {
21
+ if (typeof el?.animate !== "function") return false;
22
+ const win = el.ownerDocument?.defaultView;
23
+ return !win?.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches;
24
+ }
25
+
26
+ // Animates +el+'s height from +from+ to +to+ (px), its overflow clipped
27
+ // meanwhile; +fade+ fades it with the height. Resolves when the animation
28
+ // is over (or canceled).
29
+ export async function animateHeight(el, from, to, { fade = false, ms = MOTION_MS } = {}) {
30
+ const at = (h, o) => (fade ? { height: `${h}px`, opacity: String(o) } : { height: `${h}px` });
31
+ const anim = el.animate([at(from, from ? 1 : 0), at(to, to ? 1 : 0)], { duration: ms, easing: "ease" });
32
+ el.style.overflow = "hidden";
33
+ try {
34
+ await anim.finished;
35
+ } catch {
36
+ // canceled: the element is where its class puts it
37
+ } finally {
38
+ el.style.overflow = "";
39
+ }
40
+ }
41
+
42
+ // A <details>' height closed: its summary and its own padding and borders.
43
+ function closedHeight(details, summary) {
44
+ const win = details.ownerDocument?.defaultView;
45
+ const cs = win?.getComputedStyle?.(details);
46
+ const px = (v) => Number.parseFloat(v) || 0;
47
+ const frame = cs ? px(cs.paddingTop) + px(cs.paddingBottom) + px(cs.borderTopWidth) + px(cs.borderBottomWidth) : 0;
48
+ return summary.offsetHeight + frame;
49
+ }
50
+
51
+ // Installs the motion on +root+ for the <details> matching +selector+ whose
52
+ // own summary is clicked. +motion+: whether to animate +el+ (canAnimate).
53
+ export function installDetailsMotion({ root, selector, motion = canAnimate }) {
54
+ const busy = new WeakSet();
55
+ root.addEventListener("click", (e) => {
56
+ const summary = e.target.closest?.("summary");
57
+ const details = summary?.parentElement;
58
+ if (!details || !details.matches?.(selector) || summary.parentElement !== details) return;
59
+ if (e.target.closest("a, button, input, label") !== null && e.target !== summary) return;
60
+ if (!motion(details)) return;
61
+ e.preventDefault();
62
+ if (busy.has(details)) return;
63
+ busy.add(details);
64
+ const done = () => busy.delete(details);
65
+ if (details.open) {
66
+ const from = details.offsetHeight;
67
+ animateHeight(details, from, closedHeight(details, summary)).then(() => { details.open = false; done(); });
68
+ } else {
69
+ const from = closedHeight(details, summary);
70
+ details.open = true;
71
+ animateHeight(details, from, details.offsetHeight).then(done);
72
+ }
73
+ });
74
+ }
@@ -57,24 +57,25 @@ main{flex:1;display:flex;flex-direction:column;gap:16px;padding:16px;overflow:au
57
57
  /* A generation opens its answer bubble empty: hidden until text streams (the thinking box shows progress meanwhile). */
58
58
  .bubble.output.streaming:empty{display:none}
59
59
  /* The answer's pop: the promoted bubble is held at the narration box's height (inline max-height, scrolled to its tail) until the rendered markdown lands, then grows to its content while the content fades in; both classes go 400 ms later. */
60
- /* A long answer folded by hand (answer_fold.js): the row is the bubble's sibling just before it, styled as a closed turn's steps, reading the answer's first line and size; folded, the bubble is hidden; unfolded, the row is the bubble's header. */
60
+ /* A long answer folded by hand (answer_fold.js): the row is the bubble's sibling just before it, in the bubble's own shape. Folded, the row is the answer's box closed ("▸ first line · N lines") and the bubble is hidden; unfolded, the row is that box's top edge ("▾ N lines") and the bubble its rest, the two one block. The shape never changes; a click changes the height (details_motion.js). */
61
61
  /* Folded: the bubble hidden, an answer or a delegate report (#history's: outranks .bubble.user's display). */
62
62
  #history>.bubble.folded{display:none}
63
- .answer-fold{display:flex;align-items:baseline;gap:0;align-self:flex-start;max-width:100%;min-width:0;margin:0;padding:3px 10px;font:inherit;font-size:12px;font-weight:500;line-height:1.4;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:transparent;border:1px solid transparent;border-left:3px solid var(--accent-strong);border-radius:6px;transition:background .12s ease}
64
- .answer-fold:hover{background:var(--bg-panel)}
63
+ .answer-fold{display:flex;align-items:baseline;gap:0;align-self:stretch;max-width:100%;min-width:0;margin:0;padding:12px 14px;font:inherit;font-size:13px;font-weight:600;line-height:1.5;color:var(--text-muted);text-align:left;cursor:pointer;user-select:none;background:var(--bg-panel);border:1px solid var(--border);border-left:3px solid var(--accent-strong);border-radius:12px}
64
+ .answer-fold:hover .answer-fold-size{color:var(--text-2)}
65
65
  .answer-fold:focus-visible{outline:2px solid var(--accent-ring);outline-offset:1px}
66
66
  .answer-fold::before{content:"▾\00a0";color:var(--accent);flex:none}
67
67
  .answer-fold.folded::before{content:"▸\00a0"}
68
- /* Open: the bubble's header, flush on top of it (-12px: #history's gap), its bar running on into the bubble's. */
69
- .answer-fold:not(.folded){margin-bottom:-12px;border-bottom-left-radius:0}
70
- /* A delegate report's row: where the report sits (right, the user accent; dashed when merged mid-turn). Open, the row and
71
- the report share one width, so the row's bar runs on into the report's. */
72
- .answer-fold.report{align-self:flex-end;max-width:85%;border-left-color:var(--accent)}
68
+ /* Open: the box's top edge (-12px: #history's gap), the bubble under it its rest. */
69
+ .answer-fold:not(.folded){margin-bottom:-12px;padding-bottom:0;border-bottom:0;border-bottom-left-radius:0;border-bottom-right-radius:0}
70
+ .answer-fold:not(.folded)+.bubble{border-top:0;border-top-left-radius:0;border-top-right-radius:0;padding-top:8px}
71
+ /* A delegate report's row: the report's box (right, its tint, the user accent; dashed when merged mid-turn), closed or
72
+ its top edge. */
73
+ .answer-fold.report{align-self:flex-end;width:85%;background:var(--accent-bg);border-left-color:var(--accent)}
73
74
  .answer-fold.report.steered{border-left-style:dashed}
74
- .answer-fold.report:not(.folded),.answer-fold.report:not(.folded)+.bubble.user{width:85%}
75
+ .answer-fold.report:not(.folded)+.bubble.user{width:85%}
75
76
  .answer-fold-label{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-2);font-size:13px}
76
77
  .answer-fold-label:empty{display:none}
77
- .answer-fold-size{flex:none;white-space:nowrap;font-variant-numeric:tabular-nums}
78
+ .answer-fold-size{flex:none;white-space:nowrap;font-variant-numeric:tabular-nums;transition:color .12s ease}
78
79
  /* Revise stacks (revise_stack.js): a superseded version folds to its stub; the Revise row under a final answer
79
80
  (zero height: its button sits on the bubble's bottom edge, clear of the copy button, shown on hover) and a stack
80
81
  top's version chip. */
@@ -320,11 +321,10 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
320
321
  .activity-row>.activity-command{margin-left:0}
321
322
  }
322
323
  .bubble.thinking{border-left:3px solid var(--think);background:color-mix(in srgb,var(--think) 6%,var(--bg-panel));padding:8px 12px}
323
- .bubble.thinking:not([open]),.bubble.activity:not([open]){padding:3px 10px;background:transparent;border-color:transparent;align-self:flex-start;border-radius:6px;transition:background .12s ease}
324
- .bubble.thinking:not([open]){border-left-color:var(--think)}
324
+ .bubble.activity:not([open]){padding:3px 10px;background:transparent;border-color:transparent;align-self:flex-start;border-radius:6px;transition:background .12s ease}
325
325
  .bubble.activity:not([open]){border-left-color:var(--accent)}
326
- .bubble.thinking:not([open]):hover,.bubble.activity:not([open]):hover{background:var(--bg-panel)}
327
- .bubble.thinking:not([open])>summary,.bubble.activity:not([open])>summary{font-size:12px;font-weight:500}
326
+ .bubble.activity:not([open]):hover{background:var(--bg-panel)}
327
+ .bubble.activity:not([open])>summary{font-size:12px;font-weight:500}
328
328
  .bubble.thinking>summary{cursor:pointer;color:var(--text-muted);font-size:13px;font-weight:600;font-style:italic;user-select:none;list-style:none}
329
329
  .bubble.thinking>summary::-webkit-details-marker{display:none}
330
330
  .bubble.thinking>summary::before{content:"▸ ";color:var(--think)}
@@ -336,9 +336,7 @@ button.system-one-wrong:hover:not(:disabled){background:var(--bg-hover);color:va
336
336
  .turn-work>summary::-webkit-details-marker{display:none}
337
337
  .turn-work>summary::before{content:"▸ ";color:var(--accent)}
338
338
  .turn-work[open]>summary::before{content:"▾ "}
339
- .turn-work:not([open]){padding:3px 10px;background:transparent;border-color:transparent;border-left-color:var(--accent);align-self:flex-start;border-radius:6px;transition:background .12s ease}
340
- .turn-work:not([open]):hover{background:var(--bg-panel)}
341
- .turn-work:not([open])>summary{font-size:12px;font-weight:500}
339
+ /* 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. */
342
340
  /* A step's body (thinking, narration, rows, cards) sits --step-indent right of its summary, so the thinking
343
341
  chevron and the row marks share one column under the step's own chevron. The summary takes it back with a
344
342
  negative margin: a child's own margin shorthand can't undo the padding. */
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.136.0
4
+ version: 0.136.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dmitry Dedov
@@ -802,6 +802,7 @@ files:
802
802
  - lib/samagotchi/web/public/ctx.js
803
803
  - lib/samagotchi/web/public/data.js
804
804
  - lib/samagotchi/web/public/delivery_line.js
805
+ - lib/samagotchi/web/public/details_motion.js
805
806
  - lib/samagotchi/web/public/diff_view.js
806
807
  - lib/samagotchi/web/public/dock_reserve.js
807
808
  - lib/samagotchi/web/public/families_view.js