Skip to content

Conversation

@AnthonyRonning
Copy link
Contributor

@AnthonyRonning AnthonyRonning commented Jan 7, 2026

Implements a simplified pull-to-refresh for the chat history list in the sidebar.

Changes

  • Add pull-to-refresh for conversations list using unified approach
  • Works on all platforms (mobile, desktop, web) with the same behavior
  • Touch events (touchstart/touchmove/touchend) for mobile
  • Mouse click-and-drag (mousedown/mousemove/mouseup) for desktop/web
  • Visual feedback with animated refresh icon that rotates as you pull
  • 60px pull threshold with 0.4 resistance factor
  • No scroll/wheel event handling (simpler, more reliable)

Why this approach?

This is a simplified rewrite of PR #367 which had become too complex with platform-specific behaviors (wheel events for desktop, different handlers for mobile). The complexity led to scroll-related bugs on web.

This version uses one unified approach: click/touch and drag down. It works consistently everywhere and is much easier to maintain.

Fixes #366

Summary by CodeRabbit

  • New Features

    • Pull-to-refresh in chat history — pull down to refresh with a dampened progress indicator and animated refresh icon.
    • Multi-gesture support — works with both touch and mouse for consistent behavior across devices.
    • Smooth content displacement and visual top indicator during pull; refresh animation while updating conversations.
  • Bug Fixes

    • Improved merging and ordering of loaded conversations to reduce reordering artifacts.
  • Chores

    • Sidebar coordinates the chat list container to enable scrolling and refresh behavior.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 7, 2026

📝 Walkthrough

Walkthrough

Adds cross-platform pull-to-refresh to the chat history by accepting an external containerRef, attaching unified touch/mouse handlers to that container, rendering a dampened pull indicator with RefreshCw, and triggering the existing conversations refresh flow. Sidebar provides historyContainerRef to ChatHistoryList. (≤50 words)

Changes

Cohort / File(s) Summary
Pull-to-Refresh Core
frontend/src/components/ChatHistoryList.tsx
Adds containerRef?: React.RefObject<HTMLElement> prop and wires it into the component; implements pull-to-refresh state/refs (isPullRefreshing, pullDistanceRef, etc.), unified touch/mouse gesture handlers (start/move/end), dampened distance math, threshold logic, refresh triggering via handleRefresh, pull indicator DOM (pullIndicatorRef) and spinner (RefreshCw), and wraps list in pullContentRef.
Loading & Merge Logic
frontend/src/components/ChatHistoryList.tsx
Introduces isLoadingMoreRef to guard concurrent loads, adjusts polling/merge logic to prefer server ordering and avoid reordering artifacts, logs polling errors without disrupting UI, and updates effects/cleanup.
UI Restructure / Item Rendering
frontend/src/components/ChatHistoryList.tsx
Reworks rendering to inject top refresh indicator and content wrapper, preserves selection/archived sections and per-item actions while aligning with new pull-to-refresh scaffolding.
Ref Wiring
frontend/src/components/Sidebar.tsx
Adds historyContainerRef attached to the sidebar/nav element and passes it as containerRef to ChatHistoryList.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Nav as Sidebar (nav / historyContainerRef)
  participant Chat as ChatHistoryList (containerRef)
  participant API as ConversationsAPI
  participant UI as Indicator (RefreshCw)

  Note over User,Nav: User pulls down on the sidebar/nav container
  User->>Nav: pointer/touch down + drag
  Nav->>Chat: pointer events observed via containerRef
  Chat->>UI: update pullDistance (dampened transform / opacity)
  alt pullDistance < threshold
    UI-->>User: show partial indicator (translate / opacity)
  else pullDistance ≥ threshold
    Chat->>UI: set isPullRefreshing (start spinner)
    Chat->>API: trigger refresh / poll conversations
    API-->>Chat: return updated conversations
    Chat->>UI: clear refreshing & animate release
    UI-->>User: refreshed list visible
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I tug the list with tiny paw,

A spinning wheel begins to draw.
Down I pull, the indicator sings,
New chats hop in on eager springs. ✨

🚥 Pre-merge checks | ✅ 4
✅ 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 accurately summarizes the main feature addition: implementing pull-to-refresh functionality for the chat history sidebar, which is the primary focus of the changeset.
Linked Issues check ✅ Passed The pull request fully implements the requirement from issue #366 to add pull-to-refresh functionality to the history list, excluding the legacy archived chat type as specified.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the pull-to-refresh feature. The modifications to ChatHistoryList and Sidebar are scoped to this feature with no unrelated alterations.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Jan 7, 2026

