samagotchi 0.132.0 → 0.133.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/bin/chi +16 -0
  4. data/docs/architecture.md +28 -1
  5. data/docs/cli.md +1 -1
  6. data/docs/configuration.md +55 -12
  7. data/docs/desktop.md +2 -1
  8. data/docs/guardrails.md +7 -1
  9. data/docs/sessions.md +60 -6
  10. data/docs/testing.md +1 -1
  11. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  12. data/lib/samagotchi/bundles/mcp/plugin.rb +10 -3
  13. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  14. data/lib/samagotchi/chi_shim.rb +77 -0
  15. data/lib/samagotchi/child_env.rb +5 -3
  16. data/lib/samagotchi/config.rb +2 -0
  17. data/lib/samagotchi/context_fetch.rb +4 -2
  18. data/lib/samagotchi/desktop/macos.rb +26 -0
  19. data/lib/samagotchi/guardrails/pair_boundary.rb +21 -2
  20. data/lib/samagotchi/guardrails/pair_command.rb +57 -1
  21. data/lib/samagotchi/guardrails/review_paths.rb +7 -4
  22. data/lib/samagotchi/home_command.rb +15 -19
  23. data/lib/samagotchi/home_doctor.rb +177 -0
  24. data/lib/samagotchi/home_notice.rb +80 -0
  25. data/lib/samagotchi/project_env.rb +360 -0
  26. data/lib/samagotchi/self_report.rb +33 -0
  27. data/lib/samagotchi/session_commands.rb +4 -2
  28. data/lib/samagotchi/session_manager.rb +18 -0
  29. data/lib/samagotchi/testing/boundary.rb +3 -1
  30. data/lib/samagotchi/tools/builtins.rb +13 -2
  31. data/lib/samagotchi/version.rb +1 -1
  32. data/lib/samagotchi/web/app.rb +50 -0
  33. data/lib/samagotchi/web/public/app.js +4 -1
  34. data/lib/samagotchi/web/public/chip_popover.js +5 -2
  35. data/lib/samagotchi/web/public/composer.css +11 -1
  36. data/lib/samagotchi/web/public/data.js +8 -4
  37. data/lib/samagotchi/web/public/index.html +51 -13
  38. data/lib/samagotchi/web/public/level_chip.js +22 -38
  39. data/lib/samagotchi/web/public/llm_context_chip.js +26 -70
  40. data/lib/samagotchi/web/public/review_start.js +13 -57
  41. data/lib/samagotchi/web/public/send_turn.js +3 -2
  42. data/lib/samagotchi/web/public/start_page.js +117 -97
  43. data/lib/samagotchi/web/public/start_settings.js +298 -0
  44. data/lib/samagotchi/web/public/start_toolkit.js +157 -0
  45. data/lib/samagotchi/web/public/thinking_chip.js +9 -22
  46. metadata +7 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ebe264e1dbd5adc620889aa40d66fd458519c64f038a3a3e5de06ee6a6da46e7
4
- data.tar.gz: 3c72148287422c9821c25bc3979c71890fb4abad81af3829650aef3c13549a3f
3
+ metadata.gz: 8b85b6006a6570eb4145c39cf37ad004a56332b33240353c78d15529d532cd71
4
+ data.tar.gz: 6c194c4f62179e781a121e1e35eb9f8407795cb6e2b519ffa3ff917d0b6096eb
5
5
  SHA512:
6
- metadata.gz: f8ffaf8ca17c125a38af6a30432d8b14a66ea97df8201686acedca8dde11869845b0e2a6abe0f9ece18db4e5848f9340b2ef73910371977fafe9889d430eebad
7
- data.tar.gz: e556232bbad1093cff1779510162a179b1284cc90bd8cc54d8082e2a4dab5e96f98bb6ebae5ef99ab82610a1bd33880c830cfae8b1076d1e6ca841c406ae45b3
6
+ metadata.gz: 777ad65e1223ba4f99058f5ecdd3d89621af1b1f2f61ff02b21a0760607a92d18d179f9da49dae7c257a07ac107fdcfadd2c0bcf14c8555a25a1570f07c8729e
7
+ data.tar.gz: e5115ba552ca82ac8a1f6d79f917d50d970bcd7202866a3ac30a15a89541ea94b61ef0a2ff61f0594f996c2a874aa01e7fe1cb3c4cf1551357f017417416c217
data/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ and commands may change between minor versions. How releases are made:
7
7
  [docs/releasing.md](docs/releasing.md).
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.133.0] - 2026-10-30
11
+
12
+ ### Added
13
+
14
+ - Sessions follow their project: a session's worker reads its folder's `mise env` once at start, and `execute`,
15
+ tasks, `!cmd`, MCP servers and context sources run in it, getting the project's Ruby (and node, …) whoever started
16
+ the worker (`session.project_env: mise`, the default; `off` skips mise). chi's own process keeps its
17
+ env (its keys, proxies and config never come from a project). The starter's GEM_HOME/GEM_PATH are dropped when the
18
+ Ruby changes. `chi` inside those tools stays the chi home's (a `chi` script in `$XDG_STATE_HOME/samagotchi/bin`). A
19
+ mise.toml edit needs `chi sessions restart`; a `mise env` that fails (an untrusted mise.toml) changes nothing and
20
+ logs one `project_env` warning. A session started from a tool for another folder never gets this one's env.
21
+ `chi self` shows `home` and `project env`. See docs/sessions.md (Project env).
22
+ - Pair turns: their `execute` runs without the project env; a command that runs `mise` or `direnv`, and a write to
23
+ mise's config (the owner's next worker start runs it), ask the owner; `.envrc` and mise's local files are secrets,
24
+ as `.env` is.
25
+ - Bundles: mcp 0.7.3 starts its servers in the session's project env.
26
+ - `chi home doctor` checks what should follow the chi home, one row each with `ok`/`warn`/`fail` and the command to
27
+ run: the chi you ran, the desktop helper's launch file (`chi desktop upgrade`), sessions saved by a newer chi than
28
+ the home's, the tools' `chi` script, a Gemfile.lock in this project that bundles samagotchi, mise, and the last
29
+ worker's `project_env` warning in this folder (e.g. `mise trust`). See docs/configuration.md (Checking it).
30
+ - A chi run at a terminal on another Ruby than the chi home's says so in one dim line, at most once a day
31
+ (`chi X on <ruby>; the chi home is <ruby> (chi Y) (chi home doctor)`).
32
+ - Web start page: a 🧰 beside ⚙ picks the new session's toolkit. **Auto** (the default, nothing sent) leaves the
33
+ first message to `toolkit.suggest.mode: on`'s suggestion when one can come, else `toolkit.default`; a picked kit is
34
+ sent (no suggestion then) and shows as a `tools: lean` chip (a dot on a phone). The list holds the built-in and
35
+ config kits a new session can start on; `GET /api/models` lists them as `toolkits`. A kit Start refuses is said in
36
+ the list. See docs/configuration.md (Toolkits).
37
+
38
+ ### Changed
39
+
40
+ - Web start page: thinking, LLM context and review files move into one ⚙ panel beside the model picker (a bottom
41
+ sheet on a phone): each a row of segments that applies at once, the first one the picked model's own
42
+ (`default (high)`), the context's apply rule and budget under **Apply and budget ▸**, and **Review files…** as a
43
+ view of its own. Only what is set shows on the row, as accent chips (`think high`, `ctx stale`,
44
+ `review: README.md`) that open the panel; on a phone ⚙ carries a dot instead. Still for the next new session
45
+ only. See docs/configuration.md (A new web chat).
46
+
47
+ Update with `chi update` (mcp 0.7.3), then restart `chi web` and the session workers (`chi sessions restart`). If
48
+ you use the desktop helper, run `chi desktop upgrade` once; `chi home doctor` says what else to do.
49
+
10
50
  ## [0.132.0] - 2026-10-29
11
51
 
12
52
  ### Added
@@ -5037,7 +5077,8 @@ and long-lived sessions.
5037
5077
  - A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
5038
5078
  hotkey panel that send selected text or the clipboard to your sessions.
5039
5079
 
