Re-gate found 2 issues in the prior fix: (1) [CORE] uploadPendingFiles still
defaulted clearPending=true and re-cleared S.pendingFiles after the await,
wiping any attachment staged during the upload window — pass clearPending:false
since we already detached the snapshot. (2) [SILENT] converting File objects to
name strings broke draft-restore signature matching ({value:name} vs canonical
{name,path,size,type}) — preserve the file objects with [..._submittedFiles].
#5912 gate found 2 defects the capture-clear salvage exposed:
- [CORE] uploadPendingFiles() read still-live S.pendingFiles, so a re-entrant/
interrupt send during the upload window inherited + re-uploaded the first
send's attachment. Now snapshot _submittedFiles before the await, detach from
S.pendingFiles, and upload exactly that snapshot.
- [SILENT] the persisted-draft clear ran AFTER the await, cancelling a draft
typed during the upload window. Moved it (and _composerDraftClearPromise)
alongside the textarea clear, before any await. Failed-send restore still
chains off the promise via the pre-await _failedSendFilesSnapshot.
Updated source-assertion tests for the new call shape + ordering.
_micRestartCount bounds the continuity auto-restart loop (_micMaxRestarts=20)
so a stolen audio session can't spin tight. But it only reset on
fresh-start / terminal-end / error -- never on a real onresult. So a long
mobile dictation with 20 natural pauses (each producing speech) hit the cap
mid-session and the mic silently stopped, undercutting the feature's whole
purpose ("keep dictation active through pauses").
Reset _micRestartCount=0 in sr.onresult: a real result proves the restarts
are PRODUCTIVE, not a failure spin. The cap still guards the failure case --
consecutive restarts that yield no speech (onend with no intervening
onresult) keep incrementing and trip the bound.
Regression test seeds the count AT the cap, fires a productive onresult, and
asserts the ensuing pause still restarts (count reset to 0 -> 1). Verified
fail-without / pass-with against boot.js.
Salvage-rebuild of #5473 (@ruizanthony), reworked to OPT-IN per maintainer
decision. New default-OFF setting new_chat_on_workspace_switch: when enabled,
selecting a DIFFERENT workspace starts a fresh chat bound to that workspace
(leaving the current conversation on its original workspace, avoiding stale
cross-workspace context) instead of mutating the current session in place.
Same-workspace selection stays an in-place refresh. Default off preserves the
shipped behavior for everyone.
Wiring: config default + bool key; window._newChatOnWorkspaceSwitch set on
boot + autosave; Settings checkbox (Preferences); gated branch in
switchToWorkspace() that runs before the in-place /api/session/update; 3 i18n
keys across all 15 locales; regression tests (default-off, gated-branch-present,
flag-wiring, checkbox+i18n).
Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
Salvaged from #5294 by @brianmmaina. Rebuilt fresh on master with the maintainer gate fix-spec: continuous+restart gated to MOBILE (desktop stays one-shot), wake-lock async race serialized, error-path toasts + abort-loop bound.
Co-authored-by: brianmmaina <brianmmaina@users.noreply.github.com>
Czech (#5546) was added after #5337 authored its share i18n keys, so the
strict cs-parity test failed on the 14 missing share_session/stop_sharing keys.
Added them to the cs block (English fallback values per locale convention).
Adversarial security re-gate on the rebased head found 4 issues; fixed:
1. [CORE] api/shares.py public snapshot could leak credentials/paths/tool
payloads. The sanitizer relied on redact_session_data (gated on the
user-toggleable api_redact_enabled) — disabling it exposed API keys, a dict
assistant content was stringified as raw tool data, and session.workspace
survived in transcript text. Now the per-message sanitizer applies ALWAYS-ON
credential redaction (_redact_fn_cached, force=True, ignores the setting) +
scrubs known session/workspace/home paths, and refuses to stringify non-text
structured content. Title is force-redacted too. Regression test proves the
boundary holds with api_redact_enabled=False.
2. [SILENT] api/models.py: my rebase wrongly kept anchor_activity_scenes in
METADATA_FIELDS, moving 250-480KB scene bodies before messages and undoing
master's metadata fast-path (#5854). Removed; 17 #5854 tests pass again.
3. [SILENT] static/index.html: the Share/StopSharing/ExportHTML button triplet
was double-applied by the rebase (duplicate IDs → only the first wired).
Deduped to one each + a count assertion test.
4. tests: fixed the stale Session.load-after-/api/session/new setup (master
keeps new sessions memory-only until first message).
Also (UX, found in the visual flow sweep): a clipboard-copy failure after a
SUCCESSFUL share showed a red 'Share failed' toast and skipped opening the page.
The copy is now isolated — the share succeeds, the page opens, and the toast
shows the link if the clipboard write fails, never a false failure.
Co-authored-by: MicoRobot.
Rebased 363 commits forward (branch-point 2026-07-05). Applied the PR's code
diff cleanly across 16 files; resolved api/models.py additively (both master's
process_wakeup_pause/anchor_scene_index fields AND the PR's share_token/
share_created_at fields kept in the Session constructor, metadata list, and
to_dict). CI workflow (.github/workflows/tests.yml) intentionally NOT taken from
the stale branch — master owns it (the branch's apparent removal of Playwright/
Office steps was a PAT-workflow-scope revert; rebase restores master's CI).
Co-authored-by: MicoRobot. Concept approved by maintainer (opt-in public share,
2026-07-11). Note: 4 dynamic share tests fail identically on the original
unrebased head (pre-existing test-env Session.load fragility, not rebase-caused);
to be resolved during the security re-gate in a proper sandbox.
Salvaged from #4750 by @harryazj — pulling ONLY the atomic composer capture-and-clear fix from the larger PR. Prevents a re-entrant/interrupt-mode send from re-reading stale textarea DOM and double-sending.
Co-authored-by: harryazj <harryazj@users.noreply.github.com>
Salvaged from #4627 by @perejaslav. Core Hermes already supports
reasoning_effort=max; this closes the WebUI parity gap (Anchor-A) — adds
max to VALID_REASONING_EFFORTS + dropdown/chip/reasoning-command, keeps
the degrade ladder for capped models.
Co-authored-by: perejaslav <perejaslav@users.noreply.github.com>
Two non-blocking UX improvements from the Fable gate on #5739:
1. Full date+time tooltip (title attr) on the worklog event-time label — the bare
clock is date-ambiguous when a settled session is reviewed later or a run crosses
midnight. New _activityFullClockLabel (range-guarded so a bad epoch never yields
'Invalid Date' in the tooltip) + test.
2. Hide the time label under 420px — its ~50px of fixed chrome squeezes the
collapsed row's flexible preview toward zero on phones.
Co-authored-by: rodboev.
Two gate findings on the worklog-event-timestamp feature (#5700/#5739):
1. Thinking-row timestamps disappeared after settlement. Reasoning source events
carry no server timestamp; the live DOM showed a fallback time but the settled
scene row rebuilt with created_at:null. Now stamp created_at on the source event
the first time it's seen (messages.js apply path) and carry row.created_at
through snapshot hydration, so the timestamp survives live->settled.
2. A garbage huge numeric timestamp (e.g. 1e20) passed the finite/>0 checks and
rendered literal 'Invalid Date'. _timestampSeconds now rejects epochs outside
JavaScript's valid Date range. Added a regression test.
Co-authored-by: rodboev. Gate findings from adversarial Codex re-gate.
Gate review (Codex) reproduced two SILENT regressions in the generation-token
ownership change, both validated against PR head + merge base:
1. sessions.js:1428 — the no-op early-return did NOT fire for a redundant
same-session load (currentSid===sid, !_loadingSessionId was false because a
keep-stale refresh was in flight). The redundant loadSession() then bumped
_loadSessionGeneration and CANCELLED the authoritative in-flight refresh —
stale rows remained while message_count advanced, so later polling could not
detect them. Restore the pre-existing behavior: also return when the in-flight
load is for the SAME sid (!_loadingSessionId || _loadingSessionId===sid).
2. sessions.js:2781 — _ensureMessagesLoaded's finally cleared the
transcript-width reload hint unconditionally, so a stale same-session response
collapsed a long session to the default tail window. Guard the cleanup with
_ownsLoad() so only the owning load clears the hint.
Both fixes retain the PR's cross-session, same-session-rejection, and INFLIGHT
race behavior (47 tests pass incl. the new isolation suite + the 4 edited files).
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
Auxiliary-model task definitions were maintained in BOTH Python and JS and could
drift (the retired session_search task lingered in one). Centralize to a single
backend catalog that panels.js renders+saves from exclusively; purge the retired
slot on reset while preserving other custom bindings; old configs still load.
Gate-pass: Codex SAFE (only session_search removed, 11 other tasks + defaults
unchanged, 22 keys match across 15 locales, legacy migrate preserves custom
bindings, executable DOM harness rendered all rows). 53 own + 887 regression green.
Co-authored-by: t <a@b>
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
_workspaceRouteForPath now resolves app-relative /api/... routes against document.baseURI
so the direct consumers (previewImg.src, media/pdf/html frame src, download href,
window.open) work under a subpath mount instead of 404ing at the server root. Root-mount
deployment unaffected (identical final URLs); api() callers not double-prefixed.
Gate-pass: Codex SAFE (no root-mount regression, all 6 consumers verified), 825 workspace
regression tests + browser-smoke green. Nathan-approved.
Co-authored-by: t <a@b>
Co-authored-by: tomtong2015 <tomtong2015@users.noreply.github.com>
Two visible render PRs, gate-passed (deep-review round), Nathan-approved to ship.
- #5757 (rodboev): mark active model with a Selected badge + open its provider group (#5748).
Screenshot-verified (badge clear + correctly placed on active row).
- #5758 (rodboev): suppress the live-token prose row that is a strict prefix of the final
answer (#5749). Provenance-gated (live-prose:* token rows after last tool row only) so it
can't eat legitimate narration; reproduction test proves the dupe is gone.
Codex SAFE + full regression green (1576 render tests). 18 own tests pass together.
Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Steering a live gateway-owned run had no process-local agent in SESSION_AGENT_CACHE,
so it errored with "No agent cached" and dropped the typed text. Now detects the live
gateway ACTIVE_RUNS stream and queues the steer as the next turn (model/provider/profile
snapshotted first), updates the queue badge, clears the draft, shows a toast — without
cancelling the stream. Gateway sibling of the shipped #5597 fallback.
Codex SAFE + Fable SHIP-UX; byte-identical to PR head; 66 steer/busy-input tests pass;
Nathan visual-approved (desktop + mobile screenshots).
Co-authored-by: t <a@b>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
The connectivity probe schedules retries with setInterval(probe, 3000)
but tore the timer down with clearTimeout(retryTimer). Browsers share a
single numeric handle space between the timer families, so clearing an
interval handle via clearTimeout only worked by coincidence and is
semantically wrong. Match the teardown to how the timer was created.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The composer terminal's output EventSource registered handlers for
output/terminal_closed/terminal_error but none for the transport-level 'error'
event, so a session expiry, killed gateway, or network drop left the terminal
frozen with no feedback and no recordClientSSEError telemetry.
Add an 'error' handler behind the existing identity guard: let the browser
auto-reconnect a CONNECTING source (one-time "reconnecting" note, no manual
loop/backoff), and on a permanently CLOSED source surface a "disconnected" line
and dispose it (null TERMINAL_UI.source) so a restart can reconnect. Notify once
per outage to avoid flooding the pane/telemetry on a flapping connection. Emits
recordClientSSEError('terminal', {ready_state, reason}) like the chat/session
consumers.
UI-evidence (screenshot of terminal on gateway-kill) still needed before merge.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Codex gate CORE: /api/updates/apply + /force ignored the request-body channel
and re-read the saved setting, so a channel switch whose debounced autosave
hadn't landed could apply the OLD channel. The banner now sends the channel the
CHECK reported for each target; the endpoints validate it against the enum and
thread it into apply_update/apply_force_update (None → saved-setting fallback,
preserving prior behavior). Agent stays channel-neutral server-side.
- test_api_timeout: assert the updates/check call keeps timeoutMs:60000 in its
new _checkBody form (functionally identical; still the 60s override)
- panels.js: reword two // comments to drop apostrophes — the smooth-text-fade
brace-matcher test treats ' in a comment as a string delimiter and lost
loadSettingsPanel's closing brace (node --check confirms the JS was always valid)
text-overflow:ellipsis is a no-op on the inline-flex badge; display:block in the
<=768px rule makes the ellipsis actually render for a long experimental describe.
MUST-FIX: /api/updates/check now accepts an explicit 'channel' in the POST body;
the Settings dropdown passes the just-picked value to checkUpdatesNow(channel) so
the immediate re-check can't race the debounced autosave PUT and answer for the
previous channel (verified live: body channel wins over stale saved setting).
SHOULD-FIX:
- copy: 'soaked' -> 'soak-tested'; append 'Applies to WebUI updates only.' to the
channel helper text (all 15 locales, native translations)
- mobile: .settings-version-badge gets max-width/overflow/ellipsis in the <=768px
query so the '· Experimental' suffix can't overflow a 320px viewport
(#5 badge-suffix i18n key deferred — non-blocking fast-follow per Fable, matches
the existing English-only version badge on master.)
- test_update_stash_recovery: add merge-base handler for the force-update
rewind guard probe (origin/master is a descendant of HEAD -> not a rewind)
- test_security_review_fixes: fake_check accepts channel kwarg
- i18n.js: native translations of the 4 update-channel keys for all 14
non-English locales (locale-coverage tests enforce key presence)
Add an update_channel setting (stable|experimental) that selects which git-tag
stream the self-updater tracks on the single linear master line:
stable -> 'v*' promoted, soaked releases (default; unchanged glob)
experimental -> 'exp-v*' every release batch (opt-in testers)
A channel is only a tag glob — no branches, no divergence — so every ff-only
guarantee (#2653/#2846/#3140) is preserved. Channel governs the WebUI repo only;
the Agent repo keeps its historical branch fall-through.
Correctness (advisor-reviewed, Codex + Fable):
- describe uses --match <glob> so a commit tagged both v* and exp-v* resolves
to the channel-correct tag
- stable never falls through to origin/master when HEAD contains the latest
stable tag (the firehose-suppression that makes channels work)
- apply_force_update refuses to reset --hard onto an ancestor ref (rewind guard)
- update cache + in-progress guard keyed by (channel, include_agent)
- channel display badge is a SEPARATE field; WEBUI_VERSION stays channel-neutral
(asset cache-busting / SW cache / stale-client skew all do exact-string equality)
Settings UX: channel dropdown with risk-forward copy + the ff-only asymmetry
helper ('switching back to Stable keeps your current version until Stable catches
up'), channel chip on the version badge, i18n keys, re-check on switch.
Tests: new tests/test_update_channels.py (12 real-git-fixture tests) + existing
update suite updated for the channel-aware signatures.