Skip to content

Commit 5effca1

Browse files
fix(web): thread jump hints wait for their delay on every hold (#25636)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 4034b5f commit 5effca1

2 files changed

Lines changed: 35 additions & 6 deletions

File tree

‎apps/web/src/components/Sidebar.logic.browser.test.tsx‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it, vi } from "vite-plus/test";
22

3-
import { render } from "../../test/render";
4-
import { useSnoozeWakeClock } from "./Sidebar.logic";
3+
import { act, fakeTimers, render } from "../../test/render";
4+
import { useSnoozeWakeClock, useThreadJumpHintsVisible } from "./Sidebar.logic";
55

66
const MOUNTED_AT_MS = Date.parse("2026-10-24T08:00:00.000Z");
77

@@ -31,3 +31,29 @@ describe("useSnoozeWakeClock", () => {
3131
await expect.element(screen.getByRole("status")).toHaveTextContent(String(lastWakeMs));
3232
});
3333
});
34+
35+
function JumpHints(props: { readonly held: boolean }) {
36+
return <output>{useThreadJumpHintsVisible(props.held) ? "shown" : "hidden"}</output>;
37+
}
38+
39+
describe("useThreadJumpHintsVisible", () => {
40+
it("waits for the delay again on a second hold", async () => {
41+
// The first render reads the clock.
42+
using _timers = fakeTimers({ now: 0 });
43+
await using screen = await render(<JumpHints held />, {
44+
// A bare hook host; nothing here links or navigates.
45+
router: false,
46+
});
47+
await act(() => vi.advanceTimersByTime(1_000));
48+
await expect.element(screen.getByRole("status")).toHaveTextContent("shown");
49+
50+
await screen.rerender(<JumpHints held={false} />);
51+
await expect.element(screen.getByRole("status")).toHaveTextContent("hidden");
52+
await act(() => vi.advanceTimersByTime(1_000));
53+
54+
await screen.rerender(<JumpHints held />);
55+
await expect.element(screen.getByRole("status")).toHaveTextContent("hidden");
56+
await act(() => vi.advanceTimersByTime(1_000));
57+
await expect.element(screen.getByRole("status")).toHaveTextContent("shown");
58+
});
59+
});

‎apps/web/src/components/Sidebar.logic.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -670,10 +670,13 @@ type ThreadStatusInput = Pick<
670670
*/
671671
export function useThreadJumpHintsVisible(shouldShow: boolean): boolean {
672672
const [showAt, setShowAt] = React.useState<number | null>(null);
673-
let nextShowAt = shouldShow ? showAt : null;
674-
if (shouldShow && showAt === null) nextShowAt = deadlineAfter(THREAD_JUMP_HINT_SHOW_DELAY_MS);
675-
if (nextShowAt !== showAt) setShowAt(nextShowAt);
676-
return useDeadlinePassed(nextShowAt);
673+
// The clock is read inside the updater: the compiler caches a bare clock read
674+
// in render once per instance, so a second hold would reuse the first deadline.
675+
// React discards this render and re-runs with the update.
676+
if (shouldShow !== (showAt !== null)) {
677+
setShowAt(shouldShow ? () => deadlineAfter(THREAD_JUMP_HINT_SHOW_DELAY_MS) : null);
678+
}
679+
return useDeadlinePassed(showAt);
677680
}
678681

679682
export function resolveThreadLastVisitedAt(

0 commit comments

Comments
 (0)