Repository navigation
fix(webui): make every connection and boot failure visible, with a way back - #23
Merged
Merged
Conversation
…y back Three gaps in error visibility, closed together because they form one story: something fails, the page says so, and the user has an action. 1. ConnectionStatus existed with zero consumers — no phase ever reached the page. The shell now mounts it in the main column under the usage banner slot, with hideWhenConnected so a healthy connection earns no pixels and 正在重连 / 连接失败 appear on their own. The component gains the two host calls its docblock reserved: hideWhenConnected (layout) and onRetry (recovery affordance, failed state only). 2. main.tsx threw before root.render for a missing #webui-root or a missing __WEBUI_CONFIG__; the error boundary mounts inside that render, so both were white pages. Both now render WebuiStartupFallback — a human sentence, the technical cause shown verbatim, and one action: 刷新重试. Rendered into #webui-root when the node exists, or a fresh body node when even the mount point is gone. The happy path stays gated on both preconditions. 3. The stream loop's automatic recovery (drop → reconnecting → one resumeSession attempt) ended at refused with nothing able to re-enter it. createSessionStreamRetry is the manual arm the banner's 重试连接 button runs: it clears the standing refusal synchronously and re-runs the same attach loop used for server-started turns, anchored on the last applied cursor (or persisted history when no cursor was recorded). One click is one attempt; a still-dead server refuses again through the identical path. The browser fixture's delayEvery now also holds sendMessage / resumeSession (the auto-answer used to win the race), which is what makes the transient reconnecting window observable at all. Tests: connection-status.test.tsx gains the host-call cases plus a shell-mount source assertion; webui-startup-fallback.test.tsx covers the surface and the main.tsx wiring; session-stream-retry.test.ts drives the retry against the real runtime store (cursor anchor, history anchor, one-attempt refusal). connection-status.spec.mjs proves both chains in a real browser: drop → 正在重连 on the page → cleared when the resume lands, and unresumable drop → 连接失败 with the transport's reason → retry reopens the stream and delivers frames again.
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-1 (错误可见性收口). Three gaps, closed as one change because they form a single story: something fails → the page says so → the user has an action.
src/were its own interface and component). No stream phase ever reached the page. The shell now mounts it in the main column, under the usage-banner slot, withhideWhenConnected— a healthy connection earns no pixels; 正在重连 / 连接失败 appear on their own. The component gains the two host calls its own docblock had reserved as "the host's layout call":hideWhenConnectedandonRetry(retry button renders in the failed state only — reconnecting is the automatic loop mid-attempt).root.renderfor a missing#webui-rootor a missing__WEBUI_CONFIG__; the error boundary mounts inside that render, so both were white pages. Both now renderWebuiStartupFallback— one human sentence, the technical cause verbatim, one action (刷新重试 →location.reload()). Rendered into#webui-rootwhen the node exists, or a fresh body node when even the mount point is gone. The happy path stays gated on both preconditions.refusedwith no way back. The automatic arm (drop →reconnecting→ oneresumeSessionattempt) was already instream-loop.tsand works; what was missing was the manual arm after the loop commitsrefused.createSessionStreamRetry(newsession-stream-retry.ts) clears the standing refusal synchronously and re-runs the same attach loop used for server-started turns, anchored on the last applied cursor (or the newest persisted message when no cursor was recorded). One click is one attempt: a still-dead server refuses again through the identical path and the banner returns with the new reason.Also: the browser fixture's
delayEvery/delayNextnow also holdsendMessage/resumeSession— the stream auto-answer used to run first and close the transientreconnectingwindow within a microtask, making it unobservable. No existing spec delayed those two operations (verified), so behaviour elsewhere is unchanged.Q-2 boundary (not in this PR)
多语言(中英切换)is deliberately not started — the roadmap routes the 接不接 decision through 莫克 first. When picked up: the language entry belongs in the settings page, which overlaps K-zone (izzy's 个性化设置 tab) and P-zone (设置中心, unclaimed). Whichever of K/P lands first should own the tab's unlock; the other must not re-unlock the same tab in a second PR. This PR deliberately adds new zh copy (banner, startup fallback) without touching any settings tab, so it creates no conflict with either zone.
Validation
Real-browser verification (required by the task; not unit-test-only)
Unable to load projects: WebUI connection failed, the transcript area showsUnable to load messages; after sending a message the connection banner appears: 连接失败 + the transport's own reason (WebUI connection failed) + a 重试连接 button.throwinWebuiSessionTranscript, built, then reverted before commit): switching sessions renders the boundary — 界面出现错误 + the error message verbatim; clearing the flag and clicking 重试 unmounts the boundary and the transcript of the newly selected session renders. Not console-only.__WEBUI_CONFIG__(variant page, no config injected): the page paints 页面未能启动 +WebUI runtime configuration is missing (__WEBUI_CONFIG__)+ 刷新重试 — not blank.#webui-root(variant page, mount node removed, config present): same surface withWebUI mount node #webui-root is missing.Automated
pnpm test:webui— 80 files / 1591 tests passed, including:connection-status.test.tsx+5 cases (hide/attention states, retry-button gating, shell-mount source assertion), newwebui-startup-fallback.test.tsx(7), newsession-stream-retry.test.ts(3, driven against the real runtime store).npx playwright test(test:webui-browser, chromium, built artifact) — 80/80 passed, including the 2 newconnection-status.spec.mjscases: drop → bannerreconnectingvisible on the page → cleared when the held resume lands; unresumable drop → bannerfailedwith the transport reason → 重试连接 click reopens the stream and a frame emitted afterwards lands in the transcript.pnpm typecheck:webui-full— passed.pnpm verify— passed, 20 gates on darwin (skips by design:test:webui-browsernot applicable on this platform,test:windows,test:release-package).pnpm check:sourcepassed.NOT RUN, platform limitations and live-service 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.