From d6ee2fb7d92e64d05702b7807659c0190ea12655 Mon Sep 17 00:00:00 2001 From: Source Open Date: Wed, 25 Mar 2026 13:42:09 -0700 Subject: [PATCH] Fix workspace switching semantics and sticky local worker ports (#1166) * fix(app): decouple workspace selection from runtime state Keep desktop workspace selection independent from backend activation so switching workers only reconnects when an action actually needs that runtime. Persist per-workspace local OpenWork server ports so long-lived local links stay stable without relying on a predictable default port. * fix(app): resolve rebased workspace runtime references * fix(test): auth opencode serve regression scripts * docs(app): clarify workspace runtime and port behavior --------- Co-authored-by: Omar McAdam Co-authored-by: Omar McAdam --- AGENTS.md | 2 +- ARCHITECTURE.md | 18 +- apps/app/scripts/_util.mjs | 10 + apps/app/src/app/app.tsx | 331 ++++++++------- .../src/app/components/session/sidebar.tsx | 6 +- .../session/workspace-session-list.tsx | 18 +- .../shared-skill-destination-modal.tsx | 6 +- .../components/workspace-right-sidebar.tsx | 4 +- apps/app/src/app/context/extensions.ts | 34 +- apps/app/src/app/context/session.ts | 8 +- apps/app/src/app/context/workspace.ts | 386 +++++++++++++----- apps/app/src/app/lib/openwork-server.ts | 5 +- apps/app/src/app/lib/tauri.ts | 21 +- apps/app/src/app/pages/config.tsx | 16 +- apps/app/src/app/pages/dashboard.tsx | 49 +-- apps/app/src/app/pages/extensions.tsx | 4 +- apps/app/src/app/pages/identities.tsx | 8 +- apps/app/src/app/pages/mcp.tsx | 8 +- apps/app/src/app/pages/onboarding.tsx | 4 +- apps/app/src/app/pages/plugins.tsx | 4 +- apps/app/src/app/pages/scheduled.tsx | 10 +- apps/app/src/app/pages/session.tsx | 70 ++-- apps/app/src/app/pages/settings.tsx | 39 +- apps/app/src/i18n/locales/en.ts | 2 +- apps/app/src/i18n/locales/ja.ts | 2 +- apps/app/src/i18n/locales/pt-BR.ts | 2 +- apps/app/src/i18n/locales/vi.ts | 2 +- apps/app/src/i18n/locales/zh.ts | 2 +- .../src-tauri/src/commands/workspace.rs | 168 +++++--- apps/desktop/src-tauri/src/lib.rs | 5 +- .../src-tauri/src/openwork_server/mod.rs | 133 +++++- .../src-tauri/src/openwork_server/spawn.rs | 109 ++++- apps/desktop/src-tauri/src/types.rs | 18 +- apps/desktop/src-tauri/src/workspace/state.rs | 173 +++++++- apps/story-book/src/new-layout.tsx | 46 +-- apps/story-book/src/story-book.tsx | 28 +- 36 files changed, 1204 insertions(+), 547 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 32fc60f7..e1ad058c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ Design principles for hot reload: * **Session-aware**: when sessions are actively running, queue reload signals. Promote to visible reload (toast or auto-reload) only after all active sessions finish. This avoids interrupting in-flight tool calls. * **Auto-reload setting**: each workspace can opt into automatic reload via `.opencode/openwork.json` (`reload.auto`). When enabled, the engine reloads automatically once queued signals are ready and no sessions are active. * **Session continuity**: before reload, capture running session IDs, agents, and models. After reload, optionally relaunch those sessions so the user experiences seamless continuity. -* **Per-workspace isolation**: the desktop file watcher only watches the active workspace root and its `.opencode/` directory. The server reload event store is already keyed by `workspaceId`. +* **Per-workspace isolation**: the desktop file watcher only watches the runtime-connected workspace root and its `.opencode/` directory. This can differ briefly from the UI-selected workspace while the user browses another workspace. The server reload event store is already keyed by `workspaceId`. ## Technology Stack diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c9f48e73..f137208a 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -362,7 +362,7 @@ workspace C ----/ +-------------------------------+ | opencode-router | - | root: active workspace | + | root: runtime active workspace| | clients: many dirs under root | +-------------------------------+ ``` @@ -373,6 +373,22 @@ Practical consequences: - If workspaces live in unrelated roots, directories outside the active router root are rejected. - OpenWork server is already multi-workspace aware. - Desktop router management is still effectively single-root at a time. +- On desktop, the file watcher follows the runtime-connected workspace root, not just the workspace currently selected in the UI. + +Terminology clarification: + +- `selected workspace` is a UI concept: the workspace the user is currently viewing and where compose/config actions should target. +- `runtime active workspace` is a backend concept: the workspace the local server/orchestrator currently reports as active. +- `watched workspace` is the desktop-host/runtime concept for which workspace root local file watching is currently attached to. +- These states must be treated separately. UI selection can change without implying that the backend has switched roots yet. +- In practice, `selected workspace` and `runtime active workspace` often converge once the user sends work, but they are allowed to diverge briefly while the UI is browsing another workspace. + +Desktop local OpenWork server ports: + +- Desktop-hosted local OpenWork server instances do not assume a fixed `8787` port. +- Each workspace gets a persistent preferred localhost port in the `48000-51000` range. +- On restart, desktop tries to reuse that workspace's saved port first. +- If that port is unavailable, desktop picks another free port in the same range and avoids ports already reserved by other known workspaces. ```text Shared-root case diff --git a/apps/app/scripts/_util.mjs b/apps/app/scripts/_util.mjs index 920b13b0..aa6bcc09 100644 --- a/apps/app/scripts/_util.mjs +++ b/apps/app/scripts/_util.mjs @@ -6,10 +6,20 @@ import { realpathSync, statSync } from "node:fs"; import { createOpencodeClient } from "@opencode-ai/sdk/v2/client"; +function resolveBasicAuthHeader() { + const password = process.env.OPENCODE_SERVER_PASSWORD?.trim() ?? ""; + if (!password) return undefined; + const username = process.env.OPENCODE_SERVER_USERNAME?.trim() || "opencode"; + const encoded = Buffer.from(`${username}:${password}`, "utf8").toString("base64"); + return `Basic ${encoded}`; +} + export function makeClient({ baseUrl, directory }) { + const authorization = resolveBasicAuthHeader(); return createOpencodeClient({ baseUrl, directory, + headers: authorization ? { Authorization: authorization } : undefined, responseStyle: "data", throwOnError: true, }); diff --git a/apps/app/src/app/app.tsx b/apps/app/src/app/app.tsx index fad8853c..a0f6f1de 100644 --- a/apps/app/src/app/app.tsx +++ b/apps/app/src/app/app.tsx @@ -1009,7 +1009,7 @@ export default function App() { const [openworkServerStatus, setOpenworkServerStatus] = createSignal("disconnected"); const [openworkServerCapabilities, setOpenworkServerCapabilities] = createSignal(null); const [openworkServerCheckedAt, setOpenworkServerCheckedAt] = createSignal(null); - const [openworkServerWorkspaceId, setOpenworkServerWorkspaceId] = createSignal(null); + const [runtimeWorkspaceId, setRuntimeWorkspaceId] = createSignal(null); const [openworkServerHostInfo, setOpenworkServerHostInfo] = createSignal(null); const [openworkServerDiagnostics, setOpenworkServerDiagnostics] = createSignal(null); const [openworkReconnectBusy, setOpenworkReconnectBusy] = createSignal(false); @@ -1293,6 +1293,21 @@ export default function App() { }); }); + createEffect(() => { + if (!isTauriRuntime()) return; + const hostInfo = openworkServerHostInfo(); + const port = hostInfo?.port; + if (!port) return; + + const current = openworkServerSettings(); + if (current.portOverride === port) return; + + updateOpenworkServerSettings({ + ...current, + portOverride: port, + }); + }); + createEffect(() => { if (typeof window === "undefined") return; if (!documentVisible()) return; @@ -1531,7 +1546,7 @@ export default function App() { const sessionStore = createSessionStore({ client, - activeWorkspaceRoot: () => workspaceStore.activeWorkspaceRoot().trim(), + selectedWorkspaceRoot: () => workspaceStore.selectedWorkspaceRoot().trim(), selectedSessionId, setSelectedSessionId, sessionModelState: () => ({ @@ -1803,6 +1818,12 @@ export default function App() { return fallback; }; + const ensureSelectedWorkspaceRuntime = async () => { + const workspaceId = workspaceStore.selectedWorkspaceId().trim(); + if (!workspaceId) return false; + return await workspaceStore.ensureWorkspaceActivated(workspaceId); + }; + async function sendPrompt(draft?: ComposerDraft) { const hasExplicitDraft = Boolean(draft); const fallbackText = prompt().trim(); @@ -1815,6 +1836,9 @@ export default function App() { const content = (resolvedDraft.resolvedText ?? resolvedDraft.text).trim(); if (!content && !resolvedDraft.attachments.length) return; + const ready = await ensureSelectedWorkspaceRuntime(); + if (!ready) return; + const c = client(); if (!c) return; @@ -2333,7 +2357,7 @@ export default function App() { } await renameSession(sessionID, trimmed); - await refreshSidebarWorkspaceSessions(workspaceStore.activeWorkspaceId()).catch(() => undefined); + await refreshSidebarWorkspaceSessions(workspaceStore.selectedWorkspaceId()).catch(() => undefined); } async function deleteSessionById(sessionID: string) { @@ -2344,7 +2368,7 @@ export default function App() { throw new Error("Not connected to a server"); } - const root = workspaceStore.activeWorkspaceRoot().trim(); + const root = workspaceStore.selectedWorkspaceRoot().trim(); const directory = toSessionTransportDirectory(root); const params = directory ? { sessionID: trimmed, directory } : { sessionID: trimmed }; unwrap(await c.session.delete(params)); @@ -2353,7 +2377,7 @@ export default function App() { // SSE will handle any further sync — calling loadSessions/refreshSidebarWorkspaceSessions // here races with SSE and can wipe unrelated sessions from the store. setSessions(sessions().filter((s) => s.id !== trimmed)); - const activeWsId = workspaceStore.activeWorkspaceId(); + const activeWsId = workspaceStore.selectedWorkspaceId(); setSidebarSessionsByWorkspaceId((prev) => ({ ...prev, [activeWsId]: (prev[activeWsId] ?? []).filter((s) => s.id !== trimmed), @@ -2373,7 +2397,7 @@ export default function App() { // If the deleted session was selected, clear selection so routing can fall back cleanly. if (selectedSessionId() === trimmed) { setSelectedSessionId(null); - const activeWorkspace = workspaceStore.activeWorkspaceId().trim(); + const activeWorkspace = workspaceStore.selectedWorkspaceId().trim(); if (activeWorkspace) { const map = readSessionByWorkspace(); if (map[activeWorkspace] === trimmed) { @@ -2409,7 +2433,7 @@ export default function App() { async function listCommands(): Promise<{ id: string; name: string; description?: string; source?: "command" | "mcp" | "skill" }[]> { const c = client(); if (!c) return []; - const list = await listCommandsTyped(c, workspaceStore.activeWorkspaceRoot().trim() || undefined); + const list = await listCommandsTyped(c, workspaceStore.selectedWorkspaceRoot().trim() || undefined); if (list.some((entry) => entry.name === "compact")) { return list; } @@ -2491,7 +2515,7 @@ export default function App() { } try { const cachedMethods = providerAuthMethods(); - const workerType = activeWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local"; + const workerType = selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local"; const authMethods = Object.keys(cachedMethods).length ? cachedMethods : await loadProviderAuthMethods(workerType); @@ -2804,7 +2828,7 @@ export default function App() { returnFocusTarget?: PromptFocusReturnTarget; preferredProviderId?: string; }) { - const workerType = activeWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local"; + const workerType = selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local"; setProviderAuthReturnFocusTarget(options?.returnFocusTarget ?? "none"); setProviderAuthPreferredProviderId(options?.preferredProviderId?.trim() || null); setProviderAuthBusy(true); @@ -2920,12 +2944,12 @@ export default function App() { const extensionsStore = createExtensionsStore({ client, projectDir: () => workspaceProjectDir(), - activeWorkspaceRoot: () => workspaceStore.activeWorkspaceRoot(), - workspaceType: () => workspaceStore.activeWorkspaceDisplay().workspaceType, + selectedWorkspaceRoot: () => workspaceStore.selectedWorkspaceRoot(), + workspaceType: () => workspaceStore.selectedWorkspaceDisplay().workspaceType, openworkServerClient, openworkServerStatus, openworkServerCapabilities, - openworkServerWorkspaceId, + runtimeWorkspaceId, setBusy, setBusyLabel, setBusyStartedAt, @@ -3255,7 +3279,7 @@ export default function App() { updateOpenworkServerSettings, openworkServerClient, openworkServerStatus, - openworkServerWorkspaceId, + runtimeWorkspaceId, onEngineStable: () => {}, engineRuntime, developerMode, @@ -3274,23 +3298,23 @@ export default function App() { const logWorkspaceScopeSnapshot = (label: string, extra?: Record) => { if (!developerMode()) return; - const activeWorkspace = workspaceStore.activeWorkspaceInfo(); - const activeWorkspaceId = workspaceStore.activeWorkspaceId().trim(); - const activeWorkspaceRoot = workspaceStore.activeWorkspaceRoot().trim(); + const activeWorkspace = workspaceStore.selectedWorkspaceInfo(); + const selectedWorkspaceId = workspaceStore.selectedWorkspaceId().trim(); + const selectedWorkspaceRoot = workspaceStore.selectedWorkspaceRoot().trim(); const engineInfo = workspaceStore.engine(); const map = readSessionByWorkspace(); wsDebug(label, { - activeWorkspaceId: activeWorkspaceId || null, + selectedWorkspaceId: selectedWorkspaceId || null, activeWorkspaceType: activeWorkspace?.workspaceType ?? null, - activeWorkspacePath: activeWorkspace?.path?.trim() ?? null, + selectedWorkspacePath: activeWorkspace?.path?.trim() ?? null, activeWorkspaceDirectory: activeWorkspace?.directory?.trim() ?? null, - activeWorkspaceRoot: activeWorkspaceRoot || null, - activeWorkspaceScope: describeDirectoryScope(activeWorkspaceRoot), + selectedWorkspaceRoot: selectedWorkspaceRoot || null, + activeWorkspaceScope: describeDirectoryScope(selectedWorkspaceRoot), clientDirectory: clientDirectory().trim() || null, clientDirectoryScope: describeDirectoryScope(clientDirectory().trim()), engineProjectDir: engineInfo?.projectDir?.trim() ?? null, engineProjectScope: describeDirectoryScope(engineInfo?.projectDir?.trim() ?? null), - lastSessionForActiveWorkspace: activeWorkspaceId ? map[activeWorkspaceId] ?? null : null, + lastSessionForActiveWorkspace: selectedWorkspaceId ? map[selectedWorkspaceId] ?? null : null, lastSessionMapKeys: Object.keys(map), ...extra, }); @@ -3548,7 +3572,7 @@ export default function App() { wsDebug("sidebar:refresh", { engineChanged, workspacesChanged, - activeWorkspaceId: workspaceStore.activeWorkspaceId(), + selectedWorkspaceId: workspaceStore.selectedWorkspaceId(), engineBaseUrl, }); @@ -3556,15 +3580,15 @@ export default function App() { // Remote->local switches commonly change engineBaseUrl, and refreshing every remote workspace // at the same time can trigger large /session responses and UI hangs. if (engineChanged && !workspacesChanged) { - void refreshLocalSidebarWorkspaceSessions(workspaceStore.activeWorkspaceId()).catch(() => undefined); + void refreshLocalSidebarWorkspaceSessions(workspaceStore.selectedWorkspaceId()).catch(() => undefined); return; } - void refreshAllSidebarWorkspaceSessions(workspaceStore.activeWorkspaceId()).catch(() => undefined); + void refreshAllSidebarWorkspaceSessions(workspaceStore.selectedWorkspaceId()).catch(() => undefined); }); createEffect(() => { - const id = workspaceStore.activeWorkspaceId().trim(); + const id = workspaceStore.selectedWorkspaceId().trim(); if (!id) return; const status = sidebarSessionStatusByWorkspaceId()[id] ?? "idle"; // Only auto-load once per workspace activation. @@ -3575,17 +3599,17 @@ export default function App() { createEffect(() => { const allSessions = sessions(); // reactive dependency on session store - // When switching workers, the session store can update before the activeWorkspaceId flips. + // When switching workers, the session store can update before the selectedWorkspaceId flips. // Use connectingWorkspaceId as the authoritative target during the switch so we don't // accidentally overwrite another worker's sidebar sessions. - const wsId = (workspaceStore.connectingWorkspaceId() ?? workspaceStore.activeWorkspaceId()).trim(); + const wsId = (workspaceStore.connectingWorkspaceId() ?? workspaceStore.selectedWorkspaceId()).trim(); if (!wsId) return; const status = sidebarSessionStatusByWorkspaceId()[wsId]; // Only sync if sidebar is already in 'ready' state (not during initial load) if (status === "ready") { const activeWorkspace = workspaceStore.workspaces().find((workspace) => workspace.id === wsId) ?? null; - const activeWorkspaceRoot = normalizeDirectoryPath( + const selectedWorkspaceRoot = normalizeDirectoryPath( activeWorkspace?.workspaceType === "local" ? activeWorkspace.path : activeWorkspace?.directory ?? activeWorkspace?.path, @@ -3593,20 +3617,20 @@ export default function App() { if ( !shouldApplyScopedSessionLoad({ loadedScopeRoot: loadedSessionScopeRoot(), - workspaceRoot: activeWorkspaceRoot, + workspaceRoot: selectedWorkspaceRoot, }) ) { if (developerMode()) { console.log("[sidebar-sync] skip stale session scope", { wsId, loadedScopeRoot: loadedSessionScopeRoot(), - activeWorkspaceRoot, + selectedWorkspaceRoot, }); } return; } - const scopedSessions = activeWorkspaceRoot - ? allSessions.filter((session) => normalizeDirectoryPath(session.directory) === activeWorkspaceRoot) + const scopedSessions = selectedWorkspaceRoot + ? allSessions.filter((session) => normalizeDirectoryPath(session.directory) === selectedWorkspaceRoot) : allSessions; const sorted = sortSessionsByActivity(scopedSessions); if (developerMode()) { @@ -3614,7 +3638,7 @@ export default function App() { wsId, status, activeWorkspace, - activeWorkspaceRoot, + selectedWorkspaceRoot, allSessions: allSessions.map((session) => ({ id: session.id, title: session.title, @@ -3681,7 +3705,7 @@ export default function App() { const sidebarWorkspaceGroups = createMemo(() => { const workspaces = workspaceStore.workspaces(); - const activeWorkspaceId = workspaceStore.activeWorkspaceId().trim(); + const selectedWorkspaceId = workspaceStore.selectedWorkspaceId().trim(); const connectingWorkspaceId = workspaceStore.connectingWorkspaceId()?.trim() ?? ""; const sessionsById = sidebarSessionsByWorkspaceId(); const statusById = sidebarSessionStatusByWorkspaceId(); @@ -3717,9 +3741,9 @@ export default function App() { } const existingWorkspace = dedupedWorkspaces[existingIndex]; const existingIsPriority = - existingWorkspace.id === activeWorkspaceId || existingWorkspace.id === connectingWorkspaceId; + existingWorkspace.id === selectedWorkspaceId || existingWorkspace.id === connectingWorkspaceId; const currentIsPriority = - workspace.id === activeWorkspaceId || workspace.id === connectingWorkspaceId; + workspace.id === selectedWorkspaceId || workspace.id === connectingWorkspaceId; if (currentIsPriority && !existingIsPriority) { dedupedWorkspaces[existingIndex] = workspace; } @@ -3753,7 +3777,7 @@ export default function App() { createEffect(() => { if (typeof window === "undefined") return; - const workspaceId = workspaceStore.activeWorkspaceId(); + const workspaceId = workspaceStore.selectedWorkspaceId(); const sessionId = selectedSessionId(); if (!workspaceId || !sessionId) return; const map = readSessionByWorkspace(); @@ -3779,23 +3803,29 @@ export default function App() { }); createEffect(() => { - const active = workspaceStore.activeWorkspaceDisplay(); + const connectedWorkspaceId = workspaceStore.connectedWorkspaceId()?.trim() ?? ""; + const connectedWorkspace = workspaceStore.workspaces().find((workspace) => workspace.id === connectedWorkspaceId) + ?? null; const client = openworkServerClient(); - const openworkUrl = openworkServerUrl().trim(); if (!client || openworkServerStatus() !== "connected") { - setOpenworkServerWorkspaceId(null); + setRuntimeWorkspaceId(null); return; } - if (active.workspaceType === "remote" && active.remoteType === "openwork") { + if (!connectedWorkspace) { + setRuntimeWorkspaceId(null); + return; + } + + if (connectedWorkspace.workspaceType === "remote" && connectedWorkspace.remoteType === "openwork") { const inferredWorkspaceId = - parseOpenworkWorkspaceIdFromUrl(active.openworkHostUrl ?? "") ?? - parseOpenworkWorkspaceIdFromUrl(active.baseUrl ?? "") ?? - parseOpenworkWorkspaceIdFromUrl(openworkUrl); - const storedId = active.openworkWorkspaceId?.trim() || inferredWorkspaceId || envOpenworkWorkspaceId || null; + parseOpenworkWorkspaceIdFromUrl(connectedWorkspace.openworkHostUrl ?? "") ?? + parseOpenworkWorkspaceIdFromUrl(connectedWorkspace.baseUrl ?? "") ?? + envOpenworkWorkspaceId; + const storedId = connectedWorkspace.openworkWorkspaceId?.trim() || inferredWorkspaceId || null; if (storedId) { - setOpenworkServerWorkspaceId(storedId); + setRuntimeWorkspaceId(storedId); return; } @@ -3805,16 +3835,18 @@ export default function App() { const response = await client.listWorkspaces(); if (cancelled) return; const items = Array.isArray(response.items) ? response.items : []; - const directoryHint = normalizeDirectoryPath(active.directory?.trim() ?? active.path?.trim() ?? ""); + const directoryHint = normalizeDirectoryPath( + connectedWorkspace.directory?.trim() ?? connectedWorkspace.path?.trim() ?? "", + ); const match = directoryHint ? items.find((entry) => { const entryPath = normalizeDirectoryPath((entry.opencode?.directory ?? entry.directory ?? entry.path ?? "").trim()); return Boolean(entryPath && entryPath === directoryHint); }) : (response.activeId ? items.find((entry) => entry.id === response.activeId) : null) ?? items[0]; - setOpenworkServerWorkspaceId(match?.id ?? response.activeId ?? null); + setRuntimeWorkspaceId(match?.id ?? response.activeId ?? null); } catch { - if (!cancelled) setOpenworkServerWorkspaceId(null); + if (!cancelled) setRuntimeWorkspaceId(null); } }; @@ -3825,23 +3857,20 @@ export default function App() { return; } - if (active.workspaceType === "local") { - const root = normalizeDirectoryPath(workspaceStore.activeWorkspaceRoot().trim()); - if (!root) { - setOpenworkServerWorkspaceId(null); - return; - } - + if (connectedWorkspace.workspaceType === "local") { let cancelled = false; const resolveWorkspace = async () => { try { const response = await client.listWorkspaces(); if (cancelled) return; const items = Array.isArray(response.items) ? response.items : []; - const match = items.find((entry) => normalizeDirectoryPath(entry.path) === root); - setOpenworkServerWorkspaceId(match?.id ?? null); + const activeMatch = response.activeId ? items.find((entry) => entry.id === response.activeId) : null; + const pathMatch = items.find( + (entry) => normalizeDirectoryPath(entry.path) === normalizeDirectoryPath(connectedWorkspace.path), + ); + setRuntimeWorkspaceId(activeMatch?.id ?? pathMatch?.id ?? response.activeId ?? null); } catch { - if (!cancelled) setOpenworkServerWorkspaceId(null); + if (!cancelled) setRuntimeWorkspaceId(null); } }; @@ -3852,7 +3881,7 @@ export default function App() { return; } - setOpenworkServerWorkspaceId(null); + setRuntimeWorkspaceId(null); }); const resolveSharedBundleWorkerTarget = () => { @@ -3957,9 +3986,9 @@ export default function App() { }; const resolveActiveSharedBundleImportTarget = (): SharedBundleImportTarget => { - const active = workspaceStore.activeWorkspaceDisplay(); + const active = workspaceStore.selectedWorkspaceDisplay(); if (active.workspaceType === "local") { - return { localRoot: workspaceStore.activeWorkspaceRoot().trim() }; + return { localRoot: workspaceStore.selectedWorkspaceRoot().trim() }; } return { @@ -3983,14 +4012,14 @@ export default function App() { const items = Array.isArray(response.items) ? response.items : []; const matchId = findSharedBundleImportWorkspaceId(items, target); if (matchId) { - setOpenworkServerWorkspaceId(matchId); + setRuntimeWorkspaceId(matchId); return { client, workspaceId: matchId }; } } catch { // ignore and keep polling } } else { - const workspaceId = openworkServerWorkspaceId(); + const workspaceId = runtimeWorkspaceId(); if (workspaceId) { return { client, workspaceId }; } @@ -4073,7 +4102,7 @@ export default function App() { const imported = await importSharedBundleIntoActiveWorker( request.request, { - localRoot: workspaceStore.activeWorkspaceRoot().trim(), + localRoot: workspaceStore.selectedWorkspaceRoot().trim(), }, request.bundle, ); @@ -4246,8 +4275,6 @@ export default function App() { setDevtoolsWorkspaceId(null); return; } - - const root = normalizeDirectoryPath(workspaceStore.activeWorkspaceRoot().trim()); let active = true; const run = async () => { @@ -4256,8 +4283,7 @@ export default function App() { if (!active) return; const items = Array.isArray(response.items) ? response.items : []; const activeMatch = response.activeId ? items.find((item) => item.id === response.activeId) : null; - const match = root ? items.find((item) => normalizeDirectoryPath(item.path) === root) : activeMatch ?? items[0]; - setDevtoolsWorkspaceId(match?.id ?? activeMatch?.id ?? null); + setDevtoolsWorkspaceId(activeMatch?.id ?? items[0]?.id ?? null); } catch { if (active) setDevtoolsWorkspaceId(null); } @@ -4321,7 +4347,7 @@ export default function App() { }); createEffect(() => { - const active = workspaceStore.activeWorkspaceDisplay(); + const active = workspaceStore.selectedWorkspaceDisplay(); if (active.workspaceType !== "remote" || active.remoteType !== "openwork") { return; } @@ -4340,7 +4366,7 @@ export default function App() { }); const openworkServerReady = createMemo(() => openworkServerStatus() === "connected"); - const openworkServerWorkspaceReady = createMemo(() => Boolean(openworkServerWorkspaceId())); + const openworkServerWorkspaceReady = createMemo(() => Boolean(runtimeWorkspaceId())); const resolvedOpenworkCapabilities = createMemo(() => openworkServerCapabilities()); const openworkServerCanWriteSkills = createMemo( () => @@ -4386,7 +4412,7 @@ export default function App() { updateOpenworkServerSettings(next); try { - if (isTauriRuntime() && workspaceStore.activeWorkspaceDisplay().workspaceType === "local") { + if (isTauriRuntime() && workspaceStore.selectedWorkspaceDisplay().workspaceType === "local") { const restarted = await restartLocalServer(); if (!restarted) { throw new Error("Failed to restart the local worker with the updated sharing setting."); @@ -4474,7 +4500,7 @@ export default function App() { }); const sharedSkillDestinationWorkspaces = createMemo(() => { - const activeId = workspaceStore.activeWorkspaceId(); + const activeId = workspaceStore.selectedWorkspaceId(); return workspaceStore .workspaces() .filter((workspace) => isSharedBundleImportWorkspace(workspace)) @@ -4691,7 +4717,7 @@ export default function App() { }); const sharedBundleWorkerOptions = createMemo(() => { - const activeWorkspaceId = workspaceStore.activeWorkspaceId().trim(); + const selectedWorkspaceId = workspaceStore.selectedWorkspaceId().trim(); const items = workspaceStore.workspaces().map((workspace) => { let disabledReason: string | null = null; if (!resolveSharedBundleImportTargetForWorkspace(workspace)) { @@ -4725,7 +4751,7 @@ export default function App() { label, detail, badge, - current: workspace.id === activeWorkspaceId, + current: workspace.id === selectedWorkspaceId, disabledReason, }; }); @@ -4957,7 +4983,7 @@ export default function App() { setOpenworkServerCheckedAt(Date.now()); const ok = result.status === "connected" || result.status === "limited"; if (ok && !isTauriRuntime()) { - const active = workspaceStore.activeWorkspaceDisplay(); + const active = workspaceStore.selectedWorkspaceDisplay(); const shouldAttach = !client() || active.workspaceType !== "remote" || active.remoteType !== "openwork"; if (shouldAttach) { await workspaceStore @@ -5015,9 +5041,9 @@ export default function App() { }; const restartLocalServer = async () => { - const activeWorkspace = workspaceStore.activeWorkspaceDisplay(); + const activeWorkspace = workspaceStore.selectedWorkspaceDisplay(); const activeLocalPath = - activeWorkspace.workspaceType === "local" ? workspaceStore.activeWorkspacePath().trim() : ""; + activeWorkspace.workspaceType === "local" ? workspaceStore.selectedWorkspacePath().trim() : ""; const runningProjectDir = workspaceStore.engine()?.projectDir?.trim() ?? ""; const workspacePath = activeLocalPath || runningProjectDir; @@ -5048,12 +5074,12 @@ export default function App() { }; const canReloadLocalEngine = () => - isTauriRuntime() && workspaceStore.activeWorkspaceDisplay().workspaceType === "local"; + isTauriRuntime() && workspaceStore.selectedWorkspaceDisplay().workspaceType === "local"; const canReloadWorkspace = createMemo(() => { if (canReloadLocalEngine()) return true; - if (workspaceStore.activeWorkspaceDisplay().workspaceType !== "remote") return false; - return openworkServerStatus() === "connected" && Boolean(openworkServerClient() && openworkServerWorkspaceId()); + if (workspaceStore.selectedWorkspaceDisplay().workspaceType !== "remote") return false; + return openworkServerStatus() === "connected" && Boolean(openworkServerClient() && runtimeWorkspaceId()); }); const reloadWorkspaceEngineFromUi = async () => { @@ -5061,12 +5087,12 @@ export default function App() { return workspaceStore.reloadWorkspaceEngine(); } - if (workspaceStore.activeWorkspaceDisplay().workspaceType !== "remote") { + if (workspaceStore.selectedWorkspaceDisplay().workspaceType !== "remote") { return false; } const client = openworkServerClient(); - const workspaceId = openworkServerWorkspaceId(); + const workspaceId = runtimeWorkspaceId(); if (!client || !workspaceId || openworkServerStatus() !== "connected") { setError("Connect to this worker before applying runtime changes."); return false; @@ -5074,7 +5100,7 @@ export default function App() { try { await client.reloadEngine(workspaceId); - await workspaceStore.activateWorkspace(workspaceStore.activeWorkspaceId()); + await workspaceStore.activateWorkspace(workspaceStore.selectedWorkspaceId()); await refreshMcpServers(); return true; } catch (error) { @@ -5296,7 +5322,7 @@ export default function App() { // Scheduler helpers - must be defined after workspaceStore const resolveOpenworkScheduler = () => { const client = openworkServerClient(); - const workspaceId = openworkServerWorkspaceId(); + const workspaceId = runtimeWorkspaceId(); if (openworkServerStatus() !== "connected" || !client || !workspaceId) return null; return { client, workspaceId }; }; @@ -5308,7 +5334,7 @@ export default function App() { const scheduledJobsSourceReady = createMemo(() => { if (scheduledJobsSource() !== "remote") return true; const client = openworkServerClient(); - const workspaceId = openworkServerWorkspaceId(); + const workspaceId = runtimeWorkspaceId(); return openworkServerStatus() === "connected" && Boolean(client && workspaceId); }); @@ -5371,7 +5397,7 @@ export default function App() { setScheduledJobsStatus(null); try { - const root = workspaceStore.activeWorkspaceRoot().trim(); + const root = workspaceStore.selectedWorkspaceRoot().trim(); const jobs = await schedulerListJobs(root || undefined); setScheduledJobs(jobs); setScheduledJobsUpdatedAt(Date.now()); @@ -5401,7 +5427,7 @@ export default function App() { if (isWindowsPlatform()) { throw new Error("Scheduler is not supported on Windows yet."); } - const root = workspaceStore.activeWorkspaceRoot().trim(); + const root = workspaceStore.selectedWorkspaceRoot().trim(); const job = await schedulerDeleteJob(name, root || undefined); setScheduledJobs((current) => current.filter((entry) => entry.slug !== job.slug)); return; @@ -5409,13 +5435,13 @@ export default function App() { createEffect(() => { if (!isTauriRuntime()) return; - workspaceStore.activeWorkspaceId(); + workspaceStore.selectedWorkspaceId(); workspaceProjectDir(); void refreshMcpServers(); }); const activeAuthorizedDirs = createMemo(() => workspaceStore.authorizedDirs()); - const activeWorkspaceDisplay = createMemo(() => workspaceStore.activeWorkspaceDisplay()); + const selectedWorkspaceDisplay = createMemo(() => workspaceStore.selectedWorkspaceDisplay()); const resolvedActiveWorkspaceConfig = createMemo( () => activeWorkspaceServerConfig() ?? workspaceStore.workspaceConfig(), ); @@ -5423,10 +5449,10 @@ export default function App() { workspaceIdOverride?: string | null, ): Promise => { const client = openworkServerClient(); - const workspaceId = (workspaceIdOverride ?? openworkServerWorkspaceId() ?? "").trim(); + const workspaceId = (workspaceIdOverride ?? runtimeWorkspaceId() ?? "").trim(); if (!client || !workspaceId) return null; - const workspace = activeWorkspaceDisplay(); + const workspace = selectedWorkspaceDisplay(); const config = await client.getConfig(workspaceId); const normalized = normalizeWorkspaceOpenworkConfig( config.openwork, @@ -5450,11 +5476,11 @@ export default function App() { return t("app.migration.desktop_required", currentLocale()); } - if (activeWorkspaceDisplay().workspaceType !== "local") { + if (selectedWorkspaceDisplay().workspaceType !== "local") { return t("app.migration.local_only", currentLocale()); } - if (!workspaceStore.activeWorkspacePath().trim()) { + if (!workspaceStore.selectedWorkspacePath().trim()) { return t("app.migration.workspace_required", currentLocale()); } @@ -5476,9 +5502,9 @@ export default function App() { const [autoConnectAttempted, setAutoConnectAttempted] = createSignal(false); createEffect(() => { - const workspace = activeWorkspaceDisplay(); + const workspace = selectedWorkspaceDisplay(); const openworkClient = openworkServerClient(); - const workspaceId = openworkServerWorkspaceId(); + const workspaceId = runtimeWorkspaceId(); const capabilities = resolvedOpenworkCapabilities(); const canReadConfig = openworkServerStatus() === "connected" && @@ -5532,10 +5558,10 @@ export default function App() { createSignal>({}); createEffect(() => { - const workspaceId = (openworkServerWorkspaceId() ?? "").trim(); + const workspaceId = (runtimeWorkspaceId() ?? "").trim(); const client = openworkServerClient(); const connected = openworkServerStatus() === "connected"; - const root = workspaceStore.activeWorkspaceRoot().trim(); + const root = workspaceStore.selectedWorkspaceRoot().trim(); const config = resolvedActiveWorkspaceConfig(); const templates = blueprintSessions(config); const materialized = blueprintMaterializedSessions(config); @@ -5885,7 +5911,7 @@ export default function App() { } async function connectNotion() { - if (workspaceStore.activeWorkspaceDisplay().workspaceType !== "local") { + if (workspaceStore.selectedWorkspaceDisplay().workspaceType !== "local") { setNotionError("Notion connections are only available for local workspaces."); return; } @@ -5897,7 +5923,7 @@ export default function App() { } const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); const openworkCapabilities = resolvedOpenworkCapabilities(); const canUseOpenworkServer = openworkServerStatus() === "connected" && @@ -5977,10 +6003,10 @@ export default function App() { }; const projectDir = workspaceProjectDir().trim(); - const isRemoteWorkspace = workspaceStore.activeWorkspaceDisplay().workspaceType === "remote"; + const isRemoteWorkspace = workspaceStore.selectedWorkspaceDisplay().workspaceType === "remote"; const isLocalWorkspace = !isRemoteWorkspace; const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); const openworkCapabilities = resolvedOpenworkCapabilities(); const canUseOpenworkServer = openworkServerStatus() === "connected" && @@ -6114,7 +6140,7 @@ export default function App() { const readMcpConfigFile = async (scope: "project" | "global") => { const projectDir = workspaceProjectDir().trim(); const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); const canUseOpenworkServer = openworkServerStatus() === "connected" && openworkClient && @@ -6133,7 +6159,7 @@ export default function App() { async function connectMcp(entry: (typeof MCP_QUICK_CONNECT)[number]) { const startedAt = perfNow(); const isRemoteWorkspace = - workspaceStore.activeWorkspaceDisplay().workspaceType === "remote" || + workspaceStore.selectedWorkspaceDisplay().workspaceType === "remote" || (!isTauriRuntime() && openworkServerStatus() === "connected"); const projectDir = workspaceProjectDir().trim(); const entryType = entry.type ?? "remote"; @@ -6146,7 +6172,7 @@ export default function App() { }); const openworkClient = openworkServerClient(); - let openworkWorkspaceId = openworkServerWorkspaceId(); + let openworkWorkspaceId = runtimeWorkspaceId(); const openworkCapabilities = resolvedOpenworkCapabilities(); if (!openworkWorkspaceId && openworkClient && openworkServerStatus() === "connected") { try { @@ -6154,7 +6180,7 @@ export default function App() { const match = response.items?.[0]; if (match?.id) { openworkWorkspaceId = match.id; - setOpenworkServerWorkspaceId(match.id); + setRuntimeWorkspaceId(match.id); } } catch { // ignore @@ -6391,12 +6417,12 @@ export default function App() { async function logoutMcpAuth(name: string) { const isRemoteWorkspace = - workspaceStore.activeWorkspaceDisplay().workspaceType === "remote" || + workspaceStore.selectedWorkspaceDisplay().workspaceType === "remote" || (!isTauriRuntime() && openworkServerStatus() === "connected"); const projectDir = workspaceProjectDir().trim(); const openworkClient = openworkServerClient(); - let openworkWorkspaceId = openworkServerWorkspaceId(); + let openworkWorkspaceId = runtimeWorkspaceId(); const openworkCapabilities = resolvedOpenworkCapabilities(); if (!openworkWorkspaceId && openworkClient && openworkServerStatus() === "connected") { try { @@ -6404,7 +6430,7 @@ export default function App() { const match = response.items?.[0]; if (match?.id) { openworkWorkspaceId = match.id; - setOpenworkServerWorkspaceId(match.id); + setRuntimeWorkspaceId(match.id); } } catch { // ignore @@ -6494,7 +6520,7 @@ export default function App() { setMcpStatus(null); const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); const canUseOpenworkServer = openworkServerStatus() === "connected" && openworkClient && @@ -6524,6 +6550,11 @@ export default function App() { } async function createSessionAndOpen() { + const ready = await ensureSelectedWorkspaceRuntime(); + if (!ready) { + return; + } + const c = client(); if (!c) { return; @@ -6549,7 +6580,7 @@ export default function App() { mark("start", { baseUrl: baseUrl(), - workspace: workspaceStore.activeWorkspaceRoot().trim() || null, + workspace: workspaceStore.selectedWorkspaceRoot().trim() || null, }); // Abort any in-flight refresh operations to free up connection resources @@ -6600,7 +6631,7 @@ export default function App() { let rawResult: Awaited>; try { - const directory = toSessionTransportDirectory(workspaceStore.activeWorkspaceRoot().trim()) || undefined; + const directory = toSessionTransportDirectory(workspaceStore.selectedWorkspaceRoot().trim()) || undefined; logWorkspaceScopeSnapshot("session:create:scope", { transportDirectory: directory ?? null, transportScope: describeDirectoryScope(directory ?? null), @@ -6649,7 +6680,7 @@ export default function App() { time: session.time, directory: session.directory, }; - const wsId = workspaceStore.activeWorkspaceId().trim(); + const wsId = workspaceStore.selectedWorkspaceId().trim(); if (wsId) { const currentSessions = sidebarSessionsByWorkspaceId()[wsId] || []; setSidebarSessionsByWorkspaceId((prev) => ({ @@ -6929,7 +6960,7 @@ export default function App() { createEffect(() => { if (typeof window === "undefined") return; - const workspaceId = workspaceStore.activeWorkspaceId(); + const workspaceId = workspaceStore.selectedWorkspaceId(); if (!workspaceId) return; setSessionModelOverridesReady(false); @@ -6948,7 +6979,7 @@ export default function App() { createEffect(() => { if (typeof window === "undefined") return; if (!sessionModelOverridesReady()) return; - const workspaceId = workspaceStore.activeWorkspaceId(); + const workspaceId = workspaceStore.selectedWorkspaceId(); if (!workspaceId) return; const payload = serializeSessionModelOverrides(sessionModelOverrideById()); @@ -6965,7 +6996,7 @@ export default function App() { createEffect(() => { const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); const canReadConfig = openworkServerCanReadConfig(); if (!openworkClient || !openworkWorkspaceId || !canReadConfig) { @@ -7016,7 +7047,7 @@ export default function App() { const persistAuthorizedFolders = async (nextFolders: string[]) => { const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); if (!openworkClient || !openworkWorkspaceId || !openworkServerCanWriteConfig()) { setAuthorizedFoldersError( "A writable OpenWork server workspace is required to update authorized folders.", @@ -7071,7 +7102,7 @@ export default function App() { const addAuthorizedFolder = async () => { const normalized = normalizeAuthorizedFolderPath(authorizedFolderDraft()); - const workspaceRoot = normalizeAuthorizedFolderPath(workspaceStore.activeWorkspaceRoot().trim()); + const workspaceRoot = normalizeAuthorizedFolderPath(workspaceStore.selectedWorkspaceRoot().trim()); if (!normalized) return; if (workspaceRoot && normalized === workspaceRoot) { setAuthorizedFolderDraft(""); @@ -7103,7 +7134,7 @@ export default function App() { const selection = await pickDirectory({ title: t("onboarding.authorize_folder", currentLocale()) }); const folder = typeof selection === "string" ? selection : Array.isArray(selection) ? selection[0] : null; const normalized = normalizeAuthorizedFolderPath(folder); - const workspaceRoot = normalizeAuthorizedFolderPath(workspaceStore.activeWorkspaceRoot().trim()); + const workspaceRoot = normalizeAuthorizedFolderPath(workspaceStore.selectedWorkspaceRoot().trim()); if (!normalized) return; setAuthorizedFolderDraft(normalized); if (workspaceRoot && normalized === workspaceRoot) { @@ -7129,15 +7160,15 @@ export default function App() { createEffect(() => { if (typeof window === "undefined") return; - const workspaceId = workspaceStore.activeWorkspaceId(); + const workspaceId = workspaceStore.selectedWorkspaceId(); if (!workspaceId) return; setWorkspaceDefaultModelReady(false); - const workspaceType = workspaceStore.activeWorkspaceDisplay().workspaceType; - const workspaceRoot = workspaceStore.activeWorkspacePath().trim(); + const workspaceType = workspaceStore.selectedWorkspaceDisplay().workspaceType; + const workspaceRoot = workspaceStore.selectedWorkspacePath().trim(); const activeClient = client(); const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); const openworkCapabilities = resolvedOpenworkCapabilities(); const canUseOpenworkServer = openworkServerStatus() === "connected" && @@ -7210,14 +7241,14 @@ export default function App() { if (!isTauriRuntime()) return; if (!defaultModelExplicit()) return; - const workspace = workspaceStore.activeWorkspaceDisplay(); + const workspace = workspaceStore.selectedWorkspaceDisplay(); if (workspace.workspaceType !== "local") return; - const root = workspaceStore.activeWorkspacePath().trim(); + const root = workspaceStore.selectedWorkspacePath().trim(); if (!root) return; const nextModel = defaultModel(); const openworkClient = openworkServerClient(); - const openworkWorkspaceId = openworkServerWorkspaceId(); + const openworkWorkspaceId = runtimeWorkspaceId(); const openworkCapabilities = resolvedOpenworkCapabilities(); const canUseOpenworkServer = openworkServerStatus() === "connected" && @@ -7528,9 +7559,9 @@ export default function App() { const workspaceSwitchWorkspace = createMemo(() => { const switchingId = workspaceStore.connectingWorkspaceId(); if (switchingId) { - return workspaceStore.workspaces().find((ws) => ws.id === switchingId) ?? activeWorkspaceDisplay(); + return workspaceStore.workspaces().find((ws) => ws.id === switchingId) ?? selectedWorkspaceDisplay(); } - return activeWorkspaceDisplay(); + return selectedWorkspaceDisplay(); }); // Avoid flashing the full-screen switch overlay for fast workspace switches. @@ -7595,7 +7626,7 @@ export default function App() { openworkToken: openworkServerSettings().token ?? "", newAuthorizedDir: newAuthorizedDir(), authorizedDirs: workspaceStore.authorizedDirs(), - activeWorkspacePath: workspaceStore.activeWorkspacePath(), + selectedWorkspacePath: workspaceStore.selectedWorkspacePath(), workspaces: workspaceStore.workspaces(), localHostLabel: localHostLabel(), engineRunning: Boolean(engine()?.running), @@ -7666,7 +7697,7 @@ export default function App() { }); const dashboardProps = () => { - const workspaceType = activeWorkspaceDisplay().workspaceType; + const workspaceType = selectedWorkspaceDisplay().workspaceType; const isRemoteWorkspace = workspaceType === "remote"; const providerAuthWorkerType: "local" | "remote" = isRemoteWorkspace ? "remote" : "local"; const openworkStatus = openworkServerStatus(); @@ -7741,8 +7772,8 @@ export default function App() { saveShareRemoteAccess, openworkServerCapabilities: devtoolsCapabilities(), openworkServerDiagnostics: openworkServerDiagnostics(), - openworkServerWorkspaceId: openworkServerWorkspaceId(), - activeWorkspaceType: workspaceStore.activeWorkspaceDisplay().workspaceType, + runtimeWorkspaceId: runtimeWorkspaceId(), + activeWorkspaceType: workspaceStore.selectedWorkspaceDisplay().workspaceType, openworkAuditEntries: openworkAuditEntries(), openworkAuditStatus: openworkAuditStatus(), openworkAuditError: openworkAuditError(), @@ -7763,12 +7794,13 @@ export default function App() { setWorkspaceAutoReloadEnabled, workspaceAutoReloadResumeEnabled: workspaceAutoReloadResumeEnabled(), setWorkspaceAutoReloadResumeEnabled, - activeWorkspaceDisplay: activeWorkspaceDisplay(), + selectedWorkspaceDisplay: selectedWorkspaceDisplay(), workspaces: workspaceStore.workspaces(), - activeWorkspaceId: workspaceStore.activeWorkspaceId(), + selectedWorkspaceId: workspaceStore.selectedWorkspaceId(), connectingWorkspaceId: workspaceStore.connectingWorkspaceId(), workspaceConnectionStateById: workspaceStore.workspaceConnectionStateById(), - activateWorkspace: workspaceStore.activateWorkspace, + selectWorkspace: workspaceStore.selectWorkspace, + ensureWorkspaceActivated: workspaceStore.ensureWorkspaceActivated, testWorkspaceConnection: workspaceStore.testWorkspaceConnection, recoverWorkspace: workspaceStore.recoverWorkspace, openCreateWorkspace: () => workspaceStore.setCreateWorkspaceOpen(true), @@ -7800,8 +7832,8 @@ export default function App() { refreshScheduledJobs: (options?: { force?: boolean }) => refreshScheduledJobs(options).catch(() => undefined), deleteScheduledJob, - activeWorkspaceRoot: workspaceStore.activeWorkspaceRoot().trim(), - isRemoteWorkspace: workspaceStore.activeWorkspaceDisplay().workspaceType === "remote", + selectedWorkspaceRoot: workspaceStore.selectedWorkspaceRoot().trim(), + isRemoteWorkspace: workspaceStore.selectedWorkspaceDisplay().workspaceType === "remote", refreshSkills: (options?: { force?: boolean }) => refreshSkills(options).catch(() => undefined), refreshHubSkills: (options?: { force?: boolean }) => refreshHubSkills(options).catch(() => undefined), refreshPlugins: (scopeOverride?: PluginScope) => @@ -7987,7 +8019,7 @@ export default function App() { }; const sessionProps = () => ({ - providerAuthWorkerType: (activeWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local") as + providerAuthWorkerType: (selectedWorkspaceDisplay().workspaceType === "remote" ? "remote" : "local") as | "remote" | "local", selectedSessionId: activeSessionId(), @@ -7997,14 +8029,15 @@ export default function App() { setTab, setSettingsTab, toggleSettings: () => toggleSettingsView("general"), - activeWorkspaceDisplay: activeWorkspaceDisplay(), - activeWorkspaceRoot: workspaceStore.activeWorkspaceRoot().trim(), + selectedWorkspaceDisplay: selectedWorkspaceDisplay(), + selectedWorkspaceRoot: workspaceStore.selectedWorkspaceRoot().trim(), activeWorkspaceConfig: resolvedActiveWorkspaceConfig(), workspaces: workspaceStore.workspaces(), - activeWorkspaceId: workspaceStore.activeWorkspaceId(), + selectedWorkspaceId: workspaceStore.selectedWorkspaceId(), connectingWorkspaceId: workspaceStore.connectingWorkspaceId(), workspaceConnectionStateById: workspaceStore.workspaceConnectionStateById(), - activateWorkspace: workspaceStore.activateWorkspace, + selectWorkspace: workspaceStore.selectWorkspace, + ensureWorkspaceActivated: workspaceStore.ensureWorkspaceActivated, testWorkspaceConnection: workspaceStore.testWorkspaceConnection, recoverWorkspace: workspaceStore.recoverWorkspace, editWorkspaceConnection: openWorkspaceConnectionSettings, @@ -8026,7 +8059,7 @@ export default function App() { shareRemoteAccessBusy: shareRemoteAccessBusy(), shareRemoteAccessError: shareRemoteAccessError(), saveShareRemoteAccess, - openworkServerWorkspaceId: openworkServerWorkspaceId(), + runtimeWorkspaceId: runtimeWorkspaceId(), engineInfo: workspaceStore.engine(), engineDoctorVersion: workspaceStore.engineDoctorResult()?.version ?? null, orchestratorStatus: orchestratorStatusState(), @@ -8211,7 +8244,7 @@ export default function App() { sessionsLoaded() && shouldRedirectMissingSessionAfterScopedLoad({ loadedScopeRoot: loadedSessionScopeRoot(), - workspaceRoot: workspaceStore.activeWorkspaceRoot().trim(), + workspaceRoot: workspaceStore.selectedWorkspaceRoot().trim(), hasMatchingSession: sessions().some((session) => session.id === id), }) ) { @@ -8333,7 +8366,7 @@ export default function App() { reloadRequired={mcpAuthNeedsReload()} reloadBlocked={activeReloadBlockingSessions().length > 0} activeSessions={activeReloadBlockingSessions()} - isRemoteWorkspace={activeWorkspaceDisplay().workspaceType === "remote"} + isRemoteWorkspace={selectedWorkspaceDisplay().workspaceType === "remote"} onForceStopSession={(sessionID) => abortSession(sessionID)} onClose={() => { setMcpAuthModalOpen(false); @@ -8396,14 +8429,14 @@ export default function App() { const ok = await workspaceStore.createWorkspaceFlow(preset, folder); if (!ok || !request) return; const imported = await importSharedBundleIntoActiveWorker(request.request, { - localRoot: workspaceStore.activeWorkspaceRoot().trim(), + localRoot: workspaceStore.selectedWorkspaceRoot().trim(), }, request.bundle); setSharedBundleCreateWorkerRequest(null); if (imported) { if (request.bundle.type === "skill") { showSharedSkillSuccessToast({ title: "Skill added", - description: `Added '${request.bundle.name.trim() || "Shared skill"}' to ${describeWorkspaceForToasts(workspaceStore.activeWorkspaceDisplay())}.`, + description: `Added '${request.bundle.name.trim() || "Shared skill"}' to ${describeWorkspaceForToasts(workspaceStore.selectedWorkspaceDisplay())}.`, }); } setSharedSkillDestinationRequest(null); @@ -8419,7 +8452,7 @@ export default function App() { request ? { onReady: async () => { - const active = workspaceStore.activeWorkspaceDisplay(); + const active = workspaceStore.selectedWorkspaceDisplay(); await importSharedBundleIntoActiveWorker(request.request, { workspaceId: active.openworkWorkspaceId?.trim() || @@ -8524,7 +8557,7 @@ export default function App() { }; })()} workspaces={sharedSkillDestinationWorkspaces()} - activeWorkspaceId={workspaceStore.activeWorkspaceId()} + selectedWorkspaceId={workspaceStore.selectedWorkspaceId()} busyWorkspaceId={sharedSkillDestinationBusyId()} onClose={() => { if (sharedSkillDestinationBusyId()) return; diff --git a/apps/app/src/app/components/session/sidebar.tsx b/apps/app/src/app/components/session/sidebar.tsx index b95f17ba..d3023e1f 100644 --- a/apps/app/src/app/components/session/sidebar.tsx +++ b/apps/app/src/app/components/session/sidebar.tsx @@ -30,7 +30,7 @@ export type SidebarProps = { expandedSections: SidebarSectionState; onToggleSection: (section: keyof SidebarSectionState) => void; workspaceGroups: WorkspaceSessionGroup[]; - activeWorkspaceId: string; + selectedWorkspaceId: string; connectingWorkspaceId?: string | null; workspaceConnectionStateById: Record; onSelectWorkspace: (workspaceId: string) => void; @@ -303,7 +303,7 @@ export default function SessionSidebar(props: SidebarProps) { > {(group) => { - const isActive = () => props.activeWorkspaceId === group.workspace.id; + const isActive = () => props.selectedWorkspaceId === group.workspace.id; const isConnecting = () => props.connectingWorkspaceId === group.workspace.id; const pathLabel = () => workspacePathLabel(group.workspace); const detailLabel = () => workspaceDetailLabel(group.workspace); @@ -319,7 +319,7 @@ export default function SessionSidebar(props: SidebarProps) { const isActivelyConnecting = () => isConnecting() && connectionStatus() === "connecting"; const hasPendingSwitch = () => { const pendingId = props.connectingWorkspaceId; - if (!pendingId || pendingId === props.activeWorkspaceId) return false; + if (!pendingId || pendingId === props.selectedWorkspaceId) return false; const pendingStatus = props.workspaceConnectionStateById[pendingId]?.status ?? "idle"; return pendingStatus === "connecting"; }; diff --git a/apps/app/src/app/components/session/workspace-session-list.tsx b/apps/app/src/app/components/session/workspace-session-list.tsx index 4d546de9..4083c487 100644 --- a/apps/app/src/app/components/session/workspace-session-list.tsx +++ b/apps/app/src/app/components/session/workspace-session-list.tsx @@ -30,7 +30,7 @@ import { type Props = { workspaceSessionGroups: WorkspaceSessionGroup[]; - activeWorkspaceId: string; + selectedWorkspaceId: string; developerMode: boolean; selectedSessionId: string | null; showSessionActions?: boolean; @@ -39,9 +39,7 @@ type Props = { workspaceConnectionStateById: Record; newTaskDisabled: boolean; importingWorkspaceConfig: boolean; - onActivateWorkspace: ( - workspaceId: string, - ) => Promise | boolean | void; + onSelectWorkspace: (workspaceId: string) => Promise | boolean | void; onOpenSession: (workspaceId: string, sessionId: string) => void; onCreateTaskInWorkspace: (workspaceId: string) => void; onOpenRenameSession?: () => void; @@ -240,11 +238,11 @@ export default function WorkspaceSessionList(props: Props) { }; onMount(() => { - expandWorkspace(props.activeWorkspaceId); + expandWorkspace(props.selectedWorkspaceId); }); createEffect(() => { - expandWorkspace(props.activeWorkspaceId); + expandWorkspace(props.selectedWorkspaceId); }); const previewCount = (workspaceId: string) => { @@ -517,7 +515,7 @@ export default function WorkspaceSessionList(props: Props) { if (group.status === "error") return taskLoadError().label; if (isConnectionActionBusy()) return "Connecting"; if (!props.developerMode) return ""; - if (props.activeWorkspaceId === workspace().id) return "Active"; + if (props.selectedWorkspaceId === workspace().id) return "Selected"; return workspaceKindLabel(workspace()); }; const statusTone = () => { @@ -536,14 +534,14 @@ export default function WorkspaceSessionList(props: Props) { role="button" tabIndex={0} class={`w-full flex items-center justify-between rounded-xl px-3.5 py-2.5 text-left text-[13px] transition-colors ${ - props.activeWorkspaceId === workspace().id + props.selectedWorkspaceId === workspace().id ? "bg-gray-2/70 text-gray-12" : "text-gray-10 hover:bg-gray-1/70 hover:text-gray-12" } ${isConnecting() ? "opacity-75" : ""}`} onClick={() => { expandWorkspace(workspace().id); void Promise.resolve( - props.onActivateWorkspace(workspace().id), + props.onSelectWorkspace(workspace().id), ); }} onKeyDown={(event) => { @@ -552,7 +550,7 @@ export default function WorkspaceSessionList(props: Props) { event.preventDefault(); expandWorkspace(workspace().id); void Promise.resolve( - props.onActivateWorkspace(workspace().id), + props.onSelectWorkspace(workspace().id), ); }} > diff --git a/apps/app/src/app/components/shared-skill-destination-modal.tsx b/apps/app/src/app/components/shared-skill-destination-modal.tsx index 7ec16a13..5c612f9a 100644 --- a/apps/app/src/app/components/shared-skill-destination-modal.tsx +++ b/apps/app/src/app/components/shared-skill-destination-modal.tsx @@ -16,7 +16,7 @@ export default function SharedSkillDestinationModal(props: { open: boolean; skill: SharedSkillSummary | null; workspaces: WorkspaceInfo[]; - activeWorkspaceId?: string | null; + selectedWorkspaceId?: string | null; busyWorkspaceId?: string | null; onClose: () => void; onSubmitWorkspace: (workspaceId: string) => void | Promise; @@ -68,7 +68,7 @@ export default function SharedSkillDestinationModal(props: { createEffect(() => { if (!props.open) return; - const activeMatch = props.workspaces.find((workspace) => workspace.id === props.activeWorkspaceId) ?? props.workspaces[0] ?? null; + const activeMatch = props.workspaces.find((workspace) => workspace.id === props.selectedWorkspaceId) ?? props.workspaces[0] ?? null; setSelectedWorkspaceId(activeMatch?.id ?? null); }); @@ -169,7 +169,7 @@ export default function SharedSkillDestinationModal(props: {
{(workspace) => { - const isActive = () => workspace.id === props.activeWorkspaceId; + const isActive = () => workspace.id === props.selectedWorkspaceId; const isSelected = () => workspace.id === selectedWorkspaceId(); const isBusy = () => workspace.id === props.busyWorkspaceId; const WorkspaceIcon = () => (workspace.workspaceType === "remote" ? : ); diff --git a/apps/app/src/app/components/workspace-right-sidebar.tsx b/apps/app/src/app/components/workspace-right-sidebar.tsx index 7799175c..0aee91ef 100644 --- a/apps/app/src/app/components/workspace-right-sidebar.tsx +++ b/apps/app/src/app/components/workspace-right-sidebar.tsx @@ -25,7 +25,7 @@ type Props = { activeWorkspaceLabel: string; activeWorkspaceType: "local" | "remote"; openworkServerClient: OpenworkServerClient | null; - openworkServerWorkspaceId: string | null; + runtimeWorkspaceId: string | null; inboxId: string; onToggleExpanded: () => void; onCloseMobile?: () => void; @@ -141,7 +141,7 @@ export default function WorkspaceRightSidebar(props: Props) {
diff --git a/apps/app/src/app/context/extensions.ts b/apps/app/src/app/context/extensions.ts index ba225cba..4999d85c 100644 --- a/apps/app/src/app/context/extensions.ts +++ b/apps/app/src/app/context/extensions.ts @@ -37,12 +37,12 @@ export type ExtensionsStore = ReturnType; export function createExtensionsStore(options: { client: () => Client | null; projectDir: () => string; - activeWorkspaceRoot: () => string; + selectedWorkspaceRoot: () => string; workspaceType: () => "local" | "remote"; openworkServerClient: () => OpenworkServerClient | null; openworkServerStatus: () => OpenworkServerStatus; openworkServerCapabilities: () => OpenworkServerCapabilities | null; - openworkServerWorkspaceId: () => string | null; + runtimeWorkspaceId: () => string | null; setBusy: (value: boolean) => void; setBusyLabel: (value: string | null) => void; setBusyStartedAt: (value: number | null) => void; @@ -211,7 +211,7 @@ export function createExtensionsStore(options: { } async function refreshHubSkills(optionsOverride?: { force?: boolean }) { - const root = options.activeWorkspaceRoot().trim(); + const root = options.selectedWorkspaceRoot().trim(); const repo = hubRepo(); const loadKey = `${root}::${repo ? hubRepoKey(repo) : "none"}`; const openworkClient = options.openworkServerClient(); @@ -314,7 +314,7 @@ export function createExtensionsStore(options: { const isRemoteWorkspace = options.workspaceType() === "remote"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && @@ -366,11 +366,11 @@ export function createExtensionsStore(options: { }; async function refreshSkills(optionsOverride?: { force?: boolean }) { - const root = options.activeWorkspaceRoot().trim(); + const root = options.selectedWorkspaceRoot().trim(); const isRemoteWorkspace = options.workspaceType() === "remote"; const isLocalWorkspace = options.workspaceType() === "local"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && @@ -555,7 +555,7 @@ export function createExtensionsStore(options: { const isRemoteWorkspace = options.workspaceType() === "remote"; const isLocalWorkspace = options.workspaceType() === "local"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && @@ -695,7 +695,7 @@ export function createExtensionsStore(options: { const isRemoteWorkspace = options.workspaceType() === "remote"; const isLocalWorkspace = options.workspaceType() === "local"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && @@ -799,7 +799,7 @@ export function createExtensionsStore(options: { const isRemoteWorkspace = options.workspaceType() === "remote"; const isLocalWorkspace = options.workspaceType() === "local"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && @@ -927,7 +927,7 @@ export function createExtensionsStore(options: { const isRemoteWorkspace = options.workspaceType() === "remote"; const isLocalWorkspace = options.workspaceType() === "local"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && @@ -983,7 +983,7 @@ export function createExtensionsStore(options: { return { ok: false, message }; } - const targetDir = options.activeWorkspaceRoot().trim(); + const targetDir = options.selectedWorkspaceRoot().trim(); if (!targetDir) { const message = translate("skills.pick_workspace_first"); setSkillsStatus(message); @@ -1034,7 +1034,7 @@ export function createExtensionsStore(options: { return; } - const root = options.activeWorkspaceRoot().trim(); + const root = options.selectedWorkspaceRoot().trim(); if (!root) { setSkillsStatus(translate("skills.pick_workspace_first")); return; @@ -1076,7 +1076,7 @@ export function createExtensionsStore(options: { return; } - const root = options.activeWorkspaceRoot().trim(); + const root = options.selectedWorkspaceRoot().trim(); if (!root) { setSkillsStatus(translate("skills.pick_workspace_first")); return; @@ -1113,7 +1113,7 @@ export function createExtensionsStore(options: { const trimmed = name.trim(); if (!trimmed) return null; - const root = options.activeWorkspaceRoot().trim(); + const root = options.selectedWorkspaceRoot().trim(); if (!root) { setSkillsStatus(translate("skills.pick_workspace_first")); return null; @@ -1122,7 +1122,7 @@ export function createExtensionsStore(options: { const isRemoteWorkspace = options.workspaceType() === "remote"; const isLocalWorkspace = options.workspaceType() === "local"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && @@ -1179,7 +1179,7 @@ export function createExtensionsStore(options: { const trimmed = input.name.trim(); if (!trimmed) return; - const root = options.activeWorkspaceRoot().trim(); + const root = options.selectedWorkspaceRoot().trim(); if (!root) { setSkillsStatus(translate("skills.pick_workspace_first")); return; @@ -1188,7 +1188,7 @@ export function createExtensionsStore(options: { const isRemoteWorkspace = options.workspaceType() === "remote"; const isLocalWorkspace = options.workspaceType() === "local"; const openworkClient = options.openworkServerClient(); - const openworkWorkspaceId = options.openworkServerWorkspaceId(); + const openworkWorkspaceId = options.runtimeWorkspaceId(); const openworkCapabilities = options.openworkServerCapabilities(); const canUseOpenworkServer = options.openworkServerStatus() === "connected" && diff --git a/apps/app/src/app/context/session.ts b/apps/app/src/app/context/session.ts index 29ab2f16..c3cf7bbb 100644 --- a/apps/app/src/app/context/session.ts +++ b/apps/app/src/app/context/session.ts @@ -145,7 +145,7 @@ const appendPartDelta = (list: Part[], partID: string, field: string, delta: str export function createSessionStore(options: { client: () => Client | null; - activeWorkspaceRoot: () => string; + selectedWorkspaceRoot: () => string; selectedSessionId: () => string | null; setSelectedSessionId: (id: string | null) => void; sessionModelState: () => SessionModelState; @@ -350,7 +350,7 @@ export function createSessionStore(options: { if (!options.markReloadRequired) return; if (!part?.id || !part.messageID) return; - const root = normalizeDirectoryPath(options.activeWorkspaceRoot()); + const root = normalizeDirectoryPath(options.selectedWorkspaceRoot()); if (root) { const session = store.sessions.find((candidate) => candidate.id === part.sessionID) ?? null; const sessionRoot = normalizeDirectoryPath(session?.directory ?? ""); @@ -787,8 +787,8 @@ export function createSessionStore(options: { scopeScope: describeDirectoryScope(scopeRoot), queryDirectory: queryDirectory ?? null, queryScope: describeDirectoryScope(queryDirectory), - activeWorkspaceRoot: options.activeWorkspaceRoot?.() ?? null, - activeWorkspaceScope: describeDirectoryScope(options.activeWorkspaceRoot?.() ?? null), + selectedWorkspaceRoot: options.selectedWorkspaceRoot?.() ?? null, + activeWorkspaceScope: describeDirectoryScope(options.selectedWorkspaceRoot?.() ?? null), }); const start = Date.now(); diff --git a/apps/app/src/app/context/workspace.ts b/apps/app/src/app/context/workspace.ts index eb79dc1f..2c8c7b5c 100644 --- a/apps/app/src/app/context/workspace.ts +++ b/apps/app/src/app/context/workspace.ts @@ -56,9 +56,11 @@ import { workspaceImportConfig, workspaceOpenworkRead, workspaceOpenworkWrite, - workspaceSetActive, + workspaceSetRuntimeActive, + workspaceSetSelected, workspaceUpdateDisplayName, workspaceUpdateRemote, + resolveWorkspaceListSelectedId, type EngineDoctorResult, type EngineInfo, type SandboxDoctorResult, @@ -148,7 +150,7 @@ export function createWorkspaceStore(options: { updateOpenworkServerSettings: (next: OpenworkServerSettings) => void; openworkServerClient?: () => OpenworkServerClient | null; openworkServerStatus?: () => OpenworkServerStatus; - openworkServerWorkspaceId?: () => string | null; + runtimeWorkspaceId?: () => string | null; setOpencodeConnectStatus?: (status: OpencodeConnectStatus | null) => void; onEngineStable?: () => void; engineRuntime?: () => EngineRuntime; @@ -350,7 +352,7 @@ export function createWorkspaceStore(options: { const [projectDir, setProjectDir] = createSignal(""); const [workspaces, setWorkspaces] = createSignal([]); - const [activeWorkspaceId, setActiveWorkspaceId] = createSignal(""); + const [selectedWorkspaceId, setSelectedWorkspaceId] = createSignal(""); const [initialWorkspaceSetupComplete, setInitialWorkspaceSetupComplete] = createSignal( readInitialWorkspaceSetupComplete(), ); @@ -358,8 +360,28 @@ export function createWorkspaceStore(options: { readStarterBootstrapState(), ); - const syncActiveWorkspaceId = (id: string) => { - setActiveWorkspaceId(id); + const syncSelectedWorkspaceId = (id: string) => { + setSelectedWorkspaceId(id); + }; + + const pickSelectedWorkspaceId = ( + nextWorkspaces: WorkspaceInfo[], + preferredIds: Array = [], + fallbackList?: { selectedId?: string; activeId?: string | null } | null, + ) => { + for (const candidate of preferredIds) { + const id = candidate?.trim() ?? ""; + if (id && nextWorkspaces.some((workspace) => workspace.id === id)) { + return id; + } + } + + const responseId = resolveWorkspaceListSelectedId(fallbackList); + if (responseId && nextWorkspaces.some((workspace) => workspace.id === responseId)) { + return responseId; + } + + return nextWorkspaces[0]?.id ?? ""; }; const applyServerLocalWorkspaces = (nextLocals: WorkspaceInfo[], nextActiveId: string | null | undefined) => { @@ -367,14 +389,9 @@ export function createWorkspaceStore(options: { const merged = [...nextLocals, ...remotes]; setWorkspaces(merged); - const currentActiveId = activeWorkspaceId(); - const fallbackActiveId = merged.some((workspace) => workspace.id === currentActiveId) - ? currentActiveId - : merged[0]?.id ?? ""; - const resolvedActiveId = nextActiveId?.trim() || fallbackActiveId; - if (resolvedActiveId) { - syncActiveWorkspaceId(resolvedActiveId); - } + syncSelectedWorkspaceId( + pickSelectedWorkspaceId(merged, [selectedWorkspaceId()], { activeId: nextActiveId ?? null }), + ); }; const [authorizedDirs, setAuthorizedDirs] = createSignal([]); @@ -385,6 +402,7 @@ export function createWorkspaceStore(options: { const [createWorkspaceOpen, setCreateWorkspaceOpen] = createSignal(false); const [createRemoteWorkspaceOpen, setCreateRemoteWorkspaceOpen] = createSignal(false); const [connectingWorkspaceId, setConnectingWorkspaceId] = createSignal(null); + const [connectedWorkspaceId, setConnectedWorkspaceId] = createSignal(null); const [workspaceConnectionStateById, setWorkspaceConnectionStateById] = createSignal< Record >({}); @@ -393,7 +411,7 @@ export function createWorkspaceStore(options: { const [migrationRepairBusy, setMigrationRepairBusy] = createSignal(false); const [migrationRepairResult, setMigrationRepairResult] = createSignal(null); - const activeWorkspaceInfo = createMemo(() => workspaces().find((w) => w.id === activeWorkspaceId()) ?? null); + const selectedWorkspaceInfo = createMemo(() => workspaces().find((w) => w.id === selectedWorkspaceId()) ?? null); const firstRunWorkspaceSetup = createMemo( () => isTauriRuntime() && !initialWorkspaceSetupComplete() && workspaces().length === 0, ); @@ -402,8 +420,8 @@ export function createWorkspaceStore(options: { persistStarterBootstrapState(next); }; - const activeWorkspaceDisplay = createMemo(() => { - const ws = activeWorkspaceInfo(); + const selectedWorkspaceDisplay = createMemo(() => { + const ws = selectedWorkspaceInfo(); if (!ws) { return { id: "", @@ -434,13 +452,122 @@ export function createWorkspaceStore(options: { value === "openwork" ? "openwork" : "opencode"; const isOpenworkRemote = (workspace: WorkspaceInfo | null) => Boolean(workspace && workspace.workspaceType === "remote" && normalizeRemoteType(workspace.remoteType) === "openwork"); - const activeWorkspacePath = createMemo(() => { - const ws = activeWorkspaceInfo(); + const selectedWorkspacePath = createMemo(() => { + const ws = selectedWorkspaceInfo(); if (!ws) return ""; if (ws.workspaceType === "remote") return ws.directory?.trim() ?? ""; return ws.path ?? ""; }); - const activeWorkspaceRoot = createMemo(() => activeWorkspacePath().trim()); + const selectedWorkspaceRoot = createMemo(() => selectedWorkspacePath().trim()); + + const resolveWorkspaceEntryId = (input: { + workspaceId?: string | null; + workspaceType?: WorkspaceInfo["workspaceType"]; + targetRoot?: string | null; + directory?: string | null; + }) => { + const explicit = input.workspaceId?.trim() ?? ""; + if (explicit && workspaces().some((workspace) => workspace.id === explicit)) { + return explicit; + } + + const scope = normalizeDirectoryPath(input.targetRoot ?? input.directory ?? ""); + if (!scope) return null; + + const match = workspaces().find((workspace) => { + const workspaceScope = normalizeDirectoryPath( + workspace.workspaceType === "remote" + ? workspace.directory?.trim() ?? workspace.path?.trim() ?? "" + : workspace.path?.trim() ?? "", + ); + if (!workspaceScope || workspaceScope !== scope) return false; + if (input.workspaceType && workspace.workspaceType !== input.workspaceType) return false; + return true; + }); + + return match?.id ?? null; + }; + + const applySelectedWorkspacePresentation = async (workspace: WorkspaceInfo) => { + syncSelectedWorkspaceId(workspace.id); + if (workspace.workspaceType === "remote") { + setProjectDir(workspace.directory?.trim() ?? ""); + setWorkspaceConfig(null); + setWorkspaceConfigLoaded(true); + setAuthorizedDirs([]); + return; + } + + setProjectDir(workspace.path); + + if (isTauriRuntime()) { + setWorkspaceConfigLoaded(false); + try { + const cfg = await loadWorkspaceConfigFromOpenworkServer(workspace.path) + ?? await workspaceOpenworkRead({ workspacePath: workspace.path }); + setWorkspaceConfig(cfg); + setWorkspaceConfigLoaded(true); + + const roots = Array.isArray(cfg.authorizedRoots) ? cfg.authorizedRoots : []; + if (roots.length) { + setAuthorizedDirs(roots); + } else { + setAuthorizedDirs([workspace.path]); + } + } catch { + setWorkspaceConfig(null); + setWorkspaceConfigLoaded(true); + setAuthorizedDirs([workspace.path]); + } + return; + } + + if (!authorizedDirs().includes(workspace.path)) { + const merged = authorizedDirs().length ? authorizedDirs().slice() : []; + if (!merged.includes(workspace.path)) merged.push(workspace.path); + setAuthorizedDirs(merged); + } + }; + + async function selectWorkspace(workspaceId: string) { + const id = workspaceId.trim(); + if (!id) return false; + const workspace = workspaces().find((entry) => entry.id === id) ?? null; + if (!workspace) return false; + const changed = selectedWorkspaceId() !== id; + + await applySelectedWorkspacePresentation(workspace); + + if (changed) { + options.setSelectedSessionId(null); + options.setMessages([]); + options.setTodos([]); + options.setPendingPermissions([]); + options.setSessionStatusById({}); + } + + if (isTauriRuntime()) { + try { + await workspaceSetSelected(id); + } catch { + // ignore + } + } + + return true; + } + + async function ensureWorkspaceActivated(workspaceId: string) { + const id = workspaceId.trim(); + if (!id) return false; + if (selectedWorkspaceId() !== id) { + await selectWorkspace(id); + } + if (connectedWorkspaceId() === id && options.client()) { + return true; + } + return await activateWorkspace(id); + } const updateWorkspaceConnectionState = ( workspaceId: string, @@ -594,7 +721,7 @@ export function createWorkspaceStore(options: { const resolveEngineRuntime = () => options.engineRuntime?.() ?? "openwork-orchestrator"; const resolveWorkspacePaths = () => { - const active = activeWorkspacePath().trim(); + const active = selectedWorkspacePath().trim(); const locals = workspaces() .filter((ws) => ws.workspaceType === "local") .map((ws) => ws.path) @@ -617,7 +744,7 @@ export function createWorkspaceStore(options: { const resolveActiveOpenworkWorkspace = () => { const client = resolveConnectedOpenworkServer(); - const workspaceId = options.openworkServerWorkspaceId?.()?.trim() ?? ""; + const workspaceId = options.runtimeWorkspaceId?.()?.trim() ?? ""; if (!client || !workspaceId) return null; return { client, workspaceId }; }; @@ -637,9 +764,6 @@ export function createWorkspaceStore(options: { const loadWorkspaceConfigFromOpenworkServer = async (workspacePath: string): Promise => { const resolved = await findOpenworkWorkspaceByPath(workspacePath); if (!resolved) return null; - if (resolved.response.activeId !== resolved.workspaceId) { - await resolved.client.activateWorkspace(resolved.workspaceId); - } const config = await resolved.client.getConfig(resolved.workspaceId); return (config.openwork as WorkspaceOpenworkConfig | null | undefined) ?? null; }; @@ -739,8 +863,8 @@ export function createWorkspaceStore(options: { const info = await engineInfo(); setEngine(info); - const isRemoteWorkspace = activeWorkspaceInfo()?.workspaceType === "remote"; - const syncLocalState = !isRemoteWorkspace; + const connectedWorkspace = workspaces().find((workspace) => workspace.id === connectedWorkspaceId()) ?? null; + const syncLocalState = connectedWorkspace?.workspaceType !== "remote"; const username = info.opencodeUsername?.trim() ?? ""; const password = info.opencodePassword?.trim() ?? ""; @@ -763,7 +887,13 @@ export function createWorkspaceStore(options: { ) { const now = Date.now(); if (now - lastEngineReconnectAt > 10_000) { - const reconnectRoot = activeWorkspacePath().trim() || info.projectDir?.trim() || ""; + const connectedWorkspace = workspaces().find((workspace) => workspace.id === connectedWorkspaceId()) ?? null; + const reconnectRoot = + (connectedWorkspace?.workspaceType === "local" + ? connectedWorkspace.path?.trim() + : connectedWorkspace?.directory?.trim()) || + info.projectDir?.trim() || + ""; lastEngineReconnectAt = now; reconnectingEngine = true; connectToServer( @@ -837,6 +967,9 @@ export function createWorkspaceStore(options: { const next = workspaces().find((w) => w.id === id) ?? null; if (!next) return false; + if (selectedWorkspaceId() !== id) { + await selectWorkspace(id); + } const isRemote = next.workspaceType === "remote"; console.log("[workspace] activate", { id: next.id, type: next.workspaceType }); const activateStart = Date.now(); @@ -844,7 +977,7 @@ export function createWorkspaceStore(options: { id: next.id, type: next.workspaceType, remoteType: next.remoteType ?? null, - prevActiveId: activeWorkspaceId(), + prevActiveId: selectedWorkspaceId(), prevProjectDir: projectDir(), startupPref: options.startupPreference(), hasClient: Boolean(options.client()), @@ -968,7 +1101,7 @@ export function createWorkspaceStore(options: { openworkWorkspaceName: workspaceInfo?.name ?? next.openworkWorkspaceName ?? null, }); setWorkspaces(ws.workspaces); - syncActiveWorkspaceId(ws.activeId); + syncSelectedWorkspaceId(pickSelectedWorkspaceId(ws.workspaces, [id, selectedWorkspaceId()], ws)); } catch { // ignore } @@ -994,7 +1127,7 @@ export function createWorkspaceStore(options: { ); } - syncActiveWorkspaceId(id); + syncSelectedWorkspaceId(id); setProjectDir(resolvedDirectory || ""); setWorkspaceConfig(null); setWorkspaceConfigLoaded(true); @@ -1002,7 +1135,7 @@ export function createWorkspaceStore(options: { if (isTauriRuntime()) { try { - await workspaceSetActive(id); + await workspaceSetRuntimeActive(id); } catch { // ignore } @@ -1042,7 +1175,7 @@ export function createWorkspaceStore(options: { return false; } - syncActiveWorkspaceId(id); + syncSelectedWorkspaceId(id); setProjectDir(next.directory?.trim() ?? ""); setWorkspaceConfig(null); setWorkspaceConfigLoaded(true); @@ -1050,7 +1183,7 @@ export function createWorkspaceStore(options: { if (isTauriRuntime()) { try { - await workspaceSetActive(id); + await workspaceSetRuntimeActive(id); } catch { // ignore } @@ -1075,7 +1208,7 @@ export function createWorkspaceStore(options: { prevProjectDir: oldWorkspacePath, }); - syncActiveWorkspaceId(id); + syncSelectedWorkspaceId(id); setProjectDir(nextRoot); if (isTauriRuntime()) { @@ -1108,7 +1241,7 @@ export function createWorkspaceStore(options: { if (!isRemote) { await activateOpenworkHostWorkspace(next.path); } - await workspaceSetActive(id); + await workspaceSetRuntimeActive(id); } catch { // ignore } @@ -1340,9 +1473,9 @@ export function createWorkspaceStore(options: { targetRoot: context?.targetRoot ?? null, targetRootScope: describeDirectoryScope(context?.targetRoot), workspaceId: context?.workspaceId ?? null, - activeWorkspaceId: activeWorkspaceId() || null, - activeWorkspaceRoot: activeWorkspaceRoot().trim() || null, - activeWorkspaceScope: describeDirectoryScope(activeWorkspaceRoot().trim()), + selectedWorkspaceId: selectedWorkspaceId() || null, + selectedWorkspaceRoot: selectedWorkspaceRoot().trim() || null, + activeWorkspaceScope: describeDirectoryScope(selectedWorkspaceRoot().trim()), projectDir: projectDir().trim() || null, clientDirectory: options.clientDirectory().trim() || null, healthTimeoutMs: resolveConnectHealthTimeoutMs(context?.reason), @@ -1403,7 +1536,9 @@ export function createWorkspaceStore(options: { directory: resolvedDirectory, }); setWorkspaces(updated.workspaces); - syncActiveWorkspaceId(updated.activeId); + syncSelectedWorkspaceId( + pickSelectedWorkspaceId(updated.workspaces, [context.workspaceId, selectedWorkspaceId()], updated), + ); } setProjectDir(resolvedDirectory); nextClient = createClient(nextBaseUrl, resolvedDirectory, auth); @@ -1417,6 +1552,14 @@ export function createWorkspaceStore(options: { options.setConnectedVersion(health.version); options.setBaseUrl(nextBaseUrl); options.setClientDirectory(resolvedDirectory); + setConnectedWorkspaceId( + resolveWorkspaceEntryId({ + workspaceId: context?.workspaceId ?? null, + workspaceType: context?.workspaceType, + targetRoot: context?.targetRoot ?? resolvedDirectory, + directory: resolvedDirectory, + }), + ); const providersPromise = (async () => { const providersAt = Date.now(); @@ -1477,14 +1620,14 @@ export function createWorkspaceStore(options: { } })(); - const targetRoot = context?.targetRoot ?? (resolvedDirectory || activeWorkspaceRoot().trim()); + const targetRoot = context?.targetRoot ?? (resolvedDirectory || selectedWorkspaceRoot().trim()); wsDebug("connect:loadSessions", { targetRoot, targetRootScope: describeDirectoryScope(targetRoot), resolvedDirectory, resolvedDirectoryScope: describeDirectoryScope(resolvedDirectory), - activeWorkspaceId: activeWorkspaceId() || null, - activeWorkspaceRoot: activeWorkspaceRoot().trim() || null, + selectedWorkspaceId: selectedWorkspaceId() || null, + selectedWorkspaceRoot: selectedWorkspaceRoot().trim() || null, }); const sessionsAt = Date.now(); await options.loadSessions(targetRoot); @@ -1523,6 +1666,7 @@ export function createWorkspaceStore(options: { } catch (e) { options.setClient(null); options.setConnectedVersion(null); + setConnectedWorkspaceId(null); const message = e instanceof Error ? e.message : safeStringify(e); wsDebug("connect:error", { ms: Date.now() - connectStart, message }); connectMetrics.totalMs = Date.now() - connectStart; @@ -1556,12 +1700,12 @@ export function createWorkspaceStore(options: { } const openEmptySession = async (scopeRoot?: string) => { - const root = (scopeRoot ?? activeWorkspaceRoot().trim()).trim(); + const root = (scopeRoot ?? selectedWorkspaceRoot().trim()).trim(); wsDebug("open-empty-session:start", { scopeRoot: scopeRoot ?? null, resolvedRoot: root || null, - activeWorkspaceId: activeWorkspaceId(), - activeWorkspace: activeWorkspaceInfo(), + selectedWorkspaceId: selectedWorkspaceId(), + activeWorkspace: selectedWorkspaceInfo(), hasClient: Boolean(options.client()), }); @@ -1596,7 +1740,7 @@ export function createWorkspaceStore(options: { return false; } - await openEmptySession(activeWorkspaceRoot().trim() || workspacePath); + await openEmptySession(selectedWorkspaceRoot().trim() || workspacePath); return true; }; @@ -1639,14 +1783,16 @@ export function createWorkspaceStore(options: { } } - applyServerLocalWorkspaces(ws.workspaces, ws.activeId); - if (ws.activeId) { - updateWorkspaceConnectionState(ws.activeId, { status: "connected", message: null }); + const nextSelectedId = pickSelectedWorkspaceId(ws.workspaces, [resolveWorkspaceListSelectedId(ws)], ws); + applyServerLocalWorkspaces(ws.workspaces, nextSelectedId); + if (nextSelectedId) { + syncSelectedWorkspaceId(nextSelectedId); + updateWorkspaceConnectionState(nextSelectedId, { status: "connected", message: null }); } setCreateWorkspaceOpen(false); - const opened = await activateFreshLocalWorkspace(ws.activeId ?? null, resolvedFolder); + const opened = await activateFreshLocalWorkspace(nextSelectedId || null, resolvedFolder); if (!opened) { return false; } @@ -1765,11 +1911,14 @@ export function createWorkspaceStore(options: { // ignore desktop mirror failures here } } - applyServerLocalWorkspaces(created.workspaces, created.activeId); + const localId = pickSelectedWorkspaceId(created.workspaces, [resolveWorkspaceListSelectedId(created)], created); + applyServerLocalWorkspaces(created.workspaces, localId); + if (localId) { + syncSelectedWorkspaceId(localId); + } setSandboxStep("workspace", { status: "done", detail: null }); // Remove the local workspace entry to avoid duplicate Local+Remote rows. - const localId = created.activeId; if (localId) { pushSandboxCreateLog("Removing local worker row (will re-add as remote sandbox)..."); const activeLocalWorkspace = openworkServer ? await findOpenworkWorkspaceByPath(resolvedFolder) : null; @@ -2075,8 +2224,9 @@ export function createWorkspaceStore(options: { sandboxContainerName: input.sandboxContainerName ?? null, }); setWorkspaces(ws.workspaces); - syncActiveWorkspaceId(ws.activeId); - console.log("[workspace] create remote complete:", ws.activeId ?? "none"); + const nextSelectedId = pickSelectedWorkspaceId(ws.workspaces, [resolveWorkspaceListSelectedId(ws)], ws); + syncSelectedWorkspaceId(nextSelectedId); + console.log("[workspace] create remote complete:", nextSelectedId || "none"); } else { const workspaceId = `remote:${resolvedBaseUrl}:${finalDirectory}`; const nextWorkspace: WorkspaceInfo = { @@ -2106,7 +2256,7 @@ export function createWorkspaceStore(options: { const withoutMatch = prev.filter((workspace) => workspace.id !== workspaceId); return [...withoutMatch, nextWorkspace]; }); - syncActiveWorkspaceId(workspaceId); + syncSelectedWorkspaceId(workspaceId); console.log("[workspace] create remote complete:", workspaceId); } @@ -2120,12 +2270,12 @@ export function createWorkspaceStore(options: { setCreateWorkspaceOpen(false); setCreateRemoteWorkspaceOpen(false); } - const activeId = activeWorkspaceId(); + const activeId = selectedWorkspaceId(); if (activeId) { updateWorkspaceConnectionState(activeId, { status: "connected", message: null }); } - await openEmptySession(activeWorkspaceRoot().trim() || finalDirectory); + await openEmptySession(selectedWorkspaceRoot().trim() || finalDirectory); return true; } catch (e) { const message = e instanceof Error ? e.message : safeStringify(e); @@ -2232,7 +2382,7 @@ export function createWorkspaceStore(options: { return false; } - const isActive = activeWorkspaceId() === id; + const isActive = connectedWorkspaceId() === id; const finalDirectory = resolvedDirectory || ""; if (isActive) { @@ -2275,7 +2425,7 @@ export function createWorkspaceStore(options: { openworkWorkspaceName: openworkWorkspace?.name ?? workspace.openworkWorkspaceName ?? null, }); setWorkspaces(ws.workspaces); - syncActiveWorkspaceId(ws.activeId); + syncSelectedWorkspaceId(pickSelectedWorkspaceId(ws.workspaces, [id, selectedWorkspaceId()], ws)); } catch { // ignore } @@ -2327,7 +2477,7 @@ export function createWorkspaceStore(options: { console.log("[workspace] forget", { id }); try { - const previousActive = activeWorkspaceId(); + const previousActive = selectedWorkspaceId(); const openworkWorkspace = workspace?.workspaceType === "local" ? await findOpenworkWorkspaceByPath(workspace.path) : null; const ws = openworkWorkspace ? await openworkWorkspace.client.deleteWorkspace(openworkWorkspace.workspaceId).then((response) => ({ @@ -2352,16 +2502,17 @@ export function createWorkspaceStore(options: { clearWorkspaceConnectionState(id); if (!openworkWorkspace) { - syncActiveWorkspaceId(ws.activeId); + syncSelectedWorkspaceId(pickSelectedWorkspaceId(ws.workspaces, [selectedWorkspaceId()], ws)); } - const active = ws.workspaces.find((w) => w.id === ws.activeId) ?? null; - if (active) { - setProjectDir(active.workspaceType === "remote" ? active.directory?.trim() ?? "" : active.path); + const nextSelectedId = pickSelectedWorkspaceId(ws.workspaces, [selectedWorkspaceId()], ws); + const selected = ws.workspaces.find((w) => w.id === nextSelectedId) ?? null; + if (selected) { + setProjectDir(selected.workspaceType === "remote" ? selected.directory?.trim() ?? "" : selected.path); } - if (ws.activeId && ws.activeId !== previousActive) { - await activateWorkspace(ws.activeId); + if (nextSelectedId && nextSelectedId !== previousActive) { + await activateWorkspace(nextSelectedId); } } catch (e) { const message = e instanceof Error ? e.message : safeStringify(e); @@ -2378,7 +2529,7 @@ export function createWorkspaceStore(options: { if (!workspace) return false; const reconnect = async () => { - if (activeWorkspaceId() === id) { + if (connectedWorkspaceId() === id) { return await activateWorkspace(id); } return await testWorkspaceConnection(id); @@ -2465,7 +2616,7 @@ export function createWorkspaceStore(options: { }); setWorkspaces(updated.workspaces); - syncActiveWorkspaceId(updated.activeId); + syncSelectedWorkspaceId(pickSelectedWorkspaceId(updated.workspaces, [id, selectedWorkspaceId()], updated)); const ok = await reconnect(); if (!ok) { @@ -2519,10 +2670,18 @@ export function createWorkspaceStore(options: { throw new Error(details || `Failed to stop sandbox (status ${result.status})`); } - // If the user stopped the active workspace, proactively disconnect the client. - if (activeWorkspaceId() === id) { + // If the user stopped the runtime-connected workspace, proactively disconnect the client. + if (connectedWorkspaceId() === id) { options.setClient(null); options.setConnectedVersion(null); + setConnectedWorkspaceId(null); + if (isTauriRuntime()) { + try { + await workspaceSetRuntimeActive(null); + } catch { + // ignore + } + } options.setSseConnected(false); } @@ -2631,7 +2790,7 @@ export function createWorkspaceStore(options: { return; } - const targetId = workspaceId?.trim() || activeWorkspaceInfo()?.id || ""; + const targetId = workspaceId?.trim() || selectedWorkspaceInfo()?.id || ""; if (!targetId) { options.setError("Select a worker to export"); return; @@ -2720,12 +2879,13 @@ export function createWorkspaceStore(options: { }); setWorkspaces(ws.workspaces); - syncActiveWorkspaceId(ws.activeId); + const nextSelectedId = pickSelectedWorkspaceId(ws.workspaces, [resolveWorkspaceListSelectedId(ws)], ws); + syncSelectedWorkspaceId(nextSelectedId); setCreateWorkspaceOpen(false); setCreateRemoteWorkspaceOpen(false); markOnboardingComplete(); - const opened = await activateFreshLocalWorkspace(ws.activeId ?? null, resolvedFolder); + const opened = await activateFreshLocalWorkspace(nextSelectedId || null, resolvedFolder); if (!opened) { return; } @@ -2739,9 +2899,9 @@ export function createWorkspaceStore(options: { function canRepairOpencodeMigration() { if (!isTauriRuntime()) return false; - const workspace = activeWorkspaceInfo(); + const workspace = selectedWorkspaceInfo(); if (!workspace || workspace.workspaceType !== "local") return false; - return Boolean(activeWorkspacePath().trim()); + return Boolean(selectedWorkspacePath().trim()); } async function repairOpencodeMigration(optionsOverride?: { navigate?: boolean }) { @@ -2754,7 +2914,7 @@ export function createWorkspaceStore(options: { if (migrationRepairBusy()) return false; - const workspace = activeWorkspaceInfo(); + const workspace = selectedWorkspaceInfo(); if (!workspace || workspace.workspaceType !== "local") { const message = t("app.migration.local_only", currentLocale()); setMigrationRepairResult({ ok: false, message }); @@ -2762,7 +2922,7 @@ export function createWorkspaceStore(options: { return false; } - const root = activeWorkspacePath().trim(); + const root = selectedWorkspacePath().trim(); if (!root) { const message = t("app.migration.workspace_required", currentLocale()); setMigrationRepairResult({ ok: false, message }); @@ -2847,12 +3007,12 @@ export function createWorkspaceStore(options: { } const overrideWorkspacePath = optionsOverride?.workspacePath?.trim() ?? ""; - if (activeWorkspaceInfo()?.workspaceType === "remote" && !overrideWorkspacePath) { + if (selectedWorkspaceInfo()?.workspaceType === "remote" && !overrideWorkspacePath) { options.setError(t("app.error.host_requires_local", currentLocale())); return false; } - const dir = (overrideWorkspacePath || activeWorkspacePath() || projectDir()).trim(); + const dir = (overrideWorkspacePath || selectedWorkspacePath() || projectDir()).trim(); if (!dir) { options.setError(t("app.error.pick_workspace_folder", currentLocale())); return false; @@ -2966,9 +3126,7 @@ export function createWorkspaceStore(options: { } } applyServerLocalWorkspaces(ws.workspaces, ws.activeId); - if (ws.activeId) { - updateWorkspaceConnectionState(ws.activeId, { status: "connected", message: null }); - } + updateWorkspaceConnectionState(id, { status: "connected", message: null }); return true; } catch (e) { const message = e instanceof Error ? e.message : safeStringify(e); @@ -2981,9 +3139,8 @@ export function createWorkspaceStore(options: { try { const ws = await workspaceUpdateDisplayName({ workspaceId: id, displayName: nextDisplayName }); setWorkspaces(ws.workspaces); - if (ws.activeId) { - updateWorkspaceConnectionState(ws.activeId, { status: "connected", message: null }); - } + syncSelectedWorkspaceId(pickSelectedWorkspaceId(ws.workspaces, [id, selectedWorkspaceId()], ws)); + updateWorkspaceConnectionState(id, { status: "connected", message: null }); return true; } catch (e) { const message = e instanceof Error ? e.message : safeStringify(e); @@ -3022,6 +3179,14 @@ export function createWorkspaceStore(options: { options.setClient(null); options.setConnectedVersion(null); + setConnectedWorkspaceId(null); + if (isTauriRuntime()) { + try { + await workspaceSetRuntimeActive(null); + } catch { + // ignore + } + } options.setSelectedSessionId(null); options.setMessages([]); options.setTodos([]); @@ -3049,12 +3214,12 @@ export function createWorkspaceStore(options: { return false; } - if (activeWorkspaceDisplay().workspaceType !== "local") { + if (selectedWorkspaceDisplay().workspaceType !== "local") { options.setError("Reload is only available for local workers."); return false; } - const root = activeWorkspacePath().trim(); + const root = selectedWorkspacePath().trim(); if (!root) { options.setError("Pick a worker folder first."); return false; @@ -3071,7 +3236,7 @@ export function createWorkspaceStore(options: { await orchestratorInstanceDispose(root); await orchestratorWorkspaceActivate({ workspacePath: root, - name: activeWorkspaceInfo()?.displayName?.trim() || activeWorkspaceInfo()?.name?.trim() || null, + name: selectedWorkspaceInfo()?.displayName?.trim() || selectedWorkspaceInfo()?.name?.trim() || null, }); const nextInfo = await engineInfo(); @@ -3217,8 +3382,8 @@ export function createWorkspaceStore(options: { async function persistAuthorizedRoots(nextRoots: string[]) { if (!isTauriRuntime()) return; - if (activeWorkspaceInfo()?.workspaceType === "remote") return; - const root = activeWorkspacePath().trim(); + if (selectedWorkspaceInfo()?.workspaceType === "remote") return; + const root = selectedWorkspacePath().trim(); if (!root) return; const existing = workspaceConfig(); @@ -3239,8 +3404,8 @@ export function createWorkspaceStore(options: { async function persistReloadSettings(next: { auto?: boolean; resume?: boolean }) { if (!isTauriRuntime()) return; - if (activeWorkspaceInfo()?.workspaceType === "remote") return; - const root = activeWorkspacePath().trim(); + if (selectedWorkspaceInfo()?.workspaceType === "remote") return; + const root = selectedWorkspacePath().trim(); if (!root) return; const existing = workspaceConfig(); @@ -3263,7 +3428,7 @@ export function createWorkspaceStore(options: { } async function addAuthorizedDir() { - if (activeWorkspaceInfo()?.workspaceType === "remote") return; + if (selectedWorkspaceInfo()?.workspaceType === "remote") return; const next = newAuthorizedDir().trim(); if (!next) return; @@ -3281,7 +3446,7 @@ export function createWorkspaceStore(options: { async function addAuthorizedDirFromPicker(optionsOverride?: { persistToWorkspace?: boolean }) { if (!isTauriRuntime()) return; - if (activeWorkspaceInfo()?.workspaceType === "remote") return; + if (selectedWorkspaceInfo()?.workspaceType === "remote") return; try { const selection = await pickDirectory({ title: t("onboarding.authorize_folder", currentLocale()) }); @@ -3302,7 +3467,7 @@ export function createWorkspaceStore(options: { } async function removeAuthorizedDir(dir: string) { - if (activeWorkspaceInfo()?.workspaceType === "remote") return; + if (selectedWorkspaceInfo()?.workspaceType === "remote") return; const roots = normalizeRoots(authorizedDirs().filter((root) => root !== dir)); setAuthorizedDirs(roots); @@ -3333,7 +3498,7 @@ export function createWorkspaceStore(options: { try { const ws = await workspaceBootstrap(); setWorkspaces(ws.workspaces); - syncActiveWorkspaceId(ws.activeId); + syncSelectedWorkspaceId(pickSelectedWorkspaceId(ws.workspaces, [resolveWorkspaceListSelectedId(ws)], ws)); } catch { // ignore } @@ -3351,7 +3516,7 @@ export function createWorkspaceStore(options: { await refreshEngineDoctor(); if (isTauriRuntime()) { - const active = workspaces().find((w) => w.id === activeWorkspaceId()) ?? null; + const active = workspaces().find((w) => w.id === selectedWorkspaceId()) ?? null; if (active) { if (active.workspaceType === "remote") { setProjectDir(active.directory?.trim() ?? ""); @@ -3384,7 +3549,7 @@ export function createWorkspaceStore(options: { options.setBaseUrl(info.baseUrl); } - const activeWorkspace = activeWorkspaceInfo(); + const activeWorkspace = selectedWorkspaceInfo(); if (activeWorkspace?.workspaceType === "remote") { options.setStartupPreference("server"); options.setOnboardingStep("connecting"); @@ -3404,11 +3569,11 @@ export function createWorkspaceStore(options: { return; } - if (activeWorkspacePath().trim()) { + if (selectedWorkspacePath().trim()) { options.setStartupPreference("local"); if (info?.running && info.baseUrl) { - const bootstrapRoot = activeWorkspacePath().trim() || info.projectDir?.trim() || ""; + const bootstrapRoot = selectedWorkspacePath().trim() || info.projectDir?.trim() || ""; options.setOnboardingStep("connecting"); const ok = await connectToServer( info.baseUrl, @@ -3426,7 +3591,7 @@ export function createWorkspaceStore(options: { } options.setOnboardingStep("connecting"); - const ok = await startHost({ workspacePath: activeWorkspacePath().trim() }); + const ok = await startHost({ workspacePath: selectedWorkspacePath().trim() }); if (!ok) { options.setOnboardingStep("local"); return; @@ -3474,7 +3639,7 @@ export function createWorkspaceStore(options: { async function onStartHost() { options.setStartupPreference("local"); options.setOnboardingStep("connecting"); - const ok = await startHost({ workspacePath: activeWorkspacePath().trim() }); + const ok = await startHost({ workspacePath: selectedWorkspacePath().trim() }); if (!ok) { options.setOnboardingStep("local"); } @@ -3483,7 +3648,7 @@ export function createWorkspaceStore(options: { async function onAttachHost() { options.setStartupPreference("local"); options.setOnboardingStep("connecting"); - const attachRoot = activeWorkspacePath().trim() || engine()?.projectDir?.trim() || ""; + const attachRoot = selectedWorkspacePath().trim() || engine()?.projectDir?.trim() || ""; const ok = await connectToServer( engine()?.baseUrl ?? "", attachRoot || undefined, @@ -3541,7 +3706,7 @@ export function createWorkspaceStore(options: { sandboxCreatePhase, projectDir, workspaces, - activeWorkspaceId, + selectedWorkspaceId, authorizedDirs, newAuthorizedDir, workspaceConfig, @@ -3549,15 +3714,16 @@ export function createWorkspaceStore(options: { createWorkspaceOpen, createRemoteWorkspaceOpen, connectingWorkspaceId, + connectedWorkspaceId, workspaceConnectionStateById, exportingWorkspaceConfig, importingWorkspaceConfig, migrationRepairBusy, migrationRepairResult, - activeWorkspaceInfo, - activeWorkspaceDisplay, - activeWorkspacePath, - activeWorkspaceRoot, + selectedWorkspaceInfo, + selectedWorkspaceDisplay, + selectedWorkspacePath, + selectedWorkspaceRoot, setCreateWorkspaceOpen, setCreateRemoteWorkspaceOpen, setProjectDir, @@ -3566,10 +3732,12 @@ export function createWorkspaceStore(options: { setWorkspaceConfig, setWorkspaceConfigLoaded, setWorkspaces, - syncActiveWorkspaceId: syncActiveWorkspaceId, + syncSelectedWorkspaceId: syncSelectedWorkspaceId, + selectWorkspace, refreshEngine, refreshEngineDoctor, activateWorkspace, + ensureWorkspaceActivated, testWorkspaceConnection, connectToServer, createWorkspaceFlow, diff --git a/apps/app/src/app/lib/openwork-server.ts b/apps/app/src/app/lib/openwork-server.ts index f8726f70..eae04df9 100644 --- a/apps/app/src/app/lib/openwork-server.ts +++ b/apps/app/src/app/lib/openwork-server.ts @@ -43,7 +43,8 @@ export type OpenworkServerDiagnostics = { approval: { mode: "manual" | "auto"; timeoutMs: number }; corsOrigins: string[]; workspaceCount: number; - activeWorkspaceId: string | null; + activeWorkspaceId?: string | null; + selectedWorkspaceId?: string | null; workspace: OpenworkWorkspaceInfo | null; authorizedRoots: string[]; server: { host: string; port: number; configPath?: string | null }; @@ -530,6 +531,8 @@ export type OpenworkReloadEvent = { timestamp: number; }; +// Fallback for explicit server-mode URL derivation. Desktop local workers replace this +// with the persisted runtime-discovered port once the host reports it. export const DEFAULT_OPENWORK_SERVER_PORT = 8787; const STORAGE_URL_OVERRIDE = "openwork.server.urlOverride"; diff --git a/apps/app/src/app/lib/tauri.ts b/apps/app/src/app/lib/tauri.ts index 930939fd..ce9e2dd2 100644 --- a/apps/app/src/app/lib/tauri.ts +++ b/apps/app/src/app/lib/tauri.ts @@ -130,10 +130,21 @@ export type WorkspaceInfo = { }; export type WorkspaceList = { - activeId: string; + // UI-selected workspace persisted by the desktop shell. + selectedId?: string; + // Runtime/watch target currently followed by the desktop host. + watchedId?: string | null; + // Legacy desktop payloads used activeId for the UI-selected workspace. + activeId?: string | null; workspaces: WorkspaceInfo[]; }; +export function resolveWorkspaceListSelectedId( + list: Pick | null | undefined, +): string { + return list?.selectedId?.trim() || list?.activeId?.trim() || ""; +} + export type WorkspaceExportSummary = { outputPath: string; included: number; @@ -166,8 +177,12 @@ export async function workspaceBootstrap(): Promise { return invoke("workspace_bootstrap"); } -export async function workspaceSetActive(workspaceId: string): Promise { - return invoke("workspace_set_active", { workspaceId }); +export async function workspaceSetSelected(workspaceId: string): Promise { + return invoke("workspace_set_selected", { workspaceId }); +} + +export async function workspaceSetRuntimeActive(workspaceId: string | null): Promise { + return invoke("workspace_set_runtime_active", { workspaceId: workspaceId ?? "" }); } export async function workspaceCreate(input: { diff --git a/apps/app/src/app/pages/config.tsx b/apps/app/src/app/pages/config.tsx index c0e0374a..e7ba08a2 100644 --- a/apps/app/src/app/pages/config.tsx +++ b/apps/app/src/app/pages/config.tsx @@ -21,7 +21,7 @@ export type ConfigViewProps = { openworkServerUrl: string; openworkServerSettings: OpenworkServerSettings; openworkServerHostInfo: OpenworkServerInfo | null; - openworkServerWorkspaceId: string | null; + runtimeWorkspaceId: string | null; updateOpenworkServerSettings: (next: OpenworkServerSettings) => void; resetOpenworkServerSettings: () => void; @@ -112,7 +112,7 @@ export default function ConfigView(props: ConfigViewProps) { }); const resolvedWorkspaceId = createMemo(() => { - const explicitId = props.openworkServerWorkspaceId?.trim() ?? ""; + const explicitId = props.runtimeWorkspaceId?.trim() ?? ""; if (explicitId) return explicitId; return parseOpenworkWorkspaceIdFromUrl(openworkUrl()) ?? ""; }); @@ -153,7 +153,7 @@ export default function ConfigView(props: ConfigViewProps) { developerMode: props.developerMode, }, workspace: { - openworkServerWorkspaceId: props.openworkServerWorkspaceId ?? null, + runtimeWorkspaceId: props.runtimeWorkspaceId ?? null, clientConnected: props.clientConnected, anyActiveRuns: props.anyActiveRuns, }, @@ -222,11 +222,11 @@ export default function ConfigView(props: ConfigViewProps) {
Workspace config
- These settings affect the active workspace (sharing, reload, bots). Global app behavior lives in Settings. + These settings affect the selected workspace. Runtime-only actions apply to whichever workspace is currently connected.
- +
- Workspace: {props.openworkServerWorkspaceId} + Workspace: {props.runtimeWorkspaceId}
@@ -491,8 +491,8 @@ export default function ConfigView(props: ConfigViewProps) { label="OpenWork server URL" value={openworkUrl()} onInput={(event) => setOpenworkUrl(event.currentTarget.value)} - placeholder="http://127.0.0.1:8787" - hint="Use the URL shared by your OpenWork server." + placeholder="http://127.0.0.1:" + hint="Use the URL shared by your OpenWork server. Local desktop workers reuse a persistent high port in the 48000-51000 range." disabled={props.busy} /> diff --git a/apps/app/src/app/pages/dashboard.tsx b/apps/app/src/app/pages/dashboard.tsx index 4f8694c4..1099e062 100644 --- a/apps/app/src/app/pages/dashboard.tsx +++ b/apps/app/src/app/pages/dashboard.tsx @@ -124,7 +124,7 @@ export type DashboardViewProps = { saveShareRemoteAccess: (enabled: boolean) => Promise; openworkServerCapabilities: OpenworkServerCapabilities | null; openworkServerDiagnostics: OpenworkServerDiagnostics | null; - openworkServerWorkspaceId: string | null; + runtimeWorkspaceId: string | null; activeWorkspaceType: "local" | "remote"; openworkAuditEntries: OpenworkAuditEntry[]; openworkAuditStatus: "idle" | "loading" | "error"; @@ -146,12 +146,13 @@ export type DashboardViewProps = { setWorkspaceAutoReloadEnabled: (value: boolean) => void | Promise; workspaceAutoReloadResumeEnabled: boolean; setWorkspaceAutoReloadResumeEnabled: (value: boolean) => void | Promise; - activeWorkspaceDisplay: WorkspaceInfo; + selectedWorkspaceDisplay: WorkspaceInfo; workspaces: WorkspaceInfo[]; - activeWorkspaceId: string; + selectedWorkspaceId: string; connectingWorkspaceId: string | null; workspaceConnectionStateById: Record; - activateWorkspace: (workspaceId: string) => Promise | boolean | void; + selectWorkspace: (workspaceId: string) => Promise | boolean | void; + ensureWorkspaceActivated: (workspaceId: string) => Promise | boolean | void; testWorkspaceConnection: (workspaceId: string) => Promise | boolean; recoverWorkspace: (workspaceId: string) => Promise | boolean; openCreateWorkspace: () => void; @@ -181,7 +182,7 @@ export type DashboardViewProps = { scheduledJobsUpdatedAt: number | null; refreshScheduledJobs: (options?: { force?: boolean }) => void; deleteScheduledJob: (name: string) => Promise | void; - activeWorkspaceRoot: string; + selectedWorkspaceRoot: string; isRemoteWorkspace: boolean; refreshSkills: (options?: { force?: boolean }) => void; refreshHubSkills: (options?: { force?: boolean }) => void; @@ -413,14 +414,9 @@ export default function DashboardView(props: DashboardViewProps) { : "Local"; const openSessionFromList = (workspaceId: string, sessionId: string) => { - // Route-driven selection: navigate first and let the route effect own selectSession. - if (workspaceId === props.activeWorkspaceId) { - props.setView("session", sessionId); - return; - } - // For different workspace, activate workspace first void (async () => { - await Promise.resolve(props.activateWorkspace(workspaceId)); + const ready = await Promise.resolve(props.ensureWorkspaceActivated(workspaceId)); + if (!ready) return; props.setView("session", sessionId); })(); }; @@ -428,12 +424,9 @@ export default function DashboardView(props: DashboardViewProps) { const createTaskInWorkspace = (workspaceId: string) => { const id = workspaceId.trim(); if (!id) return; - if (id === props.activeWorkspaceId) { - props.createSessionAndOpen(); - return; - } void (async () => { - await Promise.resolve(props.activateWorkspace(id)); + const ready = await Promise.resolve(props.ensureWorkspaceActivated(id)); + if (!ready) return; props.createSessionAndOpen(); })(); }; @@ -1105,14 +1098,14 @@ export default function DashboardView(props: DashboardViewProps) {
{title()} @@ -1209,7 +1202,7 @@ export default function DashboardView(props: DashboardViewProps) { refreshJobs={props.refreshScheduledJobs} deleteJob={props.deleteScheduledJob} isWindows={props.isWindows} - activeWorkspaceRoot={props.activeWorkspaceRoot} + selectedWorkspaceRoot={props.selectedWorkspaceRoot} createSessionAndOpen={props.createSessionAndOpen} setPrompt={props.setPrompt} newTaskDisabled={props.newTaskDisabled} @@ -1225,7 +1218,7 @@ export default function DashboardView(props: DashboardViewProps) { @@ -1325,7 +1318,7 @@ export default function DashboardView(props: DashboardViewProps) { openworkServerUrl={props.openworkServerUrl} openworkServerSettings={props.openworkServerSettings} openworkServerHostInfo={props.openworkServerHostInfo} - openworkServerWorkspaceId={props.openworkServerWorkspaceId} + runtimeWorkspaceId={props.runtimeWorkspaceId} updateOpenworkServerSettings={props.updateOpenworkServerSettings} resetOpenworkServerSettings={props.resetOpenworkServerSettings} testOpenworkServerConnection={props.testOpenworkServerConnection} @@ -1365,8 +1358,8 @@ export default function DashboardView(props: DashboardViewProps) { openworkServerHostInfo={props.openworkServerHostInfo} openworkServerCapabilities={props.openworkServerCapabilities} openworkServerDiagnostics={props.openworkServerDiagnostics} - openworkServerWorkspaceId={props.openworkServerWorkspaceId} - activeWorkspaceRoot={props.activeWorkspaceRoot} + runtimeWorkspaceId={props.runtimeWorkspaceId} + selectedWorkspaceRoot={props.selectedWorkspaceRoot} activeWorkspaceType={props.activeWorkspaceType} openworkAuditEntries={props.openworkAuditEntries} openworkAuditStatus={props.openworkAuditStatus} diff --git a/apps/app/src/app/pages/extensions.tsx b/apps/app/src/app/pages/extensions.tsx index 66e36099..a50330d2 100644 --- a/apps/app/src/app/pages/extensions.tsx +++ b/apps/app/src/app/pages/extensions.tsx @@ -130,7 +130,7 @@ export default function ExtensionsView(props: ExtensionsViewProps) { Promise; restartLocalServer: () => Promise; - openworkServerWorkspaceId: string | null; - activeWorkspaceRoot: string; + runtimeWorkspaceId: string | null; + selectedWorkspaceRoot: string; developerMode: boolean; showHeader?: boolean; }; @@ -202,7 +202,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) { const [messagingRestartAction, setMessagingRestartAction] = createSignal<"enable" | "disable">("enable"); const workspaceId = createMemo(() => { - const explicitId = props.openworkServerWorkspaceId?.trim() ?? ""; + const explicitId = props.runtimeWorkspaceId?.trim() ?? ""; if (explicitId) return explicitId; return parseOpenworkWorkspaceIdFromUrl(props.openworkServerUrl) ?? ""; }); @@ -217,7 +217,7 @@ export default function IdentitiesView(props: IdentitiesViewProps) { const serverReady = createMemo(() => props.openworkServerStatus === "connected" && Boolean(openworkServerClient())); const scopedWorkspaceReady = createMemo(() => Boolean(workspaceId())); - const defaultRoutingDirectory = createMemo(() => props.activeWorkspaceRoot.trim() || "Not set"); + const defaultRoutingDirectory = createMemo(() => props.selectedWorkspaceRoot.trim() || "Not set"); let lastResetKey = ""; diff --git a/apps/app/src/app/pages/mcp.tsx b/apps/app/src/app/pages/mcp.tsx index 5488263b..9168c8fb 100644 --- a/apps/app/src/app/pages/mcp.tsx +++ b/apps/app/src/app/pages/mcp.tsx @@ -40,7 +40,7 @@ import { currentLocale, t, type Language } from "../../i18n"; export type McpViewProps = { busy: boolean; - activeWorkspaceRoot: string; + selectedWorkspaceRoot: string; isRemoteWorkspace: boolean; readConfigFile?: (scope: "project" | "global") => Promise; showHeader?: boolean; @@ -166,7 +166,7 @@ export default function McpView(props: McpViewProps) { let configRequestId = 0; createEffect(() => { - const root = props.activeWorkspaceRoot.trim(); + const root = props.selectedWorkspaceRoot.trim(); const nextId = (configRequestId += 1); const readConfig = props.readConfigFile; @@ -207,13 +207,13 @@ export default function McpView(props: McpViewProps) { const canRevealConfig = () => { if (!isTauriRuntime() || revealBusy()) return false; - if (configScope() === "project" && !props.activeWorkspaceRoot.trim()) return false; + if (configScope() === "project" && !props.selectedWorkspaceRoot.trim()) return false; return Boolean(activeConfig()?.exists); }; const revealConfig = async () => { if (!isTauriRuntime() || revealBusy()) return; - const root = props.activeWorkspaceRoot.trim(); + const root = props.selectedWorkspaceRoot.trim(); if (configScope() === "project" && !root) { setConfigError(tr("mcp.pick_workspace_error")); diff --git a/apps/app/src/app/pages/onboarding.tsx b/apps/app/src/app/pages/onboarding.tsx index 0d44d9c5..71a004e2 100644 --- a/apps/app/src/app/pages/onboarding.tsx +++ b/apps/app/src/app/pages/onboarding.tsx @@ -20,7 +20,7 @@ export type OnboardingViewProps = { openworkToken: string; newAuthorizedDir: string; authorizedDirs: string[]; - activeWorkspacePath: string; + selectedWorkspacePath: string; workspaces: WorkspaceInfo[]; localHostLabel: string; engineRunning: boolean; @@ -271,7 +271,7 @@ export default function OnboardingView(props: OnboardingViewProps) {