* fix(web): reset scroll restoration cache when sessionStorage is full
TanStack keeps scroll state in RAM; pruning only the JSON blob did not stop
repeat quota throws. On persist failure for the scroll key, clear the library
cache when guarding real sessionStorage. Treat any write error on that key
(not only QuotaExceededError-shaped) as recoverable.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): cover scroll cache hard reset and non-quota recovery
Exercise TanStack scrollRestorationCache reset on real sessionStorage,
mock-storage isolation, and generic storage write failures for #708.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): sync pruned scroll cache and avoid hard-reset recursion
After a successful storage prune, align TanStack's in-memory
scrollRestorationCache with the trimmed payload. Temporarily unwrap
sessionStorage.setItem when writing through the library cache so hard
reset cannot recurse through the guard (Codex PR review #707).
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Store the last known permission mode in session metadata so YOLO and other
modes survive hub restarts, resume after archive, and reconnect without
waiting for CLI keepalive.
Co-authored-by: Cursor <cursoragent@cursor.com>
When a session is open, the web app now keeps an always-on all:true SSE
connection for sidebar session-updated events while using a second
session-scoped stream for message delivery. Also bump session activity on
hub sendMessage so web-originated sends refresh list timestamps.
Fixestiann/hapi#693
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): embed agent text in voice ready event for readback
Voice onReady now extracts the last speakable assistant message and
embeds it in the ready inject so ConvAI can summarize without the user
re-prompting. Also formats Codex/Cursor stream-json messages for live
context updates and session history.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(web): use domain-neutral voice formatter fixtures
Replace jellybot/subtitle dogfood strings in tests with generic examples.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(web): guard extractSpeakableFromContent for non-arrays in formatMessage
extractSpeakableFromContent also handles content arrays (joins text items),
so calling it unconditionally before the existing array loop caused mixed
text+tool_use payloads to return early without formatting the tool_use item.
Guard with !isContentArray so the loop handles arrays as before.
Adds regression test: mixed text+tool_use array must produce both the text
and the tool-call line (was red before this fix).
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
* fix(web): narrow extractSpeakableFromContent to codex type only
The helper matched any object with a string type and a data property,
so sendSessionEvent({ type: 'message', message }) events (which arrive as
{ type: 'event', data: { type: 'message', message } }) were falsely formatted
as speakable assistant text and could be selected as the ready readback.
Narrow the Codex path to content.type === 'codex' as the comment already states.
Adds regression test: session status event must return null from formatMessage.
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: HAPI <noreply@hapi.run>