Skip to content

Improve DiffPanel turn chip strip navigation and scrolling UX#77

Merged
juliusmarminge merged 5 commits intomainfrom
codething/c71d8ea1
Feb 20, 2026
Merged

Improve DiffPanel turn chip strip navigation and scrolling UX#77
juliusmarminge merged 5 commits intomainfrom
codething/c71d8ea1

Conversation

@juliusmarminge
Copy link
Copy Markdown
Member

@juliusmarminge juliusmarminge commented Feb 19, 2026

Summary

  • Refactors the DiffPanel turn selector into a dedicated horizontal chip strip with left/right scroll controls.
  • Adds scroll-state awareness to enable/disable navigation buttons and show edge fade indicators when overflow exists.
  • Supports vertical mouse-wheel to horizontal scrolling in the turn strip and auto-scrolls the selected chip into view.
  • Updates turn chip timestamp formatting to a compact time-only display.
  • Hides turn strip scrollbars and contains horizontal overscroll behavior via new .turn-chip-strip styles.

Testing

  • Not run (tests not provided in this change set).
  • Not run (lint status not available in provided context).

Open with Devin

Note

Low Risk
UI-only changes to diff navigation/scroll behavior; primary risk is minor usability regressions or event-handler edge cases across browsers/Electron.

Overview
Refactors the DiffPanel turn selector into a dedicated horizontal chip strip with left/right chevron controls, overflow fade indicators, and scroll-state tracking to enable/disable navigation buttons.

Adds wheel-to-horizontal scrolling, auto-scrolls the selected chip into view, and switches turn timestamps to a compact time-only format. Updates CSS with .turn-chip-strip styles to hide scrollbars and contain horizontal overscroll.

Written by Cursor Bugbot for commit f039878. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • Added a horizontal, scrollable turn-strip in the diff panel with left/right chevrons for navigation.
    • Individual turn chips are selectable and show formatted timestamps; an "All turns" chip remains at the start.
    • Supports wheel-based horizontal scrolling, auto-scroll-to-selection, and responsive scroll syncing on resize/selection.
  • Style

    • UI styling updated to hide scrollbars and prevent horizontal overscroll for a cleaner appearance.

- Add left/right controls, edge fades, and wheel-to-horizontal scrolling for turn chips
- Auto-scroll selected turn chip into view and hide the chip strip scrollbar
- Simplify chip metadata to show compact per-turn time
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 19, 2026

Walkthrough

Replaces the static "All turns" chip in DiffPanel with a horizontal, scrollable turn-strip containing an All-turns chip and per-turn chips, adds left/right chevrons and wheel-based horizontal scrolling, tracks scroll state with a ref and resize/scroll observers, auto-scrolls/highlights the selected chip, and adds CSS to hide the strip scrollbar and contain overscroll.

Changes

Cohort / File(s) Summary
Turn Strip UI
apps/web/src/components/DiffPanel.tsx
Adds a horizontal, scrollable turn-strip UI: left/right chevrons, turnStripRef, canScrollTurnStripLeft / canScrollTurnStripRight flags, updateTurnStripScrollState, scrollTurnStripBy, and onTurnStripWheel. Introduces ResizeObserver and scroll/wheel handlers, per-turn chips with formatTurnChipTimestamp, selection-aware styling, auto-scroll-to-selected behavior, and replaces the previous single "All turns" button. Adjusts layout/drag-region attributes and imports React hooks and chevron icons.
Turn Strip Styling
apps/web/src/index.css
Adds .turn-chip-strip rules to hide native scrollbars across browsers and prevents horizontal overscroll via overscroll-behavior: contain; includes a WebKit scrollbar rule to conceal the scrollbar.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 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 title 'Improve DiffPanel turn chip strip navigation and scrolling UX' directly and specifically describes the main change in the pull request—refactoring the turn selector into a horizontal chip strip with enhanced navigation and scrolling UX, which aligns with all major objectives.

✏️ 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 codething/c71d8ea1

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

@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Feb 19, 2026

Improve DiffPanel turn chip strip navigation and scrolling UX in DiffPanel.tsx

