Skip to content

Add AI dock drawer#978

Open
tannerlinsley wants to merge 5 commits into
mainfrom
taren/ai-dock
Open

Add AI dock drawer#978
tannerlinsley wants to merge 5 commits into
mainfrom
taren/ai-dock

Conversation

@tannerlinsley

@tannerlinsley tannerlinsley commented Jun 12, 2026

Copy link
Copy Markdown
Member

Summary

Adds a persistent AI dock drawer that can be opened from the navbar and used alongside the site without changing page layout. The dock supports hover reopen behavior after it has active content, chat history, compact search results, a draggable minimal width, and a maximized mode capped at a reasonable width.

Notable changes

  • Adds the navbar AI button and lazy-mounted AI dock.
  • Keeps the normal search experience available from the search icon.
  • Adds dock-specific Kapa chat layout, sticky header controls, history/copy/new chat actions, tooltips, and compact search-result controls.
  • Adds persisted minimal dock width with a 50vw resize cap and a 1200px max mode.
  • Hides injected reCAPTCHA/devtools corner UI that conflicted with the dock.

Validation

  • pnpm test:tsc
  • Commit hook ran pnpm run format && pnpm run test; it passed with existing lint warnings unrelated to this change.

Summary by CodeRabbit

  • New Features

    • Introduced AI Dock — resizable, dockable search & chat panel with maximize/minimize controls and persisted sizing
  • Improvements

    • Search controls use icon-only presentation on desktop; added dedicated AI Dock button
    • Chat, search results and input now support compact/dock-aware rendering and internal-link handling
    • Dropdowns accept additional focus/pointer handlers; AI Dock mounts lazily for performance
  • Style

    • Hidden Devtools trigger and invisible reCAPTCHA badge via CSS adjustments

@coderabbitai

coderabbitai Bot commented Jun 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Dockable AI chat added with persisted, resizable width; search/chat panels made surface-aware (modal/dock); SearchContext gains AI-dock lifecycle and hover-close scheduling; navbar integrates an icon-only AiDockButton and lazy-loads the dock.

Changes

AI Dock Feature with Resizable Panel

Layer / File(s) Summary
AI Dock state management
src/contexts/SearchContext.tsx
SearchContextType gains AI dock state fields (isAiDockOpen,isAiDockDirty), control methods, and hover-close scheduling; SearchProvider implements timeout-based hover-close with cleanup and uses hasLoadedSearch for lazy modal rendering.
AiDock component and persistence
src/components/SearchModal.tsx
Dock width helpers (read/write/clamp) use localStorage with SSR fallback. AiDock exported with resizable width, pointer/keyboard resizing, viewport clamping, input focus on open, and renders SearchPanel with surface="dock".
Surface-aware panel refactoring
src/components/SearchModal.tsx
SearchPanel, KapaChatPanel, and KapaUnavailablePanel accept surface and optional maximize controls; layout, spacing, header stickiness, and height rules adapt per surface and dock dirty state is tracked.
Dock UI controls and compact mode
src/components/SearchModal.tsx
Adds dock maximize/minimize controls; CopyChatButton and KapaHistoryButton support compact mode, adjusted labels/styling, timer cleanup, and cancel hover-close on selection.
Message rendering and results in dock
src/components/SearchModal.tsx
KapaWelcome/KapaAnswer accept compact; SafeLink computes internal routing targets; InputBar and SearchResultsInChat accept surface and adapt placeholders, sizing, and header/hide-results UI for dock mode.
Navbar lazy loading and dock mount
src/components/Navbar.tsx
Lazy-loads AiDock via React.lazy; introduces AiDockMount that mounts the lazy dock once isAiDockOpen activates and inserts it into the navbar render tree.
AI dock button and search button variants
src/components/SearchButton.tsx, src/components/Navbar.tsx
SearchButtonProps adds iconOnly; SearchButton supports icon-only rendering. Adds AiGlyph and AiDockButton wired to useSearchContext with hover/click handlers and hover-close scheduling. Navbar uses AiDockButton, SearchButton(iconOnly) on sm+, ThemeToggle, and NavbarCartButton.
UI polish and supporting changes
src/components/ThemeToggle.tsx, src/components/Dropdown.tsx, src/components/AppDevtools.tsx, src/styles/app.css, src/components/SearchModal.tsx
ThemeToggle becomes icon-only. DropdownContent forwards optional onFocus, onPointerEnter, onPointerLeave. AppDevtools configures TanStackDevtools with customTrigger set to a hidden span. CSS hides the reCAPTCHA badge and TanStack Devtools open button. MessageSquarePlus icon import added.
Vite bundling tweak
vite.config.ts
Marks @tanstack/redact and listed client packages as noExternal in RSC and SSR configurations via serverBundledClientPackages to control externalization.
Misc dependency
package.json
Adds @fingerprintjs/fingerprintjs-pro-react to dependencies.
sequenceDiagram
  participant User
  participant AiDockButton
  participant SearchContext
  participant Navbar
  participant AiDock
  participant localStorage

  User->>AiDockButton: hover or click
  AiDockButton->>SearchContext: openAiDock()
  SearchContext-->>Navbar: isAiDockOpen = true
  Navbar->>AiDock: AiDockMount mounts LazyAiDock
  AiDock->>localStorage: readDockWidth()
  localStorage-->>AiDock: persisted width or fallback
  User->>AiDock: resize handle drag
  AiDock->>localStorage: writeDockWidth() on release
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • LadyBluenotes
  • schiller-manuel

