feat(web/claw): open on the newest turn, walk back, and resume from the seq
A reload of a weeks-old session replayed the whole record before the first pixel, and every reconnect reset the mirror and replayed it again. The stream now names each frame's seq and the client keeps it, so a retry sends since=<seq> and receives only the gap, and the mirror is left alone: its items are still true. A first open with no cursor gets the tail, cut to a turn boundary, which paints the latest conversation at once. The turns above it come from the history route, one whole turn per page, fetched when the reader nears the top or when the transcript is too short to fill the screen; a page folds through the same reducer as the live stream and joins above, and the scroll position moves down by the page's height so the reader keeps their place. A reset comment (the record no longer holds the cursor) drops the mirror; the tail that follows rebuilds it. The dev proxy rebuilt the upstream URL from the pathname alone, so since and before never reached the daemon under nuxt dev: every resume was a full replay and every history page was the tail. It forwards the query string now.
This commit is contained in:
1 parent
744ba8b06c
commit
ed79c0cf1b
6 files changed
+337
-49
No files matched your search
+25
-8
@@ -59,17 +59,34 @@ Two consequences worth knowing before changing this code:
|
||||
- **The daemon owns the conversation.** `POST .../messages` is
|
||||
fire-and-forget (202); the reply streams back on
|
||||
`GET /v1/sessions/{key}/events`. The transcript here is a mirror of
|
||||
the stream, and the stream opens with the claw's replay of the
|
||||
session's recorded turns before the live feed — a reload and every
|
||||
reconnect reset the mirror and rebuild from the replay, so the
|
||||
transcript survives the browser, not just the claw.
|
||||
the stream, and the stream opens on the newest turn before the live
|
||||
feed — a reload paints the latest conversation at once instead of
|
||||
replaying a session that may have run for weeks, and the turns above
|
||||
it come from `GET /v1/sessions/{key}/history`, one whole turn per
|
||||
page, as the reader scrolls up (or right away, while the transcript
|
||||
is shorter than the screen). So the transcript survives the browser,
|
||||
not just the claw.
|
||||
- **Every frame names its seq, and a reconnect names it back.** The
|
||||
frame's `id:` is the record's seq for the event, and
|
||||
`useClaw().subscribeEvents` keeps the last one it delivered: a retry
|
||||
sends `?since=<seq>` and receives exactly the gap, so a dropped
|
||||
connection costs nothing on screen and nothing on the wire. The mirror
|
||||
is not reset on reconnect for the same reason — its items are still
|
||||
true. A `reset;` comment means the cursor named events the record no
|
||||
longer holds (a wiped database): the mirror drops, and the tail that
|
||||
follows rebuilds it.
|
||||
- **SSE goes over fetch, not `EventSource`.** The claw's auth reads
|
||||
only the `Authorization: Bearer` header (`http.rs` `require_auth`); it
|
||||
has no query-param fallback, and `EventSource` cannot set headers. So
|
||||
`useClaw().subscribeEvents` holds a `GET` with the auth header, reads
|
||||
the body as a `ReadableStream`, and parses `data:` frames by hand
|
||||
(`composables/useClaw.ts`). Drops retry with backoff; the hub's
|
||||
the body as a `ReadableStream`, and parses `data:`/`id:` frames by
|
||||
hand (`composables/useClaw.ts`). Drops retry with backoff; the hub's
|
||||
`lagged` comments surface as notices.
|
||||
- **The dev proxy forwards the query string.** `server/middleware/
|
||||
claw-proxy.ts` rebuilds the upstream URL from the request's pathname,
|
||||
and `since`/`before` are the whole history protocol: dropping them
|
||||
silently turns every resume into a full replay and every history page
|
||||
into the tail.
|
||||
|
||||
## What the transcript does with the stream
|
||||
|
||||
@@ -83,8 +100,8 @@ Two consequences worth knowing before changing this code:
|
||||
inline card: kind `secret` gets a password input plus submit, kind
|
||||
`phone` gets Approve/Deny, and both carry a deny link
|
||||
(`DELETE /v1/consents/{id}`). The card flips when the answer lands
|
||||
and again on the stream's `consent_resolved` echo; a reconnect's
|
||||
replay rebuilds the cards, so nothing persists client-side.
|
||||
and again on the stream's `consent_resolved` echo; nothing persists
|
||||
client-side, so a reload rebuilds the cards from the replay.
|
||||
- **Attachments render inline.** An `attachment` event becomes a card
|
||||
— media glyph, file name, B/KB/MB size, caption. Images fetch the
|
||||
bearer-guarded bytes (`GET /v1/attachments/{id}`, blob → object URL,
|
||||
|
||||
Reference in new issue
Block a user