From 6bd941337395933504d637ae5d2143f4fbbb0a30 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 29 Mar 2026 02:48:59 -0700 Subject: [PATCH 1/5] debounce thread jump hint pills Co-authored-by: codex --- apps/web/src/components/Sidebar.logic.test.ts | 66 ++++++++++++++++++- apps/web/src/components/Sidebar.logic.ts | 51 ++++++++++++++ apps/web/src/components/Sidebar.tsx | 33 +++++++++- 3 files changed, 146 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index b54ec1cb93..a089396800 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -1,6 +1,7 @@ -import { describe, expect, it } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { + createThreadJumpHintVisibilityController, getVisibleSidebarThreadIds, resolveAdjacentThreadId, getFallbackThreadIdAfterDelete, @@ -15,6 +16,7 @@ import { shouldClearThreadSelectionOnMouseDown, sortProjectsForSidebar, sortThreadsForSidebar, + THREAD_JUMP_HINT_SHOW_DELAY_MS, } from "./Sidebar.logic"; import { ProjectId, ThreadId } from "@t3tools/contracts"; import { @@ -52,6 +54,68 @@ describe("hasUnseenCompletion", () => { }); }); +describe("createThreadJumpHintVisibilityController", () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it("delays showing jump hints until the configured delay elapses", () => { + const visibilityChanges: boolean[] = []; + const controller = createThreadJumpHintVisibilityController({ + delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, + onVisibilityChange: (visible) => { + visibilityChanges.push(visible); + }, + }); + + controller.sync(true); + vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS - 1); + + expect(visibilityChanges).toEqual([]); + + vi.advanceTimersByTime(1); + + expect(visibilityChanges).toEqual([true]); + }); + + it("hides immediately when the modifiers are released", () => { + const visibilityChanges: boolean[] = []; + const controller = createThreadJumpHintVisibilityController({ + delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, + onVisibilityChange: (visible) => { + visibilityChanges.push(visible); + }, + }); + + controller.sync(true); + vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS); + controller.sync(false); + + expect(visibilityChanges).toEqual([true, false]); + }); + + it("cancels a pending reveal when the modifier is released early", () => { + const visibilityChanges: boolean[] = []; + const controller = createThreadJumpHintVisibilityController({ + delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, + onVisibilityChange: (visible) => { + visibilityChanges.push(visible); + }, + }); + + controller.sync(true); + vi.advanceTimersByTime(Math.floor(THREAD_JUMP_HINT_SHOW_DELAY_MS / 2)); + controller.sync(false); + vi.advanceTimersByTime(THREAD_JUMP_HINT_SHOW_DELAY_MS); + + expect(visibilityChanges).toEqual([]); + }); +}); + describe("shouldClearThreadSelectionOnMouseDown", () => { it("preserves selection for thread items", () => { const child = { diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 1e0871e0d2..749d42f5fa 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -8,6 +8,7 @@ import { } from "../session-logic"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; +export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 180; export type SidebarNewThreadEnvMode = "local" | "worktree"; type SidebarProject = { id: string; @@ -16,6 +17,7 @@ type SidebarProject = { updatedAt?: string | undefined; }; type SidebarThreadSortInput = Pick; +type TimeoutHandle = ReturnType; export type ThreadTraversalDirection = "previous" | "next"; @@ -46,6 +48,55 @@ type ThreadStatusInput = Pick< "interactionMode" | "latestTurn" | "lastVisitedAt" | "proposedPlans" | "session" >; +export function createThreadJumpHintVisibilityController(input: { + delayMs: number; + onVisibilityChange: (visible: boolean) => void; + setTimeoutFn?: typeof globalThis.setTimeout; + clearTimeoutFn?: typeof globalThis.clearTimeout; +}): { + sync: (shouldShow: boolean) => void; + dispose: () => void; +} { + const setTimeoutFn = input.setTimeoutFn ?? globalThis.setTimeout; + const clearTimeoutFn = input.clearTimeoutFn ?? globalThis.clearTimeout; + let isVisible = false; + let timeoutId: TimeoutHandle | null = null; + + const clearPendingShow = () => { + if (timeoutId === null) { + return; + } + clearTimeoutFn(timeoutId); + timeoutId = null; + }; + + return { + sync: (shouldShow) => { + if (!shouldShow) { + clearPendingShow(); + if (isVisible) { + isVisible = false; + input.onVisibilityChange(false); + } + return; + } + + if (isVisible || timeoutId !== null) { + return; + } + + timeoutId = setTimeoutFn(() => { + timeoutId = null; + isVisible = true; + input.onVisibilityChange(true); + }, input.delayMs); + }, + dispose: () => { + clearPendingShow(); + }, + }; +} + export function hasUnseenCompletion(thread: ThreadStatusInput): boolean { if (!thread.latestTurn?.completedAt) return false; const completedAt = Date.parse(thread.latestTurn.completedAt); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 100d0e3f47..a119435901 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -105,6 +105,7 @@ import { import { useThreadSelectionStore } from "../threadSelectionStore"; import { isNonEmpty as isNonEmptyString } from "effect/String"; import { + createThreadJumpHintVisibilityController, getVisibleSidebarThreadIds, getVisibleThreadsForProject, resolveAdjacentThreadId, @@ -116,6 +117,7 @@ import { shouldClearThreadSelectionOnMouseDown, sortProjectsForSidebar, sortThreadsForSidebar, + THREAD_JUMP_HINT_SHOW_DELAY_MS, } from "./Sidebar.logic"; import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; @@ -362,6 +364,9 @@ export default function Sidebar() { ReadonlySet >(() => new Set()); const [showThreadJumpHints, setShowThreadJumpHints] = useState(false); + const threadJumpHintVisibilityControllerRef = useRef | null>(null); const renamingCommittedRef = useRef(false); const renamingInputRef = useRef(null); const dragInProgressRef = useRef(false); @@ -1106,6 +1111,27 @@ export default function Sidebar() { [renderedProjects], ); + useEffect(() => { + const controller = createThreadJumpHintVisibilityController({ + delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, + onVisibilityChange: (visible) => { + setShowThreadJumpHints(visible); + }, + setTimeoutFn: window.setTimeout.bind(window), + clearTimeoutFn: window.clearTimeout.bind(window), + }); + threadJumpHintVisibilityControllerRef.current = controller; + + return () => { + controller.dispose(); + threadJumpHintVisibilityControllerRef.current = null; + }; + }, []); + + const updateThreadJumpHintsVisibility = useCallback((shouldShow: boolean) => { + threadJumpHintVisibilityControllerRef.current?.sync(shouldShow); + }, []); + useEffect(() => { const getShortcutContext = () => ({ terminalFocus: isTerminalFocused(), @@ -1113,7 +1139,7 @@ export default function Sidebar() { }); const onWindowKeyDown = (event: KeyboardEvent) => { - setShowThreadJumpHints( + updateThreadJumpHintsVisibility( shouldShowThreadJumpHints(event, keybindings, { platform, context: getShortcutContext(), @@ -1161,7 +1187,7 @@ export default function Sidebar() { }; const onWindowKeyUp = (event: KeyboardEvent) => { - setShowThreadJumpHints( + updateThreadJumpHintsVisibility( shouldShowThreadJumpHints(event, keybindings, { platform, context: getShortcutContext(), @@ -1170,7 +1196,7 @@ export default function Sidebar() { }; const onWindowBlur = () => { - setShowThreadJumpHints(false); + updateThreadJumpHintsVisibility(false); }; window.addEventListener("keydown", onWindowKeyDown); @@ -1190,6 +1216,7 @@ export default function Sidebar() { routeTerminalOpen, routeThreadId, threadJumpThreadIds, + updateThreadJumpHintsVisibility, ]); function renderProjectItem( From 5fa62b244b8821ed7cef7880dedfa545e5a9efc5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 29 Mar 2026 09:29:09 -0700 Subject: [PATCH 2/5] fix sidebar jump hint ordering Co-authored-by: codex --- apps/web/src/components/Sidebar.logic.test.ts | 21 +++++++++++++++ apps/web/src/components/Sidebar.logic.ts | 5 +++- apps/web/src/components/Sidebar.tsx | 26 ++++++++----------- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/apps/web/src/components/Sidebar.logic.test.ts b/apps/web/src/components/Sidebar.logic.test.ts index a089396800..ac343b7288 100644 --- a/apps/web/src/components/Sidebar.logic.test.ts +++ b/apps/web/src/components/Sidebar.logic.test.ts @@ -235,6 +235,27 @@ describe("getVisibleSidebarThreadIds", () => { ThreadId.makeUnsafe("thread-6"), ]); }); + + it("skips threads from collapsed projects whose thread panels are not shown", () => { + expect( + getVisibleSidebarThreadIds([ + { + shouldShowThreadPanel: false, + renderedThreads: [ + { id: ThreadId.makeUnsafe("thread-hidden-2") }, + { id: ThreadId.makeUnsafe("thread-hidden-1") }, + ], + }, + { + shouldShowThreadPanel: true, + renderedThreads: [ + { id: ThreadId.makeUnsafe("thread-12") }, + { id: ThreadId.makeUnsafe("thread-11") }, + ], + }, + ]), + ).toEqual([ThreadId.makeUnsafe("thread-12"), ThreadId.makeUnsafe("thread-11")]); + }); }); describe("isContextMenuPointerDown", () => { diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 749d42f5fa..a1324dc35c 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -122,13 +122,16 @@ export function resolveSidebarNewThreadEnvMode(input: { export function getVisibleSidebarThreadIds( renderedProjects: readonly { + shouldShowThreadPanel?: boolean; renderedThreads: readonly { id: TThreadId; }[]; }[], ): TThreadId[] { return renderedProjects.flatMap((renderedProject) => - renderedProject.renderedThreads.map((thread) => thread.id), + renderedProject.shouldShowThreadPanel === false + ? [] + : renderedProject.renderedThreads.map((thread) => thread.id), ); } diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index a119435901..2b4e57b3a5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -1075,23 +1075,22 @@ export default function Sidebar() { visibleThreads, ], ); + const visibleSidebarThreadIds = useMemo( + () => getVisibleSidebarThreadIds(renderedProjects), + [renderedProjects], + ); const threadJumpCommandById = useMemo(() => { const mapping = new Map>>(); - let visibleThreadIndex = 0; - - for (const renderedProject of renderedProjects) { - for (const thread of renderedProject.renderedThreads) { - const jumpCommand = threadJumpCommandForIndex(visibleThreadIndex); - if (!jumpCommand) { - return mapping; - } - mapping.set(thread.id, jumpCommand); - visibleThreadIndex += 1; + for (const [visibleThreadIndex, threadId] of visibleSidebarThreadIds.entries()) { + const jumpCommand = threadJumpCommandForIndex(visibleThreadIndex); + if (!jumpCommand) { + return mapping; } + mapping.set(threadId, jumpCommand); } return mapping; - }, [renderedProjects]); + }, [visibleSidebarThreadIds]); const threadJumpThreadIds = useMemo( () => [...threadJumpCommandById.keys()], [threadJumpCommandById], @@ -1106,10 +1105,7 @@ export default function Sidebar() { } return mapping; }, [keybindings, sidebarShortcutLabelOptions, threadJumpCommandById]); - const orderedSidebarThreadIds = useMemo( - () => getVisibleSidebarThreadIds(renderedProjects), - [renderedProjects], - ); + const orderedSidebarThreadIds = visibleSidebarThreadIds; useEffect(() => { const controller = createThreadJumpHintVisibilityController({ From fc9fc3719f84ea618a77c1734f675bc49ae67bd0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 29 Mar 2026 09:33:44 -0700 Subject: [PATCH 3/5] extract thread jump hint visibility hook Co-authored-by: codex --- apps/web/src/components/Sidebar.logic.ts | 13 +++--- apps/web/src/components/Sidebar.tsx | 29 +------------- .../src/hooks/useThreadJumpHintVisibility.ts | 40 +++++++++++++++++++ 3 files changed, 49 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/hooks/useThreadJumpHintVisibility.ts diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index a1324dc35c..3245d9fab9 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -17,7 +17,6 @@ type SidebarProject = { updatedAt?: string | undefined; }; type SidebarThreadSortInput = Pick; -type TimeoutHandle = ReturnType; export type ThreadTraversalDirection = "previous" | "next"; @@ -48,19 +47,21 @@ type ThreadStatusInput = Pick< "interactionMode" | "latestTurn" | "lastVisitedAt" | "proposedPlans" | "session" >; +export interface ThreadJumpHintVisibilityController { + sync: (shouldShow: boolean) => void; + dispose: () => void; +} + export function createThreadJumpHintVisibilityController(input: { delayMs: number; onVisibilityChange: (visible: boolean) => void; setTimeoutFn?: typeof globalThis.setTimeout; clearTimeoutFn?: typeof globalThis.clearTimeout; -}): { - sync: (shouldShow: boolean) => void; - dispose: () => void; -} { +}): ThreadJumpHintVisibilityController { const setTimeoutFn = input.setTimeoutFn ?? globalThis.setTimeout; const clearTimeoutFn = input.clearTimeoutFn ?? globalThis.clearTimeout; let isVisible = false; - let timeoutId: TimeoutHandle | null = null; + let timeoutId: NodeJS.Timeout | null = null; const clearPendingShow = () => { if (timeoutId === null) { diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 2b4e57b3a5..3b4b9ab2b6 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -69,6 +69,7 @@ import { serverConfigQueryOptions } from "../lib/serverReactQuery"; import { readNativeApi } from "../nativeApi"; import { useComposerDraftStore } from "../composerDraftStore"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; +import { useThreadJumpHintVisibility } from "../hooks/useThreadJumpHintVisibility"; import { useThreadActions } from "../hooks/useThreadActions"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { toastManager } from "./ui/toast"; @@ -105,7 +106,6 @@ import { import { useThreadSelectionStore } from "../threadSelectionStore"; import { isNonEmpty as isNonEmptyString } from "effect/String"; import { - createThreadJumpHintVisibilityController, getVisibleSidebarThreadIds, getVisibleThreadsForProject, resolveAdjacentThreadId, @@ -117,7 +117,6 @@ import { shouldClearThreadSelectionOnMouseDown, sortProjectsForSidebar, sortThreadsForSidebar, - THREAD_JUMP_HINT_SHOW_DELAY_MS, } from "./Sidebar.logic"; import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; @@ -363,10 +362,7 @@ export default function Sidebar() { const [expandedThreadListsByProject, setExpandedThreadListsByProject] = useState< ReadonlySet >(() => new Set()); - const [showThreadJumpHints, setShowThreadJumpHints] = useState(false); - const threadJumpHintVisibilityControllerRef = useRef | null>(null); + const { showThreadJumpHints, updateThreadJumpHintsVisibility } = useThreadJumpHintVisibility(); const renamingCommittedRef = useRef(false); const renamingInputRef = useRef(null); const dragInProgressRef = useRef(false); @@ -1107,27 +1103,6 @@ export default function Sidebar() { }, [keybindings, sidebarShortcutLabelOptions, threadJumpCommandById]); const orderedSidebarThreadIds = visibleSidebarThreadIds; - useEffect(() => { - const controller = createThreadJumpHintVisibilityController({ - delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, - onVisibilityChange: (visible) => { - setShowThreadJumpHints(visible); - }, - setTimeoutFn: window.setTimeout.bind(window), - clearTimeoutFn: window.clearTimeout.bind(window), - }); - threadJumpHintVisibilityControllerRef.current = controller; - - return () => { - controller.dispose(); - threadJumpHintVisibilityControllerRef.current = null; - }; - }, []); - - const updateThreadJumpHintsVisibility = useCallback((shouldShow: boolean) => { - threadJumpHintVisibilityControllerRef.current?.sync(shouldShow); - }, []); - useEffect(() => { const getShortcutContext = () => ({ terminalFocus: isTerminalFocused(), diff --git a/apps/web/src/hooks/useThreadJumpHintVisibility.ts b/apps/web/src/hooks/useThreadJumpHintVisibility.ts new file mode 100644 index 0000000000..5a3446abee --- /dev/null +++ b/apps/web/src/hooks/useThreadJumpHintVisibility.ts @@ -0,0 +1,40 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import { + createThreadJumpHintVisibilityController, + THREAD_JUMP_HINT_SHOW_DELAY_MS, + type ThreadJumpHintVisibilityController, +} from "../components/Sidebar.logic"; + +export function useThreadJumpHintVisibility(): { + showThreadJumpHints: boolean; + updateThreadJumpHintsVisibility: (shouldShow: boolean) => void; +} { + const [showThreadJumpHints, setShowThreadJumpHints] = useState(false); + const controllerRef = useRef(null); + + useEffect(() => { + const controller = createThreadJumpHintVisibilityController({ + delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, + onVisibilityChange: (visible) => { + setShowThreadJumpHints(visible); + }, + setTimeoutFn: window.setTimeout.bind(window), + clearTimeoutFn: window.clearTimeout.bind(window), + }); + controllerRef.current = controller; + + return () => { + controller.dispose(); + controllerRef.current = null; + }; + }, []); + + const updateThreadJumpHintsVisibility = useCallback((shouldShow: boolean) => { + controllerRef.current?.sync(shouldShow); + }, []); + + return { + showThreadJumpHints, + updateThreadJumpHintsVisibility, + }; +} From 207beb7fb67b37d34be326b069ec158a539dc01f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 29 Mar 2026 09:35:13 -0700 Subject: [PATCH 4/5] kewl --- apps/web/src/components/Sidebar.logic.ts | 35 ++++++++++++++++ apps/web/src/components/Sidebar.tsx | 3 +- .../src/hooks/useThreadJumpHintVisibility.ts | 40 ------------------- 3 files changed, 37 insertions(+), 41 deletions(-) delete mode 100644 apps/web/src/hooks/useThreadJumpHintVisibility.ts diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 3245d9fab9..9f0c096f3d 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -1,3 +1,4 @@ +import * as React from "react"; import type { SidebarProjectSortOrder, SidebarThreadSortOrder } from "@t3tools/contracts/settings"; import type { Thread } from "../types"; import { cn } from "../lib/utils"; @@ -98,6 +99,40 @@ export function createThreadJumpHintVisibilityController(input: { }; } +export function useThreadJumpHintVisibility(): { + showThreadJumpHints: boolean; + updateThreadJumpHintsVisibility: (shouldShow: boolean) => void; +} { + const [showThreadJumpHints, setShowThreadJumpHints] = React.useState(false); + const controllerRef = React.useRef(null); + + React.useEffect(() => { + const controller = createThreadJumpHintVisibilityController({ + delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, + onVisibilityChange: (visible) => { + setShowThreadJumpHints(visible); + }, + setTimeoutFn: window.setTimeout.bind(window), + clearTimeoutFn: window.clearTimeout.bind(window), + }); + controllerRef.current = controller; + + return () => { + controller.dispose(); + controllerRef.current = null; + }; + }, []); + + const updateThreadJumpHintsVisibility = React.useCallback((shouldShow: boolean) => { + controllerRef.current?.sync(shouldShow); + }, []); + + return { + showThreadJumpHints, + updateThreadJumpHintsVisibility, + }; +} + export function hasUnseenCompletion(thread: ThreadStatusInput): boolean { if (!thread.latestTurn?.completedAt) return false; const completedAt = Date.parse(thread.latestTurn.completedAt); diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 3b4b9ab2b6..31787c7627 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -69,7 +69,7 @@ import { serverConfigQueryOptions } from "../lib/serverReactQuery"; import { readNativeApi } from "../nativeApi"; import { useComposerDraftStore } from "../composerDraftStore"; import { useHandleNewThread } from "../hooks/useHandleNewThread"; -import { useThreadJumpHintVisibility } from "../hooks/useThreadJumpHintVisibility"; + import { useThreadActions } from "../hooks/useThreadActions"; import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore"; import { toastManager } from "./ui/toast"; @@ -117,6 +117,7 @@ import { shouldClearThreadSelectionOnMouseDown, sortProjectsForSidebar, sortThreadsForSidebar, + useThreadJumpHintVisibility, } from "./Sidebar.logic"; import { SidebarUpdatePill } from "./sidebar/SidebarUpdatePill"; import { useCopyToClipboard } from "~/hooks/useCopyToClipboard"; diff --git a/apps/web/src/hooks/useThreadJumpHintVisibility.ts b/apps/web/src/hooks/useThreadJumpHintVisibility.ts deleted file mode 100644 index 5a3446abee..0000000000 --- a/apps/web/src/hooks/useThreadJumpHintVisibility.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { useCallback, useEffect, useRef, useState } from "react"; -import { - createThreadJumpHintVisibilityController, - THREAD_JUMP_HINT_SHOW_DELAY_MS, - type ThreadJumpHintVisibilityController, -} from "../components/Sidebar.logic"; - -export function useThreadJumpHintVisibility(): { - showThreadJumpHints: boolean; - updateThreadJumpHintsVisibility: (shouldShow: boolean) => void; -} { - const [showThreadJumpHints, setShowThreadJumpHints] = useState(false); - const controllerRef = useRef(null); - - useEffect(() => { - const controller = createThreadJumpHintVisibilityController({ - delayMs: THREAD_JUMP_HINT_SHOW_DELAY_MS, - onVisibilityChange: (visible) => { - setShowThreadJumpHints(visible); - }, - setTimeoutFn: window.setTimeout.bind(window), - clearTimeoutFn: window.clearTimeout.bind(window), - }); - controllerRef.current = controller; - - return () => { - controller.dispose(); - controllerRef.current = null; - }; - }, []); - - const updateThreadJumpHintsVisibility = useCallback((shouldShow: boolean) => { - controllerRef.current?.sync(shouldShow); - }, []); - - return { - showThreadJumpHints, - updateThreadJumpHintsVisibility, - }; -} From 6f731bfc83f5c889b47d56c359d96aa4fe4ffb52 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 29 Mar 2026 09:36:17 -0700 Subject: [PATCH 5/5] 100 is enough --- apps/web/src/components/Sidebar.logic.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.logic.ts b/apps/web/src/components/Sidebar.logic.ts index 9f0c096f3d..6350644d7f 100644 --- a/apps/web/src/components/Sidebar.logic.ts +++ b/apps/web/src/components/Sidebar.logic.ts @@ -9,7 +9,7 @@ import { } from "../session-logic"; export const THREAD_SELECTION_SAFE_SELECTOR = "[data-thread-item], [data-thread-selection-safe]"; -export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 180; +export const THREAD_JUMP_HINT_SHOW_DELAY_MS = 100; export type SidebarNewThreadEnvMode = "local" | "worktree"; type SidebarProject = { id: string;