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

View file

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

View file

@ -25,9 +25,11 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
status: SDKConnectionStatus status: SDKConnectionStatus
attempt: number attempt: number
error?: string error?: string
connectedOnce: boolean
}>({ }>({
status: "connecting", status: "connecting",
attempt: 0, attempt: 0,
connectedOnce: false,
}) })
let stream: AbortController | undefined let stream: AbortController | undefined
let pending: Promise<void> | undefined let pending: Promise<void> | undefined
@ -68,7 +70,7 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
clearTimeout(timeout) clearTimeout(timeout)
attempt = 0 attempt = 0
events.emit(first.value.type, first.value) events.emit(first.value.type, first.value)
setConnection({ status: "connected", attempt: 0, error: undefined }) setConnection({ status: "connected", attempt: 0, error: undefined, connectedOnce: true })
connected() connected()
while (!abort.signal.aborted && !controller.signal.aborted) { while (!abort.signal.aborted && !controller.signal.aborted) {
const event = await iterator.next() const event = await iterator.next()
@ -140,6 +142,9 @@ export const { use: useSDK, provider: SDKProvider } = createSimpleContext({
error() { error() {
return connection.error return connection.error
}, },
connectedOnce() {
return connection.connectedOnce
},
}, },
reload, 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 () => { test("tracks session status from active sessions and execution events", async () => {
const events = createEventStream() const events = createEventStream()
const calls = createFetch((url) => { const calls = createFetch((url) => {