Deploying maple with  Cloudflare Pages  Cloudflare Pages

Latest commit: 9d8f01f
Status: ✅  Deploy successful!
Preview URL: https://d298104c.maple-ca8.pages.dev
Branch Preview URL: https://feat-simple-pull-to-refresh.maple-ca8.pages.dev

View logs

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 7, 2026

Greptile Summary

This PR simplifies pull-to-refresh by replacing the complex platform-specific approach from PR #367 with a unified touch/mouse drag implementation that works consistently across all platforms.

Key Changes:

  • Added pull-to-refresh with 60px threshold and 0.4 resistance factor
  • Unified approach using touch events (mobile) and mouse drag (desktop)
  • Visual feedback with animated refresh icon that rotates during pull
  • Uses requestAnimationFrame for smooth performance
  • Properly cancels long-press timers when pull gesture starts
  • Improved conversation merging logic to preserve server ordering

Previous Issues Addressed:

  • Long-press selection conflicts resolved by clearing timer on pull start
  • Text selection prevented via e.preventDefault() during mouse drag
  • Excludes interactive elements (buttons, links) from triggering pull-to-refresh

Confidence Score: 4/5

  • This PR is generally safe to merge with thorough testing on mobile and desktop
  • The implementation addresses previous review concerns about long-press conflicts and text selection. The simplified unified approach reduces complexity. However, mouse drag pull-to-refresh on desktop/web is an unconventional pattern that should be thoroughly tested for UX edge cases.
  • Pay close attention to frontend/src/components/ChatHistoryList.tsx - test mouse drag behavior thoroughly on desktop to ensure it doesn't interfere with normal interactions

Important Files Changed

Filename Overview
frontend/src/components/ChatHistoryList.tsx Implements pull-to-refresh with unified touch/mouse handling. Previous issues with long-press conflicts and text selection have been addressed.
frontend/src/components/Sidebar.tsx Passes containerRef to ChatHistoryList to enable pull-to-refresh. Minimal, clean integration.

Sequence Diagram

sequenceDiagram
    participant User
    participant Container as Sidebar Container
    participant ChatHistoryList
    participant PTR as Pull-to-Refresh Handler
    participant API as pollForUpdates()

    Note over User,API: Touch/Mouse Pull-to-Refresh Flow

    User->>Container: touchstart/mousedown (at top)
    Container->>PTR: handleTouchStart/handleMouseDown
    PTR->>PTR: Clear long-press timer
    PTR->>PTR: Set isPulling=true, pullStartY
    
    User->>Container: touchmove/mousemove (drag down)
    Container->>PTR: handleTouchMove/handleMouseMove
    PTR->>PTR: Calculate distance with resistance
    PTR->>ChatHistoryList: Update pull indicator & content transform
    Note over ChatHistoryList: Visual feedback: icon rotates, content shifts down
    
    User->>Container: touchend/mouseup
    Container->>PTR: handleTouchEnd/handleMouseUp
    PTR->>PTR: Set isPulling=false
    
    alt distance > 60px threshold
        PTR->>PTR: Set isRefreshing=true
        PTR->>ChatHistoryList: Animate to 60px, show spinner
        PTR->>API: Call pollForUpdates()
        API->>API: Fetch latest 20 conversations
        API->>ChatHistoryList: Merge with existing conversations
        API-->>PTR: Complete
        PTR->>ChatHistoryList: Reset to 0px with transition
        PTR->>PTR: Set isRefreshing=false
    else distance <= 60px
        PTR->>ChatHistoryList: Animate back to 0px
    end
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link

@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: 0

🧹 Nitpick comments (2)
frontend/src/components/ChatHistoryList.tsx (2)

184-199: Consider cleanup for the setTimeout on unmount.

If the component unmounts while refreshing, the setTimeout at line 193 will still execute and attempt to update state. While React 18 handles this gracefully, you could store the timeout ID in a ref and clear it on cleanup to avoid potential warnings.

