feat(frontend): palette search, screen-reader announcements, page-level Escape, calmer polling and scrolling - #320
Merged
Merged
Conversation
…e full time on hover A message showed only its clock time, so reopening an old thread repeated one time of day on every turn with no way to tell which day. formatMessageTime now adds the month and day for an earlier day this year and the year for an earlier year, in the viewer's locale and time zone, and the time element carries the full date and time in its title. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…here an image was dropped Fenced code could only scroll sideways: the toolbar now has a Wrap toggle that applies to every block and is remembered in local storage (falling back to the session when storage is blocked). GFM task lists showed a bullet and a checkbox; the marker is now removed and the checkbox styled. Images are still never loaded, but where one was dropped there is now a placeholder naming its alt text, with the source on hover, instead of nothing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…and back-off on failure Changing a poll's interval restarted its effect and ran the callback at once, so every switch between the fast and idle cadence cost an extra request; it now only re-arms the timer. The llama.cpp status was polled every two seconds for as long as a GGUF model was selected; it is now polled every two seconds only while the runtime is downloading, starting or stopping or a reply is generating, and every fifteen seconds otherwise. A poll that fails three times in a row now doubles its wait up to thirty seconds and returns to normal on the first success, and a callback that rejects is no longer left as an unhandled rejection. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…reduced motion for scripted scrolling The plain transcript treated the last 80px as the bottom while Virtuoso used its 4px default, so following a reply behaved differently either side of forty messages. Both now share one constant. Following streamed output assigned scrollTop on every frame under a stylesheet that smooth-scrolls, restarting an animation each time; it now scrolls instantly and only the reader's own Jump to latest animates. Neither the smooth jump nor Virtuoso's smooth follow honoured prefers-reduced-motion, which only the stylesheet did; a small useMediaQuery hook now lets both fall back to instant scrolling. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…rtcuts ignore AltGr, repeats and IME Escape only stopped a response while the composer had focus, although the shortcuts dialog advertises it without that condition. A page-level handler now stops it while a response is running, leaving Escape to open dialogs, menus and listboxes, to other text fields, and to anything that already handled the key. useHotkey re-registered its listener on every render, fired again for every auto-repeat, fired during IME composition, and matched AltGr (reported as Ctrl+Alt) as Ctrl, so AltGr+K opened the palette while typing and holding Ctrl+K made it flicker. It now holds the handler in a ref, ignores repeats and composition, and requires Alt to be up for a modifier combo. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A screen reader was never told that a response began or ended: the streaming bubble is not a live region (and should not be, or every token would be read), the composer status reverts, and the pending bubble is gone at the first token. A visually hidden polite status now says Response started, Response complete with the token count when known, Response stopped, or Response failed, following the chat store so an outcome published by the stream host is heard whichever page was showing. A failed job's completion after its failure is not announced a second time, and an outcome already waiting when the page opened is not read out again. Includes page-level tests for Escape from the body, from the composer (once), and with an overlay or another field open. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… the theme through system The palette rendered only the eight newest chats, so cmdk could never find an older one although the placeholder promised chat search. With text typed it now searches all chats (scored, best first, at most fifty shown, and paused for 120ms of typing so a long history is not scored on every key), with the heading changing between Recent chats and Chats, and the text is forgotten when the palette closes. Chat rows now carry their id as the cmdk value and their title as a keyword, so several chats with the same title are separate, reachable rows rather than one shared highlight. The theme item cycled system into dark; it now goes system, light, dark and names the theme it would switch to. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…n CSS, and a duplicate defaults table Deletes what has no call site: the useRafBatchedText hook (the stream hook has its own batching helper that is the one in use), the client methods diagnostics, shutdown, executionStatus, startCodeExecution, startScratchCompute, startRecipeImageTransform, stageAttachment and streamExecution with the two tests that only exercised them, and the CSS for .rounded-picker, .input-with-icon, .field-value, .gguf-download-status and .execution-task-capabilities. ChatPage no longer keeps a second copy of the six generation defaults; the composer already falls back to the one in lib/generationParams. streamJob's return type loses a stray void. A new test fails for any class in tokens.css that no component names (with a short, checked allow-list for classes the source builds or a library emits), so the next orphan is caught when it is made. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…e item The command palette item is now Change theme and cycles system, light, dark, so the screenshot script that pressed Toggle theme once to reach light and once to return to dark no longer found the item and would have ended on light. It now presses Change theme once (system, which its light colour-scheme emulation resolves to light) and twice to return to dark. Not run here: it needs the staged showcase server. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
With the runtime polled every fifteen seconds while it is idle or ready, a reply that finished between two polls left the model picker saying the model was not loaded until the next one. A generation that ends now triggers one status check (only while a GGUF model is selected), which also keeps the one-request-per-poll accounting intact: starting a generation only changes the interval. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…le a response runs The page-level Escape rule is tested with synthetic dialog, menu and listbox nodes; this adds a test with the real shortcuts dialog (Base UI) and the real command palette (cmdk), checking that Escape closes each without stopping the response and that the same key stops it once both are closed. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Two textual conflicts, both resolved by keeping both sides: - frontend/src/app/runtimeAvailability.test.ts: keep this branch's llamacppPollInterval suite and main's inventory-loading suite. - frontend/src/styles/tokens.css: keep main's new GGUF picker, toast and new-chat-guide rules; drop the .gguf-download-status rule this branch had already removed as dead, since main's extracted GGUFDownloadForm still does not use it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Main's Undo toast holds a delete that only goes to the backend when the toast closes, so a keypress that closes or acts on it is not free. The page-wide Escape (stop a response from anywhere) now leaves the key alone while focus is inside the toast region (data-keeps-escape), as it already does for dialogs, menus, listboxes and text fields. Tests: the hook ignores a notification target and takes Escape elsewhere; with the real ToastProvider beside ChatPage, Escape on a focused Undo button neither stops the response nor touches the toast, and stops it from anywhere else; the response announcer and an error or status toast are each read once. Also waits for the composer to take focus back before the text-field Escape test focuses its own field, which raced under load. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…etion The palette searches every chat; it must agree with the sidebar while a deleted chat waits out its Undo window. Covers the recent list and search during the window, the chat coming back after Undo, and staying gone after the delete is sent. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Owner
Author
|
Merged How each conflict was resolvedOnly two files conflicted textually; both were resolved by keeping both sides.
Semantic checks on the files that merged cleanly
Tests covering both pull requests' behaviour
Checks run (from
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Nine frontend defects, mostly accessibility and navigation:
useHotkeyre-registered every render, fired on every auto-repeat and during IME composition, and matched AltGr (Ctrl+Alt) as Ctrl, so AltGr+K opened the palette while typing.prefers-reduced-motion.scrollTopon every frame under ascroll-behavior: smoothcontainer, restarting an animation each time.api/,hooks/and the stylesheet.Root cause
Each was a local omission, confirmed against the current code before changing it. The ones worth stating: the palette rendered
chats.slice(0, 8), and cmdk can only filter rendered items;useVisiblePollingputintervalMsin its effect dependencies and ran the callback at the top of the effect, so any interval change was also "start polling"; both poll callbacks swallowed their own errors, so nothing upstream could see a failure; and cmdk derives an item's identity from its text, so untitled chats (all "New Chat") share one highlight.Change
One commit per concern:
formatMessageTime(value, now)adds month and day for an earlier day this year and the year for an earlier year (viewer's locale and zone); the<time>carries the full date and time intitle.localStorage(session-only if storage is blocked); task-list CSS; images are still never loaded but a placeholder names the alt text with the source intitle.useVisiblePollingre-arms its timer on an interval change without running the callback; runs once on being enabled; counts a throw or rejection as a failure and, withbackoff, doubles the wait from the third consecutive failure up to 30 s, resetting on success. llama.cpp is polled every 2 s only while the runtime is downloading, starting or stopping (or a reply is generating) and every 15 s otherwise; one check follows each generation that ends. The execution poll now sees its failures (the callers that refresh after an action keep swallowing them).NEAR_END_PXfor both transcripts (atBottomThresholdon Virtuoso);scrollToBottom(behavior)is instant by default and smooth only for "Jump to latest"; a smalluseMediaQueryhook makes both the smooth jump and Virtuoso's smooth follow fall back under reduced motion.usePageEscapestops a running response from anywhere on the page, but not when something already handled the key, a dialog/menu/listbox is open, focus is in another text field, or the key is a repeat/composition/modified.useHotkeyholds its handler in a ref, ignores repeats and composition, and requires Alt to be up for a modifier combo.ResponseAnnouncer: a visually hidden polite status that says "Response started", "Response complete, N tokens", "Response stopped" or "Response failed", driven by the chat store so an outcome the stream host publishes is heard whichever page was showing. The transcript itself stays non-live, and nothing is announced per token.useRafBatchedText(the stream hook has its own batcher), the client methodsdiagnostics,shutdown,executionStatus,startCodeExecution,startScratchCompute,startRecipeImageTransform,stageAttachment,streamExecutionwith the two tests that only exercised them, ChatPage's second copy of the generation defaults, and the CSS for.rounded-picker,.input-with-icon,.field-value,.gguf-download-status,.execution-task-capabilities.streamJobloses a strayvoid. A new test fails for anytokens.cssclass no component names (with a checked allow-list).tools/screenshots/capture_showcase.mjspressed "Toggle theme"; it now presses "Change theme" and reaches light and back to dark through the new cycle.Compatibility and rollback
No API, contract or persisted-data change. New
localStoragekeycortex.codeWrap(a boolean). The palette item is renamed "Toggle theme" to "Change theme" (two App tests and the screenshot script updated). Deleted client methods mirror backend routes that still exist. Everything reverts withgit revertof the individual commits; none depends on another except that the FE-22 commit needsnextThemePreferencefromlib/theme.ts, which it adds.Checks
Run from the worktree, after the final rebase onto origin/main unless noted:
npm ci,npm run typecheck,npm run lint: clean.npm test -- --run: 62 files, 858 tests passed (baseline before this branch: 58 files, 741 tests).npm run test:coverage: passed (857 tests, measured before the final test-only commit), statements 89.55%, branches 84.43%, functions 88.4%, lines 92.06% (floors 84/79/83/87, not changed).npx playwright testoncommand-palette,virtualized-transcript,accessibility,theme,chatandcomposer: 26 passed. Run before the last three commits (the screenshot script, the post-generation status check, and the real-overlay test) and not repeated after.attachments,chat-groups,executionandreal-apiwere not run.scripts/check.ps1, quick tier) on both pushes: all 11 checks passed (ruff, actionlint/zizmor, mypy, pytest, artifact-boundary review, contract drift, tsc, eslint, vitest). It ran on Python 3.14.0 only; no Python file changed, so the 3.10/3.12/3.13 matrix was not exercised.reports an approval that went through even when the refresh after it fails, which is a guard for the new split of the execution refresh.Security / data-loss / concurrency
title.stopped).Limits
instantscroll behaviour and reduced motion are tested through jsdom, a mockedmatchMediaand Playwright's Chromium.capture_showcase.mjswas only syntax-checked; it needs the staged showcase server.tabIndex={-1}on articles) is not done. Both were outside the scope given for this change.cancelJobandlistHuggingFaceGGUFFiles(the download-cancel and file-listing work will use them),createChatandhealth(client.test.tsuses them as request vehicles),useUiStore.dismissToast(toast work), andLocalSetup/SystemStatusCard(no importer today, but the new-chat screen work may use them;README.mdcontains no sentence about a setup screen, so there was nothing to remove there). Thetheme-*class onAppShellis unused, but dropping its prop means editing the very long<AppShell ...>line inApp.tsx, which parallel chat-deletion work also edits. ChatPage's duplicate generation defaults were the only remaining one;MessageComposeralready imported the shared table.defaultFilter) through a customfilterthat scores chat rows on their title keyword rather than their id value. Debouncing means a match for an older chat appears about 120 ms after typing pauses, during which "No results." is suppressed. The 50-result cap is not announced to the user.usePageEscapeis a role selector (dialog,alertdialog,menu,listbox); a future overlay without one of those roles would need adding. A test with the real Base UI dialog and the cmdk palette pins the two that exist.Plan item: FE-22, FE-37, FE-39, FE-40, FE-18, FE-19, FE-20, FE-13, FE-16
🤖 Generated with Claude Code