samagotchi 0.112.0 → 0.114.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -1
  3. data/bin/chi +8 -0
  4. data/docs/cli.md +24 -6
  5. data/docs/configuration.md +22 -10
  6. data/docs/internals/prompt-caching.md +2 -1
  7. data/docs/internals/web-push.md +28 -4
  8. data/docs/sessions.md +3 -3
  9. data/lib/samagotchi/bootstrap_command.rb +70 -5
  10. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  11. data/lib/samagotchi/config.rb +24 -0
  12. data/lib/samagotchi/config_text_edit.rb +10 -3
  13. data/lib/samagotchi/gem_update.rb +5 -1
  14. data/lib/samagotchi/idle_recap.rb +38 -10
  15. data/lib/samagotchi/push_command.rb +188 -0
  16. data/lib/samagotchi/recap_store.rb +20 -3
  17. data/lib/samagotchi/recap_title.rb +78 -0
  18. data/lib/samagotchi/run_options.rb +1 -0
  19. data/lib/samagotchi/session_manager.rb +10 -3
  20. data/lib/samagotchi/sessions_command.rb +14 -11
  21. data/lib/samagotchi/title_command.rb +6 -1
  22. data/lib/samagotchi/version.rb +1 -1
  23. data/lib/samagotchi/web/app.rb +46 -3
  24. data/lib/samagotchi/web/markdown_renderer.rb +20 -0
  25. data/lib/samagotchi/web/public/app.js +6 -0
  26. data/lib/samagotchi/web/public/index.html +43 -4
  27. data/lib/samagotchi/web/public/markdown_tip.js +33 -0
  28. data/lib/samagotchi/web/public/notifications.js +22 -8
  29. data/lib/samagotchi/web/public/push_devices.js +187 -0
  30. data/lib/samagotchi/web/push/attention.rb +2 -1
  31. data/lib/samagotchi/web/push/store.rb +14 -0
  32. data/lib/samagotchi/web/push.rb +5 -0
  33. data/lib/samagotchi/web/push_routes.rb +17 -3
  34. data/lib/samagotchi/web/server.rb +46 -14
  35. data/lib/samagotchi/web/session_show.rb +2 -0
  36. data/lib/samagotchi/web/session_summary.rb +7 -2
  37. metadata +5 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bd818cf8fcf56a7040bdf84299d75a547b2031eb7bea13a1fdb087d12ed051d0
4
- data.tar.gz: 27890e5a3913053d11b5240c66f335ac66e0c5b4e55a9c0410b7a885ec9319d3
3
+ metadata.gz: a59aa63d1ccf6a6114afcaf5b1cf19088cb482989c08acb3aa5bfede3a67358d
4
+ data.tar.gz: 3fe5245e7c864ca0b814d9b187b402d5658ffd88aa5d895881482f4b40e9391f
5
5
  SHA512:
6
- metadata.gz: 44e69d35e152608b949071ca95d22367115c8524c284378a1e854ab98636e40f8f53bd5644bd816bfa903a2f3125e3c09a2f297999d4c251ee1bed6bf7a10e52
7
- data.tar.gz: d47e33d2b044590f507f3f551b179c60aa848e11ca79c9543d3f1af2a4b199f9ffee9bfa590488f66a0b439c65e7600a05c4c2caa335b4c26534782929d6c89e
6
+ metadata.gz: 412b5e6a15d73f4817729879e71aa3f2813ae73705039a8db561cbafffa0e246f6cf0f37760ebf346144c88369174a42cd902f36d31f3c02703345c99d18505f
7
+ data.tar.gz: 25d265a240a37194b5670afeb6d4784b55b43ddb33a857039da16ffc1a1184d79a6a4b30e7111a8c0698f538c130cd2c930545c03e3474a9bbf814bd22dbb48f
data/CHANGELOG.md CHANGED
@@ -8,6 +8,51 @@ and commands may change between minor versions. How releases are made:
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [0.114.0] - 2026-10-25
12
+
13
+ ### Added
14
+
15
+ - Auto titles: the idle recap also writes a 2-6 word title for the session, in the same request, and a session
16
+ without a `/title` is named by it on the web's cards, top bar, tab and notifications and in `chi sessions list`
17
+ (json: `auto_title`). Later recaps keep it unless the focus moved; a hand title always wins.
18
+ - chi web tells you Markdown rendering exists: while `web.markdown` is off, a session shows a one-line tip (restart
19
+ chi web with `--web-markdown`, or `web.markdown: true` in config.yml; plus `gem install commonmarker` when the gem
20
+ is missing). Its × hides it for that browser.
21
+ - `chi bootstrap` on a terminal asks "Render answers as Markdown in chi web? [Y/n]": yes installs the commonmarker
22
+ gem when it is missing and writes `web: markdown: true` into config.yml, its comments kept; a failed install says
23
+ why in one line and writes nothing. Without a terminal, a new config's next steps name `chi web --web-markdown`.
24
+
25
+ ### Fixed
26
+
27
+ - chi web's all sessions list: once it held more cards than fit, every card was cut to 72px whatever it held, so a
28
+ two-line title's meta row ran over the card's border and a parent's "▸ N delegates" chip was drawn under the next
29
+ card, out of reach.
30
+
31
+ Update with `chi update` (the system bundle 0.114.0), then restart `chi web`.
32
+
33
+ ## [0.113.0] - 2026-10-25
34
+
35
+ ### Added
36
+
37
+ - chi web: in the phone's Home Screen app the bell opens a notifications sheet: this device's switch, then every
38
+ device chi web pushes to with its last delivery, a "Long turns done" switch each and Remove; devices from an
39
+ earlier address are one line.
40
+ - `chi web push ls|rm ID|test [ID]`: list the phones chi web pushes to, remove one, or send a test notification,
41
+ from the terminal (chi web need not run; never prints a push address or keys).
42
+
43
+ ### Fixed
44
+
45
+ - chi web behind an auth proxy: a browser on this machine opening `127.0.0.1:<port>` got the token page with a paste
46
+ box that cannot work there (loopback takes the token as `Authorization: Bearer` only, so no cookie is traded); the
47
+ 401 page now says chi web opens at its public address and links it, and the API's 401 detail says the same.
48
+ - `chi web --tunnel tailscale`: a Ctrl-C while it turned Tailscale Serve on left port 443 forwarding to chi web's port
49
+ after it exited, and a second Ctrl-C during the teardown could cut it short with 443 still on: both now wait for the
50
+ command and turn 443 off.
51
+ - `chi web --tunnel tailscale` turns Tailscale Serve on only once chi web has bound its port: a program that took the
52
+ port between the free-port check and the bind was reachable from the tailnet until chi web gave up.
53
+
54
+ Update with `chi update` (the system bundle 0.113.0), then restart `chi web`.
55
+
11
56
  ## [0.112.0] - 2026-10-25