♻️ Optional fix to handle unmount during refresh
+ const refreshTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+
  const handleRefresh = useCallback(async () => {
    isRefreshingRef.current = true;
    setIsPullRefreshing(true);
    try {
      await pollForUpdates();
    } catch (error) {
      console.error("Refresh failed:", error);
    } finally {
-     setTimeout(() => {
+     refreshTimeoutRef.current = setTimeout(() => {
        setIsPullRefreshing(false);
        setPullDistance(0);
        isRefreshingRef.current = false;
      }, 300);
    }
  }, [pollForUpdates]);
+
+ // Add cleanup in a useEffect
+ useEffect(() => {
+   return () => {
+     if (refreshTimeoutRef.current) {
+       clearTimeout(refreshTimeoutRef.current);
+     }
+   };
+ }, []);

240-245: Consider checking for left mouse button only.

handleMouseDown doesn't verify which mouse button was pressed. Right-click (context menu) or middle-click could unintentionally start a pull gesture.

♻️ Proposed fix to check for primary button
 const handleMouseDown = (e: MouseEvent) => {
+  // Only handle left mouse button (button 0)
+  if (e.button !== 0) return;
   if (container.scrollTop === 0 && !isRefreshingRef.current) {
     pullStartY.current = e.clientY;
     isPulling.current = true;
   }
 };
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bad4b01 and 31e8698.

📒 Files selected for processing (2)
  • frontend/src/components/ChatHistoryList.tsx
  • frontend/src/components/Sidebar.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files
Use 2-space indentation, double quotes, and enforce 100-character line limit in TypeScript/React code
Maintain strict TypeScript and avoid using any type
Use PascalCase for component names and camelCase for variables and function names
Use functional components with React hooks instead of class components
Use React context for global state management and TanStack Query for server state management
Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation

Files:

  • frontend/src/components/Sidebar.tsx
  • frontend/src/components/ChatHistoryList.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Greptile Review
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-ios
  • GitHub Check: build-android
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
frontend/src/components/Sidebar.tsx (1)

106-106: LGTM!

The historyContainerRef is correctly created and wired to the <nav> element, then passed down to ChatHistoryList for pull-to-refresh gesture detection. The type assertions are necessary to satisfy TypeScript's strict type checking between MutableRefObject and RefObject.

Also applies to: 257-260, 269-269

frontend/src/components/ChatHistoryList.tsx (4)

9-10: LGTM!

The new imports (CheckSquare, RefreshCw) are appropriately used, and the optional containerRef prop enables pull-to-refresh without breaking existing consumers.

Also applies to: 35-35, 62-63


85-91: Good pattern for gesture state management.

Using refs (pullDistanceRef, isRefreshingRef) alongside state avoids stale closure issues in event handlers while still triggering re-renders when needed.


273-291: Event listener management looks solid.

Good use of passive: false for touchmove to enable preventDefault(), and attaching mousemove/mouseup to window ensures the drag gesture works even when the cursor leaves the container.


706-722: Nice pull-to-refresh indicator implementation.

The indicator provides good visual feedback: height and opacity scale with pull distance, the icon rotates during pull to indicate progress, and switches to animate-spin once refreshing. The opacity reaching 1.0 at the 60px threshold nicely signals the trigger point.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link

@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: 0

🧹 Nitpick comments (1)
frontend/src/components/ChatHistoryList.tsx (1)

710-726: LGTM! Nice visual feedback for pull-to-refresh.

The indicator provides clear visual feedback with smooth transitions, progressive rotation during pull, and spin animation during refresh.

For enhanced accessibility, consider adding a keyboard shortcut (e.g., Cmd/Ctrl+R) or a refresh button in the UI to allow keyboard-only users to trigger a manual refresh. Not required for this PR, but worth considering for inclusive design.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 31e8698 and 1aa6eae.

📒 Files selected for processing (1)
  • frontend/src/components/ChatHistoryList.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files
Use 2-space indentation, double quotes, and enforce 100-character line limit in TypeScript/React code
Maintain strict TypeScript and avoid using any type
Use PascalCase for component names and camelCase for variables and function names
Use functional components with React hooks instead of class components
Use React context for global state management and TanStack Query for server state management
Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation

Files:

  • frontend/src/components/ChatHistoryList.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Greptile Review
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-linux
  • GitHub Check: build-android
  • GitHub Check: build-ios
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (5)
frontend/src/components/ChatHistoryList.tsx (5)

9-10: LGTM! Clean import and prop additions.

The RefreshCw icon import and optional containerRef prop are well-typed and integrate cleanly with the existing component structure.

Also applies to: 35-35


85-91: LGTM! Appropriate state management.

Good separation between state (for UI updates) and refs (for tracking without re-renders). This prevents unnecessary render cycles during gesture tracking.


184-199: LGTM! Solid refresh handler.

The function correctly gates concurrent refreshes with isRefreshingRef, reuses the existing pollForUpdates flow, and ensures UI state resets after 300ms regardless of outcome.


201-295: Excellent implementation of pull-to-refresh gesture handling.

The previous review concerns have been successfully addressed:

  • Primary button check prevents activation on right/middle clicks (line 241)
  • Interactive element filtering prevents interference with buttons, links, and inputs (line 244)
  • preventDefault() on mouse move prevents text selection during drag (line 257)

The unified touch/mouse approach is clean, with proper passive flags for performance, window-level mouse listeners for dragging outside the container, and thorough cleanup.


221-222: Consider verifying the pull threshold in user testing.

The 0.4 resistance factor combined with the 60px threshold means users must pull ~150px raw distance to trigger refresh. While this prevents accidental refreshes, it may feel stiff on mobile devices.

Consider testing with users to ensure the gesture feels natural. If the threshold proves too high, you could:

  • Increase resistance factor to 0.5 (requires 120px raw pull), or
  • Lower threshold to 50px (requires 125px raw pull)

Also applies to: 232-232, 258-259, 269-269

Copy link

@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: 0

🧹 Nitpick comments (1)
frontend/src/components/ChatHistoryList.tsx (1)

184-199: Clean refresh implementation.

The refresh flow correctly manages state and prevents concurrent refreshes using isRefreshingRef. The 300ms delay provides smooth visual feedback.

♻️ Optional: Extract magic number to constant

For maintainability, consider extracting the 300ms delay:

+const REFRESH_ANIMATION_DELAY = 300;
+
 const handleRefresh = useCallback(async () => {
   isRefreshingRef.current = true;
   setIsPullRefreshing(true);
   try {
     await pollForUpdates();
   } catch (error) {
     console.error("Refresh failed:", error);
   } finally {
     setTimeout(() => {
       setIsPullRefreshing(false);
       setPullDistance(0);
       isRefreshingRef.current = false;
-    }, 300);
+    }, REFRESH_ANIMATION_DELAY);
   }
 }, [pollForUpdates]);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1aa6eae and ace6665.

