samagotchi 0.170.0 → 0.171.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 +29 -1
- data/docs/architecture.md +2 -2
- data/docs/cli.md +37 -23
- data/docs/configuration.md +1 -1
- data/docs/internals/block-renderers.md +2 -2
- data/docs/internals/bridge.md +5 -4
- data/docs/internals/context-telemetry.md +13 -1
- data/docs/internals/engine.md +4 -4
- data/docs/internals/inline-widgets.md +1 -1
- data/docs/internals/objectives.md +1 -1
- data/docs/internals/participants.md +235 -119
- data/docs/internals/web-frontend.md +164 -33
- data/docs/internals/working-set.md +69 -5
- data/docs/releasing.md +2 -1
- data/docs/sessions.md +6 -3
- data/lib/samagotchi/bridge/turn_accumulator.rb +8 -4
- data/lib/samagotchi/bridge.rb +39 -12
- data/lib/samagotchi/bundles/system/manifest.yml +1 -1
- data/lib/samagotchi/delivery.rb +13 -0
- data/lib/samagotchi/engine.rb +5 -1
- data/lib/samagotchi/event_fanout.rb +22 -1
- data/lib/samagotchi/grants.rb +5 -0
- data/lib/samagotchi/guardrails/review_paths.rb +2 -2
- data/lib/samagotchi/image_store.rb +51 -12
- data/lib/samagotchi/kernel_loop.rb +7 -1
- data/lib/samagotchi/llm/stray_tool_call.rb +7 -4
- data/lib/samagotchi/llm_context_stale.rb +4 -1
- data/lib/samagotchi/participant_messages.rb +39 -0
- data/lib/samagotchi/participant_revise.rb +61 -0
- data/lib/samagotchi/participant_uploads.rb +98 -0
- data/lib/samagotchi/reply_wait/poll.rb +5 -1
- data/lib/samagotchi/revise_anchor.rb +9 -0
- data/lib/samagotchi/send_command.rb +5 -0
- data/lib/samagotchi/session_inbox.rb +20 -12
- data/lib/samagotchi/session_manager/delivery.rb +7 -0
- data/lib/samagotchi/session_stores.rb +2 -0
- data/lib/samagotchi/tool_runner.rb +39 -5
- data/lib/samagotchi/tools/memory.rb +26 -7
- data/lib/samagotchi/turn_anchors.rb +0 -3
- data/lib/samagotchi/turn_stages/verify_anchors.rb +13 -1
- data/lib/samagotchi/verify_anchor.rb +6 -0
- data/lib/samagotchi/version.rb +1 -1
- data/lib/samagotchi/web/app.rb +42 -77
- data/lib/samagotchi/web/participant_access.rb +129 -26
- data/lib/samagotchi/web/participant_images.rb +185 -0
- data/lib/samagotchi/web/participant_review.rb +19 -138
- data/lib/samagotchi/web/participant_show.rb +240 -0
- data/lib/samagotchi/web/participant_stream_filter.rb +25 -2
- data/lib/samagotchi/web/participant_view.rb +331 -24
- data/lib/samagotchi/web/public/annotate_bar.css +1 -2
- data/lib/samagotchi/web/public/annotate_bar.js +1 -2
- data/lib/samagotchi/web/public/app.js +124 -88
- data/lib/samagotchi/web/public/block_host.js +7 -4
- data/lib/samagotchi/web/public/bubbles.js +9 -1
- data/lib/samagotchi/web/public/cards_offscreen.js +69 -0
- data/lib/samagotchi/web/public/chip_popover.js +5 -4
- data/lib/samagotchi/web/public/claim_link.js +4 -4
- data/lib/samagotchi/web/public/composer.css +4 -6
- data/lib/samagotchi/web/public/composer_mode.js +11 -6
- data/lib/samagotchi/web/public/composer_mount.js +4 -4
- data/lib/samagotchi/web/public/ctx.js +12 -3
- data/lib/samagotchi/web/public/delivery_line.js +34 -5
- data/lib/samagotchi/web/public/history_shape.js +5 -1
- data/lib/samagotchi/web/public/images.js +7 -1
- data/lib/samagotchi/web/public/index.html +40 -4
- data/lib/samagotchi/web/public/info_bar_model.js +2 -1
- data/lib/samagotchi/web/public/markdown_place.js +5 -4
- data/lib/samagotchi/web/public/owner_claim.js +3 -3
- data/lib/samagotchi/web/public/owner_verify.js +0 -2
- data/lib/samagotchi/web/public/participant_bar.js +108 -0
- data/lib/samagotchi/web/public/participant_main.js +289 -0
- data/lib/samagotchi/web/public/participant_model.js +5 -292
- data/lib/samagotchi/web/public/participant_names.js +6 -1
- data/lib/samagotchi/web/public/participant_revise.js +66 -0
- data/lib/samagotchi/web/public/participants_panel.js +3 -12
- data/lib/samagotchi/web/public/question_cards.js +14 -3
- data/lib/samagotchi/web/public/refs.js +53 -36
- data/lib/samagotchi/web/public/reload_check.js +68 -11
- data/lib/samagotchi/web/public/revise_stack.js +21 -10
- data/lib/samagotchi/web/public/send_turn.js +24 -6
- data/lib/samagotchi/web/public/session_fields.js +1 -1
- data/lib/samagotchi/web/public/session_state.js +12 -4
- data/lib/samagotchi/web/public/session_sync.js +9 -2
- data/lib/samagotchi/web/public/session_view.js +17 -7
- data/lib/samagotchi/web/public/stage_asking.js +6 -4
- data/lib/samagotchi/web/public/stage_owner_wait.js +35 -0
- data/lib/samagotchi/web/public/stage_view.js +18 -4
- data/lib/samagotchi/web/public/stage_waiting.js +74 -6
- data/lib/samagotchi/web/public/storage.js +22 -0
- data/lib/samagotchi/web/public/stream_handlers.js +2 -1
- data/lib/samagotchi/web/public/tokens.css +1 -1
- data/lib/samagotchi/web/public/turn_block.js +3 -2
- data/lib/samagotchi/web/public/turn_events.js +15 -6
- data/lib/samagotchi/web/public/turn_html.js +9 -2
- data/lib/samagotchi/web/public/turn_range.js +1 -2
- data/lib/samagotchi/web/public/verify_target.js +2 -4
- data/lib/samagotchi/web/public/viewer.js +59 -0
- data/lib/samagotchi/web/public/viewer_self.js +53 -0
- data/lib/samagotchi/web/public/working_set.js +11 -5
- data/lib/samagotchi/web/public/working_set_view.js +53 -8
- data/lib/samagotchi/web/public/ws_cards.js +89 -0
- data/lib/samagotchi/web/public/ws_peek.js +106 -0
- data/lib/samagotchi/web/public/ws_preview.css +126 -0
- data/lib/samagotchi/web/public/ws_reader.js +319 -0
- data/lib/samagotchi/web/public/ws_timeline.js +200 -0
- data/lib/samagotchi/web/public/ws_way_in.js +25 -0
- data/lib/samagotchi/web/session_control.rb +39 -14
- data/lib/samagotchi/web/session_create.rb +1 -1
- data/lib/samagotchi/web/session_show.rb +75 -28
- data/lib/samagotchi/web/viewer.rb +70 -0
- data/lib/samagotchi/web/working_set_routes.rb +132 -0
- data/lib/samagotchi/worker.rb +19 -14
- data/lib/samagotchi/working_set.rb +260 -8
- metadata +20 -6
- data/lib/samagotchi/web/public/participant.html +0 -218
- data/lib/samagotchi/web/public/participant.js +0 -719
- data/lib/samagotchi/web/public/participant_annotate.js +0 -76
- data/lib/samagotchi/web/public/review_history.js +0 -72
- data/lib/samagotchi/web/public/tools_overview.js +0 -42
|
@@ -5,10 +5,11 @@ reaches the screen, and the guards every new piece should use. The modules' own
|
|
|
5
5
|
does; this page says how they hang together. The server side is [architecture.md](../architecture.md) (Web);
|
|
6
6
|
the working set's overlay is [working-set.md](working-set.md), participants are [participants.md](participants.md).
|
|
7
7
|
|
|
8
|
-
No build step: `index.html`
|
|
8
|
+
No build step: `index.html` loads ES modules as they are, with lit-html (`vendor/`) as the
|
|
9
9
|
only library. Pure modules (`*_model.js`, `session_state.js`, `sessions_list.js`, `turn_range.js`, …) touch no DOM
|
|
10
10
|
and have `node --test` specs under `spec/web/public/` (happy-dom for the DOM ones); the pages as a whole are covered
|
|
11
|
-
by `npm run e2e` (`spec/e2e/`). `app.js` has no unit spec.
|
|
11
|
+
by `npm run e2e` (`spec/e2e/`). `app.js` has no unit spec of its own; `viewer_boot.test.js` boots the whole page
|
|
12
|
+
(`page_boot.js`, happy-dom, in a process of its own) for what each viewer gets (The viewer, below).
|
|
12
13
|
|
|
13
14
|
**Every module has a size cap, `app.js` its own**: `npm run lint` fails past `max-lines` in `eslint.config.js`
|
|
14
15
|
(counted as `wc -l` counts, comments and blank lines too). Every `public/*.js` module: 1200 (`samagotchi/module-size`,
|
|
@@ -28,23 +29,19 @@ page. A factory dep a new version stops passing is the same hazard the other way
|
|
|
28
29
|
out of `send_turn.js`, `session_actions.js` and `lists_view.js`); a page loaded before keeps all its old modules
|
|
29
30
|
until a reload, so the risk is a mixed cache, not an open tab. `answer_fold.js`, `details_motion.js`,
|
|
30
31
|
`turn_range.js`, `history_reveal.js`, `latest.js`, `session_payload.js`, `method_api.js`, `session_store.js`,
|
|
31
|
-
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `block_watch.js`, `card_stamps.js`, `recap_stamp.js` (a saved recap's place by its `opening_id` stamp, else its count), `inline_widgets.js`, `widget_walk.js`, `widget_picks.js`, `history_shape.js` (the history's shape, live against a reload: the e2e reload checks and the opt-in reload check share its normalizer and its `ACCEPTED` allowlist), `turn_pieces.js` (a finished turn's one order and its answer rule: the live hand-off's `handOffOrder` and `turnEnded`, and a reload's `turnHistoryNodes`), `reload_check.js` (the opt-in `web.reload_check`: after the stage's hand-off, the turn against a reload of it, the counts posted), `turn_drawing.js` (a finished turn's drawing, `turn_drawings` in the show: its fold into steps and its end kind; `turn_block.js` draws it), `stage_stall.js` (the stage's quiet signal), `history_end.js` (the stage's one move of pieces to the history's end), `stage_opened.js` (a running turn opened in the history while a card waits: the lead-in bubble, the moves, the end fold), `stage_asking.js` (the stage's docked card and its fold: the docking and lead-in rules, the opening, a waiting card's own toggle, the folded row's text per kind, the fold's choice, reveal and the docked card's fold; `stage_view.js` wires it), `stage_waiting.js` (the stage's waiting cards as data, keyed by `data-qid` / `data-card-id`: kind, docked, approval, the stage's order, the shown card and its pager, the docked card's fold
|
|
32
|
+
`session_fields.js`, `feature_gates.js`, `revise_stack.js`, `revise_switch.js`, `shell_command.js`, `stale_params.js`, `popover_modal.js`, `more_sheet.js`, `hash_intent.js` (what the hash asks for: session, all, start or keep), `worker_features.js`, `version_notices.js`, `owner_claim.js`, `owner_verify.js`, `composer_mode.js`, `strip_hidden.js`, `block_open.js`, `text_roots.js`, `block_source.js`, `block_frame.js`, `block_host.js`, `block_watch.js`, `card_stamps.js`, `recap_stamp.js` (a saved recap's place by its `opening_id` stamp, else its count), `inline_widgets.js`, `widget_walk.js`, `widget_picks.js`, `history_shape.js` (the history's shape, live against a reload: the e2e reload checks and the opt-in reload check share its normalizer and its `ACCEPTED` allowlist), `turn_pieces.js` (a finished turn's one order and its answer rule: the live hand-off's `handOffOrder` and `turnEnded`, and a reload's `turnHistoryNodes`), `reload_check.js` (the opt-in `web.reload_check`: after the stage's hand-off, the turn against a reload of it, the counts posted; it compares the turn's own cards and questions, dropping every entry CardStore attributes to another turn), `cards_offscreen.js` (the reload check's placer: `createCardsPlace`, `createBubbles` and `createQuestionCards` over a detached holder, every page hook inert - no stage, no scroll, no session id, no api, no storage - so a snapshot's cards land where a reload would put them without touching the page), `turn_drawing.js` (a finished turn's drawing, `turn_drawings` in the show: its fold into steps and its end kind; `turn_block.js` draws it), `stage_stall.js` (the stage's quiet signal), `history_end.js` (the stage's one move of pieces to the history's end), `stage_opened.js` (a running turn opened in the history while a card waits: the lead-in bubble, the moves, the end fold), `stage_asking.js` (the stage's docked card and its fold: the docking and lead-in rules, the opening, a waiting card's own toggle, the folded row's text per kind, the fold's choice, reveal and the docked card's fold; `stage_view.js` wires it), `stage_waiting.js` (the stage's waiting cards as data, keyed by `data-qid` / `data-card-id`: kind, docked, approval, the stage's order, the shown card and its pager, the docked card's fold, kept for this tab in `chi_asking_fold:<session>`; `front`, `count`, `shown`, `blocking`), `stage_cards.js` (the cards that wait in the stage: `PENDING`, `ASKS`, `waitsInStage`, read by `stage_model.js placeKind`), `resync_reach.js` (a re-read that can't reach chi web keeps the drawn history; the status chip says retrying / offline, outside a turn), `session_sync.js` (the open session's sync: `select`, `resync` and its retries, the stream's life; the stream handle and the re-read bookkeeping are its own), `session_view.js` (the open session's view: `renderSessionView` draws a read, `forgetSession` resets the page for another session, `readTurnEnd`, `endTurnView`; app.js makes both right after the cards, each reaching the other and the later views through getters), `participant_main.js`, `participant_bar.js`, `viewer_self.js`, `stage_owner_wait.js`, `participant_revise.js` (a participant's page: The participant's page, below), `ws_timeline.js`, `ws_cards.js`, `ws_peek.js`, `ws_reader.js`, `ws_way_in.js` (what chi has of a working-set item: below, The working set's reader), `no_contact_fill.js` (a text field that isn't a contact form: autocomplete off and a name with no contact word, against iOS Safari's AutoFill Contact bar; a template's fields carry the same attributes, and its spec checks the page's markup for them) and the `objectives_*.js` modules are new modules for that reason. The last four `block_*` draw fenced blocks (`block_watch.js` finds and watches them, `block_host.js` is its frame drawer for a bundle's renderers): [block-renderers.md](block-renderers.md). `inline_widgets.js` is its core drawer for `chi-ui` blocks, drawing `widget_walk.js`'s and `widget_picks.js`'s widgets in a shadow root styled by `widgets.css`: [inline-widgets.md](inline-widgets.md).
|
|
32
33
|
|
|
33
|
-
##
|
|
34
|
+
## One page, every viewer
|
|
34
35
|
|
|
35
36
|
| Page | Entry | Model |
|
|
36
37
|
|---|---|---|
|
|
37
|
-
| **
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
`
|
|
43
|
-
|
|
44
|
-
`EventSource`. Its parallel copies of the owner's Verify and claim helpers (`verifyFreshState`, `verifyAnchorFor`,
|
|
45
|
-
`showPendingClaim`, `CLAIM_ROOTS`; the owner's are `owner_verify.js` and `owner_claim.js`) differ on purpose in places (the source kind `reply` vs `answer`, the server's
|
|
46
|
-
`fresh_running` vs `verify_fresh`, no home/cwd in its claim options). Don't converge them as a side effect of another
|
|
47
|
-
change.
|
|
38
|
+
| **The main page**: the start page and a session page, the owner's and a participant's | `index.html` → `app.js` | **Render once, then patch.** A session's open (`session_sync.js` `select` → `resync` → `session_view.js` `renderSessionView`) draws the history from `GET /api/sessions/:id`; from then on each stream event changes the DOM in place (`stream_handlers.js`). The DOM *is* the log. The start page is the same app with `store.selected` null. |
|
|
39
|
+
|
|
40
|
+
A participant's link (a review or pair grant) opens the same page: chi web serves them `index.html` stamped with
|
|
41
|
+
their viewer (The viewer, below), their show and stream are filtered (`Web::ParticipantView`,
|
|
42
|
+
[participants.md](participants.md#what-a-participant-sees)), and what is theirs on the page is
|
|
43
|
+
`participant_main.js` (The participant's page, below). There is no second page: the participant page
|
|
44
|
+
(`participant.html`, `participant.js`, `GET …/review`) went with the participant main page's Phase C.
|
|
48
45
|
|
|
49
46
|
## Where state lives (owner page)
|
|
50
47
|
|
|
@@ -58,7 +55,7 @@ change.
|
|
|
58
55
|
| **The history DOM** | the conversation as drawn (a running turn's reason bubble, `.bubble.said`, is live only: see A turn opened in the history): which turn a bubble belongs to (`data-turn-id`, `data-answer-turn`), what opens a turn (`TURN_PROMPT`, `bubbles.js`: a prompt or a wake's note, the same turns `Steer.turn_opening?` counts for a card's `turns_since`), an opening's id for a card's and the saved recap's stamp (`data-turn-id` on a prompt, `data-opening-id` on a wake's note or `/continue no`'s reason line; `card_stamps.js` places a snapshot's card after the stamped opening, `recap_stamp.js` the saved recap after the last turn it covers, each by its count (`turns_since`) only without one), kept command bubbles, which texts were shown, unnamed badges, folded answers and long prompts (`.folded`), Revise stamps (`data-revises` on a prompt; a Verify turn's `data-verify`) and the stacks' folded versions (`.stack-hidden`, `.stack-stub.open`); the switcher's version on view is `revise_stack.js`'s own (a copy `.stack-view` before the top answer, the top `.stack-swapped`; never stored) | `bubbles.js`, `cards_place.js`, `question_cards.js`, `turn_block.js`/`stage_view.js`, `answer_fold.js`, `revise_stack.js`, `stream_handlers.js` (a live `turn_completed`'s `revises` and `verify` onto its prompt) | read back as state: `turn_range.js turnElements` (one walker for a turn's elements), `keptCommandBubbles`, `seedSeenFromDom`, `nameUnnamedBadges`/`nameVerdictCards`, `answer_fold.js` and `revise_stack.js` (their observers decorate every path that fills `#history`; the stacks are `stacks()` of the stamps, read again on each change, never a store field), `history_reveal.js revealInHistory` (the one way a jump unfolds what hides its target) |
|
|
59
56
|
| **A drawn turn** (`turn_drawings` of the full read) | every finished turn, as live drew it: `turnGroups` groups by the stamps only, a turn's stamped items (`drawn: <turn_id>`) one group, and `turnHistoryNodes` draws it by feeding its drawing's events to a live turn block offscreen (`turn_block.js drawnTurnNodes`: the block's own handlers, as the stage's), the stage's extras (a reminder, merged lines, a card that asks or warns) and end as live, then the answer, the merged lines and the steps' texts from their saved items (markdown, refs). Each of its top-level elements carries `data-drawn`: `cards_place.js` leaves that turn's in-turn CardStore entries (`of_turn`) to it, and a kept reminder bubble of that turn (`data-turn-id`) stays out after a resync. chi web sends a drawing for every finished turn (one made from its messages where live wrote none): `drawnGroup` pairs the record the end names (`end.record_id`, by place for a turn saved before turn ids), else by the turn's id, and the answer's `data-answer-turn` is the end's own `turn_id` only (a made-up id, `m…` or `…~N`, keys none: no Revise). Nothing is rebuilt from messages: an item that comes unstamped is a `plain` group drawn as a plain row of its text (its prompt or answer bubble, no steps), the running turn's as expected (`runningTurnId`, the show's `current_turn`: it has no line yet), any other a miss, marked `data-undrawn`, logged once per render (`console.warn`) and counted by the reload check (`undrawn`), never a throw; a drawing with no parts (a minimal line) draws its prompt, answer and end | chi web (the show) | `renderHistory`, the reload check's render |
|
|
60
57
|
| **localStorage** (`storage.js` `readStored`/`writeStored`, guarded) | per-browser choices only | the module that owns the choice | read at start (see Storage keys) |
|
|
61
|
-
| **SSE** | see Streams | the server |
|
|
58
|
+
| **SSE** | see Streams | the server | events go into the DOM directly (a participant's through `participant_main.js wrapHandlers` first) |
|
|
62
59
|
|
|
63
60
|
The objectives' strip keeps the list in its own closure (`objectives_strip.js`, seeded by `renderSessionView`, reset by
|
|
64
61
|
`forgetSession`, updated by `objectives_updated`): [objectives.md](objectives.md#the-page).
|
|
@@ -203,6 +200,113 @@ the running turn's DOM) is cleared there, and the feature gates are primed for t
|
|
|
203
200
|
(`pendingReviseAnchor`) is set only through `setReviseAnchor`, which redraws its chip and the Revise buttons (a
|
|
204
201
|
redraw only it feeds, not a store field).
|
|
205
202
|
|
|
203
|
+
## The viewer
|
|
204
|
+
|
|
205
|
+
Who looks at the main page and what they may do there is the **viewer** (`viewer.js`), chi web's `Web::Viewer`
|
|
206
|
+
(`lib/samagotchi/web/viewer.rb`: `Viewer.for(principal)` is the one place that decides it; the owner gets every
|
|
207
|
+
capability). chi web stamps it on `<body>` (`data-viewer-kind`, `data-viewer-can`: the names, space-separated;
|
|
208
|
+
`App#index_data_attributes`), and the owner's session show carries it as `viewer` next to `capabilities`. app.js
|
|
209
|
+
makes one `viewer` from the body at boot; `setCapabilities` passes a show's `viewer` to `viewer.update` (it narrows
|
|
210
|
+
the stamp, never widens it: the boot's controls were made from the stamp) and `forgetSession`'s
|
|
211
|
+
`setCapabilities(null, null)` resets it to the stamp. A page with no stamp may do nothing. It is not the show's
|
|
212
|
+
`capabilities` (what the page renders: markdown display, the editor) nor `refEditor()` (where a ref opens: the
|
|
213
|
+
editor and its URL, refs.js's actions): those two are per request.
|
|
214
|
+
|
|
215
|
+
**The null-control rule.** An owner-only control or call goes through `viewer.can(name)` where it is made, never at
|
|
216
|
+
each use: a control the viewer may not use is not created and not wired (`const x = viewer.can("…") ? createX(…) :
|
|
217
|
+
null`, then `x?.…` where it is read; a module's hook passed as `null`, as `stage_view.js onCancel`,
|
|
218
|
+
`working_set_view.js sendLine`, `composer_mount.js loadHistory`), and its static element in `index.html` stays hidden
|
|
219
|
+
(`viewer.control(el, name)` / `controls(els, name)`: the element when allowed, else hidden, marked `data-withheld`
|
|
220
|
+
and `null`; index.html's `[data-withheld]{display:none!important}` beats an element's own `display` rule, which a
|
|
221
|
+
bare `hidden` does not). A module the
|
|
222
|
+
page reads everywhere takes a flag instead (`question_cards.js answers`, `block_host.js renderers`,
|
|
223
|
+
`turn_block.js turnHistoryNodes`'s `apply`). No scattered `if (owner)`, and no 403 tolerated as a way of gating. The
|
|
224
|
+
gates are boot-time: a show's narrower `viewer` changes what `viewer.can` answers later, not the controls made.
|
|
225
|
+
|
|
226
|
+
| Capability | Controls | Calls |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| `sessions` | `#newBtn`, `#scope` (and `project_picker.js`), `#topStrip`, `#stripStatus`, `#stripGrip` (`strip_hidden.js`) | `refresh` (`GET /api/sessions`), `openSessionEvents` (`GET /api/events`), `/api/projects` |
|
|
229
|
+
| `models` | `#modelPick` (never shown without the list) | `startPage.loadModels` (`GET /api/models`) |
|
|
230
|
+
| `prompt_history` | ↑/↓ over the prompt history | `GET /api/history` (`loadHistory`) |
|
|
231
|
+
| `push` | `#notifyBtn` (`notifications.js` not made; the tab title is set directly) | `/api/push*`, presence, the second `/api/events` stream |
|
|
232
|
+
| `context` | the context chips (`context_chips.js`) | `…/context*` |
|
|
233
|
+
| `working_set` | the stage's working set (`working_set_view.js`) | `GET …/working_set` |
|
|
234
|
+
| `working_set_items` | the list rows' `▸` peek, the reader over the stage, a tool row's `timeline`, the ref menu's "What chi has" (`ws_reader.js createWorkingSetPreview`: not made without it, so `working_set_view.js` gets `peek: null`, `ws_way_in.js` stays off, `installRefClicks` gets `whatChiHas: null`) | `GET …/working_set/item` (on a tap only) |
|
|
235
|
+
| `participants_admin` | the participants chip and panel, Copy link's participant target (`claimTarget`) | `…/participants*` |
|
|
236
|
+
| `rename` | `#titleRenameBtn` | (`/title` goes as a session command) |
|
|
237
|
+
| `images` | `#attachBtn`, `#attachInput`; paste and drop toast `IMAGES_NOT_HERE` | `POST …/images` |
|
|
238
|
+
| `session_controls` | `#infoContinueBtn`, `#infoArchiveBtn`, `#infoStopBtn`, `#infoDeleteBtn`, `#infoMoreBtn` (the ⋯ sheet), the ⚙ chip and panel, the `/` list, the stage's task stop, the working set's drop/undrop | `POST …/command` (`composer.js`: without it a `/` or `!` line goes as a turn, and the worker decides), `…/stop`, `…/holds*`, `…/tasks/:id/stop`, `…/reload_check`, continue → (`POST /api/sessions`) |
|
|
239
|
+
| `revise` | the answers' Revise (`revise_stack.js`) | (a Revise goes with `POST …/turn`) |
|
|
240
|
+
| `revise_own_turn` (a participant's) | Revise on the answers to their own messages (`participant_revise.js`, `revise_stack.js mayRevise`); the stacks for everyone | (`POST …/turn` with `revises`, checked by chi web) |
|
|
241
|
+
| `apply` | the Apply row (`turn_block.js`, `placeApply`, its click) | `GET …/apply` |
|
|
242
|
+
| `answer_cards` | question, approval and continue cards (`question_cards.js`), System 1's "wrong" | `…/answer`, `…/question/dismiss`, `…/guardrails/wrong` |
|
|
243
|
+
| `render` | | `POST /api/render` (`markdown_place.js`: step texts, a question's kept reply) |
|
|
244
|
+
| `block_renderers` | a bundle's drawn blocks (the core `chi-ui` drawer stays) | `/api/block_renderers*` |
|
|
245
|
+
| `stop_any_turn` | the stage's Stop turn (`onCancel`) | `POST …/cancel` |
|
|
246
|
+
| `steer_any_turn` | none: the delivery line reads a participant's own `steer_own_turn` instead (`viewerTurn`) | |
|
|
247
|
+
|
|
248
|
+
A participant's `can` (`Viewer::PARTICIPANT_CAPABILITIES`, in their show's `viewer` and on `<body>`) names
|
|
249
|
+
capabilities of their own, not the owner's: `send` (the composer), `stop_own_turn` (Stop turn on a turn they
|
|
250
|
+
started, `POST …/cancel` with its `turn_id`), `steer_own_turn` (a message during their own turn goes in at its next
|
|
251
|
+
step, on a worker with `participant_steer`), `revise_own_turn` (Revise on the answer to their own message); and of the owner's only `working_set` and `images`, which their routes
|
|
252
|
+
serve filtered: the working set's project files ([participants.md](participants.md#the-main-pages-show)); their own
|
|
253
|
+
uploads within their cap and quota, read back only by them ([participants.md](participants.md#participant-images)).
|
|
254
|
+
Their messages carry their own images as refs (thumbnails) and anyone else's as a count, which `images.js
|
|
255
|
+
thumbsTemplate` draws as a muted "N images" line (`.thumbs-hidden`). `stop_any_turn` / `stop_own_turn` both make the
|
|
256
|
+
stage's Stop turn (`onCancel`); a participant's shows on their own turn only (`stage_view.js canCancel`).
|
|
257
|
+
|
|
258
|
+
The owner routes (`App::ROUTES` rows marked `:owner`) the page never calls without one of the above:
|
|
259
|
+
`/api/update*` and `…/restart` (`version_notices.js`: its versions come only from the hub stream, `sessions`);
|
|
260
|
+
`…/archive`, `…/unarchive`, `DELETE …/:id` (the bar's buttons, `session_controls`; the strip and the all view,
|
|
261
|
+
`sessions`); `POST /api/sessions` from the start page (a participant's page opens the grant's session and never the start
|
|
262
|
+
page) and the
|
|
263
|
+
participants panel's review start (`participants_admin`); `GET …/images/:file` (a message's image refs; a
|
|
264
|
+
participant's messages carry refs only for their own uploads, the one image a participant's route serves them); the objectives strip's posts and the plugin cards' actions (`…/command`:
|
|
265
|
+
drawn only from the show's `objectives` / `cards` and their stream events, which `ParticipantView` drops). Not called
|
|
266
|
+
by the page at all: `/api/sessions/summaries`, `…/output`, `…/notes`, `…/wake`, `…/worker`, `…/exit`, `…/recap`,
|
|
267
|
+
`…/stats`, `…/children/counts`, `…/turns/:id/records`, `/api/participants`. `viewer_boot.test.js` pins the table:
|
|
268
|
+
with every capability today's calls and controls, with none no owner call and no owner control, each one alone,
|
|
269
|
+
and a participant's (their show, the working set and their stream, nothing else).
|
|
270
|
+
|
|
271
|
+
## The participant's page
|
|
272
|
+
|
|
273
|
+
What is a participant's on the main page is `participant_main.js`, made by app.js only when `viewer.kind()` is
|
|
274
|
+
`participant` (`participant`, null on the owner's page) and wired in a few lines; its pure views are
|
|
275
|
+
`participant_bar.js`. The page stays the owner's model (render once, then patch); the participant's parts only read:
|
|
276
|
+
|
|
277
|
+
- **One session.** `<body data-session>` (the grant's) is the session `selectFromHash` opens, whatever the hash
|
|
278
|
+
names (an old `#s=ID` link, a claim link's `#s=…&t=…&q=…`, which `owner_claim.js` still shows); `allViewOpen` is
|
|
279
|
+
never true; there is no start page.
|
|
280
|
+
- **Who they are** (`viewer_self.js`): set from each show's `viewer` (`setCapabilities` → `participant.shown`),
|
|
281
|
+
since `<body>` names no ids. `turn_events.js isOwn` counts their client id (`review:<grant>`, chi web's for every
|
|
282
|
+
tab of theirs) as the tab's own; a `turn_started` with `mine` starts their queued bubble (`bubbles.js
|
|
283
|
+
queuedOwnBubble`: no origin names its enqueued id to them). `turn_html.js userBubbleTemplate` gives their echo
|
|
284
|
+
their badge and the owner's prompts the "owner" label; `participant_names.js authorBadgeText` reads a pair turn as
|
|
285
|
+
the name alone for them ("· review" stays).
|
|
286
|
+
- **The rows.** The top row: a plain logo (`.plain-brand`, not `#newBtn`), their name ("Alex · invited") and, on a
|
|
287
|
+
desktop, when the link ends. The bottom row (`#participantBar`) in `#infoText`'s place, drawn by the `infoBar`
|
|
288
|
+
redraw (`updateInfoBar` returns early): status, ctx %, the last speed (the turn's time while one runs), their
|
|
289
|
+
spend of the cap, and on a desktop what their turns may do (`RULES_TEXT`). Never the owner's cost, the model,
|
|
290
|
+
memories or a path.
|
|
291
|
+
- **The running turn.** `wrapHandlers` wraps `stream_handlers.js`'s handlers: `turn_started`'s author and `mine`
|
|
292
|
+
decide Stop turn (`canCancel`) and the delivery line (`deliveryTurn` → `delivery_line.js viewerTurn`: their own
|
|
293
|
+
turn on a `participant_steer` worker takes the message at its next step, Alt+Enter queues; anyone else's queues it
|
|
294
|
+
as their own turn) and the placeholder (`composer_mode.js steers`); `question_requested` / `continue_offered`
|
|
295
|
+
(their `kind` alone) set the stage's waiting look (`stage_owner_wait.js`, through `stage_view.js ownerWait`), no
|
|
296
|
+
card; a turn's end reads the show again for their usage (the light re-reads carry no viewer).
|
|
297
|
+
- **The closed composer** (`#participantClosed`): their cap reached (the show's `cap_reached`, or a send refused
|
|
298
|
+
`cap_reached`: the show read again), a send refused `session_stopped`, or a link that ended (`token_revoked`,
|
|
299
|
+
`token_expired`: a refused send's words, or `linkEnded`, `onAuthLost`'s handler, from a brief read's 401). The
|
|
300
|
+
refusals show the server's `detail` as it is (`send_turn.js closesOn`); every other refusal is the toast.
|
|
301
|
+
- **Revise** (`participant_revise.js`): the stacks are installed for `revise_own_turn` too, so their page folds every
|
|
302
|
+
stack (the owner's included); the Revise button is `installReviseStacks`' `mayRevise`, `participantMayRevise`:
|
|
303
|
+
`mayReviseOwn` mirrors `ParticipantRevise` on `historyTurns`' list (their prompt's `data-author` or this tab's
|
|
304
|
+
echo, an answer, no `data-verify`, and only their own turns after it), false while the composer is closed
|
|
305
|
+
(`participant.closed()`; `onClosed` redraws on a flip). The gate is the `participant_revise` feature, its words
|
|
306
|
+
`PARTICIPANT_REVISE_UNAVAILABLE`, the one page copy of `ParticipantMessages::REVISE_UNAVAILABLE`. A send refused
|
|
307
|
+
`revise_not_own` / `revise_overtaken` is a failed line with the server's `detail`, the text and the chip kept
|
|
308
|
+
(`send_turn.js`).
|
|
309
|
+
|
|
206
310
|
## Render idioms
|
|
207
311
|
|
|
208
312
|
Three, for one job. Pick by what the piece is:
|
|
@@ -211,7 +315,7 @@ Three, for one job. Pick by what the piece is:
|
|
|
211
315
|
|---|---|---|
|
|
212
316
|
| **lit `render(template, keptContainer)`** | most components (~26): the info bar, chips and their popovers, panels, the init row, the working set, `start_page.js` | anything redrawn while the user may be in it: lit keeps the nodes, so focus, typed text, an open `<details>` and a tooltip survive. **The default for new UI.** |
|
|
213
317
|
| **lit as a string builder**: `turn_html.js elementOf(template)`, `lists_view.js` (a fresh fragment, then `replaceChildren`) | history bubbles appended once; the session lists | a node built once and then owned by the DOM (history), or a list redrawn whole. `lists_view.js` throws away lit's diffing and binds card listeners per draw, with an `entering` tracker for its animations |
|
|
214
|
-
| **hand-built DOM** (`createElement`) | `question_cards.js`, `verdict_card.js`, `verdict_fresh.js
|
|
318
|
+
| **hand-built DOM** (`createElement`) | `question_cards.js`, `verdict_card.js`, `verdict_fresh.js` and a few small ones | older pieces; don't add new ones this way |
|
|
215
319
|
|
|
216
320
|
Listeners: every factory is a page-lifetime singleton, so `addEventListener` without a remove is fine; a per-render
|
|
217
321
|
listener lands on a fresh node. A popover goes through `chip_popover.js createChipPopover` (its own outside-click,
|
|
@@ -268,11 +372,11 @@ so a Safari tab's toolbar never covers the composer). index.html's `:root` names
|
|
|
268
372
|
(`env(safe-area-inset-*)`, 0 on a desktop), and only these read them: main's padding (16px, or the inset where it is
|
|
269
373
|
wider; its bottom is 16px less than `--safe-b`, since the session's card floats 16px above main's bottom; a phone's
|
|
270
374
|
session panel has no bottom padding and runs to the screen's bottom, its card at `--dock-bottom`, 16px or the
|
|
271
|
-
indicator's line), a bottom
|
|
272
|
-
sheet's
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
piece reads `--safe-*`, never `env()`: the e2e set the names by hand (`web.spec.js`, a portrait phone and a landscape one),
|
|
375
|
+
indicator's line), a bottom sheet's offset (`chip_popover.js place`: 8px above the indicator, or a keyboard's offset
|
|
376
|
+
alone), the update sheet, the toast's fallback place (in a session `--toast-bottom` already sits it above the card), the
|
|
377
|
+
lightbox's padding and the signed-out banner (`auth_lost.js`, `env()` itself).
|
|
378
|
+
On phones, the standalone app also has a 12px body inset, keeping its content below iOS 26's blurred band under the
|
|
379
|
+
clock. A new fixed or bottom-anchored piece reads `--safe-*`, never `env()`: the e2e set the names by hand (`web.spec.js`, a portrait phone and a landscape one),
|
|
276
380
|
as Chromium has no insets. The theme-color metas are tokens.css's `--bg-top` per theme up to 600px wide (a phone's page starts from that colour at its top edge, so the status bar meets the top bar with no seam), else its `--bg` (`theme.test.js` pins them). An iOS 26 Home Screen app ignores them for the status bar strip: it paints the strip in the body's `background-color`, so a phone's body gradient keeps a `--bg-top` colour layer under it (black strip without one).
|
|
277
381
|
In a session, the dock (composer card) shares the history's centered column, at most 960px wide; its right edge follows a classic scrollbar (`--history-sb`, 0 with overlay scrollbars; index.html and `dock_reserve.js`).
|
|
278
382
|
|
|
@@ -298,8 +402,13 @@ edge, with **a shrink for each asking form and after an answer** (`forgetContent
|
|
|
298
402
|
frame (`frameTarget` returns null) and an unfold refits to what shows. Three things decide it, kept apart: the user's
|
|
299
403
|
**choice** (the ▾ by hand, in memory, written to `chi_stage_collapsed`; never read back from storage to decide, so
|
|
300
404
|
another tab's write doesn't apply live), **revealed** (a ref's "show in turn" opened it for this turn; the choice stays)
|
|
301
|
-
and **the docked card's fold** (the waiting card the user folded, by its key
|
|
302
|
-
|
|
405
|
+
and **the docked card's fold** (the waiting card the user folded, by its key, held in `chi_asking_fold:<session>`
|
|
406
|
+
in sessionStorage: this tab's own, so a reload or a resync mid-turn draws it folded as it was — `stage_waiting.js`
|
|
407
|
+
writes it on the fold and reads it back in `trackFold` once the replay has drawn the card; it goes when the card is
|
|
408
|
+
answered or cancelled (`question_cards.js`), when the fold drops, when the turn hands off, and when the page opens
|
|
409
|
+
another session (`session_sync.js`); `reset` clears the memory only, so the replay can take the fold back). In the
|
|
410
|
+
asking form only that fold counts: the ▾ and a tap on the folded row set or clear it and write nothing to
|
|
411
|
+
`chi_stage_collapsed` (another blocking card that comes to wait meanwhile
|
|
303
412
|
drops it, so the stage opens on the front card), so a card always arrives open (in the card's task, before
|
|
304
413
|
`question_cards.js focusWhenIdle` focuses it) and a new one never matches an old fold; the same card drawn again with its
|
|
305
414
|
id keeps it. The fold reads the registry checked against the slots first (`reconcileWaiting`), so a card answered in
|
|
@@ -423,6 +532,25 @@ writes only when its text changes. Expanded, it is a right-aligned span in the n
|
|
|
423
532
|
the timing line in the status row (`.ts-stall-head`). Hooks: `.turn-stage.stalled` and `data-stall` (1, 2, net) on
|
|
424
533
|
the stage's root. The dot keeps blinking meanwhile.
|
|
425
534
|
|
|
535
|
+
## The working set's reader
|
|
536
|
+
|
|
537
|
+
What chi has of one working-set item ([working-set.md](working-set.md#timeline-what-the-model-has-of-one-item)):
|
|
538
|
+
`ws_timeline.js` is the pure part (the server's events into cards newest first, reads merged by state, and between the same edits, into line
|
|
539
|
+
slices, the file strip, the peek's pick, the step tags from the item's `runs`: working_set.js `itemsOfTurn` keeps each
|
|
540
|
+
row's `tool_id` with its place), `ws_cards.js` the cards' templates (`diff_view.js` for diffs, `.read-line` for lines),
|
|
541
|
+
`ws_peek.js` the popover's peek (one row at a time; working_set_view.js draws it under the row and grows the popover to
|
|
542
|
+
`PEEK_WIDTH`, a phone's sheet to `SHEET_TALL`: `chip_popover.js` reads `width`/`sheetShare` as functions per
|
|
543
|
+
placing), `ws_reader.js` the reader and the wiring (`createWorkingSetPreview`: the peek, the reader, `attach(workingSet)`
|
|
544
|
+
for the set, its jumps and its list, and `whatChiHas` for refs.js), `ws_way_in.js` a tool row's `timeline` button (a
|
|
545
|
+
flag turn_html.js reads; on only once the reader is attached). State: the reader's own closure (the open item, its
|
|
546
|
+
answer, the tab), never `store.current`. The reader is a `section` in `#center` over the history and the composer
|
|
547
|
+
(both `inert` while it is open, so their scroll place stays; a phone's is `position: fixed`). Each item answer goes
|
|
548
|
+
through `createLatest` keyed by the session and the item; a new server snapshot (`onServer`) asks again while it is
|
|
549
|
+
open (the route's messages lag a running turn). Leaving the session closes it and the peek: `session_view.js
|
|
550
|
+
forgetSession` (a switch, the start page, + New, a deleted or archived session) calls `wsPreview.reset()`, so
|
|
551
|
+
`#center` is never left inert, whatever the next session's GET does; close gives focus back to where it was at the
|
|
552
|
+
open (not after a jump or a reset). Styles: `ws_preview.css`.
|
|
553
|
+
|
|
426
554
|
## Stale answers
|
|
427
555
|
|
|
428
556
|
The page switches sessions, reopens a popover or asks again while a fetch is out. An answer for the old ask must be
|
|
@@ -434,11 +562,11 @@ dropped. **`latest.js`** is the guard:
|
|
|
434
562
|
ask out stale (a close, a reset). For a list that is re-read.
|
|
435
563
|
|
|
436
564
|
On it: `context_chips.js`, `project_picker.js`, `participants_panel.js` (`load`), `cards_place.js`
|
|
437
|
-
(`refreshCardBodies`), `stream_handlers.js` (`answer_display`), `working_set_view.js` (`fetchServer`), the
|
|
565
|
+
(`refreshCardBodies`), `stream_handlers.js` (`answer_display`), `working_set_view.js` (`fetchServer`), `ws_peek.js` and `ws_reader.js` (an item's timeline), the
|
|
438
566
|
turn-end re-reads (`session_view.js` `readTurnEnd`, app.js's `refreshTimingFromSession`, `mergeTurnTiming`) and app.js's init-row timers. Still by hand
|
|
439
567
|
(the id decides more than "drop it"): `session_sync.js` `resync`/`retryResync` (the re-read budget, `resyncAgain`), the stream's
|
|
440
568
|
close, `session_actions.js` (continue/review open another session on purpose), `send_turn.js` (a failure for another
|
|
441
|
-
session becomes a toast)
|
|
569
|
+
session becomes a toast). A new await in a component
|
|
442
570
|
uses `latest.js`.
|
|
443
571
|
|
|
444
572
|
## API clients
|
|
@@ -448,7 +576,8 @@ uses `latest.js`.
|
|
|
448
576
|
HTML) or a fetch that fails on a sign-in redirect reports to `auth_lost.js` (`onAuthLost`, set by app.js).
|
|
449
577
|
`cantText(what, error)` is the toast.
|
|
450
578
|
- `method_api.js methodApi(method, path, body)`: the same, as `notifications.js` and `push_devices.js` call it.
|
|
451
|
-
- `
|
|
579
|
+
- A participant's page uses `data.js api` too: a 401 (their link ended) goes to `participant_main.js linkEnded`
|
|
580
|
+
(`onAuthLost` set again by app.js), never the owner's signed-out banner.
|
|
452
581
|
|
|
453
582
|
## Streams
|
|
454
583
|
|
|
@@ -457,10 +586,10 @@ uses `latest.js`.
|
|
|
457
586
|
| The hub, `GET /api/events` (`data.js openEvents`) | app.js `openSessionEvents` (scoped by `?dir`) | `snapshot` then `session` / `session_gone` → `applySessionEvent` → `allSessions`; EventSource reconnects itself (a fresh snapshot); `onClosed` (a proxy sign-in on reconnect): probe, reopen after `EVENTS_REOPEN_MS` |
|
|
458
587
|
| A second hub stream, unscoped | `notifications.js` (`allEvents`) on a project's page | the Home Screen app's device tidy-up across projects |
|
|
459
588
|
| The open session, `GET /api/sessions/:id/stream?from_seq=` (`data.js openStream`) | `session_sync.js` `startStream` from `store.current.streamSeq` | events → `stream_handlers.js`. It closes itself on an error; the page resyncs (`resync`: a fresh render and a new stream from its cursor), once at once when it had opened, else after `STREAM_RETRY_MS`. Meanwhile a turn the stage holds reads "reconnecting…" (Quiet, under Frames), and the Bridge's `: ping` is invisible to the page. A re-read that can't reach chi web keeps the history it drew (`resync_reach.js`): the session bar's status chip says "retrying" while `retryResync` tries, "offline" once the tries ran out (`store.current.reach`; short words that fit the chip's reserved width, the full words in its tooltip), but only while no turn runs: during a turn the stage's "reconnecting…" is the one signal (on a phone the bar can hide its status) and the chip keeps the turn's word, and any hub event for the session then re-reads it (`followSelectedSession`); the next drawn re-read clears it. Only a first load (select's "Loading…") shows the error in the history |
|
|
460
|
-
| The participant stream | `participant.js connect` | listens for `STREAM_EVENTS`, which is `Web::ParticipantView::EVENTS` (checked by `spec/web/participant_view_spec.rb`); reopened from fresh data when CLOSED |
|
|
461
589
|
|
|
462
|
-
A new event type
|
|
463
|
-
`EVENTS` or `
|
|
590
|
+
A new event type is a handler in `stream_handlers.js`; the participant view spec makes it decide `EVENTS` or `DROPPED`,
|
|
591
|
+
and checks that every type in `EVENTS` has a handler there or in `participant_main.js` (a participant's page opens
|
|
592
|
+
the same stream, filtered).
|
|
464
593
|
|
|
465
594
|
## Storage keys
|
|
466
595
|
|
|
@@ -474,13 +603,15 @@ reach another device or the server.
|
|
|
474
603
|
| `chi_composer_collapsed` | `composer_fold.js` | the composer folded (a card's text field folds it for the time it has the focus on a narrow or touch screen, `composer_grip.js watchCardField`, on top of this and of the visit's state: neither is written) |
|
|
475
604
|
| `chi_composer_floor` | `composer_grip.js`, `composer_mount.js` | the composer's dragged height |
|
|
476
605
|
| `chi_strip_hidden` (`chi_zen`: the old name, read once) | `strip_hidden.js` | the session strip hidden |
|
|
477
|
-
| `chi_stage_collapsed` | `
|
|
606
|
+
| `chi_stage_collapsed` | `stage_asking.js` | the stage collapsed by hand (not written from the asking form, whose fold is that question's only) |
|
|
478
607
|
| `chi_working_set_collapsed` | `working_set_rail.js` | the working set rail collapsed |
|
|
479
608
|
| `chi_markdown_tip_dismissed` | `markdown_tip.js` | the markdown tip dismissed |
|
|
480
609
|
| `chi_update_available_shown` | `version_notices.js` | update notices already shown |
|
|
481
610
|
| `chi_notify`, `chi_push_device` | `notifications.js` (an injected storage) | the bell's state, this device's push id |
|
|
482
611
|
| `chi_answer_fold:<session>` | `answer_fold.js`, `revise_stack.js` | the answers and long prompts (`<turn id>:prompt`) folded by hand, and the Revise stubs unfolded by hand (`stub:<turn id>`) (swept with the session list) |
|
|
483
|
-
| `chi_review_history:<grant>`, `chi_review_history_session:<session>` | `review_history.js` (participant page) | a participant's composer history |
|
|
484
612
|
|
|
485
|
-
sessionStorage
|
|
613
|
+
sessionStorage (this tab only, `storage.js` `readSession`/`writeSession`/`removeSession`, guarded the same way):
|
|
614
|
+
`chi_client_id` (`turn_events.js`: this tab's client id, kept over a reload mid-turn),
|
|
615
|
+
`chi_asking_fold:<session>` (`stage_waiting.js`: the docked card the user folded and how many cards waited at the
|
|
616
|
+
fold, so a reload or a resync mid-turn draws the asking form as they left it), and
|
|
486
617
|
`chi.authReloadAt` (`auth_lost.js`: one automatic reload a minute).
|
|
@@ -13,6 +13,17 @@ after the call: `ToolRun#stamp`, `{path:, mtime_ns:, size:}`. The path is the on
|
|
|
13
13
|
path against the session's folder, `File.expand_path`, no realpath). A stat that fails, or that isn't a file's (a
|
|
14
14
|
directory), gives no stamp; nor does any other tool.
|
|
15
15
|
|
|
16
|
+
An ok `memory_read` is stamped differently: `Tools::MemoryRead.call` returns a `Tools::MemoryRead::Result`, a String
|
|
17
|
+
with `reads` besides it - per name read, the file that answered and the model overlay appended to it, if any (KernelLoop
|
|
18
|
+
hands those to the runner as `result[:memory_reads]`). Its stamp is
|
|
19
|
+
`{memories: {name => {path:, mtime_ns:, size:, overlay: {…}}}}`, one entry per name, with no `mtime_ns` at its top.
|
|
20
|
+
That is compat N-1 in both directions: a reader that only knows file stamps (`WorkingSet.stamp_of` needs a top-level
|
|
21
|
+
`mtime_ns`) sees nothing in it, and a session saved before memory stamps has no stamp at all - either way its memories
|
|
22
|
+
are never stale. The name it read from, and its overlay, are what "changed since" is measured against, so a system
|
|
23
|
+
memory the model read is stale when its file or the overlay read with it moved, even if a project memory of the same
|
|
24
|
+
name exists. A `read`, `edit` or `write` of a memory's file is still a file item: the page has no memory row for
|
|
25
|
+
that path, so counting it as the memory comes with the page builds that map it too.
|
|
26
|
+
|
|
16
27
|
Both `ToolResponse` shapes save the stamps as `tool_stamps`, one per run, read by the run's place
|
|
17
28
|
(`ToolIds::Ref#run`):
|
|
18
29
|
- `ToolResponse.joined` (the native loop's batch entry): `runs.map(&:stamp)`, only when one is non-nil, like
|
|
@@ -84,7 +95,8 @@ saved the build doesn't pair the sent conversation at all (the view returns the
|
|
|
84
95
|
dropped` and the page offers its undrop.
|
|
85
96
|
|
|
86
97
|
**Stale** is a file item whose latest run left a stamp, and whose file now differs from that stamp in mtime or size,
|
|
87
|
-
or is gone.
|
|
98
|
+
or is gone. A memory item goes the same way: the entry's file differs from the stamped one, or the model overlay
|
|
99
|
+
stamped with it does, or either is gone. No stamp, live. A forgotten item stays forgotten whatever its file did.
|
|
88
100
|
|
|
89
101
|
**An item's outputs.** `WorkingSet.outputs_of(conversation, kind:, key:, strategy:, cwd:)` answers an item's
|
|
90
102
|
content outputs (`WorkingSet::Output`: `id`, the tool_response entry's `index`, `derived` for a legacy entry's id,
|
|
@@ -93,8 +105,9 @@ content outputs (`WorkingSet::Output`: `id`, the tool_response entry's `index`,
|
|
|
93
105
|
when it has outputs and none is `sent`. An edit-only item, a failed run's file or an unknown key has none; a
|
|
94
106
|
`memory_read` of several names is an output of each name.
|
|
95
107
|
|
|
96
|
-
**Cost.** Two `paired_runs` walks, one view build
|
|
97
|
-
10 ms on an M5 Ultra and 75 ms on a CI runner; `spec/working_set_spec.rb` only guards
|
|
108
|
+
**Cost.** Two `paired_runs` walks, one view build, a stat per file item and one per memory stamp. A 500-message
|
|
109
|
+
session builds in about 10 ms on an M5 Ultra and 75 ms on a CI runner; `spec/working_set_spec.rb` only guards
|
|
110
|
+
against a blow-up (< 400 ms).
|
|
98
111
|
|
|
99
112
|
## Delivery
|
|
100
113
|
|
|
@@ -122,10 +135,11 @@ a canceled continue's restore, a failed turn's) announces the set at once (`anno
|
|
|
122
135
|
smaller set reaches the page then, not at the next request. It goes out whatever the web's flag;
|
|
123
136
|
the Bridge (`ClientEvent.project`) passes it on unchanged, and the attached TUI ignores it.
|
|
124
137
|
|
|
125
|
-
**The route.** `GET /api/sessions/:id/working_set` (`Web::
|
|
138
|
+
**The route.** `GET /api/sessions/:id/working_set` (`Web::WorkingSetRoutes#handle_working_set`, mixed into App) answers the snapshot's `to_h`
|
|
126
139
|
and `running`. With a live worker it builds from the worker's snapshot messages (as `handle_show` (`Web::SessionShow`) reads them, keys
|
|
127
140
|
symbolized) and the worker's `llm_context` summary. Without one it builds from the session file and its saved chip
|
|
128
|
-
(`SessionSetting::LLM_CONTEXT`)
|
|
141
|
+
(`SessionSetting::LLM_CONTEXT`); both through `working_set_source` (a `WorkingSetRoutes::Source`), which the item's
|
|
142
|
+
timeline route builds from too (Timeline, below). The folder is the session's `working_directory`. `running` is true when the
|
|
129
143
|
snapshot has a turn in progress (`current_turn`). Between turns, a newer worker's `idle_working_set` is used directly;
|
|
130
144
|
an older worker without that key still gets the file-build fallback.
|
|
131
145
|
|
|
@@ -138,6 +152,11 @@ event's, so the page takes it (below). Before the turn's first event the turn's
|
|
|
138
152
|
Engine emits only a set that differs from the last one), and the GET builds from the messages as between turns; so
|
|
139
153
|
does one from an older worker without the field, its answer then behind the turn's events, which the page keeps.
|
|
140
154
|
|
|
155
|
+
**An item's timeline while a turn runs** (`GET …/working_set/item`, [Timeline](#timeline-what-the-model-has-of-one-item))
|
|
156
|
+
has no such event to answer: it always builds from the snapshot's messages, so mid-turn it lags the turn's runs
|
|
157
|
+
(the running turn's reads of the item are not in it yet). Accepted for v1: the page asks again on each
|
|
158
|
+
`:working_set_updated` while its reader or a peek is open, and the turn's end brings the messages up to date.
|
|
159
|
+
|
|
141
160
|
## Steering: drop and undrop
|
|
142
161
|
|
|
143
162
|
`/working-set drop KEY…` and `undrop KEY…` (`WorkingSetCommand`, `SessionCommands#working_set`; the web's list posts
|
|
@@ -214,6 +233,51 @@ projection), on both loops. It costs one model step; a model may skip it (the no
|
|
|
214
233
|
A running turn's set has none (the note was sent when it started), so the Bridge snapshot's `working_set` and the
|
|
215
234
|
GET's `running` answer leave it out.
|
|
216
235
|
|
|
236
|
+
## Timeline: what the model has of one item
|
|
237
|
+
|
|
238
|
+
**The projection.** `WorkingSet.timeline_of(conversation, kind:, key:, strategy:, cwd:)` answers a
|
|
239
|
+
`WorkingSet::Timeline` (`kind`, `key`, `events`): every run of the item in conversation order, edits and writes too,
|
|
240
|
+
from the same `all_touches` walk `build` and `outputs_of` use, so the set, the steer and the timeline never disagree
|
|
241
|
+
on which runs are an item's. Each `WorkingSet::Event` is `tool_id` (ToolIds; `derived` for a legacy entry's), the
|
|
242
|
+
tool_response entry's `index`, the global `step` (`LLMContextStale::PairedRun#step`: the page's step tags are its own,
|
|
243
|
+
see below), `op` (`read`, `edit`, `write`, `memory_read`, `memory_write`, `fetch`), `sent` (the projection's
|
|
244
|
+
`still_sent?`; an edit or a write always), `dropped` (the user's applied drop), `forgotten` (not sent, not by a drop),
|
|
245
|
+
and what it holds:
|
|
246
|
+
- a read: `lines` (`WorkingSet::Lines`: `first`, `last`, `whole`, `partial`) from `LLMContextStale.file_run`, never
|
|
247
|
+
re-parsed from the arguments: `last` is counted off the text, so an `end_line` past the file is clamped (its
|
|
248
|
+
`[read: end_line …]` note is taken off the `text`); a whole read over `read.truncate_at_bytes` (the head/tail
|
|
249
|
+
preview) and a ToolRunner-cut output are `partial`, shown as stored, kept out of the file strip and the merging.
|
|
250
|
+
`text`: the stored output without its `[read]` lead;
|
|
251
|
+
- an edit or a write: `diff`, the saved `tool_diffs` (never re-diffed): `ToolResponse.joined` saves an array (one per
|
|
252
|
+
run, nil for none), `.single` the bare Hash, read as `MessageParts` does. nil for a no-op or a failed snapshot, maybe
|
|
253
|
+
`truncated`, `{skipped}` or `{error}`; `diff_view.js diffTemplate` draws them all;
|
|
254
|
+
- a memory_read: the whole output (a read of several names is not split: `shared_with` names the others, as
|
|
255
|
+
`Output#shared_with` does), and `memory` (`WorkingSet::MemoryPart`, from the stamp): `scope` (a file under
|
|
256
|
+
`memories/projects/<key>/` is the project's, else the system's), `path`, `overlay_path`, and for a read of one name
|
|
257
|
+
the appended overlay's text (`overlay`, taken off `text`); nil for a session saved before memory stamps;
|
|
258
|
+
- a memory_write: the text it wrote (its `content` argument); a fetch: the output.
|
|
259
|
+
|
|
260
|
+
Merging overlapping reads, the newest-first order and the step tags are the page's (presentation): the page keeps
|
|
261
|
+
each row's `tool_id` with its place (`working_set.js` items' `runs`), so an event's tag is its row's (`t3·s14`) and
|
|
262
|
+
"go to the call" is the step tags' jump; a legacy row (no id) falls back to the server's global `step`. The file
|
|
263
|
+
strip's length is the largest last line any read knows (a whole read's is the file's then), not a stat. The page:
|
|
264
|
+
[web-frontend.md](web-frontend.md#the-working-sets-reader).
|
|
265
|
+
|
|
266
|
+
**Now.** `WorkingSet.memory_now(conversation, key:, …)` answers what the memory's latest `memory_read` would answer
|
|
267
|
+
now (`WorkingSet::Now`: `text`, `diff`, `gone`): each name that read read, from the file its stamp names, with the
|
|
268
|
+
overlay stamped with it appended as `Tools::MemoryRead.call` appends it (`Model-specific guidance (<key>):`, the key
|
|
269
|
+
off the overlay's file name), and a missing name's `Error:` tail kept; `diff` is `TextDiff.unified` of what the read
|
|
270
|
+
sent against that (`EditPreview`'s shape, so `diff_view.js` draws it; an empty `text` is "no change"). `gone` when a
|
|
271
|
+
file is; nil without a stamp.
|
|
272
|
+
|
|
273
|
+
**The route.** `GET /api/sessions/:id/working_set/item?kind=&key=` (`WorkingSetRoutes#handle_working_set_item`,
|
|
274
|
+
`:owner, :session`: a participant gets 403 from `not_permitted_for`) answers `timeline.to_h`, and with `&now=1` for a
|
|
275
|
+
memory `now:` too. Unlike the set's route it never answers a worker's precomputed set: it builds from
|
|
276
|
+
`working_set_source` (the worker's snapshot messages, else the file's; the session's strategy and folder). Its texts
|
|
277
|
+
are whole (about 1.7k lines at the most seen in real sessions): the tool limits bound them (`read.truncate_at_bytes`,
|
|
278
|
+
the ToolRunner cap), so the page asks this route, never a row's `output` (a 2000-char `ToolOutputCut::WEB_PREVIEW_CHARS`
|
|
279
|
+
preview for a long one). 400 without a known `kind` and a `key`.
|
|
280
|
+
|
|
217
281
|
## The page's overlay
|
|
218
282
|
|
|
219
283
|
The page keeps its row-derived items and their `at` ordinals (the step tags' jump path), and overlays the server's
|
data/docs/releasing.md
CHANGED
|
@@ -119,7 +119,8 @@ The agent does each step and stops where the user has to say yes.
|
|
|
119
119
|
`spec/compat_spec.rb` names it, and the shim goes (or its date moves)
|
|
120
120
|
before the release. The bump also refreshes `spec/shared/compat_wire/*.json`
|
|
121
121
|
by running `UPDATE_COMPAT_WIRE=1 bundle exec rspec spec/compat/wire_readers_spec.rb`;
|
|
122
|
-
commit those fixtures with the release corpus.
|
|
122
|
+
commit those fixtures with the release corpus. If that refresh fails, the
|
|
123
|
+
bump is already written; follow its rerun command before reviewing or committing.
|
|
123
124
|
5. **Check.** Add all release files, including the new compatibility corpus, then run it:
|
|
124
125
|
`git add -A && git commit -m "Release X.Y.Z"` and `bundle exec rake release:check`.
|
|
125
126
|
Fix anything it finds in new commits and run it again.
|