Skip to content

feat(webui): reopen-state parity — sessions and UI state survive a reload (webui-parity slice 07) - #50

Merged
fengzhi09 merged 1 commit into
mainfrom
fix/reopen-state-parity
Sep 27, 2026
Merged

fengzhi09 merged 1 commit into
mainfrom
fix/reopen-state-parity

Conversation

@fengzhi09

Copy link
Copy Markdown
Collaborator

What

Slice 07 closes the explicit user requirement 「要支持 页面关闭后再打开不会乱」.

The measured gap before this slice: localStorage held only webui_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.

  • The session is addressable — ?session=<id>. Cold load and popstate call applySessionRestore() (server wins); SSE snapshots replaceState the URL to the authoritative session; an unknown or deleted id drops the URL and surfaces a visible hint instead of silently jumping.
  • One persistence keyspace, reusing slice 01's webui: namespace rather than a parallel scheme: webui:ui:v1:<cid> (panel, tab, sidebar, last session) and webui:scroll:v1:<cid>:<sessionId> (per-session scroll position), both version- and cid-guarded, debounced.
  • Server wins, no cross-talk — the URL is the cold-load key; once the first SSE frame lands, the authoritative-session effect mirrors state.mcodeSessionId back into the URL.
  • Real error boundaries — app/global-error.tsx (root, self-contained) and app/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

  • Refresh restores session AND scroll — verified live twice (4200px, re-run at 5000px) with panel and tree state surviving the same reload.
  • Keyspace — corrupt JSON and version-2 payloads are dropped with a safe boot; two browser contexts get distinct cids with non-interfering keys; slice 01's tree expansion still restores (panels.tsx untouched by the diff).
  • Server wins — a stale bogus id yields the hint banner and the URL is rewritten to the server's authoritative session; a fresh cid lands home.
  • Error boundaries are real, not cosmetic — the agent injected genuine render throws in its own environment and reverted them: error.tsx verified in dev and prod with working actions, and global-error.tsx verified 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 .status to thrown errors.

Merge-order note: this branch edits chat.tsx, page.tsx and i18n.ts, which slices 06 and 12 also touch. The acceptance mapped the overlap (07 and 12 touch different regions of chat.tsx; both add ChatProps fields) — low textual and semantic conflict risk, but the landing order matters. Recommended order: 07 → 06 → 12.

Gates

test:webapp 424/424 · typecheck ✓ · build ✓ · check:source ✓ (4605 files)

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.
@fengzhi09
fengzhi09 merged commit e7c5864 into main Sep 27, 2026
8 checks passed
@fengzhi09
fengzhi09 deleted the fix/reopen-state-parity branch September 27, 2026 12:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant