Skip to content
Open
Show file tree
Hide file tree
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
5 changes: 5 additions & 0 deletions .changeset/quiet-opentui-scroll-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hunkdiff": patch
---

Fail clearly when an OpenTUI upgrade removes the shifted-wheel scroll reset Hunk requires.
19 changes: 19 additions & 0 deletions src/ui/components/panes/DiffPane.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { describe, expect, test } from "bun:test";
import { ScrollBoxRenderable } from "@opentui/core";
import { resetOpenTuiScrollAccumulators } from "./DiffPane";

describe("resetOpenTuiScrollAccumulators", () => {
test("requires the OpenTUI 0.5.1 compatibility operation", () => {
const resetScrollAccumulators = (
ScrollBoxRenderable.prototype as unknown as { resetScrollAccumulators?: () => void }
).resetScrollAccumulators;

expect(resetScrollAccumulators).toBeFunction();
});

test("fails clearly when an OpenTUI upgrade removes the compatibility operation", () => {
expect(() => resetOpenTuiScrollAccumulators({} as unknown as ScrollBoxRenderable)).toThrow(
"OpenTUI 0.5.1 ScrollBoxRenderable.resetScrollAccumulators is required after shifted wheel input.",
);
});
});
25 changes: 22 additions & 3 deletions src/ui/components/panes/DiffPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,27 @@ import {

const EMPTY_VISIBLE_AGENT_NOTES: VisibleAgentNote[] = [];

/**
* Resets OpenTUI's wheel remainder after Hunk reroutes a shifted wheel event.
*
* OpenTUI 0.5.1 keeps this operation private, so retain this compatibility bridge only until
* OpenTUI exposes a public reset API. A missing operation must fail loudly rather than let a
* later vertical wheel event consume the stale remainder and move the review viewport.
*/
export function resetOpenTuiScrollAccumulators(scrollBox: ScrollBoxRenderable) {
const compatibilityScrollBox = scrollBox as unknown as {
resetScrollAccumulators?: () => void;
};

if (!compatibilityScrollBox.resetScrollAccumulators) {
throw new Error(
"OpenTUI 0.5.1 ScrollBoxRenderable.resetScrollAccumulators is required after shifted wheel input. Update this compatibility bridge when upgrading OpenTUI.",
);
}

compatibilityScrollBox.resetScrollAccumulators();
}

/**
* Clamp one vertical scroll target into the currently reachable review-stream extent.
*
Expand Down Expand Up @@ -479,9 +500,7 @@ export function DiffPane({

currentScrollBox.scrollTo({ x: preservedScrollLeft, y: preservedScrollTop });
currentScrollBox.scrollAcceleration.reset();
(
currentScrollBox as unknown as { resetScrollAccumulators?: () => void }
).resetScrollAccumulators?.();
resetOpenTuiScrollAccumulators(currentScrollBox);
});

event.preventDefault();
Expand Down
34 changes: 34 additions & 0 deletions test/pty/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,40 @@ describe("PTY layout", () => {
}
});

test("shifted mouse-wheel input scrolls code horizontally in a real PTY", async () => {
const fixture = harness.createLongWrapFilePair();
const session = await harness.launchHunk({
args: ["diff", fixture.before, fixture.after, "--mode", "split"],
cols: 102,
rows: 20,
});

try {
const initial = await session.waitForText(/View\s+Navigate\s+Agent\s+Help/, {
timeout: 15_000,
});

expect(initial).toContain("this is a very long");
expect(initial).not.toContain("ge';");

let shifted = initial;
for (let index = 0; index < 96; index += 1) {
// SGR button 69 is a wheel-down event with the Shift modifier.
session.writeRaw("\x1b[<69;61;11M");
await session.waitIdle();
shifted = await session.text({ immediate: true });
if (shifted.includes("ge';")) {
break;
}
}

expect(shifted).toContain("ge';");
expect(shifted).not.toContain("this is a very long");
} finally {
session.close();
}
});

test("wrap toggles reset horizontal code scrolling in a real PTY", async () => {
const fixture = harness.createLongWrapFilePair();
const session = await harness.launchHunk({
Expand Down