From baf5ce3b7eb01292b34de872232453a436bf51f2 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Fri, 14 Aug 2026 10:14:42 -0400 Subject: [PATCH] fix(tui): debounce shared storage reloads (#42579) --- packages/tui/src/context/storage.tsx | 13 +++++++++++-- packages/tui/test/context/session-tabs.test.tsx | 12 ++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/packages/tui/src/context/storage.tsx b/packages/tui/src/context/storage.tsx index 47aa497eea..0d1630463c 100644 --- a/packages/tui/src/context/storage.tsx +++ b/packages/tui/src/context/storage.tsx @@ -110,10 +110,19 @@ function createStorage(root: string, channel: string) { }, } - const watcher = watch(directory, () => entries.forEach((entry) => entry.reload())) + let reload: ReturnType | undefined + const watcher = watch(directory, () => { + clearTimeout(reload) + // Atomic writes notify for the temporary file before its final rename, and some + // platforms coalesce the rename event. Reload after the event burst has settled. + reload = setTimeout(() => entries.forEach((entry) => entry.reload()), 50) + }) return { storage, - close: () => watcher.close(), + close: () => { + clearTimeout(reload) + watcher.close() + }, } } diff --git a/packages/tui/test/context/session-tabs.test.tsx b/packages/tui/test/context/session-tabs.test.tsx index b259bacb9d..f6854cbf39 100644 --- a/packages/tui/test/context/session-tabs.test.tsx +++ b/packages/tui/test/context/session-tabs.test.tsx @@ -18,10 +18,10 @@ import { TestTuiContexts } from "../fixture/tui-environment" import { tmpdir } from "../fixture/fixture" import { createTuiResolvedConfig } from "../fixture/tui-runtime" -async function wait(fn: () => boolean | Promise, timeout = 2_000) { +async function wait(fn: () => boolean | Promise, timeout = 2_000, label = "condition") { const start = Date.now() while (!(await fn())) { - if (Date.now() - start > timeout) throw new Error("timed out waiting for condition") + if (Date.now() - start > timeout) throw new Error(`timed out waiting for ${label}`) await Bun.sleep(10) } } @@ -219,11 +219,11 @@ test("only the foreground TUI mutates unread state", async () => { let background: Awaited> | undefined try { - foreground = await renderSessionTabs("first", { state: temporary.path }) + foreground = await renderSessionTabs("first", { state: temporary.path, persisted: ["first", "second"] }) background = await renderSessionTabs("second", { state: temporary.path }) foreground.focus() background.blur() - await wait(() => foreground?.tabs.tabs().length === 2 && background?.tabs.tabs().length === 2) + await wait(() => foreground?.tabs.tabs().length === 2 && background?.tabs.tabs().length === 2, 2_000, "shared tabs") const firstDone = executionSucceeded("first") foreground.emit(firstDone) @@ -239,6 +239,8 @@ test("only the foreground TUI mutates unread state", async () => { () => foreground?.tabs.status("second").unread === "activity" && background?.tabs.status("second").unread === "activity", + 10_000, + "shared unread activity", ) foreground.tabs.select("second") @@ -246,6 +248,8 @@ test("only the foreground TUI mutates unread state", async () => { () => foreground?.tabs.status("second").unread === undefined && background?.tabs.status("second").unread === undefined, + 10_000, + "shared unread clearing", ) } finally { if (foreground) await foreground.destroy()