feat(webui): reopen-state parity — sessions and UI state survive a reload (webui-parity slice 07) - #50
Merged
Conversation
Goal: refresh / reopen does not silently reset the user to the home
screen. Deliver the "页面关闭后再打开不会乱" contract.
What this slice delivers:
1. URL-addressable session. ?session=<id> deep-links to a conversation
on cold load. Back / forward (popstate) re-evaluates the same
path. Server wins: an unknown or deleted session id falls back to
the home screen with a brief visible hint, not a silent jump.
The page and the URL stay in sync via replaceState, so a refresh
lands the user back on the same session.
2. Unified per-cid persistence under webapp/lib/persist.ts. The same
`webui:<feature>:<guard>` keyspace slice 01 established for the
file tree (webui:files-tree:<workspaceDir>) now hosts:
- webui:ui:v1:<cid> panel open/closed, sidebar collapsed
- webui:scroll:v1:<cid>:<sessionId> per-session transcript scroll
Every payload carries a version discriminator and a cid guard, so
an old payload or a different browser is dropped instead of
silently interpreted. Writes are best-effort, debounced (150 ms).
3. Error boundaries that show a readable page on render failure
instead of degrading to a bare 404 - the failure mode that cost
hours of debugging earlier in this slice's evidence chain.
- app/global-error.tsx root-level catch (layout errors)
- app/error.tsx route-level catch
Both render with the same shape: bilingual title, copyable
diagnostics (cid + build id + URL + UA + ts + stack, capped 2 KB),
and three actions (reload / home / copy). The copy-diagnostics
block is the regression-proof: future incidents leave a trail.
Server-vs-client merge: URL is the cold-load key; SSE is the source
of truth once the first frame arrives. If the SSE snapshot says the
active session differs from the URL, the URL is rewritten to match
the server and the hint banner clears - exactly the "server wins,
never silently drop the user" contract the dispatch brief named.
Slice boundaries (per AGENTS.md):
- Owned outright: webapp/lib/persist.ts, webapp/lib/url-restore.ts,
app/error.tsx, app/global-error.tsx, app/page.tsx (URL-restore +
panel persist only).
- Minimal additive to slice 12 files (panels.tsx, chat.tsx) and
shell.tsx: a single optional onScrollPersist / sessionKey prop
on Chat (5-10 lines each), a useState initializer that seeds
collapsed from the persisted UI state in AppShell. Each is
flagged with a webui-parity 07 comment.
Tests (test:webapp, 424 pass, 0 fail):
- ui-persist.test.ts deserialization round-trip, version/cid guard,
per-(cid, sessionId) scroll keys, anon fallback.
- url-restore.test.ts parse grammar (URL parsing edge cases),
applySessionRestore decision tree (ok / not-found / no-op /
error) via globalThis.fetch stub in the same pattern as
api-permissions.test.ts.
Self-check (browser, isolated dev port 18156, own DATA_DIR):
- open session A -> reload -> same session, scroll restored (1200 px).
- ?session=<bogus> -> home + hint banner; URL auto-clears.
- sidebar collapse survives reload.
- right-panel files open survives reload.
- two windows on different cids do not pollute each other.
- deliberate render-time error renders error.tsx with diagnostics,
not a 404.
Gates: webapp:typecheck 0 errors; test:webapp 424/424; webapp:build
emits /_next/static/chunks/app/{error,global-error}-*.js with the
expected data-testids; source-inventory --write + check:source pass.
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.
What
Slice 07 closes the explicit user requirement 「要支持 页面关闭后再打开不会乱」.
The measured gap before this slice:
localStorageheld onlywebui_cid; no UI state was persisted; the URL carried no session. Opening a 48,957-char session and refreshing silently dumped the user back to the home screen. The webapp also had no error boundary at all, so any render failure degraded into a bare Next 404 — which cost hours of debugging earlier the same day.?session=<id>. Cold load andpopstatecallapplySessionRestore()(server wins); SSE snapshotsreplaceStatethe URL to the authoritative session; an unknown or deleted id drops the URL and surfaces a visible hint instead of silently jumping.webui:namespace rather than a parallel scheme:webui:ui:v1:<cid>(panel, tab, sidebar, last session) andwebui:scroll:v1:<cid>:<sessionId>(per-session scroll position), both version- and cid-guarded, debounced.state.mcodeSessionIdback into the URL.app/global-error.tsx(root, self-contained) andapp/error.tsx(route, antd skin): bilingual title, reload / home / copy-diagnostics actions, diagnostics carrying cid, build version, URL, user agent, timestamp and a capped 2 KB stack.Acceptance (independent agent) — PASS-WITH-CONCERNS
panels.tsxuntouched by the diff).error.tsxverified in dev and prod with working actions, andglobal-error.tsxverified in production against a true layout-subtree throw.Disclosed limitations, judged acceptable: scroll restore fires after the persisted unit count stabilises (one RAF) and clamps when content shrank; the no-cross-talk guarantee is per-cid rather than per-tab (localStorage is per-origin by definition); the hint banner uses a string match because the typed request helper does not attach
.statusto thrown errors.Merge-order note: this branch edits
chat.tsx,page.tsxandi18n.ts, which slices 06 and 12 also touch. The acceptance mapped the overlap (07 and 12 touch different regions ofchat.tsx; both addChatPropsfields) — low textual and semantic conflict risk, but the landing order matters. Recommended order: 07 → 06 → 12.Gates
test:webapp424/424 · typecheck ✓ · build ✓ ·check:source✓ (4605 files)