🐰 A dock so fine, with width you can resize,
Search and chat cozy, no modal surprise,
Context keeps watch with hover-close care,
Lazy-loaded dock appears only when you dare,
Hidden badges gone — the devtools trigger, rare.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add AI dock drawer' accurately and concisely summarizes the main change: introducing a persistent AI dock drawer component accessible from the navbar.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch taren/ai-dock

Comment @coderabbitai help to get the list of available commands and usage tips.

@tannerlinsley tannerlinsley marked this pull request as ready for review June 13, 2026 04:16

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 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 `@src/components/SearchButton.tsx`:
- Around line 58-106: The button currently tracks hover-open with a local
openedByHoverRef which the AiDock close logic can't see; move the "opened by
hover" mode into the shared search context so hover-close only applies to
hover-opened sessions. Add a boolean state and updater in the search context
(e.g., aiDockOpenedByHover + setAiDockOpenedByHover or an openAiDock(mode:
'hover'|'explicit') API) and update useSearchContext to expose it. In
AiDockButton (replace openedByHoverRef usage) call the new context updater when
opening via hover and clear it on explicit click; continue using
cancelAiDockHoverClose/scheduleAiDockHoverClose but change
scheduleAiDockHoverClose/close logic inside the AiDock component to check the
shared aiDockOpenedByHover flag (or mode) before scheduling/performing
hover-close. Update closeAiDock to clear the hover flag as well.

In `@src/components/SearchModal.tsx`:
- Around line 2928-3186: Split AiDock into its own client-only component file:
create a new component (e.g., AiDock.tsx) and move the AiDock function and only
the helper symbols it directly needs (useSearchContext, readAiDockWidth,
writeAiDockWidth, clampAiDockWidth, getAiDockMaxWidth,
AI_DOCK_MIN_WIDTH/AI_DOCK_MAXIMIZED_WIDTH/constants, and any local hooks/refs)
into that file; keep references to InstantSearch, SearchFiltersProvider,
DynamicFilters, SearchPanel, searchClient, and searchIndexName in the new file
so the component can render independently. In SearchModal.tsx remove the AiDock
implementation and replace usages with an import from the new client file (or a
lazy/dynamic client-only import), ensuring SearchModal no longer imports heavy
dependencies required only by AiDock; keep all other SearchModal internals
unchanged. Ensure exported symbol name remains AiDock and preserve prop/closure
usage of newChatRequestId, isAiDockOpen, cancelAiDockHoverClose,
scheduleAiDockHoverClose, and any state logic that AiDock needs from
useSearchContext so behavior is identical.
- Around line 3123-3183: The off-screen dock remains keyboard-focusable because
only aria-hidden and pointer-events-none are applied; toggle the inert state
when closed so the subtree is removed from the tab order. Modify the mounted
container referenced by contentRef (the div with ref={contentRef}) to set/remove
the inert attribute when isAiDockOpen changes (e.g., in a useEffect:
contentRef.current.inert = !isAiDockOpen), and keep aria-hidden as-is; if you
need to support browsers without native inert, include the inert polyfill or
fallback to programmatically set tabIndex=-1 on interactive descendants inside
the container. Ensure this logic targets the same contentRef/aside subtree so
the search input and buttons become unfocusable when closed.
🪄 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

