fix(tui): suppress transient reconnect overlay flashes (#34924)

This commit is contained in:
Shoubhit Dash 2026-07-02 18:54:51 +05:30 committed by GitHub
parent f016392368
commit 7ec7413fdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 98 additions and 2 deletions

View file

@ -1116,6 +1116,34 @@ function App(props: { onSnapshot?: () => Promise<string[]>; 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<typeof setTimeout> | 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 (
<box
width={dimensions().width}
@ -1161,7 +1189,7 @@ function App(props: { onSnapshot?: () => Promise<string[]>; pluginHost: TuiPlugi
<Show when={!startup.skipInitialLoading}>
<StartupLoading ready={ready} />
</Show>
<Show when={sdk.connection.status() === "connecting"}>
<Show when={showReconnecting()}>
<Reconnecting attempt={sdk.connection.attempt()} error={sdk.connection.error()} />
</Show>
</box>

View file

@ -636,6 +636,9 @@ export const { use: useData, provider: DataProvider } = createSimpleContext({
error() {
return sdk.connection.error()
},
connectedOnce() {
return sdk.connection.connectedOnce()
},
},
session: {
list() {

View file

@ -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<void> | 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,
}

View file

@ -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<Uint8Array> | undefined
const eventResponse = () =>
new Response(
new ReadableStream<Uint8Array>({
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<typeof useData>
function Probe() {
data = useData()
return <box />
}
const app = await testRender(() => (
<TestTuiContexts>
<SDKProvider client={createClient(calls.fetch)} api={createApi(calls.fetch)}>
<ProjectProvider>
<DataProvider>
<Probe />
</DataProvider>
</ProjectProvider>
</SDKProvider>
</TestTuiContexts>
))
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) => {