5040
- [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.132.0...HEAD
5080
+ [Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.133.0...HEAD
5081
+ [0.133.0]: https://github.com/dm1try/samagotchi/compare/v0.132.0...v0.133.0
5041
5082
  [0.132.0]: https://github.com/dm1try/samagotchi/compare/v0.131.0...v0.132.0
5042
5083
  [0.131.0]: https://github.com/dm1try/samagotchi/compare/v0.130.1...v0.131.0
5043
5084
  [0.130.1]: https://github.com/dm1try/samagotchi/compare/v0.130.0...v0.130.1
data/bin/chi CHANGED
@@ -8,6 +8,14 @@ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
8
8
  require "samagotchi/utf8_default"
9
9
  Samagotchi::Utf8Default.apply!
10
10
 
11
+ # A chi a session's tool runs: back to that worker's own env before anything
12
+ # reads config (its API keys and proxies, not the project's mise env), and
13
+ # the workers it starts never take the project's (docs/sessions.md, Project env).
14
+ if ENV.key?("SAMAGOTCHI_PROJECT_ENV_ORIG")
15
+ require "samagotchi/project_env"
16
+ Samagotchi::ProjectEnv.take_orig!
17
+ end
18
+
11
19
  require "samagotchi/config"
12
20
 
13
21
  begin
@@ -27,6 +35,14 @@ if ENV.key?("SAMAGOTCHI_WEB_RESTARTED") || ENV.key?("SAMAGOTCHI_WEB_ADOPT_PIDS")
27
35
  web_restarted = Samagotchi::Web::Server.take_restart_env!
28
36
  end
29
37
 
38
+ # A person's chi at a terminal on another Ruby than the chi home's: one dim
39
+ # line a day saying so (HomeNotice: never in a worker's tools, a script or
40
+ # machine output).
41
+ unless web_restarted
42
+ require "samagotchi/home_notice"
43
+ Samagotchi::HomeNotice.show
44
+ end
45
+
30
46
  # `chi self`: print version, source dir, config/memory/session paths, model and
31
47
  # bundles (read-only, offline) — lets the agent find its own code and state.
32
48
  if ARGV[0] == "self"
data/docs/architecture.md CHANGED
@@ -158,11 +158,38 @@ home's Ruby is gone, when no `samagotchi-*.gemspec` is under its gem path, and f
158
158
  | `Desktop::MacOS#launch_config` | argv `CHI_BOOT + ["--"]`; `launch_ok?` / `launch_problem` check only absolute argv entries | `[ruby, wrapper or bin/chi]` |
159
159
  | `InstalledVersions`, `Web::UpdateCheck` | the home's specification dirs and gem dir | the running process's |
160
160
 
161
- Unchanged: `ContextProviders`' `{ruby}` (`RbConfig.ruby`: the home's ruby inside a home worker), `chi bundle
161
+ Unchanged: `ContextProviders`' `{ruby}` (`RbConfig.ruby`: the home's ruby inside a home worker; it runs under the
162
+ project env below, so a `mise env` that sets GEM_HOME reaches it), `chi bundle
162
163
  try/test/check` (`-I LIB_DIR`), hooks (in-process). The worker's boot string, `run_session_loop(id, state_dir:)` and
163
164
  `SAMAGOTCHI_BOOT_FALLBACK` stay the cross-version contract (`spec/worker_contract_spec.rb`): the home only adds the
164
165
  prelude in front. An older chi that wakes a session doesn't read home.json and starts its worker as before.
165
166
 
167
+ **The tools a worker runs** follow the project, not the boot, and only they do: a spawned worker
168
+ (`WORKER_PROCESS_ENV`; never an in-process loop) calls `ProjectEnv.compute` in `run_session_loop`, after the owner
169
+ lock and before `Worker.new`, and installs the result (`ProjectEnv.install!`; a frozen `ProjectEnv::Overlay`,
170
+ `{key => value or nil}`). Its ENV is never written: chi's own process (LLM calls, `HostKeys`, Net::HTTP proxies,
171
+ Config, Paths, hooks, plugins) keeps the spawner's env. The overlay is the session folder's `mise env -C DIR --json`
172
+ (mise's `__MISE_*`/`MISE_SHELL` unset; the spawner's GEM_HOME/GEM_PATH unset when the first `ruby` on PATH changes,
173
+ unless mise sets them; never `HOME`, `XDG_*`, `SAMAGOTCHI_*`, `CHI_REMOTE`, `RACK_ENV`, `CI`), `ChiShim`'s dir
174
+ (`$XDG_STATE_HOME/samagotchi/bin`, only a `chi` that execs `Record#boot_argv(run: CHI_BOOT)`) first on PATH when there
175
+ is a home, `SAMAGOTCHI_PROJECT_ENV` (`mise:<ruby>` or `none:<reason>`, for `chi self`) and
176
+ `SAMAGOTCHI_PROJECT_ENV_ORIG` (the worker's own value of each key it changes). It is merged into: `execute` and
177
+ `task_create` for the owner's turns (`Builtins.project_env`, less `TaskRuntime::SANITIZED_ENV_KEYS`; never a
178
+ participant's turn), `!cmd` (`SessionCommands#shell`), context sources (`ContextFetch`) and MCP servers (the mcp
179
+ bundle's `server_env`). A worker started from the worker inherits its clean ENV. A `chi` a tool runs (through the shim or not, a fresh
180
+ process with the tools' env) takes the worker's values back from the record in `bin/chi`, before anything reads
181
+ config (`ProjectEnv.take_orig!`): its own API keys and proxies, and the env of any worker it starts, are the
182
+ worker's (`ChildEnv` still drops the record and marker for a child). `restore_env` never returns a reserved key and
183
+ returns `DYLD_*`/`LD_*`/`RUBYOPT`/`RUBYLIB`/`RUBYGEMS_*`/`BUNDLE*` only as unsets. Hooks run in-process, with the
184
+ worker's env. Specs never reach a real mise: `SAMAGOTCHI_MISE_CLI` (only that path when set) points at a missing
185
+ file. User side: [sessions.md](sessions.md#project-env).
186
+
187
+ **Checking it.** `chi home doctor` (`HomeDoctor`) reads home.json and what should follow it, each condition's text
188
+ from its owner: `ChiHome.problem`, `HomeNotice.mismatch` (also the once-a-day notice `bin/chi` prints at a TTY on
189
+ another Ruby), `Desktop::MacOS#launch_problem` / `#home_problem`, `ChiShim.problem`, `ProjectEnv::NO_MISE` and
190
+ `ProjectEnv.last_warning` (the newest `project_env` / `project_env_applied` record for the folder, from the debug
191
+ log's last 4 MB). Rows: [configuration.md](configuration.md#checking-it-chi-home-doctor).
192
+
166
193
  ## Session retention & ordering
167
194
 
168
195
  - **Files:** `~/.local/state/samagotchi/sessions/<uuid>.json` + `<uuid>/input|output|owner.lock|bridge.json` (XDG-aware), and the `<uuid>/stopped` and `<uuid>/archived` marker files.
data/docs/cli.md CHANGED
@@ -35,7 +35,7 @@
35
35
  - `chi models [--format text|json] [--timeout S] [TEXT]` — list the models every configured host offers, as the names `--model` takes (see [Listing the models](#listing-the-models))
36
36
  - `chi self` — print version, source dir (checkout or installed gem), config/memory/session paths, model/host and bundles. Run by a session's `execute`, the `model` row (and host, loop, profile, thinking, served model) is **that session's** model, labelled `(this session <id8>; default …)`; elsewhere it is the default, `(default)`. `model key` is the memory overlay key (`<name>.<key>.md`), and `model notes` the model notes a prompt of that model loads (`model_notes_deepseek (system, 612 chars)`, `none`; inside a session its `--mute` list is left out). `chi self --model` prints only the model a new session starts on, an alias resolved to its target (the desktop helper's hint)
37
37
  - `chi update [--dry-run] [--to VERSION] [--json] [--no-gem] [--no-bundles] [--no-desktop]` — update an installed chi: the gem, the system bundle, the shipped bundles you installed and the desktop helper, in one table (see [Updating](#updating))
38
- - `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it (for now: its Ruby is there and has samagotchi; exit 1 says what to do). This machine only (see [The chi home](configuration.md#the-chi-home))
38
+ - `chi home`, `chi home set [--ruby PATH]`, `chi home doctor` — the chi home: the one Ruby (and its gem path) every chi launch boots, whoever launches it. `chi home` shows it (its Ruby, the newest chi in it, gem home and path, `mise`, when and by which chi it was set, and the chi you ran when that is another one; exit 1 with the problem when it can't boot chi), `set` records this chi's Ruby and gem path (or `--ruby PATH`'s own; it must have samagotchi), `doctor` checks it and what should follow it, one row each with `ok`, `warn` or `fail` and what to run (exit 1 on a `fail`; see [Checking it](configuration.md#checking-it-chi-home-doctor)). At a terminal, a chi on another Ruby than the home's says so in one dim line a day. This machine only (see [The chi home](configuration.md#the-chi-home))
39
39
  - `chi bundle install|upgrade|uninstall|status|diff|list|build|trash|new|test|check` — manage memory bundles (see [Bundle hooks](hooks.md#bundle-hooks-unified-workflow-bundle)); `list` shows the installed ones and the ones shipped with chi, which `install <name>` installs; `core` and `dev` are profiles that install a set of them (see [Bundle profiles](memory.md#bundle-profiles-core-and-dev), [Guardrails](guardrails.md), [Plugins](plugins.md#the-btw-bundle), [the mcp bundle](plugins.md#the-mcp-bundle) [the loop-guard bundle](plugins.md#the-loop-guard-bundle), [the check-in bundle](plugins.md#the-check-in-bundle) and [the skills bundle](plugins.md#the-skills-bundle)). `trash` lists and empties the bundle trash (moved files from uninstalls/upgrades). A usage error (an unknown subcommand or flag, a missing argument, a bad `build --scope`) exits 2, as for the other subcommands. Plain `chi` and `chi web` exit 1 on an unknown flag, a flag missing its value or a stray argument. `new`, `test` and `check` are for writing one: see [Writing a bundle](#writing-a-bundle-new-test-check)
40
40
 
41
41
  - `chi guardrails replay [--dry-run] [--threshold T] [--model M]` — seed System 1's record from the approvals this machine's sessions already answered: each ask is judged by the configured model and written to `guardrails/judgments.jsonl` with the answer the user gave, then a per-rule table prints (asks, allowed, denied, would-be auto-allows, false allows, asks with no judgment). A parent agent's answer is not the user's: only its deny is replayed, and only where the parent could have allowed. `--dry-run` judges without writing, `--threshold` overrides `guardrails.system_one.threshold` for the table, `--model` the configured model for this run. Exit 2 when `guardrails.system_one.mode` is off or no `host` is set; a usage error exits 2 (see [System 1](guardrails.md#system-1-experimental))
@@ -70,6 +70,7 @@ model_aliases:
70
70
  session:
71
71
  shared: true
72
72
  # idle_exit_minutes: 30 # an unused worker exits after this (0 = never)
73
+ # project_env: mise # a worker's tools get its folder's `mise env` (off: the starter's env)
73
74
  # keep_empty: false # true keeps sessions nothing happened in (default: deleted when left)
74
75
  # max_children: 4 # running sessions one session may have delegated at a time (the delegate tool)
75
76
  # delegate_reports: wake # a child's reply reaches its parent by itself: wake (an idle parent runs a turn), queue, off
@@ -171,7 +172,7 @@ or `chi web` restart.
171
172
  | `hooks`, `bundles` | Worker start | Hooks, plugins and their bundle settings load with the engine. |
172
173
  | `recap.*` | Worker start | The engine builds its recap job at start, and again when it binds a session with its own `recap.enabled` or `recap.sentences` (`--set`, see "Per-session settings") and when `/recap on\|off\|default` changes the session's own value. |
173
174
  | `session.shared` | Next `chi` invocation | It selects attached-worker or in-process mode for that invocation. |
174
- | `session.idle_exit_minutes`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer; the other values are read for the next corresponding decision. |
175
+ | `session.idle_exit_minutes`, `session.project_env`, `session.keep_empty`, `session.max_children`, `session.delegate_reports`, `session.task_wakes`, `session.max_wakes` | Worker start or next matching lifecycle decision | Existing workers keep their idle-exit timer and project env; the other values are read for the next corresponding decision. |
175
176
  | `session.retention_*`, `session.max_count`, `session.keep_status`, `session.sweep_interval_hours` | Next retention sweep | `chi web` sweeps on its configured schedule and on `GET /api/sessions`; `chi sessions prune` uses the values for that invocation. The sweep interval is checked on the next hub tick. |
176
177
  | `web.*` except `web.participants.*` | `chi web` start | Restart `chi web` to change its bind address, port or page settings. |
177
178
  | `log.*` | Process start | Each process holds its configured log sink and level after first use; restart that process. |
@@ -655,7 +656,7 @@ models:
655
656
  came from);
656
657
  - `chi --thinking LEVEL` on a run that starts or resumes a session (the REPL, `-p`, `--resume`, an attached one);
657
658
  - `chi send --new --thinking LEVEL`;
658
- - the web's `think` chip, in the session bar and on the start page (`POST /api/sessions` takes `"thinking": "low"`;
659
+ - the web's `think` chip in the session bar, and the start page's ⚙ panel (`POST /api/sessions` takes `"thinking": "low"`;
659
660
  anything that isn't a level answers `400 invalid_thinking`).
660
661
  - `chi web --thinking LEVEL` is the default for the sessions its workers run, below a session's own. A session started
661
662
  with only the env (`SAMAGOTCHI_THINKING_LEVEL=low chi send --new …`) loses the level when a worker without that env
@@ -1383,8 +1384,9 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
1383
1384
  runs the home's `chi update`, and installs into the home's gems with the home's `gem`), `chi bootstrap`'s
1384
1385
  commonmarker install, chi web's "installed; restart" notice and the macOS helper after its next `chi desktop
1385
1386
  install` or `upgrade`. The boot sets the home's gem path inside Ruby (`Gem.paths`), never in an environment
1386
- variable: the commands a session runs (`execute`, tasks, `!cmd`, MCP servers) keep the GEM_HOME/GEM_PATH of whoever
1387
- started the worker, or none. How: [How chi launches itself](architecture.md#how-chi-launches-itself).
1387
+ variable: the commands a session runs (`execute`, tasks, `!cmd`, MCP servers) follow the session's project (its
1388
+ `mise env`, [Project env](sessions.md#project-env)), and their `chi` is the home's. How:
1389
+ [How chi launches itself](architecture.md#how-chi-launches-itself).
1388
1390
  - **No home, or one that can't boot chi** (its Ruby gone after a Ruby upgrade, samagotchi uninstalled there, a
1389
1391
  home.json that isn't JSON): each launch uses the Ruby that starts it, as without a home, and the log says why once
1390
1392
  per process (`home_unusable`, `home_invalid`). After a Ruby upgrade: `gem install samagotchi` with the new Ruby,
@@ -1397,6 +1399,30 @@ launches it. It is recorded in `~/.config/samagotchi/home.json` (`$XDG_CONFIG_HO
1397
1399
  (`log.file`) and exits, rather than leave the spawner waiting.
1398
1400
  - Each machine has its own home (a remote's is that machine's).
1399
1401
 
1402
+ ### Checking it: chi home doctor
1403
+
1404
+ `chi home doctor` prints one row per check, `ok`, `warn` or `fail`, each with what to run; it exits 1 when a row
1405
+ fails (a launch can't boot what it should), 0 otherwise. Run it in a project folder to check that project too.
1406
+
1407
+ | Row | When | What to run |
1408
+ |---|---|---|
1409
+ | the home | `fail`: its Ruby is gone (a Ruby upgrade) or has no samagotchi | `gem install samagotchi` with the new Ruby, then `chi home set` from it |
1410
+ | this chi | `warn`: the chi you ran is another Ruby's, or another version, than the home's (`ok` for a source checkout, which starts its own code) | `chi home set` to make this one the home, or run the home's |
1411
+ | the desktop helper | `fail`: its launch file's Ruby or chi is gone; `warn`: it runs `[ruby, chi]` (installed before the home), boots the home as it was before a `chi home set`, or sets `GEM_HOME`/`GEM_PATH` | `chi desktop install --force` / `chi desktop upgrade` |
1412
+ | sessions | `warn`: sessions last saved by a newer chi than the home's (their workers run without what it added) | `chi update` |
1413
+ | the tools' `chi` | `warn`: `$XDG_STATE_HOME/samagotchi/bin/chi` boots another chi than the home | the next worker start rewrites it (`chi sessions restart ID` for a running session) |
1414
+ | this project's Gemfile.lock | `warn`: the nearest Gemfile.lock up to the git root bundles samagotchi (`bundle exec chi` isn't supported) | `bundle remove samagotchi` there; run chi itself |
1415
+ | mise | `ok` with the mise found; without one, `warn` when `session.project_env: mise` is set explicitly (`ok` for the default) | install mise, or `session.project_env: off` |
1416
+ | the last worker here | `warn`: the newest worker record for this folder in the debug log is a `project_env` warning (an untrusted mise.toml, a timeout) | `mise trust` there (or fix what it says), then `chi sessions restart ID` |
1417
+
1418
+ The rows after the home's first one need a home that can boot chi; the last three are checked with or without one.
1419
+
1420
+ **The notice.** An installed chi run by a person at a terminal (stdin and stderr TTYs) on another Ruby, or another
1421
+ version, than the home's prints one dim line on stderr, at most once a day: `chi 0.131.0 on /x/bin/ruby; the chi home
1422
+ is /y/bin/ruby (chi 0.132.0) (chi home doctor)`. Never inside a session's tools, for `--json`, `--format json`,
1423
+ `--non-interactive`, `--version`, `--help`, `chi home`, `chi update` or `chi self`, from a source checkout, or in a
1424
+ test run (`SAMAGOTCHI_ENV`, `RACK_ENV`, `CI`). The day is stamped in `$XDG_STATE_HOME/samagotchi/home_noticed`.
1425
+
1400
1426
  ## Project specific description
1401
1427
 
1402
1428
  If an AGENTS.md file is present in the project root, samagotchi injects its
@@ -1548,6 +1574,18 @@ toolkit:
1548
1574
  `full`. A continue and a fork keep the toolkit name. `/toolkit` shows and changes it. The kit's tools, prompt
1549
1575
  text and group selectors are part of its definition, not the session file: edits reach a saved name at its next
1550
1576
  worker start.
1577
+ - **A new web chat: 🧰.** The start page's 🧰 (beside ⚙; a bottom sheet on a phone) picks the new session's toolkit.
1578
+ **Auto**, checked by default, sends nothing: the session is on `toolkit.default` with `toolkit_origin: default`, so
1579
+ with `toolkit.suggest.mode: on` its first message may switch it to a suggested kit. Auto is listed only when that
1580
+ can happen as far as chi web can tell (on mode, a `guardrails.system_one.host`, two described config kits); bundle
1581
+ and memory kits can add candidates it doesn't see. A kit picked is sent as `POST /api/sessions`'s `toolkit` and
1582
+ saved as `chosen`: no suggestion, the default kit included (its row says `default`); without Auto, the default's
1583
+ row is the one checked and sends nothing. The list holds the built-ins and config kits a new session can start
1584
+ on: not `review`, `child` or a kit named `undecided`, not a kit whose `groups:` refuses a new session, and not one
1585
+ that doesn't resolve in chi web (a base that is a bundle kit); bundle and memory kits load in the worker, so pick
1586
+ them with `/toolkit`. The configured default is always listed. A pick closes the list and shows a `tools: lean`
1587
+ chip on the row (a dot on 🧰 on a phone); it is for that create only, never remembered. A review session is
1588
+ always `chosen`: with review files set the list says it starts on the default unless you pick a kit.
1551
1589
  - **Left out: reachable or removed.** A built-in missing from a kit's base list (`lean` has no `delegate`; a
1552
1590
  `kits:` entry on `base: lean` or `child` inherits that) is left out softly: it is **reachable**. `find_tools` searches it as group `chi` (`chi/delegate`, with its inputSchema) beside
1553
1591
  the MCP servers' tools, and `call_tool(tool: "chi/delegate", args: {…})` runs it as `delegate` itself, through the
@@ -1667,14 +1705,18 @@ its own). The first target is a long session such as a day-long coordinator, whe
1667
1705
  - **Set it** at start with `chi --llm-context stale,forget --llm-context-apply turn_end --llm-context-budget 64k`
1668
1706
  (the same flags on `chi send --new`), in the session with `/llm-context strategy … apply … budget …`
1669
1707
  (docs/cli.md, "Session commands"), or from the web's `llm ctx` chip in the info bar.
1670
- - **A new web chat:** the start page's `llm ctx` chip, beside the model picker, shows the picked model's values
1671
- and follows a model pick. Its form (titled "new session") sets a choice for that one chat: the chip reads
1672
- `llm ctx stale · new session` in the accent colour, and the create sends it as `POST /api/sessions`'s
1673
- `llm_context` (`{"strategy": "stale,forget", "apply": "turn_end", "budget": "64k"}`, each a word as
1674
- `/llm-context` takes it, `default` for unset; anything else answers `400 invalid_llm_context` and starts
1675
- nothing), so the session file holds it before the first turn runs. It is not remembered: it resets after a
1676
- create, on a reload and when you open a session (a failed create keeps it), and an untouched chip sends nothing
1677
- (the model's values apply, as without the chip).
1708
+ - **A new web chat:** the start page's ⚙ panel (beside the model picker; a bottom sheet on a phone) holds the new
1709
+ session's rarely used settings, each a row of segments that applies at once: **Thinking** and **Context** (the
1710
+ strategy; **Apply and budget ▸** opens the other two fields, with their own Set), the first segment following the
1711
+ picked model (`default (high)`), and **Review files…** (docs/sessions.md, Review sessions). A row the picked model
1712
+ has no values for is hidden. What is set shows on the row as an accent chip (`think high`, `ctx stale`,
1713
+ `review: README.md`) that opens the panel; on a phone ⚙ carries a dot and its label names them. The create sends
1714
+ the context choice as `POST /api/sessions`'s `llm_context` (`{"strategy": "stale,forget", "apply": "turn_end",
1715
+ "budget": "64k"}`, each a word as `/llm-context` takes it, `default` for unset; anything else answers
1716
+ `400 invalid_llm_context` and starts nothing), so the session file holds it before the first turn runs. Nothing
1717
+ is remembered: the choices reset after a create, on a reload and when you open a session (a failed create keeps
1718
+ them); a choice made before picking a model without that row stays (its chip says it is sent anyway), and nothing
1719
+ set sends nothing (the model's values apply).
1678
1720
  - **Values:** `none` and a budget of `off` are values the session sets on purpose (they win over the model's);
1679
1721
  `default` (or `/llm-context reset` for all three) unsets one, back to following the model. A budget is 4k to
1680
1722
  10M tokens (`4000`–`10000000`, or `4k`–`10000k`). The layers are kept in chi's order (`stale`, then `forget`).
@@ -2042,6 +2084,7 @@ column says so, a `--kebab-name` flag. The maps (`hosts:`, `models:`,
2042
2084
  | `broadcast.threshold` | `0.5` | | The P(yes) a recipient needs, when the triage host gives logprobs; a plain yes or no is 1 or 0. |
2043
2085
  | `session.shared` | `true` | | Plain `chi` runs its session in a worker and attaches; `--no-shared` per run. |
2044
2086
  | `session.idle_exit_minutes` | `30` | yes | An unused worker exits after this; `0` = never. |
2087
+ | `session.project_env` | `mise` | | The commands a session's worker runs follow its project: `mise` runs them in the session folder's `mise env`, read once at worker start (`chi sessions restart` picks up a mise.toml edit); `off` doesn't ask mise: the tools keep the env of whoever started the worker (with a chi home, the home's `chi` still goes first on their PATH). See [Project env](sessions.md#project-env). |
2045
2088
  | `session.keep_empty` | `false` | | Keep sessions nothing happened in. |
2046
2089
  | `session.max_children` | `4` | | Running delegated sessions one session may have. |
2047
2090
  | `session.delegate_reports` | `wake` | | When a delegate child ends a turn, its reply reaches the parent: `wake` runs an idle parent's turn for it, `queue` waits for the parent's next turn, `off` leaves it to `delegate_result`. |
data/docs/desktop.md CHANGED
@@ -300,7 +300,8 @@ exports. So `install` writes `~/Library/Application Support/Chi Helper/launch.js
300
300
  whose chi may be found only through them, as under chruby): the home's boot sets its gem path itself, and they
301
301
  would reach every command the helper's sessions run (a project's `bundle exec` on the wrong gems). A launch file
302
302
  from an older chi loses `RUBYLIB` at the next `chi update`, and `GEM_HOME`/`GEM_PATH` once it boots the chi home
303
- (`chi desktop upgrade` moves it to the home);
303
+ (`chi desktop upgrade` moves it to the home). `chi home doctor` flags a helper that isn't on the home: a
304
+ `[ruby, chi]` argv, a boot of the home as it was before a `chi home set`, or `GEM_HOME`/`GEM_PATH` in its env;
304
305
  - `PATH`: the shell's PATH when the file is written, with only absolute folders, in order, each once. The helper's
305
306
  chi, the sessions it starts and their `execute` commands can find the tools your terminal finds (mise, Homebrew,
306
307
  `codex`, `node`);
data/docs/guardrails.md CHANGED
@@ -429,7 +429,13 @@ message:
429
429
  get-access-token`, `heroku auth:token`, `kubectl config view --raw`;
430
430
  `PairCommand::CREDENTIAL_PRINTERS`). On top of it, a pair turn's `execute` runs
431
431
  without every variable whose name matches `KEY|TOKEN|SECRET|PASS|CREDENTIAL|AUTH`
432
- (`PairBoundary::SECRET_ENV_NAME`, case-insensitive), and a command still
432
+ (`PairBoundary::SECRET_ENV_NAME`, case-insensitive) and without the session's
433
+ project env ([Project env](sessions.md#project-env)); a command that runs
434
+ `mise` or `direnv` asks the owner (`ENV_LOADER`: they load the project's env
435
+ files, and `.envrc`, `mise.local.toml` and its kin are secrets like `.env`),
436
+ and so does a write (by a file tool or a command) to mise's config
437
+ (`MISE_CONFIG`: the owner's next worker start runs its `[env]`);
438
+ and a command still
433
439
  running at the limit is killed, never handed over as a background task;
434
440
  - git settings that make git run a command (`core.hooksPath`,
435
441
  `core.fsmonitor`, `core.sshCommand`, `core.pager`, `core.editor`, a `!`
data/docs/sessions.md CHANGED
@@ -96,9 +96,9 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
96
96
  - The web header's project chip opens a picker. The start project is the default row (including its start folder); other rows show their root, session and waiting counts. **All projects** opens every session, and choosing a project drops the open-session hash while keeping `?view=`.
97
97
 
98
98
  - `Session.list` / `SessionManager.list_sessions` / `GET /api/sessions?sort=&order=&limit=&offset=` default to `updated_at desc` (newest activity first). Also supports `created_at`, `asc`. `X-Total-Count` header when paginated.
99
- - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new sessions start in that folder; the header chip says `<project> · all`, and `all` opens the same place as `?scope=all` (every session; a new session there starts in chi web's start folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the start folder's project). A bare `/` (the LAN link, the Home Screen app) is the start folder's project too: the folder chi web runs in, or `web.start_dir` / `chi web --start-dir DIR`; outside a repo it shows every session. A pushed notification opens its session in that session's own project (`/?dir=<its project>#/s/<id>`; `?scope=all` outside a repo). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat, and the grip on the chat's top edge — the same thin handle as the composer's, its twin — puts them away and brings them back: a drag up (or a click, or Enter/Space on it) hides them, a drag down (or a click) shows them again, and the choice is remembered. A hidden strip leaves its status in the top bar (`N sessions · ● 1 live · ● 1 waiting`, from the cards), and a click on that pill opens All sessions, without showing the strip first. In a window under 900 px wide, where the strip scrolls sideways, the grip stays under it, outside the scroller; a window under 700 px tall hides the strip by itself, so a question or approval card in the stage keeps room, and the grip shows it until the window is tall again. A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new session. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`); after a `/model` switch the info bar's tooltip adds a `per model:` row per run of turns on one model above those totals (`splash:qwen3-35b · 6 turns · 96.0k in / 9.0k out · ~$0.02`, a switch's prefill under its row: `└ switch prefill 58.0k uncached prompt ≈$0.01`; the timing's `model_segments`, replaced by each `generation_completed`'s; the session cards stay as they were), and right after the switch its ctx % is the new model's window's; and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new session starts on, and a fresh chain link's (see [Session chains](#session-chains); there a pick shows as a `/model` command bubble in the link); it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`), and `POST /api/sessions` takes `model` (blank means the default). Beside it, an `llm ctx` chip shows the LLM context the picked model starts under (each `GET /api/models` row's `llm_context`, as the info bar's); its form sets one for the new session only, sent as `POST /api/sessions`'s `llm_context` and never remembered (see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)). The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
99
+ - Web UI (`chi web`): the page's scope is in its URL. Started in a git repo, `chi web` opens `/?dir=<that folder>`: that project's sessions, and new sessions start in that folder; the header chip says `<project> · all`, and `all` opens the same place as `?scope=all` (every session; a new session there starts in chi web's start folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the start folder's project). A bare `/` (the LAN link, the Home Screen app) is the start folder's project too: the folder chi web runs in, or `web.start_dir` / `chi web --start-dir DIR`; outside a repo it shows every session. A pushed notification opens its session in that session's own project (`/?dir=<its project>#/s/<id>`; `?scope=all` outside a repo). One server serves every project: a second `chi web` (from another repo) finds it through `GET /api/info` and prints (with `--open`, opens) its page for its own folder instead of starting another. The 3 latest sessions sit above the chat, and the grip on the chat's top edge — the same thin handle as the composer's, its twin — puts them away and brings them back: a drag up (or a click, or Enter/Space on it) hides them, a drag down (or a click) shows them again, and the choice is remembered. A hidden strip leaves its status in the top bar (`N sessions · ● 1 live · ● 1 waiting`, from the cards), and a click on that pill opens All sessions, without showing the strip first. In a window under 900 px wide, where the strip scrolls sideways, the grip stays under it, outside the scroller; a window under 700 px tall hides the strip by itself, so a question or approval card in the stage keeps room, and the grip shows it until the window is tall again. A parent and its delegates are one card there: its `▸ 2 delegates · 1 live · 1 waiting` chip (the waiting part in the attention colour, the card's border too) opens their rows in a popover under it (a row per delegate at any depth, indented; a click opens that one, Esc or a click outside closes it, and the strip's order holds while it is open). A fork is a card of its own, and so is a delegate whose parent isn't listed (archived, another project), with its `↳ <parent>` chip; the pill counts cards, and every live or waiting session. "All sessions" (or `/`) opens every session at `#/sessions`, with a search over preview, id and status (Esc or Back returns). There a parent's card holds its delegates the same way, the chip opening their rows inside the card (each with an archive button on hover), open by itself while one of them waits on you; the count says `14 (+7 delegates)`, a search that matches a delegate shows its parent's card with the list open and the matching rows marked, and select mode picks cards (archiving a parent takes its delegates). The open session is in the URL (`#/s/<id>`), so a reload or a copied link opens it again; the chi logo top left goes back to the empty start for a new session. The message box grows with its text; drag its top edge to keep it taller (double-click resets). A file in a tool row (a read, edit or write's title) and its diff lines are refs: hover one and click to open the current file in your editor at that line, on this machine only (`web.editor`; the browser's "always allow" for the page's port and VS Code's "allow opening local files" are asked once; see [Opening files from chi web](configuration.md#opening-files-from-chi-web)). So are an edit or write approval card's paths (at the first line the dry run changes) and its diff lines, before you approve, and the numbered lines of a read's output: `lines 40–60` under it opens the text as read, numbered, when the output is the file as-is (not the head/tail preview of a big file, an error or an image; no numbers after a cut). A `path:line` (or `path:line-line`) in an answer or a step's text is one too once the turn has ended, and after a reload, when it names a file a row or card on the page touched: the path under the session's folder, else the one touched file it ends with (a bare `app.js:12` when only one touched file is named so; two such files make it none); a path without a line counts only when it is a touched file exactly. Code blocks, links, bare URLs and inline code with more than the ref in it are left alone, and a file the session didn't touch is never linked; a link whose text is such a ref alone (github-pr's link to the PR diff) becomes a ref too, and its plain click still goes to the PR. A plugin's refs on an answer (an `after_turn` hook's, see [Refs on the answer](hooks.md#refs-on-the-answer-display-only)) are paired with its links and text whatever the rows touched: they can link a file the session didn't touch, or an issue or a commit, and add their own actions to the ref's menu. A plugin tool's row carries the refs its `refs:` gives (see [Refs on a tool's row](plugins.md#refs-on-a-tools-row)): its title opens the first, and its `▾` menu adds the others as `<ref> · <action>` (the URL on hover); their URL actions work on a phone too. A ref with more than one target shows a small `▾` on hover: it opens a menu of them, in order **PR diff** (a link's), **Open in editor**, **Show in turn** and **Copy path**, and a plain click runs the first (a link's own, else the editor, else show in turn). **Show in turn** scrolls to the tool row that touched the file (looked for in the ref's own step, then its own turn, then the rows before it, then the nearest after it; there, the last one showing the ref's line, else the last one), opens the steps, the stage's steps and the row's diff (or the read showing the line) and flashes the row and the line; an answer's or a step's ref and an approval card's path have it, not a row's own title or lines. It needs no editor, so a viewer without one (a phone on the LAN) gets an answer's refs with **Show in turn** as their click; a row's title and diff lines stay plain text there. On a touch screen a tap opens the menu. A ref with a target is a tab stop: Enter runs its first target, ↓ opens the menu (↑/↓ move, Enter picks, Esc closes back to the ref). Diff and read lines have their click only. The info bar copies `chi --attach <id>` for a terminal. A session whose prompt carried model notes has a `notes: deepseek` chip there (the names without `model_notes_`; the tooltip lists each with its scope and size; `session.prompt_notes` in `GET /api/sessions/:id`, the live worker's, else the session file's; see [Memory](memory.md#model-notes)). Its `llm ctx` chip names the LLM context strategy the next turn runs under (`llm ctx stale,forget · session`, in the accent colour when the session set its own; the tooltip gives each value's source), and its popover sets the session's own strategy, apply rule and budget, or follows the model again, by running `/llm-context` in the worker (the reply shows as a command bubble); `GET /api/sessions/:id`'s `session.llm_context` carries the values (the live worker's, else resolved from the session file), and a `command_ran` that changed them carries the new ones. Its `think` chip does the same for the thinking level (`/thinking`), and on a chat host its `reasoning` chip for the reasoning history (`reasoning all · session`, `/reasoning-history`; `session.reasoning_history`, null on a native host). A `/thinking` or `/reasoning-history` change that restarts the prompt cache asks first: the reply bubble has an **Apply (restarts the cache)** button that sends its `--yes` line (the chips' Set sends that line itself; see [Changes that restart the prompt cache](configuration.md#changes-that-restart-the-prompt-cache)). It shows the context used (`ctx 42%`) and the last generation's decode speed (`87 tok/s`, `~64 tok/s` when estimated; see `/stats` in [cli.md](cli.md#session-commands)); hovering `ctx` there or on a session card lists the session's tokens in (summed over every request, with the cached share), out (with reasoning) and the cost when the provider reports one, for this session only (one estimated from a `hosts.<name>.models` price shows with `~`: `cost: ~$0.54 ($0.42 reported, ~$0.12 estimated)`); after a `/model` switch the info bar's tooltip adds a `per model:` row per run of turns on one model above those totals (`splash:qwen3-35b · 6 turns · 96.0k in / 9.0k out · ~$0.02`, a switch's prefill under its row: `└ switch prefill 58.0k uncached prompt ≈$0.01`; the timing's `model_segments`, replaced by each `generation_completed`'s; the session cards stay as they were), and right after the switch its ctx % is the new model's window's; and the memory indexes its prompt holds (`memory index: ~3.4k tokens in this session's prompt (system 2.0k, project 1.4k)`; see [Memory](memory.md#the-indexs-size)). The start page's model picker (bottom left of the composer, the chosen name `▾`) chooses the model a new session starts on, and a fresh chain link's (see [Session chains](#session-chains); there a pick shows as a `/model` command bubble in the link); it opens a search list over the conversation (↓ or a click; downward when the start page leaves no room above): an empty search shows the last 5 picks (Recent) and then each host's model ids A–Z under the host, the default host first (an id the host declares under `hosts.<name>.models` first, noted `config`, its tooltip `served by config (hosts.<name>.models)`; a host that failed to list its models keeps those ids, greyed and noted `host down`, still pickable; a remembered or default model stays chosen even then, greyed on the button); typed words must each match the host or the id (a substring, else for 3+ characters the letters in order, so `deepseek4.1 fla` finds `openrouter · deepseek/deepseek-v4.1-flash`), ⏎ picks, Esc/Tab close, a pick puts the focus in the message box: `GET /api/models` lists the hosts' models as chi spells them (`{default, models: [{name, host, id, configured?, unavailable?, sampling?, llm_context?}], warning?}`; `configured: true` marks an id declared under `hosts.<name>.models`, listed first under its host, `unavailable: true` one whose host failed to list its models; `sampling` is the model's configured request parameters as `/model` words them, the row's tooltip in the list; bare for the default host, `host:model` for the others, from the same cached lists as `/models`, a bounded wait, a host that is down noted in `warning`; `toolkits`: `{default, suggest, kits: [{name, description?}]}`, the start page's 🧰 list: the configured `toolkit.default`, `suggest` `on` only when a new session's first message may get a suggested kit (`toolkit.suggest.mode: on`, a System 1 host, two described kits; else `audit` or `off`), and the built-in and config kits a new session can start on, without `review`, `child`, `undecided`, a kit `groups:` refuses or one that doesn't resolve (a base that is a bundle kit), the default always listed; left out when it can't be read), and `POST /api/sessions` takes `model` (blank means the default). Beside it, ⚙ opens the new session's rarely used settings (thinking and LLM context, following each `GET /api/models` row's `thinking` and `llm_context`, and the review files; see [configuration.md](configuration.md#llm-context-a-sessions-own-strategy)), and 🧰 its toolkit: **Auto** (with `toolkit.suggest.mode: on`: the first message may get a suggested kit, else the default) or a kit from the payload's `toolkits`, sent as `POST /api/sessions`'s `toolkit` (see [configuration.md](configuration.md#toolkits)); a refused kit (`400 toolkit_refused`) is said in the list, opened again. What is set shows on the row as an accent chip (`think high`, `tools: lean`; a dot on ⚙ or 🧰 on a phone); each is for the next new session only, never remembered. The start page's first message goes as later ones do: `POST /api/sessions` with `idle: true` and `preview` (the message, which names the session until its turn is saved), then `POST /api/sessions/:id/turn`, so a failed first turn puts its prompt back in the composer too; a first line that is one of the new session's commands (`/model …`) goes to `POST /api/sessions/:id/command` instead, as later ones do. The browser remembers the last model picked; a running session's model is in the info bar and changes only with `/model`. In the composer a line is a command when it is `!…` or starts with one of the session's command names (its `commands` list; `/stats`, `/exit` and the other terminal words get the page's own reply); an unknown `/word` (`/usr/bin/env is missing`) goes to the model as a prompt, as in a terminal; a one-word `/word` no command answers (`/modle`) is not sent: the page shows `Unknown command /modle. Did you mean /model? /help lists the commands.` (the "Did you mean" part only when a name is close). A `POST /turn` (or a session's first `prompt`) that is one of the worker's commands runs as that command too (`202 {status, command_id, session_id}`), so `chi send`, `chi -p` and a page whose command list lags all get the command. ↑/↓ in the composer walk the prompt history the TUI's ↑ walks ([Persistent Prompt History](cli.md#persistent-prompt-history)): ↑ with the caret on the first line, ↓ from the last line while browsing, ↓ past the newest brings the draft back; `GET /api/history` returns `{entries: [...]}`, oldest first (the page fetches it on focus and after each send). The server records a sent line in the routes once the worker took it (`POST /turn` accepted, a `!command` through `POST /command` or as a session's first line); `"history": false` in the body keeps it out (an image-only send).
100
100
  - Web notifications: when a session needs you while the tab is hidden or behind another window (a question or a guardrail approval, a plugin card with buttons during a turn such as check-in's, a failed turn, a turn done after 10 s or more; a delegated session only for its questions, a canceled turn never), the page title counts it, `(N) Chi`, until you come back to the tab. The bell at the right of the top bar also turns on OS notifications ("needs an answer", "needs approval", "needs you", "turn failed", "done in 42 s", under the session's first message; a click opens the session): its first click asks the browser for the permission, and the choice is kept per browser. Any session of the page's scope counts, not only the open one. What was already so when the page loaded never counts, and several chi tabs show one notification per event. A chi tab in front tells the others (a `BroadcastChannel`), so while you look at one, the ones behind neither notify nor count what it shows; coming back to a tab clears those events in the other tabs' titles too. The hub's `session` frames carry what this needs: `pending_question` (`{id, kind}`, kind `question` or `approval`), `pending_card` (`{id, bundle}`: the running turn's card with actions, from `pending_card.json` that the worker keeps in the session's folder while one is open; a card without actions, such as loop-guard's, never counts) and `last_turn` (`{outcome, ended_at, seconds, origin}`, saved in the session's JSON as the engine ends each turn).
101
- - The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's Stop turn and drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html` and `participant.html`), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `review_history.js` (a participant's ↑/↓ list on the participant page: their own comments, in localStorage per grant, never the owner's `/api/history`; `participant.js` mounts `composer_mount.js` with it, no `/` list, no delivery choices, images off), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker and its LLM context and thinking chips, a choice for the next new session only; a fresh chain link's picker and its `/model`), `session_actions.js` (Continue, Archive and its Undo, Delete and Stop, and the start page when the open session is gone), `send_turn.js` (the composer's send: a new session first on the start page, the route, the echo, the image upload, the `/turn` ack, a fast failure's restore and each error's place). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
101
+ - The web frontend is a zero-build ES-module stack in `lib/samagotchi/web/public/`: `data.js` (retrieval, typed SSE `openStream` for a session and `openEvents` for the session list), `sessions_list.js` (the list as a pure reducer over `GET /api/events`: a snapshot replaces it, an upsert keeps a known card in place, a removal drops it; `families()` groups delegates under their parent), `families_view.js` (a parent's family chip, its rows and the strip's popover; the chip and rows as lit-html templates), `keep_focus.js` (lit reuses a list's nodes by position: a redraw keeps the focus on the same item by its `data-id` / `data-name`), `chains.js` (session chains: the links from `continues`, the cards folded into the latest link's, its `#N` chip and rows as lit-html templates, the info bar's ← → links, a refused continue, and the fresh-link rule), `notify.js` (which of those events needs the user, for the notifications), `app.js` (presentation/state; a session with no live stream gets one when its `session` event says `bridge_up`, after one re-read: `event_seq` starts over in each worker, so a dropped stream is never resumed with its old cursor), `bubbles.js` (the history's bubbles between the turns' rows: output chunks, end lines, every client's prompt bubbles by `enqueued_id` with their queued/steered badges, the idle recap, guardrail warnings, hooks' notices, plugins' steers, command and note bubbles; lit-html templates, put in the history with the sticky follow by `createBubbles`), `cards_place.js` (where plugin cards and a snapshot's notices go in the history: a card added or updated in place by its id, a running turn's into its step, a snapshot's each above the first turn after it, a finished turn's notice in its step's rows and its question resolved where it was asked; a card's action buttons), `format.js` (pure formatters such as `previewOf`), `plain_line.js` (markdown out of the plain one-line places: the stage headline, a closed step's title, an answered question card's kept-reply line), `prompt_history.js` (the composer's ↑/↓ over the shared prompt history), `commands.js` (session commands: which composer lines are commands, the page's own replies, a `command_ran`'s bubble, the page's command bubbles kept across a re-read), `composer.js` (where a composer line goes, `routeComposerLine`: /cut and /queue, the start page's own replies, a session command or `!cmd`, a typo hint, else a turn; and a session command sent from the composer or a chip), `delivery_line.js` (the composer's row above the text field in a session, always there so the composer keeps its height: the key hint while idle, and the delivery line while a turn runs: where a message typed now goes in, from the step's phase, clock and size, whether "cut in now" can land, and the queue link for a worker with the `delivery` feature, as a pure `deliveryLine`, its lit-html template and `createDeliveryControl`, which ticks it and keeps a choice armed from a link; see docs/cli.md, "Typing during a turn"), `restore.js` (a failed turn's prompt handed back into this tab's composer, before or after its `/turn` ack), `lists_view.js` (the session lists: the strip and its pill, all sessions with its search, count and select mode, their cards, the family and chain popovers; the cards, tile and pill as lit-html templates, drawn as new nodes each time, and the lists' orders as pure functions), `stream_handlers.js` (the open session's stream event handlers, by event type, over the page's views and hooks; a snapshot replays through them too), `stage_view.js` (the stage view: the running turn pinned above the composer, then handed off into the history; its pure rules and texts in `stage_model.js`, its live slots (headline, now row, trail, ticks) in `stage_slots.js`, its cap and held height in `stage_frame.js`, the in-use watch that times the hand-off in `stage_watch.js`), `info_bar.js` (the session bar's info line as a lit-html template over a plain model: id, status, model, chain links, chips, ctx, speed, clock, worker badge), `info_bar_model.js` (that model and the bar's buttons' states, built from the page's session state, its list and its in-flight continues and restarts as pure functions; `app.js`'s `updateInfoBar` applies them every second while a turn runs), `session_state.js` (what the page knows about the open session: its empty state, and `sessionStateFrom`, the fields a `GET /sessions/:id` payload sets), `refs.js` (refs: a kind and a target on the element, a plugin's ref data (label, http(s) URL actions) bound in a WeakMap by `bindRef`, the targets per kind, show in turn, one delegated set of click, key and hover listeners), `ref_menu.js` (a ref's `▾` and its menu), `ref_scan.js` (the `path:line` refs in an answer's or a step's text that name a file the session's rows touched), `answer_refs.js` (the refs a plugin attached to an answer, its `display_refs`: bound to the answer's bubble, paired with its links and text, bound with `bindRef`), `markdown_place.js` (where rendered markdown and its refs go: a text as markdown HTML, its `path:line` refs, a final answer's rendering), `images.js` (image chips in the composer: paste, drop and the image button, the upload before a turn, the thumbnails and the lightbox), `composer_grip.js` (the composer's grip and fold: the drag, tap and keys, the fold per visit), `composer_mount.js` (the composer mounted on a page's markup: the message box and its height, the image chips from a paste, a drop or the image button, the grip and fold, ↑/↓ over the prompt history and the keys; the page's own parts come in as hooks: what Enter and Send do, keys taken first, Esc, where the history comes from, the info bar's Stop turn and drop while folded, typing anywhere to open it; its styles, the card it sits in (`.composer-card`, its footer and its line above the text field) included, are the shared `composer.css`, linked after `tokens.css` by `index.html` and `participant.html`), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `review_history.js` (a participant's ↑/↓ list on the participant page: their own comments, in localStorage per grant, never the owner's `/api/history`; `participant.js` mounts `composer_mount.js` with it, no `/` list, no delivery choices, images off), `storage.js` (the page's per-browser choices in localStorage, guarded: a choice that can't be read or written is not remembered), `unqueue.js` (the × on a message queued for after the turn: takes it back, its text into an empty composer or a toast), `toasts.js` (the page's toast stack: a timer each, sticky ones until acted on, the same text reusing its item), `toast_place.js` (where the toast sits: above the composer card, the stage and an open model panel), `dock_reserve.js` (what floats over the history: the history's bottom reserve (`--dock-h`) at the composer card's and the showing stage's height, a reader at the end kept there, and the toast's place measured for `toast_place.js`), `notice_links.js` (a hook notice line's http(s) URLs as links), `start_page.js` (the start page's model picker, its ⚙ panel's and 🧰 list's choices for the next new session only and their override chips; a fresh chain link's picker and its `/model`), `start_settings.js` (the start page's ⚙ panel: thinking and LLM context as rows of segments, applied at once, the Apply and budget disclosure, the review files' view; the override chips' words and ⚙'s dot), `start_toolkit.js` (the start page's 🧰 list: Auto with suggest on, the kits, the default marked; a pick closes it; its override chip and dot), `session_actions.js` (Continue, Archive and its Undo, Delete and Stop, and the start page when the open session is gone), `send_turn.js` (the composer's send: a new session first on the start page, the route, the echo, the image upload, the `/turn` ack, a fast failure's restore and each error's place). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
102
102
  - Selecting a session in the web UI is read-only: `GET /api/sessions/:id` never spawns a worker (it reads a live worker's snapshot when one runs, else the session file). A prompt (`POST /turn`) or a command (`POST /command`) wakes the worker, and `/stream` briefly waits for a freshly-spawned bridge before answering. A caught-up SSE reconnect holds the stream open; `reset` markers are only sent for reconnects behind the ring window, or with a cursor from another worker (event ids are `<event_seq>-<epoch>`, one epoch per worker).
103
103
  - The web session page's message box folds away for room to read. Drag the grip on its top edge (the pill that shows on hover; always on a phone) down, past the box by 32 px: the pill turns accent while releasing would hide it, and on release the box folds and the info bar stays as the page's bottom edge. The grip stays as a handle on the info bar's top edge, drawn always: pull it up (8 px opens it, and the box follows the pointer in the same gesture), click or tap it (opens it and focuses the box), or just type on the page (the key lands in the box) to bring it back; Enter or Space on the focused grip folds or opens too. A draft (text or images, which folding never clears) colours the handle with the accent, a session waiting on you with the attention colour; its tooltip says so. Stop turn moves into the info bar while it is folded. The fold is remembered per browser (`chi_composer_collapsed`), an archived session opens folded for the visit, and opening it there, like the page opening it itself (a restored line, a quote, continue →'s opener), is for that visit only. The fold keeps the height the grip set (`chi_composer_floor`): the box opens again at the height it had, and only a double-click on the grip forgets it. That height never takes more than half of the window the box is drawn in: a floor saved in a bigger window is held to that half in a smaller one (a rotated phone, a resized window, another device on the same origin), and the saved floor itself stays, so the height comes back when the room does. A click on the expanded grip hides the box as well: it folds 250 ms after the press, so a second click inside that window is read as a double-click and resets the height instead (a drag, Enter or Space act at once, and a drag after a click takes the wait back). The start page's grip only resizes.
104
104
 
@@ -296,10 +296,10 @@ sessions](cli.md#review-sessions)); both start idle and print the `chi share` li
296
296
  folder and setup are taken (`dir` and the setup fields beside it are 400 `invalid_review`) and `model` is the
297
297
  only choice. A subject refused, or the source's folder gone, is 400 `invalid_review` with the one message; an
298
298
  unknown source 404 `not_found`.
299
- - **The page:** the start page's `review…` chip (beside think and llm ctx) takes the files (comma- or
300
- space-separated, relative to the page's folder; a path with a space in double quotes, `"docs/my plan.md"`): its
301
- chip then reads `review: plan.md +1`, and Start makes a review session (the message typed, if any, is its first
302
- turn; with none it starts idle). A refused file shows the server's one message in the chip's form, opened again. On a session that isn't a review session the participants panel (the session bar's
299
+ - **The page:** the start page's ⚙ panel, **Review files…**, takes the files (comma- or
300
+ space-separated, relative to the page's folder; a path with a space in double quotes, `"docs/my plan.md"`): the
301
+ row then shows a `review: plan.md +1` chip, and Start makes a review session (the message typed, if any, is its first
302
+ turn; with none it starts idle). A refused file shows the server's one message in the panel's review view, opened again. On a session that isn't a review session the participants panel (the session bar's
303
303
  participants chip) says so before its Invite, with the share warning (that a participant's turns act there, writes
304
304
  included and asks to the owner, or only read while an older worker holds it; how many tool outputs its history holds)
305
305
  and **Start a review of it**: a review fork of it, opened, with a toast (`Review 4b7e9a01 started from 3f9a1c2e's
@@ -364,6 +364,60 @@ otherwise; `settings` beside `continues` is refused). A respawned or woken worke
364
364
  delegate child starts without (a bundle has its own setting for children, such as cost-cap's `child:`), so its recap
365
365
  follows config.yml. `/stats` names them on a `settings:` line, and `chi sessions stats --format json` as `settings`.
366
366
 
367
+ ## Project env
368
+
369
+ The commands a session's worker starts for the session (`execute`, tasks, `!cmd`, MCP servers, context sources)
370
+ follow the session's project, not whoever started the worker (a terminal on another Ruby, chi web, the macOS helper
371
+ under launchd). At start a worker runs `mise env -C <session folder> --json` once (`ProjectEnv`,
372
+ `session.project_env: mise`, the default) and merges the answer into the env of those commands: a project whose
373
+ mise.toml pins Ruby 3.4 gets that `ruby`, `gem` and `bundle` in its tools while chi itself runs on the
374
+ [chi home](configuration.md#the-chi-home).
375
+
376
+ - **Tools only.** The worker's own process keeps the env it was started with: chi's model calls and API keys, its
377
+ proxies, config, dirs, hooks and plugins never see the project's `[env]` (a mise.toml that sets
378
+ `ANTHROPIC_API_KEY` or `https_proxy` changes only what its tools see). Hooks run with the worker's env, not the
379
+ project's. A `chi` a tool runs (`chi -p`, `chi models`, `chi send`) takes the worker's values back first thing, so
380
+ it too runs on your keys and proxies, not the project's.
381
+ - **Once per worker.** A mise.toml edit (or a `mise use` in a tool) reaches the session at its next worker:
382
+ `chi sessions restart ID` (or the web's restart) picks it up.
383
+ - **GEM_HOME/GEM_PATH.** The starter's are dropped from the tools' env when the project's `ruby` is another one (they
384
+ belong to the starter's Ruby), unless mise sets its own; kept when the Ruby stays the same.
385
+ - **Never what a `chi` in a tool reads.** The tools' env never takes `HOME`, any `XDG_*`, any `SAMAGOTCHI_*`,
386
+ `CHI_REMOTE`, `RACK_ENV` or `CI` from the project (a `chi` run there would read another config or state dir, or
387
+ send its session commands to another machine): they are dropped
388
+ and the debug log names them once (`project_env_reserved`). `mise env` runs without `MISE_YES` (it would trust an
389
+ untrusted mise.toml for you) and `CI`; your `MISE_TRUSTED_CONFIG_PATHS` stays.
390
+ - **Only this session.** A worker started from this one (a restart, a delegate child) starts on the worker's own
391
+ env. One started from a tool (`chi send --new --dir other/project`) too: the tools' env carries the worker's values
392
+ (`SAMAGOTCHI_PROJECT_ENV_ORIG`) and that `chi` takes them back, so project A's PATH, `[env]` values, GEM_* and
393
+ library paths never reach a session in B.
394
+ - **Context sources** run in the project env of the worker that fetches them: the same source can read differently
395
+ from another session's worker (or a REPL, which has none).
396
+ - **Pair turns** (a participant's, see [Participants](internals/participants.md)) run their `execute` without the
397
+ project env. MCP servers are the session's, started once in the project env, so a pair turn's MCP calls do use
398
+ it. A participant's command that runs `mise` or `direnv`, and any write to mise's config (`mise.toml`,
399
+ `.mise.toml`, `mise.<env>.toml`, `.config/mise.toml`, `.config/mise/…`, `mise/` or `.mise/config*.toml`: your
400
+ next worker start runs it, and its `[env]` can run commands), asks you first. `.envrc` and mise's local files
401
+ (`mise.local.toml`, `mise.<env>.local.toml`, dotted or not, and `config.local.toml` / `config.<env>.local.toml` in
402
+ `.config/mise/`, `mise/`, `.mise/`) are secrets to them, as `.env` is.
403
+ - **What tools still drop**: every `execute`, task and `!cmd` takes no `RUBYOPT`, `RUBYLIB`, `BUNDLE_GEMFILE`,
404
+ `BUNDLE_BIN_PATH` or `BUNDLER_VERSION` from the project's mise env: it runs without `RUBYLIB` and Bundler's
405
+ variables, and with the worker's own `RUBYOPT`, Bundler's part removed (`TaskRuntime`).
406
+ - **`chi` in a tool** stays the home's: with a chi home the tools' PATH starts with `$XDG_STATE_HOME/samagotchi/bin`,
407
+ a dir holding only a `chi` script that boots the home (rewritten at each worker start when the home changed; it
408
+ unsets `RUBYOPT`, `RUBYLIB`, `RUBYGEMS_GEMDEPS`, `GEMRC` and `BUNDLE*` before it runs, so a `bundle exec` around it
409
+ doesn't load the project's gems into chi; and `LD_PRELOAD`, `LD_LIBRARY_PATH` and their `DYLD_*` kin). The project's `ruby`, `gem` and `bundle` stay the project's; `bin/chi`, a
410
+ checkout path or `bundle exec chi` stay themselves. Without a home there is no script and no PATH change.
411
+ - **When it does nothing**: `session.project_env: off` (mise isn't asked; with a chi home the `chi` script still
412
+ goes first on the tools' PATH); a session without a folder (`none (no_dir)`); no `mise` (looked up as home.json's `mise`, then PATH, then
413
+ `/opt/homebrew/bin`, `/usr/local/bin`, `~/.local/bin`); the session's folder is gone (the worker runs elsewhere).
414
+ A `mise env` that fails (an untrusted mise.toml: run `mise trust` there; a pinned Ruby not installed), takes over
415
+ 5 s or doesn't answer JSON changes nothing and leaves one `project_env` warning in the debug log; `chi home doctor`
416
+ run in that folder shows it, with the fix, until a later worker there applies the env.
417
+ - `chi self` run in a tool shows what the worker computed (`project env`: `mise (ruby PATH)` or `none (why)`) and the
418
+ chi home (`home`).
419
+ - The REPL modes (`--no-shared`, `--non-interactive`, `--verbose`) run no worker: their tools keep the shell's env.
420
+
367
421
  ## Delegating
368
422
 
369
423
  The `delegate` tool hands a task to a **child session**: an ordinary chi session in a worker of its own, started in the parent's `working_directory` with the task, verbatim, as its first user message, on the parent's model unless the call names one (`model:` takes a name or an alias, resolved as `--model` is; an unknown host comes back as the tool's `Error:` line; an id the host's saved model list doesn't have starts the child with a `Warning: host 'h' doesn't list model 'x' (did you mean: …?)` line in the result). Children run in parallel and only their **final reply** comes back to the parent: the child's newest `output/<timestamp>.txt` file, which the worker writes at the end of each turn that produced visible text, cut head-and-tail above 64 KB (`DelegateWait.cut`); a reply longer than `max_tool_output_chars` keeps its start and its end (the conclusion), with a gap line naming that `output/` file, where the whole reply is (`ToolOutputCut`'s `:head_tail_ref`; see [Tool output guardrails](internals/tool-guardrails.md#the-runners-cut-max_tool_output_chars)). Nothing of the child's trace enters the parent's context. Nothing is hidden: a child shows in `chi sessions list` (`↳ <parent>`), the web (folded into its parent's card on the strip and in all sessions, whose `▸ N delegates` chip lists the delegates at any depth, see Web UI above; a card of its own with a `↳` chip when its parent isn't listed; `delegated by` in its info bar; the parent's info bar carries a `⑂ N` chip counting its delegates, with how many run, wait, are done, failed, stopped or idle in the tooltip and the waiting ones named in the attention colour; a click opens all sessions), the terminal's status row (a parent's `⑂ 2 running · 1 waiting`, see [Status Line](cli.md#status-line)) and the `list_sessions` tool (`child`; the parent shows as `parent` in the child's own list); the user can `chi --attach <child id>` and steer it mid-run, and `chi send` and `send_note` reach it like any session.
data/docs/testing.md CHANGED
@@ -44,7 +44,7 @@ exception's text changes between Ruby versions: write the string yourself).
44
44
  The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_view.js`,
45
45
  `turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
46
46
  `question_cards.js`, `stream_handlers.js`, `stage_view.js`, `bubbles.js`, `cards_place.js`, the chips, `info_bar.js`, `info_bar_model.js`, `lists_view.js`, `context_chips.js`, `families_view.js`,
47
- `chains.js`, `working_set_view.js`, `working_set_rail.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `session_actions.js`, `send_turn.js`, `composer_mount.js`, `dock_reserve.js`,
47
+ `chains.js`, `working_set_view.js`, `working_set_rail.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `start_settings.js`, `start_toolkit.js`, `session_actions.js`, `send_turn.js`, `composer_mount.js`, `dock_reserve.js`,
48
48
  `vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a devDependency only,
49
49
  never shipped; lit reads `document` as it loads). The shim's `markupOf(template)`,
50
50
  `htmlOf(nodes)` and `canon(html)` compare markup without lit's marker comments; the DOM writes text with only `&`, `<`, `>`
@@ -1,12 +1,12 @@
1
1
  ---
2
2
  name: mcp
3
- version: 0.7.2
3
+ version: 0.7.3
4
4
  scope: system
5
5
  description: "Tools from MCP servers (stdio): each server in config.yml bundles: mcp: servers: runs for the session; their tools go into chi's tool index: the model searches them with find_tools and calls one with call_tool (<server>/<tool>); /mcp lists them"
6
6
  trust_level: reviewed
7
7
  files: {}
8
8
  plugin:
9
9
  file: plugin.rb
10
- sha256: sha256:c4bb4cf99ad43168ea68c25283a153c5dea1e5be123743c8a4c1278c16b5ea90
10
+ sha256: sha256:65532b75b33fa7a58b4a35f9ed32e2705f4e3d323c6f770e71448da5cc9314d0
11
11
  # 0.67.0 is the release with chi.tool_source (the core tool index the bundle publishes into).
12
12
  requires_chi: ">= 0.121.0"
@@ -107,7 +107,7 @@ class Plugin
107
107
  @next_id = 0
108
108
  @dead = nil
109
109
  @closing = false
110
- @stdin, @stdout, @stderr, @wait = Open3.popen3(env.to_h { |k, v| [k.to_s, v.to_s] }, *command,
110
+ @stdin, @stdout, @stderr, @wait = Open3.popen3(env.to_h { |k, v| [k.to_s, v&.to_s] }, *command,
111
111
  chdir: cwd, pgroup: true)
112
112
  @pid = @wait.pid
113
113
  start_watchdog
@@ -433,7 +433,7 @@ class Plugin
433
433
  def spawn(server, ctx)
434
434
  cancelled = -> { ctx.cancelled? }
435
435
  client = server.client = Client.new(
436
- server.command, env: server.env, cwd: server.cwd,
436
+ server.command, env: server_env(server), cwd: server.cwd,
437
437
  log: ->(event, **fields) { ctx.log.debug("mcp_#{event}", server: server.name, **fields) },
438
438
  on_exit: ->(reason) { exited(server, reason, ctx) },
439
439
  on_notification: lambda { |method, _params|
@@ -454,6 +454,13 @@ class Plugin
454
454
  client
455
455
  end
456
456
 
457
+ # A server's env: the session's project env (the worker's mise env, on a
458
+ # chi that has it: Samagotchi::ProjectEnv) under the server's own keys.
459
+ def server_env(server)
460
+ project = defined?(Samagotchi::ProjectEnv) ? Samagotchi::ProjectEnv.current.env : {}
461
+ project.merge(server.env)
462
+ end
463
+
457
464
  # The first sentence of initialize's instructions (at most
458
465
  # DESCRIPTION_CHARS), nil when there are none.
459
466
  def info_of(result)
@@ -494,7 +501,7 @@ class Plugin
494
501
  return nil unless lock.flock(File::LOCK_EX | File::LOCK_NB)
495
502
 
496
503
  cancelled = -> { ctx.cancelled? }
497
- client = Client.new(server.command, env: server.env, cwd: server.cwd,
504
+ client = Client.new(server.command, env: server_env(server), cwd: server.cwd,
498
505
  log: ->(event, **fields) { ctx.log.debug("mcp_#{event}", server: server.name, **fields) })
499
506
  begin
500
507
  initialized = client.request("initialize", { protocolVersion: PROTOCOL_VERSION, capabilities: {},
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: samagotchi-system
3
- version: 0.132.0
3
+ version: 0.133.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: