mirror of
https://github.com/different-ai/openwork
synced 2026-05-14 11:06:25 +02:00
fix(workspaces): make workspace switches less disruptive
Keep the dashboard stable when switching workspaces by preventing implicit session navigation during connect/reconnect flows. Delay the switch overlay to avoid flashes on fast switches and reuse an existing local host when bouncing remote<->local.
This commit is contained in:
@@ -1653,6 +1653,9 @@ export default function App() {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
// Only auto-select a session when the user is on the session route.
|
||||
// Switching workspaces while on the dashboard should not force navigation.
|
||||
if (currentView() !== "session") return;
|
||||
if (!client()) return;
|
||||
if (!sessionsLoaded()) return;
|
||||
if (creatingSession()) return;
|
||||
@@ -4005,13 +4008,28 @@ export default function App() {
|
||||
return activeWorkspaceDisplay();
|
||||
});
|
||||
|
||||
// Avoid flashing the full-screen switch overlay for fast workspace switches.
|
||||
// Only show it if a switch is still in progress after a short delay.
|
||||
const [workspaceSwitchDelayElapsed, setWorkspaceSwitchDelayElapsed] = createSignal(false);
|
||||
createEffect(() => {
|
||||
if (typeof window === "undefined") return;
|
||||
const switchingId = workspaceStore.connectingWorkspaceId();
|
||||
if (!switchingId) {
|
||||
setWorkspaceSwitchDelayElapsed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setWorkspaceSwitchDelayElapsed(false);
|
||||
const timer = window.setTimeout(() => setWorkspaceSwitchDelayElapsed(true), 250);
|
||||
onCleanup(() => window.clearTimeout(timer));
|
||||
});
|
||||
|
||||
const workspaceSwitchOpen = createMemo(() => {
|
||||
if (booting()) return true;
|
||||
if (workspaceStore.connectingWorkspaceId()) return true;
|
||||
if (workspaceStore.connectingWorkspaceId()) return workspaceSwitchDelayElapsed();
|
||||
if (!busy() || !busyLabel()) return false;
|
||||
const label = busyLabel();
|
||||
return (
|
||||
label === "status.connecting" ||
|
||||
label === "status.starting_engine" ||
|
||||
label === "status.restarting_engine"
|
||||
);
|
||||
|
||||
@@ -473,7 +473,7 @@ export function createWorkspaceStore(options: {
|
||||
info.projectDir ?? undefined,
|
||||
{ reason: "engine-refresh" },
|
||||
auth ?? undefined,
|
||||
{ quiet: true },
|
||||
{ quiet: true, navigate: false },
|
||||
)
|
||||
.catch(() => undefined)
|
||||
.finally(() => {
|
||||
@@ -592,6 +592,7 @@ export function createWorkspaceStore(options: {
|
||||
reason: "workspace-switch-openwork",
|
||||
},
|
||||
resolvedAuth,
|
||||
{ navigate: false },
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
@@ -648,12 +649,18 @@ export function createWorkspaceStore(options: {
|
||||
return false;
|
||||
}
|
||||
|
||||
const ok = await connectToServer(baseUrl, next.directory?.trim() || undefined, {
|
||||
workspaceId: next.id,
|
||||
workspaceType: next.workspaceType,
|
||||
targetRoot: next.directory?.trim() ?? "",
|
||||
reason: "workspace-switch-direct",
|
||||
});
|
||||
const ok = await connectToServer(
|
||||
baseUrl,
|
||||
next.directory?.trim() || undefined,
|
||||
{
|
||||
workspaceId: next.id,
|
||||
workspaceType: next.workspaceType,
|
||||
targetRoot: next.directory?.trim() ?? "",
|
||||
reason: "workspace-switch-direct",
|
||||
},
|
||||
undefined,
|
||||
{ navigate: false },
|
||||
);
|
||||
|
||||
if (!ok) {
|
||||
updateWorkspaceConnectionState(id, {
|
||||
@@ -742,13 +749,54 @@ export function createWorkspaceStore(options: {
|
||||
options.setPendingPermissions([]);
|
||||
options.setSessionStatusById({});
|
||||
|
||||
const ok = await startHost({ workspacePath: next.path });
|
||||
if (!ok) {
|
||||
updateWorkspaceConnectionState(id, {
|
||||
status: "error",
|
||||
message: "Failed to start local engine.",
|
||||
});
|
||||
return false;
|
||||
// If a local host engine is already running (common when bouncing between remote/local),
|
||||
// reuse it instead of restarting to keep switching snappy.
|
||||
let connectedToLocalHost = false;
|
||||
const existingEngine = engine();
|
||||
const runtime = existingEngine?.runtime ?? resolveEngineRuntime();
|
||||
const canReuseHost =
|
||||
isTauriRuntime() &&
|
||||
Boolean(existingEngine?.running && existingEngine.baseUrl);
|
||||
|
||||
if (canReuseHost && runtime === "openwrk") {
|
||||
try {
|
||||
await openwrkWorkspaceActivate({
|
||||
workspacePath: next.path,
|
||||
name: next.displayName?.trim() || next.name?.trim() || null,
|
||||
});
|
||||
await activateOpenworkHostWorkspace(next.path);
|
||||
|
||||
const nextInfo = await engineInfo();
|
||||
setEngine(nextInfo);
|
||||
|
||||
const username = nextInfo.opencodeUsername?.trim() ?? "";
|
||||
const password = nextInfo.opencodePassword?.trim() ?? "";
|
||||
const auth = username && password ? { username, password } : undefined;
|
||||
setEngineAuth(auth ?? null);
|
||||
|
||||
if (nextInfo.baseUrl) {
|
||||
connectedToLocalHost = await connectToServer(
|
||||
nextInfo.baseUrl,
|
||||
nextInfo.projectDir ?? undefined,
|
||||
{ reason: "workspace-attach-local" },
|
||||
auth,
|
||||
{ navigate: false },
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
connectedToLocalHost = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!connectedToLocalHost) {
|
||||
const ok = await startHost({ workspacePath: next.path, navigate: false });
|
||||
if (!ok) {
|
||||
updateWorkspaceConnectionState(id, {
|
||||
status: "error",
|
||||
message: "Failed to start local engine.",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -776,17 +824,18 @@ export function createWorkspaceStore(options: {
|
||||
const auth = username && password ? { username, password } : undefined;
|
||||
setEngineAuth(auth ?? null);
|
||||
|
||||
if (newInfo.baseUrl) {
|
||||
const ok = await connectToServer(
|
||||
newInfo.baseUrl,
|
||||
newInfo.projectDir ?? undefined,
|
||||
{ reason: "workspace-openwrk-switch" },
|
||||
auth,
|
||||
);
|
||||
if (!ok) {
|
||||
options.setError("Failed to reconnect after workspace switch");
|
||||
if (newInfo.baseUrl) {
|
||||
const ok = await connectToServer(
|
||||
newInfo.baseUrl,
|
||||
newInfo.projectDir ?? undefined,
|
||||
{ reason: "workspace-openwrk-switch" },
|
||||
auth,
|
||||
{ navigate: false },
|
||||
);
|
||||
if (!ok) {
|
||||
options.setError("Failed to reconnect after workspace switch");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Stop the current engine
|
||||
const info = await engineStop();
|
||||
@@ -806,17 +855,18 @@ export function createWorkspaceStore(options: {
|
||||
setEngineAuth(auth ?? null);
|
||||
|
||||
// Reconnect to server
|
||||
if (newInfo.baseUrl) {
|
||||
const ok = await connectToServer(
|
||||
newInfo.baseUrl,
|
||||
newInfo.projectDir ?? undefined,
|
||||
{ reason: "workspace-restart" },
|
||||
auth,
|
||||
);
|
||||
if (!ok) {
|
||||
options.setError("Failed to reconnect after workspace switch");
|
||||
if (newInfo.baseUrl) {
|
||||
const ok = await connectToServer(
|
||||
newInfo.baseUrl,
|
||||
newInfo.projectDir ?? undefined,
|
||||
{ reason: "workspace-restart" },
|
||||
auth,
|
||||
{ navigate: false },
|
||||
);
|
||||
if (!ok) {
|
||||
options.setError("Failed to reconnect after workspace switch");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
const message = e instanceof Error ? e.message : safeStringify(e);
|
||||
@@ -847,7 +897,7 @@ export function createWorkspaceStore(options: {
|
||||
reason?: string;
|
||||
},
|
||||
auth?: OpencodeAuth,
|
||||
connectOptions?: { quiet?: boolean },
|
||||
connectOptions?: { quiet?: boolean; navigate?: boolean },
|
||||
) {
|
||||
console.log("[workspace] connect", {
|
||||
baseUrl: nextBaseUrl,
|
||||
@@ -855,6 +905,7 @@ export function createWorkspaceStore(options: {
|
||||
workspaceType: context?.workspaceType ?? null,
|
||||
});
|
||||
const quiet = connectOptions?.quiet ?? false;
|
||||
const navigate = connectOptions?.navigate ?? true;
|
||||
options.setError(null);
|
||||
if (!quiet) {
|
||||
options.setBusy(true);
|
||||
@@ -936,7 +987,7 @@ export function createWorkspaceStore(options: {
|
||||
|
||||
options.refreshSkills({ force: true }).catch(() => undefined);
|
||||
options.refreshPlugins().catch(() => undefined);
|
||||
if (!options.selectedSessionId()) {
|
||||
if (navigate && !options.selectedSessionId()) {
|
||||
options.setTab("scheduled");
|
||||
options.setView("session");
|
||||
}
|
||||
@@ -1484,7 +1535,7 @@ export function createWorkspaceStore(options: {
|
||||
}
|
||||
}
|
||||
|
||||
async function startHost(optionsOverride?: { workspacePath?: string }) {
|
||||
async function startHost(optionsOverride?: { workspacePath?: string; navigate?: boolean }) {
|
||||
if (!isTauriRuntime()) {
|
||||
options.setError(t("app.error.tauri_required", currentLocale()));
|
||||
return false;
|
||||
@@ -1552,15 +1603,16 @@ export function createWorkspaceStore(options: {
|
||||
const auth = username && password ? { username, password } : undefined;
|
||||
setEngineAuth(auth ?? null);
|
||||
|
||||
if (info.baseUrl) {
|
||||
const ok = await connectToServer(
|
||||
info.baseUrl,
|
||||
info.projectDir ?? undefined,
|
||||
{ reason: "host-start" },
|
||||
auth,
|
||||
);
|
||||
if (!ok) return false;
|
||||
}
|
||||
if (info.baseUrl) {
|
||||
const ok = await connectToServer(
|
||||
info.baseUrl,
|
||||
info.projectDir ?? undefined,
|
||||
{ reason: "host-start" },
|
||||
auth,
|
||||
{ navigate: optionsOverride?.navigate ?? true },
|
||||
);
|
||||
if (!ok) return false;
|
||||
}
|
||||
|
||||
markOnboardingComplete();
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user