Compare commits

...
1 Commits
Author SHA1 Message Date
Kit Langton 70efd3fb38 fix(tui): preserve background hint timing (#42577) 2026-08-14 13:18:53 +00:00
3 changed files with 88 additions and 7 deletions
+6 -2
View File
@@ -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 (
<Show when={visible() && shortcut()}>
{(value) => (
+23 -5
View File
@@ -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<T>(source: Accessor<T | undefined>, delay: number) {
export function createDelayedPresence<T>(
source: Accessor<T | undefined>,
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))
})
@@ -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()
}
})