Skip to content

feat: Introduce ScrollableRow - #337

Merged
ilyabo merged 15 commits into
mainfrom
feat--Introduce-ScrollableRow
Feb 4, 2026
Merged

ilyabo merged 15 commits into
mainfrom
feat--Introduce-ScrollableRow

Conversation

@ilyabo

@ilyabo ilyabo commented Feb 3, 2026 •

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • New reusable horizontal scrolling control with arrow buttons; integrated into tab lists and prompt suggestions.
    • Tab search/dropdown supports grouping and optional recency-sorted results; inline tab editing improved.
  • Improvements

    • Added last-opened timestamps for sessions, maps, and queries to enable recency sorting and tracking.
    • Editable text input adds autofocus, optional select-on-focus, and refined tab-focus behavior.
  • Documentation

    • whats-new updated to document recency sorting option.
Kapture.2026-02-03.at.21.57.49.mp4

@coderabbitai

coderabbitai Bot commented Feb 3, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a new public ScrollableRow component and replaces manual horizontal-scrolling in PromptSuggestions and TabStrip with it; introduces per-item/tab/map/query lastOpenedAt timestamps and propagates recency through TabStrip search; extends EditableText focus/select behavior and updates related UI composition and docs.

Changes

Cohort / File(s) Summary
ScrollableRow Component
packages/ui/src/components/scrollable-row.tsx, packages/ui/src/index.ts
Add exported ScrollableRow: horizontally scrollable container with chevrons, smooth scrolling, ResizeObserver-based visibility state, configurable visibility/styling and scrollRef support.
PromptSuggestions Refactor
packages/ai-core/src/components/PromptSuggestions.tsx
Replace manual scroll logic, refs, ResizeObserver and arrow UI with ScrollableRow; simplify layout, placeholders, and item sizing/positioning.
TabStrip & Search Dropdown
packages/ui/src/components/tab-strip.tsx
Use ScrollableRow for tab list; add per-tab last-opened tracking in context (getLastOpenedAt), richer TabStripSearchDropdown API (empty/search messages, closed-tabs grouping, optional sortSearchItems: 'recent'), and accessibility/controls changes.
EditableText API
packages/ui/src/components/editable-text.tsx
Add props autoFocus?, selectOnFocus? (default false), allowTabFocusWhenNotEditing? (default true); adjust focus/select behavior and tabIndex handling.
SessionControls & Examples
packages/ai-core/src/components/SessionControls.tsx, examples/kepler/src/components/KeplerMapsContainer.tsx
Expose per-session lastOpenedAt into tab data; enable TabStrip.SearchDropdown recency sorting via sortSearchItems="recent" and getTabLastOpenedAt.
lastOpenedAt: Configs, Schemas, Slices
packages/ai-config/src/AiSliceConfig.ts, packages/ai-config/src/schema/AnalysisSessionSchema.ts, packages/kepler/src/KeplerSlice.ts, packages/kepler-config/src/index.ts, packages/sql-editor-config/src/SqlEditorSliceConfig.ts, packages/sql-editor/src/SqlEditorSlice.tsx
Add optional/required lastOpenedAt timestamp fields to session/map/query schemas and configs; initialize new entries with Date.now() and update lastOpenedAt on create/select/switch/delete flows.
Query Editor Tab Composition
packages/sql-editor/src/components/QueryEditorPanelTabsList.tsx
Replace simple TabStrip render with composed SearchDropdown (recency-enabled) + Tabs + NewButton.
Docs
docs/whats-new.md
Document new optional recency sorting for TabStrip search via sortSearchItems="recent" and getTabLastOpenedAt.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • igorDykhta

Poem

"I hopped along a scrolling lane,
Chevrons glint and items name,
Tabs remember when they played,
Timestamps tucked where memories stayed,
A rabbit claps for tidy change. 🐇"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 title accurately reflects the main change: introducing a new ScrollableRow component that is used across multiple files as a reusable scrolling solution.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat--Introduce-ScrollableRow

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 | 🟠 Major

Move menu and close controls outside the TabsTrigger button and render as separate <button> elements.

The menu trigger and close control are span elements with role="button" inside a TabsTrigger button, which violates W3C HTML and ARIA specifications. The ARIA in HTML spec explicitly forbids interactive-content descendants (including those with role="button") within elements that have role="button". This creates WCAG 4.1.2 (Name, Role, Value) failures and causes unreliable behavior with keyboard navigation and screen readers. Even tabIndex={-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 proper aria-label and 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 every children change.

Including children in the dependency array causes the scroll listener and ResizeObserver to be torn down and reattached each time children changes. React best practices recommend keeping the listener setup stable by depending only on the ref, and triggering updateScrollState() in a separate effect that depends on children. This pattern prevents unnecessary DOM event listener reattachments while still allowing the handler to respond to content changes.

Comment thread packages/ui/src/components/scrollable-row.tsx

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Include lastOpenedAt in the tabs mapping to enable correct session recency sorting.

The tabs array at lines 39-42 maps sessions to {id, name} only, but the getTabLastOpenedAt callback at lines 189-191 attempts to access tab.lastOpenedAt, which will always be undefined. While the TabStrip component has a fallback to use its own internal tracking (getLastOpenedAt(tab.id)), this prevents the persistent lastOpenedAt timestamps from the session objects (which are created and updated in AiSlice) from being used for sorting.

Add lastOpenedAt to the tabs mapping:

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 updating lastOpenedAt.

The pattern of finding a map and updating its lastOpenedAt timestamp is repeated in setCurrentMapId, 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 capturing Date.now() before the produce callback for consistency.

This works correctly, but other methods (createSession, switchSession) capture now before the produce call. 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;
                     }

@ilyabo
ilyabo merged commit d1d90cc into main Feb 4, 2026
4 checks passed
@ilyabo
ilyabo deleted the feat--Introduce-ScrollableRow branch February 4, 2026 15:18
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