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
+53
-8
@@ -99,7 +99,19 @@ const activeKey = ref(stored(ACTIVE_STORE_KEY) ?? DEFAULT_KEY)
|
||||
watch(activeKey, (key) => persist(ACTIVE_STORE_KEY, key))
|
||||
|
||||
const transcript = useTranscript()
|
||||
const { items: transcriptItems, live: liveTurn } = transcript
|
||||
const {
|
||||
items: transcriptItems,
|
||||
live: liveTurn,
|
||||
loadingOlder,
|
||||
prependCount,
|
||||
} = transcript
|
||||
|
||||
// Whether the record still holds turns above what the mirror shows.
|
||||
// The stream opens on the newest turn and the reader walks back page
|
||||
// by page (ChatPanel asks when the scroller nears its top, or when the
|
||||
// transcript is too short to fill the screen), so a session that ran
|
||||
// for weeks costs nothing up front and still reaches its first message.
|
||||
const olderAvailable = ref(true)
|
||||
|
||||
// Attachment ids this page already tried to fetch; a failed fetch is
|
||||
// not retried until the stream resets and clears the set.
|
||||
@@ -529,20 +541,27 @@ let stream: { close: () => void } | null = null
|
||||
function resubscribe(): void {
|
||||
stream?.close()
|
||||
transcript.reset()
|
||||
olderAvailable.value = true
|
||||
attemptedAttachments.clear()
|
||||
notices.value = []
|
||||
streamStatus.value = 'connecting'
|
||||
stream = claw.subscribeEvents(activeKey.value, {
|
||||
onEvent: (event) => transcript.applyEvent(event),
|
||||
onEvent: (event, seq) => transcript.applyEvent(event, seq),
|
||||
// The record lost what this mirror holds (a wiped or reset
|
||||
// database): drop it, and the tail the claw sends next rebuilds it.
|
||||
onReset: () => {
|
||||
transcript.reset()
|
||||
olderAvailable.value = true
|
||||
attemptedAttachments.clear()
|
||||
scannedItems = 0
|
||||
},
|
||||
onStatus: (status) => {
|
||||
streamStatus.value = status
|
||||
// A retry reopens the stream with the claw's replay of the
|
||||
// recorded turns: drop the mirror and let it rebuild, or the
|
||||
// replay would double everything already on screen. The
|
||||
// attempted-attachment set goes too — a fetch that failed during
|
||||
// the outage must retry against the recovered stream.
|
||||
// A retry resumes from the last seq this mirror holds, so the
|
||||
// claw sends the gap and nothing else: the mirror stays as it
|
||||
// is. Only the attachment set is cleared — a fetch that failed
|
||||
// during the outage should retry against the recovered stream.
|
||||
if (status === 'reconnecting') {
|
||||
transcript.reset()
|
||||
attemptedAttachments.clear()
|
||||
scannedItems = 0
|
||||
}
|
||||
@@ -552,6 +571,28 @@ function resubscribe(): void {
|
||||
})
|
||||
}
|
||||
|
||||
/** Fetches one page of turns older than the mirror's first and stacks
|
||||
* it above. Whole turns only, so the page never duplicates or skips:
|
||||
* the daemon ends it exactly where the mirror begins. */
|
||||
async function loadOlder(): Promise<void> {
|
||||
const before = transcript.firstSeq.value
|
||||
if (before === null || !olderAvailable.value || transcript.loadingOlder.value) return
|
||||
transcript.loadingOlder.value = true
|
||||
const key = activeKey.value
|
||||
try {
|
||||
const page = await claw.history(key, before)
|
||||
// The reader may have switched sessions while the page was in
|
||||
// flight; a page belongs to the mirror it was asked for.
|
||||
if (key !== activeKey.value) return
|
||||
transcript.prependOlder(page.events)
|
||||
olderAvailable.value = page.more
|
||||
} catch (err) {
|
||||
noteApiError(err)
|
||||
} finally {
|
||||
transcript.loadingOlder.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(activeKey, resubscribe)
|
||||
|
||||
let healthTimer: ReturnType<typeof setInterval> | undefined
|
||||
@@ -641,8 +682,12 @@ onUnmounted(() => {
|
||||
:session="activeSession"
|
||||
:active-backend="providersState?.active ?? null"
|
||||
:stream-status="streamStatus"
|
||||
:loading-older="loadingOlder"
|
||||
:older-available="olderAvailable"
|
||||
:prepend-count="prependCount"
|
||||
:identity="identity"
|
||||
:avatar-url="identityAvatarUrl"
|
||||
@older="loadOlder"
|
||||
@send="send"
|
||||
@open-providers="go({ view: 'settings', section: 'providers' })"
|
||||
@consent-answer="answerConsent"
|
||||
|
||||
Reference in new issue
Block a user