📒 Files selected for processing (1)
  • frontend/src/components/ChatHistoryList.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files
Use 2-space indentation, double quotes, and enforce 100-character line limit in TypeScript/React code
Maintain strict TypeScript and avoid using any type
Use PascalCase for component names and camelCase for variables and function names
Use functional components with React hooks instead of class components
Use React context for global state management and TanStack Query for server state management
Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation

Files:

  • frontend/src/components/ChatHistoryList.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build-android
  • GitHub Check: build-ios
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (6)
frontend/src/components/ChatHistoryList.tsx (6)

9-10: LGTM! Clean import additions for pull-to-refresh.

The RefreshCw icon import is appropriate for the pull-to-refresh indicator.


35-35: Good prop design for external container control.

The optional containerRef prop enables external components to provide a scroll container reference for pull-to-refresh, maintaining good component boundaries.


85-91: Excellent state management approach.

Good use of state for UI-driven values (isPullRefreshing, pullDistance) and refs for tracking gesture state without triggering re-renders. This is a performant pattern.


206-246: Excellent touch event implementation with proper long-press handling.

The touch handlers correctly:

  • Cancel the long-press timer when pull gesture starts (lines 208-211, 225-228) — this addresses the previous concern about conflicts between pull-to-refresh and long-press selection
  • Use preventDefault during active pulls to prevent scrolling interference (line 224)
  • Apply resistance factor (0.4) and maximum distance (80px) for natural pull feedback
  • Check scroll position to only activate at the top

248-291: Well-implemented mouse drag handlers with proper safeguards.

The mouse handlers correctly address previous review concerns:

  • preventDefault added: Line 269 prevents text selection during pull (previously missing)
  • Long-press cancellation: Lines 253-256 and 270-273 cancel the long-press timer when pull gesture starts or continues
  • Interactive element filtering: Line 252 prevents activation on buttons, links, and inputs

The implementation uses the primary mouse button check (line 249) and matches the touch handler behavior with consistent resistance factor and thresholds.


726-742: Polished pull-to-refresh indicator with great visual feedback.

The indicator implementation provides excellent UX:

  • Opacity gradually increases to 1.0 at the 60px threshold (line 732)
  • Icon rotates proportionally to pull distance (pullDistance * 3), giving immediate feedback
  • Switches to spin animation when actively refreshing
  • Uses performant CSS transforms

@AnthonyRonning
Copy link
Contributor Author

@TestFlight build

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

🚀 TestFlight deployment triggered! Check the Actions tab for progress.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

✅ TestFlight deployment completed successfully!

AnthonyRonning and others added 3 commits January 7, 2026 16:50
Adds pull-to-refresh functionality using a unified approach:
- Works on all platforms (mobile, desktop, web) with same behavior
- Touch events for mobile devices
- Mouse click-and-drag for desktop/web
- Visual feedback with animated refresh icon
- 60px pull threshold with resistance factor

