From 7ec7413fdbda65bdbd2ffc3e4350d0224a369f84 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Thu, 2 Jul 2026 18:54:51 +0530 Subject: [PATCH] fix(tui): suppress transient reconnect overlay flashes (#34924) --- packages/tui/src/app.tsx | 30 ++++++++++++- packages/tui/src/context/data.tsx | 3 ++ packages/tui/src/context/sdk.tsx | 7 ++- packages/tui/test/cli/tui/data.test.tsx | 60 +++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/app.tsx b/packages/tui/src/app.tsx index 32c7d4a8806..fa80ec5d1b0 100644 --- a/packages/tui/src/app.tsx +++ b/packages/tui/src/app.tsx @@ -1116,6 +1116,34 @@ function App(props: { onSnapshot?: () => Promise; pluginHost: TuiPlugi return render({ params: route.data.data }) }) + // Suppress the full-screen reconnecting overlay for transient disconnects (initial startup, host + // reload, sub-second event-stream blips). After the first successful connect, show it only once the + // connection has been lost for a full second; before the first connect give a longer grace period so + // startup never flashes it, but a server that dies before ever connecting still surfaces instead of + // leaving a silent empty app. Hide it immediately the moment status leaves "connecting". + const [showReconnecting, setShowReconnecting] = createSignal(false) + let reconnectTimer: ReturnType | undefined + createEffect(() => { + if (reconnectTimer) { + clearTimeout(reconnectTimer) + reconnectTimer = undefined + } + if (sdk.connection.status() !== "connecting") { + setShowReconnecting(false) + return + } + reconnectTimer = setTimeout( + () => { + reconnectTimer = undefined + setShowReconnecting(true) + }, + sdk.connection.connectedOnce() ? 1000 : 5000, + ).unref() + }) + onCleanup(() => { + if (reconnectTimer) clearTimeout(reconnectTimer) + }) + return ( Promise; pluginHost: TuiPlugi - + diff --git a/packages/tui/src/context/data.tsx b/packages/tui/src/context/data.tsx index 88e3649e638..97b2ec7492d 100644 --- a/packages/tui/src/context/data.tsx +++ b/packages/tui/src/context/data.tsx @@ -636,6 +636,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({ error() { return sdk.connection.error() }, + connectedOnce() { + return sdk.connection.connectedOnce() + }, }, session: { list() { diff --git a/packages/tui/src/context/sdk.tsx b/packages/tui/src/context/sdk.tsx index 0d5cce8a8fe..f8297dd80b5 100644 --- a/packages/tui/src/context/sdk.tsx +++ b/packages/tui/src/context/sdk.tsx @@ -25,9 +25,11 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ status: SDKConnectionStatus attempt: number error?: string + connectedOnce: boolean }>({ status: "connecting", attempt: 0, + connectedOnce: false, }) let stream: AbortController | undefined let pending: Promise | undefined @@ -68,7 +70,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ clearTimeout(timeout) attempt = 0 events.emit(first.value.type, first.value) - setConnection({ status: "connected", attempt: 0, error: undefined }) + setConnection({ status: "connected", attempt: 0, error: undefined, connectedOnce: true }) connected() while (!abort.signal.aborted && !controller.signal.aborted) { const event = await iterator.next() @@ -140,6 +142,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({ error() { return connection.error }, + connectedOnce() { + return connection.connectedOnce + }, }, reload, } diff --git a/packages/tui/test/cli/tui/data.test.tsx b/packages/tui/test/cli/tui/data.test.tsx index ed4f4079930..bdf928cb48c 100644 --- a/packages/tui/test/cli/tui/data.test.tsx +++ b/packages/tui/test/cli/tui/data.test.tsx @@ -169,6 +169,66 @@ test("reconnects the event stream and bootstraps fresh data", async () => { } }) +test("connectedOnce is false until first connect and persists across disconnect", async () => { + const encoder = new TextEncoder() + let stream: ReadableStreamDefaultController | undefined + const eventResponse = () => + new Response( + new ReadableStream({ + start(controller) { + stream = controller + }, + }), + { headers: { "content-type": "text/event-stream" } }, + ) + const connect = () => + stream?.enqueue( + encoder.encode(`data: ${JSON.stringify({ id: "evt_connected", type: "server.connected", data: {} })}\n\n`), + ) + const disconnect = () => { + stream?.close() + stream = undefined + } + + const calls = createFetch((url) => { + if (url.pathname === "/api/event") return eventResponse() + }) + let data!: ReturnType + + function Probe() { + data = useData() + return + } + + const app = await testRender(() => ( + + + + + + + + + + )) + + try { + await wait(() => stream !== undefined) + expect(data.connection.status()).toBe("connecting") + expect(data.connection.connectedOnce()).toBe(false) + + connect() + await wait(() => data.connection.status() === "connected") + expect(data.connection.connectedOnce()).toBe(true) + + disconnect() + await wait(() => data.connection.status() === "connecting") + expect(data.connection.connectedOnce()).toBe(true) + } finally { + app.renderer.destroy() + } +}) + test("tracks session status from active sessions and execution events", async () => { const events = createEventStream() const calls = createFetch((url) => {