Skip to content

fix(ui): hide add-note affordance while scrolling#328

Merged
benvinegar merged 1 commit into
mainfrom
fix/hide-add-note-on-scroll
May 19, 2026
Merged

fix(ui): hide add-note affordance while scrolling#328
benvinegar merged 1 commit into
mainfrom
fix/hide-add-note-on-scroll

Conversation

@benvinegar
Copy link
Copy Markdown
Member

Summary

  • hide the hover-only [+] add-note badge during mouse wheel scrolling
  • restore the badge after the short scroll suppression window if the hover idle timeout is still active
  • keep the active add-note target updated while the badge is visually suppressed

Tests

  • bun run typecheck
  • bun test src/ui/components/ui-components.test.tsx
  • bun test src/ui/components/ui-components.test.tsx -t "DiffPane hides the hover add-note"
  • bun run lint

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR suppresses the hover-only [+] add-note badge during mouse-wheel scrolling and restores it after a 160 ms window if the 2 s idle timeout is still active, preventing the badge from flickering across rows as the diff scrolls.

  • DiffPane: adds addNoteHoverSuppressed state and suppressAddNoteHoverForMouseScroll callback; injects both the suppression flag and the scroll handler into each DiffSection.
  • DiffSection / PierreDiffView: threads hoverSuppressed down to the render layer, where showAddNoteBadge is gated by !hoverSuppressed; hover state and affordance are still tracked while suppressed so the badge can be restored correctly.
  • Test: adds an integration test that scrolls, confirms the badge disappears, and confirms it reappears after the suppression window expires.

Confidence Score: 3/5

Safe to review further; the core scroll-hiding behavior works correctly for the common single-row case, but multi-row hover during the suppression window desynchronizes the parent affordance from the still-visible badge.

The new idle-timeout logic in the suppressed branch only creates a single timeout anchored to the first hovered row. If the mouse crosses additional rows within the 160 ms suppression window, that first timeout fires 2 s later and unconditionally calls onActiveAddNoteAffordanceChange?.(null), clearing the parent's record of the active row even though hoveredRowKey has moved on and the badge is still visible.

src/ui/diff/PierreDiffView.tsx — the suppressed branch of activateHoveredRow and its idle-timeout callback need a second look for the multi-row scenario.

Important Files Changed

Filename Overview
src/ui/diff/PierreDiffView.tsx Adds hoverSuppressed prop and tracks idle deadline/last-hovered refs; the suppressed code path has a desync bug where the idle timeout for the first hovered row unconditionally clears the parent affordance even when hoveredRowKey has moved to a later row.
src/ui/components/panes/DiffPane.tsx Cleanly adds scroll-suppression state and wires suppressAddNoteHoverForMouseScroll into the existing mouse-wheel handler; cleanup effect on unmount is correctly included.
src/ui/components/panes/DiffSection.tsx Adds hoverSuppressed/onMouseScroll props and memo comparison for hoverSuppressed, but onMouseScroll is omitted from the custom comparator (harmless today due to stable callback, but inconsistent).
src/ui/components/ui-components.test.tsx Adds a focused integration test covering scroll suppression and badge restoration; covers the single-row happy path but does not exercise the multi-row hover-during-suppression scenario.

Sequence Diagram

sequenceDiagram
    participant User
    participant DiffPane
    participant DiffSection
    participant PierreDiffView

    User->>DiffPane: wheel scroll
    DiffPane->>DiffPane: "suppressAddNoteHoverForMouseScroll()<br/>(sets addNoteHoverSuppressed=true, starts 160ms timer)"
    DiffPane->>DiffSection: "hoverSuppressed=true"
    DiffSection->>PierreDiffView: "hoverSuppressed=true"

    User->>PierreDiffView: onHoverRow (during suppression)
    PierreDiffView->>PierreDiffView: "activateHoveredRow()<br/>updates hoveredRowKey + affordance,<br/>starts idle timeout if no deadline set"

    Note over DiffPane: 160ms elapses
    DiffPane->>DiffPane: setAddNoteHoverSuppressed(false)
    DiffPane->>DiffSection: "hoverSuppressed=false"
    DiffSection->>PierreDiffView: "hoverSuppressed=false"
    PierreDiffView->>PierreDiffView: "useEffect: restore hoveredRowKey<br/>if idle deadline still active"

    Note over PierreDiffView: badge shows again

    Note over PierreDiffView: 2000ms idle timeout fires
    PierreDiffView->>PierreDiffView: clear hoveredRowKey + affordance
    Note over PierreDiffView: badge hides
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
src/ui/diff/PierreDiffView.tsx:127-135
**Stale affordance after multi-row hover during suppression**

