Răsfoiți Sursa

fix(tui): preserve background hint timing (#42577)

Kit Langton 21 ore în urmă
părinte
comite
70efd3fb38

+ 6 - 2
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 (
     <Show when={visible() && shortcut()}>
       {(value) => (

+ 23 - 5
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<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))
   })
 

+ 59 - 0
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()
+  }
+})