Fixes #366

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
- Skip interactive elements (buttons, links, inputs, menu items)
- Only trigger on primary mouse button
- Prevent text selection while dragging

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Prevents conflict between pull-to-refresh and long-press selection mode
by canceling the long-press timer when a pull gesture begins.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@AnthonyRonning AnthonyRonning force-pushed the feat/simple-pull-to-refresh branch from ace6665 to b77cf05 Compare January 7, 2026 22:53
@AnthonyRonning
Copy link
Contributor Author

@TestFlight build

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

🚀 TestFlight deployment triggered! Check the Actions tab for progress.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

2 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Copy link

@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: 2

🤖 Fix all issues with AI agents
In @frontend/src/components/ChatHistoryList.tsx:
- Around line 248-291: handleMouseMove/handleMouseUp fail to reset pullDistance
when the user moves back up past the start, which can leave
pullDistanceRef.current > 0 and wrongly trigger handleRefresh; in
handleMouseMove (function handleMouseMove) add the same upward-movement guard
used for touch handlers: when computed distance <= 0 or currentY <=
pullStartY.current, set pullDistanceRef.current = 0 and call setPullDistance(0)
(and clear any longPressTimerRef.current if present), and in handleMouseUp
ensure you also reset pullDistanceRef.current = 0 and setPullDistance(0) before
returning so no stale distance remains to trigger handleRefresh.
- Around line 206-246: Add a touchcancel handler and ensure pullDistance is
reset when the finger moves back above the start: implement handleTouchCancel
that clears longPressTimerRef, sets isPulling.current = false, sets
pullDistanceRef.current = 0 and calls setPullDistance(0) (mirroring
handleTouchEnd cleanup), and register it alongside handleTouchStart/Move/End in
the event listeners and cleanup. In handleTouchMove, when computing distance, if
distance <= 0 immediately reset pullDistanceRef.current = 0 and call
setPullDistance(0) (and clear any longPressTimerRef) so upward movement clears
the visual indicator and prevents triggering refresh based on peak distance;
keep the existing resistanceFactor, adjustedDistance clamp, and the existing
check for isRefreshingRef.current and container.scrollTop === 0. Ensure all
references use the existing refs/vars: handleTouchStart, handleTouchMove,
handleTouchEnd, longPressTimerRef, pullStartY, isPulling, pullDistanceRef,
setPullDistance, handleRefresh, and isRefreshingRef.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ace6665 and b77cf05.

📒 Files selected for processing (2)
  • frontend/src/components/ChatHistoryList.tsx
  • frontend/src/components/Sidebar.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files
Use 2-space indentation, double quotes, and enforce 100-character line limit in TypeScript/React code
Maintain strict TypeScript and avoid using any type
Use PascalCase for component names and camelCase for variables and function names
Use functional components with React hooks instead of class components
Use React context for global state management and TanStack Query for server state management
Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation

Files:

  • frontend/src/components/ChatHistoryList.tsx
  • frontend/src/components/Sidebar.tsx
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-android
  • GitHub Check: build-ios
  • GitHub Check: Greptile Review
🔇 Additional comments (9)
frontend/src/components/ChatHistoryList.tsx (6)

9-10: LGTM!

The RefreshCw icon import is appropriate for the pull-to-refresh indicator.


35-35: LGTM!

The optional containerRef prop is a clean API extension that enables external control of the scroll container for pull-to-refresh functionality.


85-91: LGTM!

The pull-to-refresh state management appropriately uses React state for values that drive rendering (isPullRefreshing, pullDistance) and refs for tracking gesture state without triggering re-renders.


184-199: LGTM!

The refresh handler properly reuses the existing pollForUpdates mechanism and includes appropriate error handling and a visual feedback delay before clearing the refresh state.


293-311: Event listener setup looks good overall.

The event listener configuration is appropriate:

  • Touch events use correct passive flags
  • Mouse move/up events on window properly track drags that leave the container
  • Cleanup function removes all listeners

However, this is affected by the missing touchcancel handler issue flagged in the previous comment.


734-750: LGTM!

The pull-to-refresh indicator provides excellent visual feedback:

  • Height and opacity scale with pull distance
  • Smooth transitions with transition-all duration-200
  • Appropriate animation states: rotation during pull, spin during refresh
frontend/src/components/Sidebar.tsx (3)

106-106: LGTM!

