fix(renderer): cancel copied prompt reset on unmount (#20906)

* fix(renderer): cancel copied prompt reset on unmount

* fix: address memory PR review regressions and withdraw false positives

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-MacBook-Air.local>
This commit is contained in:
OrcaWin
2026-09-17 20:11:21 -07:00
committed by GitHub
co-authored by m4air m4air
parent d3032da299
commit d7d3bcfc66
2 changed files with 64 additions and 1 deletions
@@ -138,6 +138,51 @@ describe('EphemeralVmsPane', () => {
})
})
it('does not schedule a reset when clipboard completion arrives after unmount', async () => {
let finishClipboard!: () => void
vi.mocked(window.api.ui.writeClipboardText).mockReturnValueOnce(
new Promise<void>((resolve) => {
finishClipboard = resolve
})
)
const container = await renderPane()
const setTimeout = vi.spyOn(window, 'setTimeout')
try {
await act(async () => {
container.querySelector<HTMLButtonElement>('button[aria-label="Copy"]')?.click()
})
await act(async () => roots.pop()?.unmount())
setTimeout.mockClear()
await act(async () => {
finishClipboard()
await Promise.resolve()
})
expect(setTimeout.mock.calls.filter(([, delay]) => delay === 1500)).toHaveLength(0)
} finally {
setTimeout.mockRestore()
}
})
it('shows copied feedback while mounted and releases its reset on unmount', async () => {
const container = await renderPane()
const setTimeout = vi.spyOn(window, 'setTimeout')
const clearTimeout = vi.spyOn(window, 'clearTimeout')
try {
await act(async () => {
container.querySelector<HTMLButtonElement>('button[aria-label="Copy"]')?.click()
})
expect(container.querySelector('button[aria-label="Copy"]')?.textContent).toBe('Copied')
const timerIndex = setTimeout.mock.calls.findIndex(([, delay]) => delay === 1500)
expect(timerIndex).toBeGreaterThanOrEqual(0)
const timer = setTimeout.mock.results[timerIndex].value
await act(async () => roots.pop()?.unmount())
expect(clearTimeout).toHaveBeenCalledWith(timer)
} finally {
setTimeout.mockRestore()
clearTimeout.mockRestore()
}
})
it('refreshes the catalog when plugin content changes', async () => {
const listRecipeCatalog = window.api.ephemeralVm.listRecipeCatalog as ReturnType<typeof vi.fn>
const container = await renderPane()
@@ -45,6 +45,15 @@ export function EphemeralVmsPane(): React.JSX.Element {
const [promptCopied, setPromptCopied] = useState(false)
const mountedRef = useMountedRef()
const refreshGenerationRef = useRef(0)
const promptResetTimerRef = useRef<number | null>(null)
useEffect(() => {
return () => {
if (promptResetTimerRef.current !== null) {
window.clearTimeout(promptResetTimerRef.current)
}
}
}, [])
// Why: an absent runtime still resolves to the local host, which is what the
// seven sibling panes rely on to reach the Windows npx preflight.
@@ -126,8 +135,17 @@ export function EphemeralVmsPane(): React.JSX.Element {
try {
await window.api.ui.writeClipboardText(AGENT_PROMPT)
useAppStore.getState().recordFeatureInteraction('ephemeral-vm-setup')
if (!mountedRef.current) {
return
}
setPromptCopied(true)
setTimeout(() => setPromptCopied(false), 1500)
if (promptResetTimerRef.current !== null) {
window.clearTimeout(promptResetTimerRef.current)
}
promptResetTimerRef.current = window.setTimeout(() => {
promptResetTimerRef.current = null
setPromptCopied(false)
}, 1500)
} catch {
toast.error(
translate(