* perf(ui): stop session loading from blocking shell
* fix(startup): open routes on their own data, retire the loader for good
Follow-up to review on #5927.
- Route openers are now classified by the data they actually read. Only
/email touches the hydrated session list (its new-chat path falls back to
the most recent session's model when no default chat is set), so every
other route opens as soon as module wiring completes instead of queueing
behind /api/sessions. This is the deferred-route half of #5926, which the
first pass left unimplemented.
- index.html's 5s fallback removes the loader node again. Leaving it in the
DOM indefinitely kept _shouldPreserveStartupComposer true forever on a
hung /api/sessions, so the composer stopped clearing on session switch.
- A missing session module settles hydration instead of leaving the sidebar
on "Loading chats…" and dropping the user's route on the floor.
- Startup sequencing moved to static/js/startupShell.js so it can be run by
tests. The source-text assertions in test_startup_shell_session_loading.py
are replaced by node-driven behavioural tests, per tests/TESTING_STANDARD.md.
- Reverted the unrequested loader a11y rework, removed the duplicated inert
writes (the module stops the wave interval through a callback), and moved
the bootstrap row's inline styles into .session-list-bootstrap.
* fix: preserve session bootstrap failure state
---------
Co-authored-by: Léo <leograndcontact@gmail.com>
* Fix: restore session URL hash writes (removed in cf4e240a)
Restores history.replaceState() calls in selectSession() and
materializePendingSession() that were dropped during the July 23 merge.
Without these, chat URLs never update the address bar hash, making
sessions unshareable and causing bare-URL reloads to land on the
welcome screen instead of restoring the last active chat.
Root cause: selectSession() had its hash-write deliberately removed;
materializePendingSession() lost its during a larger refactor that
added the stale-response and incognito guards.
Fixes#5870 (upstream)
* fix: session URL hash lost when sending message mid-stream
Two independent bugs caused the session hash to disappear from the URL:
Bug 1 — ReferenceError in catch block silently killed error recovery
In handleChatSubmit, two const variables (streamingTTS at line 1922 and
abortCtrl at line 1741) were declared inside the try block but referenced
in the catch block. Since const is block-scoped in JavaScript, they were
undefined in catch, causing a ReferenceError that silently aborted the
error handler. This prevented materializePendingSession() from ever being
called, so no hash was written to the URL.
Fix: Hoisted both as let declarations before the try { block.
Bug 2 — Dual sessions.js ES module instances with mismatched state
app.js imported sessions.js with a version query string
(?v=20260722ctxheader4) while every other module imported ./sessions.js
without one. The browser treated them as different URLs, creating two
separate module instances with independent _pendingChat and
currentSessionId state. createDirectChat() set pending on one instance
while handleChatSubmit() checked hasPendingChat() on the other — so the
pending session never materialized.
Fix: Removed the version query string from the sessions.js import in
app.js and from the modulepreload + script tags in index.html. All
modules now share a single sessions.js instance.
Bonus guard: _adoptOpenedSessionBeforeAutoCreate() now checks
hasPendingChat() before adopting a stale DOM-active session, preventing
the send path from landing in the wrong session when a New Chat is pending.
---------
Co-authored-by: samy <samy@users.noreply.github.com>
The session-dropdown Esc handler only closed .session-dropdown-menu,
leaving the .session-folder-submenu (Move to folder → folder list)
orphaned on screen. Same gap on the click-away path. Extend both
selectors to cover the submenu so a single Esc / outside-click
dismisses the whole stack.
sessions.js executes before chat.js in ES module order, so
window.chatModule is not yet set when _checkServerStream runs on page
load. The resumeStream guard evaluates false and the spinner fallback
kicks in; that fallback only polls stream_status and never retries the
live-resume path, leaving the user with a dead spinner for the entire
duration of the detached agent run.
Fix: add a one-shot retry in the polling loop. On the first tick where
window.chatModule.resumeStream is available, attempt to attach. If it
succeeds, clear the interval and remove the spinner — live SSE streaming
takes over. If the run has already finished (404), the loop continues to
poll status and calls selectSession on completion.
Fixes#3048
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: prevent document link click from resetting active session
Clicking a #document-<uuid> link in chat caused the session to reset
because of two issues:
1. chatRenderer.js: clicking on the text inside an <a> yields a Text
node target whose .closest() is undefined, so preventDefault never
fires and the browser performs a default hash-navigation
2. sessions.js: the hashchange handler treated the entity hash
(document-<uuid>) as a session lookup, found no match, and the
subsequent loadSessions created a new default-model chat
Fix: walk past Text nodes before calling .closest(), and skip
entity-prefixed hashes in the hashchange handler.
Fixes#2035
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(documents): move isOpen=true after container check in openPanel
isOpen was set to true before the #chat-container existence check.
If the container was missing during a race, the function returned
early but isOpen stayed true, preventing the panel from ever
reopening on subsequent calls.
Move isOpen=true to after the container guard so a failed open
doesn't leave the flag stuck.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: live-resume chat stream on session re-entry (#2539)
When a session was re-entered after a page refresh or in a new tab while
its agent run was still streaming, the UI showed a frozen "Generating
response..." spinner, polled stream_status until the run finished, and
then did a full reload. The live tokens were never shown.
Add resumeStream() in chat.js: it consumes GET /api/chat/resume/{id}
(which replays the run's buffer then streams live), renders reply tokens
as they arrive, and reloads the session on completion for the canonical
final render. sessions.js _checkServerStream now calls it on re-entry and
falls back to the previous spinner+poll path if it is unavailable.
* Finalize plain-text resume in place instead of reloading
On stream completion, resumeStream() called selectSession(), forcing a full
history re-fetch and a visible flicker right as the stream finished.
For plain text replies (no tool calls, sources, doc streaming, or multi-round
output) the live tokens are already rendered, so finalize in place: replace the
live bubble with a canonical single message via chatRenderer.addMessage (markdown
+ footer actions + metrics, the same renderer history uses), captured from the
streamed metrics event. No history refetch, no extra round-trip, no flicker.
Rich responses still reload, since their canonical render (tool bubbles, sources,
multi-bubble) is rebuilt from the saved DB record.
* Use a dedicated set for the resume re-attach lock; fix stale docblock
resumeStream() marked its re-attach lock in _backgroundStreams, which
checkBackgroundStream() also reads. On a second re-entry of the same session
while a resume was still live, checkBackgroundStream() mistook that entry for a
same-tab POST stream and spawned its own spinner+poll bubble. Move the lock to a
dedicated _resumingStreams set (also covered by hasActiveStream) so the two paths
no longer collide. Also update the resumeStream docblock to describe the
in-place finalize vs reload split.
The sidebar delete handler fired the DELETE API call without awaiting
it, then called loadSessions() which re-fetches the session list from
the server. If the server hadn't processed the deletion yet, the
session reappeared in the sidebar immediately after being removed.
Await the DELETE response before reloading so the server-side deletion
completes first.
Fixes#1358
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Multimodal content (list of {type, text/image_url} blocks) couldn't be
stored in the DB Text column, causing silent persist failures. On reload
the frontend fell back to String() on the array, rendering
[object Object],[object Object] in the chat.
- Serialize list content as JSON in _persist_message()
- Deserialize back to list in _db_to_session() via _parse_msg_content()
- Extract text parts from multimodal arrays in sessions.js instead of
String() coercion
The list keyboard handler (_onSessionListKeydown) treats Backspace and
Delete as "delete the focused session". When the user double-clicks a
chat to rename it, an <input class="session-rename-input"> is mounted
inside the .list-item row. Backspace on the input bubbles up to the list
container, the handler walks closest('.list-item[data-session-id]') from
e.target, finds the parent row and DELETEs the session via the API —
so a single typo correction nukes the whole conversation.
Bail out at the top of the handler when e.target is an INPUT, TEXTAREA,
or contentEditable element. Arrow / Enter / Delete navigation still
works for rows themselves (the row is the focused element then, not the
input). Mirrors the guard pattern already used in ui.js, notes.js,
tasks.js, calendar.js, emailLibrary.js and galleryEditor.js.
Closes#1007.