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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/bin/chi +8 -0
- data/docs/cli.md +24 -6
- data/docs/configuration.md +22 -10
- data/docs/internals/prompt-caching.md +2 -1
- data/docs/internals/web-push.md +28 -4
- data/docs/sessions.md +3 -3
- data/lib/samagotchi/bootstrap_command.rb +70 -5
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/config.rb +24 -0
- data/lib/samagotchi/config_text_edit.rb +10 -3
- data/lib/samagotchi/gem_update.rb +5 -1
- data/lib/samagotchi/idle_recap.rb +38 -10
- data/lib/samagotchi/push_command.rb +188 -0
- data/lib/samagotchi/recap_store.rb +20 -3
- data/lib/samagotchi/recap_title.rb +78 -0
- data/lib/samagotchi/run_options.rb +1 -0
- data/lib/samagotchi/session_manager.rb +10 -3
- data/lib/samagotchi/sessions_command.rb +14 -11
- data/lib/samagotchi/title_command.rb +6 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +46 -3
- data/lib/samagotchi/web/markdown_renderer.rb +20 -0
- data/lib/samagotchi/web/public/app.js +6 -0
- data/lib/samagotchi/web/public/index.html +43 -4
- data/lib/samagotchi/web/public/markdown_tip.js +33 -0
- data/lib/samagotchi/web/public/notifications.js +22 -8
- data/lib/samagotchi/web/public/push_devices.js +187 -0
- data/lib/samagotchi/web/push/attention.rb +2 -1
- data/lib/samagotchi/web/push/store.rb +14 -0
- data/lib/samagotchi/web/push.rb +5 -0
- data/lib/samagotchi/web/push_routes.rb +17 -3
- data/lib/samagotchi/web/server.rb +46 -14
- data/lib/samagotchi/web/session_show.rb +2 -0
- data/lib/samagotchi/web/session_summary.rb +7 -2
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a59aa63d1ccf6a6114afcaf5b1cf19088cb482989c08acb3aa5bfede3a67358d
|
|
4
|
+
data.tar.gz: 3fe5245e7c864ca0b814d9b187b402d5658ffd88aa5d895881482f4b40e9391f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
|
713
|
-
the
|
|
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
|
-
|
|
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
|
|
1183
|
-
|
|
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
|
|
data/docs/configuration.md
CHANGED
|
@@ -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).
|
|
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:
|
|
1737
|
-
|
|
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.
|
|
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.
|
|
1788
|
-
|
|
1789
|
-
|
|
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
|
|
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
|
|
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
|
data/docs/internals/web-push.md
CHANGED
|
@@ -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
|
|
8
|
-
plan
|
|
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,
|
|
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
|
|
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"]
|
|
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}")
|
data/lib/samagotchi/config.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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
|