fix(tui): delay background work hint (#41577)

This commit is contained in:
Kit Langton
2026-08-10 13:47:31 -04:00
committed by GitHub
parent f0b8ad1242
commit bbb1b5e7d0
3 changed files with 83 additions and 8 deletions
+12 -8
View File
@@ -102,11 +102,13 @@ import { useSessionTabs } from "../../context/session-tabs"
import { createSingleFlight } from "../../util/single-flight"
import type { SessionPending } from "@opencode-ai/schema/session-pending"
import { generateThinkingSyntax } from "./thinking-syntax"
import { createDelayedPresence } from "../../util/delayed-presence"
addDefaultParsers(parsers.parsers)
// Exclude temporary bottom space when measuring the real transcript height.
const NAVIGATION_SLACK_ID = "session-navigation-slack"
const BACKGROUND_TOOL_HINT_DELAY = 1_000
// Tail-first transcript mounting: rows mounted with the session, then backfill cadence.
// The tail comfortably overfills a tall viewport; backfill drains a 200-message transcript
@@ -1326,18 +1328,20 @@ function turnTokenToolSummary(tool: SessionMessageAssistantTool) {
function BackgroundToolHint(props: { messages: SessionMessageInfo[] }) {
const theme = useTheme()
const shortcut = Keymap.useShortcut("session.background")
const visible = createMemo(() => {
const running = createMemo(() => {
if (!shortcut()) return
const current = props.messages.findLast(
(message): message is SessionMessageAssistant => message.type === "assistant" && !message.time.completed,
)
return (
current?.content.some((part) => {
if (part.type !== "tool" || part.state.status !== "running") return false
const display = toolDisplay(part.name)
return display === "shell" || display === "subagent"
}) ?? false
)
const part = current?.content.find((part): part is SessionMessageAssistantTool => {
if (part.type !== "tool" || part.state.status !== "running") return false
const name = canonicalToolName(part.name)
return name === "shell" || name === "subagent"
})
if (!current || !part) return
return `${current.id}:${part.id}`
})
const visible = createDelayedPresence(running, BACKGROUND_TOOL_HINT_DELAY)
return (
<Show when={visible() && shortcut()}>
{(value) => (
+16
View File
@@ -0,0 +1,16 @@
import { createEffect, createSignal, onCleanup, type Accessor } from "solid-js"
export function createDelayedPresence<T>(source: Accessor<T | undefined>, delay: number) {
const [visible, setVisible] = createSignal(false)
createEffect(() => {
const value = source()
setVisible(false)
if (value === undefined) return
const timer = setTimeout(() => setVisible(true), delay)
onCleanup(() => clearTimeout(timer))
})
return visible
}
@@ -0,0 +1,55 @@
import { expect, jest, test } from "bun:test"
import { createRoot, createSignal } from "solid-js"
import { createDelayedPresence } from "../../src/util/delayed-presence"
test("shows only after the same value remains present for the delay", async () => {
jest.useFakeTimers()
const scope = createRoot((dispose) => {
const [value, setValue] = createSignal<string>()
return { dispose, setValue, visible: createDelayedPresence(value, 1_000) }
})
try {
scope.setValue("first")
await Promise.resolve()
jest.advanceTimersByTime(500)
expect(scope.visible()).toBe(false)
scope.setValue("second")
await Promise.resolve()
jest.advanceTimersByTime(999)
expect(scope.visible()).toBe(false)
jest.advanceTimersByTime(1)
expect(scope.visible()).toBe(true)
} finally {
scope.dispose()
jest.useRealTimers()
}
})
test("cancels the delay when the value disappears or the owner is disposed", async () => {
jest.useFakeTimers()
const scope = createRoot((dispose) => {
const [value, setValue] = createSignal<string>()
return { dispose, setValue, visible: createDelayedPresence(value, 1_000) }
})
try {
scope.setValue("running")
await Promise.resolve()
jest.advanceTimersByTime(500)
scope.setValue(undefined)
await Promise.resolve()
jest.advanceTimersByTime(1_000)
expect(scope.visible()).toBe(false)
scope.setValue("running")
await Promise.resolve()
scope.dispose()
jest.advanceTimersByTime(1_000)
expect(scope.visible()).toBe(false)
} finally {
scope.dispose()
jest.useRealTimers()
}
})