The historyContainerRef is correctly typed and initialized for forwarding to the ChatHistoryList component.


257-260: LGTM!

The historyContainerRef is correctly attached to the nav element, which is the appropriate scroll container (with overflow-y-auto) for the pull-to-refresh functionality.


269-269: LGTM!

The containerRef prop is correctly forwarded to ChatHistoryList, completing the ref forwarding pattern that enables pull-to-refresh functionality.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 7, 2026

✅ TestFlight deployment completed successfully!

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@AnthonyRonning
Copy link
Contributor Author

@TestFlight build

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

🚀 TestFlight deployment triggered! Check the Actions tab for progress.

Copy link

@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: 3

🤖 Fix all issues with AI agents
In @frontend/src/components/ChatHistoryList.tsx:
- Around line 275-303: The pull distance peak isn't cleared when the user moves
back up, so handleTouchMove must detect distance <= 0 and reset the pull state:
when distance <= 0 (and container.scrollTop <= 0) call setPullDistancePx(0) and
set pullDistanceRef.current = 0 (and optionally set isPulling.current = false)
to clear the stale peak; keep the existing longPressTimerRef and isRefreshingRef
guards. Also ensure handleTouchEnd uses the live pullDistanceRef.current value
(not a stale captured value) when deciding to call handleRefresh.
- Around line 320-348: The mouse move handler (handleMouseMove) currently only
updates pullDistance when distance>0, so if the user moves back above the start
point the peak value remains in pullDistanceRef and can falsely trigger a
refresh; update handleMouseMove to set pullDistance to 0 (via
setPullDistancePx(0)) and clear any longPressTimer when distance <= 0 (and/or
when container.scrollTop > 0) so pullDistanceRef.current is reset to the final
position, ensuring handleMouseUp's check of pullDistanceRef.current uses the
actual final distance; keep references to isPulling, pullStartY,
pullDistanceRef, and setPullDistancePx when implementing this fix.
- Around line 305-318: The pull-to-refresh activates too easily and clicks on
plain divs (conversation titles) can start a pull; update handleMouseDown to
ignore non-touch interactions on text/divs by checking target.closest for
div/textbox (or simply restrict mouse-based initiation), and add a minimum drag
threshold (e.g., 10px) to the gesture logic: when setting pullStartY in
handleMouseDown keep current behavior but ensure handleTouchMove and
handleMouseMove compute delta = currentY - pullStartY.current and only set
isPulling.current = true and call setPullDistancePx(delta) when delta >= 10;
also preserve clearing longPressTimerRef as before. Ensure references:
handleMouseDown, handleTouchMove, handleMouseMove, pullStartY, isPulling,
setPullDistancePx, longPressTimerRef.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b77cf05 and 54b8c8c.

📒 Files selected for processing (2)
  • frontend/src/components/ChatHistoryList.tsx
  • frontend/src/components/Sidebar.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files
Use 2-space indentation, double quotes, and enforce 100-character line limit in TypeScript/React code
Maintain strict TypeScript and avoid using any type
Use PascalCase for component names and camelCase for variables and function names
Use functional components with React hooks instead of class components
Use React context for global state management and TanStack Query for server state management
Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation

Files:

  • frontend/src/components/ChatHistoryList.tsx
  • frontend/src/components/Sidebar.tsx
🧬 Code graph analysis (1)
frontend/src/components/ChatHistoryList.tsx (2)
frontend/src/components/ui/checkbox.tsx (1)
  • Checkbox (26-26)
frontend/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (170-170)
  • DropdownMenuTrigger (171-171)
  • DropdownMenuContent (172-172)
  • DropdownMenuItem (173-173)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Greptile Review
  • GitHub Check: build-android
  • GitHub Check: build-ios
  • GitHub Check: build-linux
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (10)
frontend/src/components/Sidebar.tsx (3)

106-106: LGTM!

The ref is correctly typed and initialized.


257-257: LGTM!

The ref is correctly attached to the scrollable nav container.


266-266: LGTM!

The containerRef prop is correctly forwarded to enable pull-to-refresh functionality.

frontend/src/components/ChatHistoryList.tsx (7)

9-10: LGTM!

Icon imports are correctly added for the new pull-to-refresh and selection features.


35-35: LGTM!

The containerRef prop is correctly typed as optional, maintaining backward compatibility.


85-94: LGTM!

The ref-based approach for pull distance tracking correctly avoids re-render jank during gestures, which is essential for smooth iOS performance.


96-147: LGTM!

The imperative styling approach with RAF scheduling and GPU-accelerated transforms provides smooth pull-to-refresh animations without triggering React re-renders.