When the mouse moves over multiple rows during the 160 ms suppression window, only the first timeout is created (the `hoverIdleDeadlineRef` guard skips subsequent calls). That first timeout captures `rowKey` (row 1) in its closure. When it fires 2 s later, it skips `setHoveredRowKey` (since `current` has moved to row 2) and skips `lastHoveredRowKeyRef` cleanup — but still calls `onActiveAddNoteAffordanceChange?.(null)` unconditionally. The parent therefore loses the active affordance for row 2 while the visual badge still shows, so any keyboard/shortcut action that relies on the affordance would silently target nothing.

### Issue 2 of 2
src/ui/components/panes/DiffSection.tsx:148-150
`onMouseScroll` is not included in the custom memo comparator. Since `suppressAddNoteHoverForMouseScroll` is always a stable `useCallback([])` reference this is harmless today, but the omission is inconsistent with the rest of the comparator and could silently misbehave if the prop's stability assumption ever changes.

```suggestion
    previous.hoverActive === next.hoverActive &&
    previous.hoverSuppressed === next.hoverSuppressed &&
    previous.onMouseScroll === next.onMouseScroll &&
    previous.theme === next.theme &&
```

Reviews (1): Last reviewed commit: "fix(ui): hide add-note affordance while ..." | Re-trigger Greptile

Comment thread src/ui/diff/PierreDiffView.tsx Outdated
Comment on lines +127 to +135
hoverIdleTimeoutRef.current = setTimeout(() => {
setHoveredRowKey((current) => (current === rowKey ? null : current));
if (lastHoveredRowKeyRef.current === rowKey) {
lastHoveredRowKeyRef.current = null;
}
hoverIdleDeadlineRef.current = null;
onActiveAddNoteAffordanceChange?.(null);
hoverIdleTimeoutRef.current = null;
}, ADD_NOTE_IDLE_HIDE_DELAY_MS);
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.

P1 Stale affordance after multi-row hover during suppression

When the mouse moves over multiple rows during the 160 ms suppression window, only the first timeout is created (the hoverIdleDeadlineRef guard skips subsequent calls). That first timeout captures rowKey (row 1) in its closure. When it fires 2 s later, it skips setHoveredRowKey (since current has moved to row 2) and skips lastHoveredRowKeyRef cleanup — but still calls onActiveAddNoteAffordanceChange?.(null) unconditionally. The parent therefore loses the active affordance for row 2 while the visual badge still shows, so any keyboard/shortcut action that relies on the affordance would silently target nothing.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/diff/PierreDiffView.tsx
Line: 127-135

Comment:
**Stale affordance after multi-row hover during suppression**

When the mouse moves over multiple rows during the 160 ms suppression window, only the first timeout is created (the `hoverIdleDeadlineRef` guard skips subsequent calls). That first timeout captures `rowKey` (row 1) in its closure. When it fires 2 s later, it skips `setHoveredRowKey` (since `current` has moved to row 2) and skips `lastHoveredRowKeyRef` cleanup — but still calls `onActiveAddNoteAffordanceChange?.(null)` unconditionally. The parent therefore loses the active affordance for row 2 while the visual badge still shows, so any keyboard/shortcut action that relies on the affordance would silently target nothing.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch. I changed the hover idle timeout to use one shared scheduler that can clear the current hovered row at the preserved idle deadline during scroll suppression, instead of clearing the parent affordance from a stale row closure.

Responded by pi using GPT-5.

Comment on lines 148 to 150
previous.hoverActive === next.hoverActive &&
previous.hoverSuppressed === next.hoverSuppressed &&
previous.theme === next.theme &&
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.

P2 onMouseScroll is not included in the custom memo comparator. Since suppressAddNoteHoverForMouseScroll is always a stable useCallback([]) reference this is harmless today, but the omission is inconsistent with the rest of the comparator and could silently misbehave if the prop's stability assumption ever changes.

Suggested change
previous.hoverActive === next.hoverActive &&
previous.hoverSuppressed === next.hoverSuppressed &&
previous.theme === next.theme &&
previous.hoverActive === next.hoverActive &&
previous.hoverSuppressed === next.hoverSuppressed &&
previous.onMouseScroll === next.onMouseScroll &&
previous.theme === next.theme &&
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/components/panes/DiffSection.tsx
Line: 148-150

Comment:
`onMouseScroll` is not included in the custom memo comparator. Since `suppressAddNoteHoverForMouseScroll` is always a stable `useCallback([])` reference this is harmless today, but the omission is inconsistent with the rest of the comparator and could silently misbehave if the prop's stability assumption ever changes.

```suggestion
    previous.hoverActive === next.hoverActive &&
    previous.hoverSuppressed === next.hoverSuppressed &&
    previous.onMouseScroll === next.onMouseScroll &&
    previous.theme === next.theme &&
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added onMouseScroll to the DiffSection memo comparator so the custom comparison covers the new callback prop too.

Responded by pi using GPT-5.

@benvinegar benvinegar force-pushed the fix/hide-add-note-on-scroll branch from 28ee2d6 to fd4df21 Compare May 19, 2026 03:36
@benvinegar benvinegar merged commit dc0cd32 into main May 19, 2026
5 checks passed
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