fix: route a workspace switch through the editor's unsaved-changes prompt

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-09-23 18:01:24 +02:00
co-authored by Claude Opus 5.5
parent 0c770dd5fd
commit 147dd09330
3 changed files with 68 additions and 17 deletions
@@ -28,7 +28,6 @@
import { page } from '$app/state'
import MultiplayerMenu from './MultiplayerMenu.svelte'
import { isCloudHosted } from '$lib/cloud'
import { workspaceAIClients } from '../copilot/lib'
import { twMerge } from 'tailwind-merge'
import {
ambiguousWorkspaceNames,
@@ -63,7 +62,6 @@
// Read before switching: the switch swaps the stores these read out from under us.
const landOnHome = landsOnHome(id)
const href = workspaceHref(id)
workspaceAIClients.init(id)
// On the sessions page, the page's family reconcile swaps out a chat that
// doesn't belong to the new workspace's family.
await switchWorkspaceAndPage(id, { landOnHome, href })
@@ -8,7 +8,6 @@
} from '$lib/stores'
import { switchWorkspaceAndPage } from './workspaceSwitchUrl'
import { base } from '$lib/base'
import { workspaceAIClients } from '$lib/components/copilot/lib'
import WorkspaceFamilyPicker from '$lib/components/sessions/WorkspaceFamilyPicker.svelte'
import WorkspaceScopeTrigger from '$lib/components/WorkspaceScopeTrigger.svelte'
import {
@@ -51,7 +50,6 @@
function switchWorkspaceDirect(id: string) {
if ($workspaceStore === id) return
workspaceAIClients.init(id)
void switchWorkspaceAndPage(id)
}
@@ -1,8 +1,18 @@
import { page } from '$app/state'
import { replaceState } from '$app/navigation'
import { goto } from '$lib/navigation'
import { AppService, FlowService, JobService, ScriptService } from '$lib/gen'
import { get } from 'svelte/store'
import {
AppService,
FlowService,
JobService,
ScriptService,
type UserDraftItemKind
} from '$lib/gen'
import { switchWorkspace } from '$lib/storeUtils'
import { workspaceStore } from '$lib/stores'
import { UserDraftDbSyncer } from '$lib/userDraftDbSyncer.svelte'
import { workspaceAIClients } from '$lib/components/copilot/lib'
type ItemExists = (workspace: string, param: string) => Promise<boolean>
@@ -34,31 +44,70 @@ const jobExists: ItemExists = async (workspace, id) => {
}
}
type ItemPage = {
param: string
exists: ItemExists
// Set on editors, whose auto-save-off edits are parked under this draft kind.
draftKind?: UserDraftItemKind
}
// Pages showing one workspace item, keyed by route id, with the check that the item named
// by the route param exists in a given workspace.
const ITEM_PAGES: Record<string, { param: string; exists: ItemExists }> = {
'/(root)/(logged)/scripts/edit/[...path]': { param: 'path', exists: scriptExists },
const ITEM_PAGES: Record<string, ItemPage> = {
'/(root)/(logged)/scripts/edit/[...path]': {
param: 'path',
exists: scriptExists,
draftKind: 'script'
},
'/(root)/(logged)/scripts/get/[...hash]': { param: 'hash', exists: scriptHashOrPathExists },
'/(root)/(logged)/flows/edit/[...path]': { param: 'path', exists: flowExists },
'/(root)/(logged)/flows/edit/[...path]': { param: 'path', exists: flowExists, draftKind: 'flow' },
'/(root)/(logged)/flows/get/[...path]': { param: 'path', exists: flowExists },
'/(root)/(logged)/apps/edit/[...path]': { param: 'path', exists: appExists },
'/(root)/(logged)/apps/edit/[...path]': { param: 'path', exists: appExists, draftKind: 'app' },
'/(root)/(logged)/apps/get/[...path]': { param: 'path', exists: appExists },
'/(root)/(logged)/apps_raw/edit/[...path]': { param: 'path', exists: appExists },
'/(root)/(logged)/apps_raw/edit/[...path]': {
param: 'path',
exists: appExists,
draftKind: 'raw_app'
},
'/(root)/(logged)/apps_raw/get/[...path]': { param: 'path', exists: appExists },
'/(root)/(logged)/run/[...run]': { param: 'run', exists: jobExists }
}
async function itemPageMissingIn(workspace: string): Promise<boolean> {
function currentItemPage(): { itemPage: ItemPage; param: string } | undefined {
const itemPage = page.route.id ? ITEM_PAGES[page.route.id] : undefined
const param = itemPage ? page.params[itemPage.param] : undefined
if (!itemPage || !param) return false
return itemPage && param ? { itemPage, param } : undefined
}
async function itemPageMissingIn(workspace: string): Promise<boolean> {
const current = currentItemPage()
if (!current) return false
try {
return !(await itemPage.exists(workspace, param))
return !(await current.itemPage.exists(workspace, current.param))
} catch {
return true
}
}
// Staying on an editor would load the target workspace's copy without a pathname change, so
// its unsaved-changes guard (which only fires on one) would never ask about parked edits.
function editorHasUnsavedEdits(): boolean {
const current = currentItemPage()
const workspace = get(workspaceStore)
if (!current?.itemPage.draftKind || !workspace) return false
const byHash = current.itemPage.draftKind === 'script' && page.url.searchParams.get('hash')
return UserDraftDbSyncer.hasUnsavedDisabledChanges({
workspace,
itemKind: current.itemPage.draftKind,
path: byHash ? '' : current.param
})
}
function switchTo(id: string) {
workspaceAIClients.init(id)
switchWorkspace(id)
}
// Workspace switch shared by the sidebar workspace pickers. An item page stays open when
// its item also exists in `id`, and goes home otherwise.
export async function switchWorkspaceAndPage(
@@ -72,14 +121,20 @@ export async function switchWorkspaceAndPage(
href?: string
}
): Promise<void> {
if (opts?.landOnHome || (await itemPageMissingIn(id))) {
const unsavedEdits = editorHasUnsavedEdits()
if (opts?.landOnHome || unsavedEdits || (await itemPageMissingIn(id))) {
// Leave before switching: an item page still mounted when the store changes
// refetches its item in `id` and toasts the 404.
await goto('/')
switchWorkspace(id)
// The param carries the switch through the editor's unsaved-changes prompt: that
// cancels this navigation and, on discard, replays this URL, which the logged layout
// applies. On cancel nothing is switched.
const from = page.route.id
await goto(`/?workspace=${encodeURIComponent(id)}`)
if (unsavedEdits && page.route.id === from) return
switchTo(id)
return
}
switchWorkspace(id)
switchTo(id)
if (opts?.href) {
await goto(opts.href)
} else if (page.url.searchParams.get('workspace')) {