Address the RED gate cert on PR #5688 (2 BRICK + 1 CORE) and the
incoming Greptile review (1 P1 + 3 P2):
- BRICK-1 (race-safety): drop the mtime heuristic and any age-based
lock removal. Add /api/updates/clear_lock with a fail-closed
fcntl.flock holder probe (refuses if any process still holds the
file, on POSIX). On non-POSIX, fails closed. Touches only
.git/index.lock (the only well-known short-lived lock git creates).
- BRICK-2 (destructive path coupling): remove lock_conflict from the
force-button condition. Add a separate btnClearUpdateLock that calls
/api/updates/clear_lock -- never apply_force_update. The recovery
path never runs checkout/clean/reset --hard.
- CORE-1 (unconditional pre-cleanup): apply_force_update no longer
iterates .git/**/*.lock. Lock cleanup is exclusively the clear_lock
endpoint's job.
- Greptile P1: when a pull-lock error fires after a stash was pushed,
run git stash pop (with apply+drop fallback) so the user's local
modifications aren't stranded in the stash silently. v2 tests
cover the stashed and the clean cases.
- Greptile P2 (broad 'lock file' substring): tightened _GIT_LOCK_SIGNATURES
to specific git error strings ('index.lock': file exists, '.lock':
file exists, 'another git process seems to be running',
'unable to create .git/index.lock') so unrelated ref-transaction
lock-loss messages no longer trigger a lock-conflict response.
- Greptile P2 (redundant git_dir.exists / magic number): both gone as a
side effect of removing the apply_force_update cleanup loop entirely.
Refactor: rebased onto current upstream/master so PR is no longer
'behind' 130 commits.
Tests:
- 14 new v2 tests covering holder probe (true/false/NOGIL probe),
_try_remove_lock (refuse-on-held/success-on-unheld),
apply_clear_lock (success/refused), signature set parameter table
including false-positive class, apply_force_update no-touch
contract, pull-lock stash restore, pull-lock no-stash-when-clean.
- 2 v1 tests removed (they encoded the unsafe behavior).
- 2 v1 parametrize rows dropped (their positive cases relied on the
broad 'lock file' substring).
Closes#5687
- Detect lock file errors (lock_conflict flag) on fetch/status/pull
failures in _apply_update_inner (#1)
- Expose Force Update button on frontend when lock_conflict is
received (#3)
- Proactively clean stale lock files (>30s old) in apply_force_update
using rglob discovery instead of a hardcoded path list (#2)
- Add 10 tests: _is_git_lock_error parametrized unit tests,
_apply_update_inner lock path coverage, and apply_force_update
stale/recent lock behavior
The responsive font-size overrides for .msg-time were applied globally,
affecting both the role header timestamp (which is only a hover title
tooltip there) AND the message footer timestamp (.msg-foot .msg-time).
Scope to .msg-role .msg-time so the footer row retains its balanced
10.5px base while the role header timestamp scales with font-size.
Remove min-height:200px from the global .mermaid-viewer-viewport rule
(it was outside the @media(max-width:640px) block despite the PR title).
Add a properly-scoped .mermaid-viewer--inline .mermaid-viewer-viewport
min-height inside an actual @media(max-width:640px) block, capped at
min(200px, 70vh) so it cannot override the lightbox fit-to-screen envelope.
Reworks the #5552 preset builder per maintainer UX feedback + Fable design advisory: one natural 'Schedule: [preset] on [day] at [time]' row; native time picker (themed) replacing hour/minute number boxes; day-of-week + day-of-month dropdowns; raw cron field shown only on Custom; live cron preview; Custom last; new jobs default to Daily 09:00; keystroke-clamp + mid-typing-hide bugs fixed. New i18n keys across all 14 locales. Full gate: Codex SAFE, Fable SHIP-UX, suite 12160/0, browser clean.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Re-adds the per-word prose fade for newly-streamed assistant text in Transparent Stream WITHOUT reintroducing the row-level entrance flicker (#5367). Reduced-motion honored at both JS and CSS layers; thinking/tool rows not animated. Option A: the fade is part of Transparent Stream regardless of the off-by-default 'Fade text effect' toggle; settings description clarified accordingly. Gate: Codex SAFE, Fable SHIP-UX, suite 12158/0, browser-smoke clean.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Generalizes renderModelDropdown() to serve both the composer and Preferences pickers (backward-compatible defaults keep the composer identical). Folded in Fable UX fixes: label->chip association, downward shadow for the down-opening settings picker, and mobile keyboard-pop suppression across all 3 render paths (open/internal-filter/refresh) with focus preserved while typing. Full gate (4 rounds): Codex SAFE, Fable SHIP-UX, suite 12153/0, browser clean; composer picker verified unchanged.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Adds 'Copy relative path' to the workspace file-tree context menu + preview header. Folded in gate fixes: preview-header button folds to icon-only on a narrow pane via @container rightpanel (not viewport @media), with data-i18n-title/aria-label so the tooltip+accessible name localize. Full gate: Codex SAFE, Fable SHIP-UX, suite 12147/0, browser clean.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Folds in gate fixes: map the stale-card 409 to a neutral info toast + retire the card (finally-guard so it stays retired), + consumed-state CSS. Full gate: Codex SAFE, Fable SHIP-UX, suite 12140/0, browser-smoke clean.
Co-authored-by: Frank Song <franksong2702@gmail.com>
Two linked causes of the mobile scroll jump-back, both confirmed by the
maintainer against origin/master in #5637.
Primary (CSS): the @media (pointer: coarse) block applied a flat
`content-visibility: auto; contain-intrinsic-size: auto 1px` to every .msg-row.
Off-screen assistant rows can be multi-thousand px (tool-result / long answers),
so a 1px reservation collapses their off-screen height; scrollHeight lurches down
by tens of thousands of px on a long transcript, the browser force-clamps
scrollTop, and the viewport is thrown to the top. A flat intrinsic-size cannot
both reserve tall rows and keep scrollHeight stable for iOS flick momentum (why
1px was chosen), so content-visibility is scoped to the short, size-predictable
user rows; assistant rows keep real rendered height. Mirrors the existing
#liveAssistantTurn opt-out pattern.
Secondary (JS): _restoreMessageScrollSnapshotSameFrame fell back to an absolute
snapshot.top when the semantic anchor restore failed. During streaming the live
activity-scene refresh fires this every tick; for a reader scrolled up into
history (unpinned), snapping to a stale absolute top nudges the viewport backward
by an amount that grows with scrollHeight. Now: hold position (no scrollTop write)
for the unpinned/anchor-failed case and let the browser's own scroll anchoring
keep the reader put. Pinned/near-bottom readers keep the tail-relative restore.
Adds tests/test_issue5637_mobile_scroll_clamp_jumpback.py (mutation-checked:
reverting either fix fails the matching assertion).
_compensateScrollForMeasurementDelta re-found its viewport anchor only by
rawIdx and bailed with a bare 'if(!row) return' when that row was recycled
out of the render window. On long sessions containing multi-thousand-px
turns, a large scroll delta recycles the anchor row, so the estimated->measured
topPad swap (a scrollHeight change of tens of thousands of px) hit scrollTop
uncompensated and threw the viewport to the top -- a mobile scroll jump-back
that did not reproduce on desktop (where the anchor row stays rendered).
Fall back when the rawIdx row is gone: (1) re-find the same row by its stable
data-session-msg-idx and compensate by its measured offset delta; (2) if that
row is also unrendered, compensate by the top-spacer (topPad) height delta
captured before the re-render. _captureMessageViewportAnchor now records
topPadBefore so the fallback has a reference.
Adds tests/test_issue4346_vscroll_recycled_anchor_jumpback.py (Node-harness
behavioral tests). All fail on the pre-fix bare-return and pass on the fix;
existing vscroll suites stay green.
Relates to #4346.
fix(ui): scale model name, icon, and TPS badge with font-size preference
The .msg-role (model name/icon header), .role-icon, .msg-role-name,
.msg-tps-inline, and .msg-time elements did not respond to the
data-font-size attribute on :root, causing them to appear
disproportionately small when users selected larger text sizes.
Add responsive CSS rules for small/large/xlarge font-size modes so
the chat role header scales proportionally alongside .msg-body text.
Also replaces the hardcoded inline style="font-size:12px" on the
assistant name span with a class-based rule (.msg-role-name) so the
responsive overrides can take effect.
Closes#5372
Co-Authored-By: Claude <noreply@anthropic.com>
@
The success branch had two adjacent calls:
if(typeof window._applyVoiceModePref==='function') window._applyVoiceModePref(); // pre-divider (the actual fix)
_applyComposerFooterVisibilitySettings();
if(typeof window._applyVoiceModePref==='function') window._applyVoiceModePref(); // post-divider (redundant)
The post-divider one ran after _applyComposerFooterVisibilitySettings,
so it couldn't affect the divider the PR was meant to fix. It looked
like a defensive double-tap, but the function is idempotent and the
pref source (localStorage) doesn't change between the two synchronous
calls — so it was just dead code that made the diff look bigger than
the actual semantic change.
Drop it. Net PR footprint is now strictly: 'use the window-qualified
name and apply once before divider recompute in both IIFE branches.'
Re-tested: 26/26 pass on test_issue4598_composer_control_visibility
+ test_issue1488_composer_voice_buttons.
Greptile P1: _applyVoiceModePref is declared with 'function' inside the
voice-mode IIFE (line 1205) and exported to window._applyVoiceModePref
at line 1212. The bare identifier _applyVoiceModePref is closure-local
and not visible from the settings-load IIFE branches at line 2997 and
line 3056 — so `typeof _applyVoiceModePref==='function'` was always
'undefined', the guard was permanently false, and the pre-divider call
silently never fired.
This undid the entire boot-order fix: divider was still computed
against the pre-sync #btnVoiceMode visibility, so the round-2 SILENT
race the PR was meant to close remained.
Fix: use window._applyVoiceModePref in both branches, matching the
already-correct call on line 2999 (which worked only by accident —
it ran AFTER _applyComposerFooterVisibilitySettings so it had no
effect on the bug, but at least it ran).
Tests: pytest tests/test_issue4598_composer_control_visibility.py
tests/test_issue1488_composer_voice_buttons.py — 26/26 pass.
Clean rebase of silent-reader-cn's #5499 (rebase-first; kept both boot-order
_applyVoiceModePref-before-recompute AND the voice-mode onchange recompute + autosave).
Co-authored-by: silent-reader-cn <silent-reader-cn@users.noreply.github.com>
(cherry picked from commit eeea3868d3b7f069dbf61470d787541b95af9181)
Codex gate found two SILENT defects, fixed:
1) Failed-steer RETRY re-uploaded the same File objects (uploadPendingFiles ran
again with clearPending:false). Added _steerUploadCache keyed by ownerSid +
file signature so a retry with the same staged set reuses the uploaded paths;
invalidated when the staged set changes (signature miss) or on accepted steer.
2) Accepted steer cleared ALL S.pendingFiles, dropping files staged during the
upload/API await. Now removes ONLY the delivered snapshot by object identity,
preserving newly staged files.
(The owner-session draft is still cleared on accepted steer — clearing the
delivered session's draft is correct even after a mid-upload switch; the existing
test asserts this.) Tests updated for the identity-based removal + new
cache/dedup guard. 69 steer/composer tests pass, node --check clean.
Steering (submitting mid-stream) dropped any pending composer attachments. Now the
pending files are uploaded before the /steer payload is sent, the uploaded file
paths are appended to the steer text so the active agent can inspect them with
tools, file chips stay staged if the steer fails (so nothing is lost), and
file-only busy submissions are handled.
Contributor stage; gate-pass. Co-authored-by: ruizanthony <ruizanthony@users.noreply.github.com>
On the mobile sidebar header the close (X) button rendered an 18px glyph — the
only 18px .panel-head-btn icon — sitting ~6px lower than the adjacent 14px
new-conversation (+) button, an inconsistent pair. Match the X glyph to 14px and
vertically center it on the ~41px panel-head row so both icons share one size and
baseline (top:calc(safe-top - 2px) keeps the existing safe-area offset so the X
still clears the notch, with no extra drawer spacing). 44x44 tap target kept for
mobile touch; mobile @media(max-width:640px) only, and the X is display:none on
desktop, so desktop is unaffected. + CHANGELOG + source-guard tests.
Two iOS-PWA bugs when tapping an old/large session:
1) closeMobileSidebar() ran AFTER the slow `await _openSidebarSession()` (3-15s
for large sessions on WKWebView), so the sidebar stayed open with only a tiny
spinner. Moved closeMobileSidebar() to run synchronously BEFORE the await in
the tap handler + lineage/child open paths for instant feedback.
2) _loadingSessionId race: an SSE-triggered idle reload
(_scheduleActiveSessionIdleReload / refreshActiveSessionIfExternallyUpdated,
fed by idle-reconcile/poll/visibility/focus) could overwrite _loadingSessionId
and silently cancel an in-flight session switch. Both paths now skip while a
different session's loadSession() is in flight.
Contributor stage; gate-pass. Co-authored-by: luperrypf <luperrypf@users.noreply.github.com>
Codex re-gate SILENT: a retry whose fetch is invalidated mid-flight (e.g. a
profile switch) left the button stuck as an inert 'Retrying…' with no request in
flight — the stale catch returns before _showSessionListLoadError and the
.finally() bails when the skeleton removed the old button. _invalidateSessionListRenders()
now clears the pending retry markers (retrying, _retryFailedFocus) so the next
repaint shows an actionable idle Retry. Regression test added (fails without the
fix); test harness extracts _invalidateSessionListRenders + declares its render-gen
globals.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
Fable UX gate SHIP-WITH-UX-FIXES (non-blocking). Applied all: (1) role=status +
aria-live=polite on the error note so retry failure/restore is announced to
screen readers; (2) pending state uses aria-disabled (not the disabled property)
so the button retains keyboard focus, with click/keydown guards keeping it inert;
(3) on a failed retry, the fresh rebuilt Retry button reclaims keyboard focus
(_retryFailedFocus flag, rAF-guarded) so keyboard users aren't dropped to <body>;
(4) label uses a true ellipsis 'Retrying…' matching the app's other progress
string. CSS styles [aria-disabled=true] alongside [aria-busy]. Tests updated +
new a11y assertions; 8/8 pass, node --check clean.
Co-authored-by: rodboev <rodboev@users.noreply.github.com>