Skip to content

Feat/1958 ai left panel UI elements#1971

Open
DSanich wants to merge 13 commits intomainfrom
feat/1958-ai-left-panel-ui-elements
Open

Feat/1958 ai left panel UI elements#1971
DSanich wants to merge 13 commits intomainfrom
feat/1958-ai-left-panel-ui-elements

Conversation

@DSanich
Copy link
Member

@DSanich DSanich commented Feb 27, 2026

Summary by CodeRabbit

  • New Features
    • New AI assistant sidebar with model selection, message history, suggestions, and chat input (Enter to send / Shift+Enter newline)
    • Resizable desktop sidebar with draggable edge; collapses to a mobile drawer on small screens
    • Message bubbles with distinct user/assistant styling and streaming indicators
    • Layout updated so main content and footer are integrated with the AI left panel
  • Chores
    • Added reusable Drawer and responsive utilities for consistent mobile behavior

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Replaces the previous top-level layout with an AiLeftPanelLayout wrapper that embeds a resizable left AI panel (desktop) and a Drawer-based panel (mobile). Moves Footer inside the new layout and adjusts page chrome and scroll behavior; adds multiple AI panel components, a useIsMobile hook, and a Drawer UI primitive.

Changes

Cohort / File(s) Summary
App Layout
apps/web/src/app/layout.tsx
Replaces prior inline MenuTop/profile block with AiLeftPanelLayout wrapper; nests children and Footer inside the layout and converts top-level container to full-height flex (h-screen, flex-col) altering scroll/ chrome behavior.
Left Panel Layout
packages/epics/src/common/ai-left-panel-layout.tsx, packages/epics/src/common/index.ts
Adds AiLeftPanelLayout with desktop resizable persistent panel and mobile Drawer fallback; exports added to common index.
Left Panel Implementation
packages/epics/src/common/ai-left-panel.tsx, packages/epics/src/common/ai-panel/index.ts, packages/epics/src/common/ai-panel/mock-data.ts
Adds AiLeftPanel component with local state, wiring to mock data and barrel exports for AI panel modules.
AI Panel Subcomponents
packages/epics/src/common/ai-panel/ai-panel-header.tsx, packages/epics/src/common/ai-panel/ai-panel-messages.tsx, packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx, packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx, packages/epics/src/common/ai-panel/ai-panel-suggestions.tsx
Introduces header (model dropdown), messages list, message bubble (bot/user styles, actions), chat input bar (auto-resize, Enter-to-send), and suggestion chips.
Responsive Hook & Re-exports
packages/epics/src/hooks/use-is-mobile.tsx, packages/epics/src/hooks/index.ts
Adds useIsMobile hook (media query for ≤767px) and re-exports it in hooks index.
Drawer UI & Exports
packages/ui/src/drawer.tsx, packages/ui/src/index.ts, packages/ui/package.json
Adds a Vaul-based Drawer primitive with styled subcomponents and re-exports it; adds vaul dependency.
Footer Styling
packages/ui/src/organisms/footer.tsx
Adds w-full class to outer footer container for full-width layout consistency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant AppLayout
    participant AiLeftPanelLayout
    participant useIsMobile
    participant AiLeftPanel
    participant Drawer
    participant DesktopPanel

    User->>AppLayout: Request page
    AppLayout->>AiLeftPanelLayout: Render wrapper
    AiLeftPanelLayout->>useIsMobile: query viewport
    useIsMobile-->>AiLeftPanelLayout: isMobile

    alt Mobile
        AiLeftPanelLayout->>Drawer: render AiLeftPanel inside Drawer
        User->>Drawer: open panel
        Drawer->>AiLeftPanel: show panel (slide in)
    else Desktop
        AiLeftPanelLayout->>DesktopPanel: render persistent left panel
        User->>DesktopPanel: drag resize handle
        DesktopPanel->>DesktopPanel: update panelWidth (clamped)
    end

    User->>AiLeftPanel: interact (select model, type, send)
    AiLeftPanel->>AiLeftPanel: update state (messages, input, suggestions)
    AiLeftPanel->>AiLeftPanelLayout: render updated content
