fix(frontend): keep the open workspace/channel across a reload#250
Merged
Conversation
The chat route was `/chat/*` and carried no selection, while `selectedWorkspaceId` lived only in memory (chatStore persists just `lastChannelByWorkspace`). So after every reload it was necessarily null and ChatLayout's bootstrap unconditionally fell back to the personal workspace — a user reading a team workspace got dumped back home, and a channel could never be linked or shared. Put the selection in the path (`/chat/:workspaceId?/:channelId?`) and make that the source of truth across reloads. The store stays the sole selection API — all 11 call sites (rail, sidebar, dialogs, invite and register flows) are untouched — with ChatLayout reconciling the two in one place: path → store on mount and back/forward, store → path for in-app selections. `hydrateSelection` applies an explicit pair without selectWorkspace's last-channel derivation, which would otherwise clobber the channel the path names. Two things worth flagging for review: - The mirror navigates with `replace`, not `push`. On mobile the conversation screen is itself a pushed history entry, so an entry per channel switch would make Back land on the previous channel instead of popping to the list. Refresh-restore and shareable links both work; browser back/forward *between channels* is deliberately not wired up, as that needs the mobile history model reworked first. - loadWorkspaces read `selectedWorkspaceId` through its mount-time closure, which the path now races: the captured null would overwrite the workspace just restored. It reads fresh via getState() and also validates membership, so a stale or foreign link falls back home instead of resting on a workspace that renders empty. Verified against the kind stack (vite dev → in-cluster gateway): reload on a team workspace stays put; reload on a channel restores it; a non-member workspace id in the path falls back and corrects the URL. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2 tasks
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 description
Refreshing the page dropped you back into the personal workspace, no matter which team workspace you were reading.
The chat route was
/chat/*and carried no selection, whileselectedWorkspaceIdlived only in memory (chatStorepersists justlastChannelByWorkspace). After a reload it was therefore alwaysnull, and ChatLayout's bootstrap fell back to the personal workspace unconditionally. The same gap meant a channel could never be linked or shared — the URL was always just/chat.This puts the selection in the path (
/chat/:workspaceId?/:channelId?) and makes that the source of truth across reloads.The store stays the sole selection API, so all 11 call sites (rail, sidebar, dialogs, invite/register flows) are untouched. ChatLayout reconciles the two in one place: path → store on mount and back/forward, store → path for in-app selections.
hydrateSelectionapplies an explicit pair withoutselectWorkspace's last-channel derivation, which would otherwise clobber the channel the path names.Two things worth a reviewer's attention
The mirror navigates with
replace, notpush— a deliberate trade-off. On mobile the conversation screen is itself a pushed history entry (openChatScreen), so adding one per channel switch would make Back land on the previous channel instead of popping to the channel list, breaking the existing back gesture. Refresh-restore and shareable links both work; browser back/forward between channels is knowingly not wired up, since that needs the mobile history model reworked first.location.stateis carried through the mirror for the same reason.A latent race in
loadWorkspaces— it readselectedWorkspaceIdthrough its mount-time closure, which the path now races: the capturednullwould land when the request resolved and overwrite the workspace the URL had just restored. It now reads fresh viagetState(), and also validates membership, so a stale or foreign link falls back home rather than resting on a workspace that renders empty.Change type
Test
npm run buildno error reportedManual verification:
parity-wsparity-ws(previously bounced to Personal)#claude-smokeis openpytestis N/A — the Python backend is gone (gateway-only perdocs/arch/ARCHITECTURE_OVERVIEW.md).Note for anyone pulling this branch: the worktree's
node_moduleswas stale and missingyaml, which white-screened vite and broketsc.npm installfixes it;package-lock.jsonis unchanged. The two pre-existingPdfViewer.tsxpdf.js type errors are untouched and out of scope.Database migration
Security and Release Impact
.env, logs, databases, uploaded files, private keys, tokens or production configurationsRelated Issues
Closes #
🤖 Generated with Claude Code