Repository navigation
fix(webui): surface the idle disconnect through the event channel's health (Q-b) - #26
Merged
Merged
Conversation
… about
Q-b, reproduced first against the built client: with no turn running,
killing every socket and gating new ones left the page with no visible
signal at all — no banner, no alert, no 重连 text anywhere. The
connection banner from Q-1 reads the session stream's phase, which
stays idle ("connected") between turns, and the one long-lived link in
that state — the watchEvents subscription, kept by the shell from boot
— reconnected silently on its 250ms loop while telling nobody.
watchEvents now reports its socket's health into a small module store
(connection-health.ts): healthy on the server's acceptance of the
watch, down on socket close, unregistered on unsubscribe. Reporting
lives inside the transport, so every current and future watcher — the
shell's and the workspace panels' — counts without any call site
wiring callbacks, and the WebuiClientEventWatcher contract is
unchanged.
The store's aggregation rule is the load-bearing part: a watcher is
degraded only once it has been healthy and then went down. A
never-accepted watcher (boot in progress, or a host that never answers
events) is unknown, not degraded — otherwise every page load would
flash a reconnecting banner over a link that never existed.
ConnectionStatus merges the two signals: a terminal stream failure
still outranks everything; otherwise either the stream reconnecting or
a degraded channel shows 正在重连; the region stays hidden when both
are fine. The channel recovers on its own loop, so no retry affordance
is added for it — the banner leaves by itself when the watch is
accepted again.
The browser fixture gains gateSockets(open) + dropAll(): drop every
live socket and decide whether new connections may form — the
between-turns outage shape, deterministic in both directions.
Tests: connection-health.test.ts (store aggregation — boot quiet,
healthy→down→healthy, multi-watcher, unregister stops counting — plus
the transport's reporting through a fake socket, including the
reconnect-accepted recovery); connection-status.test.tsx gains the
merge cases (idle+degraded shows 正在重连, boot/healthy stays hidden,
failed outranks degraded); connection-status.spec.mjs gains the
end-to-end idle disconnect: banner appears, holds while the outage
holds, clears on the watcher's own reconnect, and reappears for a
second outage.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Change
Roadmap Q-b: 空闲时断线,页面什么都不显示. Reproduced first against the built client, then fixed.
Reproduction (pre-fix, built client, real Chromium): with the app idle on
#session=A(no turn running), every live socket was dropped and new connections gated shut via the fixture's newgateSockets(false)+dropAll(). After 1.5s: connection banner count 0,role=alertcount 0, and no 重连 / 连接失败 text anywhere on the page — the disconnect was completely invisible.Why: the WebUI holds no single connection — every request opens its own WebSocket, and the Q-1 banner reads the session stream's phase, which stays
idle(projected "connected") between turns. The only long-lived link in the idle state is thewatchEventssubscription the shell keeps from boot; when it drops, the transport's own 250ms reconnect loop retries silently and tells nobody.Fix — three small pieces:
connection-health.ts(new): a module store for the event channel's health. A watcher is degraded only once it has been healthy and then went down — a never-accepted watcher (boot in progress, or a host that never answers events) is unknown, not degraded, so a page load never flashes a reconnecting banner over a link that never existed. Aggregation: any once-healthy watcher being down degrades the channel; a watcher that unsubscribes stops counting.transport.tswatchEvents: registers on subscribe, marks healthy when the server accepts the watch (the same frameonReconnectkeys on), marks down on socket close, unregisters on unsubscribe. Reporting lives inside the transport, so all three current watchers (shell + two workspace panels) — and any future one — are covered with zero call-site changes and zeroWebuiClientEventWatchercontract changes.ConnectionStatus: merges the two signals — a terminal streamfailedstill outranks everything; otherwise the stream'sreconnectingor a degraded channel shows 正在重连 (「连接中断,正在自动恢复」); both fine → hidden. The channel recovers on its own 250ms loop, so no manual retry affordance is added for it — verified that the banner leaves by itself when the link returns.Post-fix verification (same environment as the repro): idle + healthy → banner count 0; drop+gate → banner
reconnecting「正在重连 · 连接中断,正在自动恢复」 and it holds while the outage holds; gate reopened → banner clears by itself once the watcher's reconnect is accepted; a second outage shows the banner again (the state machine does not wedge off after recovery).Validation
pnpm test:webui— 82 files / 1647 tests passed, including newconnection-health.test.ts(8: store aggregation — boot stays quiet, healthy→down→healthy, multi-watcher any-down, unregister stops counting, unknown tokens ignored; transport reporting through a fake socket — accept/close, reconnect-accepted recovery withonReconnectper acceptance, unsubscribe unregisters) and 3 new merge cases inconnection-status.test.tsx(idle+degraded → 正在重连; boot/healthy → hidden; failed outranks degraded, reason and retry button intact).npx playwright test— 86/86 passed, including the new end-to-end idle-disconnect case inconnection-status.spec.mjs(appear → hold → auto-clear → reappear).pnpm typecheck:webui-full,pnpm check:source(inventory regenerated),pnpm verify— passed, 20 gates on darwin.gateSockets(open)+dropAll(); existing specs untouched (default gate open = previous behaviour).NOT RUN / boundaries
Publication and contribution checks
release/public-source.json; new tests are declared intest/vitest-suites.jsonwhere applicable.Maintainer handoff
Publication scope or license changes (if any): none.
Shared-source port: not needed.