241-256: LGTM!

The refresh handler correctly integrates with the existing pollForUpdates mechanism and provides appropriate user feedback.


793-806: LGTM!

The pull indicator is correctly implemented with proper accessibility attributes and performance hints.


808-982: LGTM!

The content wrapper and rendering logic correctly integrate pull-to-refresh while preserving all existing functionality.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

✅ TestFlight deployment completed successfully!

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Copy link

@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: 0

🧹 Nitpick comments (1)
frontend/src/components/ChatHistoryList.tsx (1)

237-253: Consider cleanup for async operations.

The setTimeout callback at lines 247-251 could execute after the component unmounts. While React 18 handles this gracefully (no crash), consider adding a cleanup pattern for a cleaner implementation.

♻️ Optional: Add cleanup for setTimeout
 const handleRefresh = useCallback(async () => {
+  let cancelled = false;
   isRefreshingRef.current = true;
   setIsPullRefreshing(true);
   setPullDistancePx(60, { transition: true });
   try {
     await pollForUpdates();
   } catch (error) {
     console.error("Refresh failed:", error);
   } finally {
     setTimeout(() => {
+      if (cancelled) return;
       setIsPullRefreshing(false);
       isRefreshingRef.current = false;
       setPullDistancePx(0, { transition: true });
     }, 300);
   }
+  return () => { cancelled = true; };
 }, [pollForUpdates, setPullDistancePx]);

Note: This is a minor refinement; the current implementation works fine in practice.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 54b8c8c and 1497562.

📒 Files selected for processing (1)
  • frontend/src/components/ChatHistoryList.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files
Use 2-space indentation, double quotes, and enforce 100-character line limit in TypeScript/React code
Maintain strict TypeScript and avoid using any type
Use PascalCase for component names and camelCase for variables and function names
Use functional components with React hooks instead of class components
Use React context for global state management and TanStack Query for server state management
Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation

Files:

  • frontend/src/components/ChatHistoryList.tsx
🧬 Code graph analysis (1)
frontend/src/components/ChatHistoryList.tsx (2)
frontend/src/components/ui/checkbox.tsx (1)
  • Checkbox (26-26)
frontend/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (170-170)
  • DropdownMenuTrigger (171-171)
  • DropdownMenuContent (172-172)
  • DropdownMenuItem (173-173)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Greptile Review
  • GitHub Check: build-ios
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-linux
  • GitHub Check: build-android
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (8)
frontend/src/components/ChatHistoryList.tsx (8)

9-11: LGTM!

The new imports for CheckSquare and RefreshCw are appropriately added for the pull-to-refresh and selection features.


35-35: LGTM!

The optional containerRef prop is well-typed and provides a clean API for attaching pull-to-refresh to an external scrolling container.


85-94: LGTM!

Well-structured refs for imperative gesture handling. Using refs instead of state for the pull gesture avoids unnecessary re-renders during the drag, and the RAF ref enables proper batching of style updates.


96-147: LGTM!

The imperative style management is well-architected:

  • Uses translate3d for GPU-accelerated animations
  • RAF batching prevents layout thrashing
  • Proper cleanup of animation frames on unmount
  • The transition option in setPullDistancePx provides smooth snapping

201-229: LGTM!

The refactored pollForUpdates logic correctly addresses chat history ordering:

  • Preserves server ordering as the source of truth for the latest page
  • Maintains referential identity for unchanged items to minimize re-renders
  • Retains paginated older items not in the latest response
  • The equality check at line 224-226 is a good optimization to avoid unnecessary state updates

347-367: LGTM!

Event listener setup is well-implemented:

  • Correct use of passive: false for touchmove to allow preventDefault()
  • touchcancel handler reuses handleTouchEnd appropriately
  • mousemove/mouseup on window enables proper drag tracking outside the container
  • All listeners properly cleaned up on effect teardown

790-803: LGTM!

The pull indicator is well-structured:

  • Positioned absolutely with pointer-events-none to not interfere with interactions
  • aria-hidden="true" for proper accessibility
  • willChange hints optimize for the animated properties
  • Initial hidden state (opacity 0, transform -60px) prevents flash on mount

805-817: LGTM!

The content wrapper properly uses willChange: transform for animation performance, and the select-none class on conversation items helps prevent text selection during pull gestures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
@AnthonyRonning
Copy link
Contributor Author

@TestFlight build

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

🚀 TestFlight deployment triggered! Check the Actions tab for progress.

Copy link

@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

