🎨 Palette: Add accessible ARIA label to external docs button - #195
MillionthOdin16 wants to merge 371 commits into
Conversation
…all sites (NousResearch#68997) subprocess.run(["git", ...], timeout=...) deadlocks on Windows: run()'s post-timeout cleanup calls an unbounded communicate() after killing git. Killing the PATH-resolved launcher can leave a suspended descendant git.exe holding duplicates of the captured stdout/stderr handles, so the pipes never reach EOF and the reader-thread join blocks forever — leaking a process + two reader threads per fired timeout (the accumulating git.exe load behind Windows Defender CPU spikes). Two fail-open probe call sites had this identical flaw: - tui_gateway/git_probe.py::run_git — on the Desktop agent-build path (_start_agent_build -> _session_info -> branch() -> run_git), where the hang turned an optional branch label into "agent initialization timed out" (NousResearch#68609). - agent/coding_context.py::_git — hangs the agent turn inside build_coding_workspace_block under an ACP host (NousResearch#66037). Consolidate both onto one shared bounded_git_probe() in hermes_cli/_subprocess_compat.py (both files already import from there, so no new import surface): - explicit communicate(timeout), then on ANY failure a tree-kill — proc.kill() AND, on Windows, best-effort taskkill /T /F so the suspended descendant that holds the pipe writers dies too — plus a bounded 1s post-kill drain; if the pipes are still held they're abandoned (the orphaned reader threads are daemonic and cost nothing). - fail open to "" on every path: spawn error, timeout, kill() raising (access denied / already reaped — a raise inside the except handler previously escaped the contract), and non-timeout communicate() failures now also terminate the child instead of leaving it running. - the taskkill spawn can't re-enter the deadlock class: it captures no pipes (DEVNULL), so its own timeout cleanup has no reader threads to join. Normal-path spawn contract is preserved byte-for-byte: PIPE/PIPE/DEVNULL, text + utf-8 errors="replace", hidden-window creationflags on Windows only, nonzero returncode -> "". Each call site keeps its own timeout (1.5s / 2.5s). Supersedes NousResearch#68622 (Sora-bluesky — git_probe fix + tree-kill) and NousResearch#66038 (iamwongeeeee — coding_context fix), folding both into one shared helper so the two sites can't drift and every timeout tree-kills the descendant. Tests consolidated onto the helper, incl. the previously-missing assertion that a Windows timeout escalates to taskkill /T /F. Co-authored-by: Sora-bluesky <sora.bluesky.dev@gmail.com> Co-authored-by: iamwongeeeee <wykim777@naver.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…as LOADED Review on NousResearch#20379, finding 1 (High). Two ways an MCP config revision could be silently acknowledged without ever being applied: Client: the poll advanced its accepted mcp_rev BEFORE calling reload.mcp, and quietRpc collapses failures to null — a reload that failed against a temporarily broken server left the revision recorded as applied, and no subsequent poll retried until an unrelated MCP edit. The handshake is now syncMcpReload(): send the observed rev with the request, advance `accepted` only when the server answers status=reloaded (to the server's loaded_rev, falling back to the requested rev on older gateways), and re-compare on EVERY poll tick — decoupled from mtime — so a transient failure heals on the next tick. An in-flight guard stops the 5s poll from stacking requests behind a slow reload. Server: generation-only coalescing let a follower triggered by revision B ack against revision A's registry when the config changed under a slow leader. The leader now re-hashes the MCP-relevant config after discovery and repeats until stable (bounded), records _mcp_reload_loaded_rev, and a follower coalesces only when the revision it was asked to load matches — otherwise it re-runs the full reload itself. Responses carry loaded_rev. Deterministic tests for the exact failure sequences: failed reload → no ack, no generation advance; A-then-B overlap → follower re-runs; matching rev → coalesces; failed leader → follower re-runs; legacy no-rev callers keep generation-only coalescing (thread ordering via an instrumented lock, no sleeps). Client: 6 vitest cases on the ack/retry/in-flight contract.
…n the terminal Review on NousResearch#20379, finding 2 (High). The boot cache seeded the previous session's background into HERMES_TUI_BACKGROUND, the same slot a CURRENT OSC-11 answer occupies — so a cache written on a light terminal pinned a now-pure-black terminal to light forever: the new OSC-11 #000000 answer is distrusted by design, the pure-white OSC-10 foreground is distrusted too, and the macOS appearance fallback refuses to run while the slot is set. Seeding now records provenance (themeBoot.seedBootEnvironment, extracted and testable against a passed env). When the current terminal answers the background probe with the untrusted fingerprint, the gateway handler calls invalidateBootBackground(): the slot clears ONLY while it still holds the seeded value (a trusted answer that overwrote it is authoritative), OSC-10 gets first claim in the same startup batch, and a short settle pass re- derives from the live fallback chain if nothing answered. The cache is also pin-coherent now: commitTheme persists the config mode pin (display.tui_theme) alongside the resolved theme + physical background. Previously "/theme light" on a dark terminal cached a light theme next to a dark background — the next launch painted light, flipped dark when the skin resolved against the seeded background, then flipped light again on config hydration: the exact multi-stage flash the cache exists to eliminate. The seeded pin counts as config-owned (bootSeededPin), so a later 'auto' can still clear it instead of mistaking it for a user shell export. Tests cover the review's sequences: stale-light cache vs current dark terminal (invalidate → fallback chain), stale-dark vs ambiguous light, pinned light on a dark physical background across restart (and the inverse), trusted-overwrite protection, and the explicit-signal guards.
Review on NousResearch#20379, finding 3 (Medium). /grid-test's `d` opens a dialog on top without clearing the grid, but the grid's input branch ran FIRST — so Esc/q/Enter mutated the hidden grid (close/unzoom/promote) instead of closing the visible dialog, contradicting its "Esc/q/Enter close" hint. Input routing now follows visual stacking: the dialog/grid dispatch is extracted into handleStackedModalInput() with the dialog branch first, the hook consumes through it, and tests drive the real dispatch against the overlay store — each advertised close key closes only the dialog (grid byte-identical), grid keys don't leak through while the dialog is up, and the same keys route to the grid again after it closes.
Review on NousResearch#20379, finding 4 (Medium). Three portability defects in the visual verification harness: - `FORCE_COLOR=3 COLORTERM=truecolor tsx ...` POSIX env assignment does not work under the Windows npm command shell. The script is now a plain Node launcher (scripts/visual/run.mjs) that sets the env itself and spawns tsx via require.resolve('tsx/cli') — no cross-env, no shell syntax. - Hardcoded /tmp/tui-visual.{html,png} resolve to a drive-root path like C:\tmp on native Windows (and fail when that directory doesn't exist). Both scripts now derive the output directory from a shared paths.mjs helper: os.tmpdir()/hermes-tui-visual (created recursively; HERMES_TUI_VISUAL_DIR overrides for CI or side-by-side runs). - electron was undeclared by ui-tui and only worked via hoisting luck. The launcher now resolves it EXPLICITLY from the install tree the desktop workspace already provides (require('electron') in plain Node returns the binary path), with an ELECTRON_BIN override and a clear error pointing at the repo-root install when it's absent — instead of declaring a second ~100MB dependency on a TUI workspace for a dev-only harness. Also fixes the trailing-whitespace line in render.tsx that `git diff --check` flags. Verified end-to-end: render writes the HTML scene sheet and the electron shot step produces the screenshot from the tmpdir path; the missing-electron error path prints the guidance message.
…d period Review on NousResearch#20379, finding 5 (Perf). Every ShimmerRows mounted its own 90 ms setInterval — the session panel can show lazy skills AND lazy tools at once, and a lazy watch session stays lazy indefinitely, so an otherwise- idle TUI ran ~22 React state updates per second forever. All shimmer compositions now subscribe to a single module-level clock: one interval regardless of how many skeletons are on screen, updates delivered in one timer callback so React batches them into a single render pass, and the interval is torn down with the last subscriber. Each mount's animation is also bounded (SHIMMER_ANIMATE_MS, 30 s): after the budget the skeleton freezes in place — it still reads as "loading" — and stops costing renders entirely. Tests: fake-timer coverage that N subscribers share one timer in lockstep, the interval stops with the last unsubscribe, and a late subscriber restarts the clock cleanly.
Worktrees symlink node_modules to the main checkout; the dir-only node_modules/ pattern doesn't match symlinks, so one slipped into a commit and broke npm ci on CI (ENOTDIR). Dropping the trailing slash matches both.
…NousResearch#69019) content-visibility:auto on turn groups (perf: off-screen turns skip style/layout/paint) pairs with contain-intrinsic-size:auto, which only remembers a turn's size after it renders. A turn that finished streaming near the bottom had its smaller mid-stream size remembered; once it scrolled off the top edge and got skipped, it collapsed to that stale height. With overflow-anchor:none the viewport can't self-correct, so the stick-to-bottom lock drifts and the view creeps up over older turns — the 'long session eventually shows old responses' visual glitch. Exempt the newest turns (live tail) from virtualization so a turn is only ever skipped after its layout has settled at its final size (remembered == real -> skipping changes no height). Off-screen older turns still skip, so the dialog/popover whole-document recalc win on long transcripts is kept (it scales with the hundreds of old turns, not the small tail).
…d-hardening fix(ui-tui): widget-grid hardening — review fast-follow for NousResearch#20379
… apps The SDK the desktop app already has, ported to the TUI: a WidgetApp contract (id/help/mode/init/reduce/render/usage), a registry, and a host that owns the active widget, routes input to its reducer, and renders it. The grid-test and dialog-test debug surfaces are reimplemented as widget apps instead of bespoke overlay state, and slash commands are generated from the registry. Input for an open widget is owned by the active app (supersedes the demo-only stacked-modal routing) — the single active widget enforces topmost-owns-input structurally.
… ASCII art /weather [location]: wttr.in current conditions behind a Dialog, art bucket table-driven off WWO weather codes, every tint a theme family tone (sun = primary, rain = shell blue, thunder = warn). Proves the async story the demos don't: init returns a loading phase and fires the fetch; results land through the new host.updateWidget, which patches state ONLY while the app is still active — a late resolution can never resurrect a closed app or clobber a different one. `r` refetches; Esc/q/Enter close. Four async-contract tests (loading→ready via updateWidget, late-resolution guard, error phase, keymap). 1253 TS tests green.
…n-flow dock Widgets can render as ambient (glanceable, non-blocking) instead of modal, docked in the normal layout flow above/below the status bar rather than taking over the screen. The slash catalog is generated from the widget registry so new apps surface automatically, and /ticker lands as the first live-animation ambient demo.
…kill Hermes can write its own widgets: a loader discovers $HERMES_HOME/tui-widgets/*.mjs, fs.watch hot-loads them the moment they land (no restart), and a tui-widgets skill teaches the agent the contract and the openWidget-at-register auto-open recipe. Load/error/remove events announce themselves in the transcript; a lazy intro skeleton covers the first paint.
…streams Reusable render primitives the SDK exposes to widget authors: sparkline/gauge/ hbars chart helpers (dimension-stable so live updates never resize the card), an Accordion for expand/collapse sections, animated shimmer loaders, and a streams demo that no longer reserves a phantom icon column on unfocused titles.
A full placement grid so the agent can put a widget where it asks — dock-top/ bottom and corner zones, with corners as reserved rails that take real space instead of floating over content. A per-widget error boundary plus lenient ShimmerRows means generated widget code can't crash the TUI.
host.tsx collapses to one placement router over a shared render context, and the grid-test app drops its width floor too (carrying the NousResearch#20379 review rule). Final formatting pass folded in.
… desktop Make the Python skin engine the single source of truth for a canonical theme shape consumed by every surface, so a skin authored in $HERMES_HOME/skins/*.yaml (by a user or by Hermes from a prompt) themes the CLI, TUI, and desktop GUI at once — the theme analogue of the plugin SDK. - @hermes/shared: canonical `HermesSkin` token shape + `SKIN_COLOR_TOKENS` enum, consumed by both TS surfaces (TUI `GatewaySkin` and desktop dedup onto it). - Desktop: `skinToDesktopTheme` resolver (skin → CSS-var palette, VS Code-style derive-from-seed) + `backend-sync` that registers backend skins into the theme registry (Appearance/Cmd-K/`/skin`) and applies on a real change. Seeds on gateway.ready (never stomps a persisted pick), applies on skin.changed and the post-turn `config.get skin` poll (catch-all for agent-edited config.yaml). - TUI: `fromSkin` now maps the status bar + `background` keys it was dropping. - Gateway: `config.get skin` also returns the full resolved palette (additive). - Skill: `hermes-themes` teaches the agent to author + activate a skin. Each surface keeps its own normalizing resolver (ansi for the TUI, CSS vars for the desktop, prompt_toolkit/Rich for the CLI).
…aml hand-edit
The skill told the agent to `patch` display.skin into config.yaml; a stray indent
corrupts the file and breaks the live gateway (the reported "/ menu broke"), and
a raw file edit never live-applies in a running CLI/TUI ("nothing happened").
Route activation through the safe writer (`hermes config set display.skin`), and
state plainly that a tool call can't hot-switch a running CLI/TUI — the user runs
`/skin <name>` (desktop still auto-repaints on the next turn).
…cher A skin Hermes activates (`hermes config set display.skin X`) or recolors in place now goes live on every surface (CLI, TUI, desktop) within ~half a second, on its own — no `/skin`, no tool-hook timing, no user action. A gateway daemon polls the resolved skin signature `(name, active-file mtime)` every 0.5s and broadcasts `skin.changed` on any real move — a name switch OR a live color edit to the active skin. It routes through the SAME path `/skin` uses, so all surfaces repaint identically. The watcher seeds its baseline at gateway.ready (stdio + ws) so it only fires on a real change; the `/skin` RPC seeds the baseline too so it never double-broadcasts. Subsumes the desktop's post-turn `config.get skin` poll (its skin.changed handler already applies).
The TUI inherited the terminal's background; now a skin's `background` paints the whole surface via OSC 11 when a skin is applied, and clears back to the terminal default (OSC 111) on revert and on exit (ridden in through resetTerminalModes). Opt-in: a skin with no `background` leaves the terminal untouched, and the restore only fires if we actually painted. Desktop already themed its own bg; this closes the loop so Hermes owns its background on every surface.
Theming was semantic-only: the gold tool `●` was `accent`, shared with headings/links/chevrons, so "recolor tool calls" was impossible and the agent had no key to point at. Add `ui_tool` (● + tool spinner) and `ui_thinking` (reasoning body) tokens that fall back to accent/muted — defaults unchanged, but now independently settable. Make diffs skinnable too (`diff_*`), which fromSkin previously hardcoded. Document the full element→key map in the skill so Hermes knows which knob turns what.
Changing one color ("make the tool ● cyan") forked `default` — which has no
`background` — so applying it reset the terminal to its own (black) default and
dropped the active skin's palette. Teach the skill to edit the active skin's file
in place for a tweak (watcher repaints on the mtime bump), and to fork a built-in
only by carrying its full palette. Hard pitfall: never fork `default` for a tweak.
…ntouched Changing a single color kept wrecking the rest because the agent hand-authored a new skin (often from `default`, which has no `background`, resetting the terminal to black). Add `hermes skin set <key> <hex>`: edits the ACTIVE skin's one key in place (a built-in is forked into an editable copy carrying its full palette), so everything else — background included — is preserved. Plus `skin use` / `skin list`. The skill now points tweaks at this command instead of hand-authoring.
Code highlighting reused brand tokens (accent/text/border/muted), so it couldn't be themed independently. Add syntax_string/number/keyword/comment skin keys → syntax* theme tokens (defaulting to those brand tokens, so defaults are unchanged) and point the highlighter at them. Documented in the element→key map.
… pipeline Element tokens (ui_tool/ui_thinking), skinnable diffs, and code-syntax keys flow through buildPalette → adaptColorsToBackground instead of a hand-mapped color block, so they inherit NousResearch#20379's contrast/polarity machinery. thinking and syntaxComment track the EFFECTIVE muted (banner_dim override included); the skin's `background` feeds the surface (it also paints the terminal via OSC 11); statusFg falls back through ui_text/banner_text. Tests assert the routing/independence contracts rather than pre-adaptation hexes.
ingestBackendSkin returned early for name === 'default' even when apply=true, so a real runtime switch to the default skin (/skin default on CLI/TUI, or config.set display.skin=default) emitted skin.changed but never repainted the desktop. 'default' is no-opinion on the PALETTE (the desktop keeps its own nous default, so we still never register a converted theme under it), but it IS a valid apply TARGET: setTheme normalizes 'default' -> nous, so switching back repaints to the desktop default. Skip only the registry step for 'default' and let it flow through the apply guard. Addresses Copilot review.
Extract the open_preview emitter into a shared tools/desktop_ui bridge (one gateway-injected sink, routed by HERMES_UI_SESSION_ID) and add a second desktop-gated tool on top of it: - focus_pane(chat|files|terminal|review|sessions) -> pane.reveal event. The desktop runs each pane's own reveal path (revealDesktopPane table) and only acts on the active window -- a background turn never moves the user's focus (desktop AGENTS.md: offer, don't hijack). open_preview now emits through the same bridge. Both tools are check_fn on HERMES_DESKTOP (zero footprint elsewhere), sitting beside read_terminal/close_terminal in _HERMES_CORE_TOOLS. Deliberately not adding run_slash: letting the agent fire slash commands mid-turn (/model, /new, /clear) fights prompt-cache + conversation invariants.
…eview-open feat(desktop): let the agent drive the shell — preview pane + pane focus
…d age util - Use the fork glyph for branch and a sine wave for read aloud (all one lib now) - Extract compact "2h ago" into formatAgo() in lib/time.ts (+ ageDays locale string) - Cover formatAgo with a unit test
A stray tsc run can emit foo.js next to foo.ts under apps/shared/src or apps/desktop/src. .gitignore hides the artifact from git status, but Vite resolves extensionless imports .js-before-.ts, so the renderer silently runs the stale compiled copy. tsc -b . --clean already knows the emit graph and deletes matching outputs. Run it before vite in all dev scripts. This bit for real: a Jul 16 artifact of websocket-url.js predated the NousResearch#68250 getGatewayWsUrl contract change ({ ok, wsUrl } IPC result), so its old 'if (fresh) return fresh' handed the whole result object to new WebSocket(), dialing ws://127.0.0.1:5174/[object%20Object] on every boot. The desktop app could never connect, and the failure survived reboots and cache wipes because the poison lived in src/. JsonRpcGatewayClient.connect() now rejects non-ws:// URLs with a readable error instead of letting new WebSocket() coerce an object into [object%20Object], so any future contract skew fails diagnosably.
…-actions Flatten assistant message actions into an inline icon row
…ousResearch#54242) A pure-Latin query (no CJK characters) routes to the unicode61 `messages_fts` table, whose tokenizer does not insert a boundary between Latin letters and adjacent CJK characters. Content like "修改youer服务端" is indexed as a single token, so `search_messages("youer")` returned zero results even though the substring is present, and the Latin path had no fallback. Add a zero-result trigram fallback to the pure-Latin path: when the unicode61 search misses, retry against the existing `messages_fts_trigram` table, which matches substrings regardless of word boundaries. The fallback is gated on `_trigram_available` and on every token being >=3 chars (the trigram minimum), and only fires on a zero-result miss, so successful Latin searches keep their unicode61 ranking unchanged. The trigram query construction shared with the CJK path is extracted into a `_run_trigram_search()` helper; the CJK branch is refactored to use it with no behavior change. Adds regression tests in tests/test_hermes_state.py::TestCJKSearchFallback.
The zero-result fallback prefers messages_fts_cjk when built: exact ranked token match for Latin runs unicode61 fused onto CJK, including <3-char tokens the trigram leg can't recover.
…aces Two narrow timing windows (reported by null-runner) silently downgraded a mid-turn correction to a plain next-turn message on the desktop client: - Turn-build window: a fresh turn flips running=True and builds the agent asynchronously, so session["agent"] is briefly None. session.redirect answered 4010 "unsupported", which the renderer's catch swallowed into a lost follow-up. Queue the correction server-side instead and return status="queued" — lossless, and honest about what happened. - Stale runtime id after reconnect: session.redirect 404s on a sid the gateway no longer maps. redirectPrompt now resumes the stored session and retries once, mirroring stopPrompt, so a correction fired right after a reconnect isn't dropped. The desktop treats "queued" like "redirected": the correction reaches the model either way, so it's recorded once as a real user message.
…board), not just stdio The cross-surface theme SDK's live-repaint relies on a gateway skin watcher that polls config and emits skin.changed on any move. But that emit is session-less and fires from a background thread, so write_json fell through its (session-transport -> contextvar -> stdio) ladder to the module stdio transport — which only reaches the stdio TUI (tee'd to the dashboard WS publisher). WS clients (the desktop app, dashboard chat) never got it, so 'Hermes themes itself' repainted the CLI/TUI but not the GUI. Add a live-transport registry (one entry per connected WS peer, maintained by handle_ws) and a _broadcast_global_event primitive that fans session-less announcements out to every connected client, falling back to write_json when none are registered (stdio path unchanged). Route both skin.changed emits (watcher + the /skin RPC) through it, so a skin switch from any surface repaints all of them. Backend-only; desktop already handles skin.changed and does not drop session-less events.
…ldowns (NousResearch#69494) When Codex returns 429 usage_limit_reached, Hermes persists the provider's reset_at on the pool entry and freezes the credential until it elapses -- which can be days out for weekly windows. But the upstream window can reopen EARLY: the user redeems a banked rate-limit reset (Codex CLI / ChatGPT UI), upgrades their plan, or OpenAI resets the window. Hermes never re-checked, so it kept erroring with 'Codex provider quota exhausted (429); retry after Ns' until a manual re-auth rewrote the tokens (issue NousResearch#43747, externally-reset variant). - hermes_cli/auth.py: add _probe_codex_quota_restored() -- a throttled (5 min/token) GET of the Codex /usage endpoint; quota counts as restored when every reported window is <100% used. Add clear_codex_pool_quota_cooldowns() to lift 429/quota-shaped cooldowns from persisted pool entries (DEAD and auth-shaped entries untouched). - resolve_codex_runtime_credentials(): before surfacing a pool-only cooldown as 'quota exhausted', probe upstream; on a positive probe clear the cooldown and return the pool credential. - agent/credential_pool.py: _available_entries() probes frozen openai-codex entries (clear_expired path only) and unfreezes them when upstream confirms the reset. - agent/account_usage.py: a successful /usage reset redemption now clears persisted pool cooldowns immediately. Negative paths preserved: probe 429/exhausted/indeterminate keeps the cooldown; read-only enumeration never probes; non-JWT tokens never probe (no network in hermetic tests).
…ed the activation Real-world failure from dogfooding the live-theme flow: display.skin was already 'synthwave' in config, but the desktop never visibly applied it (the activation event predated the WS transport fix / the connect). The desktop's gateway.ready seed records the baseline WITHOUT painting (by design — never stomp the persisted desktop theme on connect), so it believed it was synced. Re-running 'hermes config set display.skin synthwave' then did nothing twice over: the watcher signature (name, skin-file mtime) hadn't moved, so no skin.changed fired; and even on an event, the desktop's name-equality guard blocked the apply against the seeded baseline. Two halves: - hermes_cli: setting display.skin touches the named skin file so the watcher signature always moves on an explicit set — a same-name re-affirm now broadcasts skin.changed like any real move. Built-ins (no file) are unaffected; a name switch already moves their signature. - desktop: track whether the synced baseline was actually APPLIED vs merely seeded at connect. A skin.changed matching a seed-only baseline is an intentional apply and repaints; once applied, repeat same-name events stay no-ops (protects a manual desktop-side theme switch from snap-back, incl. across a reconnect re-seed).
…hadow-guard fix(desktop): clean stale tsc emit + guard gateway WS URLs
…ousResearch#54855) (NousResearch#67364) * chore(gitignore): ignore installer .install_method stamp Salvage of NousResearch#54855 by @drissman — rebased onto current main with root-scoped rule and sister-marker comments alongside .update-incomplete. Closes NousResearch#66189 Root cause: scripts/install.sh writes <install>/.install_method but git did not ignore it, so managed checkouts show ?? .install_method and hermes update may autostash the untracked marker. Fix: add /.install_method to .gitignore (repo-root only). Verification: git check-ignore -v .install_method * test(update): assert .install_method survives update autostash (NousResearch#66189) Add hermetic regression mirroring the .hermes-bootstrap-complete test: adopt the real .gitignore, drop the installer .install_method stamp, run the exact 'git stash push --include-untracked' the updater uses, and assert the marker is neither swept nor reported dirty. Requested by hermes-sweeper review on NousResearch#67364.
…n-steering feat: redirect active turns when users correct the agent
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…ighten comments _emit and _broadcast_global_event were each building the JSON-RPC event envelope — extract _event_frame and use it from both. Type the registry as set[Transport] (protocol already imported), and cut comment bloat at the call sites. No behavior change; suites stay green.
…broadcast fix(themes): live skin sync reaches every surface — WS fan-out + missed-activation recovery
Live theme authoring's core loop — Hermes recolors the skin file it just activated — repainted the TUI but not the GUI. The event path was fine (post-NousResearch#69533 the WS broadcast lands and ingestBackendSkin refreshes the $backendThemes registry); the same-name apply guard also no-ops correctly (it's what protects a manual desktop theme pick). The repaint was supposed to come from the registry: the active theme IS that skin, its palette just changed. But ThemeProvider memoized deriveTheme on [themeName, resolvedMode] only, while deriveTheme reads the registry non-reactively via resolveTheme — so the store update re-rendered the provider and handed back the stale palette. Name switches repainted (themeName moves); recolors never did. Add the theme stores (user/backend/registry) to the memo's deps — they are deriveTheme's actual reactivity, same as the availableThemes memo directly above. applyTheme is idempotent, and $backendThemes only publishes on a real palette change, so no spurious repaints. Tests: render ThemeProvider for real — activation applies; a same-name recolor repaints (fails without the fix); an inactive-skin seed doesn't touch the painted theme.
…edit-repaint fix(themes): desktop repaints when the ACTIVE skin is edited in place
…ousResearch#69578) The submit "session context drift" guard (regression 7acaff5 / NousResearch#54527, partially fixed by 8c28876 and da52ffe) aborted a prompt submission whenever the selected stored id OR the route token changed mid-submit. Both signals churn programmatically on a busy gateway, so on machines with background streaming sessions, per-minute cron sessions, the Telegram surface, or gateway-profile switches, essentially every send from a second chat aborted silently: the optimistic message was dropped, the draft was left in the composer, no error was shown, and prompt.submit never fired. The false-positive churn sources were: - selection null-resets — gateway-switch's setSelectedStoredSessionId(null) on a gateway/profile switch or reconnect read as a switch away; - search/hash-only route-token changes — overlays and side panels park state in location.search/hash, so the pathname (the only part that selects a chat) was unchanged yet the raw token differed; - background-event active-ref retargets — createBackendSessionForSend's 3-prong check also watched activeSessionIdRef, which gateway events retarget while other sessions stream (NousResearch#47709 class), during a seconds-long session.create round-trip. New shared helper session-context-drift.ts reduces a route token to the chat it targets (pathname only; the new-chat route is '__new__', non-chat routes null) and reports drift only when selection or the routed chat moves to a DIFFERENT, non-null chat that is not the submit's own target. Selection null-resets, search/hash-only churn, and moves onto the submit target are no longer drift; genuine user switches (click another chat, click New Session mid-submit) still abort. Site A (submit.ts) routes all five guard points through the helper and logs '[submit-drift-abort]' with a per-site phase; the post-create active-ref check and baseline re-pin from 8c28876 are kept intact. Site B (createBackendSessionForSend) drops the active-ref prong entirely — every real switch retargets selection and route synchronously — and logs before closing the orphaned session. (cherry picked from commit b390e3a) Co-authored-by: Kennedy Umege <kenmege@yahoo.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Add missing `aria-label` and `tabIndex={-1}` to the icon-only `<Button>` for external docs in `OAuthProvidersCard` to prevent a double-focus trap when nesting interactive elements in a link.
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
📝 WalkthroughWalkthroughThe OAuth provider documentation icon button now includes an explicit accessible label and is excluded from sequential keyboard tab navigation. ChangesOAuth provider docs accessibility
Estimated code review effort: 1 (Trivial) | ~2 minutes Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@web/src/components/OAuthProvidersCard.tsx`:
- Line 225: Update the documentation-link markup in OAuthProvidersCard so the
anchor is the sole interactive element; remove the nested Button and apply its
styling through the supported asChild pattern or equivalent. Move the accessible
“Open {p.name} docs” label to the anchor and preserve the existing icon-button
appearance and link behavior.
- Line 225: Update the documentation Button in OAuthProvidersCard to use a
localized translation key instead of the hard-coded “Open … docs” aria-label,
and reuse that translated label for both aria-label and title. Follow the
component’s existing translation pattern and preserve the provider name
interpolation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 0022f554-f409-4732-b0e4-0f5703d76528
📒 Files selected for processing (1)
web/src/components/OAuthProvidersCard.tsx
| title={`Open ${p.name} docs`} | ||
| > | ||
| <Button ghost size="icon"> | ||
| <Button ghost size="icon" aria-label={`Open ${p.name} docs`} tabIndex={-1}> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Do not nest Button inside the documentation link.
tabIndex={-1} prevents double tab stops but does not make nested interactive elements valid. Keep the <a> as the sole interactive element—using the button styling via a supported asChild pattern or equivalent—and move the accessible label to the anchor.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/OAuthProvidersCard.tsx` at line 225, Update the
documentation-link markup in OAuthProvidersCard so the anchor is the sole
interactive element; remove the nested Button and apply its styling through the
supported asChild pattern or equivalent. Move the accessible “Open {p.name}
docs” label to the anchor and preserve the existing icon-button appearance and
link behavior.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use a localized label.
This hard-codes English in an otherwise localized component, so screen readers will announce English text for non-English users. Add a translation key and reuse it for both aria-label and title.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/src/components/OAuthProvidersCard.tsx` at line 225, Update the
documentation Button in OAuthProvidersCard to use a localized translation key
instead of the hard-coded “Open … docs” aria-label, and reuse that translated
label for both aria-label and title. Follow the component’s existing translation
pattern and preserve the provider name interpolation.
💡 What: Added
aria-labelandtabIndex={-1}to the icon-only<Button>within the external docs<a>link inOAuthProvidersCard.🎯 Why: Resolves a keyboard navigation double-focus trap and provides clear screen reader context.
📸 Before/After: N/A (non-visual structural accessibility change)
♿ Accessibility: Screen readers will now announce "Open [Provider Name] docs", and keyboard users will not have to "Tab" twice over a single interactive anchor tag.
PR created automatically by Jules for task 2171264551938717870 started by @MillionthOdin16
Summary by CodeRabbit