Repository navigation
feat: Introduce ScrollableRow - #337
Conversation
Signed-off-by: Ilya Boyandin <[email protected]>
📝 WalkthroughWalkthroughAdds a new public ScrollableRow component and replaces manual horizontal-scrolling in PromptSuggestions and TabStrip with it; introduces per-item/tab/map/query Changes
Sequence DiagramsequenceDiagram
actor User
participant ScrollableRow as ScrollableRow
participant Content as Scroll Container (DOM)
participant Observer as ResizeObserver
User->>ScrollableRow: mount / interact (scroll / click chevron)
ScrollableRow->>Content: attach ref / render children
ScrollableRow->>Observer: observe content size/changes
Observer->>ScrollableRow: notify resize/changes
ScrollableRow->>ScrollableRow: compute canScrollLeft/canScrollRight
User->>ScrollableRow: click chevron (left/right)
ScrollableRow->>Content: scrollBy(direction, amount, smooth)
Content->>ScrollableRow: emit scroll event
ScrollableRow->>User: update chevron visibility/state
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/ui/src/components/tab-strip.tsx (1)
170-246:⚠️ Potential issue | 🟠 MajorMove menu and close controls outside the TabsTrigger button and render as separate
<button>elements.The menu trigger and close control are
spanelements withrole="button"inside aTabsTriggerbutton, which violates W3C HTML and ARIA specifications. The ARIA in HTML spec explicitly forbids interactive-content descendants (including those withrole="button") within elements that haverole="button". This creates WCAG 4.1.2 (Name, Role, Value) failures and causes unreliable behavior with keyboard navigation and screen readers. EventabIndex={-1}does not resolve the nesting violation.Restructure the tab control to use a non-interactive container with separate sibling
<button>elements for the menu trigger and close control, each with properaria-labeland focus styles.
🤖 Fix all issues with AI agents
In `@packages/ui/src/components/scrollable-row.tsx`:
- Around line 76-109: The left/right icon-only buttons in the ScrollableRow
component (buttons that call scrollBy and use canScrollLeft/canScrollRight,
arrowBaseClass and arrowIconClassName with ChevronLeft/ChevronRight) should
include type="button" to avoid default form submissions and an accessible name
via aria-label (e.g., aria-label="Scroll left" / "Scroll right") so screen
readers can announce their purpose; update both button elements to add these
attributes while keeping the existing title and disabled logic.
🧹 Nitpick comments (1)
packages/ui/src/components/scrollable-row.tsx (1)
50-64: Avoid reattaching listeners on everychildrenchange.Including
childrenin the dependency array causes the scroll listener andResizeObserverto be torn down and reattached each timechildrenchanges. React best practices recommend keeping the listener setup stable by depending only on theref, and triggeringupdateScrollState()in a separate effect that depends onchildren. This pattern prevents unnecessary DOM event listener reattachments while still allowing the handler to respond to content changes.
There was a problem hiding this comment.
Actionable comments posted: 0
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/ai-core/src/components/SessionControls.tsx (1)
39-42:⚠️ Potential issue | 🟡 MinorInclude
lastOpenedAtin thetabsmapping to enable correct session recency sorting.The
tabsarray at lines 39-42 maps sessions to{id, name}only, but thegetTabLastOpenedAtcallback at lines 189-191 attempts to accesstab.lastOpenedAt, which will always beundefined. While the TabStrip component has a fallback to use its own internal tracking (getLastOpenedAt(tab.id)), this prevents the persistentlastOpenedAttimestamps from the session objects (which are created and updated in AiSlice) from being used for sorting.Add
lastOpenedAtto thetabsmapping:Fix
const tabs = useMemo( - () => sessions.map((s) => ({id: s.id, name: s.name})), + () => sessions.map((s) => ({id: s.id, name: s.name, lastOpenedAt: s.lastOpenedAt})), [sessions], );Also applies to: 188-191
🧹 Nitpick comments (2)
packages/kepler/src/KeplerSlice.ts (1)
357-362: Consider extracting a helper function for updatinglastOpenedAt.The pattern of finding a map and updating its
lastOpenedAttimestamp is repeated insetCurrentMapId,deleteMap(twice). A small helper could reduce duplication:function updateMapLastOpenedAt( maps: KeplerSliceConfig['maps'], mapId: string ) { const map = maps.find((m) => m.id === mapId); if (map) map.lastOpenedAt = Date.now(); }This is a minor suggestion—the current code is clear and works correctly.
Also applies to: 439-444
packages/ai-core/src/AiSlice.ts (1)
628-628: Consider capturingDate.now()before theproducecallback for consistency.This works correctly, but other methods (
createSession,switchSession) capturenowbefore theproducecall. For consistency, you could follow the same pattern here.♻️ Optional consistency improvement
deleteSession: (sessionId: string) => { // Clean up per-session state const abortController = sessionAbortControllers.get(sessionId); if (abortController) { abortController.abort(SESSION_DELETED); } sessionAbortControllers.delete(sessionId); sessionChatStops.delete(sessionId); sessionChatSendMessages.delete(sessionId); sessionAddToolResults.delete(sessionId); + const now = Date.now(); set((state) => produce(state, (draft) => { // ... existing code ... if (firstSession) { draft.ai.config.currentSessionId = firstSession.id; - firstSession.lastOpenedAt = Date.now(); + firstSession.lastOpenedAt = now; }
Summary by CodeRabbit
New Features
Improvements
Documentation
Kapture.2026-02-03.at.21.57.49.mp4