Rebased Paladin173's #4657 onto current master. The per-control visibility
toggles half already shipped via his own #4598, so this rebase carries only the
net-new JS fit-by-overflow engine (_fitComposerFooter: ResizeObserver +
MutationObserver measure .composer-left overflow, step full -> cf-icons ->
cf-burger). Co-authored-by: Paladin173 <Paladin173@users.noreply.github.com>
The new-conversation splash mark was wrapped in a glossy navy rounded-rect
that reads as a macOS app-launcher tile ('this is an app') rather than a
brand mark. Major AI chat UIs (Claude, ChatGPT, Gemini) all render their
empty-state mark bare. Remove the tile, scale the winged caduceus up
(~88px), and back it with a soft cyan/blue radial 'spirit' bloom.
Light and dark get separately tuned marks: dark keeps the bright
cyan->blue gradient + luminous bloom; light uses a deeper #0AA6CC->#1E63D6
gradient + cooler restrained bloom so the mark holds contrast on the pale
background instead of washing out. Entrance is gated behind
prefers-reduced-motion.
Salvages PR #3976 by ksj5131513-ops, ported fresh onto current master and
re-applied on top of #4195's external-refresh gate (the original PR predated
#3916/#4195's rework of refreshActiveSessionIfExternallyUpdated).
Two parts:
1. Avoid an unconditional same-session force reload the moment streaming
settles. _scheduleActiveSessionIdleReload previously called
loadSession(force:true) on every idle reconcile, which on mobile PWA caused
a visible end-of-turn flash and could briefly restore the pane with stale
layout geometry. It now reconciles against server metadata first via
refreshActiveSessionIfExternallyUpdated('idle-reconcile', {ignoreStreamJustFinished:true})
— reloading ONLY when the message count actually changed — and falls back to
the original forced reload only if the metadata probe itself fails.
This COEXISTS with #4195: the poll-only external-session early-return
(if((reason||'poll')==='poll' && !_isExternalSession(S.session)) return) is
left untouched. The idle path uses a non-'poll' reason so it never trips that
gate, and opts (read via arguments[1], same pattern as loadSession) only
bypasses the post-stream cooldown, not the external gate. The function keeps
its (reason) signature and now returns a status string
('skipped'/'unchanged'/'reloaded'/'failed') for callers that want it.
2. Mobile viewport reflow guard for PWA visualViewport changes. When the
on-screen keyboard / browser chrome shows or hides, the phone layout can stay
painted against stale geometry. _forceMobileViewportReflow() toggles a
one-frame html.viewport-reflow class (cheap GPU-promotion transform) to force
a repaint, then resyncs the workspace panel + sidebar aria. Wired to
visualViewport resize/scroll (debounced) where available, falling back to the
window resize handler otherwise. Adds safe-area-inset padding to topbar,
messages-inner, and composer-wrap on phones. Uses overflow-x:clip (NOT hidden)
on .layout per the #4856 lesson to avoid coercing overflow-y into a scroller.
Co-authored-by: ksj5131513-ops <ksj5131513-ops@users.noreply.github.com>
Surfaces prompt-cache hit rate on the Insights page so cache-efficiency
(a key cost-optimisation signal) is visible at a glance.
Backend (api/routes.py, _handle_insights):
- Aggregate cache_read_tokens per model and per day, plus a total.
- Graceful fallback for older agent state.db schemas lacking the
cache_read_tokens column (COALESCE + OperationalError second query).
- New response fields: total_cache_read_tokens, total_cache_hit_percent,
per-model cache_read_tokens + cache_hit_percent, per-day cache_read_tokens.
Frontend:
- Models table gains a Cache column (panels.js + style.css 6-col grid).
- Daily bar tooltip includes the bounded cache hit rate.
- New i18n keys insights_model_cache / insights_cache_hit in all 13 locales.
Correctness fix (maintainer-flagged on #3912): the original computed
hit-rate as cache_read / input_tokens (reads-over-misses), which EXCEEDS
100% on cache-heavy sessions. This uses the FULL prompt total as the
denominator -- cache_read / (input_tokens + cache_read) -- via the shared
api.usage.prompt_cache_hit_percent helper, so it is bounded 0-100% and
means "% of the prompt served from cache". Returns None (rendered as a
muted dash) when there are no cache reads.
Tests (tests/test_insights.py):
- Updated existing assertions for the new response shape.
- Added a non-vacuous <=100% boundary test: a cache-heavy case
(cache_read=9900, input=100) where the OLD formula reports 9900% but the
bounded denominator yields 99%. Verified the test FAILS under the old
formula and PASSES under the fix.
Salvage note: PR #3912's diff was ~99 releases stale (mostly re-derived old
code, illusory +5405). This ports ONLY the real ~190-line feature fresh
onto current master.
Closes#3911
Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
New brand caduceus (vector mark, cyan #08EBF1->#3889FD) on a radial-depth navy
badge. Favicon family regenerated (svg/ico/png/apple-touch). Titlebar and
empty-state logos now use the self-contained navy badge so they render correctly
on every theme (light + dark) without washing out. Removed 5 per-skin CSS rules
that force-filled the titlebar icon to a flat accent color (they assumed the old
monochrome caduceus).
GitHub (#4634): graphite + Primer-blue. Codex (#4636): minimal editor, sage accent.
Terracotta (#4635): warm clay accent, renamed from the originally-proposed name to a
descriptive material name. All CSS-only, namespaced under [data-skin], opt-in via
Settings > Appearance. Gate: Codex SAFE + full suite 10778 pass + 3 accents verified live.
Co-authored-by: gottipx <gottipx@users.noreply.github.com>
Renders generic registry post_install guidance + lifecycle requirements in the
Settings extensions gallery card (manifest-driven, no vendor special-casing).
Fixes#4959. Rebased onto current master (squash-applied #4964's net diff so it
coexists with the #4979 sidecar-runtime-status feature that landed in v0.51.697).
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Cron run output is never authored Markdown -- it is logs/JSON/plain text.
Using renderMd() with any heuristic or synthetic fence is fragile:
- Content starting with #, |, > becomes headings/tables/blockquotes
- Embedded triple-backtick in content breaks the synthetic fence
Fix: use DOM-created pre/code with textContent throughout:
- No heuristic, no synthetic fence -- every byte renders verbatim
- Both render paths (initial load + View full output) use the same approach
The mobile .messages-inner rule used overflow-x:hidden, which per the CSS
overflow spec coerces overflow-y from visible to auto — turning the inner
into a scroll container. A scroll container's min-height:auto resolves to 0,
so the inner (a flex-shrink:1 item inside the .messages column flexbox)
collapsed to the viewport height instead of growing to its content. The
transcript then overflowed the clipped inner rather than the .messages
scroller, leaving nothing to scroll and pinning scrollTop at 0 — every
open/send/receive stranded mobile users at the top with no way to scroll down.
Fix: overflow-x:clip suppresses horizontal overflow (the original #4583/#4553
intent) WITHOUT coercing overflow-y or creating a scroll container, so
min-height:auto stays min-content and the inner grows to full height.
Verified empirically via headless Chrome mobile emulation (390x844, mobile=true):
scrollableDelta 0 -> 7746; open/send/receive all land at bottom; mid-scroll
position preserved across re-render. Desktop unaffected.
Updated #4553 test (clip satisfies its horizontal-pan goal) + added dedicated
The .extension-gallery-install-btn used color:#fff on a var(--accent) background;
with the gold accent (#FFD700 dark / #B8860B light) the white text was washed out /
low-contrast. Switched to the readable outlined treatment (accent-tinted background +
var(--accent-text) text + var(--accent) border), matching the uninstall button + the
steer-recovery-retry convention. Verified readable on Gallery + Installed tabs in
both dark and light.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
- Add KANBAN_UNASSIGNED_LANE constant and _kanbanLaneKey/_kanbanLaneLabel
helpers so unassigned tasks map to a stable internal key '__unassigned__'
rather than a locale-dependent translated string
- _kanbanLaneNames now places 'default' first, other assignees alphabetically,
and the Unassigned lane last
- _kanbanRenderProfileLanes filters by _kanbanLaneKey, renders label via
_kanbanLaneLabel, and adds kanban-profile-lane-unassigned CSS class
- _kanbanTaskMeta always emits the unassigned label so sidebar shows it too
- _kanbanRenderBoard checks unfiltered total and shows a 'Tasks hidden by
filters' state with a Clear filters button when text search hides all tasks
- Add kanban_tasks_hidden_by_filters and kanban_clear_filters i18n keys to
all 13 locale blocks
- Add .kanban-profile-lane-unassigned header border using warning token
Fixes: on Dynasty board mobile, Ready=3 CLI but default lane showed Ready=0
because those tasks were unassigned and had no visible lane.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The .steer-recovery buttons set background:var(--surface) but no color, so native
<button> text fell back to the UA ButtonText (dark) color -> dark-on-dark unreadable
'Retry'/'Dismiss' in dark mode. Added explicit color:var(--text) on buttons + label,
styled Retry as an accent-colored primary (color+border var(--accent), bold), and
white-space:nowrap so labels don't wrap on mobile. Verified readable across dark,
light, and mobile viewports.
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Root cause: .messages had overflow-anchor:none unconditionally. On mobile
touch devices (iOS Safari, Android Chrome), the compositor can paint a frame
with scrollTop=0 between innerHTML='' and snapshot restore, scroll-janking
the user to the top of the transcript.
Three-layer fix:
1. CSS (style.css): Use overflow-anchor:auto on touch devices (mobile),
keep overflow-anchor:none on desktop (mouse-driven) via
@media (hover:hover) and (pointer:fine) — preserves the existing
behavior where tool/card inserts don't yank a reading user.
2. JS guard (ui.js): Add window._fixMobileScrollJank() that temporarily
sets overflowAnchor='auto' on #messages before innerHTML='' and
clears it one rAF later — prevents the compositor frame with scrollTop=0.
3. Streaming protection (messages.js): Call _fixMobileScrollJank() at the
top of each _doRender() tick so streaming DOM growth doesn't jank the
viewport position.
Tests:
- Updated test_issue1360_streaming_scroll_hardening.py: split the
overflow-anchor test into desktop (none) and mobile (auto) variants.
- Added test_mobile_scroll_jank.html: interactive visual test that
simulates the scroll jank on a mobile-sized viewport.
Took master's i18n.js as base (it has all 13 locale blocks + had reorganized the
kanban keys, so the stale-branch conflict block would have duplicated them in
zh-Hant), then inserted #2814's 5 kanban_view_* keys into every locale block after
kanban_new_task — real translations for it/ja/zh/zh-Hant/fr (from the PR), English
fallback elsewhere. All 13 locales now have exactly 1 of each key; locale-parity +
kanban tests green (291 passed).
greptile P2: .skeleton-empty-hint set opacity:.4 but used skeletonFadeInDim,
which ends at opacity:.5 with fill-mode:both — so the animated rest state was .5
while the reduced-motion (animation:none) path rested at .4, an inconsistency.
Use a dedicated skeletonEmptyHintFadeIn keyframe that ends at .4 so both the
animated and reduced-motion paths rest at the same intended opacity.
The profile-switch skeleton (#4671) always rendered a fixed content shape; for
a brand-new/empty profile that flashed a misleading 'loading conversations'
skeleton that resolved to an empty list. Track each profile's last-seen session
count (localStorage, per profile name, recorded after each non-all-profiles
render) and, when switching into a profile KNOWN to have zero sessions, render a
quiet empty-state placeholder instead of the content skeleton. Unknown profiles
keep the content skeleton (safe default); skipped under an active project/source
filter (the count is an unfiltered total). prefers-reduced-motion + light/dark
aware. Frontend-only. 7 source-structural tests; Phase-1 skeleton tests still
green.
The assistant message footer (model · duration · tokens · cost · cache% · time · actions)
could overflow its row on mobile/narrow panes, pushing the per-message action buttons off
the right edge where they were unreachable. #4724 proposed ellipsing every stat, which
shrank them into unreadable stubs (D…, 98…). This instead lets .msg-foot flex-wrap to a
second line: every stat stays full-size + readable, the actions/time are pinned flex:0 0
auto so they never shrink and stay on-screen, and on wider screens where it already fits
wrap never triggers. Pure CSS. + source-structure regression test.
Co-authored-by: starship-s <starship-s@users.noreply.github.com>
Addresses @rodboev + greptile review of #4671:
- panels.js switchToProfile(): pass timeoutToast:false to the /api/profile/switch
POST so a superseded or transient-but-successful switch no longer pops api()'s
generic 'Request timed out' error toast; the generation-guarded catch handler is
now the single source of truth for switch failure.
- panels.js: re-check _switchGen after the awaited renderSessionList() in the
in-progress-session branch so a rapid second switch can't have its workspace
skeleton cleared / a stale toast popped by a slower earlier switch (mirrors the
no-messages branch guard).
- style.css: group labels now fade in via skeletonFadeInDim (ends at opacity:.5)
instead of the shared skeletonFadeIn (ends at opacity:1 with fill-mode:both),
which was overriding their intended resting 50% opacity.
- 4 new static-source regression assertions locking the three fixes; CHANGELOG
Unreleased entry updated (fixed-shape skeleton note + the three review fixes).
Builds on Nathan's switch-failure workspace-skeleton-clear fix (which already
covered Greptile P1). Remaining two:
- [P2] Removed dead _clearSessionListSkeletonFlag() — the flag is reset inline in
renderSessionListFromCache(); the helper had no call site.
- [stagger] The .skeleton-row:nth-child(N) fade-in delays skipped most rows
because group-label siblings are interleaved with rows in the DOM (positions
shift: rows land at child 2, 4-6, 8-11, so 9/10/11 got no delay). Replaced the
CSS :nth-child rules with an inline per-row animationDelay set in JS (rowIndex
* 0.025s, capped 0.2s) so every row gets its correct stagger regardless of
group structure.
Tests: +1 behavioural (every row carries a real inline stagger delay; last row
not skipped). 26 pass.
The initial sheen (15% sheen / 9% base, 1.4s) was too subtle to read as
animation. Bump to a 5-stop gradient (22-24% sheen / 8% base, 1.25s), define
explicit light-mode --skeleton-base/--skeleton-sheen tokens (was relying on
fallbacks only), and give the tree glyph the same shimmering gradient so it
animates with the bars. prefers-reduced-motion still disables the sweep.
Show polished loading skeletons the instant a profile switch begins instead of
leaving the previous profile's conversation list and workspace file tree visible
until their fetches resolve (~1s).
- static/style.css: theme-token skeleton system (single-line session rows with
group labels + timestamp bar; tree rows with glyph + name + size bar); subtle
shimmer sweep; dark-mode tokens; prefers-reduced-motion fallback.
- static/sessions.js: showSessionListSkeleton() mirrors real sidebar anatomy
(group headers + single-line rows); _sessionListSkeletonActive flag cleared on
real render.
- static/workspace.js: showWorkspaceTreeSkeleton() mirrors the file-tree row.
- static/panels.js switchToProfile(): clear stale content + show both skeletons
up front; parallelize the independent list + workspace refreshes (Promise.all);
restore real content from cache on switch failure so a skeleton never strands.
Verified live: switch fires -> skeleton instant (0 stale rows) -> resolves to new
profile's real rows. Polished across 1920/1440/1280/390 + dark mode (screenshots
pending Nathan's visual review before gate+PR).
1-line CSS: adds touch-action:manipulation + -webkit-tap-highlight-color:transparent to
button/.icon-btn/.send-btn/.approval-btn/[onclick] inside the touch/compact media block
(@media max-width:1399.98px), removing iOS Safari's ~300ms double-tap-zoom delay so taps
fire on first touch. Added a regression test pinning the rule.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>