Add horizontal scrolling with hidden scrollbars, wheel-to-horizontal mapping, left/right nav buttons, and auto-scroll for the selected chip in DiffPanel.tsx; render compact locale time for chips; add strip styles in index.css.

📍Where to Start

Start with the turn strip logic and event handlers in DiffPanel within apps/web/src/components/DiffPanel.tsx.


Macroscope summarized f039878.

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Feb 19, 2026

Greptile Summary

Refactored DiffPanel's turn selector into a horizontal scrollable chip strip with left/right chevron navigation, scroll-state indicators, and vertical-wheel-to-horizontal scrolling support.

  • Replaced simple overflow layout with chevron-controlled strip that shows/hides navigation buttons based on scroll position
  • Added ResizeObserver and scroll/wheel event listeners to sync button states and enable wheel-based horizontal navigation
  • Updated turn chip timestamps to compact time-only format via formatTurnChipTimestamp
  • Auto-scrolls selected chip into view when turn selection changes
  • Added .turn-chip-strip CSS to hide scrollbars and contain horizontal overscroll

Issue Found:

  • Event listener stability issue in apps/web/src/components/DiffPanel.tsx:256-273 where onTurnStripWheel callback causes unnecessary listener churn

Confidence Score: 4/5

  • Safe to merge with one event listener optimization recommended
  • UI-only changes with solid scroll handling logic. One performance issue with event listener recreation that should be fixed but won't cause functional bugs. All cleanup logic is correct and memory leaks are prevented.
  • Pay close attention to apps/web/src/components/DiffPanel.tsx for the event listener optimization

Important Files Changed

Filename Overview
apps/web/src/components/DiffPanel.tsx Adds horizontal scrollable turn chip strip with left/right chevron controls, scroll-state tracking, wheel-to-horizontal scrolling, and auto-scroll-to-selection. Contains one potential issue with event listener stability.
apps/web/src/index.css Adds .turn-chip-strip CSS rules to hide scrollbars and contain horizontal overscroll. Clean, straightforward styling changes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Component Mount] --> B[Initialize turnStripRef]
    B --> C[Setup Event Listeners]
    C --> D{User Interaction}
    
    D -->|Scroll| E[onScroll Handler]
    E --> F[updateTurnStripScrollState]
    F --> G[Update canScrollLeft/Right States]
    
    D -->|Vertical Wheel| H[onTurnStripWheel Handler]
    H --> I{scrollWidth > clientWidth?}
    I -->|Yes| J{abs deltaY > abs deltaX?}
    J -->|Yes| K[preventDefault]
    K --> L[scrollBy deltaY horizontally]
    I -->|No| M[Allow Default]
    J -->|No| M
    
    D -->|Click Chevron| N[scrollTurnStripBy offset]
    N --> O[element.scrollBy with smooth]
    O --> E
    
    D -->|Turn Selection Change| P[Auto-scroll Effect]
    P --> Q[Find selected chip]
    Q --> R[scrollIntoView smooth]
    
    S[ResizeObserver] --> F
    
    T[Component Unmount] --> U[Cleanup Listeners]
    U --> V[Disconnect ResizeObserver]
Loading

Last reviewed commit: c00354c

Comment thread apps/web/src/components/DiffPanel.tsx
@cursor

This comment has been minimized.

Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 82-87: The formatTurnChipTimestamp function is calling
Intl.DateTimeFormat(...).format(new Date(isoDate)) without guarding against
invalid dates; update formatTurnChipTimestamp to validate the parsed date (e.g.,
const d = new Date(isoDate); if (Number.isNaN(d.getTime())) return a safe
fallback like an empty string or "-" ) and only call Intl.DateTimeFormat.format
when the date is valid. Keep the function name formatTurnChipTimestamp and
ensure callers (e.g., deriveTurnDiffSummaries and any places using completedAt)
continue to receive a stable string even if the timestamp is malformed.

