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>
The #5466 fix added these two cleanup calls to _restoreSettledSession; the
test_issue5224 node harness extracts+runs that function with every helper
stubbed on globalThis, but hadn't stubbed the two new ones — so the calls threw,
the function's catch returned 'error' instead of 'restored', failing 8 CI shards.
Add the stubs. (Local full-suite missed it on first run; CI sharding surfaced it.)
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.