samagotchi 0.93.0 → 0.94.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +23 -1
- data/README.md +1 -1
- data/docs/cli.md +1 -1
- data/docs/hooks.md +1 -1
- data/docs/internals/bundles.md +1 -1
- data/docs/memory.md +26 -1
- data/docs/plugins.md +37 -0
- data/docs/releasing.md +2 -2
- data/docs/sessions.md +1 -1
- data/docs/testing.md +10 -2
- data/lib/samagotchi/bootstrap_command.rb +1 -0
- data/lib/samagotchi/bundle_command.rb +1 -0
- data/lib/samagotchi/bundles/dev/manifest.yml +2 -1
- data/lib/samagotchi/bundles/model-notes/manifest.yml +11 -0
- data/lib/samagotchi/bundles/model-notes/model_notes_deepseek.md +7 -0
- data/lib/samagotchi/bundles/model-notes/notes/README.md +37 -0
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/memory_bundle/profile.rb +22 -4
- data/lib/samagotchi/memory_bundle/shipped_update.rb +5 -1
- data/lib/samagotchi/testing/sandbox.rb +26 -0
- data/lib/samagotchi/update_command.rb +1 -1
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/app.js +15 -82
- data/lib/samagotchi/web/public/dock_reserve.js +103 -0
- data/lib/samagotchi/web/public/index.html +3 -3
- data/lib/samagotchi/web/public/toast_place.js +1 -1
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e845f8db72c7f6034ff39ec4cd1d8098d175d9264c00e172a76543adc4116191
|
|
4
|
+
data.tar.gz: 0a792367f0de17fe4b5e99697bc63164c4b79529cd6be64fd8ebcbc2059f23e5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eb8f05f1f3e723cd5c34cf42c91082ea704405d486c027d895317de940cc35b4dcbd1d24d5e85f115998bc39cbacec96cf261083ba1f86f95932d1f2c67cab4c
|
|
7
|
+
data.tar.gz: b0ed71c67050674b4edd25b1664cd38acc5c40de1788d5c428cd6bc669610a98e60a4393fef616b31090997cd95eea70433e958d4ca6860542a214cc3fa15e7f
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,27 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.94.0] - 2026-10-20
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Bundle test kit: `sandbox.model_notes_for(model_id, small:)` lists the model notes chi would load for a model id
|
|
16
|
+
(from the sandbox's memories, matched as chi matches them), and `sandbox.model_note_limits` the sizes chi warns
|
|
17
|
+
over, so a bundle that ships model notes can test which models its `models:` globs catch.
|
|
18
|
+
- The `model-notes` bundle (0.1.0, in the `dev` profile, now 0.7.0): reviewed model notes, for now
|
|
19
|
+
`model_notes_deepseek` for DeepSeek V4 / V4.1 (`models: *deepseek-v4*`). A same-name note you already have is
|
|
20
|
+
skipped and keeps loading; to take the bundle's, remove yours and install again. See
|
|
21
|
+
[docs/plugins.md](docs/plugins.md#the-model-notes-bundle).
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- A profile's install (`chi bundle install|upgrade core|dev`, `chi update`, `chi bootstrap`) names each memory file
|
|
26
|
+
of a member it installs that you already had, which it leaves as yours: `model-notes: kept your own
|
|
27
|
+
model_notes_deepseek.md (not the bundle's; to take the bundle's, remove yours with memory_write remove: true, then
|
|
28
|
+
chi bundle install model-notes)`. Before, only `chi bundle install <member>` said so.
|
|
29
|
+
|
|
30
|
+
Update with `chi update` (the system bundle 0.94.0, dev 0.7.0 with the new model-notes 0.1.0), then restart `chi web`.
|
|
31
|
+
|
|
11
32
|
## [0.93.0] - 2026-10-20
|
|
12
33
|
|
|
13
34
|
### Changed
|
|
@@ -3634,7 +3655,8 @@ and long-lived sessions.
|
|
|
3634
3655
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3635
3656
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3636
3657
|
|
|
3637
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3658
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.94.0...HEAD
|
|
3659
|
+
[0.94.0]: https://github.com/dm1try/samagotchi/compare/v0.93.0...v0.94.0
|
|
3638
3660
|
[0.93.0]: https://github.com/dm1try/samagotchi/compare/v0.92.0...v0.93.0
|
|
3639
3661
|
[0.92.0]: https://github.com/dm1try/samagotchi/compare/v0.91.0...v0.92.0
|
|
3640
3662
|
[0.91.0]: https://github.com/dm1try/samagotchi/compare/v0.90.0...v0.91.0
|
data/README.md
CHANGED
|
@@ -80,7 +80,7 @@ the config. With a config already there, it adds a `hosts:` entry and keeps
|
|
|
80
80
|
the rest of the file. Then it installs the `core` bundles (loop-guard,
|
|
81
81
|
check-in, guardrails, slow-commands, agents-md, cost-cap: the safety set for a
|
|
82
82
|
small local model, the cost cap inert until you set one) and, on a terminal, offers `dev` (known-names, mcp, btw, skills,
|
|
83
|
-
source-links, github-pr, coordinator, claude-peer, codex-peer, ruby; see
|
|
83
|
+
source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes; see
|
|
84
84
|
[Bundle profiles](docs/memory.md#bundle-profiles-core-and-dev)). Edit the
|
|
85
85
|
file later as in [Configure](#configure); `chi bootstrap --help` has the
|
|
86
86
|
options.
|
data/docs/cli.md
CHANGED
|
@@ -85,7 +85,7 @@ chi bootstrap # try localhost 8080, 11434, 1234, 8000
|
|
|
85
85
|
- **The bundles.** Unless writing the config failed, it installs the system
|
|
86
86
|
bundle and the `core` profile (loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap), one line
|
|
87
87
|
each; on a terminal it then asks `Also install dev (known-names, mcp, btw,
|
|
88
|
-
skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby)? [y/N]`. Run again, it installs nothing new: a bundle
|
|
88
|
+
skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes)? [y/N]`. Run again, it installs nothing new: a bundle
|
|
89
89
|
you uninstalled stays out ([Bundle profiles](memory.md#bundle-profiles-core-and-dev)).
|
|
90
90
|
A bundle that fails to install makes the exit 1. `--dry-run` says what it
|
|
91
91
|
would install.
|
data/docs/hooks.md
CHANGED
|
@@ -608,7 +608,7 @@ Notes:
|
|
|
608
608
|
- Ordering: bundle hooks fire by `(priority, bundle_name, hook_name)` (lower priority first), then plain `config.yml` hooks in registration order.
|
|
609
609
|
- Settings: a hook class with `initialize(settings = {})` gets the bundle's section of `config.yml` `bundles:` (see [Settings](#settings)).
|
|
610
610
|
- A bundle can also ship a `plugin.rb` whose `chi.on(event)` blocks are bundle hooks too, next to commands and tools; see [Plugins](plugins.md). There `ctx.notify`, `ctx.steer` and the other `ctx` helpers act as this page's `event[:notify]`… do for the event the block runs for.
|
|
611
|
-
- Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle))
|
|
611
|
+
- Shipped bundles (each in a profile: `core` or `dev`, see [Bundle profiles](memory.md#bundle-profiles-core-and-dev)): `chi bundle install guardrails` (rules, see [Guardrails](guardrails.md#the-guardrails-bundle)) and `chi bundle install known-names` (a hook, see [Guardrails](guardrails.md#the-known-names-bundle)), `chi bundle install source-links` (a hook: announces source refs, see [The source-links bundle](#the-source-links-bundle)), `chi bundle install btw` (a plugin: `/btw`, see [Plugins](plugins.md#the-btw-bundle)), `chi bundle install mcp` (a plugin: tools from MCP servers, see [Plugins](plugins.md#the-mcp-bundle)) `chi bundle install loop-guard` (a plugin: breaks tool-call loops, see [Plugins](plugins.md#the-loop-guard-bundle)), `chi bundle install check-in` (a plugin: checks on a long turn, see [Plugins](plugins.md#the-check-in-bundle)), `chi bundle install slow-commands` (a plugin: commands that ran past the execute limit are run as tasks next time, see [Plugins](plugins.md#the-slow-commands-bundle)), `chi bundle install agents-md` (a plugin: a nested AGENTS.md is steered into the conversation on the first touch under its directory, see [Plugins](plugins.md#the-agents-md-bundle)), `chi bundle install cost-cap` (a plugin: a per-session $ cap, `/cap`, see [Plugins](plugins.md#the-cost-cap-bundle)), `chi bundle install ruby` (a plugin: an RSpec run with no examples at all, a hint, see [Plugins](plugins.md#the-ruby-bundle)) `chi bundle install skills` (a plugin: `/skill`, versions of skills, see [Plugins](plugins.md#the-skills-bundle)), `chi bundle install github-pr` (attached context: the branch's open PR, see [Plugins](plugins.md#the-github-pr-bundle)), `chi bundle install coordinator` (a plugin: parallel work in worktrees and child sessions, see [Plugins](plugins.md#the-coordinator-bundle)), `chi bundle install claude-peer` / `codex-peer` (a plugin: talk to a Claude Code or Codex session, see [Plugins](plugins.md#the-claude-peer-bundle)) and `chi bundle install model-notes` (memories: reviewed model notes, see [Plugins](plugins.md#the-model-notes-bundle)).
|
|
612
612
|
- Installing a bundle executes its hook code at `Engine` startup. Only install bundles you trust, as you would a gem. Hooks are **not** executed at install time (copy-only); they are `module_eval`'d at `Engine.new` inside per-bundle `Samagotchi::Bundles::<name>` namespaces (no top-level `require` collisions). Keep hook files side-effect-free at load time; do work in `#call` — top-level side effects (require, IO, `at_exit`, global assignment) run once per `Engine.new` (class redefinition is idempotent).
|
|
613
613
|
|
|
614
614
|
Lifecycle:
|
data/docs/internals/bundles.md
CHANGED
|
@@ -39,7 +39,7 @@ user-facing side (what a bundle owns, profiles, needs) is in [memory.md](../memo
|
|
|
39
39
|
| `Provenance` | the `.bundles/<name>/` dir: `#write` the record + bases, `#record` → `InstalledBundle`, `.each_installed`, `.claimants` (which bundles own a file) |
|
|
40
40
|
| `InstalledBundle`, `BundleHook`, `PluginRef` | value objects for the record; tolerant of older records (missing keys read as empty) |
|
|
41
41
|
| `Builder` | `chi bundle build`: memories (+ the installed bundle's hooks, rules, plugin, scripts) → zip/tar/dir |
|
|
42
|
-
| `Profile` | meta bundles (`includes:`): install the members, record which |
|
|
42
|
+
| `Profile` | meta bundles (`includes:`): install the members, record which; `InstallResult#kept_own` names a new member's memory files the user already had (`kept_own_line`, printed by `chi bundle`, `chi update` and `chi bootstrap`) |
|
|
43
43
|
| `SystemBundle`, `ShippedUpdate` | the built-in system bundle (`ensure!` at Engine start, `sync` in `chi update`) and `chi update`'s upgrade of shipped bundles; both drive the Installer with `upgrade: true` |
|
|
44
44
|
| `Uninstaller`, `Trash`, `Status`, `Listing` | remove, keep removed files, `chi bundle status`, `chi bundle list` |
|
|
45
45
|
| `IndexUpdater`, `IndexSync` | the managed `index.md` lines (Installer; and the write/edit tools for memories) |
|
data/docs/memory.md
CHANGED
|
@@ -106,6 +106,31 @@ Working habits for this model:
|
|
|
106
106
|
longest run without an edit and the Continues and steers each needed; run a
|
|
107
107
|
note on and muted for the same kind of task. See
|
|
108
108
|
[testing.md](testing.md) ("The model notes report").
|
|
109
|
+
- **Shipped notes: the model-notes bundle.** `chi bundle install model-notes`
|
|
110
|
+
(or the `dev` profile) installs reviewed notes into the system scope; for
|
|
111
|
+
now `model_notes_deepseek` (`models: *deepseek-v4*`). Each note's provenance
|
|
112
|
+
and evidence is in the bundle's `notes/README.md`, and its spec pins which
|
|
113
|
+
model ids load which note ([Plugins](plugins.md#the-model-notes-bundle)).
|
|
114
|
+
- **Your own note of the same name wins.** The install skips a
|
|
115
|
+
`model_notes_<name>.md` you already have and never owns it, so your note
|
|
116
|
+
is what loads, the two never stack, and upgrades leave it alone.
|
|
117
|
+
`chi bundle install model-notes` says `Skipped model_notes_deepseek.md
|
|
118
|
+
(already exists; use --force to overwrite)`; through the `dev` profile
|
|
119
|
+
(`chi bundle install dev`, `chi bundle upgrade dev`, `chi update`) it is
|
|
120
|
+
`model-notes: kept your own model_notes_deepseek.md (not the bundle's; to
|
|
121
|
+
take the bundle's, remove yours with memory_write remove: true, then chi
|
|
122
|
+
bundle install model-notes)`.
|
|
123
|
+
- **To take the bundle's version instead,** remove your note
|
|
124
|
+
(`memory_write` with `remove: true` moves it to the trash) and run
|
|
125
|
+
`chi bundle install model-notes` again: the install writes the missing
|
|
126
|
+
file and owns it from then on. Don't use `--force`: it overwrites every
|
|
127
|
+
file of the bundle with no trash copy.
|
|
128
|
+
- **Other notes stack.** A note of another name for the same models
|
|
129
|
+
(`model_notes_my_deepseek`), or a project-scope `model_notes_deepseek`,
|
|
130
|
+
loads beside the bundle's (notes are told apart by their file, not their
|
|
131
|
+
name), and the total may pass the 3,000-character warning.
|
|
132
|
+
- **Your edits to an installed note** are kept by an upgrade (a 3-way
|
|
133
|
+
merge), or come back as a conflict.
|
|
109
134
|
- Without a model note the system prompt is what it was.
|
|
110
135
|
|
|
111
136
|
|
|
@@ -323,7 +348,7 @@ profiles, meta bundles that install a set of them:
|
|
|
323
348
|
| profile | bundles |
|
|
324
349
|
|---|---|
|
|
325
350
|
| `core` | loop-guard, check-in, guardrails, slow-commands, agents-md, cost-cap: the recommended safety set |
|
|
326
|
-
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby |
|
|
351
|
+
| `dev` | known-names, mcp, btw, skills, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes |
|
|
327
352
|
|
|
328
353
|
`chi bootstrap` installs `core` and, on a terminal, offers `dev`. An existing
|
|
329
354
|
install gets neither by itself: `chi bundle install core` (or `dev`).
|
data/docs/plugins.md
CHANGED
|
@@ -1186,6 +1186,13 @@ bundle.shutdown
|
|
|
1186
1186
|
`chi.*` calls `register` made, in order, as made), `bundle.namespace`, `bundle.hook_names(event)`,
|
|
1187
1187
|
`bundle.run_inits`, `sandbox.write_memory(name, text, scope:)` / `memory_dir`, `sandbox.fake_executable(name,
|
|
1188
1188
|
body)` (first on `PATH`), `wait_until { }`.
|
|
1189
|
+
- **Model notes:** `sandbox.model_notes_for(model_id, small: nil, cwd: nil)` lists the model notes chi would load
|
|
1190
|
+
into the prompt for a bare model id (`deepseek/deepseek-v4.1-flash`, no host prefix), from the sandbox's
|
|
1191
|
+
memories (an installed bundle's included), matched as chi matches them: each `models:` glob against the id and
|
|
1192
|
+
its model key; `small:` says whether the model counts as small for a `small` entry (nil: chi's own rule). The
|
|
1193
|
+
notes come in prompt order, and each answers four methods, its whole contract: `name`, `scope` (`"system"` or
|
|
1194
|
+
`"project"`), `chars` (the body's length) and `body` (what the prompt carries, the `models:` line left out). `sandbox.model_note_limits` gives the sizes chi
|
|
1195
|
+
warns over (`note:`, `total:`).
|
|
1189
1196
|
- **`ctx.http` is faked.** `bundle.http.stub(method, url_or_regexp, status: 200, headers: {}, body: nil, json: nil,
|
|
1190
1197
|
error: nil) { |request| … }` answers it: a String URL matches exactly (query included), a Regexp the full URL, later
|
|
1191
1198
|
stubs win, and a stub answers every matching call (a block that counts makes a sequence). `error: :refused` or
|
|
@@ -2297,6 +2304,36 @@ gem.
|
|
|
2297
2304
|
- Guardrail rules keyed by a tool's name apply to plugin tools, as they do to
|
|
2298
2305
|
chi's own tools; path and command rules see what `targets:` says.
|
|
2299
2306
|
|
|
2307
|
+
## The model-notes bundle
|
|
2308
|
+
|
|
2309
|
+
`chi bundle install model-notes` (or the `dev` profile) installs reviewed
|
|
2310
|
+
[model notes](memory.md#model-notes) into the system scope: working habits
|
|
2311
|
+
for one model family, loaded into the prompt only for the models a note's
|
|
2312
|
+
`models:` line names. It has no plugin or hook. It ships one note:
|
|
2313
|
+
|
|
2314
|
+
| note | models | for |
|
|
2315
|
+
|---|---|---|
|
|
2316
|
+
| `model_notes_deepseek` | `*deepseek-v4*` | DeepSeek V4 / V4.1: explore briefly then act, commit one item at a time, stop digging, `memory_read` for memories, file tools rather than heredocs that guardrail rules ask about |
|
|
2317
|
+
|
|
2318
|
+
- **Reviewed before it ships.** A note is generic (no repo or machine facts),
|
|
2319
|
+
says nothing the harness already does (a tool's description, an alias),
|
|
2320
|
+
talks to the model as "you", stays under 1,500 characters, and the user
|
|
2321
|
+
approves its text. A model may propose one; it never edits the bundle.
|
|
2322
|
+
`notes/README.md` in the bundle records each note's sources, the
|
|
2323
|
+
`script/model_notes_report.rb` numbers and the approval.
|
|
2324
|
+
- **Globs per measured version.** A glob is tried against the id with no host
|
|
2325
|
+
prefix and against its model key, case-insensitively, and `*` also matches
|
|
2326
|
+
`/`: `*deepseek-v4*` covers `deepseek/deepseek-v4.1-flash` on any provider,
|
|
2327
|
+
MLX and GGUF names, but not `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B`.
|
|
2328
|
+
- **Tested.** `spec/model_ids.yml` lists real id forms and the exact notes
|
|
2329
|
+
each loads (negative rows included); the spec installs the bundle and
|
|
2330
|
+
resolves each through `sandbox.model_notes_for`
|
|
2331
|
+
([Testing a bundle](#testing-a-bundle-experimental)). It also checks each
|
|
2332
|
+
note's shape, size and that the tools it names are ones chi declares.
|
|
2333
|
+
- **Your own notes:** a same-name note you have wins and keeps its place
|
|
2334
|
+
through upgrades; other notes stack with the bundle's
|
|
2335
|
+
([Model notes](memory.md#model-notes)).
|
|
2336
|
+
|
|
2300
2337
|
## The bundle commands
|
|
2301
2338
|
|
|
2302
2339
|
- `chi bundle install <dir>`: copies the plugin to
|
data/docs/releasing.md
CHANGED
|
@@ -13,7 +13,7 @@ can run the whole release; the user approves the notes before the tag and the
|
|
|
13
13
|
and `rake release:check` enforce it). The system bundle upgrades itself when
|
|
14
14
|
chi starts.
|
|
15
15
|
- **Every other shipped bundle** (btw, check-in, guardrails, known-names,
|
|
16
|
-
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, and the profiles core and dev) has its own semver in its
|
|
16
|
+
loop-guard, mcp, skills, slow-commands, agents-md, cost-cap, source-links, github-pr, coordinator, claude-peer, codex-peer, ruby, model-notes, and the profiles core and dev) has its own semver in its
|
|
17
17
|
`manifest.yml` and, when it needs a newer chi, a `requires_chi:` line. They never upgrade by themselves: users run
|
|
18
18
|
`chi update` (all of them, with the gem) or `chi bundle upgrade NAME`. So:
|
|
19
19
|
- a change to a bundle's files bumps that bundle's `version:`
|
|
@@ -66,7 +66,7 @@ as the GitHub release body.
|
|
|
66
66
|
| `rake gem:validate`, `gem:build`, `gem:install`, `gem:clean` | Check the gemspec; build the gem into `pkg/`; build and install it locally; remove the built gems. |
|
|
67
67
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
68
68
|
| `rake release:bump[X.Y.Z]` | VERSION, the system manifest, Gemfile.lock, CHANGELOG, and `requires_chi: ">= X.Y.Z"` for each bundle marked `requires_chi_next` (the marker dropped). No commit. |
|
|
69
|
-
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, `bundles:check`, `rake lint` (RuboCop), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
69
|
+
| `rake release:check` | Clean tracked tree, VERSION == system bundle, a CHANGELOG section, no `requires_chi_next` left, `bundles:check`, `rake lint` (RuboCop) + `npm run lint` (the web frontend's ESLint), rspec (both roots: `spec` and the bundles' own `spec/`) + `rake bundles:specs` + npm test, `gem build`, then a clean install into a temp GEM_HOME that runs `chi --version`, `chi self` and `chi bundle list` with a temp HOME. Needs the network (gem dependencies). |
|
|
70
70
|
| `rake release:notes[X.Y.Z]` | Prints the CHANGELOG section (default: the current VERSION). |
|
|
71
71
|
|
|
72
72
|
## Runbook
|
data/docs/sessions.md
CHANGED
|
@@ -88,7 +88,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
88
88
|
- `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.
|
|
89
89
|
- 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 without `?dir` (every session; a new session there starts in the server's own folder, shown on the start page, and cards name their folder; `← <project>` goes back to the project view it came from, or to the server's own project). 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).
|
|
90
90
|
- 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).
|
|
91
|
-
- 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), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `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), `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`).
|
|
91
|
+
- 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), `strip_grip.js` (the latest-sessions strip's grip, the composer grip's twin: hides and shows the strip), `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`).
|
|
92
92
|
- 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).
|
|
93
93
|
- 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, and a fold forgets the height the grip set (`chi_composer_floor`): the box reopens at one line, or at its text's height. The start page's grip only resizes.
|
|
94
94
|
|
data/docs/testing.md
CHANGED
|
@@ -5,6 +5,7 @@ bundle exec parallel_rspec -n 8 spec lib/samagotchi/bundles/*/spec # Ruby spec
|
|
|
5
5
|
bundle exec rspec spec lib/samagotchi/bundles/*/spec # the same, plain
|
|
6
6
|
bundle exec rake bundles:specs # the shipped bundles' own specs, on the bundle test kit alone
|
|
7
7
|
npm test # web frontend specs
|
|
8
|
+
npm run lint # web frontend: undefined names and unused imports (ESLint; CI runs it)
|
|
8
9
|
npm run e2e # web UI happy paths in Chromium, fake model (once: npx playwright install chromium)
|
|
9
10
|
npx playwright test spec/e2e/stage.spec.js # one e2e file (the root playwright.config.js is the suite's)
|
|
10
11
|
bundle exec rake lint # RuboCop: no offenses (CI runs it)
|
|
@@ -15,6 +16,13 @@ code's own style and has a safe autocorrect, so `-a` fixes them. What it leaves
|
|
|
15
16
|
bug) or a broken spec: fix it, or disable that line with the reason (`# rubocop:disable Cop -- why`).
|
|
16
17
|
`.rubocop.yml` says why each cop that is off is off.
|
|
17
18
|
|
|
19
|
+
`npm run lint` is the web frontend's twin, for the modules in `lib/samagotchi/web/public/` (not `vendor/`): ESLint
|
|
20
|
+
with two rules only, `no-undef` (a name used but never imported or declared: a dropped import) and `no-unused-vars`
|
|
21
|
+
(an import or variable left over), over the browser's globals (`eslint.config.js`). It is a missing-name guard, not a
|
|
22
|
+
style pass: add a rule only for a bug class it would have caught. It runs in about 2 s, in CI's `npm test` job and in
|
|
23
|
+
`rake release:check`; `rake lint` stays RuboCop alone (CI's rubocop job has no node). A name that is also a browser
|
|
24
|
+
global (`name`, `status`, `open`…) passes even when its import is gone: give an export such a name only with care.
|
|
25
|
+
|
|
18
26
|
No spec reads your own setup. The suite points `XDG_CONFIG_HOME` and `XDG_STATE_HOME` at temp folders,
|
|
19
27
|
clears your `SAMAGOTCHI_*` environment, and keeps every spec off the network (WebMock).
|
|
20
28
|
|
|
@@ -26,7 +34,7 @@ exception's text changes between Ruby versions: write the string yourself).
|
|
|
26
34
|
The web specs run in plain node. One that loads lit-html, itself or through a module it imports (`turn_view.js`,
|
|
27
35
|
`turn_html.js`, `card.js`, `copy.js`, `images.js`, `scope.js`, `command_view.js`, `diff_view.js`, `read_view.js`,
|
|
28
36
|
`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`,
|
|
29
|
-
`chains.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `session_actions.js`, `send_turn.js`,
|
|
37
|
+
`chains.js`, `unqueue.js`, `toasts.js`, `notice_links.js`, `start_page.js`, `session_actions.js`, `send_turn.js`, `dock_reserve.js`,
|
|
30
38
|
`vendor/lit-html.js`) needs a DOM: `import "./dom_shim.js";` as its first import (happy-dom, a devDependency only,
|
|
31
39
|
never shipped; lit reads `document` as it loads). The shim's `markupOf(template)`,
|
|
32
40
|
`htmlOf(nodes)` and `canon(html)` compare markup without lit's marker comments; the DOM writes text with only `&`, `<`, `>`
|
|
@@ -124,7 +132,7 @@ file's layer (any `terminal_ui/*.rb` → `spec/terminal_ui_golden_spec.rb`; `tur
|
|
|
124
132
|
words the REPL prints, pick it by a rule), then the other name matches (a spec named after several changed files first, e.g. `engine_toolkit_spec.rb` for `engine.rb` and
|
|
125
133
|
`toolkit.rb`; then one beside the file, so `plugin/context.rb` picks `spec/samagotchi/plugin/*context*` before
|
|
126
134
|
`spec/guardrails/context_spec.rb`), then the specs that name a changed file's class. The default range is the merge-base with main (or origin/main) plus uncommitted and untracked files.
|
|
127
|
-
The Ruby specs go to stdout; the node tests to run (`npm test` targets), "web changed: also run npm run e2e" and a
|
|
135
|
+
The Ruby specs go to stdout; the node tests to run (`npm test` targets), "web changed: also run npm run e2e" (with `npm run lint` too when a frontend module changed) and a
|
|
128
136
|
hint for a hub class (named in too many specs to list) go to stderr. Nothing printed means nothing matched.
|
|
129
137
|
|
|
130
138
|
It finds a change's own specs, not the regressions it causes elsewhere. So before committing, run the full suite
|
|
@@ -406,6 +406,7 @@ module Samagotchi
|
|
|
406
406
|
result.failed.each { |member, why| parts << "#{member} failed (#{why})" }
|
|
407
407
|
parts << "nothing new to install" if parts.empty?
|
|
408
408
|
@stdout.puts("#{result.name} bundles: #{parts.join("; ")}")
|
|
409
|
+
result.kept_own_lines.each { |line| @stdout.puts(" #{line}") }
|
|
409
410
|
!result.failed?
|
|
410
411
|
end
|
|
411
412
|
|
|
@@ -401,6 +401,7 @@ module Samagotchi
|
|
|
401
401
|
lines << "already installed: #{result.already.join(", ")}" unless result.already.empty?
|
|
402
402
|
result.skipped.each { |member, why| lines << "skipped #{member}: #{why}" }
|
|
403
403
|
result.failed.each { |member, why| lines << "failed #{member}: #{why}" }
|
|
404
|
+
lines.concat(result.kept_own_lines)
|
|
404
405
|
lines << "nothing new to install" if lines.empty?
|
|
405
406
|
lines.each { |line| @stdout.puts " #{line}" }
|
|
406
407
|
@stdout.puts "(dry-run: no changes written)" if dry_run
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: model-notes
|
|
3
|
+
version: 0.1.0
|
|
4
|
+
scope: system
|
|
5
|
+
description: "Reviewed model notes: working habits a model family needs, loaded into the prompt only for the models each note's models: line names"
|
|
6
|
+
trust_level: reviewed
|
|
7
|
+
requires_chi: ">= 0.41.0"
|
|
8
|
+
files:
|
|
9
|
+
model_notes_deepseek.md:
|
|
10
|
+
sha256: sha256:0b07e40edc1a3d4a3ad6bea08d9b338913f78cab19a5809bcf4593f459f9bc5e
|
|
11
|
+
description: "DeepSeek V4 working habits: explore briefly then act, commit per item, stop digging, memory_read for memories, file tools over heredocs"
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
models: *deepseek-v4*
|
|
2
|
+
Working habits for this model:
|
|
3
|
+
- Explore briefly, then act. Once you know where the change goes, stop reading and make it; past ~20 steps without an edit, start implementing or say what blocks you.
|
|
4
|
+
- For multi-item tasks, finish and commit one item at a time (spec, fix, spec green, commit) before starting the next.
|
|
5
|
+
- When something doesn't work after a few attempts (a live check, a setup), stop digging: report what you tried and move on.
|
|
6
|
+
- Look up memories with memory_read (the index is in your prompt), not with shell commands on the memories folder.
|
|
7
|
+
- Guardrail rules match an execute command's raw text, heredocs included. Write file contents with write/edit, put commit messages in a file (`git commit -F <file>`), and if a guardrail asks, switch to the file tools instead of rewording.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Model notes: provenance and evidence
|
|
2
|
+
|
|
3
|
+
This folder is not installed: an install takes only the top-level files the manifest lists. It records where each
|
|
4
|
+
shipped note came from, what it was measured against and who approved its text.
|
|
5
|
+
|
|
6
|
+
A note ships only after the review in docs/memory.md ("Shipped notes: the model-notes bundle"): generic (no repo or
|
|
7
|
+
machine facts), not already the harness's job, written to "you", at most 1,500 characters, no first-person lines,
|
|
8
|
+
and its final text approved by the user. A model may propose a note in a plan or a diff; it never edits this
|
|
9
|
+
bundle itself.
|
|
10
|
+
|
|
11
|
+
## model_notes_deepseek
|
|
12
|
+
|
|
13
|
+
- **models:** `*deepseek-v4*`: DeepSeek V4 and V4.1 (`deepseek/deepseek-v4.1-flash`, `deepseek/deepseek-v4-flash`,
|
|
14
|
+
`deepseek/deepseek-v4.1-flash-1017`, a GGUF such as `unsloth/DeepSeek-V4-Flash-GGUF:Q4_K_M`). The note that ran
|
|
15
|
+
before had `deepseek/*|deepseek-*`, which also caught `deepseek-ai/DeepSeek-R1-Distill-Qwen-32B` (a Qwen
|
|
16
|
+
distill). Widen the glob to another version only on evidence for it. `spec/model_ids.yml` pins which ids load it.
|
|
17
|
+
- **Origin, 2026-10-03:** written by hand after a DeepSeek run of about 100 steps with no commit: explore briefly
|
|
18
|
+
then act, one item at a time with a commit each, stop digging after a few failed attempts, read memories with
|
|
19
|
+
`memory_read`.
|
|
20
|
+
- **Guardrail line, 2026-10-18:** written by DeepSeek itself (`memory_write`) in chi build #60, after guardrail rules
|
|
21
|
+
asked about its heredoc writes and commit messages. It was in the first person and named this repo's paths
|
|
22
|
+
(`tmp/commit-msg.txt`); rewritten generically for this bundle.
|
|
23
|
+
- **Dropped:** the "never background with `&`" line. The `execute` tool's description already says it ("Don't background with &;
|
|
24
|
+
use task_create"),
|
|
25
|
+
and the report counts 2 bare `&` calls (one session) without a note and none with it.
|
|
26
|
+
- **Approved:** the text as shipped, by the user, 2026-10-20 (the plan's P0).
|
|
27
|
+
- **Evidence:** `ruby script/model_notes_report.rb --model '*deepseek*'` (read-only, chi's sessions folder,
|
|
28
|
+
2026-10-20). Medians in tool calls:
|
|
29
|
+
|
|
30
|
+
| DeepSeek V4.1 Flash via OpenRouter | n | 1st edit | 1st commit |
|
|
31
|
+
|---|---|---|---|
|
|
32
|
+
| no note (2026-10-03 to 10-08) | 11 | 47.5 | 81.5 |
|
|
33
|
+
| the note before this rewrite, pinned to DigitalOcean (2026-10-15 to 10-17) | 11 | 12 | 25 |
|
|
34
|
+
|
|
35
|
+
The dates and tasks differ, so this is a hint, not an A/B: an A/B is the same kind of task with the note on and
|
|
36
|
+
muted (`chi send --new --mute model_notes_deepseek`). The shipped text is shorter than the one measured (795
|
|
37
|
+
characters against 1,519) and keeps its habits.
|
|
@@ -25,9 +25,14 @@ module Samagotchi
|
|
|
25
25
|
# shipped meta expands: includes: in any other bundle is ignored.
|
|
26
26
|
module Profile
|
|
27
27
|
# installed: members installed now; already: members found installed
|
|
28
|
-
# (recorded, not reinstalled); skipped and failed: name => why
|
|
29
|
-
|
|
28
|
+
# (recorded, not reinstalled); skipped and failed: name => why;
|
|
29
|
+
# kept_own: member installed now => its memory files the user already
|
|
30
|
+
# had, left as they are and not owned (Installer#skip_existing).
|
|
31
|
+
InstallResult = Struct.new(:name, :version, :installed, :already, :skipped, :failed, :kept_own, keyword_init: true) do
|
|
30
32
|
def failed? = !failed.empty?
|
|
33
|
+
|
|
34
|
+
# One line per kept file (Profile.kept_own_line).
|
|
35
|
+
def kept_own_lines = (kept_own || {}).flat_map { |member, files| files.map { |file| Profile.kept_own_line(member, file) } }
|
|
31
36
|
end
|
|
32
37
|
# removed: members uninstalled; blocked: name => why; gone: the meta
|
|
33
38
|
# itself was removed (only once every member is); trash: member =>
|
|
@@ -69,6 +74,15 @@ module Samagotchi
|
|
|
69
74
|
|
|
70
75
|
def installed_names = Provenance.each_installed.map { |name, _| name }
|
|
71
76
|
|
|
77
|
+
# What a profile's install says of a member's memory file the user
|
|
78
|
+
# already had: chi bundle install says "Skipped … (already exists)",
|
|
79
|
+
# which a profile's member install would leave unsaid (docs/memory.md,
|
|
80
|
+
# "Shipped notes").
|
|
81
|
+
def kept_own_line(member, file)
|
|
82
|
+
"#{member}: kept your own #{file} (not the bundle's; to take the bundle's, remove yours with " \
|
|
83
|
+
"memory_write remove: true, then chi bundle install #{member})"
|
|
84
|
+
end
|
|
85
|
+
|
|
72
86
|
# The one rule.
|
|
73
87
|
def to_install(manifest, bundle = Provenance.new(name: manifest.name).record, installed: installed_names)
|
|
74
88
|
manifest.includes - recorded(bundle, manifest) - installed
|
|
@@ -84,7 +98,7 @@ module Samagotchi
|
|
|
84
98
|
was = recorded(bundle, manifest)
|
|
85
99
|
installed = installed_names
|
|
86
100
|
result = InstallResult.new(name: manifest.name, version: manifest.version, installed: [],
|
|
87
|
-
already: (manifest.includes - was) & installed, skipped: {}, failed: {})
|
|
101
|
+
already: (manifest.includes - was) & installed, skipped: {}, failed: {}, kept_own: {})
|
|
88
102
|
|
|
89
103
|
to_install(manifest, bundle, installed: installed).each do |member|
|
|
90
104
|
install_member(member, shipped_dir, chi_version, dry_run, force, result)
|
|
@@ -109,8 +123,12 @@ module Samagotchi
|
|
|
109
123
|
return result.skipped[member] = failure if failure
|
|
110
124
|
return result.installed << member if dry_run
|
|
111
125
|
|
|
112
|
-
Installer.new(source: member_dir, name: member, scope: "system", force: force, strict: true,
|
|
126
|
+
installer = Installer.new(source: member_dir, name: member, scope: "system", force: force, strict: true,
|
|
127
|
+
shipped_dir: shipped_dir)
|
|
128
|
+
installer.run
|
|
113
129
|
result.installed << member
|
|
130
|
+
kept = installer.results.select { |_, entry| entry[:status] == "skipped" && entry[:reason] == "already exists" }.keys
|
|
131
|
+
result.kept_own[member] = kept.sort unless kept.empty?
|
|
114
132
|
rescue StandardError => e
|
|
115
133
|
result.failed[member] = e.message.lines.first.to_s.strip
|
|
116
134
|
end
|
|
@@ -37,7 +37,10 @@ module Samagotchi
|
|
|
37
37
|
# :failed. note says why for skipped/failed. kept and replaced are
|
|
38
38
|
# file names; under plan they are what apply would keep or replace.
|
|
39
39
|
# adds is nil but for a profile: the members it installs.
|
|
40
|
-
|
|
40
|
+
# kept_own is nil but for a profile that installed a member: its
|
|
41
|
+
# Profile.kept_own_line lines.
|
|
42
|
+
Row = Struct.new(:name, :from, :to, :status, :note, :kept, :replaced, :source_dir, :scope, :adds, :kept_own,
|
|
43
|
+
keyword_init: true)
|
|
41
44
|
|
|
42
45
|
module_function
|
|
43
46
|
|
|
@@ -84,6 +87,7 @@ module Samagotchi
|
|
|
84
87
|
result = Profile.install(row.source_dir, shipped_dir: File.dirname(row.source_dir))
|
|
85
88
|
row.dup.tap do |r|
|
|
86
89
|
r.adds = result.installed
|
|
90
|
+
r.kept_own = result.kept_own_lines
|
|
87
91
|
r.status = result.failed? ? :failed : :updated
|
|
88
92
|
r.note = result.failed.map { |member, why| "#{member}: #{why}" }.join("; ") if result.failed?
|
|
89
93
|
end
|
|
@@ -124,6 +124,32 @@ module Samagotchi
|
|
|
124
124
|
File.join(memory_dir(scope, cwd: cwd || Dir.pwd), "#{name}.md")
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
+
# The model notes chi loads into the prompt for +model_id+ (the bare
|
|
128
|
+
# id, no host prefix: `deepseek/deepseek-v4.1-flash`), from this
|
|
129
|
+
# sandbox's memories (the system scope's, then the project scope's for
|
|
130
|
+
# the checkout at +cwd+), matched as chi matches them: each `models:`
|
|
131
|
+
# glob against the id and its model key.
|
|
132
|
+
# @param small [Boolean, nil] whether the model counts as a small one
|
|
133
|
+
# for a `small` entry; nil: chi's own rule (guardrails.small_models)
|
|
134
|
+
# @return [Array<#name, #scope, #chars, #body>] the notes in prompt
|
|
135
|
+
# order (`.map(&:name)` for the names); none: []
|
|
136
|
+
def model_notes_for(model_id, small: nil, cwd: nil)
|
|
137
|
+
require_relative "../model_notes"
|
|
138
|
+
require_relative "../model_overlay"
|
|
139
|
+
small_check = small.nil? ? nil : -> { small }
|
|
140
|
+
in_dir(cwd) do
|
|
141
|
+
ModelNotes.for(name: model_id, key: ModelOverlay.key_for(model_id), small: small_check, warn: false)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# The sizes chi warns over (it still loads the notes): one note's
|
|
146
|
+
# body (+:note+) and all of a model's notes (+:total+), in chars.
|
|
147
|
+
# @return [Hash{Symbol => Integer}]
|
|
148
|
+
def model_note_limits
|
|
149
|
+
require_relative "../model_notes"
|
|
150
|
+
{ note: ModelNotes::NOTE_LIMIT, total: ModelNotes::TOTAL_LIMIT }
|
|
151
|
+
end
|
|
152
|
+
|
|
127
153
|
# @return [String] a temp folder for the session's cwd
|
|
128
154
|
def work_dir = File.join(@root, "work")
|
|
129
155
|
|
|
@@ -203,7 +203,7 @@ module Samagotchi
|
|
|
203
203
|
row = Row.new(component: update.name, from: update.from, to: update.to)
|
|
204
204
|
case update.status
|
|
205
205
|
when :would_update, :updated
|
|
206
|
-
notes = [adds(update.adds), kept(update.kept, update.name), replaced(update.replaced)].compact
|
|
206
|
+
notes = [adds(update.adds), kept(update.kept, update.name), replaced(update.replaced), *update.kept_own].compact
|
|
207
207
|
done(row, :update, notes.empty? ? nil : notes.join("; "))
|
|
208
208
|
when :up_to_date then up_to_date(row)
|
|
209
209
|
when :skipped then skipped(row, update.note)
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -36,9 +36,8 @@ import { createTurnView, turnHistoryNodes } from "./turn_view.js";
|
|
|
36
36
|
import { elementOf, emptyAnswerTemplate } from "./turn_html.js";
|
|
37
37
|
import { createBubbles, hintEl, outputBubbleTemplate } from "./bubbles.js";
|
|
38
38
|
import { createCardsPlace } from "./cards_place.js";
|
|
39
|
-
import { createStageView
|
|
39
|
+
import { createStageView } from "./stage_view.js";
|
|
40
40
|
import { createMarkdownPlace } from "./markdown_place.js";
|
|
41
|
-
import { reducedMotion, STAGE_GAP } from "./stage_frame.js";
|
|
42
41
|
import { openerText, openTitle } from "./chains.js";
|
|
43
42
|
import { createTaskStopper } from "./task_stop.js";
|
|
44
43
|
import { appendQuote } from "./annotations.js";
|
|
@@ -55,8 +54,8 @@ import { createCommandList } from "./command_list.js";
|
|
|
55
54
|
import { createPromptHistory } from "./prompt_history.js";
|
|
56
55
|
import { ALL_SESSIONS_HASH, sessionHash, sessionIdFromHash } from "./route.js";
|
|
57
56
|
import { scopeDir, scopeTemplate } from "./scope.js";
|
|
58
|
-
import {
|
|
59
|
-
import {
|
|
57
|
+
import { render } from "./vendor/lit-html.js";
|
|
58
|
+
import { shouldFollowScroll } from "./scroll.js";
|
|
60
59
|
import { SHORT_WINDOW_QUERY, stripAutoHidden } from "./strip.js";
|
|
61
60
|
import { createStripGrip } from "./strip_grip.js";
|
|
62
61
|
import { waitingOn } from "./notify.js";
|
|
@@ -81,7 +80,7 @@ import { createDeliveryControl, QUEUE_NEEDS_WORKER } from "./delivery_line.js";
|
|
|
81
80
|
import { liveSlots } from "./stage_model.js";
|
|
82
81
|
import { currentGen } from "./turn_model.js";
|
|
83
82
|
import { toolName } from "./activity.js";
|
|
84
|
-
import {
|
|
83
|
+
import { createDockReserve } from "./dock_reserve.js";
|
|
85
84
|
import { createToasts } from "./toasts.js";
|
|
86
85
|
import { createUnqueue } from "./unqueue.js";
|
|
87
86
|
import { createStartPage } from "./start_page.js";
|
|
@@ -218,6 +217,17 @@ const turnDom = view === "stage"
|
|
|
218
217
|
renderMarkdown, linkRefs: linkRefsIn,
|
|
219
218
|
})
|
|
220
219
|
: createTurnView(viewArgs);
|
|
220
|
+
// The history's bottom reserve and the toast's place follow the composer
|
|
221
|
+
// card and the stage floating over the history (dock_reserve.js).
|
|
222
|
+
const { placeToast } = createDockReserve({
|
|
223
|
+
dockEl: $("#dock"),
|
|
224
|
+
centerEl: $("#center"),
|
|
225
|
+
stageEl: view === "stage" ? turnDom.root : null,
|
|
226
|
+
historyEl,
|
|
227
|
+
toastEl: $("#toast"),
|
|
228
|
+
modelPanelEl: $("#modelPanel"),
|
|
229
|
+
atHistoryEnd,
|
|
230
|
+
});
|
|
221
231
|
// Where a running turn's rows can be: the history, and the stage while it
|
|
222
232
|
// holds a turn (lookups by id, copy, annotate, card actions).
|
|
223
233
|
function turnRoots() {
|
|
@@ -1441,55 +1451,6 @@ promptEl.addEventListener("input", fitPrompt);
|
|
|
1441
1451
|
window.addEventListener("resize", fitPrompt);
|
|
1442
1452
|
fitPrompt();
|
|
1443
1453
|
|
|
1444
|
-
// The card floats over the history: keep the history's bottom padding at the
|
|
1445
|
-
// card's height, and stay pinned to the end if the reader was there, since the
|
|
1446
|
-
// card grows (text, chips, Cancel, a wrapping footer) under a reader at the end.
|
|
1447
|
-
// In the stage view the stage floats above the card, out of the dock's flow
|
|
1448
|
-
// (stage_view.js): the reserve counts it too, with its 10 px gap, while it
|
|
1449
|
-
// shows.
|
|
1450
|
-
const dockEl = $("#dock");
|
|
1451
|
-
const centerEl = $("#center");
|
|
1452
|
-
// A leaving stage (the hand-off's slide) no longer counts: the reserve eases
|
|
1453
|
-
// down over the slide (index.html, #center.reserve-easing).
|
|
1454
|
-
const stageEl = view === "stage" ? turnDom.root : null;
|
|
1455
|
-
function dockReserve() {
|
|
1456
|
-
const shown = stageEl && !stageEl.hidden && !stageEl.classList.contains("leaving");
|
|
1457
|
-
return dockEl.offsetHeight + (shown ? stageEl.offsetHeight + STAGE_GAP : 0);
|
|
1458
|
-
}
|
|
1459
|
-
const historyEnd = () => historyEl.scrollHeight - historyEl.clientHeight;
|
|
1460
|
-
// A reader at the end stays there when the reserve changes: at once for the
|
|
1461
|
-
// composer card, eased for the stage (+fromStage+: it appeared, grew or
|
|
1462
|
-
// leaves), so the history glides rather than jumps. An eased follow running
|
|
1463
|
-
// counts as at the end.
|
|
1464
|
-
function updateReserve({ fromStage = false } = {}) {
|
|
1465
|
-
const follow = atHistoryEnd() || historyEl.dataset.following !== undefined;
|
|
1466
|
-
const leaving = !!stageEl?.classList.contains("leaving");
|
|
1467
|
-
centerEl.classList.toggle("reserve-easing", leaving);
|
|
1468
|
-
const reserve = `${dockReserve()}px`;
|
|
1469
|
-
const changed = centerEl.style.getPropertyValue("--dock-h") !== reserve;
|
|
1470
|
-
if (changed) centerEl.style.setProperty("--dock-h", reserve);
|
|
1471
|
-
if (changed && follow) {
|
|
1472
|
-
if (fromStage) easeScrollTo(historyEl, historyEnd, { ms: leaving ? FOLD_MS : FOLLOW_MS, reduce: reducedMotion() });
|
|
1473
|
-
else if (historyEl.dataset.following === undefined) historyEl.scrollTop = historyEl.scrollHeight;
|
|
1474
|
-
}
|
|
1475
|
-
placeToast();
|
|
1476
|
-
}
|
|
1477
|
-
const dockObserver = new ResizeObserver((entries) => updateReserve({ fromStage: entries.some((e) => e.target === stageEl) }));
|
|
1478
|
-
dockObserver.observe(dockEl);
|
|
1479
|
-
if (stageEl) {
|
|
1480
|
-
dockObserver.observe(stageEl);
|
|
1481
|
-
// Only showing, hiding and leaving change the reserve (not the stage's
|
|
1482
|
-
// other classes: waiting, in-use…).
|
|
1483
|
-
let stageShown = "";
|
|
1484
|
-
new MutationObserver(() => {
|
|
1485
|
-
const shown = `${stageEl.hidden}|${stageEl.classList.contains("leaving")}`;
|
|
1486
|
-
if (shown === stageShown) return;
|
|
1487
|
-
stageShown = shown;
|
|
1488
|
-
updateReserve({ fromStage: true });
|
|
1489
|
-
}).observe(stageEl, { attributes: true, attributeFilter: ["class", "hidden"] });
|
|
1490
|
-
}
|
|
1491
|
-
window.addEventListener("resize", () => placeToast());
|
|
1492
|
-
|
|
1493
1454
|
promptEl.addEventListener("paste", (e) => {
|
|
1494
1455
|
const files = imageFiles(e.clipboardData);
|
|
1495
1456
|
// Text pasted along with an image still lands in the textarea.
|
|
@@ -1625,35 +1586,7 @@ function fillOpener(created) {
|
|
|
1625
1586
|
promptEl.setSelectionRange(promptEl.value.length, promptEl.value.length);
|
|
1626
1587
|
}
|
|
1627
1588
|
|
|
1628
|
-
// The toast sits above the session's composer card (measured: it grows with
|
|
1629
|
-
// a wrapping footer or a taller box), so it never covers the card's
|
|
1630
|
-
// controls, and above the stage while it shows and a fresh link's open
|
|
1631
|
-
// model panel (toast_place.js); with no card in sight (the start page, all
|
|
1632
|
-
// sessions) the CSS's 24 px from the bottom.
|
|
1633
|
-
function placeToast() {
|
|
1634
|
-
const dock = document.getElementById("dock");
|
|
1635
|
-
const r = dock.getBoundingClientRect();
|
|
1636
|
-
const shownTop = (el) => {
|
|
1637
|
-
const box = el && !el.hidden ? el.getBoundingClientRect() : null;
|
|
1638
|
-
return box && box.height > 0 ? box.top : null;
|
|
1639
|
-
};
|
|
1640
|
-
const px = toastBottom({
|
|
1641
|
-
inSession: document.body.classList.contains("has-session") && r.height > 0,
|
|
1642
|
-
viewportHeight: window.innerHeight,
|
|
1643
|
-
dockTop: r.top,
|
|
1644
|
-
tops: [shownTop(document.getElementById("turnStage")), shownTop(document.getElementById("modelPanel"))],
|
|
1645
|
-
toastHeight: document.getElementById("toast").offsetHeight,
|
|
1646
|
-
});
|
|
1647
|
-
const bottom = px === null ? "" : `${px}px`;
|
|
1648
|
-
const root = document.documentElement.style;
|
|
1649
|
-
if (root.getPropertyValue("--toast-bottom") !== bottom) {
|
|
1650
|
-
if (bottom) root.setProperty("--toast-bottom", bottom);
|
|
1651
|
-
else root.removeProperty("--toast-bottom");
|
|
1652
|
-
}
|
|
1653
|
-
}
|
|
1654
1589
|
const toasts = createToasts({ el: $("#toast"), onChange: placeToast });
|
|
1655
|
-
// The model panel opening, closing or changing height (its list filters).
|
|
1656
|
-
new ResizeObserver(() => placeToast()).observe(document.getElementById("modelPanel"));
|
|
1657
1590
|
|
|
1658
1591
|
function showToast(text, action = null, options = {}) { toasts.show(text, action, options); }
|
|
1659
1592
|
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { easeScrollTo, FOLLOW_MS } from "./scroll.js";
|
|
2
|
+
import { FOLD_MS } from "./stage_view.js";
|
|
3
|
+
import { reducedMotion, STAGE_GAP } from "./stage_frame.js";
|
|
4
|
+
import { toastBottom } from "./toast_place.js";
|
|
5
|
+
|
|
6
|
+
// What floats over the history (used by app.js): the composer card, and in
|
|
7
|
+
// the stage view the stage above it. Two measures follow it.
|
|
8
|
+
//
|
|
9
|
+
// The reserve: the card floats over the history, so the history's bottom
|
|
10
|
+
// padding (--dock-h on #center, index.html) is kept at the card's height,
|
|
11
|
+
// and a reader at the end stays pinned there, since the card grows (text,
|
|
12
|
+
// chips, Cancel, a wrapping footer) under a reader at the end. In the stage
|
|
13
|
+
// view the stage floats above the card, out of the dock's flow
|
|
14
|
+
// (stage_view.js): the reserve counts it too, with its STAGE_GAP, while it
|
|
15
|
+
// shows. A leaving stage (the hand-off's slide) no longer counts: the
|
|
16
|
+
// reserve eases down over the slide (#center.reserve-easing).
|
|
17
|
+
//
|
|
18
|
+
// The toast: it sits above the session's composer card (measured: it grows
|
|
19
|
+
// with a wrapping footer or a taller box), so it never covers the card's
|
|
20
|
+
// controls, and above the stage while it shows and a fresh link's open
|
|
21
|
+
// model panel (toast_place.js); with no card in sight (the start page, all
|
|
22
|
+
// sessions) the CSS's 24 px from the bottom (--toast-bottom unset).
|
|
23
|
+
//
|
|
24
|
+
// +stageEl+: the stage (stage view) or null (turn view). +atHistoryEnd()+:
|
|
25
|
+
// the reader is at the history's end (app.js, scroll.js's threshold).
|
|
26
|
+
// +reduce()+ / +ease+: prefers-reduced-motion and the eased follow
|
|
27
|
+
// (scroll.js), defaults the page's.
|
|
28
|
+
// The observers that call these are installed here: the dock's and the
|
|
29
|
+
// stage's size, the stage showing, hiding or leaving, the window's size and
|
|
30
|
+
// the model panel's. app.js calls placeToast() when the page enters or
|
|
31
|
+
// leaves a session and when a toast shows or goes (toasts.js onChange).
|
|
32
|
+
export function createDockReserve({
|
|
33
|
+
dockEl, centerEl, stageEl, historyEl, toastEl, modelPanelEl, atHistoryEnd,
|
|
34
|
+
reduce = reducedMotion, ease = easeScrollTo,
|
|
35
|
+
}) {
|
|
36
|
+
const doc = dockEl.ownerDocument;
|
|
37
|
+
const win = doc.defaultView;
|
|
38
|
+
|
|
39
|
+
function dockReserve() {
|
|
40
|
+
const shown = stageEl && !stageEl.hidden && !stageEl.classList.contains("leaving");
|
|
41
|
+
return dockEl.offsetHeight + (shown ? stageEl.offsetHeight + STAGE_GAP : 0);
|
|
42
|
+
}
|
|
43
|
+
const historyEnd = () => historyEl.scrollHeight - historyEl.clientHeight;
|
|
44
|
+
|
|
45
|
+
// A reader at the end stays there when the reserve changes: at once for the
|
|
46
|
+
// composer card, eased for the stage (+fromStage+: it appeared, grew or
|
|
47
|
+
// leaves), so the history glides rather than jumps. An eased follow running
|
|
48
|
+
// counts as at the end.
|
|
49
|
+
function updateReserve({ fromStage = false } = {}) {
|
|
50
|
+
const follow = atHistoryEnd() || historyEl.dataset.following !== undefined;
|
|
51
|
+
const leaving = !!stageEl?.classList.contains("leaving");
|
|
52
|
+
centerEl.classList.toggle("reserve-easing", leaving);
|
|
53
|
+
const reserve = `${dockReserve()}px`;
|
|
54
|
+
const changed = centerEl.style.getPropertyValue("--dock-h") !== reserve;
|
|
55
|
+
if (changed) centerEl.style.setProperty("--dock-h", reserve);
|
|
56
|
+
if (changed && follow) {
|
|
57
|
+
if (fromStage) ease(historyEl, historyEnd, { ms: leaving ? FOLD_MS : FOLLOW_MS, reduce: reduce() });
|
|
58
|
+
else if (historyEl.dataset.following === undefined) historyEl.scrollTop = historyEl.scrollHeight;
|
|
59
|
+
}
|
|
60
|
+
placeToast();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function placeToast() {
|
|
64
|
+
const r = dockEl.getBoundingClientRect();
|
|
65
|
+
const shownTop = (el) => {
|
|
66
|
+
const box = el && !el.hidden ? el.getBoundingClientRect() : null;
|
|
67
|
+
return box && box.height > 0 ? box.top : null;
|
|
68
|
+
};
|
|
69
|
+
const px = toastBottom({
|
|
70
|
+
inSession: doc.body.classList.contains("has-session") && r.height > 0,
|
|
71
|
+
viewportHeight: win.innerHeight,
|
|
72
|
+
dockTop: r.top,
|
|
73
|
+
tops: [shownTop(stageEl), shownTop(modelPanelEl)],
|
|
74
|
+
toastHeight: toastEl.offsetHeight,
|
|
75
|
+
});
|
|
76
|
+
const bottom = px === null ? "" : `${px}px`;
|
|
77
|
+
const root = doc.documentElement.style;
|
|
78
|
+
if (root.getPropertyValue("--toast-bottom") !== bottom) {
|
|
79
|
+
if (bottom) root.setProperty("--toast-bottom", bottom);
|
|
80
|
+
else root.removeProperty("--toast-bottom");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const dockObserver = new win.ResizeObserver((entries) => updateReserve({ fromStage: entries.some((e) => e.target === stageEl) }));
|
|
85
|
+
dockObserver.observe(dockEl);
|
|
86
|
+
if (stageEl) {
|
|
87
|
+
dockObserver.observe(stageEl);
|
|
88
|
+
// Only showing, hiding and leaving change the reserve (not the stage's
|
|
89
|
+
// other classes: waiting, in-use…).
|
|
90
|
+
let stageShown = "";
|
|
91
|
+
new win.MutationObserver(() => {
|
|
92
|
+
const shown = `${stageEl.hidden}|${stageEl.classList.contains("leaving")}`;
|
|
93
|
+
if (shown === stageShown) return;
|
|
94
|
+
stageShown = shown;
|
|
95
|
+
updateReserve({ fromStage: true });
|
|
96
|
+
}).observe(stageEl, { attributes: true, attributeFilter: ["class", "hidden"] });
|
|
97
|
+
}
|
|
98
|
+
win.addEventListener("resize", () => placeToast());
|
|
99
|
+
// The model panel opening, closing or changing height (its list filters).
|
|
100
|
+
new win.ResizeObserver(() => placeToast()).observe(modelPanelEl);
|
|
101
|
+
|
|
102
|
+
return { updateReserve, placeToast };
|
|
103
|
+
}
|
|
@@ -547,7 +547,7 @@ button.task-stop:hover:not(:disabled){background:var(--danger-bg-dim);filter:non
|
|
|
547
547
|
@media (hover:none){.copy-btn{opacity:.55}}
|
|
548
548
|
/* The dock is the composer card: the text field, a toolbar row, the info bar as its footer. */
|
|
549
549
|
#dock{display:flex;flex-direction:column;flex-shrink:0;margin:0 12px 12px;border-radius:16px;position:relative;transition:border-color .12s ease,box-shadow .12s ease}
|
|
550
|
-
/* In a session the card floats over the history, so the glass blurs it; the history's bottom padding follows the card (--dock-h, set from
|
|
550
|
+
/* In a session the card floats over the history, so the glass blurs it; the history's bottom padding follows the card (--dock-h, set from dock_reserve.js). */
|
|
551
551
|
/* The session panel lets the page's glow through (the history's own layers go clear), so the card and its footer read as glass over it, not as a flat box on black. */
|
|
552
552
|
body.has-session #center{background:color-mix(in srgb,var(--bg) 35%,transparent);border-color:var(--glass-border);position:relative}
|
|
553
553
|
body.has-session #stage,body.has-session #history{background:transparent}
|
|
@@ -719,7 +719,7 @@ body.stage-view #dock,body.stage-view #dock:focus-within{background:transparent;
|
|
|
719
719
|
.turn-stage{display:flex;flex-direction:column;min-height:0;max-height:var(--stage-max,60vh);border-left:3px solid var(--accent);border-radius:14px;overflow:hidden}
|
|
720
720
|
/* Out of the dock's flow: it floats 10 px above the composer card, so its rise, growth and leaving never move #dock (a
|
|
721
721
|
bottom-pinned box changing height is a layout shift, animated or not). Painted before .dock-card (no z-index): the command
|
|
722
|
-
list (z-index 20 inside .dock-card) opens over its lower edge. The history's bottom reserve counts it (
|
|
722
|
+
list (z-index 20 inside .dock-card) opens over its lower edge. The history's bottom reserve counts it (dock_reserve.js, --dock-h). */
|
|
723
723
|
body.stage-view .turn-stage{position:absolute;left:0;right:0;bottom:100%;margin-bottom:10px}
|
|
724
724
|
.turn-stage[hidden]{display:none}
|
|
725
725
|
.turn-stage.ended{border-left-color:var(--border-strong)}
|
|
@@ -1183,7 +1183,7 @@ body.all-view #center, body.all-view #topStrip{display:none}
|
|
|
1183
1183
|
.card.archived{opacity:.6}
|
|
1184
1184
|
.card.archived:hover{opacity:.85}
|
|
1185
1185
|
.card .archived-badge,.family-row .archived-badge{font-size:10px;padding:1px 6px;border-radius:6px;border:1px solid var(--border-strong);color:var(--text-muted)}
|
|
1186
|
-
/* bottom: above the session's composer card when one shows (--toast-bottom,
|
|
1186
|
+
/* bottom: above the session's composer card when one shows (--toast-bottom, dock_reserve.js placeToast). */
|
|
1187
1187
|
#toast{position:fixed;left:50%;bottom:var(--toast-bottom,24px);transform:translateX(-50%);width:max-content;max-width:calc(100vw - 32px);box-sizing:border-box;z-index:50;display:flex;flex-direction:column;align-items:center;gap:8px}
|
|
1188
1188
|
#toast[hidden]{display:none}
|
|
1189
1189
|
#toast .toast-item{display:flex;gap:12px;align-items:center;width:max-content;max-width:100%;box-sizing:border-box;padding:8px 14px;border-radius:10px;background:var(--bg-raised);color:var(--text);border:1px solid var(--border-strong);box-shadow:var(--toast-shadow);font-size:13px}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Where the toast sits (
|
|
1
|
+
// Where the toast sits (dock_reserve.js placeToast measures, this decides): in a
|
|
2
2
|
// session, +gap+ above the highest of the composer card and what opens
|
|
3
3
|
// over it (the stage while it shows, a fresh link's open model panel), so
|
|
4
4
|
// it covers none of them; null elsewhere (the CSS's 24 px from the bottom).
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: samagotchi
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.94.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -248,6 +248,9 @@ files:
|
|
|
248
248
|
- lib/samagotchi/bundles/loop-guard/plugin.rb
|
|
249
249
|
- lib/samagotchi/bundles/mcp/manifest.yml
|
|
250
250
|
- lib/samagotchi/bundles/mcp/plugin.rb
|
|
251
|
+
- lib/samagotchi/bundles/model-notes/manifest.yml
|
|
252
|
+
- lib/samagotchi/bundles/model-notes/model_notes_deepseek.md
|
|
253
|
+
- lib/samagotchi/bundles/model-notes/notes/README.md
|
|
251
254
|
- lib/samagotchi/bundles/ruby/manifest.yml
|
|
252
255
|
- lib/samagotchi/bundles/ruby/plugin.rb
|
|
253
256
|
- lib/samagotchi/bundles/skills/manifest.yml
|
|
@@ -675,6 +678,7 @@ files:
|
|
|
675
678
|
- lib/samagotchi/web/public/data.js
|
|
676
679
|
- lib/samagotchi/web/public/delivery_line.js
|
|
677
680
|
- lib/samagotchi/web/public/diff_view.js
|
|
681
|
+
- lib/samagotchi/web/public/dock_reserve.js
|
|
678
682
|
- lib/samagotchi/web/public/families_view.js
|
|
679
683
|
- lib/samagotchi/web/public/favicon.svg
|
|
680
684
|
- lib/samagotchi/web/public/format.js
|