Calm entry and error states #344
- MA
## Why
Cold loads flashed incomplete chrome, ordinary documents showed “Opening parent document…”, and failed routes used internal language outside the workspace frame. Hosted sign-in also looked unrelated to local sign-in. A `/channels/:id` deep link could mount and refetch the room twice, then remount its parent when a child opened or closed.
## What changed
- Loading keeps the real sidebar chrome and quiet skeleton rows in place. The document label appears after 600 ms and uses “Opening document…”. - Not-found, access, and generic failures appear inside the workspace frame with plain language and a useful recovery action. Unknown repositories offer Add project; 401/403 notices do not link to an inaccessible repository. - Hosted sign-in reuses `LocalLoginShell` and the shared GitHub icon. - A `/channels/:id` link resolves to its canonical document path before any room mounts. The document host mounts once under the document route, so opening and closing a child keeps the same parent room. Stale channel resolutions and metadata callbacks cannot redirect a newer route. This brings the focused fix and regression tests from #391 into this PR. - Returning from a child looks up the live parent research card when focus is restored, after the card finishes loading.
## Screenshots
Hosted sign-in, desktop:

Document not found, desktop:

Phone layout:

## Verification
- `bun run types` and `bun run ci` pass on the combined branch. - `bun test apps/web` passed all 535 tests before the final #337 preflight rebase. Focused hosted, route-swap, navigation, and menu tests pass on the combined branch. - `bun run build` passes the initial JavaScript budget on the combined branch: 249,951 B raw / 78,851 B gzip. - Browser regression tests cover one room mount and one channel read across a deep link, parent preservation while opening and closing a child, and focus restoration to the live research card after reconnect. The latest run is awaiting GitHub CI; local E2E uses shared ports.
## Design-contract review
The reviewed dynamic-owner hashes were renewed for the combined source. `document-workspace-host.tsx` changed route callback authority without changing the data or callbacks forwarded through `workspaceProps`. The sidebar and navigation still compose finite, token-based class choices.
- MA
**Review:** Changes needed
The visual side is good: hosted sign-in matches local sign-in on desktop and phone, the in-shell notices are calm and plainly worded (sentence case, no "channel"), the 600 ms silent loading feels right, and the icon-free loading chrome reads as quiet rather than broken. Rename via a `/channels/:id` entry updates the URL without a remount, archive works, back/forward work, and Add project from the unknown-repository notice works. The route aliasing in `hosted.tsx` still remounts the workspace, though, in two places. I traced this with a MutationObserver on `[data-workspace-room]` plus an `/api` request log on a fake-GitHub server.
**Blocking**
1. **`/channels/:id` still remounts the room and fetches it twice.** In `hosted.tsx` ~L414-419 the `channelSources` cache is only filled once the layer's source is a document. On the first render after canonicalization it creates a **new** `{ id, page: "channel" }` object. `ChannelWorkspace`'s effect depends on `source`, so it runs again: `setLoaded(undefined)` → `Loading` → a second `GET /api/channels/:id` → remount. Trace: `MOUNT room a5b4… / UNMOUNT room a5b4… / LOADING / MOUNT room a5b4…` (17 ms apart), with two `/api/channels/a5b4…` requests. The 600 ms label delay hides this, but the editor still remounts, and it is now a hard cut where main had a crossfade. Fix (I checked it locally: one mount, one request): seed the cache with the original channel source. ```ts let channelSource = source.page === "channel" ? source : undefined; if (channelSource && !channelSources.current.has(layer.key)) channelSources.current.set(layer.key, channelSource); ``` You could also make the effect depend on `source.id`.
2. **The persistent `document:… → channel:…` alias breaks child documents for that parent.** `ready()` (L376-379) aliases the canonical document identity to the channel key for the whole session. From then on that document renders through `ChannelWorkspace`, even when you reach it later from the sidebar. Child routes are keyed by `document:<parent>`, so they never match the channel layer: - **Opening a child** of a parent entered via `/channels/:id` builds a new layer and remounts the parent: `MOUNT a5b4 + MOUNT dddd … UNMOUNT a5b4`, plus refetches. - **Escape/close** swaps back to the channel layer and remounts the parent again, with another `/api/channels/…` fetch. - **Entering via the document URL** keeps the parent mounted through child open and close, which is the intended behaviour.
On main the channel route canonicalized to a `document:` layer, so children did not remount the parent after the first load. Suggested fix: resolve `/channels/:id` to its canonical path *before* mounting any workspace, so `DocumentWorkspaceHost` mounts once under the `document:` key. Alternatively, drop the document→channel alias once a document or child route for that parent is requested. Please add an e2e for "channel deep link → open child → close child keeps the same room element".
**Should fix**
3. `hosted.tsx` ~L453: `onMetadataPath` passes `requestedRoute.current.routeKey` as the metadata identity, so the `metadataRouteKey !== authority.routeKey` check in `metadataPath` can never fail. Pass the layer's own identity captured at render (`documentRouteIdentity(layer.source)`), as `DocumentWorkspaceHost` does. AGENTS.md: derive async results from stable request identity. 4. 401/403 copy, `failureCopy`/`Failure`: "You don't have access to octo-org/score" offers **Open octo-org/score** as the primary action. That link is a dead end for someone without access. Drop the repository link for 401/403. 5. A channel 404 with no recovery record shows "Document not found" with **no action at all**. Add a fallback ("Go to Chopin"), especially on phone where the sidebar is hidden. 6. Budget note is inaccurate: the CI `container` job measured **81,946 B gzip > 81,500**, so gzip is also over on Linux, not only raw (257,921 B). Please update "Needs a budget decision".
**Nits**
7. `navigation-shell.tsx` ~L797-817: the `onKeyDown` / Sign out reformat is unrelated churn. It also widens the dynamic-owner hash renewal (L795). Please revert it. 8. Sidebar cold load: the label goes **New document** (skeleton) → **Loading projects…** (real sidebar while the creation target loads) → **New document**. The skeleton bars then vanish before rows arrive (skeleton → empty "Projects" → rows). Match the skeleton label to the real loading label, or keep the skeleton until the project list has loaded.
**Checks:** `format, lint, types, tests` fails only on the listed design-contract dynamic-owner hashes, which is acceptable. `e2e` and `container` fail at `bun run build` on the bundle budget, so **browser integration did not run**, including the updated `research-child-surface.e2e.ts`.
MaggieAppletonreferenced this in Keep workspace chrome still when switching documents #346last month on GitHub
MaggieAppletonhead-ref-force-pushed 834a007fba67last month on GitHub
MaggieAppletonbase-ref-changedlast month on GitHub
MaggieAppletonbase-ref-force-pushedlast month on GitHub
MaggieAppletonautomatic-base-change-succeededlast month on GitHub
MaggieAppletonhead-ref-force-pushed 437948d512f0last month on GitHub
MaggieAppletonreferenced this in Resolve channel links before mounting a document workspace #391last month on GitHub
MaggieAppletonhead-ref-force-pushed e2e71922f519last month on GitHub
MaggieAppletonreferenced 81c8ff84571dlast month on GitHub
MaggieAppletonhead-ref-force-pushed 81c8ff84571dlast month on GitHub
MaggieAppletonauto-rebase-enabledlast month on GitHub
MaggieAppletonauto-merge-disabledlast month on GitHub
MaggieAppletonhead-ref-force-pushed a81e679d00b0last month on GitHub
MaggieAppletonmerged f1e81673334blast month on GitHub
MaggieAppletonhead-ref-deletedlast month on GitHub
MaggieAppletonreferenced 9261fa6bc6dblast month on GitHub