Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions src/ui/components/panes/DiffPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,10 @@ export function DiffPane({
return;
}

const updateViewport = () => {
let cancelled = false;
let scheduled = false;

const readViewport = () => {
const nextTop = scrollBox.scrollTop ?? 0;
const nextHeight = scrollBox.viewport.height ?? 0;

Expand All @@ -341,16 +344,34 @@ export function DiffPane({
);
};

// OpenTUI emits `change` synchronously from inside its own slider sync, and other
// useLayoutEffects in this pane scroll the box from inside React's commit phase.
// Calling setScrollViewport directly from the listener can run setState while React
// is already committing — which downstream layout effects can amplify into a render
// loop and trip React's max-update-depth guard. Coalesce listener events into a
// single microtask-deferred read so the setState is dispatched outside the emit
// call stack and repeated events between paints collapse into one update.
const handleViewportChange = () => {
updateViewport();
if (scheduled) {
return;
}
scheduled = true;
queueMicrotask(() => {
scheduled = false;
if (cancelled) {
return;
}
readViewport();
});
Comment on lines +359 to +365
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 The scheduled = false reset happens unconditionally before the cancelled guard. If readViewport() throws (e.g., the scrollbox is torn down between the microtask being queued and it running), scheduled is already false and a new event could queue a fresh microtask before the thrown exception propagates. Resetting scheduled only on the non-cancelled branch makes the intent clearer and keeps the flag consistent through the full life of the microtask.

Suggested change
queueMicrotask(() => {
scheduled = false;
if (cancelled) {
return;
}
readViewport();
});
queueMicrotask(() => {
if (cancelled) {
scheduled = false;
return;
}
scheduled = false;
readViewport();
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/ui/components/panes/DiffPane.tsx
Line: 359-365

Comment:
The `scheduled = false` reset happens unconditionally before the `cancelled` guard. If `readViewport()` throws (e.g., the scrollbox is torn down between the microtask being queued and it running), `scheduled` is already `false` and a new event could queue a fresh microtask before the thrown exception propagates. Resetting `scheduled` only on the non-cancelled branch makes the intent clearer and keeps the flag consistent through the full life of the microtask.

```suggestion
      queueMicrotask(() => {
        if (cancelled) {
          scheduled = false;
          return;
        }
        scheduled = false;
        readViewport();
      });
```

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

};

updateViewport();
readViewport();
scrollBox.verticalScrollBar.on("change", handleViewportChange);
scrollBox.viewport.on("layout-changed", handleViewportChange);
scrollBox.viewport.on("resized", handleViewportChange);

return () => {
cancelled = true;
scrollBox.verticalScrollBar.off("change", handleViewportChange);
scrollBox.viewport.off("layout-changed", handleViewportChange);
scrollBox.viewport.off("resized", handleViewportChange);
Expand Down