Comment on lines +82 to +87
function formatTurnChipTimestamp(isoDate: string): string {
return new Intl.DateTimeFormat(undefined, {
hour: "numeric",
minute: "2-digit",
}).format(new Date(isoDate));
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n "useTurnDiffSummaries|TurnDiffSummary|completedAt" --type=ts --type=tsx -C 3

Repository: pingdotgg/codething-mvp

Length of output: 94


🏁 Script executed:

# Also check the DiffPanel.tsx file to see the full context around formatTurnChipTimestamp
head -120 apps/web/src/components/DiffPanel.tsx | tail -50

Repository: pingdotgg/codething-mvp

Length of output: 1594


🏁 Script executed:

# Search for Zod schema definitions in packages/contracts
find packages/contracts -type f \( -name "*.ts" -o -name "*.tsx" \) | head -20

Repository: pingdotgg/codething-mvp

Length of output: 727


🏁 Script executed:

# Search for TurnDiffSummary, useTurnDiffSummaries, completedAt without type flags
rg "TurnDiffSummary|useTurnDiffSummaries|completedAt" -C 3

Repository: pingdotgg/codething-mvp

Length of output: 21964


🏁 Script executed:

# Check DiffPanel.tsx to see how formatTurnChipTimestamp is called
rg "formatTurnChipTimestamp" -C 5

Repository: pingdotgg/codething-mvp

Length of output: 1731


🏁 Script executed:

# Look for provider event schema in ws.ts and provider.ts
cat packages/contracts/src/ws.ts

Repository: pingdotgg/codething-mvp

Length of output: 3163


Guard against invalid completedAt timestamps to avoid runtime errors.

While the current code path guarantees completedAt is a non-empty string (due to filtering in deriveTurnDiffSummaries and Zod validation in persistence), Intl.DateTimeFormat(...).format(new Date(isoDate)) remains unguarded and vulnerable to invalid dates. Add a safe fallback for consistency with defensive date-parsing patterns used elsewhere in the codebase (e.g., store.ts, Sidebar.tsx).

🛡️ Proposed guard
 function formatTurnChipTimestamp(isoDate: string): string {
-  return new Intl.DateTimeFormat(undefined, {
-    hour: "numeric",
-    minute: "2-digit",
-  }).format(new Date(isoDate));
+  const date = new Date(isoDate);
+  if (Number.isNaN(date.getTime())) return "--";
+  return new Intl.DateTimeFormat(undefined, {
+    hour: "numeric",
+    minute: "2-digit",
+  }).format(date);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/DiffPanel.tsx` around lines 82 - 87, The
formatTurnChipTimestamp function is calling Intl.DateTimeFormat(...).format(new
Date(isoDate)) without guarding against invalid dates; update
formatTurnChipTimestamp to validate the parsed date (e.g., const d = new
Date(isoDate); if (Number.isNaN(d.getTime())) return a safe fallback like an
empty string or "-" ) and only call Intl.DateTimeFormat.format when the date is
valid. Keep the function name formatTurnChipTimestamp and ensure callers (e.g.,
deriveTurnDiffSummaries and any places using completedAt) continue to receive a
stable string even if the timestamp is malformed.

juliusmarminge and others added 2 commits February 19, 2026 15:48
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Copy link
Copy Markdown

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 253-260: The wheel handler onTurnStripWheel currently calls
event.preventDefault() but React's onWheel is passive so preventDefault() is
ignored; update the component to attach the listener directly to the DOM node
referenced by turnStripRef using element.addEventListener('wheel', handler, {
passive: false }) inside a useEffect (and remove the onWheel prop if present),
implement the same logic as onTurnStripWheel in that handler (check
turnStripRef.current, scrollWidth <= clientWidth + 1, compare deltaY/deltaX,
call preventDefault(), call element.scrollBy), and ensure you return a cleanup
that removes the event listener to avoid leaks.

---

Duplicate comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 77-82: The function formatTurnChipTimestamp currently calls new
Date(isoDate) and formats it without checking validity; update
formatTurnChipTimestamp to construct the Date once, check its validity (e.g.,
isNaN(date.getTime()) or !isFinite(date.getTime())), and if invalid return a
safe fallback (empty string or a placeholder) instead of calling
Intl.DateTimeFormat.format on an invalid date; otherwise proceed to format the
valid date as before.

Comment thread apps/web/src/components/DiffPanel.tsx Outdated
@juliusmarminge
Copy link
Copy Markdown
Member Author

@greptileai review

Copy link
Copy Markdown

@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

Comment thread apps/web/src/components/DiffPanel.tsx Outdated
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
apps/web/src/components/DiffPanel.tsx (1)

279-285: Consider simplifying the effect dependencies.

The effect depends on both selectedTurn?.turnId and selectedTurnId, but selectedTurn is derived from selectedTurnId. One dependency should suffice.

♻️ Optional simplification
   useEffect(() => {
     const element = turnStripRef.current;
     if (!element) return;
 
     const selectedChip = element.querySelector<HTMLElement>("[data-turn-chip-selected='true']");
     selectedChip?.scrollIntoView({ block: "nearest", inline: "nearest", behavior: "smooth" });
-  }, [selectedTurn?.turnId, selectedTurnId]);
+  }, [selectedTurnId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/DiffPanel.tsx` around lines 279 - 285, The useEffect
in DiffPanel that scrolls the selected chip uses both selectedTurn?.turnId and
selectedTurnId even though selectedTurn is derived from selectedTurnId; remove
the redundant dependency and keep only the source of truth (e.g.,
selectedTurnId) in the dependency array. Update the effect declaration that
references turnStripRef and selectedChip (inside the useEffect callback) to
depend solely on selectedTurnId (or, if you prefer, on selectedTurn but then
remove selectedTurnId) so the effect triggers correctly without
duplicate/derived dependencies.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 70-75: The formatTurnChipTimestamp function can produce "Invalid
Date" for bad or missing timestamps; update formatTurnChipTimestamp(isoDate:
string) to validate the parsed Date (e.g., check isNaN(date.getTime()) or
Number.isNaN(+date)) before formatting and return a safe fallback (empty string
or a localized "Unknown") when invalid; ensure callers that pass completedAt
rely on this safe fallback so the UI never displays "Invalid Date".

---

Nitpick comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 279-285: The useEffect in DiffPanel that scrolls the selected chip
uses both selectedTurn?.turnId and selectedTurnId even though selectedTurn is
derived from selectedTurnId; remove the redundant dependency and keep only the
source of truth (e.g., selectedTurnId) in the dependency array. Update the
effect declaration that references turnStripRef and selectedChip (inside the
useEffect callback) to depend solely on selectedTurnId (or, if you prefer, on
selectedTurn but then remove selectedTurnId) so the effect triggers correctly
without duplicate/derived dependencies.

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issue.

Comment thread apps/web/src/components/DiffPanel.tsx
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (1)
apps/web/src/components/DiffPanel.tsx (1)

246-254: Remove unused onTurnStripWheel callback.

This callback is defined but never used. The wheel event handler is defined inline within the useEffect at lines 263-269 with identical logic. Remove this dead code to avoid duplication and confusion.

🧹 Remove dead code
   const scrollTurnStripBy = useCallback((offset: number) => {
     const element = turnStripRef.current;
     if (!element) return;
     element.scrollBy({ left: offset, behavior: "smooth" });
   }, []);
-  const onTurnStripWheel = useCallback((event: WheelEvent) => {
-    const element = turnStripRef.current;
-    if (!element) return;
-    if (element.scrollWidth <= element.clientWidth + 1) return;
-    if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
-
-    event.preventDefault();
-    element.scrollBy({ left: event.deltaY, behavior: "auto" });
-  }, []);

   useEffect(() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/src/components/DiffPanel.tsx` around lines 246 - 254, Remove the
unused dead callback onTurnStripWheel: it's defined with turnStripRef but never
referenced and duplicates the inline wheel handler inside the useEffect; delete
the onTurnStripWheel definition and any imports/hooks solely used for it so the
component uses only the single inline wheel event logic in the useEffect.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 70-75: The function formatTurnChipTimestamp should validate the
incoming ISO string before formatting to guard against malformed completedAt
values: in formatTurnChipTimestamp(isoDate) parse the string into a Date, check
that date.getTime() is a valid number (not NaN), and if invalid return a safe
fallback (e.g., empty string or a clear placeholder) instead of calling
Intl.DateTimeFormat; otherwise proceed to format and return the localized time.
Ensure you reference formatTurnChipTimestamp and the completedAt input when
adding this guard so callers receive a stable value for malformed timestamps.

---

Nitpick comments:
In `@apps/web/src/components/DiffPanel.tsx`:
- Around line 246-254: Remove the unused dead callback onTurnStripWheel: it's
defined with turnStripRef but never referenced and duplicates the inline wheel
handler inside the useEffect; delete the onTurnStripWheel definition and any
imports/hooks solely used for it so the component uses only the single inline
wheel event logic in the useEffect.

@cursor
Copy link
Copy Markdown
Contributor

cursor bot commented Feb 20, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Unused onTurnStripWheel callback duplicates inline handler
    • Removed the duplicate inline onWheel handler from the useEffect and replaced it with a reference to the existing onTurnStripWheel useCallback, eliminating dead code and deduplicating the logic.

Create PR

Or push these changes by commenting:

@cursor push 432668d253
Preview (432668d253)
diff --git a/apps/web/src/components/DiffPanel.tsx b/apps/web/src/components/DiffPanel.tsx
--- a/apps/web/src/components/DiffPanel.tsx
+++ b/apps/web/src/components/DiffPanel.tsx
@@ -260,26 +260,18 @@
     updateTurnStripScrollState();
     const onScroll = () => updateTurnStripScrollState();
     
-    const onWheel = (event: WheelEvent) => {
-      if (!element) return;
-      if (element.scrollWidth <= element.clientWidth + 1) return;
-      if (Math.abs(event.deltaY) <= Math.abs(event.deltaX)) return;
-      event.preventDefault();
-      element.scrollBy({ left: event.deltaY, behavior: "auto" });
-    };
-
     element.addEventListener("scroll", onScroll, { passive: true });
-    element.addEventListener("wheel", onWheel, { passive: false });
+    element.addEventListener("wheel", onTurnStripWheel, { passive: false });
 
     const resizeObserver = new ResizeObserver(() => updateTurnStripScrollState());
     resizeObserver.observe(element);
 
     return () => {
       element.removeEventListener("scroll", onScroll);
-      element.removeEventListener("wheel", onWheel);
+      element.removeEventListener("wheel", onTurnStripWheel);
       resizeObserver.disconnect();
     };
-  }, [updateTurnStripScrollState]);
+  }, [updateTurnStripScrollState, onTurnStripWheel]);
 
   useEffect(() => {
     updateTurnStripScrollState();

@juliusmarminge
Copy link
Copy Markdown
Member Author

@cursor push 432668d

…useCallback in useEffect

Applied via @cursor push command
@juliusmarminge juliusmarminge merged commit 749a207 into main Feb 20, 2026
3 checks passed
Turn {turnDiffSummaries.length - index}
</span>
<span className="text-[9px] leading-tight opacity-70">
{formatTurnChipTimestamp(summary.completedAt)}
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.

🟡 Medium

components/DiffPanel.tsx:383 formatTurnChipTimestamp can throw if summary.completedAt is invalid. Consider adding a guard similar to inferCheckpointTurnCountByTurnId that checks Number.isNaN(Date.parse(...)) before formatting.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/web/src/components/DiffPanel.tsx around line 383:

`formatTurnChipTimestamp` can throw if `summary.completedAt` is invalid. Consider adding a guard similar to `inferCheckpointTurnCountByTurnId` that checks `Number.isNaN(Date.parse(...))` before formatting.

Evidence trail:
apps/web/src/components/DiffPanel.tsx lines 70-75 (formatTurnChipTimestamp function definition - no validation), line 383 (usage site). apps/web/src/session-logic.ts lines 571-581 (inferCheckpointTurnCountByTurnId with Number.isNaN guard at line 581). Commit f039878edd64cac3cd7b3e5d95d41e228544b3aa.

jjalangtry pushed a commit to jjalangtry/t3code that referenced this pull request Mar 16, 2026
Improve DiffPanel turn chip strip navigation and scrolling UX
smraikai pushed a commit to smraikai/t3code that referenced this pull request Apr 16, 2026
Improve DiffPanel turn chip strip navigation and scrolling UX
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.

2 participants