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>
* stage #4660 v2 (franksong2702): full-screen PWA mobile sidebar drawer — REBUILT on current master
REBUILD: the prior stage was cut before tonight's #4597/#4584/#4583 ships, so its 3-dot
diff falsely appeared to revert them (Codex CORE/SILENT flags). Rebuilt on current master
(authoritative head a035b1343): clean apply, tonight's fixes confirmed INTACT (_apply_sidebar_
state_db_overrides, bottomDistance, overflow-x:hidden all present). #4660's own change is
additive — full-screen drawer width, edge-guard, mobile close button, overlay->display:none;
does NOT touch the scroll/perf/mobile-pan lines. Nathan concept-greenlit + visual signed off.
71 mobile-layout+swipe tests pass.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
* fix#4660 edge-guard regression: defer preventDefault until horizontal intent + touch-action:pan-y; update 2 tests to assert corrected behavior
* fix#4660 edge-guard hit-test interception (Codex CORE): pointer-events:none + drop guard listener
Deeper fix than the prior preventDefault change: the #pwaSidebarEdgeGuard was a fixed
interactive element (z-index 198, no pointer-events:none) that intercepted hit-testing for
taps/vertical-scrolls starting in the left 24px strip (since .messages is the real scroller
under body:overflow:hidden). Now guard is pointer-events:none with NO dedicated touch
listener; the existing window-level CAPTURE handlers still catch the edge-swipe-to-open.
Removed the orphaned _onPwaSidebarEdgeGuardStart fn. Updated 2 tests to assert the new
non-interactive design. 71 mobile tests pass.
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
Re-staged from the contributor's rebased head (0cee92c9) which independently converged on
the same 5-test-file class-tolerance fixups I'd made — shipping the contributor's exact
version, not my private variant. Tags md/markdown/mdx fenced blocks with md-source-block
class in both renderers; CSS wraps them while code/diff/csv keep horizontal-scroll. 90
renderer-family tests pass.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: TomBanksAU <TomBanksAU@users.noreply.github.com>
#4584 (fixes#4295): scroll re-pin requires deliberate downward intent (deltaY<-30 unpin
threshold, re-pin only if not user-unpinned OR bottomDistance<=80, reset counter) so reading
mid-stream isn't hijacked back to bottom. ui.js only + 5 scroll-regression tests (4 existing
assertions adjusted, all green; new test_issue4295_scroll_pin_reentry).
#4583 (fixes#4553): mobile transcript overflow-x:hidden + max-width:100% + word-break so wide
content can't pan sideways. style.css only + own test. Disjoint files. 32 scroll-family tests pass.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: rodboev <rodboev@users.noreply.github.com>
#4644: close mobile sidebar drawer after a rail-click panel switch (was left open).
#4645: animate drawer open/close with transform:translateX instead of left offset.
Disjoint product files (panels.js vs style.css); both add tests to test_mobile_layout.py
in distinct functions (no conflict). Mobile-only, desktop unaffected. 60 mobile-layout
tests pass. Code byte-faithful from PR heads.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
UI enable/disable of installed manifest extensions via a bounded fail-safe
extension-overrides.json state file. Safe boundary: no manifest edits, no asset
fetches, no sidecar proxy, no extension-owned routes. /api/extensions/toggle is
auth+CSRF gated; anchored _EXTENSION_ID_RE (no path traversal); atomic UTF-8 write
under _EXTENSION_STATE_LOCK; 32KiB/512-id DoS bounds; disabled override suppresses
asset/sidecar injection end-to-end. Nathan greenlit the extensions-phase-2 concept.
Code byte-faithful from PR head 8769b4a3902e. 49 own tests pass.
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* stage #4612 (santastabber): surface loopback sidecar diagnostics + CHANGELOG
Settings -> Extensions surfaces manifest-declared loopback sidecar companion services
(type:loopback, http(s) on 127.0.0.1/localhost/::1 only, sanitized origin+health_path)
as read-only health cards w/ a browser-side credentials-omitted health probe. Backend
hard-whitelists scheme + loopback host, rejects userinfo/path/query/traversal, rebuilds
output from parsed components (no echo of rejected input); frontend esc()'s every field,
whitelists badge status, fetch never reads body. Purely additive to GET /api/extensions/status.
Code applied byte-faithful from PR head 7080929f3727. 33 own tests pass.
Nathan concept-approved (loopback-companion direction).
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* fix#4612 Codex gate findings: reject encoded query/fragment in health_path + align CSP loopback allowlist
Codex SHIP-WITH-FIXES (2 reproduced SILENT findings):
1. _normalize_sidecar_health_path percent-decoded AFTER the raw query/fragment ban,
so /health%3Ftoken=abc -> ?token=abc survived into the probed URL. Now re-rejects
decoded ? and # + regression test (%3F/%23 skipped with sidecar_health_path_rejected).
2. The origin validator accepts https:// and [::1], but CSP connect-src only listed
http 127.0.0.1/localhost -> accepted https/IPv6 loopback sidecars would be CSP-blocked.
Aligned _CSP_CONNECT_BASE (added https v4/localhost + http(s) [::1]) + updated the 4
exact-string CSP tests + a regression test asserting all loopback forms are allowed.
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* Release v0.51.564 — Release TW (loopback sidecar diagnostics; #4612)
* fix(#4612): drop browser-invalid [::1]:* CSP source (browser-smoke fail)
The CSP-loopback-alignment fix added http(s)://[::1]:* to connect-src, but CSP
host-source grammar can't express a port wildcard on a bracketed IPv6 literal —
Chromium rejects '[::1]:*' as an invalid source, which browser-smoke flags as a
console error (CI fail). Keep the valid https://127.0.0.1:*/localhost:* additions;
drop the IPv6 wildcard entries. IPv6 [::1] sidecars are still accepted by the
validator (displayed in the card); their browser health probe surfaces as
'blocked' under CSP unless an operator adds a specific-port [::1]:<port> via
HERMES_WEBUI_CSP_CONNECT_EXTRA. Tests updated to match + assert [::1]:* absent.
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>