Skip to content

feat(web): restore last org + agent + thread on reopen#3945

Merged
pedrofrxncx merged 5 commits into
mainfrom
feat/restore-last-location
Jun 16, 2026
Merged

feat(web): restore last org + agent + thread on reopen#3945
pedrofrxncx merged 5 commits into
mainfrom
feat/restore-last-location

Conversation

@pedrofrxncx

@pedrofrxncx pedrofrxncx commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

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 with save / read / clear.
  • PersistunifiedChatRoute.beforeLoad saves the location on every navigation to a thread. defaultPreload is off, so this only fires on real navigation (not hover-preload).
  • RestorehomeRoute.beforeLoad (path /) redirects to the saved /$org/$taskId?virtualmcpid=… before the existing lastOrgSlug fast path. The read is synchronous, so cold entry stays instant (no added network round-trip). autosend and other transient search params are intentionally not restored, so reopening never re-sends a message.
  • Self-healOrgAccessGate now also clears last-location (not just lastOrgSlug) when the restored org turns out to be stale, so a revoked-access org can't loop back to itself. A stale taskId self-heals via useEnsureTask (re-fetch from server; idempotent create only if it genuinely doesn't exist).

Affected areas

  • apps/mesh/src/web/index.tsx — restore + persist hooks
  • apps/mesh/src/web/lib/last-location.ts — new helper
  • apps/mesh/src/web/lib/localstorage-keys.ts — new lastLocation key
  • apps/mesh/src/web/components/org-access-gate.tsx — loop-safe self-heal

Testing

  • 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 in packages/sandbox).
  • Not yet run live — needs a manual pass:
    1. Open a thread under an agent, send a message.
    2. Close the tab / quit the PWA, reopen at /.
    3. Expect to land back in that same thread (org + agent + thread), with no auto-resend.
    4. Lose access to that org (or delete it) → reopening should bounce cleanly to a valid org, not loop or dead-end.

Note: this changes the default landing for returning users from the org home to their last thread — which is the intent of the request.

🤖 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 single last-location source of truth, won’t undo a recent org switch, and avoids auto-resend and extra network on cold entry.

  • New Features

    • Added a last-location localStorage helper (save/read/clear).
    • Record org via orgLayout.beforeLoad; add taskId/virtualmcpid via unifiedChatRoute.beforeLoad.
    • Restore on “/” via homeRoute.beforeLoad from last-location (thread first, else org); fall back to lastOrgSlug only if absent.
    • Self-heal: stale taskId is recovered by useEnsureTask.
  • Bug Fixes

    • Guarded null editor in 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 cached lastOrgSlug. Clear a matching last-location so it won’t be restored.

Written for commit fc56bf7. Summary will update on new commits.

Review in cubic

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.
@pedrofrxncx pedrofrxncx merged commit 99ef093 into main Jun 16, 2026
14 checks passed
@pedrofrxncx pedrofrxncx deleted the feat/restore-last-location branch June 16, 2026 04:04
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
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