diff --git a/packages/tui/src/routes/session/index.tsx b/packages/tui/src/routes/session/index.tsx index e90da6f0830..d2c4b8cee23 100644 --- a/packages/tui/src/routes/session/index.tsx +++ b/packages/tui/src/routes/session/index.tsx @@ -1530,9 +1530,13 @@ function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) { return name === "shell" || name === "subagent" }) if (!current || !part) return - return `${current.id}:${part.id}` + return { key: `${current.id}:${part.id}`, started: part.time.ran ?? part.time.created } }) - const visible = createDelayedPresence(running, BACKGROUND_TOOL_HINT_DELAY) + const visible = createDelayedPresence( + running, + (tool) => Math.max(0, BACKGROUND_TOOL_HINT_DELAY - (Date.now() - tool.started)), + (previous, next) => previous.key === next.key && previous.started === next.started, + ) return ( {(value) => ( diff --git a/packages/tui/src/util/delayed-presence.ts b/packages/tui/src/util/delayed-presence.ts index 75c0b1a6311..894dc8bde79 100644 --- a/packages/tui/src/util/delayed-presence.ts +++ b/packages/tui/src/util/delayed-presence.ts @@ -1,14 +1,32 @@ -import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js" +import { createEffect, createMemo, createSignal, onCleanup, type Accessor } from "solid-js" -export function createDelayedPresence(source: Accessor, delay: number) { +export function createDelayedPresence( + source: Accessor, + delay: number | ((value: T) => number), + equals?: (previous: T, next: T) => boolean, +) { const [visible, setVisible] = createSignal(false) + const value = equals + ? createMemo(source, undefined, { + equals: (previous, next) => { + if (previous === undefined || next === undefined) return previous === next + return equals(previous, next) + }, + }) + : source createEffect(() => { - const value = source() + const current = value() setVisible(false) - if (value === undefined) return + if (current === undefined) return - const timer = setTimeout(() => setVisible(true), delay) + const remaining = typeof delay === "function" ? delay(current) : delay + if (remaining <= 0) { + setVisible(true) + return + } + + const timer = setTimeout(() => setVisible(true), remaining) onCleanup(() => clearTimeout(timer)) }) diff --git a/packages/tui/test/util/delayed-presence.test.ts b/packages/tui/test/util/delayed-presence.test.ts index 599bd02ff71..8b1e5f89938 100644 --- a/packages/tui/test/util/delayed-presence.test.ts +++ b/packages/tui/test/util/delayed-presence.test.ts @@ -53,3 +53,62 @@ test("cancels the delay when the value disappears or the owner is disposed", asy jest.useRealTimers() } }) + +test("uses the remaining delay for the current value", async () => { + jest.useFakeTimers() + const scope = createRoot((dispose) => { + const [value, setValue] = createSignal<{ age: number }>() + return { + dispose, + setValue, + visible: createDelayedPresence(value, (current) => Math.max(0, 1_000 - current.age)), + } + }) + + try { + scope.setValue({ age: 400 }) + await Promise.resolve() + jest.advanceTimersByTime(599) + expect(scope.visible()).toBe(false) + jest.advanceTimersByTime(1) + expect(scope.visible()).toBe(true) + + scope.setValue({ age: 1_000 }) + await Promise.resolve() + expect(scope.visible()).toBe(true) + } finally { + scope.dispose() + jest.useRealTimers() + } +}) + +test("does not restart the delay for an equivalent value", async () => { + jest.useFakeTimers() + const scope = createRoot((dispose) => { + const [value, setValue] = createSignal<{ id: string }>() + return { + dispose, + setValue, + visible: createDelayedPresence(value, 1_000, (previous, next) => previous.id === next.id), + } + }) + + try { + scope.setValue({ id: "first" }) + await Promise.resolve() + jest.advanceTimersByTime(500) + scope.setValue({ id: "first" }) + await Promise.resolve() + jest.advanceTimersByTime(499) + expect(scope.visible()).toBe(false) + jest.advanceTimersByTime(1) + expect(scope.visible()).toBe(true) + + scope.setValue({ id: "second" }) + await Promise.resolve() + expect(scope.visible()).toBe(false) + } finally { + scope.dispose() + jest.useRealTimers() + } +})