🤖 Fix all issues with AI agents
In @frontend/src/components/ChatHistoryList.tsx:
- Around line 279-307: The pull distance peak isn't cleared when the user moves
back above the start, so update handleTouchMove to set pullDistance to 0 when
distance <= 0: when calculating distance in handleTouchMove (inside the block
that checks container.scrollTop), add an else branch that calls
setPullDistancePx(0) and also sets pullDistanceRef.current = 0 (and clear any
longPressTimerRef there as needed) so pullDistanceRef always matches the visible
state; keep existing logic for adjustedDistance when distance > 0 and ensure
handleTouchEnd continues to read pullDistanceRef.current to decide refresh.
🧹 Nitpick comments (1)
frontend/src/components/ChatHistoryList.tsx (1)

244-260: Refresh handler implementation is correct.

The 300ms delay before resetting provides good visual feedback. Consider adding user-visible error feedback (e.g., toast notification) on refresh failure rather than just logging.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1497562 and 9d8f01f.

📒 Files selected for processing (1)
  • frontend/src/components/ChatHistoryList.tsx
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.{ts,tsx}: Use path aliases (@/* maps to ./src/*) for imports in TypeScript/React files
Use 2-space indentation, double quotes, and enforce 100-character line limit in TypeScript/React code
Maintain strict TypeScript and avoid using any type
Use PascalCase for component names and camelCase for variables and function names
Use functional components with React hooks instead of class components
Use React context for global state management and TanStack Query for server state management
Run just format, just lint, and just build after making TypeScript/React changes to ensure code quality and compilation

Files:

  • frontend/src/components/ChatHistoryList.tsx
🧬 Code graph analysis (1)
frontend/src/components/ChatHistoryList.tsx (2)
frontend/src/components/ui/checkbox.tsx (1)
  • Checkbox (26-26)
frontend/src/components/ui/dropdown-menu.tsx (4)
  • DropdownMenu (170-170)
  • DropdownMenuTrigger (171-171)
  • DropdownMenuContent (172-172)
  • DropdownMenuItem (173-173)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Greptile Review
  • GitHub Check: build-android
  • GitHub Check: build-macos (universal-apple-darwin)
  • GitHub Check: build-linux
  • GitHub Check: build-ios
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (10)
frontend/src/components/ChatHistoryList.tsx (10)

27-36: Props interface looks good.

The optional containerRef prop is correctly typed and allows the parent component to provide a container element for pull-to-refresh event handling.


86-95: Good use of refs for imperative animation.

Using refs for pull state (isPulling, pullDistanceRef, isRefreshingRef) avoids unnecessary re-renders during the gesture, while pullRafRef correctly gates RAF scheduling to prevent duplicate frames.


97-148: Well-structured imperative animation logic.

The RAF-gated scheduling, GPU-accelerated transforms (translate3d), and conditional transitions provide smooth pull-to-refresh animations without React re-render overhead.


203-236: Solid deduplication logic with server-ordered merging.

The algorithm correctly maintains server ordering for the latest page while preserving older paginated items. The referential equality optimization (line 216-218) only compares title—if other metadata fields matter, they should be included in the comparison.


354-374: Event listener setup is correct.

The passive: false on touchmove enables preventDefault(), and attaching mousemove/mouseup to window correctly tracks the cursor outside the container. The cleanup is complete.


390-435: Good guard against concurrent pagination requests.

The isLoadingMoreRef prevents duplicate API calls when the IntersectionObserver fires rapidly, and the deduplication logic ensures clean merging.


811-824: Pull indicator UI is well-structured.

Good use of aria-hidden="true" for the decorative indicator, GPU-optimized with willChange, and the animate-spin class provides clear visual feedback during refresh.


826-918: Conversation list rendering is clean.

The select-none class on items prevents accidental text selection during gestures, and the conditional refs/handlers are correctly applied based on mode and position.


920-999: Loading and archived sections implemented correctly.

The loading indicator provides clear pagination feedback, and archived chats correctly omit selection mode support as specified in the issue requirements.


324-352: Same upward movement bug in mouse handlers.

The mouse handlers have the identical issue: moving back above the starting point doesn't reset pullDistanceRef, which can cause incorrect refresh triggers.

Likely an incorrect or invalid review comment.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 8, 2026

✅ TestFlight deployment completed successfully!

@AnthonyRonning AnthonyRonning merged commit 2e016fe into master Jan 8, 2026
9 checks passed
@AnthonyRonning AnthonyRonning deleted the feat/simple-pull-to-refresh branch January 8, 2026 15:42
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.

Pull down to refresh

2 participants