fix(web): the bugs a chat that never closes for a turn exposes
An investigation pass over the client, driven by steering's new shape — a composer that is always live makes adjacent latent bugs visible. The event stream now watches for silence: the claw keeps the connection alive with a comment every fifteen seconds, and three missed ones abort the fetch and reconnect, so a half-open TCP after a laptop sleep no longer reads as 'live' forever. A reconnect clears the attempted-attachment set with the transcript, or an image whose fetch failed during the outage never retries against the recovered stream. Saving a token reloads the identity and accounts the 401 froze: the banner cleared, but the pages stayed empty until a browser reload. A failed POST retracts its optimistic sent row (and its pending echo, so a message the claw did take still renders from the stream's echo); before, a send into an unreachable claw left a phantom row waiting for a reply that could never come. Drafts are per-session now — the composer's text belonged to no session, so a draft typed into one session sent into whichever was selected at Enter. The providers login form distinguishes a landed login (the roster carries its id) from a failed one and no longer closes on an unrelated state refresh. Two cost fixes that read as jank on long turns: the streamed reply's markdown re-parsed the whole text per delta (quadratic) and now re-renders on a short trailing debounce; the attachment-image watcher walked the whole transcript on every streamed chunk and now scans only the items it has not seen. The accounts page drops a stray Rust-ism comment, and the avatar row's meta line admits webp like its accept attribute always did.
This commit is contained in:
1 parent
041240f706
commit
94b82e2636
8 files changed
+160
-30
No files matched your search
@@ -178,8 +178,10 @@ export function useTranscript() {
|
||||
* image count is the whole signature the stream carries, and that is
|
||||
* enough — the echo dedupes against what this surface sent, not
|
||||
* against whichever row happens to sit last: a steered turn's end
|
||||
* marker can land between the optimistic row and its echo. */
|
||||
const pendingSent: { text: string; images: number }[] = []
|
||||
* marker can land between the optimistic row and its echo. `itemId`
|
||||
* links an entry to its row, so a send that failed to post can take
|
||||
* both the row and its pending echo back out. */
|
||||
const pendingSent: { text: string; images: number; itemId: number }[] = []
|
||||
|
||||
function ensureLive(): TurnBlock {
|
||||
if (!live.value) live.value = { reply: '', entries: [], outcome: null }
|
||||
@@ -327,10 +329,25 @@ export function useTranscript() {
|
||||
|
||||
/** A locally sent message, shown immediately; the reply streams after.
|
||||
* Its images render from the staged previews — the claw stores them
|
||||
* and the stream's echo dedupes against this row. */
|
||||
function addSent(text: string, images: SentImage[] = []): void {
|
||||
pendingSent.push({ text, images: images.length })
|
||||
items.value.push({ kind: 'sent', id: nextId++, text, images })
|
||||
* and the stream's echo dedupes against this row. Returns the row's
|
||||
* id, so a failed POST can retract it. */
|
||||
function addSent(text: string, images: SentImage[] = []): number {
|
||||
const id = nextId++
|
||||
pendingSent.push({ text, images: images.length, itemId: id })
|
||||
items.value.push({ kind: 'sent', id, text, images })
|
||||
return id
|
||||
}
|
||||
|
||||
/** Retracts a locally sent row whose POST failed: the message never
|
||||
* reached the claw, so neither the reply nor the echo will ever come.
|
||||
* (If it did reach the claw and only the answer was lost, the
|
||||
* pending echo goes with the row — the stream's `user_message` then
|
||||
* renders a fresh one, with the stored attachment ids.) */
|
||||
function removeSent(id: number): void {
|
||||
const index = items.value.findIndex((item) => item.kind === 'sent' && item.id === id)
|
||||
if (index !== -1) items.value.splice(index, 1)
|
||||
const pending = pendingSent.findIndex((entry) => entry.itemId === id)
|
||||
if (pending !== -1) pendingSent.splice(pending, 1)
|
||||
}
|
||||
|
||||
/** A consent card resolved: flipped in place, or ignored when no
|
||||
@@ -412,5 +429,15 @@ export function useTranscript() {
|
||||
pendingSent.length = 0
|
||||
}
|
||||
|
||||
return { items, live, applyEvent, addSent, resolveConsent, setAttachmentUrl, setSentImageUrl, reset }
|
||||
return {
|
||||
items,
|
||||
live,
|
||||
applyEvent,
|
||||
addSent,
|
||||
removeSent,
|
||||
resolveConsent,
|
||||
setAttachmentUrl,
|
||||
setSentImageUrl,
|
||||
reset,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user