Run ID: 785dcf10-bb96-455a-bf67-1be391fc0ea7

📥 Commits

Reviewing files that changed from the base of the PR and between 72f9041 and 542b738.

📒 Files selected for processing (8)
  • src/components/AppDevtools.tsx
  • src/components/Dropdown.tsx
  • src/components/Navbar.tsx
  • src/components/SearchButton.tsx
  • src/components/SearchModal.tsx
  • src/components/ThemeToggle.tsx
  • src/contexts/SearchContext.tsx
  • src/styles/app.css

Comment on lines +58 to +106
export function AiDockButton({ className }: { className?: string }) {
const {
cancelAiDockHoverClose,
closeAiDock,
isAiDockDirty,
isAiDockOpen,
openAiDock,
scheduleAiDockHoverClose,
} = useSearchContext()
const openedByHoverRef = React.useRef(false)

const handlePointerEnter = (event: React.PointerEvent<HTMLButtonElement>) => {
if (event.pointerType === 'touch') {
return
}

cancelAiDockHoverClose()

if (isAiDockDirty && !isAiDockOpen) {
openedByHoverRef.current = true
openAiDock()
}
}

const handlePointerLeave = (event: React.PointerEvent<HTMLButtonElement>) => {
if (event.pointerType === 'touch') {
return
}

openedByHoverRef.current = false

if (isAiDockDirty) {
scheduleAiDockHoverClose()
}
}

const handleClick = () => {
if (openedByHoverRef.current) {
openedByHoverRef.current = false
return
}

if (isAiDockOpen) {
closeAiDock()
return
}

openAiDock()
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Track hover-open vs explicit-open separately.

Right now the button only keeps openedByHoverRef locally, while AiDock always schedules a close on panel leave. That means a dock opened intentionally with a click is still treated like a hover session and collapses as soon as the pointer leaves the panel, which breaks the “persistent dock drawer” behavior in this PR. The open mode needs to be shared with the dock close logic so hover-close only applies to hover-opened sessions.

🤖 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 `@src/components/SearchButton.tsx` around lines 58 - 106, The button currently
tracks hover-open with a local openedByHoverRef which the AiDock close logic
can't see; move the "opened by hover" mode into the shared search context so
hover-close only applies to hover-opened sessions. Add a boolean state and
updater in the search context (e.g., aiDockOpenedByHover +
setAiDockOpenedByHover or an openAiDock(mode: 'hover'|'explicit') API) and
update useSearchContext to expose it. In AiDockButton (replace openedByHoverRef
usage) call the new context updater when opening via hover and clear it on
explicit click; continue using cancelAiDockHoverClose/scheduleAiDockHoverClose
but change scheduleAiDockHoverClose/close logic inside the AiDock component to
check the shared aiDockOpenedByHover flag (or mode) before scheduling/performing
hover-close. Update closeAiDock to clear the hover flag as well.

Comment on lines +2928 to +3186
export function AiDock() {
const {
cancelAiDockHoverClose,
isAiDockOpen,
newChatRequestId,
scheduleAiDockHoverClose,
} = useSearchContext()
const [hasActivated, setHasActivated] = React.useState(isAiDockOpen)
const [isDockVisible, setIsDockVisible] = React.useState(false)
const [isDockMaximized, setIsDockMaximized] = React.useState(false)
const [dockWidth, setDockWidth] = React.useState(readAiDockWidth)
const [viewportWidth, setViewportWidth] = React.useState(() =>
typeof window === 'undefined'
? AI_DOCK_DEFAULT_WIDTH / AI_DOCK_MAX_WIDTH_RATIO
: window.innerWidth,
)
const [isResizingDock, setIsResizingDock] = React.useState(false)
const contentRef = React.useRef<HTMLDivElement>(null)
const isResizingDockRef = React.useRef(false)
const displayedDockWidth = clampAiDockWidth(dockWidth, viewportWidth)
const dockMaxWidth = getAiDockMaxWidth(viewportWidth)
const dockStyle: AiDockStyle = {
'--ai-dock-width': `${displayedDockWidth}px`,
'--ai-dock-max-width': `${AI_DOCK_MAXIMIZED_WIDTH}px`,
}

React.useEffect(() => {
if (!isAiDockOpen) {
setIsDockVisible(false)
return
}

setHasActivated(true)

let enterFrame = 0
const mountFrame = requestAnimationFrame(() => {
enterFrame = requestAnimationFrame(() => {
setIsDockVisible(true)
})
})

return () => {
cancelAnimationFrame(mountFrame)
cancelAnimationFrame(enterFrame)
}
}, [isAiDockOpen])

React.useEffect(() => {
if (typeof window === 'undefined') {
return
}

const handleResize = () => {
setViewportWidth(window.innerWidth)
}

handleResize()
window.addEventListener('resize', handleResize)

return () => {
window.removeEventListener('resize', handleResize)
}
}, [])

React.useEffect(() => {
if (!isAiDockOpen || !isDockVisible) {
return
}

const frame = requestAnimationFrame(() => {
contentRef.current
?.querySelector<HTMLInputElement>('input[type="search"]')
?.focus({ preventScroll: true })
})

return () => cancelAnimationFrame(frame)
}, [isAiDockOpen, isDockVisible])

const toggleDockMaximized = React.useCallback(() => {
setIsDockMaximized((current) => !current)
}, [])

const handleResizePointerDown = React.useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
if (isDockMaximized || typeof window === 'undefined') {
return
}

event.preventDefault()
event.stopPropagation()
cancelAiDockHoverClose()

const startX = event.clientX
const startWidth = displayedDockWidth
let nextWidth = startWidth
const previousCursor = document.body.style.cursor
const previousUserSelect = document.body.style.userSelect

isResizingDockRef.current = true
setIsResizingDock(true)
document.body.style.cursor = 'ew-resize'
document.body.style.userSelect = 'none'

const stopResizing = () => {
window.removeEventListener('pointermove', handlePointerMove)
window.removeEventListener('pointerup', stopResizing)
window.removeEventListener('pointercancel', stopResizing)
document.body.style.cursor = previousCursor
document.body.style.userSelect = previousUserSelect
isResizingDockRef.current = false
setIsResizingDock(false)
writeAiDockWidth(nextWidth)
}

const handlePointerMove = (moveEvent: PointerEvent) => {
const currentViewportWidth = window.innerWidth
nextWidth = clampAiDockWidth(
startWidth + startX - moveEvent.clientX,
currentViewportWidth,
)
setViewportWidth(currentViewportWidth)
setDockWidth(nextWidth)
}

window.addEventListener('pointermove', handlePointerMove)
window.addEventListener('pointerup', stopResizing)
window.addEventListener('pointercancel', stopResizing)
},
[cancelAiDockHoverClose, displayedDockWidth, isDockMaximized],
)

const handleResizeKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLDivElement>) => {
if (isDockMaximized) {
return
}

const step = event.shiftKey ? 48 : 16
let nextWidth: number | null = null

if (event.key === 'ArrowLeft') {
nextWidth = displayedDockWidth + step
} else if (event.key === 'ArrowRight') {
nextWidth = displayedDockWidth - step
} else if (event.key === 'Home') {
nextWidth = AI_DOCK_MIN_WIDTH
} else if (event.key === 'End') {
nextWidth = dockMaxWidth
}

if (nextWidth === null) {
return
}

event.preventDefault()

const clampedWidth = clampAiDockWidth(nextWidth, viewportWidth)
setDockWidth(clampedWidth)
writeAiDockWidth(clampedWidth)
},
[displayedDockWidth, dockMaxWidth, isDockMaximized, viewportWidth],
)

const handlePointerEnter = (event: React.PointerEvent<HTMLElement>) => {
if (event.pointerType === 'touch') {
return
}

cancelAiDockHoverClose()
}

const handlePointerLeave = (event: React.PointerEvent<HTMLElement>) => {
if (event.pointerType === 'touch') {
return
}

if (isResizingDockRef.current) {
return
}

if (
event.relatedTarget instanceof Element &&
event.relatedTarget.closest('.dropdown-content')
) {
return
}

scheduleAiDockHoverClose()
}

if (!hasActivated) {
return null
}

return (
<div
ref={contentRef}
aria-label="TanStack AI"
aria-hidden={!isAiDockOpen}
style={dockStyle}
className={twMerge(
'fixed top-[var(--navbar-height)] right-0 bottom-0 z-[1000] w-full max-w-full pointer-events-none',
isDockMaximized
? 'sm:w-[min(var(--ai-dock-max-width),100vw)]'
: 'sm:w-[var(--ai-dock-width)]',
!isResizingDock && 'transition-[width] duration-300 ease-out',
)}
>
<aside
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
className={twMerge(
'pointer-events-auto absolute right-0 top-0 h-full w-full max-w-full text-left outline-none transition-[transform,translate,opacity] duration-300 ease-out',
isDockVisible
? 'translate-x-0 opacity-100'
: 'translate-x-full opacity-0 pointer-events-none',
)}
>
{!isDockMaximized ? (
<div
role="separator"
aria-label="Resize AI panel"
aria-orientation="vertical"
aria-valuemin={AI_DOCK_MIN_WIDTH}
aria-valuemax={dockMaxWidth}
aria-valuenow={displayedDockWidth}
tabIndex={0}
onPointerDown={handleResizePointerDown}
onKeyDown={handleResizeKeyDown}
className="group/resize absolute left-0 top-0 z-40 hidden h-full w-3 -translate-x-1.5 cursor-ew-resize touch-none outline-none sm:block"
>
<div
className={twMerge(
'absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-transparent transition-colors',
isResizingDock
? 'bg-cyan-500/70'
: 'group-hover/resize:bg-cyan-500/50 group-focus/resize:bg-cyan-500/60',
)}
/>
</div>
) : null}
<h2 className="sr-only">TanStack AI</h2>
<InstantSearch searchClient={searchClient} indexName={searchIndexName}>
<SearchFiltersProvider>
<DynamicFilters />
<SearchPanel
isFullHeight
onToggleFullHeight={() => {}}
newChatRequestId={newChatRequestId}
surface="dock"
isDockMaximized={isDockMaximized}
onToggleDockMaximized={toggleDockMaximized}
/>
</SearchFiltersProvider>
</InstantSearch>
</aside>
</div>
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Split AiDock out of SearchModal.tsx before merge.

Exporting the dock from this module makes the navbar lazy import pull src/components/SearchModal.tsx’s full dependency graph into the client-reference bundle. CI is already failing on that bundle with Rolldown failed to resolve import "@fingerprintjs/fingerprintjs-pro-react" from "virtual:vite-rsc/client-references/group/facade:src/components/SearchModal.tsx", so this PR does not build as-is. Move AiDock (and only the shared internals it needs) into a dedicated client component instead of lazy-importing it from the modal module.

🤖 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 `@src/components/SearchModal.tsx` around lines 2928 - 3186, Split AiDock into
its own client-only component file: create a new component (e.g., AiDock.tsx)
and move the AiDock function and only the helper symbols it directly needs
(useSearchContext, readAiDockWidth, writeAiDockWidth, clampAiDockWidth,
getAiDockMaxWidth, AI_DOCK_MIN_WIDTH/AI_DOCK_MAXIMIZED_WIDTH/constants, and any
local hooks/refs) into that file; keep references to InstantSearch,
SearchFiltersProvider, DynamicFilters, SearchPanel, searchClient, and
searchIndexName in the new file so the component can render independently. In
SearchModal.tsx remove the AiDock implementation and replace usages with an
import from the new client file (or a lazy/dynamic client-only import), ensuring
SearchModal no longer imports heavy dependencies required only by AiDock; keep
all other SearchModal internals unchanged. Ensure exported symbol name remains
AiDock and preserve prop/closure usage of newChatRequestId, isAiDockOpen,
cancelAiDockHoverClose, scheduleAiDockHoverClose, and any state logic that
AiDock needs from useSearchContext so behavior is identical.

Source: Pipeline failures

Comment on lines +3123 to +3183
<div
ref={contentRef}
aria-label="TanStack AI"
aria-hidden={!isAiDockOpen}
style={dockStyle}
className={twMerge(
'fixed top-[var(--navbar-height)] right-0 bottom-0 z-[1000] w-full max-w-full pointer-events-none',
isDockMaximized
? 'sm:w-[min(var(--ai-dock-max-width),100vw)]'
: 'sm:w-[var(--ai-dock-width)]',
!isResizingDock && 'transition-[width] duration-300 ease-out',
)}
>
<aside
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
className={twMerge(
'pointer-events-auto absolute right-0 top-0 h-full w-full max-w-full text-left outline-none transition-[transform,translate,opacity] duration-300 ease-out',
isDockVisible
? 'translate-x-0 opacity-100'
: 'translate-x-full opacity-0 pointer-events-none',
)}
>
{!isDockMaximized ? (
<div
role="separator"
aria-label="Resize AI panel"
aria-orientation="vertical"
aria-valuemin={AI_DOCK_MIN_WIDTH}
aria-valuemax={dockMaxWidth}
aria-valuenow={displayedDockWidth}
tabIndex={0}
onPointerDown={handleResizePointerDown}
onKeyDown={handleResizeKeyDown}
className="group/resize absolute left-0 top-0 z-40 hidden h-full w-3 -translate-x-1.5 cursor-ew-resize touch-none outline-none sm:block"
>
<div
className={twMerge(
'absolute left-1/2 top-0 h-full w-px -translate-x-1/2 bg-transparent transition-colors',
isResizingDock
? 'bg-cyan-500/70'
: 'group-hover/resize:bg-cyan-500/50 group-focus/resize:bg-cyan-500/60',
)}
/>
</div>
) : null}
<h2 className="sr-only">TanStack AI</h2>
<InstantSearch searchClient={searchClient} indexName={searchIndexName}>
<SearchFiltersProvider>
<DynamicFilters />
<SearchPanel
isFullHeight
onToggleFullHeight={() => {}}
newChatRequestId={newChatRequestId}
surface="dock"
isDockMaximized={isDockMaximized}
onToggleDockMaximized={toggleDockMaximized}
/>
</SearchFiltersProvider>
</InstantSearch>
</aside>

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remove the closed dock from the tab order.

After the first activation, the dock stays mounted even when isAiDockOpen is false. In the closed state this only applies aria-hidden plus pointer-events-none, so the search input and buttons inside the off-screen panel can still be reached by keyboard navigation. Please make the hidden subtree inert/otherwise unfocusable while closed.

🤖 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 `@src/components/SearchModal.tsx` around lines 3123 - 3183, The off-screen dock
remains keyboard-focusable because only aria-hidden and pointer-events-none are
applied; toggle the inert state when closed so the subtree is removed from the
tab order. Modify the mounted container referenced by contentRef (the div with
ref={contentRef}) to set/remove the inert attribute when isAiDockOpen changes
(e.g., in a useEffect: contentRef.current.inert = !isAiDockOpen), and keep
aria-hidden as-is; if you need to support browsers without native inert, include
the inert polyfill or fallback to programmatically set tabIndex=-1 on
interactive descendants inside the container. Ensure this logic targets the same
contentRef/aside subtree so the search input and buttons become unfocusable when
closed.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
src/components/SearchButton.tsx (1)

69-106: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Dirty dock sessions still can't be pinned open with a mouse.

On desktop, Line 76 hover-opens the dirty dock before the click arrives, so Line 95 always eats that click instead of converting the session into an explicit open. Line 89 then still schedules hover-close on leave, which breaks the persistent drawer behavior this PR is adding. The open intent needs to move into shared search state so both the trigger and the dock close logic can distinguish hover previews from explicit opens.

🤖 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 `@src/components/SearchButton.tsx` around lines 69 - 106, The hover-open vs
explicit-open intent must be moved from the local openedByHoverRef to shared
search state so both the trigger and dock logic can tell a preview from a
user-open; update handlePointerEnter, handlePointerLeave, and handleClick to
stop relying on openedByHoverRef and instead set/clear a shared flag (e.g.,
aiDockOpenIntent or setAiDockExplicitOpen) in the central search state when the
user explicitly opens the dock, and ensure openAiDock/closeAiDock calls consult
that flag: on pointer enter keep preview behavior but do not mark explicit
intent, on click if the dock was only preview-open set the shared explicit-open
flag and call openAiDock to promote it to persistent, and on pointer leave do
not schedule hover-close if the shared explicit-open flag is true (only
schedule/perform hover-close for previews). Also remove/replace openedByHoverRef
usage throughout so both the trigger and dock close logic use the shared
aiDockOpenIntent flag alongside isAiDockDirty/isAiDockOpen, and keep
cancelAiDockHoverClose/scheduleAiDockHoverClose behavior scoped to preview-only
sessions.
🤖 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.

Duplicate comments:
In `@src/components/SearchButton.tsx`:
- Around line 69-106: The hover-open vs explicit-open intent must be moved from
the local openedByHoverRef to shared search state so both the trigger and dock
logic can tell a preview from a user-open; update handlePointerEnter,
handlePointerLeave, and handleClick to stop relying on openedByHoverRef and
instead set/clear a shared flag (e.g., aiDockOpenIntent or
setAiDockExplicitOpen) in the central search state when the user explicitly
opens the dock, and ensure openAiDock/closeAiDock calls consult that flag: on
pointer enter keep preview behavior but do not mark explicit intent, on click if
the dock was only preview-open set the shared explicit-open flag and call
openAiDock to promote it to persistent, and on pointer leave do not schedule
hover-close if the shared explicit-open flag is true (only schedule/perform
hover-close for previews). Also remove/replace openedByHoverRef usage throughout
so both the trigger and dock close logic use the shared aiDockOpenIntent flag
alongside isAiDockDirty/isAiDockOpen, and keep
cancelAiDockHoverClose/scheduleAiDockHoverClose behavior scoped to preview-only
sessions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 4efce74e-51dc-46a9-925e-cc6affb0ae14

📥 Commits

Reviewing files that changed from the base of the PR and between a5fb18b and 21808a3.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (10)
  • package.json
  • src/components/AppDevtools.tsx
  • src/components/Dropdown.tsx
  • src/components/Navbar.tsx
  • src/components/SearchButton.tsx
  • src/components/SearchModal.tsx
  • src/components/ThemeToggle.tsx
  • src/contexts/SearchContext.tsx
  • src/styles/app.css
  • vite.config.ts
🚧 Files skipped from review as they are similar to previous changes (9)
  • vite.config.ts
  • src/components/Dropdown.tsx
  • src/components/AppDevtools.tsx
  • package.json
  • src/components/Navbar.tsx
  • src/contexts/SearchContext.tsx
  • src/styles/app.css
  • src/components/ThemeToggle.tsx
  • src/components/SearchModal.tsx

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant