After a runtime locale change (setLocale from Settings), the cached
recognition object retains its boot-time lang, causing dictation to
run in the wrong language. Set lang immediately before .start() at
the plain dictation path in _startMicCapture, matching the pattern
already used in _ensureSpeechRecognition().
Closes#5483
Codex gate: _normalizeWebUIVersion suppressed empty/__WEBUI_VERSION__/'not detected'
but NOT 'unknown' — api/updates.py emits WEBUI_VERSION='unknown' on git-describe
failure (Docker/CI images), so a real client version would falsely fire the
stale-client banner against server='unknown'. Suppress 'unknown' (+ case-insensitive
for all sentinels). + regression test asserting unknown/UNKNOWN/Unknown don't fire.
* fix(#5472): harden failed-send draft restore — order persist after clear, honest test
Two NIT-level follow-ups from the Opus review of #5484 (both on already-shipped
v0.51.842 code; neither is a functional break):
1. Draft clear/save ordering. send() fires an unawaited _clearComposerDraft POST
(text:'') at send time, and the failed-send restore fired a separate
_saveComposerDraftNow POST (text:<draft>). Under HTTP/2 multiplexing those two
same-origin writes can be reordered, leaving the SERVER draft empty after a
reload (the in-memory composer restore was always fine). send() now captures
the clear POST's promise and passes it to the restore helper, which chains the
re-persist after the clear resolves (falls back to immediate persist if the
promise is absent/rejects).
2. Honest happy-path test. test_send_still_clears_composer_on_the_happy_path
asserted a bare $('msg').value='' string that matches ~13 sites, so it would
pass even if the MAIN-path clear were removed. It now anchors to the unique
main-path _clearComposerDraft line and window-checks the composer wipe
immediately precedes it.
Tests: +2 (promise-ordering behavioral via node-vm microtask ordering; absent-promise
fallback), signature/call assertions updated. 14 in the #5472 file, 93 across the
related composer/send/draft/upload surface — all green.
* fix(#5472): make deferred draft persist stale-aware (Codex #5488 catch)
The promise-ordering fix introduced a regression: the deferred _persist saved the
CAPTURED snapshot and its _saveComposerDraftNow clearTimeout cancelled any newer
debounced save. So if the user edited the restored composer before the clear POST
settled, the delayed persist overwrote their edit with the stale original text
(lost on reload).
Restructure the helper so the deferred persist is stale-aware at FIRE time:
- If the failed session is still visible, re-read the LIVE composer (.value
+ current S.pendingFiles) instead of the captured snapshot — a post-restore edit
is captured, not clobbered.
- If we restored the visible session but the user has since switched away, SKIP the
stale write entirely (the session-switch save path already saved that session).
- Background failure (sid never visible): persist the captured snapshot, since
there's no live composer to read — it's the only copy.
The visible-composer restore + session guard are unchanged in behavior (moved above
the persist). Helper now returns whether it restored the VISIBLE composer.
Tests: +3 regression cases (post-restore edit captured; switch-away skip; background
snapshot persist), signature/guard assertions updated. 17 in the #5472 file, 87
across the related surface — all green.
Codex re-gate found 2 more false-skips (DOM state OUTSIDE the signature path):
- CORE: profile-switch skeleton stays visible if the new payload signature
matches the last one (empty/same-shaped profile) — skip fires over the skeleton.
- SILENT: stale 'Could not load conversations' banner persists when a transient
fetch failure heals with identical rows.
Fix: capture _hadSessionListSkeleton + _hadSessionListLoadError BEFORE clearing
them, and disallow the identical-signature skip when either was set. + regression
test. (messages.js:1462 finding is a base-comparison artifact for #5472/#5479/#5484,
not in #5467's diff.)
Codex gate found the render-skip signature omitted fields the sidebar renders:
- CORE: pending/running (active_stream_id/has_pending_user_message/pending_started_at)
- SILENT: attention dots
- SILENT: source/read-only/worktree/lineage/child/model/profile + _sidebarReferenceSessions
Any omitted field = a false-skip = stale sidebar.
Root-cause fix (most reliable): serialize the FULL applied row objects +
_sidebarReferenceSessions instead of a hand-picked allowlist — inherently
complete, can't rot when a new rendered field is added. Rewrote the
'ignores non-display fields' test into positive full-serialization + no-strip
regression assertions.
* fix(#5472): preserve composer draft when a send fails
When a provider/background error aborts a send, send() has already cleared
the composer and the persisted draft before /api/chat/start durably accepts
the turn. On a start-time throw the turn is never persisted, so the user's
typed message is lost and must be retyped.
Add _restoreComposerDraftAfterFailedSend(text, sid): restores the typed text
into the composer, keeps staged files in S.pendingFiles, and re-persists the
draft (via _saveComposerDraftNow) so it also survives a reload. Guarded to
no-op when there's nothing to restore and to never clobber a new message the
user began typing during the async send window. Wired into the /api/chat/start
throw handler's general-error branch. Mirrors the existing _trySteer /
_stashClarifyDraft draft-restore idioms.
Tests: tests/test_issue5472_preserve_draft_on_failed_send.py — 3 static wiring
assertions + 4 node-vm behavioral tests of the helper's branching logic.
* fix(#5472): address gate findings — original-payload snapshot, re-stage files, session-guard
Codex + Opus gate on the first commit converged on three real edges:
1. /moa and bundle sends rewrite `text` before /api/chat/start, so restoring
`text` replayed the transformed payload, not the user's typed command.
Fix: capture an immutable _failedSendDraftText snapshot right after the
post-flush trim, before any slash rewrite, and restore that.
2. uploadPendingFiles() clears S.pendingFiles=[] before the start call, so the
original 'keep staged files' assumption was false — a resend silently
dropped attachments. Fix: snapshot S.pendingFiles before the upload drain
(_failedSendFilesSnapshot) and re-stage from it (+ renderTray()) on failure.
3. A start failure after a session switch could write the failed old session's
draft into the currently-visible composer. Fix: the helper now only mutates
the visible composer/tray when sid === the visible session; the sid-keyed
_saveComposerDraftNow persist stays unconditional so the draft survives a
switch-back/reload.
Helper signature is now (draftText, filesSnapshot, sid). Tests expanded to 11
(5 static + 6 node-vm behavioral) covering all three edges: original-vs-mutated
payload, re-stage drained attachments, and cross-session no-pollution.
* test(#5472): bound send() body slice to LIVE_STREAMS instead of fixed char window
The #5472 snapshot-capture lines added ~400 chars near the top of send(), which
pushed 'cancelStream' (in the busy-interrupt branch) past the arbitrary
[send_idx:send_idx+5000] window that test_1062's structural assertions used —
a real CI failure (shard 1, all 3 Pythons) caused by a brittle test, not by the
fix logic. Bound both send()-body slices in test_1062 to the actual function
extent (up to the 'const LIVE_STREAMS=' decl that immediately follows send()) so
future additions near the top of send() don't false-fail these assertions.
---------
Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
Codex gate findings (2 SILENT, both fixed):
- ui.js: incremental prose node now flows through the shared row-decoration
block (data-anchor-scene-row/-row-id/-row-role/-source-event-type) instead of
returning early, so live incremental rows keep the identity attrs the scene
reconciler matches on.
- messages.js: _cancelThrottledSnapshotTimer() + _clearAnchorProseIncrementalNode()
now run on apperror, cancel, _handleStreamError, and _restoreSettledSession
terminal paths (previously only fallback/done/stream_end) — closes the
snapshot-timer/anchor-cache/window-global teardown gap.
- test: extend test_messages_js_stream_perf_cleanup_lifecycle to pin teardown on
all four additional terminal paths.
CORE equivalence (Codex flagged smd != renderMd): reconciled — Opus's 1498-frame
harness against real smd.min.js proves incremental-smd == whole-smd byte-identical;
live-smd-vs-renderMd difference is the already-shipped fidelity model (main live
body streams smd, settles renderMd), and the incremental path is gated on !settled
so settled DOM still comes from renderMd. Not a regression.
Add a dedicated visibilitychange hook for the 60s relative-time refresh poll so skipped hidden-tab ticks repaint immediately when the tab becomes visible, even when no streaming poll is active.
The 30s streaming poll (renderSessionList -> GET /api/sessions + full sidebar
rebuild) and the 60s relative-time refresh (renderSessionListFromCache -> full
DOM rebuild) ran unconditionally, including in hidden background tabs, so a
backgrounded window kept fetching and rebuilding the sidebar it could not show.
Guard both interval callbacks with document.hidden and refresh the list on
visibilitychange when the tab is shown again, mirroring the existing gateway
poll. Visually neutral while the tab is visible; a hidden tab simply stops
burning network and DOM churn and re-syncs the moment it is shown.
Verified: node --check + ESLint runtime guard clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The 30s streaming poll and the gateway poll re-applied /api/sessions and ran a
full sidebar DOM rebuild (list.innerHTML='' + one node tree per row) on every
tick, even when the payload was byte-identical to what was already on screen —
the common case on an idle sidebar.
Add a signature over the applied rows + projects + coarse display state in
_applySessionListPayload and skip renderSessionListFromCache() when it matches
the last rendered signature and no entrance animation is pending. A streaming
row's fields advance each poll so its signature changes and it still repaints;
client-only display toggles (search, active, density, select, lineage
expansion) render through their own paths. The per-refresh INFLIGHT cleanup is
still run on the skip path. The 60s relative-time refresh and all other render
triggers call renderSessionListFromCache directly and are unaffected.
Output-neutral: a skip only happens when the render would produce the same DOM.
node --check + ESLint runtime guard clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two O(n^2) costs on the per-token/per-frame streaming path, both over the
growing answer:
WS2.2 — snapshotLiveTurn() serialized the whole live turn via turn.outerHTML on
every render frame. That snapshot only backs mid-stream session-switch restore,
and the switch path (sessions.js) plus tool/done event boundaries already
capture synchronously, so the per-frame call is throttled to a coarse trailing
snapshot instead of serializing the entire growing DOM each frame.
WS2.3 — the token handler ran _parseStreamState() (strip tool-call XML + extract
inline thinking over the FULL accumulated text) on every token, but _doRender
re-parses once per throttled frame anyway and the only per-token use is the
assistant-row creation gate. Skip the parse once the row exists (the common
case); keep it only until the row is created. Also drop the full-string
toLowerCase() allocation in _stripXmlToolCalls for a non-allocating regex test.
Output-neutral: same rendered DOM; the row-creation gate and snapshot semantics
are preserved (row appears within one 66ms frame; switch/settle snapshots stay
synchronous). node --check + ESLint runtime guard clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In compact-worklog mode the anchor activity scene re-renders every frame, and
its live prose row was rebuilt with renderMd(fullText) each time — re-parsing
the entire growing answer per frame, which is O(n^2) over a long streamed
response (the incremental smd parser that the main body already uses was
bypassed and its segment hidden).
Keep a persistent per-segment smd parser + DOM node (keyed by the prose row's
local_id, using the same safe renderer as the live body) and feed it only the
delta each frame; hand that persistent node back to the scene builder instead
of re-parsing. Turns the per-frame prose cost from O(n) into O(delta), i.e. the
whole answer from O(n^2) to O(n).
Safe by construction: settled rows and any failure (no smd, no stable key,
parser error, text divergence) fall through to the existing renderMd path, so
worst case is identical to today. Cache is bounded (<=32 segments) and self-heals
on non-append text changes, mirroring _smdWrite.
Verified: node --check + ESLint runtime guard clean. Streaming render profiled
in-browser via a per-frame timing probe.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex re-gate found 2 SILENT regressions the certifier missed:
- inline lost the 'Fit to screen' toolbar button (PR gated Fit to lightbox-only)
- the new readable-scale/envelope path leaked into lightbox mode (changed zoom-out floor + fit init for wide diagrams)
Fix: toolbar restored to master shape (Fit for all modes, Fullscreen inline-only);
_minScale() returns the flat _MERMAID_VIEWER_MIN_SCALE for lightbox; lightbox init
restored to master's _fitScale()/box*initialFit; the new readable-height sizing is
now scoped strictly to inline mode. Tests updated to the master-consistent contract.