12
57
 
13
58
  ### Added
@@ -4291,7 +4336,9 @@ and long-lived sessions.
4291
4336
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
4292
4337
  hotkey panel that send selected text or the clipboard to your sessions.
4293
4338
 
4294
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.112.0...HEAD
4339
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.114.0...HEAD
4340
+ [0.114.0]: https://github.com/dm1try/samagotchi/compare/v0.113.0...v0.114.0
4341
+ [0.113.0]: https://github.com/dm1try/samagotchi/compare/v0.112.0...v0.113.0
4295
4342
  [0.112.0]: https://github.com/dm1try/samagotchi/compare/v0.111.1...v0.112.0
4296
4343
  [0.111.1]: https://github.com/dm1try/samagotchi/compare/v0.111.0...v0.111.1
4297
4344
  [0.111.0]: https://github.com/dm1try/samagotchi/compare/v0.110.0...v0.111.0
data/bin/chi CHANGED
@@ -97,6 +97,14 @@ if (path, klass = SUBCOMMANDS[ARGV[0]])
97
97
  exit Samagotchi.const_get(klass).new(ARGV[1..] || []).run
98
98
  end
99
99
 
100
+ # `chi web push ls|rm|test`: this machine's Web Push devices. Not a
101
+ # SUBCOMMANDS key: `chi web` itself is RunOptions' (and a remote's refusal
102
+ # came above: chi web runs on this machine only).
103
+ if ARGV[0] == "web" && ARGV[1] == "push"
104
+ require "samagotchi/push_command"
105
+ exit Samagotchi::PushCommand.new(ARGV[2..]).run
106
+ end
107
+
100
108
  # `chi scratch`: the run options below, for a session that leaves nothing
101
109
  # behind (TerminalUI scratch:). The word goes before OptionParser sees it.
102
110
  scratch = ARGV[0] == "scratch"
data/docs/cli.md CHANGED
@@ -14,6 +14,7 @@
14
14
  - `chi web [--port 4567] [--open] [--scope=all]` — start the Web UI (single localhost port session control plane) on this git project's sessions (`--scope=all`, or a folder in no repo: every session); if a chi web already runs on the port, print (with `--open`, open) its page for this folder and exit. Something else on the port (an older chi web too) exits 1 with "port N is in use"