Loading
sequenceDiagram
    participant User
    participant AiLeftPanel
    participant AiPanelHeader
    participant AiPanelMessages
    participant AiPanelChatBar

    User->>AiLeftPanel: mount
    AiLeftPanel->>AiPanelHeader: pass selectedModel, handlers
    AiLeftPanel->>AiPanelMessages: pass messages, suggestions
    AiLeftPanel->>AiPanelChatBar: pass input, onChange, onSend

    User->>AiPanelHeader: open model menu / select
    AiPanelHeader->>AiLeftPanel: onModelSelect
    AiLeftPanel->>AiLeftPanel: set selectedModel

    User->>AiPanelChatBar: type message / press Enter
    AiPanelChatBar->>AiLeftPanel: onSend
    AiLeftPanel->>AiLeftPanel: append message, clear input
    AiLeftPanel->>AiPanelMessages: re-render messages
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • evgenibir
  • sergey3bv
  • plitzenberger

Poem

🐰 I dug a panel, left and neat,
A draggable edge for nimble feet,
On mobiles I tuck, on desktops I span,
Chat bubbles hop — that's my plan! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Feat/1958 ai left panel UI elements' clearly and specifically describes the main change: introducing UI elements for an AI left panel component.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/1958-ai-left-panel-ui-elements

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.

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (6)
packages/epics/src/common/ai-panel/mock-data.ts (1)

23-29: Move shared Message type out of mock-data.ts.

Message is now part of component contracts across files, so keeping it in a mock fixture module creates unnecessary coupling. Please extract it into a shared type module (e.g., ai-panel.types.ts) and import it from both UI components and mock data.

Based on learnings: DSanich prefers extracting repeated type declarations into shared modules for reusability and maintainability, particularly for token types that are used across multiple files in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/mock-data.ts` around lines 23 - 29,
Extract the Message type from the mock module into a new shared types module
(e.g., ai-panel.types.ts): declare and export "Message" there, remove the local
declaration from the mock fixture (mock-data.ts), and replace it with an import
of Message from the new module; then update all UI components and any other
files that previously duplicated the type to import Message from
ai-panel.types.ts so there is a single source of truth.
apps/web/src/app/layout.tsx (1)

137-142: Remove unnecessary fragment wrapper.

The <>...</> fragment wrapping the div and Footer is redundant since AiLeftPanelLayout accepts children as React.ReactNode, which can be multiple elements.

♻️ Suggested simplification
                 <AiLeftPanelLayout>
-                  <>
-                    <div className="w-full shrink-0 pb-8">{children}</div>
-                    <Footer />
-                  </>
+                  <div className="w-full shrink-0 pb-8">{children}</div>
+                  <Footer />
                 </AiLeftPanelLayout>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/app/layout.tsx` around lines 137 - 142, Remove the redundant
fragment wrapping the child elements passed to AiLeftPanelLayout: instead of
passing <> <div className="w-full shrink-0 pb-8">{children}</div> <Footer />
</>, pass the two elements directly as children to AiLeftPanelLayout (the div
and Footer). Update the JSX where AiLeftPanelLayout is used so it contains the
div with {children} and the Footer as sibling children without the <>...</>
fragment.
packages/epics/src/common/ai-left-panel.tsx (1)

23-23: Consider defaulting safely instead of using non-null assertion.

Using ! assumes MOCK_MODEL_OPTIONS is never empty. A safer pattern would handle the potential undefined case, though for mock data this is low risk.

♻️ Optional safer approach
-  const [selectedModel, setSelectedModel] = useState(MOCK_MODEL_OPTIONS[0]!);
+  const [selectedModel, setSelectedModel] = useState(MOCK_MODEL_OPTIONS[0] ?? MOCK_MODEL_OPTIONS[0]);

Or if TypeScript config allows, trust the mock data array will always have at least one element and leave as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-left-panel.tsx` at line 23, The initialization
for selectedModel uses a non-null assertion on MOCK_MODEL_OPTIONS[0]; change it
to safely handle an empty array by providing a fallback or allowing undefined:
initialize selectedModel with MOCK_MODEL_OPTIONS[0] ?? a sensible default model
(or initialize as undefined and update the state type accordingly), and update
any code using selectedModel to handle the possible undefined case; refer to the
useState call that declares selectedModel and setSelectedModel and the
MOCK_MODEL_OPTIONS constant when making the change.
packages/epics/src/common/ai-left-panel-layout.tsx (1)

57-96: Remove unnecessary fragment wrapper.

The <>...</> fragment wrapping the desktop panel content is unnecessary since it's the only child in that conditional branch.

♻️ Suggested simplification
       {!isMobile && (
-        <>
-          <div
-            className={cn(
-              'relative flex h-full flex-shrink-0 flex-col transition-all duration-300',
-              panelOpen ? '' : 'w-0 overflow-hidden',
-            )}
-            style={panelOpen ? { width: panelWidth } : undefined}
-          >
-            <AiLeftPanel onClose={() => setPanelOpen(false)} />
-
-            {panelOpen && (
-              <div
-                role="separator"
-                ...
-              >
-                ...
-              </div>
-            )}
-          </div>
-        </>
+        <div
+          className={cn(
+            'relative flex h-full flex-shrink-0 flex-col transition-all duration-300',
+            panelOpen ? '' : 'w-0 overflow-hidden',
+          )}
+          style={panelOpen ? { width: panelWidth } : undefined}
+        >
+          <AiLeftPanel onClose={() => setPanelOpen(false)} />
+
+          {panelOpen && (
+            <div
+              role="separator"
+              ...
+            >
+              ...
+            </div>
+          )}
+        </div>
       )}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-left-panel-layout.tsx` around lines 57 - 96,
Remove the unnecessary React fragment wrapping the desktop panel branch: replace
the fragment that encloses the top-level div (the element with className built
via cn and style using panelWidth) by directly returning that div when !isMobile
is true; ensure AiLeftPanel, the conditional panelOpen block (with the separator
div using onResizeMouseDown and aria attributes referencing panelWidth), and the
isDragging-based classes remain unchanged.
packages/epics/src/common/ai-panel/ai-panel-header.tsx (2)

27-39: Consider adding listener only when menu is open.

The mousedown listener is added unconditionally and checks showModelMenu inside the handler. For better performance, the listener could be added/removed based on menu state.

♻️ Optional optimization
   useEffect(() => {
+    if (!showModelMenu) return;
+
     const handleClickOutside = (e: MouseEvent) => {
-      if (
-        showModelMenu &&
-        menuRef.current &&
-        !menuRef.current.contains(e.target as Node)
-      ) {
+      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
         setShowModelMenu(false);
       }
     };
     document.addEventListener('mousedown', handleClickOutside);
     return () => document.removeEventListener('mousedown', handleClickOutside);
   }, [showModelMenu]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx` around lines 27 - 39,
Currently the mousedown listener is always attached inside the useEffect and
only checks showModelMenu inside the handler; update the effect (the useEffect
containing handleClickOutside) to add the document.addEventListener('mousedown',
handleClickOutside) only when showModelMenu is true and remove it in the cleanup
(and also when showModelMenu flips to false) so the listener is not attached
unnecessarily; keep using menuRef and setShowModelMenu inside the handler but
ensure the effect's dependency array includes showModelMenu so the listener
lifecycle is tied to the menu state.

89-96: Refresh button has no handler.

The Refresh button renders but has no onClick handler. If this is intentional placeholder behavior, consider adding a TODO comment or disabling the button visually.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx` around lines 89 - 96,
The Refresh button in ai-panel-header.tsx renders without an onClick handler
(the <button> with title/aria-label "Refresh" and the RefreshCw icon), so either
wire it to a real handler or mark it as intentionally inactive: add an onClick
that calls a refresh function (e.g., handleRefresh defined in the component or
passed as an onRefresh prop) to perform the refresh logic, or if it's a
placeholder, add a clear TODO comment and set the button to disabled (and update
aria-disabled/visual styles) so users and screen readers know it’s inactive;
ensure you update any prop types/interfaces if you add an onRefresh prop and
reference the button element and the handler name (handleRefresh or onRefresh)
when making the change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/epics/src/common/ai-left-panel-layout.tsx`:
- Around line 74-79: The conditional class string in ai-left-panel-layout.tsx
contains a duplicate Tailwind class ('hover:bg-primary/20') inside the
isDragging false branch; update the ternary in the className (the expression
with isDragging ? 'bg-primary' : 'hover:bg-primary/20 group
hover:bg-primary/20') to remove the duplicate so it only includes each utility
once (e.g., use 'group hover:bg-primary/20' as the false branch).

In `@packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx`:
- Around line 41-45: The Enter key handler (handleKeyDown) currently calls
onSend even when the input is empty and the Send/Stop button disabling logic
prevents clicking Stop while streaming; update handleKeyDown to only call onSend
when e.key === 'Enter', !e.shiftKey, and the current input (e.g., inputValue or
message) has non-empty trimmed content; change the Send button disabled
expression so it is disabled only when not streaming and the input is empty
(e.g., disabled = !isStreaming && input.trim() === ''), and ensure the Stop
action/button (which calls onStop) is enabled when isStreaming (do not include
isStreaming in the disabled clause for the Stop button).

In `@packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx`:
- Around line 53-80: The icon-only action buttons in ai-panel-message-bubble.tsx
(the <button> elements that render <Copy />, <ThumbsUp />, <ThumbsDown />, and
<RefreshCw />) currently rely only on title for accessibility; add explicit
aria-label attributes to each button (e.g., aria-label="Copy",
aria-label="Thumbs up", aria-label="Thumbs down", aria-label="Refresh" or more
descriptive variants like "Copy message") to match the visible title and provide
reliable screen-reader support.

In `@packages/ui/package.json`:
- Around line 19-21: Update the vaul dependency entry in package.json: replace
the current "vaul": "^0.9.9" with "vaul": "^1.1.2" so the project can pick up
the 1.x releases; after updating the dependency string run your package manager
install (npm/yarn/pnpm) to refresh lockfiles and verify no breaking changes
affect imports or APIs that reference vaul.

---

Nitpick comments:
In `@apps/web/src/app/layout.tsx`:
- Around line 137-142: Remove the redundant fragment wrapping the child elements
passed to AiLeftPanelLayout: instead of passing <> <div className="w-full
shrink-0 pb-8">{children}</div> <Footer /> </>, pass the two elements directly
as children to AiLeftPanelLayout (the div and Footer). Update the JSX where
AiLeftPanelLayout is used so it contains the div with {children} and the Footer
as sibling children without the <>...</> fragment.

In `@packages/epics/src/common/ai-left-panel-layout.tsx`:
- Around line 57-96: Remove the unnecessary React fragment wrapping the desktop
panel branch: replace the fragment that encloses the top-level div (the element
with className built via cn and style using panelWidth) by directly returning
that div when !isMobile is true; ensure AiLeftPanel, the conditional panelOpen
block (with the separator div using onResizeMouseDown and aria attributes
referencing panelWidth), and the isDragging-based classes remain unchanged.

In `@packages/epics/src/common/ai-left-panel.tsx`:
- Line 23: The initialization for selectedModel uses a non-null assertion on
MOCK_MODEL_OPTIONS[0]; change it to safely handle an empty array by providing a
fallback or allowing undefined: initialize selectedModel with
MOCK_MODEL_OPTIONS[0] ?? a sensible default model (or initialize as undefined
and update the state type accordingly), and update any code using selectedModel
to handle the possible undefined case; refer to the useState call that declares
selectedModel and setSelectedModel and the MOCK_MODEL_OPTIONS constant when
making the change.

In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx`:
- Around line 27-39: Currently the mousedown listener is always attached inside
the useEffect and only checks showModelMenu inside the handler; update the
effect (the useEffect containing handleClickOutside) to add the
document.addEventListener('mousedown', handleClickOutside) only when
showModelMenu is true and remove it in the cleanup (and also when showModelMenu
flips to false) so the listener is not attached unnecessarily; keep using
menuRef and setShowModelMenu inside the handler but ensure the effect's
dependency array includes showModelMenu so the listener lifecycle is tied to the
menu state.
- Around line 89-96: The Refresh button in ai-panel-header.tsx renders without
an onClick handler (the <button> with title/aria-label "Refresh" and the
RefreshCw icon), so either wire it to a real handler or mark it as intentionally
inactive: add an onClick that calls a refresh function (e.g., handleRefresh
defined in the component or passed as an onRefresh prop) to perform the refresh
logic, or if it's a placeholder, add a clear TODO comment and set the button to
disabled (and update aria-disabled/visual styles) so users and screen readers
know it’s inactive; ensure you update any prop types/interfaces if you add an
onRefresh prop and reference the button element and the handler name
(handleRefresh or onRefresh) when making the change.

In `@packages/epics/src/common/ai-panel/mock-data.ts`:
- Around line 23-29: Extract the Message type from the mock module into a new
shared types module (e.g., ai-panel.types.ts): declare and export "Message"
there, remove the local declaration from the mock fixture (mock-data.ts), and
replace it with an import of Message from the new module; then update all UI
components and any other files that previously duplicated the type to import
Message from ai-panel.types.ts so there is a single source of truth.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a432be and 8f51f03.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (17)
  • apps/web/src/app/layout.tsx
  • packages/epics/src/common/ai-left-panel-layout.tsx
  • packages/epics/src/common/ai-left-panel.tsx
  • packages/epics/src/common/ai-panel/ai-panel-chat-bar.tsx
  • packages/epics/src/common/ai-panel/ai-panel-header.tsx
  • packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx
  • packages/epics/src/common/ai-panel/ai-panel-messages.tsx
  • packages/epics/src/common/ai-panel/ai-panel-suggestions.tsx
  • packages/epics/src/common/ai-panel/index.ts
  • packages/epics/src/common/ai-panel/mock-data.ts
  • packages/epics/src/common/index.ts
  • packages/epics/src/hooks/index.ts
  • packages/epics/src/hooks/use-is-mobile.tsx
  • packages/ui/package.json
  • packages/ui/src/drawer.tsx
  • packages/ui/src/index.ts
  • packages/ui/src/organisms/footer.tsx

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
packages/epics/src/common/ai-panel/ai-panel-messages.tsx (2)

22-23: Remove placeholder scroll anchor until scroll behavior is implemented.

Line 22 and Line 38 keep an endRef anchor, but there is no effect using it. Either wire auto-scroll behavior now or remove this placeholder to keep the component minimal.

Minimal cleanup
-import { useRef } from 'react';
@@
-  const endRef = useRef<HTMLDivElement>(null);
@@
-      <div ref={endRef} />

Also applies to: 38-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx` around lines 22 -
23, Remove the unused scroll anchor: delete the const endRef =
useRef<HTMLDivElement>(null) declaration and the JSX anchor element that uses
ref={endRef} inside the AiPanelMessages component, and also remove the
now-unused useRef import to avoid linter warnings; leave a short TODO comment if
you want to reintroduce auto-scroll later and run the build to ensure no
unused-symbol errors remain.

7-7: Decouple UI typing from the mock-data module.

Line 7 imports Message from ./mock-data, which makes this production UI component depend on a mock-focused module boundary. Move Message to a shared types module and import it from there.

Proposed refactor
- import type { Message } from './mock-data';
+ import type { Message } from './types';
// packages/epics/src/common/ai-panel/types.ts
export type Message = {
  id: string;
  role: 'user' | 'assistant';
  content: string;
  timestamp: Date;
  isStreaming?: boolean;
};

Based on learnings: DSanich prefers extracting repeated type declarations into shared modules for reusability and maintainability, particularly for token types that are used across multiple files in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx` at line 7, The UI
component imports the Message type from the mock-data module which couples
production UI to mock fixtures; extract the Message type into a shared types
module (e.g., export type Message = { id: string; role: 'user'|'assistant';
content: string; timestamp: Date; isStreaming?: boolean }) and update
ai-panel-messages.tsx to import Message from that new types module instead of
./mock-data; ensure any other files currently importing Message from mock-data
are updated to the new shared types export to keep typings consistent.
packages/epics/src/common/ai-panel/ai-panel-header.tsx (2)

51-61: Add explicit menu semantics for assistive tech.

The model dropdown works visually, but it is missing ARIA menu semantics (aria-expanded, aria-haspopup, menu/menuitem roles), which hurts screen-reader clarity.

Suggested accessibility refinement
           <button
             type="button"
             onClick={() => setShowModelMenu(!showModelMenu)}
+            aria-haspopup="menu"
+            aria-expanded={showModelMenu}
             className="flex items-center gap-1.5 rounded-lg border border-border bg-secondary px-2.5 py-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
           >
@@
-            <div className="absolute left-0 top-full z-50 mt-1 w-44 animate-in fade-in slide-in-from-top-2 rounded-xl border border-border bg-popover py-1 shadow-2xl duration-200">
+            <div
+              role="menu"
+              aria-label="Select model"
+              className="absolute left-0 top-full z-50 mt-1 w-44 animate-in fade-in slide-in-from-top-2 rounded-xl border border-border bg-popover py-1 shadow-2xl duration-200"
+            >
@@
                   <button
                     key={m.id}
                     type="button"
+                    role="menuitemradio"
+                    aria-checked={m.id === selectedModel.id}
                     onClick={() => {

Also applies to: 65-82

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx` around lines 51 - 61,
The model dropdown currently lacks ARIA menu semantics; update the toggle button
(the element using onClick and setShowModelMenu, currently rendering
SelectedIcon and ChevronDown) to include aria-haspopup="menu" and
aria-expanded={showModelMenu} and an id; wrap the conditional menu container
(the div rendered when showModelMenu is true) with role="menu" and
aria-labelledby pointing to the button id; ensure each selectable entry inside
the menu uses role="menuitem" (or menuitemradio/menuitemcheckbox as appropriate)
and keyboard focusability so assistive tech can announce and navigate the menu.

27-39: Avoid attaching a document listener when the menu is closed.

The outside-click listener can be registered only while showModelMenu is true to reduce global event work.

Suggested optimization
   useEffect(() => {
+    if (!showModelMenu) return;
     const handleClickOutside = (e: MouseEvent) => {
-      if (
-        showModelMenu &&
-        menuRef.current &&
-        !menuRef.current.contains(e.target as Node)
-      ) {
+      if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
         setShowModelMenu(false);
       }
     };
     document.addEventListener('mousedown', handleClickOutside);
     return () => document.removeEventListener('mousedown', handleClickOutside);
   }, [showModelMenu]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx` around lines 27 - 39,
The effect currently always registers the document 'mousedown' listener even
when the menu is closed; modify the useEffect (the hook that defines
handleClickOutside) to only add the listener when showModelMenu is true (e.g.,
wrap the addEventListener/removeEventListener logic in an if (showModelMenu)
block or return early when false), keep the same cleanup that removes the
listener, and continue to reference menuRef and setShowModelMenu inside
handleClickOutside to close the menu when a click occurs outside.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx`:
- Around line 87-94: The Refresh button in ai-panel-header.tsx is an interactive
control with no handler (the <button> containing <RefreshCw />), so either wire
it to the component's refresh logic or make it non-interactive; add an onClick
prop that calls the existing refresh/refreshState function or a passed-in prop
like onRefresh (or if none exists, expose a new prop and call the parent
handler), ensure accessibility by keeping aria-label and optionally add disabled
when refresh is unavailable, or replace the element with a non-button if it
should be purely decorative.

---

Nitpick comments:
In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx`:
- Around line 51-61: The model dropdown currently lacks ARIA menu semantics;
update the toggle button (the element using onClick and setShowModelMenu,
currently rendering SelectedIcon and ChevronDown) to include
aria-haspopup="menu" and aria-expanded={showModelMenu} and an id; wrap the
conditional menu container (the div rendered when showModelMenu is true) with
role="menu" and aria-labelledby pointing to the button id; ensure each
selectable entry inside the menu uses role="menuitem" (or
menuitemradio/menuitemcheckbox as appropriate) and keyboard focusability so
assistive tech can announce and navigate the menu.
- Around line 27-39: The effect currently always registers the document
'mousedown' listener even when the menu is closed; modify the useEffect (the
hook that defines handleClickOutside) to only add the listener when
showModelMenu is true (e.g., wrap the addEventListener/removeEventListener logic
in an if (showModelMenu) block or return early when false), keep the same
cleanup that removes the listener, and continue to reference menuRef and
setShowModelMenu inside handleClickOutside to close the menu when a click occurs
outside.

In `@packages/epics/src/common/ai-panel/ai-panel-messages.tsx`:
- Around line 22-23: Remove the unused scroll anchor: delete the const endRef =
useRef<HTMLDivElement>(null) declaration and the JSX anchor element that uses
ref={endRef} inside the AiPanelMessages component, and also remove the
now-unused useRef import to avoid linter warnings; leave a short TODO comment if
you want to reintroduce auto-scroll later and run the build to ensure no
unused-symbol errors remain.
- Line 7: The UI component imports the Message type from the mock-data module
which couples production UI to mock fixtures; extract the Message type into a
shared types module (e.g., export type Message = { id: string; role:
'user'|'assistant'; content: string; timestamp: Date; isStreaming?: boolean })
and update ai-panel-messages.tsx to import Message from that new types module
instead of ./mock-data; ensure any other files currently importing Message from
mock-data are updated to the new shared types export to keep typings consistent.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8f51f03 and b380b0f.

📒 Files selected for processing (4)
  • packages/epics/src/common/ai-panel/ai-panel-header.tsx
  • packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx
  • packages/epics/src/common/ai-panel/ai-panel-messages.tsx
  • packages/epics/src/common/ai-panel/mock-data.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/epics/src/common/ai-panel/mock-data.ts
  • packages/epics/src/common/ai-panel/ai-panel-message-bubble.tsx

Comment on lines +87 to +94
<button
type="button"
className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
title="Refresh"
aria-label="Refresh"
>
<RefreshCw className="h-3.5 w-3.5" />
</button>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Refresh is a no-op clickable control.

Line 90-92 presents an actionable refresh button, but there is no handler. This is confusing UX unless intentionally disabled.

Suggested fix
 type AiPanelHeaderProps = {
   onClose: () => void;
+  onRefresh?: () => void;
   modelOptions: ModelOption[];
   selectedModel: ModelOption;
   onModelSelect: (model: ModelOption) => void;
 };
 
 export function AiPanelHeader({
   onClose,
+  onRefresh,
   modelOptions,
   selectedModel,
   onModelSelect,
 }: AiPanelHeaderProps) {
@@
         <button
           type="button"
+          onClick={onRefresh}
+          disabled={!onRefresh}
           className="flex h-7 w-7 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
           title="Refresh"
           aria-label="Refresh"
         >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/epics/src/common/ai-panel/ai-panel-header.tsx` around lines 87 - 94,
The Refresh button in ai-panel-header.tsx is an interactive control with no
handler (the <button> containing <RefreshCw />), so either wire it to the
component's refresh logic or make it non-interactive; add an onClick prop that
calls the existing refresh/refreshState function or a passed-in prop like
onRefresh (or if none exists, expose a new prop and call the parent handler),
ensure accessibility by keeping aria-label and optionally add disabled when
refresh is unavailable, or replace the element with a non-button if it should be
purely decorative.

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