Commit graph

2422 commits

Author SHA1 Message Date
nesquena-hermes
c77f4cf014 release #5516: composer-grow re-pin — hot-path optimization + CHANGELOG
Addresses Greptile P2 nit: autoResize() now only calls the re-pin when the
composer actually grew (offsetHeight before/after compare), skipping the DOM
read on a no-height-change keystroke. The #composerWrap ResizeObserver remains
the safety net for growth paths that bypass autoResize. Test tightened to assert
the grow-guard. CHANGELOG entry added (self-built, release owns changelog).
2026-07-04 06:06:05 +00:00
nesquena-hermes
7e7e71f695 fix(chat): widen composer re-pin observer to #composerWrap (Fable #5516 finding)
Fable senior review (COMMENT, no blockers) + DRY nit:

- Widen the ResizeObserver from #msg to the whole #composerWrap so growth from the
  attachment tray / selection chips (not just textarea rows) also re-pins a bottom-
  pinned reader. Fable flagged tray/chip growth as a likely REMAINING #5515 trigger
  — same viewport-shrink physics, now covered by the same guarded re-pin at one seam.
  #composerWrap contains #msg, so the textarea path is still covered.
- DRY: _repinMessagesAfterComposerResize() now reuses _messageBottomDistance()
  instead of re-implementing the bottom-distance expression inline.

Re-verified live (composer grow -> bottomDist stays 0) + 524 scroll/composer
regression tests green + 6 issue tests green.
2026-07-04 05:52:14 +00:00
nesquena-hermes
dde008748e fix(chat): re-pin transcript to bottom when the composer grows (#5514, #5515)
#5514 (deterministic): with the chat pinned to the bottom, growing the composer
past one row (multi-line typing, Shift+Enter, a multi-line / WisprFlow paste)
shrinks the flex:1 .messages viewport by the same delta. scrollTop is unchanged
but max-scrollTop rose by delta, so the reader is stranded delta-px above the
bottom — the transcript 'scrolls up one row per composer row.' autoResize()
resized the textarea and called updateSendBtn() but never re-pinned the transcript.
Reproduced live: composer 176px -> viewport shrank 745->613 -> transcript stranded
132px above bottom while _scrollPinned stayed true.

#5515 (intermittent 'random scroll up'): the same viewport-shrink from composer
growth via paths that don't route through input->autoResize (paste, draft restore,
programmatic value set, reflow) reads to the user as a random upward jump. Covered
by a ResizeObserver seam (below).

Fix: new _repinMessagesAfterComposerResize() (static/ui.js) re-pins to the bottom
ONLY when the reader is genuinely still pinned (honors _messageUserUnpinned /
_scrollPinned — never yanks a reader who scrolled away; verified live). Called from:
 (a) autoResize() after the height write, and
 (b) a ResizeObserver on the #msg textarea that re-pins on GROW only, catching
     every composer height-change path (not just the typed-input seam).

Verified live on a 40-message seeded session: pinned reader stays at bottomDist=0
after a 4-line composer grow; an unpinned (scrolled-up) reader is NOT moved.

Tests: tests/test_issue5514_composer_grow_scroll_pin.py (3 static wiring + 3 node-vm
behavioral). 524 scroll/composer regression tests across 39 files still green.

NOTE on #5515: this resolves the composer-growth cause (which the reporter's
WisprFlow-paste symptom points at). If a purely non-composer intermittent jump
persists, the remaining suspect is touch-device overflow-anchor:auto on content-
above height change — that path has no repro yet and is tracked as needinfo on
#5515.
2026-07-04 05:41:33 +00:00
nesquena-hermes
fcd3de7d8f Merge PR #5487 from webtecnica: prevent unintended session switch on stray pointerup (#5462) 2026-07-04 03:39:18 +00:00
nesquena-hermes
dbf3c9d3c0 Merge PR #5460 from ruizanthony: replace cross-profile empty sessions on profile switch 2026-07-04 03:05:45 +00:00
nesquena-hermes
091103cf6d Merge PR #5496 from webtecnica: refresh recognition.lang before dictation start (#5483) 2026-07-04 02:41:01 +00:00
root
fe4e2851f7 fix: refresh recognition.lang before dictation start
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
2026-07-03 22:28:35 -03:00
nesquena-hermes
568d5c4b0e gate-fix #5480: suppress 'unknown' server version (case-insensitive) to prevent false banner
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.
2026-07-04 01:15:26 +00:00
nesquena-hermes
b04f0387c4 Merge PR #5480 from rodboev: surface stale WebUI client recovery (#5428) 2026-07-04 01:09:14 +00:00
nesquena-hermes
0eb80f6197 Merge PR #5452 from rodboev: fit fullscreen Mermaid diagrams to the lightbox (#5413) 2026-07-04 00:07:04 +00:00
nesquena-hermes
ed508ff6ef Merge remote-tracking branch 'origin/master' into release/stage-5467
# Conflicts:
#	CHANGELOG.md
2026-07-03 23:39:31 +00:00
nesquena-hermes
5c54fd598c
fix(#5472): order draft re-persist after clear + honest happy-path test (#5488)
* 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.
2026-07-03 16:36:21 -07:00
nesquena-hermes
329e911327 fix: prevent unintended session switch on stray pointerup (#5462)
Clean rebase of webtecnica's #5487 (rebase-first).

Co-authored-by: webtecnica <webtecnica@users.noreply.github.com>
2026-07-03 23:34:37 +00:00
nesquena-hermes
b438976507 gate-fix #5467 r2: force render when recovering from skeleton/error-banner DOM
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.)
2026-07-03 23:04:41 +00:00
nesquena-hermes
3999418b0e gate-fix #5467: serialize full rows in render-skip signature (close false-skips)
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.
2026-07-03 22:58:21 +00:00
nesquena-hermes
603375d71c
fix(#5472): preserve composer draft when a send fails (#5484)
* 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>
2026-07-03 15:57:33 -07:00
nesquena-hermes
10e5e941d3 Merge PR #5467 from ai-ag2026: skip full session-list rebuild when payload unchanged (#5455) 2026-07-03 22:47:40 +00:00
nesquena-hermes
8db6ec1536 Merge remote-tracking branch 'origin/master' into release/stage-5469 2026-07-03 22:32:54 +00:00
nesquena-hermes
e81711d3a9 Merge PR #5469 from allenliang2022: recover viewport anchor via sessionIdx (desktop scroll jump-back) 2026-07-03 22:18:17 +00:00
nesquena-hermes
5cac66b4e5 Merge PR #5438 from rodboev: add preset cron schedule builder (#5427) 2026-07-03 22:16:37 +00:00
Rod Boev
f32938021c fix(#5428): surface stale WebUI client recovery 2026-07-03 17:43:46 -04:00
nesquena-hermes
eaf7d1e9ab gate-fix #5466: stamp anchor attrs on incremental node + teardown on all terminal paths
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.
2026-07-03 21:24:08 +00:00
nesquena-hermes
819f1a1e7b Merge PR #5466 from ai-ag2026: reduce O(n^2) streaming render to O(n) (#5455) 2026-07-03 21:04:16 +00:00
nesquena-hermes
5e5b44e733 fix(webui): replace cross-profile empty sessions on profile switch + open all-profile rows under owning profile
Clean rebase of ruizanthony's #5460 (rebase-first).

Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
2026-07-03 20:49:51 +00:00
nesquena-hermes
8fe2d63ec0 Merge remote-tracking branch 'origin/master' into release/stage-p1c
# Conflicts:
#	CHANGELOG.md
2026-07-03 20:44:31 +00:00
nesquena-hermes
6edcf14bbc Merge PR #5465 from ai-ag2026: pause background session polling while tab hidden (#5455) 2026-07-03 20:16:35 +00:00
nesquena-hermes
8120e69e1d Merge PR #5442 from rodboev: persist TTS and voice preferences server-side (#5435) 2026-07-03 20:04:55 +00:00
nesquena-hermes
00e7203ec8 Merge PR #5449 from rodboev: hide read-only cron fork affordances (#5439) 2026-07-03 19:44:33 +00:00
nesquena-hermes
15fbcb4ce0 fix(chat): recover viewport anchor via sessionIdx when content-derived key goes stale
Clean rebase of allenliang2022's #5469 (rebase-first).

Co-authored-by: allenliang2022 <allenliang2022@users.noreply.github.com>
2026-07-03 19:44:32 +00:00
TARS
3f4ebf18a8 fix(sidebar): refresh timestamp labels on tab return
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.
2026-07-03 20:13:24 +02:00
TARS
5973d2b0e0 fix: narrow sidebar render-skip signature 2026-07-03 20:10:44 +02:00
TARS
eedbf8b05f fix: clear stream perf cleanup state 2026-07-03 20:10:40 +02:00
Rod Boev
af3eb88225 Drop redundant live-row suppression after removing the animation source 2026-07-03 13:48:20 -04:00
TARS
fcf3e23643 perf(sidebar): pause background session polling while the tab is hidden (#5455)
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>
2026-07-03 18:41:49 +02:00
TARS
a53f3bdaba perf(sidebar): skip full session-list rebuild when the payload is unchanged (#5455)
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>
2026-07-03 18:41:49 +02:00
TARS
ed922f4f18 perf(stream): throttle live-turn snapshot and skip redundant per-token parse (#5455)
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>
2026-07-03 18:41:49 +02:00
TARS
304a82d9b4 perf(stream): render anchor-scene live prose incrementally via smd (#5455)
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>
2026-07-03 18:41:49 +02:00
Rod Boev
8c82ed8f11 fix(#5435): read voice silence timeout at use time 2026-07-03 08:57:44 -04:00
Rod Boev
7a21825722 fix(#5435): keep speech settings payload sparse 2026-07-03 00:22:39 -04:00
Rod Boev
904d741ed2 fix(#5367): suppress transparent stream live row animation 2026-07-02 23:31:57 -04:00
Rod Boev
1127be609c fix(#5413): smooth Mermaid lightbox resize behavior 2026-07-02 22:19:13 -04:00
Rod Boev
2034d6b51c fix(#5413): fit fullscreen Mermaid diagrams to the lightbox 2026-07-02 21:28:40 -04:00
nesquena-hermes
0735a1b853 fix(#5439): hide read-only cron fork affordances
Clean rebase of rodboev's #5449 (rebase-first).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-03 00:58:30 +00:00
Rod Boev
3b344b1d9a fix(#5435): preserve local speech prefs until settings.json owns them 2026-07-02 19:33:13 -04:00
nesquena-hermes
a6504a8c58 fix(#5434): scope new Mermaid sizing to inline only — restore master lightbox behavior + inline Fit button (Codex gate findings)
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.
2026-07-02 22:37:12 +00:00
nesquena-hermes
a4ad03bd68 fix(#5413): restore readable Mermaid sizing (rebased onto current master w/ #5437)
Clean rebase of rodboev's #5434 (rebase-first, re-based post-#5437).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-02 21:56:29 +00:00
nesquena-hermes
dc7e9e34d2 Merge #5441: cap generated update summary cache (#5435) [rodboev] 2026-07-02 21:14:03 +00:00
Rod Boev
ca31bf969b fix(#5435): keep saved default speech prefs authoritative 2026-07-02 16:49:45 -04:00
nesquena-hermes
e65013676a fix(#5435): cap generated update summary cache
Clean rebase of rodboev's #5441 (rebase-first).

Co-authored-by: rodboev <rodboev@users.noreply.github.com>
2026-07-02 20:46:33 +00:00
nesquena-hermes
01487009d6 fix(#5408): add missing German providers_status_model locale key; correct CHANGELOG to note Test-connection reuses the authenticated onboarding probe (Codex gate findings) 2026-07-02 20:37:15 +00:00