15
15
  - `chi web --web-host lan` — the Web UI on your home network too, for your phone: a link with an access token and its QR code (see [chi web on your phone](#chi-web-on-your-phone)); `chi web --new-token` replaces the token
16
16
  - `chi web --tunnel tailscale` — the Web UI over https on your tailnet, for your phone anywhere: Tailscale Serve (never Funnel) on this machine's `<machine>.<tailnet>.ts.net` name for this run, the owner your Tailscale login, turned off on exit (see [Tailscale Serve](configuration.md#tailscale-serve-your-tailnet)); `--tunnel` without `web` exits 2
17
+ - `chi web push ls [--format json]`, `chi web push rm ID`, `chi web push test [ID]` — the phones chi web pushes to (see [Phone notifications](configuration.md#phone-notifications-web-push)): list them (short id, label, origin, added, last delivery, done turns, last error; `stale` for one made on another public URL than chi web's now: never sent to), remove one, or send a test notification to ID or to every device not stale. chi web need not run; nothing printed holds a device's push address or keys. A usage error exits 2; a store others can read, a corrupt one, an unknown device or a failed test push exit 1. This machine only: `chi --remote X web push` exits 2
17
18
  - `chi web --web-markdown` — opt in to sanitized Markdown rendering for completed assistant messages, the stage's asking text and a resolved question card's reply
18
19
  - `chi web --web-view turn` — draw turns with the turn view (each turn as one block of steps, the running one at the bottom of the history) instead of the default stage view (the running turn pinned above the composer); `?view=stage|turn` on the page URL overrides it (see [Web views](#web-views))
19
20
  - `chi web --no-web-working-set` — turns the stage view's working set off (on by default; `--web-working-set` turns a config's `web.working_set: false` back on): what each turn brought into the model's context, in the stage and under each finished turn; `?working_set=1|0` on the page URL overrides it (see [The working set](#the-working-set))
@@ -98,6 +99,16 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
98
99
  you uninstalled stays out ([Bundle profiles](memory.md#bundle-profiles-core-and-dev)).
99
100
  A bundle that fails to install makes the exit 1. `--dry-run` says what it
100
101
  would install.
102
+ - **Markdown in chi web.** Then, on a terminal and unless config.yml has
103
+ `web: markdown: true` already, it asks `Render answers as Markdown in chi web? [Y/n]`
104
+ (a closed stdin is no). Yes installs the `commonmarker` gem when it is
105
+ missing (`gem install commonmarker` through the Ruby chi runs on; it can take
106
+ a while where no precompiled build exists) and writes `web: markdown: true`
107
+ into config.yml, its comments kept ("web: markdown on (config.yml)"). A failed
108
+ install says so in one line with gem's last output line, writes nothing and
109
+ leaves the exit code as it was. Without a terminal it doesn't ask; a new
110
+ config's next steps then name `chi web --web-markdown`. `--dry-run` says it
111
+ would ask.
101
112
 
102
113
  ### Writing a bundle: new, test, check
103
114
 
@@ -709,8 +720,9 @@ attach and `chi -p --non-interactive` are refused. `/parent-answers` alone answe
709
720
  mid-turn too; a change waits for the turn's end. No chip: its reply is where the
710
721
  web shows it. See [Guardrails](guardrails.md#answers-from-a-parent-agent).
711
722
 
712
- `/title` shows the session's title (`title: Coordinator`, or `title: none (shows
713
- the first message: …)`); `/title TEXT` names the session (whitespace collapsed,
723
+ `/title` shows the session's title (`title: Coordinator`, or without one what names
724
+ the session: `title: none (shows the auto title: …)`, the recap's, or `title: none
725
+ (shows the first message: …)`); `/title TEXT` names the session (whitespace collapsed,
714
726
  cut at 80 characters), `/title --clear` drops the name (anything after `--clear`
715
727
  is refused with the usage line). It is saved at once,
716
728
  mid-turn too, and the lists, cards, top bar and tab show it in place of the first
@@ -896,7 +908,8 @@ Bundler, `gem install commonmarker`):
896
908
  chi web --web-markdown
897
909
  ```
898
910
 
899
- The setting also supports `SAMAGOTCHI_WEB_MARKDOWN=true` or the global config:
911
+ `chi bootstrap` offers to turn it on (and install commonmarker) on a terminal
912
+ (see [First setup](#first-setup)). The setting also supports `SAMAGOTCHI_WEB_MARKDOWN=true` or the global config:
900
913
 
901
914
  ```yaml
902
915
  web:
@@ -913,7 +926,11 @@ with markdown off), the same renderer and sanitizer. Generated HTML is
913
926
  sanitized, raw HTML in model output is not trusted, and unsafe links are
914
927
  removed. If Markdown is enabled without
915
928
  commonmarker installed, Chi Web keeps the normal escaped-text display and shows a
916
- warning explaining how to install the optional gem.
929
+ warning explaining how to install the optional gem. While Markdown is off, the
930
+ owner's session view shows a one-line tip saying how to turn it on (and, when
931
+ commonmarker isn't installed, how to install it; the text is the server's,
932
+ `markdown_tip` in the session payloads, never on a participant's page); its ×
933
+ hides it for that browser (`chi_markdown_tip_dismissed` in localStorage).
917
934
 
918
935
  Every prompt and answer has a copy button (on hover; always shown, dimmed, on a
919
936
  touch screen), and so does each code block of a rendered answer. An answer
@@ -1179,8 +1196,9 @@ access token", paste the token there (the part of the link after
1179
1196
  name on your tailnet (Tailscale Serve) for its run and prints its link's QR
1180
1197
  code; see [Tailscale Serve](configuration.md#tailscale-serve-your-tailnet).
1181
1198
  - Notifications on the phone, also while chi web isn't open there: on the tunnel's https link, Add to Home Screen,
1182
- open the Chi app and tap its bell; approvals and questions then push to the phone (see
1183
- [Phone notifications](configuration.md#phone-notifications-web-push)).
1199
+ open the Chi app, tap its bell and turn on **This device**; approvals, questions and failed turns then push to the
1200
+ phone. The same sheet lists the other devices (a long-turns-done switch each, Remove), and `chi web push ls|rm|test`
1201
+ does it from the Mac (see [Phone notifications](configuration.md#phone-notifications-web-push)).
1184
1202
 
1185
1203
  ### Remote sessions
1186
1204
 
@@ -50,7 +50,8 @@ hosts:
50
50
  # alias is applied, and its host:/the alias's host picks the host when host_ref
51
51
  # is left out; a model naming no host goes where a bare --model goes (the
52
52
  # default host). A model naming another host than host_ref turns recap off with
53
- # a warning. recap: false turns it off.
53
+ # a warning. recap: false turns it off. Each recap also names the session with
54
+ # a short auto title (shown when it has no /title), in the same request.
54
55
  recap:
55
56
  # host_ref: small-box
56
57
  # model: your-small-model-id
@@ -1618,7 +1619,8 @@ What changes once `web.public_url` is set:
1618
1619
  its defaults) would otherwise look like this machine. On this machine the page opens through the proxy too: a
1619
1620
  request that didn't come through it takes the token only as `Authorization: Bearer` (no cookie: a cookie for
1620
1621
  127.0.0.1 would go to every server on any local port; a phone on the LAN, with `web.host: lan`, keeps its LAN link
1621
- and cookie). chi's own probe of the port (a second `chi web`, `chi share`,
1622
+ and cookie). A browser on this machine that opens the port directly gets a 401 page with no paste box: it links
1623
+ `web.public_url`, the address to open instead. chi's own probe of the port (a second `chi web`, `chi share`,
1622
1624
  `chi update`) sends no token; chi web answers it the few fields it checks. Links and the start line come from this
1623
1625
  machine's config, never from whatever answers on the port.
1624
1626
  - **A request through the proxy** (it carries `X-Forwarded-For`, `Forwarded`, `X-Real-IP`, a known identity header,
@@ -1733,8 +1735,9 @@ Tailnet Lock is off: a device Tailscale's servers add to your tailnet could reac
1733
1735
  ```
1734
1736
 
1735
1737
  - It checks before it changes anything: Tailscale is connected, this machine has its `ts.net` name and a certificate
1736
- for it, and what Serve already has on port 443. Nothing there: it runs
1737
- `tailscale serve --bg --https=443 http://127.0.0.1:<port>`. Exactly that one handler for this port (a leftover of an
1738
+ for it, and what Serve already has on port 443. Nothing there: once chi web has bound its port (so Serve never
1739
+ forwards your tailnet to another program that took the port first), it runs
1740
+ `tailscale serve --bg --https=443 http://127.0.0.1:<port>`, then prints its lines. Exactly that one handler for this port (a leftover of an
1738
1741
  earlier run killed hard, or step 3 done by hand): it adopts it. Anything else on 443 (another port, other paths, TCP
1739
1742
  forwarding, Funnel) is refused and left alone. Serve's other ports are never touched.
1740
1743
  - The owner is `web.proxy.owner` if config.yml has it, else this machine's own Tailscale login (a tagged machine has
@@ -1742,7 +1745,9 @@ Tailnet Lock is off: a device Tailscale's servers add to your tailnet could reac
1742
1745
  config.yml: a `web.public_url` or `web.proxy.identity_header` that says otherwise (in config.yml or the environment)
1743
1746
  is refused, so a config from step 4 with this machine's name works as it is.
1744
1747
  - On exit (Ctrl-C, a kill, closing the terminal) it runs `tailscale serve --https=443 off`, but only while 443 still
1745
- serves exactly what it set; if you changed 443 meanwhile it leaves it and says so. Killed hard (`kill -9`, a power
1748
+ serves exactly what it set; if you changed 443 meanwhile it leaves it and says so. A Ctrl-C while it turns Serve on
1749
+ or off waits for that command (at most 15 s each) and the teardown still runs; a teardown that fails is one warning
1750
+ line. Killed hard (`kill -9`, a power
1746
1751
  loss) the serve stays on: Serve answers 502 for the name while nothing listens on the port, and whatever later
1747
1752
  listens on 127.0.0.1:<port> is reachable from your tailnet; the next `chi web --tunnel tailscale` adopts it and
1748
1753
  turns it off on exit, a plain `chi web` doesn't look.
@@ -1784,9 +1789,17 @@ What it needs:
1784
1789
  - No Apple account or registration: chi web signs its pushes with a key of its own (VAPID).
1785
1790
 
1786
1791
  Turning it on, on the phone: open the `Proxied:` link in Safari → Share → **Add to Home Screen** → open the **Chi**
1787
- app → tap the **bell** at the top → Allow. A test notification arrives at once. Tap the
1788
- bell again to turn it off on that phone. In a browser tab the bell stays the tab's own notifications (while the tab
1789
- is in the background), as before.
1792
+ app → tap the **bell** at the top → turn on **This device** in the sheet → Allow. A test notification arrives at once.
1793
+ In a browser tab the bell stays the tab's own notifications (while the tab is in the background), as before.
1794
+
1795
+ **The devices** (the bell's sheet in the app; a bottom sheet on the phone): this device first, with its switch (off
1796
+ removes it from chi web and is the same as the bell turned off), then every other device chi web pushes to: its label,
1797
+ the last delivery and the last error, a **Long turns done** switch each, and **Remove** (it can turn its bell on again
1798
+ to come back). Devices made on an earlier public URL show as one line ("1 more device from an earlier address"): they
1799
+ are never sent to; `chi web push rm ID` removes them. From the Mac: `chi web push ls` lists them, `chi web push test
1800
+ [ID]` sends a test notification, `chi web push rm ID` removes one ([CLI](cli.md)). A device iOS dropped silently still
1801
+ shows a recent delivery (Apple keeps accepting pushes for it): if one stops getting notifications, switch it off and on
1802
+ there; the new subscription replaces the dead one.
1790
1803
 
1791
1804
  - **What is sent**: the session's name and the reason ("needs approval", "needs an answer", "hit its step limit",
1792
1805
  "needs you", "turn failed", "done in N s"), never the question or the command, as the tab's notification on the
@@ -1798,8 +1811,7 @@ is in the background), as before.
1798
1811
  - **When**: the same things the tab notifies for, still open 3 s after they came (3.5 s for a delegated child's
1799
1812
  approval, which its parent may relay instead); one push each. A delegated child's failed or finished turn is its
1800
1813
  parent's to report. **Done turns** (10 s or longer, or one nobody started: a delegate's report, a context change, a
1801
- finished task) are **off** by default, per device; the switch comes with the devices list (for now:
1802
- `PATCH /api/push/subscriptions/ID {"prefs": {"done": true}}`, the id from `GET /api/push`'s `devices`).
1814
+ finished task) are **off** by default, per device: the **Long turns done** switch in the devices sheet turns them on.
1803
1815
  - **Quiet while you look**: nothing is pushed while any chi web page (a tab on the Mac, or the app) is in front and
1804
1816
  was touched in the last 2 minutes; what came meanwhile is never pushed later (the page has it). A page left in
1805
1817
  front untouched stops quieting the phone after about 2 minutes.
@@ -124,7 +124,8 @@ right-hand column happens.
124
124
  only when the first participant joins and when the last leaves or expires: one history re-read each, then stable.
125
125
  Under D3(a) the declared tools stay the session's, so an author switch costs nothing: the review boundary is a
126
126
  gate on the calls, not a toolkit switch. The forged-header mark (`(quoted) `) is a pure function of the text too.
127
- - Side requests (idle recap, `/btw`) go through `IdleClient` with their own short system prompt. On a current
127
+ - Side requests (idle recap, `/btw`) go through `IdleClient` with their own short system prompt (the recap's also
128
+ asks for its auto title line, `RecapPrompt::TITLE`: static text, still that one request). On a current
128
129
  llama.cpp they don't evict the session (see the server settings below); chi never pins them to a slot.
129
130
 
130
131
  ## The turn-end warm-up
@@ -4,8 +4,8 @@ chi web pushes to the owner's phone when a session needs them, for what the tab
4
4
  question, a step-limit question, a card with actions, a failed turn, and (per device, off by default) a done one. The
5
5
  phone runs chi web as a Home Screen app (iOS 16.4+), its bell subscribes the device, and chi web, the only sender,
6
6
  pushes through the device's push service (Apple's `web.push.apple.com`). The user side is in
7
- [configuration.md](../configuration.md#phone-notifications-web-push). This page is how it is built (P1 and P2a of the
8
- plan; the devices list in the page and `chi web push` come later).
7
+ [configuration.md](../configuration.md#phone-notifications-web-push). This page is how it is built (P1, P2a and P2b of the
8
+ plan: P2b is the devices sheet and `chi web push`).
9
9
 
10
10
  ```
11
11
  SessionHub ──emit──▶ Notifier#call(event) ──Queue──▶ sender thread
@@ -55,6 +55,8 @@ files, one per machine, never in config.yml:
55
55
  endpoint's SHA-256: the only name of a device outside the store. `Sub#public_h` (id, label, origin, the dates,
56
56
  `last_error`, `prefs: {done:}`) and `#inspect` never carry the endpoint or the keys; neither do the log lines.
57
57
  `prefs` is `{"done": false}` until `Store#set_prefs(id, done:, origin:)` (the PATCH route) sets it; `Sub#done?`.
58
+ `Store#remove_device(id, origin:)` removes by short id only on that origin (nil: any; the DELETE route, `chi web
59
+ push rm`); `#subscriptions!` is `#subscriptions` that raises `Corrupt` instead of reading none (the CLI says so).
58
60
  - `sent.json`: `{"version": 1, "keys": {KEY: iso time}}`, the attention keys already handled (a device took the
59
61
  show, or it was skipped while a page was in front, or capped), written by `Store#mark_sent` under `store.lock`,
60
62
  keys older than a day (`SENT_KEEP`) pruned on each write. `Store#sent_keys` reads it; one that doesn't read is
@@ -201,7 +203,10 @@ public URL, or `web.push.enabled: false`):
201
203
  - `POST /api/push/subscriptions {subscription: {endpoint, keys}, replaces?: id}` → 201 `{id, label}`; the label is
202
204
  chi's own ("iPhone · <date>", from the User-Agent's device class: iPhone, iPad, Mac, else Browser); `replaces`
203
205
  drops the device's earlier entry (`Store#add`).
204
- - `DELETE /api/push/subscriptions {endpoint}` → 204.
206
+ - `DELETE /api/push/subscriptions {endpoint}` → 204: this device's bell turned off.
207
+ - `DELETE /api/push/subscriptions/ID` → 204, 404 `unknown_device` (another origin's, or not a short id); logs
208
+ `push_removed`. The devices sheet's Remove for another device; this device's own row never uses it (its switch is
209
+ the bell, so the page's state and the store stay one).
205
210
  - `PATCH /api/push/subscriptions/ID {prefs: {done: true|false}}` → 200 the device's `Sub#public_h`; 404
206
211
  `unknown_device` (not this origin's, or not a short id: the id is a `([^/]+)` capture checked in the handler, as
207
212
  `participant_gate_spec`'s `path_for` fills only those), 400 `invalid_prefs`; logs `push_prefs`. ROUTES' first
@@ -209,7 +214,8 @@ public URL, or `web.push.enabled: false`):
209
214
  - `POST /api/push/presence {page, front}` → 204 (also with push off: a desk tab has nothing to be told), 400
210
215
  `invalid_presence`.
211
216
  - `POST /api/push/test {endpoint?}` → a test push to that device or every one on this origin, answered per device
212
- (`{devices: [{id, label, status, code, ms}]}`); the bell sends one after subscribing.
217
+ (`{devices: [{id, label, status, code, ms}]}`); the bell sends one after subscribing. The payload is
218
+ `Push::TEST_PAYLOAD` (TTL `TEST_TTL`), shared with `chi web push test`.
213
219
 
214
220
  With push off the subscription writes and the test answer 409 `push_off`.
215
221
 
@@ -240,6 +246,24 @@ a test push; a tap when on unsubscribes and deletes, and forgets the id, unless
240
246
  delete (revoked meanwhile): then the id stays for the next subscribe to replace. The tab's own OS notification is left to the push there; the title count stays. In a browser tab, or with
241
247
  chi web not pushing, the bell is the in-tab one, unchanged (no duplicate desktop pushes).
242
248
 
249
+ **The devices sheet** (`public/push_devices.js`, `createPushDevices` on `chip_popover.js`'s shell: a bottom sheet
250
+ under 600 px, else a popover at the bell; made once chi web says it pushes, so a tab never gets one): in push mode
251
+ the bell's tap opens it (`toggleNotify` → `sheet.toggle()`), and it reads `GET /api/push?endpoint=` on each open.
252
+ This device's row has the switch, the bell's own `togglePush` (permission from that tap; off forgets
253
+ `PUSH_DEVICE_KEY`), never Remove; every device has a **Long turns done** switch (`PATCH …/ID {prefs: {done}}`),
254
+ another device Remove (`DELETE …/ID`); `GET`'s `stale` is one line. One change at a time, the list read again after
255
+ each; a failure is said in the sheet (and the bell's tooltip). Pure `deviceRows` / `staleText` / `sheetModel` /
256
+ `sheetTemplate` run under node (push_devices.test.js); notifications.js takes the sheet as a `devices` factory.
257
+
258
+ **`chi web push`** (`lib/samagotchi/push_command.rb`, `PushCommand`): `ls [--format json]`, `rm ID`, `test [ID]` on
259
+ this machine's `Store` directly (chi web need not run). "Stale" is another origin than
260
+ `PushCommand.current_origin`: the running chi web's (`Web::Running.live_public_url`), else `web.public_url`'s, none
261
+ marked when neither. `test` builds a `Client` per device (`Push.load_key`, `Push.contact(sub.origin)`), stamps the
262
+ delivery (`Store#mark`) and removes a `:gone` device, as the route does. bin/chi branches on `ARGV[0..1] == web
263
+ push` after the `SUBCOMMANDS` dispatch and before `RunOptions.parse` (which refuses a stray word after `web`); it is
264
+ no `SUBCOMMANDS` key (`web` is RunOptions', and the contract spec would want a TABLE/MACHINE entry). `chi --remote X
265
+ web push …` is refused before that by `SessionAccess::Dispatch` (`web` is local only).
266
+
243
267
  **The device's tidy-up** (`syncDevice`, the Home Screen app with the bell on): closes the shown notifications whose
244
268
  tag is a waiting key (`push.js isWaitingTag`, so `chi:test` and a failed or done turn's stay) and isn't open in the
245
269
  page's `byId` (`push.js openPushKeys`: `Attention.open_keys`'s rule), and sets or clears the app
data/docs/sessions.md CHANGED
@@ -237,9 +237,9 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
237
237
 
238
238
  ## Session titles
239
239
 
240
- A session is named by its first message until you give it a title: `/title Coordinator` (in the REPL, an attached terminal, the web composer or `chi send -m "/title Coordinator" ID`; a parent agent may name its delegates the same way). The title is saved in the session file (`title`), whitespace collapsed and cut at 80 characters, and runs mid-turn too, at once. `/title` alone shows it (`title: none (shows the first message: hey chi …)` without one); `/title --clear` drops it, and the first message names the session again. A session whose first line was the command itself (`chi send --new -m "/title X"`) takes its preview from its first real message.
240
+ A session is named by its first message until you give it a title: `/title Coordinator` (in the REPL, an attached terminal, the web composer or `chi send -m "/title Coordinator" ID`; a parent agent may name its delegates the same way). The title is saved in the session file (`title`), whitespace collapsed and cut at 80 characters, and runs mid-turn too, at once. `/title` alone shows it (`title: none (shows the auto title: Parser tab handling)` or `title: none (shows the first message: hey chi …)` without one); `/title --clear` drops it, and the first message names the session again. A session whose first line was the command itself (`chi send --new -m "/title X"`) takes its preview from its first real message.
241
241
 
242
- Without a title, the web names a session by its recap's first sentence once it has one (see [Session recap](cli.md#session-recap); its subject phrase stripped), else by its first message: on its card (which then leaves out its recap line, the same sentence), the families and chain rows, the top bar and the browser tab, the delete confirm and the search; `chi sessions list` already reads title, else recap, else the first message. The notifications keep the first message. A participant's page is unchanged (the title or the review's subjects).
242
+ Without a title, a session is named by its **auto title** once its recap has one, else by its recap's first sentence (see [Session recap](cli.md#session-recap); its subject phrase stripped), else by its first message: on its card (which leaves out its recap line when the name is that same sentence), the families and chain rows, the top bar and the browser tab, the delete confirm and the search, the tab's and the phone's notifications, and `chi sessions list` (plain and `--live`; json has `auto_title`, tsv and json's `desc` keep the first message). The auto title is 2-6 words the recap's model writes on the line before the recap, in the same request (`Title: …`, split off by `RecapTitle`; the recap text never holds it). It is saved in the session's `recap.json` (`title`), so it goes when a stale recap is dropped; each later recap is shown the earlier title and keeps it unless the focus moved, and a reply without a usable one (not 2-6 words, or naming the user or the assistant) keeps the earlier title. A hand title always wins and is never overwritten. With the recap off no new auto title is written; a saved one still shows. The auto title is for display only: never in a model request (the `list_sessions` tool, `ctx.sessions`, a parent's children, `chi broadcast` keep the title, else the first message), nor on the terminal's status row, nor on a participant's page (the title or the review's subjects).
243
243
 
244
244
  The title names the session wherever its first message did: `chi sessions list` (plain, `--live`, tsv and json's `desc`, before the recap; json has `title` too), `chi sessions delete`'s line, the `list_sessions` tool, `chi broadcast`, the desktop helper, a parent's children (the coordinator bundle's `/children` card, `ctx.sessions.children`; `ctx.sessions.read` too), and in the web the cards, the families and chain rows, the top bar and the browser tab, the notifications and the search; the terminal's status row too (the REPL's and an attached one's, first, see [Status Line](cli.md#status-line)). In the web, `✎` (Rename this session) sits next to the session's title, on its active strip card or after the top bar's title: a small input over the title, prefilled with it; Enter or a click elsewhere sends `/title TEXT` when it changed (empty: `/title --clear`), Esc closes it unsent, and so does the title leaving the screen (All sessions opened, the card swiped out of a phone's strip). A session a chi REPL owns has no `✎`, nor one that shows no title yet (`/title` still works from the composer). A continue's new link keeps the title (see [Session chains](#session-chains)). An older `chi web` (or any chi from before the field) that saves a session drops its title: restart `chi web` after upgrading.
245
245
 
@@ -257,7 +257,7 @@ A session can **continue** an earlier one: "the next run of the same routine" (a
257
257
  - **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
258
258
  - **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model's host can't run it when no default model is configured to fall back to. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
259
259
  - **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context`, `thinking`, `settings`, `toolkit`, `tools`, `no_tools` or `parent_answers` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
260
- - **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft written in this session stays instead; one carried over from another session or the start page gives way to the opener, and the toast's **Restore draft** brings it back): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's preview (and its title, if it has one) when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link with a recap of its own reads its recap's first sentence instead (a link without one reads its chain's name, and a first link's recap names its chain). A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its own title when it differs from the card's (a link inherits its chain's, so mostly it doesn't), else its recap's first sentence, else its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
260
+ - **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft written in this session stays instead; one carried over from another session or the start page gives way to the opener, and the toast's **Restore draft** brings it back): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's preview (and its title, if it has one) when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link with a recap of its own reads its auto title, else its recap's first sentence, instead (a link without one reads its chain's name, and a first link's recap names its chain). A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its own title when it differs from the card's (a link inherits its chain's, so mostly it doesn't), else its recap's first sentence, else its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
261
261
  - **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
262
262
  - **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
263
263
 
@@ -5,6 +5,7 @@ require_relative "config"
5
5
  require_relative "model_profile"
6
6
  require_relative "host_registry"
7
7
  require_relative "host_keys"
8
+ require_relative "gem_update"
8
9
  require_relative "bootstrap/probe"
9
10
  require_relative "bootstrap/config_writer"
10
11
  require_relative "bootstrap/bundles"
@@ -18,7 +19,8 @@ module Samagotchi
18
19
  # Anthropic's Messages API, Bootstrap::Probe), picks the model, sends one
19
20
  # test request and writes config.yml: a fresh file, or a hosts: entry
20
21
  # added to an existing one. Then it installs the
21
- # system bundle and the core profile, and on a terminal asks about dev.
22
+ # system bundle and the core profile, and on a terminal asks about dev
23
+ # and Markdown in chi web (installing commonmarker when it is missing).
22
24
  # Prompts only on a terminal; a script gets the list and exit 2 instead.
23
25
  class BootstrapCommand
24
26
  include CLI::Command
@@ -53,7 +55,7 @@ module Samagotchi
53
55
  entry added (after a backup), and its other lines stay as they are.
54
56
  Then it installs the system bundle and the core bundles (loop-guard,
55
57
  check-in, guardrails, slow-commands, agents-md), and on a terminal
56
- offers the dev bundles.
58
+ offers the dev bundles and Markdown in chi web.
57
59
  TEXT
58
60
 
59
61
  FLAGS = CLI::Flags.new(help: CLI::Command::HELP_WORDS) do |f|
@@ -75,13 +77,18 @@ module Samagotchi
75
77
  DEV = "dev"
76
78
  # The config outcomes after which the bundles are installed.
77
79
  INSTALLS_BUNDLES = %i[new appended exists snippet].freeze
80
+ # The optional gem chi web renders Markdown with (web.markdown).
81
+ MARKDOWN_GEM = "commonmarker"
82
+ MARKDOWN_HINT = "answers stay plain text — gem install #{MARKDOWN_GEM}, then chi web --web-markdown".freeze
78
83
 
79
84
  # @param argv [Array<String>] the arguments after "bootstrap"
80
85
  # @param probe [Bootstrap::Probe, nil] (specs)
81
86
  # @param bundles [Bootstrap::Bundles, nil] (specs: the bundles go where
82
87
  # the process ENV points, not +env+)
88
+ # @param gem_runner [#run] runs `gem install commonmarker` (GemUpdate::Runner)
89
+ # @param gem_present [#call, nil] → whether commonmarker is installed
83
90
  def initialize(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr, env: ENV, probe: nil, config_path: nil,
84
- platform: RbConfig::CONFIG["host_os"], bundles: nil)
91
+ platform: RbConfig::CONFIG["host_os"], bundles: nil, gem_runner: GemUpdate::Runner.new, gem_present: nil)
85
92
  @argv = argv.dup
86
93
  @stdin = stdin
87
94
  @stdout = stdout
@@ -91,6 +98,9 @@ module Samagotchi
91
98
  @config_path = config_path || ConfigFile.global_path(env: env)
92
99
  @platform = platform
93
100
  @bundles = bundles || Bootstrap::Bundles.new
101
+ @gem_runner = gem_runner
102
+ # Installed, without loading its native extension into this process.
103
+ @gem_present = gem_present || -> { Gem::Specification.find_all_by_name(MARKDOWN_GEM).any? }
94
104
  end
95
105
 
96
106
  # @return [Integer] 0 written (or already there), 1 failed (a failed
@@ -358,7 +368,8 @@ module Samagotchi
358
368
  end
359
369
 
360
370
  # The system bundle, core, and dev when a terminal says yes: one line
361
- # each. 1 when something failed.
371
+ # each. Then Markdown in chi web (#offer_markdown). 1 when a bundle
372
+ # failed; the Markdown step never changes it.
362
373
  def install_bundles(dry_run: false)
363
374
  ok = system_bundle_line(@bundles.sync_system(dry_run: dry_run), dry_run)
364
375
  ok = install_profile(CORE, dry_run) && ok
@@ -366,9 +377,61 @@ module Samagotchi
366
377
  @stdout.print("Also install #{DEV} (#{members.join(", ")})? [y/N] ")
367
378
  ok = install_profile(DEV, false) && ok if @stdin.gets.to_s.strip.downcase.start_with?("y")
368
379
  end
380
+ offer_markdown(dry_run)
369
381
  ok ? 0 : 1
370
382
  end
371
383
 
384
+ # On a terminal, unless config.yml has web.markdown on already: ask, and
385
+ # on yes install commonmarker when it is missing, then write
386
+ # web.markdown: true. A closed stdin is no, whatever the default.
387
+ # Sets @markdown_on for #next_steps.
388
+ def offer_markdown(dry_run)
389
+ @markdown_on = markdown_on?
390
+ return if @markdown_on || !tty?
391
+ return @stdout.puts("web: would ask about Markdown in chi web") if dry_run
392
+
393
+ @stdout.print("Render answers as Markdown in chi web? [Y/n] ")
394
+ answer = @stdin.gets
395
+ return if answer.nil?
396
+
397
+ answer = answer.strip.downcase
398
+ return unless answer.empty? || answer.start_with?("y")
399
+
400
+ @markdown_on = install_markdown_gem && write_markdown
401
+ end
402
+
403
+ # Whether @config_path (not the process's Config) has web.markdown on.
404
+ def markdown_on?
405
+ data = ConfigFile.read_yaml(path: @config_path)
406
+ raw = data["web"]["markdown"] if data.is_a?(Hash) && data["web"].is_a?(Hash)
407
+ Config.coerce!(Config.find_by_key("web.markdown"), raw) == true
408
+ rescue SessionConfig::Error
409
+ false
410
+ end
411
+
412
+ # true when commonmarker is there or `gem install` (this Ruby's own gem
413
+ # command) put it there; else one line with gem's last word on it.
414
+ def install_markdown_gem
415
+ return true if @gem_present.call
416
+
417
+ @stdout.puts("#{MARKDOWN_GEM}: installing…")
418
+ output, ok = @gem_runner.run([GemUpdate.gem_bin, "install", MARKDOWN_GEM, "--no-document"])
419
+ return true if ok
420
+
421
+ last = output.to_s.lines.map(&:strip).reject(&:empty?).last || "no output"
422
+ @stdout.puts("#{MARKDOWN_GEM}: install failed (#{last}); #{MARKDOWN_HINT}")
423
+ false
424
+ end
425
+
426
+ def write_markdown
427
+ ConfigFile.write_setting!("web.markdown", true, path: @config_path)
428
+ @stdout.puts("web: markdown on (config.yml)")
429
+ true
430
+ rescue StandardError => e
431
+ @stdout.puts("web: markdown not written (#{e.message.lines.first.to_s.strip}); #{MARKDOWN_HINT}")
432
+ false
433
+ end
434
+
372
435
  # One line for the profile; false when it (or a member) failed.
373
436
  def install_profile(name, dry_run)
374
437
  profile_line(@bundles.install(name, dry_run: dry_run), dry_run)
@@ -477,7 +540,9 @@ module Samagotchi
477
540
  def next_steps
478
541
  dev = dev_to_install
479
542
  bundles = dev.empty? ? ["chi bundle list", "the installed bundles"] : ["chi bundle install #{DEV}", "more bundles (#{dev.join(", ")})"]
480
- lines = [["chi", "a session"], ["chi web --open", "the web UI"], bundles]
543
+ lines = [["chi", "a session"], ["chi web --open", "the web UI"]]
544
+ lines << ["chi web --web-markdown", "Markdown answers#{" (needs gem install #{MARKDOWN_GEM})" unless @gem_present.call}"] unless @markdown_on
545
+ lines << bundles
481
546
  lines << ["chi desktop install", "macOS \"Send to chi\" helper"] if @platform.to_s.include?("darwin")
482
547
  lines.each_with_index do |(command, what), i|
483
548
  @stdout.puts("#{i.zero? ? "next:" : " "} #{command.ljust(22)} # #{what}")
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.112.0
3
+ version: 0.114.0
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:
@@ -1245,6 +1245,30 @@ module Samagotchi
1245
1245
  true
1246
1246
  end
1247
1247
 
1248
+ # Sets one registry key (a Config::Entry with a section.key yaml_path,
1249
+ # e.g. "web.markdown") in config.yml, its value typed as the entry
1250
+ # reads it (true is written bare); the rest of the file, comments
1251
+ # included, stays as written (ConfigTextEdit). No Config.reload!: a
1252
+ # caller reading Config.get afterwards reloads itself.
1253
+ # @raise [ArgumentError] an unknown key, or one config.yml doesn't hold
1254
+ # as section.key
1255
+ # @raise [SessionConfig::Error] a value the entry's type doesn't take
1256
+ def write_setting!(key, value, env: ENV, path: global_path(env: env))
1257
+ entry = Config.find_by_key(key)
1258
+ raise ArgumentError, "unknown config key: #{key}" unless entry
1259
+ raise ArgumentError, "#{key} isn't a section.key setting of config.yml" unless entry.config_exposed? && entry.yaml_path.size == 2
1260
+
1261
+ typed = Config.coerce!(entry, value)
1262
+ raise ArgumentError, "#{key} needs a value" if typed.nil?
1263
+
1264
+ section, leaf = entry.yaml_path
1265
+ expected = deep_copy(read_yaml(env: env, path: path) || {})
1266
+ expected[section] = {} unless expected[section].is_a?(Hash)
1267
+ expected[section][leaf] = typed
1268
+ write_config_key(path, section, leaf, typed, expected)
1269
+ typed
1270
+ end
1271
+
1248
1272
  def model_aliases(env: ENV, path: global_path(env: env))
1249
1273
  data = read_yaml(env: env, path: path)
1250
1274
  return {} unless data.is_a?(Hash)
@@ -25,7 +25,8 @@ module Samagotchi
25
25
  # @param text [String, nil] the file's text (nil: no file)
26
26
  # @param section [String] a top-level key ("default")
27
27
  # @param key [String] the key under it; an existing key matches in any case
28
- # @param value [String] the value, written as a quoted string
28
+ # @param value [String, Integer, Boolean] the value: a string quoted, an
29
+ # Integer or true/false bare (as the file's types read them back)
29
30
  # @param expected [Hash] the whole file's data after the change
30
31
  # @return [String] the new text
31
32
  def set(text, section:, key:, value:, expected:)
@@ -77,7 +78,13 @@ module Samagotchi
77
78
  line[/\A\s*(["']?)([^"':#\s][^"':#]*?)\1:(?:\s|\z)/, 2]
78
79
  end
79
80
 
80
- # JSON's quoting is valid YAML: model ids hold "/" and ":".
81
- def scalar(value) = JSON.generate(value.to_s)
81
+ # JSON's quoting is valid YAML: model ids hold "/" and ":". true, false
82
+ # and Integers go bare, so they read back as themselves, not as text.
83
+ def scalar(value)
84
+ case value
85
+ when true, false, Integer then value.to_s
86
+ else JSON.generate(value.to_s)
87
+ end
88
+ end
82
89
  end
83
90
  end
@@ -39,8 +39,12 @@ module Samagotchi
39
39
  end
40
40
  end
41
41
 
42
+ # This Ruby's own gem command (chi bootstrap installs commonmarker with
43
+ # it too).
44
+ def self.gem_bin = File.join(RbConfig::CONFIG["bindir"], "gem")
45
+
42
46
  # @param fetcher [#call, nil] → the latest.json body (specs)
43
- def initialize(env: ENV, fetcher: nil, runner: Runner.new, gem_bin: File.join(RbConfig::CONFIG["bindir"], "gem"))
47
+ def initialize(env: ENV, fetcher: nil, runner: Runner.new, gem_bin: GemUpdate.gem_bin)
44
48
  @env = env
45
49
  @fetcher = fetcher || method(:fetch)
46
50
  @runner = runner