Commit graph

1722 commits

Author SHA1 Message Date
Nathan Esquenazi
266d9cdbd3 fix(sse): guard gateway reopen on focus so transient blur can't thrash it (#4151)
The #4151 focus hook reopened both global sidebar streams on `focus` with a
bare `startGatewaySSE()`. But `startGatewaySSE()` is NOT idempotent — it begins
with an unconditional `stopGatewaySSE()` (sessions.js:3778), which closes the
live gateway EventSource, cancels its poll fallback (`stopGatewayPollFallback`),
and resets `_gatewayProbeInFlight` / `_gatewaySSEWarningShown`.

So on a transient blur shorter than the 1s debounce — where the blur-close
timer is cleared on the following focus and the stream was never torn down —
the focus handler still dropped and reconnected the gateway on every
window-switch, cancelling its fallback and resetting probe/warning state. That
is the exact thrash the debounce exists to prevent, in the multi-window PWA
scenario this PR targets. (`ensureSessionEventsSSE()` is already idempotent via
`if(_sessionEventsSSE) return`, so the session-events stream was unaffected;
only the gateway churned. greptile P1.)

Guard the reopen on the gateway actually being closed: `if(!_gatewaySSE)
startGatewaySSE()`, mirroring the session-events idempotency. A sustained blur
(timer fires → both streams closed) still reopens the gateway on focus; a
transient blur leaves the still-open gateway untouched. Verified with a
blur/focus/timer harness (transient → no teardown/reconnect; sustained → close
then reopen; CLI-disabled → stays closed) and a new regression guard
`test_focus_reopen_does_not_thrash_the_gateway_stream`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 18:07:12 -07:00
nesquena-hermes
408dbeb4f0 fix(sse): close global sidebar streams on PWA window blur (#4151)
Two side-by-side PWA standalone windows saturate the browser's per-origin
HTTP/1.1 connection limit, causing 'Request timed out' toasts to cycle every
~30s. The idle-SSE close added in #3992/#3996 keys off the Page Visibility API
(visibilitychange / document.hidden), but a PWA standalone window does not
reliably fire visibilitychange when it loses focus to another window of the
same app -- document.hidden only flips on minimize. So both windows stay
'visible', each holds its two global sidebar SSE streams (session-events +
gateway) open, 2x3 = 6 hits the connection cap, and subsequent fetch() calls
(the 30s background polls) queue behind the saturated pool and time out.

The two global sidebar streams now also close on a sustained window blur (gated
on document.hasFocus(), the signal visibilitychange misses) and reopen --
catching up the session list -- on focus, via a shared _sidebarSseBackgrounded()
predicate and a debounced _installSidebarSseFocusHook().

The per-session live stream (messages.js startSessionStream) is intentionally
left visibility-only: it carries live bg_task_complete toasts and
server_turn_started live-view that an unfocused-but-visible window must still
receive (the OS-notification path is gated on document.hidden, so the in-app
toast is the only completion signal a visible-unfocused window gets). Closing
it on blur would regress the multi-window live-view UX.

Reported by @ddiall with a thorough root-cause analysis.

Co-authored-by: ddiall <ddiall@users.noreply.github.com>
2026-06-14 00:54:28 +00:00
nesquena-hermes
e044a2e938 Merge #4143 (focus existing chat tab on notification click, #4109) onto master 2026-06-14 00:25:09 +00:00
nesquena-hermes
5812df5f7b Merge #4112 (skill bundles in WebUI slash commands, #4087) onto master 2026-06-13 23:13:28 +00:00
Rod Boev
d7e8f70423 fix(commands): block transient bundle shadowing 2026-06-13 18:53:46 -04:00
nesquena-hermes
4f1ce2661e Merge #3998 v2 (imported-session titles + reload-before-save fix, #3987) onto master 2026-06-13 21:46:36 +00:00
Matt Van Horn
c3f4d33392
fix: focus the existing chat tab when a notification is clicked instead of opening a new tab
Fixes #4109
2026-06-13 14:42:41 -07:00
nesquena-hermes
f7047d4d19 Merge #4072 (wire /credits through WebUI command dispatch, #4071) onto master 2026-06-13 21:15:22 +00:00
ai-ag2026
cea9d42fcc fix: extend timeout for full history session loads
(cherry picked from commit 27cbff974eeaa58780d91fb4f25802cf0ac76381)
2026-06-13 22:37:19 +02:00
nesquena-hermes
5a33a51cec Merge #4131 (badge pending prompts in document title, #1694) onto master
# Conflicts:
#	CHANGELOG.md
2026-06-13 19:19:24 +00:00
Rod Boev
e5640a42b9 Keep prompt-attention title badges from breaking title ownership 2026-06-13 15:05:03 -04:00
Rod Boev
4329768288 feat(topbar): badge pending prompts in document title (#4121) 2026-06-13 15:04:20 -04:00
nesquena-hermes
45b88b55b7 Merge #4127 (run /yolo immediately on busy send path) onto master 2026-06-13 19:04:13 +00:00
nesquena-hermes
540af21444 Merge #4130 (hide unsupported max reasoning effort from WebUI) onto master 2026-06-13 18:43:11 +00:00
Rod Boev
c7c313672c fix(reasoning): hide unsupported max effort (#4119) 2026-06-13 14:33:26 -04:00
nesquena-hermes
acac104362 Merge #4126 (anchor renderer snapshot adapter, slice 8, inert) onto master
# Conflicts:
#	CHANGELOG.md
2026-06-13 18:20:36 +00:00
Rod Boev
707a19e34c Keep busy-slash regressions aligned with /yolo support 2026-06-13 14:15:35 -04:00
Rod Boev
6c7d551b41 fix(commands): keep slash command ownership ahead of bundles (#4087) 2026-06-13 14:06:35 -04:00
Rod Boev
5a3c6c6f05 fix(commands): surface skill bundles in WebUI slash commands (#4087) 2026-06-13 14:06:35 -04:00
Frank Song
409fb59760 Add assistant turn anchor renderer snapshot adapter 2026-06-14 02:03:45 +08:00
Rod Boev
61ddf4d21e fix(commands): run /yolo from busy slash intercept (#4117) 2026-06-13 13:59:04 -04:00
Frank Song
0fdcd00858 Fix PWA final answer scroll refresh 2026-06-14 01:46:04 +08:00
Frank Song
c52c832d05 Add assistant turn anchor dual-run reconciler 2026-06-14 01:04:29 +08:00
nesquena-hermes
310edac58e fix(#4108): add creation-time backstop cleanup to bound the registry-leak class
Codex found leak paths across 3 rounds (settled-restore, stream-end-fallback, reconnect-
preflight-inactive, stale-bailout, non-current-pane error, external cancelSessionStream).
Rather than chase every current+future teardown route, schedule an identity-guarded cleanup
at registry CREATION (600s) so window._liveAnchorRegistries self-expires no matter how the
stream ends — including external paths (boot.js cancelSessionStream) that bypass the SSE
handlers. Explicit terminal-path calls just expire it sooner. Shadow-only + identity-guarded,
so a rare >10min active stream only loses reconnect-continuity of an unconsumed registry.
2026-06-13 16:19:18 +00:00
nesquena-hermes
3cf96b1293 fix(#4108): cleanup anchor registry on reconnect-preflight-inactive + stale-stream-bailout paths (Codex round 2)
Two more early-return paths created the registry (early in attachLiveStream) then
returned without scheduling cleanup: (1) reconnect preflight when /api/chat/stream/status
is inactive/no-replay (~3966); (2) _bailOutOfTerminalEventsFromStaleStream (~1490) when a
stale stream's terminal event arrives after a newer stream owns the session. Both now
schedule identity-guarded cleanup (map.get(streamId)===registry, keyed on the stale
closure's own streamId so the newer registry is untouched).
2026-06-13 16:13:55 +00:00
nesquena-hermes
9594b97223 fix(#4108): schedule anchor registry cleanup on settled-restore + stream-end-fallback terminal paths (Codex leak catch)
Codex found window._liveAnchorRegistries retained registries for streams ending via
_restoreSettledSession() and _finalizeStreamEndFallback() (both terminal-close without
cleanup). Added _flushReasoningToAnchor()+_scheduleAnchorRegistryCleanup() to both —
identity-guarded delete already prevents clobbering a newer registry. All anchor helpers
are nested in attachLiveStream's closure (hoisted), ESLint runtime gate clean.
2026-06-13 16:09:20 +00:00
Rod Boev
59562e416a fix(#4071): wire /credits through WebUI command dispatch 2026-06-13 07:50:16 -04:00
Frank Song
aa96bc52bc Add assistant turn anchor live shadow feed 2026-06-13 16:26:52 +08:00
nesquena-hermes
f61f88f16b Merge #4016 (re-arm browser voice mode when speechSynthesis drops, #3983) onto master 2026-06-13 07:37:31 +00:00
nesquena-hermes
5bbddbad1e fix(#3900): false streaming + activity-timer reset on session switch (absorb #3899) + re-anchor regression tests
Production fixes (Tamaz-sujashvili, reviewed sound by maintainer):
- loadSession clears S.busy/S.activeStreamId as soon as metadata confirms no active_stream_id, before the async message-load gap (idle session no longer shows streaming chrome).
- Snapshots the live turn before wiping msgInner + seeds INFLIGHT, restores on the active-stream return path (timer/trace survive switch-back).

Re-anchored the 2 brittle regression tests per maintainer review: test_..snapshots.. now anchors on the unique 'Loading conversation...' marker (was matching the no-space 'Session not available' error path); test_..restores.. now asserts the LIVE Phase 2a restore (after loadInflightState) instead of the unreachable Phase-2b/1184 branch. CHANGELOG stamped v0.51.384 (MW).
2026-06-13 06:39:22 +00:00
Lynn-Lee
46a5ce1b1a fix tab title clobber during bot name refresh 2026-06-13 14:03:02 +08:00
Frank Song
59c2ccd9cf feat(anchor): project activity scene rows 2026-06-13 13:23:27 +08:00
Frank Song
b990bd09dd feat(anchor): project settled final answer through anchor 2026-06-13 13:23:15 +08:00
nesquena-hermes
e4bdcbfeae Merge #3823 (tool iteration limit terminal state) onto master
# Conflicts:
#	CHANGELOG.md
2026-06-13 05:06:46 +00:00
nesquena-hermes
2bd8b40b15 harden(#4063): stamp disclosure-state snapshot with session id (Opus belt-and-suspenders)
_captureWorklogDetailDisclosureState stamps state._sid; _restoreWorklogDetailDisclosureState
skips restore on sid mismatch. Cross-session isolation no longer depends solely on the
sessions.js #msgInner wipe invariant — a future change to that loading-placeholder
behavior can't silently replay one session's collapse state onto another.
2026-06-13 04:47:36 +00:00
nesquena-hermes
c9930d2a3d Merge #4063 (preserve Worklog detail disclosure across rebuilds) onto master
# Conflicts:
#	CHANGELOG.md
#	static/ui.js
2026-06-13 04:41:17 +00:00
nesquena-hermes
f5fb50983b fix(#4006): defensive !==false at the 2 remaining settings-hydration sites (Opus note)
panels.js _autosaveAppearanceSettings (6309) + _applySavedSettingsUi (7837) read
auto_scroll_follow from a settings/body object — use !==false (default True) for
consistency with boot.js + the checkbox render, so a future partial-body refactor
can't silently default the True setting OFF. The .checked reads (6223/8170) keep !!
(a real bool).
2026-06-13 04:27:01 +00:00
nesquena-hermes
6ced477822 fix(#4006): explicit-settle flag + pre-wipe snapshot capture (Codex r3)
1. _settleMessageScrollToBottom(force, explicit): when explicit (End-button /
   scrollToBottom), late-layout settling runs even with Auto-follow OFF — the
   setting only suppresses AUTOMATIC follow, not a deliberate jump. The RO + 2s
   fallback guards now bail on (!_autoScrollFollow && !explicit). scrollToBottom()
   passes explicit=true; the automatic scrollIfPinned() path stays guarded.
2. renderMessages now captures the pre-wipe scrollSnapshot when (Auto-follow off
   && user unpinned) too, so the non-preserve restore lands the reader where they
   were instead of receiving a null snapshot (no-op).
2026-06-13 04:16:48 +00:00
nesquena-hermes
36239700e6 i18n(#4006): add auto-follow setting label+desc to the remaining 11 locales (Opus note)
The keys shipped only in en+zh; added it/ja/ru/es/de/zh-Hant/pt/ko/fr/tr/pl so the
Settings → Appearance auto-follow toggle isn't English-only outside those two.
2026-06-13 04:12:11 +00:00
nesquena-hermes
2bdef4166f fix(#4006): gate the #3920 RO settle + render fall-through on _autoScrollFollow (Codex cross-PR catch)
Codex found that with Auto-follow OFF the #3920 ResizeObserver settle + 2s fallback
+ the non-preserve render path still force-scrolled to bottom (the setting was
incomplete vs the scroll machinery shipped in #3920). Fixes:
1. RO settle guard (ui.js:3153): bail when !_autoScrollFollow.
2. 2s static fallback guard (ui.js:3186): bail when !_autoScrollFollow.
3. non-preserve render fall-through (ui.js:8653): when Auto-follow off AND the user
   has unpinned, restore the scroll snapshot instead of scrollToBottom() — also
   closes the send() race (renderMessages runs before S.activeStreamId is set).
   A fresh session load (not unpinned) still lands at the bottom. Explicit
   user-initiated scrollToBottom() (End button) is untouched.
2026-06-13 04:09:58 +00:00
nesquena-hermes
c02c31c837 Merge #4006 (scroll auto-follow setting + pin guards) onto master
# Conflicts:
#	api/config.py
#	static/panels.js
2026-06-13 03:54:11 +00:00
nesquena-hermes
b5eabe1494 fix(#3920): apply Codex gate findings — observe #msgInner, instance-owned RO cleanup, restore .messages overflow-anchor
Codex SHIP-ONLY-WITH-FIXES (3 real issues; Codex+Opus disagreed on #1, stricter wins):
1. CORE — .messages lost overflow-anchor:none, reopening the #1360 (d21c9720) streaming-scroll-anchor regression. Restored it (native anchoring stays OFF; the RO does the settle). Opus had judged this removal acceptable; took Codex's stricter call.
2. SILENT — the ResizeObserver observed #messages (the scroll container, fixed by flex layout, never resizes) instead of #msgInner (.messages-inner, the growing transcript node) → callback would never fire. Now observes #msgInner.
3. SILENT — stale RO callbacks mutated the global _settleRO, which could disconnect a newer active observer after settle re-entry. Now instance-owned: close over const ro, only clear the global when _settleRO===ro.
2026-06-13 03:32:16 +00:00
nesquena-hermes
f9055c5d97 fix(#3920): address review — restore sidebar overflow-anchor, wire _settleFinalScroll via 2s fallback, + CHANGELOG v0.51.377
3 maintainer-review items the contributor hadn't addressed:
1. (blocking) restored .session-list overflow-anchor:none — the PR accidentally reverted the deliberate Firefox sidebar fix (2dfe765b); only the on-topic .messages removal is kept.
2/3. _settleFinalScroll was dead code + the promised 2s static-content safety timeout was missing — wired _settleFinalScroll(token) via a single 2s top-level fallback after _settleRO.observe(el), declared _settleFinalTimer + cleared it in _cancelBottomSettle and at settle re-entry.
2026-06-13 03:25:48 +00:00
nesquena-hermes
965baeb78c Merge #3920 (Firefox scroll ResizeObserver) onto master 2026-06-13 03:23:49 +00:00
nesquena-hermes
35877dab1b fix(#3971): move show-thinking gate to the real Worklog render paths (Opus catch)
Opus final gate caught that the staged re-implementation placed the
window._showThinking===false gate in _worklogReasonNodeFromText, which has
ZERO callers — so live/settled reasoning rows (built by _syncWorklogReasonFromAnchor
and _appendWorklogReason) were never actually hidden, and removeThinking swept
data-worklog-reason-source=reasoning which the real rows (data-worklog-anchor-reason=1)
don't carry. The string-match tests passed against the dead function = green-suite blind spot.

Fix:
- Gate _syncWorklogReasonFromAnchor (live + settled): when _showThinking===false, remove any
  existing reasoning row and bail before building.
- Gate _appendWorklogReason (settled rebuild): return null when _showThinking===false.
- removeThinking + the settled-rebuild cleanup now sweep BOTH data-worklog-anchor-reason=1
  (the real attribute) and the legacy data-worklog-reason-source=reasoning.
- Re-anchor the tests to assert against the real render functions + the correct attribute.
2026-06-13 03:09:50 +00:00
nesquena-hermes
52c502a984 Merge #3971 (hide Worklog reasoning when Thinking disabled) onto master 2026-06-13 02:52:31 +00:00
nesquena-hermes
dc909043cf
Merge branch 'master' into franksong2702/stable-assistant-turn-anchors-slice3-registry 2026-06-12 19:41:20 -07:00
nesquena-hermes
c45adb910a fix(#4024): wire dormant recency-fade to live turn + include Interrupted in progress-bar completed set (Opus final-gate observations)
- _applyTransparentRowFading was only called from the settled loop (which the fn skips), so live-turn recency fade never applied — now called from _syncTransparentEventControls (internally live-gated, no-ops on settled).
- _attachProgressBar isCompleted now includes Interrupted so an interrupted row's bar fills 100% instead of rendering empty.
2026-06-13 01:53:11 +00:00
nesquena-hermes
d952dfe3a2 fix(#4024): trifecta round-2 — rehydrate root-turn, always-preserve detail tab, drop redundant Running: name prefix
- C1-r2 (both Codex+Opus): _rehydrateTransparentStreamDom now also rewires the root element when it IS the assistant turn (querySelectorAll only matched descendants), so turn-toggle + event-controls re-bind after a live-turn restore.
- O-Bug2-r2: preserve the Full/Output detail tab regardless of open state.
- r2 #4: drop the 'Running: ' name prefix — the legible status badge already carries it (V6 redundancy class).
2026-06-13 01:16:56 +00:00
nesquena-hermes
ed3ac13fc5 fix(#4024): trifecta UX + correctness pass — rehydrate live-restore, thinking dedup, quiet thinking rows, legible failures, skin-proof reset, compact args, flat output, live-only fade/anim
Functional (Codex+Opus trifecta findings):
- C1: restoreLiveTurnHtmlForSession now calls _rehydrateTransparentStreamDom (controls were dead after active-session live-turn restore).
- O-Bug1: per-turn echoed-thinking dedup in the transparent settled path (mirrors the compact seenReasons), fixes duplicate out-of-order Thinking rows.
- O-Bug2: tool completion preserves the row's open state + Full/Output tab.
- O-Edge: settled/reloaded done===false tools show 'Interrupted' (not a permanent Running shimmer); progress bar clears for Interrupted too.

Visual (trifecta CSS):
- V1: reset .thinking-card to flat/transparent (was the heaviest object in the stream).
- V2: failed=error color + left border, running/interrupted legible (was an invisible muted badge).
- V3: scoped !important reset beats the 6 per-skin .tool-card re-card rules.
- V4: inline key:value args (was a tall stacked form).
- V5/V7: output pre flattened to a quiet left-rail, max-height:none (one expanded zone, no nested scroll).
- V6: drop redundant tool/status pseudo-args from the detail body.
- V8: row fading gated to the live turn only + floor raised for WCAG; settled history stays full-opacity.
- V9: entrance animation scoped to #liveAssistantTurn (was replaying on every renderMessages).
- V10: larger mobile header tap targets + active detail-tab underline.

O-Bug3 (live thinking overwrite when no seq) intentionally deferred — documented follow-up; settled render is already correct and a live heuristic risks a streaming regression.
2026-06-13 01:08:41 +00:00