samagotchi 0.74.0 → 0.75.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 +22 -1
- data/docs/internals/bundles.md +2 -1
- data/docs/memory.md +14 -1
- data/docs/plugins.md +1 -0
- data/docs/releasing.md +1 -1
- data/docs/sessions.md +4 -4
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/memory_bundle/installer.rb +18 -4
- data/lib/samagotchi/memory_bundle/memory_files.rb +47 -0
- data/lib/samagotchi/session.rb +1 -1
- data/lib/samagotchi/session_chain.rb +2 -8
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/public/chains.js +42 -21
- data/lib/samagotchi/web/public/index.html +2 -0
- data/lib/samagotchi/web/public/info_bar.js +4 -4
- data/lib/samagotchi/web/public/info_bar_model.js +16 -7
- data/lib/samagotchi/web/public/lists_view.js +2 -2
- data/lib/samagotchi/web/public/session_state.js +4 -0
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 583a10afc0042a87287fe62bb6e0e73aae26f69de5668c7926178d951c4fdc04
|
|
4
|
+
data.tar.gz: 9f2e0d659bf688b83b85fdff65a7f55e39bef32051af15a00abab893bb3d767e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6cefe884285bab628351d25729aa036d076d002830e904e8362a335a50e47d83f8ba878b8ca0127b31943ca14715e8c962937566d22849a9af1658e94dab87ff
|
|
7
|
+
data.tar.gz: da32e58a43000c308c7fd88addfb66655baadd2f521a915d836cb4e60219a54117f603fed100166529fe0dbfab21801e6f0feae4b6ef8d026060f4747d6440b1
|
data/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,26 @@ and commands may change between minor versions. How releases are made:
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
## [0.75.0] - 2026-10-17
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Web: a session chain's links are numbered `#N` instead of `day N` (the chip `↩ #3`, `#2 of 3`; its popover's rows),
|
|
16
|
+
since a chain may be continued several times a day. The info bar's ← / → show the neighbour's time (`← 09:12`) when
|
|
17
|
+
it started on the open link's day, else its date, with its full start in the tooltip; a popover row adds its time
|
|
18
|
+
when another link of the chain started the same day. The carry note names the previous link's start with its time
|
|
19
|
+
(`This session continues 3fa2c1d0 (2026-10-17 09:12), …`).
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- `chi bundle install` / `upgrade` no longer make a bundle repo's README, CHANGELOG, LICENSE, CONTRIBUTING,
|
|
24
|
+
CODE_OF_CONDUCT or SECURITY file a memory, and a manifest whose `files:` lists memories installs only those (an
|
|
25
|
+
unshipped name warns). An upgrade moves such a file an older install made a memory to the trash. Older chi still
|
|
26
|
+
installs every top-level `*.md` (`requires_chi` doesn't gate memories): a bundle for it too names its readme
|
|
27
|
+
`README.markdown`. `rake bundles:check` fails a shipped bundle's `*.md` that its `files:` doesn't list.
|
|
28
|
+
|
|
29
|
+
Update with `chi update` (the system bundle 0.75.0), then restart `chi web`.
|
|
30
|
+
|
|
11
31
|
## [0.74.0] - 2026-10-17
|
|
12
32
|
|
|
13
33
|
### Changed
|
|
@@ -3156,7 +3176,8 @@ and long-lived sessions.
|
|
|
3156
3176
|
- A macOS desktop helper (`chi desktop install`): a "Send to chi" Service and a
|
|
3157
3177
|
hotkey panel that send selected text or the clipboard to your sessions.
|
|
3158
3178
|
|
|
3159
|
-
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.
|
|
3179
|
+
[Unreleased]: https://github.com/dm1try/samagotchi/compare/v0.75.0...HEAD
|
|
3180
|
+
[0.75.0]: https://github.com/dm1try/samagotchi/compare/v0.74.0...v0.75.0
|
|
3160
3181
|
[0.74.0]: https://github.com/dm1try/samagotchi/compare/v0.73.0...v0.74.0
|
|
3161
3182
|
[0.73.0]: https://github.com/dm1try/samagotchi/compare/v0.72.0...v0.73.0
|
|
3162
3183
|
[0.72.0]: https://github.com/dm1try/samagotchi/compare/v0.71.0...v0.72.0
|
data/docs/internals/bundles.md
CHANGED
|
@@ -49,7 +49,8 @@ The order matters: warnings print in it, and a plugin/script the manifest names
|
|
|
49
49
|
|
|
50
50
|
1. `load_source`: normalize, read `manifest.yml` (strict: a missing/invalid one raises; non-strict: a warning).
|
|
51
51
|
2. `resolve_target`: scope = CLI > manifest > `system`.
|
|
52
|
-
3. `install_memories`, per
|
|
52
|
+
3. `install_memories`, per memory (`MemoryFiles.select`: a non-empty `files:` list, else every top-level `*.md` but
|
|
53
|
+
README, CHANGELOG, LICENSE and the like; a listed name not shipped warns): an upgrade 3-way merges the files the bundle owns (`merge_memory`), a dry run only
|
|
53
54
|
classifies (`preview_memory`), a plain install writes new files and skips existing ones (`skip_existing`). A
|
|
54
55
|
same-name file the bundle didn't write is the user's: skipped and never recorded (`@owned`). Model overlays
|
|
55
56
|
(`<name>.<key>.md`) get no index line (`note_overlay`).
|
data/docs/memory.md
CHANGED
|
@@ -199,7 +199,7 @@ step instead of fixing the skill.
|
|
|
199
199
|
## What a bundle owns
|
|
200
200
|
|
|
201
201
|
A bundle owns the memory files it wrote, nothing else. `chi bundle install`
|
|
202
|
-
copies each of its
|
|
202
|
+
copies each of its memories into the scope dir; a file of the same name
|
|
203
203
|
that is already there is left as it is and reported `Skipped` (`--force`
|
|
204
204
|
overwrites it, and then the bundle owns it). A skipped file stays yours: the
|
|
205
205
|
bundle doesn't record it, so `chi bundle upgrade` never merges into it and
|
|
@@ -207,6 +207,19 @@ bundle doesn't record it, so `chi bundle upgrade` never merges into it and
|
|
|
207
207
|
for `chi bundle build` then installing the result on the same machine, and
|
|
208
208
|
for a file another bundle installed.
|
|
209
209
|
|
|
210
|
+
- **Which files are memories.** A manifest whose `files:` lists any: exactly
|
|
211
|
+
those (a listed name the bundle doesn't ship gets a warning). Without a
|
|
212
|
+
list (no `files:`, or `files: {}`): every top-level `*.md` except a repo's
|
|
213
|
+
meta files: README, CHANGELOG, LICENSE (or LICENCE), CONTRIBUTING,
|
|
214
|
+
CODE_OF_CONDUCT and SECURITY, in any case. So a bundle kept in its own git
|
|
215
|
+
repo can have a `README.md` without it becoming a memory. A listed file is
|
|
216
|
+
installed whatever its name (`chi bundle build` of your own `security.md`
|
|
217
|
+
lists it). Since chi 0.75.0. Older chi installs every top-level `*.md`
|
|
218
|
+
whatever `requires_chi` says (it gates hooks, rules and the plugin, not
|
|
219
|
+
memories), so a bundle for older chi too names its readme
|
|
220
|
+
`README.markdown` (or puts docs in a subdirectory).
|
|
221
|
+
`chi bundle upgrade` moves a README an older install made a memory to the
|
|
222
|
+
trash (unless you edited it, like any file a bundle stops shipping).
|
|
210
223
|
- **The index says where a memory came from.** The `index.md` line of a file
|
|
211
224
|
a bundle installed ends in `· from <bundle>`, before the description:
|
|
212
225
|
`- **memory_guide** · system · 2026-10-03 · 5120 · from samagotchi-system —
|
data/docs/plugins.md
CHANGED
data/docs/releasing.md
CHANGED
|
@@ -55,7 +55,7 @@ as the GitHub release body.
|
|
|
55
55
|
| Task | What it does |
|
|
56
56
|
| --- | --- |
|
|
57
57
|
| `rake bundles:sha` | Recomputes the sha256 lines of every shipped bundle manifest (`files:`, `hooks:`, `plugin:`). |
|
|
58
|
-
| `rake bundles:check` | Sha lines match; a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: mark it `requires_chi_next` if the change uses anything new. Notes a bundle marked `requires_chi_next`. The release workflow and `release:check` run it in full. |
|
|
58
|
+
| `rake bundles:check` | Sha lines match; every top-level `*.md` of a bundle is listed in its `files:` (an install takes only the listed ones); a bundle changed since the last `v*` tag has a higher version (skipped, with a note, before the first tag). Warns (doesn't fail) when such a bundle's `requires_chi` still admits the tagged chi: mark it `requires_chi_next` if the change uses anything new. Notes a bundle marked `requires_chi_next`. The release workflow and `release:check` run it in full. |
|
|
59
59
|
| `rake 'bundles:check[ci]'` | The CI workflow's (every push and PR): the same, except the version check leaves out the system bundle, which is changed and unbumped between releases (`release:bump` moves its version with VERSION). A task argument, not `CI=1`: GitHub sets `CI` in the release workflow too. |
|
|
60
60
|
| `rake release:draft_changelog` | A draft Unreleased section from the commit subjects since the last tag, grouped. Stdout only. |
|
|
61
61
|
| `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. |
|
data/docs/sessions.md
CHANGED
|
@@ -76,7 +76,7 @@ chi sessions clean --dry-run --days 7 # test sessions older than 7 da
|
|
|
76
76
|
- `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.
|
|
77
77
|
- 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 chats start in that folder; the header chip says `<project> · all`, and `all` opens the same place without `?dir` (every session; a new chat 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 (`hide ▴` puts them away behind a `▾ N sessions` pill, remembered; in a window under 900 px wide, where the strip scrolls sideways, `▴ hide` sits in the top bar where the pill comes back; a window under 700 px tall hides them by itself, so a question or approval card in the stage keeps room, and the pill shows them 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 chat. 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)`), 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 chat 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 chat 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).
|
|
78
78
|
- 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).
|
|
79
|
-
- 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
|
|
79
|
+
- 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`). Unit-tested via `npm test` (`spec/web/public/*.test.js`).
|
|
80
80
|
- 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).
|
|
81
81
|
|
|
82
82
|
**Test-session hygiene:**
|
|
@@ -204,19 +204,19 @@ chi send --new --continues last:3fa2 -m "new day" # the next link of 3fa2's
|
|
|
204
204
|
|
|
205
205
|
## Session chains
|
|
206
206
|
|
|
207
|
-
A session can **continue** an earlier one: "the next
|
|
207
|
+
A session can **continue** an earlier one: "the next run of the same routine" (a daily coordinator, say, continued once a day or several times). The new session's `continues` field names the previous link; the chain is derived from it, never stored: follow `continues` back for the earlier links, forward for the later ones. It is not a parent: archiving one link leaves the next alone, and lists fold delegates, not links. A link is continued at most once, so a chain has no forks and its latest link is the one nobody continues.
|
|
208
208
|
|
|
209
209
|
`SessionManager.continue_session` makes the next link:
|
|
210
210
|
|
|
211
211
|
- **What it copies:** the previous link's folder, its model (the name it was typed as, so an alias holds) unless the continue names one (`--model`, the web's `model`), and its own LLM context values. A model given is checked like a new session's (a host config.yml doesn't have refuses the continue, nothing archived). An inherited model whose host can't run it any more (gone from config.yml, `enabled: false`, an alias naming another host) **falls back to the default model** with a warning, `gone:org/model can't run (unknown host 'gone' in model …); started on <default>`, decided before the archive; with no default model configured the continue is refused. The carry note doesn't mention the model. The new link carries no transcript, so a different model, or another provider, starts clean there. Its preview is the previous link's (until its first message is saved), unless it starts with a message.
|
|
212
|
-
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Never more than the recap: no transcript. An idle new link holding only the note is kept, not discarded as empty.
|
|
212
|
+
- **The carry note:** the new link starts with a context note from `session chain` (shown in its transcript as `note from session chain`), queued before its worker spawns, so its first turn sees it: `This session continues 3fa2c1d0 (2026-10-08 09:12), the previous link of its chain. Its recap, as of 2026-10-08 18:02:` and the previous link's recap (`recap.json`; cut at 8000 characters). No recap (fewer user turns than a recap needs, recaps off, none written in time): `… It left no recap.` Never more than the recap: no transcript. An idle new link holding only the note is kept, not discarded as empty.
|
|
213
213
|
- **The recap first:** a previous link with a live worker is asked for a recap (the Bridge's `POST /recap`) and given up to 30 s to write it; one with none wrote its recap when its worker left.
|
|
214
214
|
- **The previous link is archived** before the new one starts, by the archive's own rules (see *Archiving a session*): a turn running, a prompt queued or the step-limit question refuse it, a live idle worker is stopped, a plain REPL's session is refused. Its finished delegates are archived with it, as any archive does.
|
|
215
215
|
- **Its open delegates move to the new link** instead: each direct delegate that is running, waiting for an answer, has a live worker or a reply its parent wasn't given, or has such a delegate below it (checked again after the recap wait, so one delegated meanwhile moves too). It moves with its own delegates, stays unarchived, and from then on is the new link's: its card folds under the new link, its reports ring the new link (an idle one runs a wake turn for them, as for its own delegates), `delegate_result` and the `⑂` chip count it there, and what its parent was already told (`delegates.json`) and any report still waiting come along, so nothing is reported twice. A moved delegate gets a context note from `session chain`, `Your parent session is now 4b7e9a01 (…), the next link of its session chain: …`, as its system prompt keeps naming the parent it started with. The move writes `<child>/parent.json` (`{parent_id, from, at}`) beside the child's session file, never the file itself, so a live child worker saving its own copy changes nothing; every reader applies it (a later move overwrites it; it is never removed). A continue is **refused for an open delegate that can't move** (`SessionContinue::Refused`, naming it): one whose live worker runs an older chi (`older chi worker 0.46.1; restart it`: `chi sessions restart <id>`, or stop it) or that is open in a chi REPL (close it there). An older `chi web` or CLI still running shows a moved delegate under the archived link until it is restarted.
|
|
216
216
|
- **If the continue fails part-way:** a new link that fails to start gives its moved delegates back before it is deleted (each gets a note saying so; its worker is stopped first, if one started, and a worker that won't stop is reported, the link left), with any report one of them rang it with meanwhile; the previous link, back in the lists, is woken for reports waiting. A continue that dies in the middle leaves a `move.json` in the new link's folder: the new link's next worker finishes that move (only the delegates it names, and without a second note to one already told) as it starts. Until the new link's own worker holds it, a ring from a moved delegate doesn't start another (a `starting` marker in its folder, which that worker removes; a stale one, over 60 s old or from a process that is gone, is ignored). An open delegate below an archived delegate refuses the continue (`under archived …`): unarchive that one, or archive this one.
|
|
217
217
|
- **Refused too:** a link that is continued already (naming the link that continues it; continue that one, or `last:<id>`), a previous link whose folder is gone, and one whose model's host can't run it when no default model is configured to fall back to. A refusal starts nothing and archives nothing; a new link that fails to start puts back what was archived. Every refusal that can be known up front (the archive's ones included) comes before the recap wait. One continue of a session runs at a time (a lock in its folder; other chains don't wait), so a double click finds the first one's link and is refused, while a `last:<id>` continue that waited follows the chain to its new end.
|
|
218
218
|
- **Web:** `POST /api/sessions` takes `continues: "<id, prefix or last:id>"`, with `idle: true` (a new link waiting for its first message, previewed by the previous link's, or `preview`) or a `prompt`, and `model` (or `model_name`) for the new link's model, but not `dir`, `llm_context` or `thinking` (400 `invalid_continues`: the previous link's are taken). 201 is the new session's summary, `continues` set, with `moved`: the ids of the delegates that moved to it (`[]` for none), and `model_warning`: the default-model fallback's warning, then any about the model id, joined by `; ` (`null` for none), and `opener`: the previous link's first typed message, in full, when that link continues one itself (`null` otherwise: a chain's first link's first message is its task); 404 `not_found`; 400 `ambiguous_id` for a prefix of several sessions, `invalid_model` for a given model whose host config.yml doesn't have (or a fallback with no default model); 409 `continued` with `next_id` (the link that continues it already, which the page opens instead), `open_children` with `ids` (open delegates that can't move), `folder_gone`, `busy`, `scratch` or `owned_by_tui`. Every session summary carries `continues`.
|
|
219
|
-
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft already there stays instead): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its
|
|
219
|
+
- **The page:** the info bar's `continue →` continues the open session (`continues: <id>, idle: true`; it reads `continuing…` through the recap wait) and opens the new link with an opener in the composer, not sent (a draft already there stays instead): the previous link's own first typed message when that link continues one (the 201's `opener`; a `!shell` line isn't taken), else `Continue where we left off.`. So whatever you sent on the last link comes back on the next one, and editing it once changes it for every later link; the previous link is archived, a toast says so (`Continued 3fa2c1d0 in 4b7e9a01; 3fa2c1d0 is archived; 1 open delegate moved`), and a moved delegate's card folds under the new link. When the previous link's model's host is gone the link starts on the default model and the toast carries the server's `model_warning` too, and then stays until closed. A fresh link (one with no turn yet, however it was made: `continue →`, `chi send --new --continues`, a reload) shows the model picker in its composer with the link's model, until its first turn starts (a failed or canceled first turn ends it too); a pick runs `/model <name>` in the link (a command bubble; the info bar follows) and changes neither the start page's remembered model nor its Recent list, and a Send right after it waits for the `/model` to be taken. A link a chi REPL owns, or one continued already, shows none. A link continued already has no `continue →`: its `→` link goes to the next link (a stale page's continue answered 409 `continued` opens `next_id`). A continue refused for open delegates that can't move shows a toast that names each one (its preview and what the server says to do: `older chi worker 0.46.1; restart it`, `open in a chi REPL; close it there`), each a link to its page; any other refusal shows the server's words. The info bar of a link carries `← 2026-10-08` (the previous link's day) and, on an earlier link, `2026-10-10 →`, each a link with its id, its full start (`2026-10-10 14:32`) and its preview in the tooltip; a neighbour that started on the open link's own day shows its time instead (`← 09:12`, `14:32 →`). A link made by `continue →` inherits the previous link's title when it is created, so its card keeps its chain's title whatever opener it is sent, and its top bar and browser tab show its card's title. A link whose title is the opener and nothing else (a `chi send --new --continues` link whose first message is `Continue where we left off.`) shows its chain's first link's title instead, on its card, in the popover and in its info bar's title line, display only (its messages and data keep the opener; its own title when the first link is gone). The lists fold a chain into the card of its latest listed link, whatever the earlier links' archived state (typing in an archived link brings it back, folded there still), their delegates with them: the card's `↩ #N` chip (`#N of M` when later links aren't listed; its tooltip `Session chain: link N of M`) opens a popover of the other links, newest first (`#N`, its date, with its time (`2026-10-17 14:32`) when another link of the chain started the same day, its recap's first sentence or its preview, `archived`), a click opens one; the chip shows a live dot or `waiting` for a link that has a worker or waits on the user, and a waiting link puts its chain's card first. All sessions folds nothing while a search is typed or "include archived" is on: every link is a card of its own then, its chip `#N of M`, with its own delegates folded in, so a search finds an earlier link and its delegates.
|
|
220
220
|
- **CLI:** `chi send --new --continues (ID|PREFIX|last:ID) [--model M] [-m TEXT]` (see *Starting a session*).
|
|
221
221
|
- **Which link:** an id, a unique prefix, or `last:<id or prefix>`, the latest link of the chain that session is in. It walks the links that still exist: a deleted link in the middle (`chi sessions delete`, the retention sweep of an unarchived one) ends the walk before it, so `last:` of an earlier link then names that earlier link, and continuing it starts a second line beside the links after the gap; name a link after the gap instead. A trigger names the chain this way: `chi send` to an archived link would bring it back (a human's input), not continue the chain.
|
|
222
222
|
|
|
@@ -11,6 +11,7 @@ require_relative "index_updater"
|
|
|
11
11
|
require_relative "merger"
|
|
12
12
|
require_relative "trash"
|
|
13
13
|
require_relative "asset_installer"
|
|
14
|
+
require_relative "memory_files"
|
|
14
15
|
require_relative "../bundle_needs"
|
|
15
16
|
require_relative "../model_overlay"
|
|
16
17
|
|
|
@@ -167,7 +168,8 @@ module Samagotchi
|
|
|
167
168
|
[scope, dir]
|
|
168
169
|
end
|
|
169
170
|
|
|
170
|
-
# Copies the bundle's
|
|
171
|
+
# Copies the bundle's memories (MemoryFiles: the files: list, else
|
|
172
|
+
# every *.md but README and the like) into the target dir and refreshes
|
|
171
173
|
# their index lines: an upgrade merges (merge_memory), a dry run only
|
|
172
174
|
# classifies (preview_memory), a plain install writes the new ones.
|
|
173
175
|
# Sets @bundle_md (every file the bundle ships) and @owned: the bundle
|
|
@@ -178,8 +180,9 @@ module Samagotchi
|
|
|
178
180
|
owned_before = @existing ? @existing.files.keys : []
|
|
179
181
|
@bundle_md = []
|
|
180
182
|
@owned = []
|
|
181
|
-
|
|
182
|
-
|
|
183
|
+
warn_unshipped_files
|
|
184
|
+
memory_files.each do |file_key|
|
|
185
|
+
file_path = File.join(@bundle_dir, file_key)
|
|
183
186
|
@bundle_md << file_key
|
|
184
187
|
note_overlay(file_key, @target_dir)
|
|
185
188
|
target_path = File.join(@target_dir, file_key)
|
|
@@ -206,6 +209,17 @@ module Samagotchi
|
|
|
206
209
|
end
|
|
207
210
|
end
|
|
208
211
|
|
|
212
|
+
# The bundle's memories' basenames (MemoryFiles.select), once.
|
|
213
|
+
def memory_files
|
|
214
|
+
@memory_files ||= MemoryFiles.select(@bundle_dir, @manifest)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# A files: entry with no such *.md in the bundle installs nothing.
|
|
218
|
+
def warn_unshipped_files
|
|
219
|
+
missing = MemoryFiles.unshipped(@bundle_dir, @manifest)
|
|
220
|
+
@warnings << "files: names #{missing.join(", ")}, which the bundle doesn't ship (not installed)" unless missing.empty?
|
|
221
|
+
end
|
|
222
|
+
|
|
209
223
|
# An upgrade's 3-way merge of one file (Merger.classify against the
|
|
210
224
|
# base the last install recorded); a conflict leaves the local file
|
|
211
225
|
# and is recorded in @conflicts for the agent step.
|
|
@@ -431,7 +445,7 @@ module Samagotchi
|
|
|
431
445
|
# An overlay-looking name with no base anywhere is an overlay too,
|
|
432
446
|
# with a warning (a dry run warns as well).
|
|
433
447
|
def note_overlay(file_key, target_dir)
|
|
434
|
-
@bundle_files ||=
|
|
448
|
+
@bundle_files ||= memory_files
|
|
435
449
|
return unless ModelOverlay.bundle_overlay?(file_key, bundle_files: @bundle_files, target_dir: target_dir)
|
|
436
450
|
|
|
437
451
|
@overlays << file_key
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Samagotchi
|
|
4
|
+
module MemoryBundle
|
|
5
|
+
# Which of a bundle dir's top-level *.md files are its memories (the
|
|
6
|
+
# ones an install copies into the scope dir, docs/memory.md):
|
|
7
|
+
# - a manifest whose files: lists any: exactly those (that exist);
|
|
8
|
+
# - else (no manifest, no files: key, or files: {}): every *.md but a
|
|
9
|
+
# repo's meta files (README, CHANGELOG, LICENSE, ...), so a bundle
|
|
10
|
+
# kept in its own git repo doesn't install its README as a memory.
|
|
11
|
+
# A listed file is installed whatever its name: `chi bundle build` of
|
|
12
|
+
# a user's own security.md lists it, and it is a memory.
|
|
13
|
+
# files: {} means "no list" rather than "no memories": shipped bundles
|
|
14
|
+
# with nothing to install write it (mcp), and an external bundle that
|
|
15
|
+
# left the map empty installed its *.md before this rule.
|
|
16
|
+
module MemoryFiles
|
|
17
|
+
# A repo's meta files, by name without extension (case-insensitive).
|
|
18
|
+
REPO_META = %w[readme changelog license licence contributing code_of_conduct security].freeze
|
|
19
|
+
META_EXTENSIONS = %w[.md .markdown .txt].freeze
|
|
20
|
+
|
|
21
|
+
module_function
|
|
22
|
+
|
|
23
|
+
# @return [Array<String>] the memories' basenames, sorted
|
|
24
|
+
def select(bundle_dir, manifest)
|
|
25
|
+
present = Dir.glob(File.join(bundle_dir, "*.md")).map { |f| File.basename(f) }.sort
|
|
26
|
+
listed = manifest&.files&.keys
|
|
27
|
+
return present & listed if listed && !listed.empty?
|
|
28
|
+
|
|
29
|
+
present.reject { |name| repo_meta?(name) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# The files: entries that name no top-level *.md of the bundle.
|
|
33
|
+
def unshipped(bundle_dir, manifest)
|
|
34
|
+
listed = manifest&.files&.keys || []
|
|
35
|
+
listed.reject { |name| name.end_with?(".md") && File.file?(File.join(bundle_dir, name)) && !name.include?("/") }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Whether +name+ is a repo's meta file: README, README.md,
|
|
39
|
+
# readme.markdown, LICENSE.txt, ...
|
|
40
|
+
def repo_meta?(name)
|
|
41
|
+
base = name.downcase
|
|
42
|
+
ext = META_EXTENSIONS.find { |e| base.end_with?(e) }
|
|
43
|
+
REPO_META.include?(ext ? base.delete_suffix(ext) : base)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
data/lib/samagotchi/session.rb
CHANGED
|
@@ -91,7 +91,7 @@ module Samagotchi
|
|
|
91
91
|
# one): see #delegate?.
|
|
92
92
|
attr_writer :delegate
|
|
93
93
|
# The session this one continues (SessionManager.continue_session): the
|
|
94
|
-
# previous link of a chain, "the next
|
|
94
|
+
# previous link of a chain, "the next run of the same routine", else
|
|
95
95
|
# nil. Not a parent: archiving one link leaves the next alone. The chain
|
|
96
96
|
# itself is derived (SessionChain), never stored.
|
|
97
97
|
attr_accessor :continues
|
|
@@ -6,7 +6,7 @@ require_relative "recap_store"
|
|
|
6
6
|
|
|
7
7
|
module Samagotchi
|
|
8
8
|
# Session chains: a session that continues an earlier one (Session#continues),
|
|
9
|
-
# "the next
|
|
9
|
+
# "the next run of the same routine". The chain is derived, never stored:
|
|
10
10
|
# follow +continues+ back for the earlier links, forward (the session whose
|
|
11
11
|
# +continues+ names this one) for the later ones. A link is continued at
|
|
12
12
|
# most once (SessionManager.continue_session refuses a second), so the
|
|
@@ -69,7 +69,7 @@ module Samagotchi
|
|
|
69
69
|
# @param recap [Hash, nil] RecapStore.read's
|
|
70
70
|
# @return [String]
|
|
71
71
|
def note_text(previous, recap:)
|
|
72
|
-
head = "This session continues #{previous.id[0, 8]} (#{
|
|
72
|
+
head = "This session continues #{previous.id[0, 8]} (#{clock(previous.created_at) || previous.created_at}), the previous link of its chain."
|
|
73
73
|
text = recap && recap[:text].to_s.strip
|
|
74
74
|
return "#{head} It left no recap." if text.nil? || text.empty?
|
|
75
75
|
|
|
@@ -102,12 +102,6 @@ module Samagotchi
|
|
|
102
102
|
nil
|
|
103
103
|
end
|
|
104
104
|
|
|
105
|
-
def day(iso)
|
|
106
|
-
Time.iso8601(iso.to_s).localtime.strftime("%Y-%m-%d")
|
|
107
|
-
rescue ArgumentError
|
|
108
|
-
iso.to_s
|
|
109
|
-
end
|
|
110
|
-
|
|
111
105
|
def clock(iso)
|
|
112
106
|
iso && Time.iso8601(iso.to_s).localtime.strftime("%Y-%m-%d %H:%M")
|
|
113
107
|
rescue ArgumentError
|
data/lib/samagotchi/version.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// Session chains on the page (docs/sessions.md, Session chains): a session
|
|
2
|
-
// that continues an earlier one (its summary's `continues`), "the next
|
|
2
|
+
// that continues an earlier one (its summary's `continues`), "the next run of
|
|
3
3
|
// the same routine". The chain is derived from the list, never stored: back
|
|
4
4
|
// through `continues`, forward through the session whose `continues` names
|
|
5
5
|
// this one. The lists fold a chain into its latest listed link's card (a
|
|
6
|
-
//
|
|
6
|
+
// `#N` chip, its earlier links in a popover); the session page links the
|
|
7
7
|
// previous and the next link. Pure: lists_view.js places the chip and rows
|
|
8
8
|
// (lit-html templates) and app.js the toast's markup, and they bind them.
|
|
9
9
|
import { normalize, previewForCard, relativeTime } from "./format.js";
|
|
@@ -33,7 +33,7 @@ export function isOpener(text) {
|
|
|
33
33
|
|
|
34
34
|
// A link's card title (format.js previewForCard), display only: a link
|
|
35
35
|
// whose title is the opener, as every link's first message is once sent,
|
|
36
|
-
// reads its chain's first link's title instead, so the
|
|
36
|
+
// reads its chain's first link's title instead, so the links of a chain
|
|
37
37
|
// keep its name. Its own title when the first link isn't in +byId+ (id →
|
|
38
38
|
// session; deleted, or a walk a missing link cut short) or is the opener too.
|
|
39
39
|
export function chainTitle(s, byId) {
|
|
@@ -112,13 +112,32 @@ export function chainNeighbours(list, id, { continues = null } = {}) {
|
|
|
112
112
|
return { prev, next };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
export function chainDate(iso) {
|
|
115
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
116
|
+
const localDate = (iso) => {
|
|
118
117
|
const d = new Date(iso || "");
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// A link's day as the carry note writes its date: the local YYYY-MM-DD of
|
|
122
|
+
// its start; "" when it has none.
|
|
123
|
+
export function chainDate(iso) {
|
|
124
|
+
const d = localDate(iso);
|
|
125
|
+
return d ? `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` : "";
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// A link's start as a local HH:MM; "" when it has none.
|
|
129
|
+
export function chainClock(iso) {
|
|
130
|
+
const d = localDate(iso);
|
|
131
|
+
return d ? `${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// A link's start as a chain list shows it: its date (chainDate), with its
|
|
135
|
+
// HH:MM when one of +otherIsos+ (the chain's other links' starts) is on the
|
|
136
|
+
// same local day, so two links of one day tell apart. "" when it has none.
|
|
137
|
+
export function chainStamp(iso, otherIsos = []) {
|
|
138
|
+
const date = chainDate(iso);
|
|
139
|
+
if (!date) return "";
|
|
140
|
+
return otherIsos.some((o) => chainDate(o) === date) ? `${date} ${chainClock(iso)}` : date;
|
|
122
141
|
}
|
|
123
142
|
|
|
124
143
|
// The families (sessions_list.js families) with the chains folded in: a
|
|
@@ -129,7 +148,7 @@ export function chainDate(iso) {
|
|
|
129
148
|
// index). +all+: every session the page holds, archived ones too (the links
|
|
130
149
|
// the list leaves out). The same array when no chain is in it.
|
|
131
150
|
// @param fold false: every family stays (each link's card carries its
|
|
132
|
-
// chip, "
|
|
151
|
+
// chip, "#1 of 3"; folded false), for a search or the archived ones
|
|
133
152
|
export function foldChains(fams, all, { fold = true } = {}) {
|
|
134
153
|
if (!all.some((s) => s.continues)) return fams;
|
|
135
154
|
const nexts = nextsOf(all);
|
|
@@ -160,15 +179,15 @@ export function allViewChains(fams, all, { query = "", includeArchived = false }
|
|
|
160
179
|
}
|
|
161
180
|
|
|
162
181
|
// The links a family's chain list shows: the chain but its head, newest
|
|
163
|
-
// first, each {session,
|
|
182
|
+
// first, each {session, n} (n: its 1-based place in the chain).
|
|
164
183
|
function chainOthers(f) {
|
|
165
184
|
const links = f?.chain?.links || [];
|
|
166
|
-
return links.map((session, i) => ({ session,
|
|
185
|
+
return links.map((session, i) => ({ session, n: i + 1 }))
|
|
167
186
|
.filter(({ session }) => session.id !== f.head.id)
|
|
168
187
|
.reverse();
|
|
169
188
|
}
|
|
170
189
|
|
|
171
|
-
// The chip in a chain's card: "
|
|
190
|
+
// The chip in a chain's card: "#3" (the head's place; "#2 of 3" when
|
|
172
191
|
// later links aren't listed), with a live dot / "waiting" when one of the
|
|
173
192
|
// other links has a worker / waits on the user (typing in an archived link
|
|
174
193
|
// brings it back, but it stays folded here). nothing for a family in no
|
|
@@ -180,12 +199,12 @@ export function chainChipTemplate(f, { open = false } = {}) {
|
|
|
180
199
|
const others = chainOthers(f);
|
|
181
200
|
const live = others.filter(({ session: s }) => s.owner === "worker").length;
|
|
182
201
|
const waiting = others.filter(({ session: s }) => waitingOn(s)).length;
|
|
183
|
-
const
|
|
184
|
-
const text =
|
|
202
|
+
const n = at + 1;
|
|
203
|
+
const text = n === links.length ? `#${n}` : `#${n} of ${links.length}`;
|
|
185
204
|
const earlier = at;
|
|
186
205
|
const later = links.length - 1 - at;
|
|
187
206
|
const parts = [earlier ? `${earlier} earlier ${earlier === 1 ? "link" : "links"}` : "", later ? `${later} later` : ""].filter(Boolean);
|
|
188
|
-
const title = `Session chain:
|
|
207
|
+
const title = `Session chain: link ${n} of ${links.length} (${parts.join(", ")}); open the list`;
|
|
189
208
|
const liveT = live ? html`<span class="cc-live"> · <span class="live-dot" aria-hidden="true"></span>${live} live</span>` : nothing;
|
|
190
209
|
const waitingT = waiting ? html` · <span class="cc-waiting">${waiting} waiting</span>` : nothing;
|
|
191
210
|
return html`<button type="button" class=${`chain-chip${waiting ? " waiting" : ""}`} aria-expanded=${open ? "true" : "false"}
|
|
@@ -193,24 +212,26 @@ export function chainChipTemplate(f, { open = false } = {}) {
|
|
|
193
212
|
}
|
|
194
213
|
|
|
195
214
|
// The chain's list (the chip's popover): one row per other link, newest
|
|
196
|
-
// first: status dot, "
|
|
215
|
+
// first: status dot, "#N", its date (with its HH:MM when another link of
|
|
216
|
+
// the chain, the head too, started the same day: chainStamp), its recap's first sentence (else
|
|
197
217
|
// its preview), the waiting / stopped badge, archived. A row's click opens
|
|
198
218
|
// that link (families_view.js's popover binds .family-row[data-id]).
|
|
199
219
|
// @param activeId the open session's id (its row is .active)
|
|
200
220
|
export function chainRowsTemplate(f, { activeId = null, now = Date.now() } = {}) {
|
|
201
221
|
const byId = new Map((f?.chain?.links || []).map((s) => [s.id, s]));
|
|
202
|
-
const
|
|
222
|
+
const links = f?.chain?.links || [];
|
|
223
|
+
const rows = chainOthers(f).map(({ session: s, n }) => {
|
|
203
224
|
const cls = ["family-row", "chain-row"];
|
|
204
225
|
if (activeId && s.id === activeId) cls.push("active");
|
|
205
226
|
if (s.archived) cls.push("archived");
|
|
206
227
|
const waiting = waitingBadge(s, { inFamily: true });
|
|
207
228
|
const stopped = waiting ? null : stoppedBadge(s);
|
|
208
|
-
const date =
|
|
229
|
+
const date = chainStamp(s.created_at, links.filter((l) => l !== s).map((l) => l.created_at));
|
|
209
230
|
const ago = relativeTime(s.updated_at || s.created_at || "", now);
|
|
210
231
|
const tip = `${s.id} · ${s.status || ""}${ago ? ` · updated ${ago}` : ""}`;
|
|
211
232
|
return html`<div class=${cls.join(" ")} data-id=${s.id} style=${"--depth:1"} role=${"listitem"}><button type="button" class="fr-open"
|
|
212
|
-
title=${tip}>${rowDotTemplate(s, waiting)}<span class="cr-day"
|
|
213
|
-
date ? html`<span class
|
|
233
|
+
title=${tip}>${rowDotTemplate(s, waiting)}<span class="cr-day">#${n}</span>${
|
|
234
|
+
date ? html`<span class=${date.length > 10 ? "cr-date timed" : "cr-date"}>${date}</span>` : nothing}<span class="fr-preview">${s.recap || chainTitle(s, byId)}</span>${
|
|
214
235
|
rowBadgesTemplate(waiting, stopped)}${s.archived ? archivedBadge : nothing}</button></div>`;
|
|
215
236
|
});
|
|
216
237
|
return html`<div class="family-rows chain-rows" role="list">${rows}</div>`;
|
|
@@ -1033,6 +1033,8 @@ body.no-glass :is(#familyPop,#chainPop){background:var(--bg-panel);-webkit-backd
|
|
|
1033
1033
|
.chain-chip .cc-waiting{font-family:system-ui,sans-serif;font-weight:600}
|
|
1034
1034
|
.chain-row .cr-day{font-family:ui-monospace,monospace;font-size:11px;color:var(--accent);white-space:nowrap}
|
|
1035
1035
|
.chain-row .cr-date{font-family:ui-monospace,monospace;font-size:11px;color:var(--text-muted);white-space:nowrap;font-variant-numeric:tabular-nums}
|
|
1036
|
+
/* A row with its time (chainStamp): every row's date as wide, so the previews line up. */
|
|
1037
|
+
.chain-rows:has(.cr-date.timed) .cr-date{min-width:16ch}
|
|
1036
1038
|
/* The session page's links to the previous and the next link of its chain. */
|
|
1037
1039
|
#infoBar .meta a.chain-link{text-decoration:none;font-family:ui-monospace,monospace;white-space:nowrap;color:var(--accent)}
|
|
1038
1040
|
#infoBar .meta a.chain-link:hover{background:var(--accent-bg)}
|
|
@@ -21,12 +21,12 @@ import { chipTemplate as toolkitChip } from "./toolkit_chip.js";
|
|
|
21
21
|
export const PREV = "←";
|
|
22
22
|
export const NEXT = "→";
|
|
23
23
|
|
|
24
|
-
// A chain's ← / → link (chains.js chainNeighbours): its date (else
|
|
25
|
-
// id), its
|
|
26
|
-
// archived}.
|
|
24
|
+
// A chain's ← / → link (chains.js chainNeighbours): its date or time (else
|
|
25
|
+
// its short id), its id, full start and preview in the tooltip. +link+:
|
|
26
|
+
// {id, date, stamp, preview, archived} (info_bar_model.js chainLinkView).
|
|
27
27
|
export function chainLinkTemplate(link, arrow, what) {
|
|
28
28
|
const short = String(link.id).slice(0, 8);
|
|
29
|
-
const title = `${what}: ${short}${link.preview ? ` · ${link.preview}` : ""}${link.archived ? " (archived)" : ""}`;
|
|
29
|
+
const title = `${what}: ${short}${link.stamp ? ` · ${link.stamp}` : ""}${link.preview ? ` · ${link.preview}` : ""}${link.archived ? " (archived)" : ""}`;
|
|
30
30
|
const text = arrow === PREV ? `${arrow} ${link.date || short}` : `${link.date || short} ${arrow}`;
|
|
31
31
|
return html`<a class="model chain-link" href=${sessionHash(link.id)} title=${title}>${text}</a>`;
|
|
32
32
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// its list and its in-flight actions. Pure: app.js's updateInfoBar applies
|
|
5
5
|
// it, every second while a turn runs (the session clock).
|
|
6
6
|
import { canStopSession, childrenSummary, delegatedBy, modelLabel, promptNotesChip } from "./format.js";
|
|
7
|
-
import { chainDate, chainNeighbours } from "./chains.js";
|
|
7
|
+
import { chainClock, chainDate, chainNeighbours } from "./chains.js";
|
|
8
8
|
import { ctxBarText, ctxBarTip, lastSpeedText, speedText } from "./ctx.js";
|
|
9
9
|
import { formatDuration, sessionDurationNow } from "./timing.js";
|
|
10
10
|
import { workerBadge } from "./update_notice.js";
|
|
@@ -21,12 +21,18 @@ export function workerBadgeFor(id, state, versions) {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
// A chain's link (chains.js chainNeighbours) as the info bar's ← / →
|
|
24
|
-
// shows it (info_bar.js chainLinkTemplate), or null.
|
|
25
|
-
//
|
|
26
|
-
|
|
24
|
+
// shows it (info_bar.js chainLinkTemplate), or null. Its date: its local
|
|
25
|
+
// HH:MM when it started on the open link's day (+selfIso+: the open
|
|
26
|
+
// session's created_at; the day is implied), else its YYYY-MM-DD; its
|
|
27
|
+
// stamp (the tooltip's) always both. +cardTitle+: the lists' title for a
|
|
28
|
+
// session (a chain's opener reads its first link's).
|
|
29
|
+
export function chainLinkView(link, cardTitle, selfIso = null) {
|
|
27
30
|
if (!link) return null;
|
|
28
|
-
|
|
29
|
-
|
|
31
|
+
const iso = link.session?.created_at;
|
|
32
|
+
const day = chainDate(iso);
|
|
33
|
+
const clock = chainClock(iso);
|
|
34
|
+
return { id: link.id, date: day && day === chainDate(selfIso) ? clock : day, stamp: day ? `${day} ${clock}` : "",
|
|
35
|
+
preview: link.session ? cardTitle(link.session) : "", archived: !!link.session?.archived };
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
// The bar's buttons: continue (not on a link continued already, which has
|
|
@@ -66,6 +72,9 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
66
72
|
const avgSpeed = speedText(tokens?.avg_decode_tps, tokens?.tps_source);
|
|
67
73
|
const sessionDuration = sessionDurationNow(state.timing, running);
|
|
68
74
|
// A delegated session names its parent, as a link to it.
|
|
75
|
+
// The open link's start: the payload's, else its list entry's (a fresh
|
|
76
|
+
// link may not be listed yet: then neither, and its links show dates).
|
|
77
|
+
const selfIso = state.createdAt || sessions.find((x) => x.id === id)?.created_at || null;
|
|
69
78
|
const parentChip = delegatedBy(state.parentId, sessions.find((x) => x.id === state.parentId) || null);
|
|
70
79
|
return {
|
|
71
80
|
id,
|
|
@@ -74,7 +83,7 @@ export function infoBarModel({ id, state, sessions, chain, cardTitle, badge, res
|
|
|
74
83
|
model: modelLabel(state.model, ...(state.served || [])),
|
|
75
84
|
// A link of a session chain: ← the previous link, → the next one (each
|
|
76
85
|
// only while the list holds it).
|
|
77
|
-
chain: { prev: chainLinkView(chain.prev, cardTitle), next: chainLinkView(chain.next, cardTitle) },
|
|
86
|
+
chain: { prev: chainLinkView(chain.prev, cardTitle, selfIso), next: chainLinkView(chain.next, cardTitle, selfIso) },
|
|
78
87
|
parent: parentChip ? { id: state.parentId, title: parentChip.title } : null,
|
|
79
88
|
// A parent names its delegates; the link opens all sessions.
|
|
80
89
|
kids: childrenSummary({ id }, sessions),
|
|
@@ -129,7 +129,7 @@ export function cardAnimStyle({ enterMs = null, now = null } = {}, period = DOT_
|
|
|
129
129
|
// member does; with rows (all sessions) an open family lists its members
|
|
130
130
|
// inline, the strip's opens in a popover; matchIds: a search's matching
|
|
131
131
|
// members. A family in a session chain (chains.js foldChains) carries its
|
|
132
|
-
//
|
|
132
|
+
// `#N` chip, which opens the other links in a popover. entering: a card
|
|
133
133
|
// new to its list (enteringCards: it fades in); anim: its inline animation
|
|
134
134
|
// phases (cardAnimStyle).
|
|
135
135
|
// +ctx+: title (cardTitle), projectName (the page's scope), activeId (the
|
|
@@ -342,7 +342,7 @@ export function createListsView(deps) {
|
|
|
342
342
|
doc.addEventListener("scroll", () => familyPop.reposition(), true);
|
|
343
343
|
|
|
344
344
|
// A chain's popover (chains.js): the other links of the chain a card's
|
|
345
|
-
//
|
|
345
|
+
// `#N` chip stands for, under the chip, in the strip or all sessions
|
|
346
346
|
// (the list it was opened from: chainFrom). A row opens that link.
|
|
347
347
|
const chainFamilies = new Map(); // head id → family, as the lists last drew it
|
|
348
348
|
let chainFrom = null; // the list element the open popover's chip is in
|
|
@@ -23,6 +23,9 @@ export function emptySessionState() {
|
|
|
23
23
|
parentId: null,
|
|
24
24
|
// The previous link of its chain (its `continues`), or null.
|
|
25
25
|
continues: null,
|
|
26
|
+
// Its start (the summary's created_at; the info bar's chain links are
|
|
27
|
+
// times on its day), or null.
|
|
28
|
+
createdAt: null,
|
|
26
29
|
ctxPct: null,
|
|
27
30
|
// Window the kernel resolved for the running generation (:generation_started).
|
|
28
31
|
ctxWindow: null,
|
|
@@ -107,6 +110,7 @@ export function sessionStateFrom(data) {
|
|
|
107
110
|
state.toolkit = s.toolkit || null;
|
|
108
111
|
state.parentId = s.parent_id || null;
|
|
109
112
|
state.continues = s.continues || null;
|
|
113
|
+
state.createdAt = s.created_at || null;
|
|
110
114
|
state.commands = Array.isArray(data.commands) ? data.commands : [];
|
|
111
115
|
return state;
|
|
112
116
|
}
|
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.75.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dmitry Dedov
|
|
@@ -380,6 +380,7 @@ files:
|
|
|
380
380
|
- lib/samagotchi/memory_bundle/installer.rb
|
|
381
381
|
- lib/samagotchi/memory_bundle/listing.rb
|
|
382
382
|
- lib/samagotchi/memory_bundle/manifest.rb
|
|
383
|
+
- lib/samagotchi/memory_bundle/memory_files.rb
|
|
383
384
|
- lib/samagotchi/memory_bundle/merger.rb
|
|
384
385
|
- lib/samagotchi/memory_bundle/placeholder.rb
|
|
385
386
|
- lib/samagotchi/memory_bundle/plugin_ref.rb
|