- 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>
* stage #4380 (santastabber): render selected context as quote cards + CHANGELOG
Evolves the shipped #2543 named-context-blocks: composer chips -> quote cards
(accent rail, editable label w/ rename, remove, clipped excerpt) + thread render
of labeled selections as <figure> cards. Label+quote esc()'d on every path
(adversarial harness 0 survivors). 3 new i18n keys in all 13 locales (parity green).
Code applied from PR head 2eefc11c02a7 (own 3-dot contribution, CHANGELOG re-stamped).
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
* fix#4380 Codex gate finding: selection-only reply must enable the primary Send button
Codex CORE: #4380 moved selected context out of the textarea into _pendingSelections,
but _composerHasContent() only checked textarea+files -> getComposerPrimaryAction()
returned disabled for a selection-only reply (desktop Enter calls send() directly so
it worked, but click/tap/mobile users were blocked). Fix: expose
window._hasPendingSelections() from messages.js, fold it into _composerHasContent(),
and call updateSendBtn() from _renderSelectionChips() so add/remove/clear refreshes
the button. + regression test.
* Release v0.51.562 — Release TU (selected-context quote cards; #4380)
---------
Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
Co-authored-by: santastabber <santastabber@users.noreply.github.com>
franksong2702's activity_scene_v1 anchor-scene model: live-stream/settled/refresh/re-entry
show the same ordered turn. Rebased clean (no conflicts) onto current master.
Co-authored-by: franksong2702 <franksong2702@users.noreply.github.com>
The outline FAB was position:fixed at z-index 9999 and could stack on top of the
scroll-to-bottom button. Anchor it inside the messages shell (position:absolute,
z-index 12) so the two controls don't overlap.
Co-authored-by: Mohamed Habib <Habib1001-m@users.noreply.github.com>
- Hamburger button: 32x32 -> 44x44 tap target (iOS HIG minimum), SVG icon
20 -> 24px. flex-shrink:0 preserved; spacer keeps the title centered.
- Edge-swipe-to-open-sidebar: edge zone 28 -> 80px, trigger 72 -> 64px, max
vertical 48 -> 56px; removes the _isPwaStandalone() gate in
_onPwaSidebarSwipeStart so the gesture works in a normal mobile browser, not
only the installed PWA. Still _isDesktopWidth()-blocked, touch/pen-only,
interactive-target-excluded, and requires horizontal intent (dx>=64 &&
dx>|dy|*1.5), so scroll/tap won't trigger it.
Known tradeoff (Nathan-approved ship-as-is): in mobile BROWSER the 80px left
edge zone overlaps iOS Safari's back-swipe; degrades gracefully (PWA, the
primary mobile target, has no back gesture).
Verified live at 390x844: hamburger renders 44x44 + tap opens sidebar; a
left-edge swipe opens the sidebar in a non-PWA context. 66 existing mobile/
swipe/titlebar tests pass.
Co-authored-by: kaishi00 <kaishi00@users.noreply.github.com>
Paused/disabled cron jobs are partitioned out of the active list into a
collapsible 'Paused (N)' <details> section so they don't drown the active
jobs. _cronList stays the single source of truth — only the render is split,
so openCronDetail / _cronNewJobIds / detail-refresh are untouched. Reuses the
already-translated cron_status_paused label (zero new i18n keys). Collapsed
state persisted in localStorage.
Maintainer polish on absorb: wrapped both localStorage read/write in try/catch
(repo convention — a storage throw in private mode must not dump the cron
sidebar into the error banner) + added the CHANGELOG entry.
Co-authored-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
The inline PDF preview rendered only page 1, so multi-page documents
looked clipped. loadPdfInline now renders every page (capped at MAX_PAGES=20
for memory) sequentially into a scrollable .pdf-preview-body, shows the page
count in the header, and appends a "Showing the first N of M pages" notice
(pdf_truncated, 13 locales) when the PDF exceeds the cap.
Robustness: renderPage(i) attaches each canvas only AFTER its render
promise resolves, and a per-page failure is caught and skipped
(`.then(()=>renderPage(i+1)).catch(()=>renderPage(i+1))`) so one corrupt
page can't halt the preview or surface an unhandled rejection. XSS-safe
(esc(fname), t() with numeric args).
Co-authored-by: Gouranga satapathy <29521030+gourangasatapathyvit@users.noreply.github.com>
"Reply with selection" no longer appends the selected text straight into
the composer. It now adds a named chip ("Context N") to a row above the
message box (composerSelectionChips). Chips can be renamed (double-click,
esc()-escaped) or removed (x); multiple stack. On send(),
_flushSelectionBlocksToComposer() materializes pending blocks into the
composer as **Name:** + markdown-quoted text, then the normal send path
runs unchanged on composer.value.
Regression-safety:
- _flushSelectionBlocksToComposer early-returns when no blocks are pending,
so send() is byte-identical for anyone who never uses the feature.
- loadSession() AND newSession() both clear pending blocks before replacing
S.session, so chips never leak across a sidebar switch OR a New Chat
(the New Chat clear + its regression test were added per Codex review).
- chip name/text rendered via esc(); XSS-safe.
Verified live (headless CDP drive): add/render/remove/rename, flush
produces correct markdown, empty-flush no-op, session-switch clears,
injected <img onerror=...> chip-name escaped (not executed).
Co-authored-by: Rod Boev <rod.boev@gmail.com>
Ships @rodboev's searchable-settings feature (#3850/#3936): a search box in
Settings that finds any field across all 5 sections with breadcrumbs +
click-to-navigate+highlight+clear. Rebased fresh onto master (post v0.51.470),
5 feature files byte-identical to PR head, re-stamped v0.51.471 (was v0.51.469,
collided with the shipped transcript-virtualization release).
Co-authored-by: Rod Boev <rod.boev@gmail.com>