feat(web): restore last org + agent + thread on reopen#3945
Merged
Conversation
Cold app entry ('/') previously landed returning users on the org home.
Now it restores the exact thread they last had open — same org, agent
(virtualmcpid), and taskId.
- last-location.ts: tiny localStorage-backed read/save/clear helper.
- unifiedChatRoute.beforeLoad: persist {org, taskId, virtualmcpid} on every
navigation to a thread (preloading is off, so it only fires on real nav).
- homeRoute.beforeLoad: redirect to the saved /$org/$taskId before the
existing lastOrgSlug fast path; read is synchronous so cold entry stays
instant. autosend and other transient search params are intentionally
not restored.
- org-access-gate: also clear last-location (not just lastOrgSlug) when the
restored org is stale, so a revoked-access org can't loop back to itself.
A stale taskId self-heals via useEnsureTask (re-fetch, else idempotent
create); a stale org self-heals via OrgAccessGate.
useEditor returns Editor | null, but the content-sync effect only checked editor?.isDestroyed — null falls through to editor.commands.setContent and throws 'Cannot read properties of null (reading commands)'. Hits when the effect runs before the editor finishes initializing while tiptapDoc already has content (e.g. restoring into a thread). Guard the null case so editor is narrowed before .commands.
lastLocation only updates when a thread is opened, but lastOrgSlug updates on every org view (incl. switching to an org's home). After switching orgs without opening a thread, the two disagree and restoring lastLocation first would bounce the user back to the previous org's thread. Gate thread restore on lastLocation.org === lastOrgSlug; otherwise fall through to the org slug.
The previous gate relied on lastOrgSlug, which is written inside shell-layout's suspense queryFn and doesn't reliably re-run on an in-app org switch when the target org is already cached — so B->A switches kept the stale org and reopened on B. A full refresh re-ran the query and masked it. Record the org directly in orgLayout.beforeLoad (re-runs whenever the $org param changes, incl. a switch); the thread route still adds taskId/virtualmcpid. homeRoute now restores from lastLocation alone (taskId -> thread, else org), so the two can't disagree. taskId is now optional on LastLocation.
orgLayout.beforeLoad records the current org optimistically (before membership is known), so the gate's lastLocation match was always true for the org in the URL — making it bounce to / instead of showing not-found/no-access. That broke the org-access-gate e2e specs. Decide the bounce on the cached slug only (the original optimistic-redirect self-heal); for a bad org, just clear a matching lastLocation so it's never restored, without bouncing.
decocms Bot
pushed a commit
that referenced
this pull request
Jun 16, 2026
PR: #3945 feat(web): restore last org + agent + thread on reopen Bump type: minor - decocms (apps/mesh/package.json): 3.25.0 -> 3.26.0
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.
Summary
When you reopen Studio, it now opens where you left off — same org, same agent, same thread — instead of dropping you on the org home.
Org restore already existed (
lastOrgSlug). This adds the missing pieces: the agent (virtualmcpid) and the thread (taskId).How it works
lib/last-location.ts(new) — a tiny localStorage-backed{ org, taskId, virtualmcpid }record withsave/read/clear.unifiedChatRoute.beforeLoadsaves the location on every navigation to a thread.defaultPreloadis off, so this only fires on real navigation (not hover-preload).homeRoute.beforeLoad(path/) redirects to the saved/$org/$taskId?virtualmcpid=…before the existinglastOrgSlugfast path. The read is synchronous, so cold entry stays instant (no added network round-trip).autosendand other transient search params are intentionally not restored, so reopening never re-sends a message.OrgAccessGatenow also clearslast-location(not justlastOrgSlug) when the restored org turns out to be stale, so a revoked-access org can't loop back to itself. A staletaskIdself-heals viauseEnsureTask(re-fetch from server; idempotent create only if it genuinely doesn't exist).Affected areas
apps/mesh/src/web/index.tsx— restore + persist hooksapps/mesh/src/web/lib/last-location.ts— new helperapps/mesh/src/web/lib/localstorage-keys.ts— newlastLocationkeyapps/mesh/src/web/components/org-access-gate.tsx— loop-safe self-healTesting
bun run --cwd=apps/mesh check— no type errors in the touched files.bun run lint/bun run fmt— clean (the 2 lint warnings are pre-existing inpackages/sandbox)./.🤖 Generated with Claude Code
Summary by cubic
On reopen, Studio now restores where you left off: same org, agent (
virtualmcpid), and thread (taskId). Uses a singlelast-locationsource of truth, won’t undo a recent org switch, and avoids auto-resend and extra network on cold entry.New Features
last-locationlocalStorage helper (save/read/clear).orgLayout.beforeLoad; addtaskId/virtualmcpidviaunifiedChatRoute.beforeLoad.homeRoute.beforeLoadfromlast-location(thread first, else org); fall back tolastOrgSlugonly if absent.taskIdis recovered byuseEnsureTask.Bug Fixes
editorin the Tiptap content-sync effect to prevent a startup crash when restoring into a thread.OrgAccessGate: show not-found/no-access for deliberately bad orgs; only bounce to “/” when the visit came from the cachedlastOrgSlug. Clear a matchinglast-locationso it won’t be restored.Written for commit fc56bf7. Summary will update on new commits.