feat(chat): remove legacy chat.html, React 19 architecture, a11y and UX polish#323
Draft
feat(chat): remove legacy chat.html, React 19 architecture, a11y and UX polish#323
Conversation
- Delete docs/chat.html (4066 lines) — the React console at /admin/chat fully replaces it; the gateway 301 redirect is already in place - Update gateway/docker/npm E2E tests for chat.html removal - Add accessibility: focus-visible outlines on all interactive buttons, focus-within to show message actions on keyboard navigation, prefers-reduced-motion to disable animations, aria-labels on message action buttons, Space key handler on sidebar session items, unique aria-label on mobile chat sidebar trigger - Add mobile polish: 480px small-phone breakpoint with wider bubbles and tighter padding, slash suggestions max-height on mobile, fix sidebar overlay blocking backdrop click - Auto-collapse admin sidebar to icons on the chat page; add tooltips to collapsed sidebar icons; show expand trigger when collapsed
…y, useTransition
- Replace 9 useState calls with a single useReducer (ChatState/ChatAction)
with typed actions: SESSION_SWITCH, SESSION_ID_UPDATE, HISTORY_LOADED,
MESSAGES_SET, ERROR_SET/CLEAR, EDIT_START/CANCEL, APPROVAL_BUSY_SET,
MOBILE_SIDEBAR_TOGGLE, RESET
- Replace useQuery for history with useSuspenseQuery — errors throw to a
new ChatErrorBoundary in the router, removing manual error-state handling
- Add useTransition for session switches with aria-busy={isPending} and
a pulse animation on the active sidebar session during load
- Replace raw validateToken useEffect with useQuery(fetchAppStatus)
- Add prefetchQuery on sidebar session hover for instant switching
- Derive branchInfoMap with useMemo instead of separate useState
- Add sessionItemPending animation with prefers-reduced-motion support
- Simplify useChatStream interface: setSessionId and setError accept
plain (string) => void instead of React.Dispatch<SetStateAction>
… a11y and perf - Reuse existing validateToken from api/client instead of duplicate fetchAppStatus - Fix ChatErrorBoundary: wrap in QueryErrorResetBoundary so "Try again" retries the failed query instead of immediately re-throwing - Add early-exit in buildBranchInfoMap when branchFamilies is empty (avoids O(n) iteration per RAF frame during streaming) - Fix stale isMobile closure in sidebar collapse cleanup - Remove redundant Space key handler on session buttons (native behavior) - Add aria-labels to branch navigation buttons
…ET action - handleNewChat: use SESSION_SWITCH instead of RESET + SESSION_ID_UPDATE (two dispatches caused an intermediate render with stale sessionId) - handleEditSave: use SESSION_SWITCH instead of SESSION_ID_UPDATE when switching to a branch (old messages stayed visible until history loaded) - Remove unused RESET action from reducer (SESSION_SWITCH covers all cases)
Member
Author
Code reviewFound 2 issues:
hybridclaw/console/src/routes/chat/chat-page.tsx Lines 296 to 300 in 17c887e
hybridclaw/console/src/routes/chat/chat-page.tsx Lines 396 to 400 in 17c887e Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
…h in startTransition - Invalidate chat-history query cache in refreshRecent so switching away and back refetches with streamed messages instead of returning stale data - Wrap handleEditSave's SESSION_SWITCH in startTransition to avoid Suspense fallback flash (consistent with handleOpenSession)
- Replace broken <img src="/static/..."> with HybridClaw SVG icon component (matches app shell sidebar pattern, works in Vite dev without gateway proxy) - Fix infinite Suspense remount loop: persist generated sessionId to localStorage inside the useReducer initializer so remounts find the stored value instead of generating a new random ID each time - Make "New Conversation" a solid primary button instead of ghost button - Add composer focus-within border tint
- Add PanelLeft icon component (lucide-react panel-left pattern) - Replace hidden topbar trigger with a visible PanelLeft button in a thin chat header bar — always visible on desktop, toggles admin sidebar between collapsed icons and full labels - On mobile, show hamburger ☰ for chat sidebar instead (PanelLeft hidden via .desktopOnly / .mobileOnly responsive classes) - Remove the aria-expanded CSS hack from styles.css — the chat page now owns its own toggle button
…e persistence, always-visible trigger Sidebar component changes: - Replace Menu (hamburger) icon with PanelLeft in SidebarTrigger - Add localStorage persistence for sidebar open/collapsed state - Add defaultOpen prop to SidebarProvider - setOpen now accepts functional updater ((prev) => next) for toggle - Make topbar trigger always visible (remove mobile-only CSS guard) Chat page simplification: - Remove auto-collapse useEffect and custom PanelLeft toggle — the topbar trigger now handles sidebar toggle on all pages consistently - Remove unused useSidebar/PanelLeft imports from chat-page Test fixes: - Clear localStorage in sidebar test beforeEach to prevent state leaking between tests from the new persistence feature
Move SidebarTrigger from the end of the topbar (far right) to the start (left, adjacent to the sidebar edge). This matches the shadcn/ui pattern where the trigger sits next to the content it controls.
… title
- Move SidebarTrigger from topbar into the sidebar header row (next to
the brand logo) — matches shadcn/ui pattern where the toggle lives
inside the sidebar itself, not in a remote topbar
- When collapsed, the trigger centers in the icon-only sidebar; when
expanded, it sits at the right end of the header row
- Hide the page title ("Chat") on the chat route — the chat page has
its own session sidebar with "HybridClaw" branding, so the topbar
title is redundant. Other pages keep their titles.
- Add topbar-compact class for routes that hide the title
- Clean up: remove topbar-sidebar-trigger CSS, update sidebar test
…age title - Convert chat session history panel to use the Sidebar component system with its own SidebarProvider — matches shadcn/ui pattern with PanelLeft toggle in the sidebar header for collapsing the sessions panel - Remove the "Chat" page title and topbar entirely on the chat route — the chat page fills the full content area - Remove custom mobile sidebar overlay (sidebarOpen, sidebarBackdrop, mobileHeader) — the Sidebar component's built-in Sheet handles mobile - Remove mobileSidebarOpen state and MOBILE_SIDEBAR_TOGGLE action from the chat reducer (the Sidebar component manages its own state) - Simplify chatPage layout from CSS grid to flexbox
- Set chat sidebar --sidebar-width-icon to 0px so it fully hides when collapsed (sessions have no icon-only state, unlike the admin nav) - Wrap chat page in ChatSidebarProvider so the main area can access the sidebar context for the trigger button - Add ChatMainHeader component that shows a PanelLeft trigger when the session sidebar is collapsed or on mobile — tapping it opens the session panel (as Sheet overlay on mobile, inline on desktop)
…etween providers
- SidebarProvider now accepts a storageKey prop (default: 'hybridclaw_sidebar_state')
to control which localStorage key is used for persistence
- Pass storageKey={false} to disable persistence entirely (for transient sidebars)
- Chat session sidebar uses storageKey={false} so it doesn't stomp on the
admin sidebar's persisted state
Add a collapsible prop to the Sidebar component matching the shadcn/ui API: - "icon" (default): collapses to --sidebar-width-icon showing only icons - "offcanvas": collapses to width 0 with hidden content — the sidebar slides fully off-screen - "none": sidebar is not collapsible, always expanded The collapsed mode is exposed via data-collapsible attribute on the <aside> element (only set when collapsed), matching shadcn's pattern. The chat session sidebar now uses collapsible="offcanvas" instead of the --sidebar-width-icon: 0px CSS variable hack.
…ead CSS - Escape the 24px main-panel padding on the chat route so both sidebars extend full viewport height (negative margin on page-content-full) - Override nested SidebarProvider layout div to respect the chat page's flex container instead of forcing min-height: 100vh - Add background: var(--page-bg) to chatMain for clear visual contrast against the sidebar's --sidebar-bg - Remove dead CSS: mobileHeader, sidebarOpen from prefers-reduced-motion
Replace the HybridClaw logo + brand text in the session sidebar header with a simple "Sessions" label. The admin sidebar already shows the brand — duplicating it in the session panel was redundant. Remove unused sidebarLogo, sidebarBrand, chatSidebarBrand CSS classes.
… composer 1. Restore the view-switch capsule nav (Chat/Agents/Admin/GitHub/Docs) on the chat page — was completely hidden, now shows in compact topbar 2. Add Chat entry to admin sidebar navigation under Overview 3. Make session sidebar non-collapsible (collapsible="none") — remove PanelLeft toggle and ChatSidebarProvider/ChatMainHeader 4. Fix session content overflow (add overflow: hidden) 5. Fix composer placeholder vertical centering — use line-height: 36px matching send button height when placeholder shown, switch to line-height: 1.5 with padding when text is entered 6. Remove dead CSS (chatMainHeader, headerButton, sidebarLogo, etc.) 7. Fix JSX nesting error from ChatSidebarProvider removal 8. Update sidebar test for new Chat nav item
- Extract ViewSwitchNav into its own module (view-switch.tsx) to avoid circular import issues with app-shell - Remove the topbar entirely on the chat route in app-shell — the chat page renders the view-switch capsule in its own chatTopbar div - This prevents the capsule from overlapping the message area (caused by page-content-full escaping the main-panel padding while the topbar stayed inside it)
- Add SquarePen icon component (lucide square-pen pattern) - Replace the full-width primary "New Conversation" button with a compact 30px icon button in the session sidebar header - Button sits next to "SESSIONS" label with subtle border, hovers to accent color - Remove ChatSidebarHeader function (inlined into SidebarHeader) - Remove unused ChatSidebarTrigger export
New component with CSS modules, matching our existing design system: Variants: - default — primary background (maps to .primary-button) - ghost — panel background with border (maps to .ghost-button) - outline — transparent with border - danger — danger-soft background (maps to .danger-button) Sizes: - default — 36px min-height, standard padding - sm — 30px min-height, compact padding - icon — 36x36 square, no padding Features: - forwardRef for DOM access - Accepts all standard button HTML attributes - Focus-visible ring matching existing color-mix pattern - SVG icons auto-sized via nested selector - Disabled state with opacity and pointer-events: none - type="button" default (prevents accidental form submission)
Replace raw <button> elements with the new Button component: - EditInline: Save (default) + Cancel (ghost) - Approval actions: Allow buttons (outline, sm) + Deny (danger, sm) - Message actions: Copy, Edit, Regenerate (ghost, icon) - Branch navigation: Previous/Next (ghost, icon) - New conversation: SquarePen icon (outline, icon) The Button component provides consistent focus rings, disabled states, hover effects, and sizing across all chat buttons. Custom CSS overrides (actionButton, branchButton, newChatButton) are kept for size/position adjustments only — the visual variant comes from Button.
Remove ~60 lines of hand-rolled button styles (hover, focus-visible, disabled, background, border) from chat-page.module.css that are now provided by the Button component. Keep only size overrides (actionButton 28px, branchButton 22px) and the success-colored approvalAllow variant. Restore focus-visible on composer attach/send buttons (not yet migrated to Button).
Dashboard's active-match was gated by 'to === /', which never held after the /admin restructure. Prefix matching then flagged Dashboard active on every /admin/* page. Exact matching applies cleanly since all nav items are top-level admin routes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vite base was '/admin/' from when the console lived exclusively under /admin. Now that /chat is a sibling SPA entry point, tying asset URLs to the /admin namespace is incidental. Set base to '/' and split the gateway's console serving into asset-only (/assets/*) vs index-only (/admin, /chat SPA entries). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…itch dynamic Two bugs found via browser testing after the standalone-/chat move: - Chat page rendered blank (error boundary fallback) because Sidebar components lost their provider when AppShell was removed from the chat route. Wrap ChatPage output in ChatSidebarProvider. - View-switch hardcoded /admin as the active view; on /chat it still highlighted Admin. Derive active state from the current pathname. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
maxnoller
pushed a commit
that referenced
this pull request
Apr 18, 2026
Extracted from #323 as a foundation PR. Adds a reusable <Button> primitive with variant/size props and two new lucide-style icons exported from the icons barrel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2 tasks
- View-switch Docs link now points at /docs instead of relying on the legacy /development redirect. - Pick up the formatter's indentation fix for chat-page.tsx after the ChatSidebarProvider wrap. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
maxnoller
pushed a commit
that referenced
this pull request
Apr 18, 2026
Extracted a11y/mobile polish from #323, independent of its larger React 19 refactor: - aria-label on copy/edit/regenerate/branch-nav message actions and the edit textarea, so screen readers announce purpose instead of glyphs - .messageBlock:focus-within reveals the action row during keyboard nav - focus-visible outlines on composer input, attach, and send buttons - .composer:focus-within border highlight - @media (max-width: 480px) small-phone breakpoint: wider bubbles, tighter padding, smaller empty-state heading - @media (prefers-reduced-motion: reduce): disable thinking-dot and sidebar slide-in animations - Cap slash suggestions at 40vh below 900px so they don't cover the whole screen - Fix sidebar overlay blocking backdrop clicks: replace inset: 0 on .sidebarOpen with explicit top/left/bottom so the 280px panel no longer stretches across the viewport Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6 tasks
maxnoller
pushed a commit
that referenced
this pull request
Apr 18, 2026
Extracted from #323. Depends on #358 for the shared Button and the PanelLeft/SquarePen icons consumed by the sidebar header and new view-switch control. Behavior: - Sidebar collapses to an icon-only rail on the chat page and restores to the full layout on navigation away - Collapsed nav items show title tooltips; brand mark gets a tooltip too - When collapsed on desktop, an expand trigger (aria-expanded="false") appears so keyboard users can restore the sidebar - New view-switch component provides a dedicated toggle for chat vs admin surfaces, wired into the router Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced Apr 18, 2026
# Conflicts: # console/src/routes/chat/chat-page.module.css # console/src/routes/chat/chat-page.tsx # docs/chat.html
Main added console/src/components/button/ with a superset Button (loading, render prop, data-disabled/data-loading hooks). The PR's older button.tsx/button.module.css at the same path shadowed it after the merge. Delete them so imports resolve to the enhanced component. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Member
Author
Code reviewFound 1 issue:
hybridclaw/console/src/routes/chat/chat-page.tsx Lines 280 to 290 in 92d39f9 Invalidation that can trigger the throw: hybridclaw/console/src/routes/chat/chat-page.tsx Lines 200 to 210 in 92d39f9 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Reverts historyQuery to useQuery and restores the error-forwarding effect. With useSuspenseQuery, a failed background refetch — triggered by refreshRecent's invalidateQueries after each stream finalize — would throw to ChatErrorBoundary and tear down the whole page, losing composer text, active session, and loaded messages. Errors now show in the inline banner instead. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Admin sidebar is now always expanded on desktop; mobile still uses the sheet. Drops the desktop toggle button and collapsed-state tooltips. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
maxnoller
pushed a commit
that referenced
this pull request
Apr 19, 2026
Revert the logo/wordmark/eyebrow changes in sidebar to avoid conflict with PR #323, which rewrites the same files. The desktop chrome only depends on the data-hc-* attributes in index.tsx, not on the branding changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolve conflicts in chat-page.tsx, chat-sidebar.tsx, chat-page.test.tsx, chat-page.module.css and accept the PR-side deletion of docs/chat.html. Integrate main's session-search feature (query, snippet, debounce, CHAT_UI_CONFIG limits) into the PR's shadcn Sidebar architecture.
Stack chat UX improvements on top of the view-switch nav + non-collapsible admin sidebar work. Adopt PR 360's Link/useRouterState-based ViewSwitchNav (replacing the window.location version from this branch) and pick up the SidebarProvider setOpen bail-out and Trigger/useSidebar exports. Keep the chat ErrorBoundary + offcanvas collapsible mode added here. Mock ViewSwitchNav in chat-page.test.tsx since it now requires router context from the PR 360 Link component.
Drop the ChatErrorBoundary + QueryErrorResetBoundary wrapper, the sidebar 'offcanvas' collapsible mode, and the .page-content-full class that this branch had been carrying. PR 360 is the base — use its versions directly instead of layering this branch's variants on top.
HISTORY_LOADED was clearing state.error unconditionally, which wiped out errors coming from other sources (e.g. the app-status query) as soon as the first history payload resolved. Drop the blanket clear; SESSION_SWITCH already resets error when the user navigates, and historyQuery.error has its own effect that replaces stale history messages.
- Restore .sidebarSearchWrap, .sidebarSearch, .sessionSnippet styles that were dropped during the earlier merge; the search input and snippet render unstyled without them. - Replace inline error coercion in handleEditSave/handleUploadFiles with getErrorMessage (already imported at the top of the file). - Hoist the shared chatSidebarContent wrapper out of the three-way branch in ChatSessionList so the branches return just the inner body.
# Conflicts: # src/gateway/gateway-http-server.ts
…fetches Use shared getErrorMessage() in use-chat-stream, guard hover-prefetch against the active session, and mark the active chat-history query as stale without an immediate refetch on stream completion. Drop decorative banner comments and narrative WHAT comments in chat-page.
SidebarCollapsible is 'icon' | 'none' — nothing in the repo sets data-collapsible="offcanvas", so this rule was dead.
6 tasks
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.
Summary
Legacy cleanup
docs/chat.html(4,066 lines) — the React console at/admin/chatfully replaces it/chatand/chat.htmlto/admin/chatin the gatewayReact 19 architectural improvements
useStatecalls with singleuseReducer(typedChatState/ChatActionwithSESSION_SWITCH,SESSION_ID_UPDATE,HISTORY_LOADED,MESSAGES_SET, etc.)useQueryfor history withuseSuspenseQuery+ChatErrorBoundarywrapped inQueryErrorResetBoundaryfor proper error retryuseTransitionfor session switches (handleOpenSession,handleEditSave) witharia-busyand pulse animation on the active sidebar sessionvalidateTokenuseEffect withuseQuery(fetchAppStatus)(reuses existingvalidateTokenvia re-export)prefetchQueryon sidebar session hover for instant switchingbranchInfoMapwithuseMemoinstead of separateuseStateuseChatStreaminterface:setSessionId/setErroraccept plain(string) => voidchat-historycache after stream completes so switching away and back refetches correctlybuildBranchInfoMapwhenbranchFamiliesis empty (avoids O(n) per RAF frame during streaming)Accessibility
styles.csscolor-mixpattern)focus-withinon.messageBlockto show message actions on keyboard navigationprefers-reduced-motionmedia query to disableslideIn,sessionPulse, andthinkingBounceanimationsaria-labelon message action buttons (copy, edit, regenerate) and branch nav buttons (previous/next)aria-label="Edit message"on the edit textareaaria-label="Open chat sidebar"on mobile hamburger to avoid collision with app shell sidebar triggerMobile polish
max-height: 40vhon mobileinset: 0replaced with explicittop/left/bottomUX improvements
titletooltips to collapsed sidebar nav icons and brand markaria-expanded="false") when collapsed on desktopTest plan
npm --workspace console run test— 165 tests passnpx vitest run tests/gateway-http-server.test.ts— 179 tests pass/chat→ verify 301 redirect to/admin/chat/admin/chat, Tab through all interactive elements → verify visible focus ringsprefers-reduced-motionin devtools → verify no animations