fix(react-app): restore desktop bootstrap and reconnect flow (#1520)

* fix(react-app): restore workspace-scoped provider bootstrap

Load providers and default model state from the selected workspace on first render so the React desktop flow matches the pre-cutover app without requiring a modal refresh.

* fix(react-app): restore desktop workspace reconnect flow

Keep desktop workspaces visible when bootstrap refreshes fail, log reconnect/bootstrap failures in the React inspector, and reattach local workspaces to the OpenWork host automatically so sessions and settings recover after workspace changes.

---------

Co-authored-by: src-opn <src-opn@users.noreply.github.com>
This commit is contained in:
Source Open
2026-04-22 14:44:35 -07:00
committed by GitHub
parent b52dad4189
commit 9e795771ea
5 changed files with 469 additions and 83 deletions

View File

@@ -338,11 +338,12 @@ export function SessionSurface(props: SessionSurfaceProps) {
setSending(true);
try {
const nextDraft = buildDraft(text, attachments);
props.onSendDraft(nextDraft);
await props.onSendDraft(nextDraft);
setDraft("");
attachments.forEach(revokeAttachmentPreview);
setAttachments([]);
props.onDraftChange(buildDraft("", []));
setSending(false);
} catch (nextError) {
setError(nextError instanceof Error ? nextError.message : "Failed to send prompt.");
setSending(false);

View File

@@ -13,6 +13,7 @@ import {
import { THINKING_PREF_KEY } from "../../app/constants";
import { coerceReleaseChannel } from "../../app/lib/release-channels";
import type { ModelRef, ReleaseChannel, SettingsTab, View } from "../../app/types";
import { readStoredDefaultModel } from "./model-config";
export type LocalUIState = {
view: View;
@@ -88,9 +89,16 @@ export function LocalProvider({ children }: LocalProviderProps) {
const [ui, setUiRaw] = useState<LocalUIState>(() =>
readPersisted(UI_STORAGE_KEY, INITIAL_UI),
);
const [prefs, setPrefsRaw] = useState<LocalPreferences>(() =>
readPersisted(PREFS_STORAGE_KEY, INITIAL_PREFS),
);
const [prefs, setPrefsRaw] = useState<LocalPreferences>(() => {
const persisted = readPersisted(PREFS_STORAGE_KEY, INITIAL_PREFS);
if (persisted.defaultModel) {
return persisted;
}
return {
...persisted,
defaultModel: readStoredDefaultModel(),
};
});
const [ready, setReady] = useState(false);
const migratedThinkingRef = useRef(false);

View File

@@ -0,0 +1,112 @@
import {
engineInfo,
engineStart,
openworkServerInfo,
orchestratorWorkspaceActivate,
} from "../../app/lib/tauri";
import { writeOpenworkServerSettings } from "../../app/lib/openwork-server";
import { safeStringify } from "../../app/utils";
import { recordInspectorEvent } from "./app-inspector";
type LocalWorkspaceLike = {
id: string;
name?: string | null;
displayNameResolved?: string | null;
path?: string | null;
workspaceType?: "local" | "remote" | string | null;
};
type EnsureDesktopLocalOpenworkOptions = {
route: "session" | "settings";
workspace: LocalWorkspaceLike | null | undefined;
allWorkspaces: LocalWorkspaceLike[];
};
function emitOpenworkSettingsChanged() {
try {
window.dispatchEvent(new CustomEvent("openwork-server-settings-changed"));
} catch {
// ignore browser event dispatch failures
}
}
function describeError(error: unknown) {
if (error instanceof Error) return error.message;
const serialized = safeStringify(error);
return serialized && serialized !== "{}" ? serialized : "Unknown error";
}
export async function ensureDesktopLocalOpenworkConnection(
options: EnsureDesktopLocalOpenworkOptions,
) {
const workspace = options.workspace;
const workspaceRoot = workspace?.path?.trim() ?? "";
if (!workspace || workspace.workspaceType !== "local" || !workspaceRoot) {
return null;
}
const workspacePaths = Array.from(
new Set(
options.allWorkspaces
.filter((item) => item.workspaceType === "local")
.map((item) => item.path?.trim() ?? "")
.filter((path) => path.length > 0),
),
);
if (!workspacePaths.includes(workspaceRoot)) {
workspacePaths.unshift(workspaceRoot);
}
recordInspectorEvent("route.local_openwork.ensure.start", {
route: options.route,
workspaceId: workspace.id,
workspaceRoot,
});
try {
const engine = await engineInfo().catch(() => null);
if (!engine?.running || !engine.baseUrl) {
await engineStart(workspaceRoot, {
runtime: "openwork-orchestrator",
workspacePaths,
});
}
await orchestratorWorkspaceActivate({
workspacePath: workspaceRoot,
name: workspace.name ?? workspace.displayNameResolved ?? null,
});
const info = await openworkServerInfo();
if (!info?.baseUrl) {
throw new Error("OpenWork server did not report a base URL after activation.");
}
writeOpenworkServerSettings({
urlOverride: info.baseUrl,
token: info.ownerToken?.trim() || info.clientToken?.trim() || undefined,
portOverride: info.port ?? undefined,
remoteAccessEnabled: info.remoteAccessEnabled === true,
});
emitOpenworkSettingsChanged();
recordInspectorEvent("route.local_openwork.ensure.success", {
route: options.route,
workspaceId: workspace.id,
workspaceRoot,
baseUrl: info.baseUrl,
});
return info;
} catch (error) {
const message = describeError(error);
console.error(`[${options.route}-route] local workspace reconnect failed`, error);
recordInspectorEvent("route.local_openwork.ensure.error", {
route: options.route,
workspaceId: workspace.id,
workspaceRoot,
message,
});
throw new Error(message);
}
}

View File

@@ -1,9 +1,15 @@
/** @jsxImportSource react */
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import type { AgentPartInput, FilePartInput, TextPartInput } from "@opencode-ai/sdk/v2/client";
import type {
AgentPartInput,
ConfigProvidersResponse,
FilePartInput,
ProviderListResponse,
TextPartInput,
} from "@opencode-ai/sdk/v2/client";
import { unwrap } from "../../app/lib/opencode";
import { createClient, unwrap } from "../../app/lib/opencode";
import { listCommands, shellInSession } from "../../app/lib/opencode-session";
import {
buildOpenworkWorkspaceBaseUrl,
@@ -43,11 +49,11 @@ import type {
TodoItem,
WorkspacePreset,
WorkspaceConnectionState,
ProviderListItem,
WorkspaceSessionGroup,
} from "../../app/types";
import { createClient } from "../../app/lib/opencode";
import { buildFeedbackUrl } from "../../app/lib/feedback";
import { isSandboxWorkspace, isTauriRuntime, normalizeDirectoryPath } from "../../app/utils";
import { isSandboxWorkspace, isTauriRuntime, normalizeDirectoryPath, safeStringify } from "../../app/utils";
import { t } from "../../i18n";
import { useLocal } from "../kernel/local-provider";
import { usePlatform } from "../kernel/platform";
@@ -75,6 +81,8 @@ import {
recordInspectorEvent,
} from "./app-inspector";
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
type RouteWorkspace = OpenworkWorkspaceInfo & {
displayNameResolved: string;
@@ -133,6 +141,61 @@ function workspaceLabel(workspace: OpenworkWorkspaceInfo) {
);
}
function describeRouteError(error: unknown) {
if (error instanceof Error) {
return error.message;
}
const serialized = safeStringify(error);
return serialized && serialized !== "{}" ? serialized : t("app.unknown_error");
}
function mergeRouteWorkspaces(
serverWorkspaces: OpenworkWorkspaceInfo[],
desktopWorkspaces: RouteWorkspace[],
): RouteWorkspace[] {
const desktopById = new Map(desktopWorkspaces.map((workspace) => [workspace.id, workspace]));
const desktopByPath = new Map(
desktopWorkspaces
.map((workspace) => [normalizeDirectoryPath(workspace.path ?? ""), workspace] as const)
.filter(([path]) => path.length > 0),
);
const mergedServer = serverWorkspaces.map((workspace) => {
const match =
desktopById.get(workspace.id) ??
desktopByPath.get(normalizeDirectoryPath(workspace.path ?? ""));
const merged = match
? {
...workspace,
displayName: match.displayName?.trim()
? match.displayName
: workspace.displayName,
name: match.name?.trim() ? match.name : workspace.name,
}
: workspace;
return {
...merged,
displayNameResolved: workspaceLabel(merged),
};
});
const mergedIds = new Set(mergedServer.map((workspace) => workspace.id));
const mergedPaths = new Set(
mergedServer
.map((workspace) => normalizeDirectoryPath(workspace.path ?? ""))
.filter((path) => path.length > 0),
);
const missingDesktop = desktopWorkspaces.filter((workspace) => {
if (mergedIds.has(workspace.id)) return false;
const normalizedPath = normalizeDirectoryPath(workspace.path ?? "");
if (normalizedPath && mergedPaths.has(normalizedPath)) return false;
return true;
});
return [...mergedServer, ...missingDesktop];
}
function toSessionGroups(
workspaces: RouteWorkspace[],
sessionsByWorkspaceId: Record<string, any[]>,
@@ -228,11 +291,13 @@ export function SessionRoute() {
const [workspaces, setWorkspaces] = useState<RouteWorkspace[]>([]);
const [sessionsByWorkspaceId, setSessionsByWorkspaceId] = useState<Record<string, any[]>>({});
const [errorsByWorkspaceId, setErrorsByWorkspaceId] = useState<Record<string, string | null>>({});
const [routeError, setRouteError] = useState<string | null>(null);
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(() => readActiveWorkspaceId() ?? "");
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
// One-way latch for "a refreshRouteState is currently running"; prevents
// overlapping route refreshes from queueing up when the user clicks fast.
const refreshInFlightRef = useRef(false);
const workspacesRef = useRef<RouteWorkspace[]>([]);
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
const [createWorkspaceBusy, setCreateWorkspaceBusy] = useState(false);
const [createWorkspaceRemoteBusy, setCreateWorkspaceRemoteBusy] = useState(false);
@@ -247,6 +312,8 @@ export function SessionRoute() {
const [modelPickerOpen, setModelPickerOpen] = useState(false);
const [modelPickerQuery, setModelPickerQuery] = useState("");
const [modelOptions, setModelOptions] = useState<ModelOption[]>([]);
const [providers, setProviders] = useState<ProviderListItem[]>([]);
const [providerConnectedIds, setProviderConnectedIds] = useState<string[]>([]);
// Provider catalog cache. Used to compute the reasoning/thinking variant
// options for whichever model is currently selected so the composer's
// behavior pill actually shows its options (bug: was empty before).
@@ -256,6 +323,7 @@ export function SessionRoute() {
const [routeEngineInfo, setRouteEngineInfo] = useState<EngineInfo | null>(null);
const [shareRemoteAccessBusy, setShareRemoteAccessBusy] = useState(false);
const [shareRemoteAccessError, setShareRemoteAccessError] = useState<string | null>(null);
const reconnectAttemptedWorkspaceIdRef = useRef("");
const openworkServerSettings = useMemo(
() => readOpenworkServerSettings(),
@@ -280,9 +348,25 @@ export function SessionRoute() {
if (refreshInFlightRef.current) return;
refreshInFlightRef.current = true;
setLoading(true);
setRouteError(null);
let desktopList = null as Awaited<ReturnType<typeof workspaceBootstrap>> | null;
let desktopWorkspaces = workspacesRef.current;
try {
const desktopList = isTauriRuntime() ? await workspaceBootstrap().catch(() => null) : null;
const desktopWorkspaces = (desktopList?.workspaces ?? []).map(mapDesktopWorkspace);
if (isTauriRuntime()) {
try {
desktopList = await workspaceBootstrap();
desktopWorkspaces = (desktopList.workspaces ?? []).map(mapDesktopWorkspace);
} catch (error) {
const message = describeRouteError(error);
console.error("[session-route] workspaceBootstrap failed", error);
recordInspectorEvent("route.workspace_bootstrap.error", {
route: "session",
message,
preservedWorkspaceCount: workspacesRef.current.length,
});
desktopWorkspaces = workspacesRef.current;
}
}
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveRouteOpenworkConnection();
setOpenworkServerHostInfoState(hostInfo);
@@ -302,50 +386,7 @@ export function SessionRoute() {
token: resolvedToken,
});
const list = await openworkClient.listWorkspaces();
const desktopById = new Map(desktopWorkspaces.map((workspace) => [workspace.id, workspace]));
const desktopByPath = new Map(
desktopWorkspaces
.map((workspace) => [normalizeDirectoryPath(workspace.path ?? ""), workspace] as const)
.filter(([path]) => path.length > 0),
);
const baseRouteWorkspaces = list.items.map((workspace) => {
const match =
desktopById.get(workspace.id) ??
desktopByPath.get(normalizeDirectoryPath(workspace.path ?? ""));
const merged = match
? {
...workspace,
displayName: match.displayName?.trim()
? match.displayName
: workspace.displayName,
name: match.name?.trim() ? match.name : workspace.name,
}
: workspace;
return {
...merged,
displayNameResolved: workspaceLabel(merged),
};
});
// If the user removed a workspace locally, the server may still know
// about it until its --workspace list gets reconciled. Hide rows that
// the desktop has forgotten so rename/delete feel instant.
const routeWorkspaces = desktopWorkspaces.length === 0
? baseRouteWorkspaces
: baseRouteWorkspaces.filter((workspace) => {
if (desktopById.has(workspace.id)) return true;
const normalized = normalizeDirectoryPath(workspace.path ?? "");
return normalized.length > 0 && desktopByPath.has(normalized);
});
const desktopRemotes = desktopWorkspaces.filter(
(workspace) => workspace.workspaceType === "remote",
);
const nextWorkspaces =
routeWorkspaces.length > 0
? [...routeWorkspaces, ...desktopRemotes]
: desktopWorkspaces;
const nextWorkspaces = mergeRouteWorkspaces(list.items, desktopWorkspaces);
const sessionEntries = await Promise.all(
nextWorkspaces.map(async (workspace) => {
@@ -405,6 +446,21 @@ export function SessionRoute() {
selectedWorkspaceId: nextWorkspaceId,
errors: Object.fromEntries(sessionEntries.filter((e) => e.error).map((e) => [e.workspaceId, e.error])),
});
} catch (error) {
const message = describeRouteError(error);
console.error("[session-route] refreshRouteState failed", error);
recordInspectorEvent("route.refresh.error", {
route: "session",
message,
preservedWorkspaceCount: desktopWorkspaces.length,
});
setRouteError(message);
if (desktopWorkspaces.length > 0) {
setWorkspaces(desktopWorkspaces);
setSelectedWorkspaceId((current) =>
current || resolveWorkspaceListSelectedId(desktopList) || desktopWorkspaces[0]?.id || "",
);
}
} finally {
setLoading(false);
refreshInFlightRef.current = false;
@@ -415,6 +471,10 @@ export function SessionRoute() {
}
}, [markBootRouteReady, selectedSessionId]);
useEffect(() => {
workspacesRef.current = workspaces;
}, [workspaces]);
useEffect(() => {
let cancelled = false;
@@ -483,6 +543,7 @@ export function SessionRoute() {
baseUrl,
tokenPresent: token.length > 0,
connected: Boolean(client),
routeError,
selectedSessionId,
selectedWorkspaceId,
persistedActiveWorkspaceId: readActiveWorkspaceId(),
@@ -513,6 +574,7 @@ export function SessionRoute() {
loading,
selectedSessionId,
selectedWorkspaceId,
routeError,
sessionsByWorkspaceId,
token,
workspaces,
@@ -549,6 +611,28 @@ export function SessionRoute() {
[selectedWorkspaceId, workspaces],
);
useEffect(() => {
if (!isTauriRuntime()) return;
if (loading) return;
if (client) {
reconnectAttemptedWorkspaceIdRef.current = "";
return;
}
if (!selectedWorkspace || selectedWorkspace.workspaceType !== "local") return;
const workspaceId = selectedWorkspace.id?.trim() ?? "";
if (!workspaceId || reconnectAttemptedWorkspaceIdRef.current === workspaceId) return;
reconnectAttemptedWorkspaceIdRef.current = workspaceId;
void ensureDesktopLocalOpenworkConnection({
route: "session",
workspace: selectedWorkspace,
allWorkspaces: workspaces,
}).catch((error) => {
const message = error instanceof Error ? error.message : describeRouteError(error);
setRouteError(message);
});
}, [client, loading, selectedWorkspace, workspaces]);
const selectedWorkspaceRoot = selectedWorkspace?.path?.trim() || "";
const opencodeBaseUrl = useMemo(() => {
if (!selectedWorkspaceId || !baseUrl) return "";
@@ -559,11 +643,83 @@ export function SessionRoute() {
const opencodeClient = useMemo(
() =>
opencodeBaseUrl && token
? createClient(opencodeBaseUrl, undefined, { token, mode: "openwork" })
? createClient(opencodeBaseUrl, selectedWorkspaceRoot || undefined, {
token,
mode: "openwork",
})
: null,
[opencodeBaseUrl, token],
[opencodeBaseUrl, selectedWorkspaceRoot, token],
);
useEffect(() => {
if (!opencodeClient) {
setProviders([]);
setProviderConnectedIds([]);
return;
}
let cancelled = false;
const applyProviderState = (value: ProviderListResponse) => {
if (cancelled) return;
setProviders((value.all ?? []) as ProviderListItem[]);
setProviderConnectedIds(value.connected ?? []);
};
void (async () => {
let disabledProviders: string[] = [];
try {
const config = unwrap(
await opencodeClient.config.get({
directory: selectedWorkspaceRoot || undefined,
}),
) as { disabled_providers?: string[] };
disabledProviders = Array.isArray(config.disabled_providers)
? config.disabled_providers
: [];
} catch {
// ignore config read failures and continue with provider discovery
}
try {
applyProviderState(
filterProviderList(
unwrap(await opencodeClient.provider.list()),
disabledProviders,
),
);
} catch {
try {
const fallback = unwrap(
await opencodeClient.config.providers({
directory: selectedWorkspaceRoot || undefined,
}),
) as ConfigProvidersResponse;
applyProviderState(
filterProviderList(
{
all: mapConfigProvidersToList(
fallback.providers,
) as ProviderListResponse["all"],
connected: [],
default: fallback.default,
},
disabledProviders,
),
);
} catch {
if (cancelled) return;
setProviders([]);
setProviderConnectedIds([]);
}
}
})();
return () => {
cancelled = true;
};
}, [opencodeClient, selectedWorkspaceRoot]);
const modelLabel = local.prefs.defaultModel
? `${local.prefs.defaultModel.providerID}/${local.prefs.defaultModel.modelID}`
: t("session.default_model");
@@ -752,6 +908,7 @@ export function SessionRoute() {
const result = await opencodeClient.session.promptAsync({
sessionID: selectedSessionId,
parts,
model: local.prefs.defaultModel ?? undefined,
agent: selectedAgent ?? undefined,
...(local.prefs.modelVariant ? { variant: local.prefs.modelVariant } : {}),
});
@@ -968,7 +1125,11 @@ export function SessionRoute() {
const workspace = workspaces.find((item) => item.id === workspaceId);
if (!workspace || !token || !baseUrl) return;
const workspaceOpencodeBaseUrl = `${(buildOpenworkWorkspaceBaseUrl(baseUrl, workspace.id) ?? baseUrl).replace(/\/+$|\/+$/g, "")}/opencode`;
const workspaceClient = createClient(workspaceOpencodeBaseUrl, undefined, { token, mode: "openwork" });
const workspaceClient = createClient(
workspaceOpencodeBaseUrl,
workspace.path?.trim() || undefined,
{ token, mode: "openwork" },
);
const session = unwrap(
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
);
@@ -1137,8 +1298,8 @@ export function SessionRoute() {
headerStatus={client ? t("status.connected") : t("status.disconnected_label")}
busyHint={loading ? t("session.loading_detail") : null}
startupPhase={loading ? "nativeInit" : "ready"}
providerConnectedIds={[]}
providers={[]}
providerConnectedIds={providerConnectedIds}
providers={providers}
mcpConnectedCount={0}
onSendFeedback={() => {
platform.openLink(
@@ -1193,7 +1354,11 @@ export function SessionRoute() {
const workspace = workspaces.find((item) => item.id === workspaceId);
if (!workspace || !token || !baseUrl) return;
const workspaceOpencodeBaseUrl = `${(buildOpenworkWorkspaceBaseUrl(baseUrl, workspace.id) ?? baseUrl).replace(/\/+$|\/+$/g, "")}/opencode`;
const workspaceClient = createClient(workspaceOpencodeBaseUrl, undefined, { token, mode: "openwork" });
const workspaceClient = createClient(
workspaceOpencodeBaseUrl,
workspace.path?.trim() || undefined,
{ token, mode: "openwork" },
);
const session = unwrap(
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
);

View File

@@ -59,10 +59,12 @@ import {
import { isDesktopProviderBlocked } from "../../app/cloud/desktop-app-restrictions";
import { useCheckDesktopRestriction } from "../domains/cloud/desktop-config-provider";
import { useCloudProviderAutoSync } from "../domains/cloud/use-cloud-provider-auto-sync";
import { isMacPlatform, isTauriRuntime, normalizeDirectoryPath } from "../../app/utils";
import { isMacPlatform, isTauriRuntime, normalizeDirectoryPath, safeStringify } from "../../app/utils";
import { CreateWorkspaceModal } from "../domains/workspace/create-workspace-modal";
import { ModelPickerModal } from "../domains/session/modals/model-picker-modal";
import type { ModelOption, ModelRef } from "../../app/types";
import { recordInspectorEvent } from "./app-inspector";
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
type RouteWorkspace = OpenworkWorkspaceInfo & {
displayNameResolved: string;
@@ -79,6 +81,61 @@ function mapDesktopWorkspace(workspace: WorkspaceInfo): RouteWorkspace {
};
}
function describeRouteError(error: unknown) {
if (error instanceof Error) {
return error.message;
}
const serialized = safeStringify(error);
return serialized && serialized !== "{}" ? serialized : t("app.unknown_error");
}
function mergeRouteWorkspaces(
serverWorkspaces: OpenworkWorkspaceInfo[],
desktopWorkspaces: RouteWorkspace[],
): RouteWorkspace[] {
const desktopById = new Map(desktopWorkspaces.map((workspace) => [workspace.id, workspace]));
const desktopByPath = new Map(
desktopWorkspaces
.map((workspace) => [normalizeDirectoryPath(workspace.path ?? ""), workspace] as const)
.filter(([path]) => path.length > 0),
);
const mergedServer = serverWorkspaces.map((workspace) => {
const match =
desktopById.get(workspace.id) ??
desktopByPath.get(normalizeDirectoryPath(workspace.path ?? ""));
const merged = match
? {
...workspace,
displayName: match.displayName?.trim()
? match.displayName
: workspace.displayName,
name: match.name?.trim() ? match.name : workspace.name,
}
: workspace;
return {
...merged,
displayNameResolved: workspaceLabel(merged),
};
});
const mergedIds = new Set(mergedServer.map((workspace) => workspace.id));
const mergedPaths = new Set(
mergedServer
.map((workspace) => normalizeDirectoryPath(workspace.path ?? ""))
.filter((path) => path.length > 0),
);
const missingDesktop = desktopWorkspaces.filter((workspace) => {
if (mergedIds.has(workspace.id)) return false;
const normalizedPath = normalizeDirectoryPath(workspace.path ?? "");
if (normalizedPath && mergedPaths.has(normalizedPath)) return false;
return true;
});
return [...mergedServer, ...missingDesktop];
}
function folderNameFromPath(path: string) {
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
const parts = normalized.split("/").filter(Boolean);
@@ -233,6 +290,8 @@ export function SettingsRoute() {
const [busy, setBusy] = useState(false);
const [busyLabel, setBusyLabel] = useState<string | null>(null);
const [routeError, setRouteError] = useState<string | null>(null);
const workspacesRef = useRef<RouteWorkspace[]>([]);
const reconnectAttemptedWorkspaceIdRef = useRef("");
const [providers, setProviders] = useState<ProviderListItem[]>([]);
const [providerDefaults, setProviderDefaults] = useState<Record<string, string>>({});
const [providerConnectedIds, setProviderConnectedIds] = useState<string[]>([]);
@@ -438,9 +497,12 @@ export function SettingsRoute() {
const opencodeClient = useMemo(
() =>
opencodeBaseUrl && token
? createClient(opencodeBaseUrl, undefined, { token, mode: "openwork" })
? createClient(opencodeBaseUrl, selectedWorkspaceRoot || undefined, {
token,
mode: "openwork",
})
: null,
[opencodeBaseUrl, token],
[opencodeBaseUrl, selectedWorkspaceRoot, token],
);
useEffect(() => {
@@ -520,9 +582,24 @@ export function SettingsRoute() {
const refreshRouteState = useMemo(() => async () => {
setLoading(true);
setRouteError(null);
let desktopList = null as Awaited<ReturnType<typeof workspaceBootstrap>> | null;
let desktopWorkspaces = workspacesRef.current;
try {
const desktopList = isTauriRuntime() ? await workspaceBootstrap().catch(() => null) : null;
const desktopWorkspaces = (desktopList?.workspaces ?? []).map(mapDesktopWorkspace);
if (isTauriRuntime()) {
try {
desktopList = await workspaceBootstrap();
desktopWorkspaces = (desktopList.workspaces ?? []).map(mapDesktopWorkspace);
} catch (error) {
const message = describeRouteError(error);
console.error("[settings-route] workspaceBootstrap failed", error);
recordInspectorEvent("route.workspace_bootstrap.error", {
route: "settings",
message,
preservedWorkspaceCount: workspacesRef.current.length,
});
desktopWorkspaces = workspacesRef.current;
}
}
const { normalizedBaseUrl, resolvedToken } = await resolveRouteOpenworkConnection();
if (!normalizedBaseUrl || !resolvedToken) {
@@ -538,21 +615,7 @@ export function SettingsRoute() {
const client = createOpenworkServerClient({ baseUrl: normalizedBaseUrl, token: resolvedToken });
const list = await client.listWorkspaces();
const serverWorkspaces = list.items.map((workspace) => ({
...workspace,
displayNameResolved: workspaceLabel(workspace),
}));
// Mirror Solid's `applyServerLocalWorkspaces`: prefer server-registered
// local workspaces (their IDs are what subsequent API calls need) and
// append any remote entries from the desktop list since the server
// doesn't track remotes.
const desktopRemotes = desktopWorkspaces.filter(
(workspace) => workspace.workspaceType === "remote",
);
const nextWorkspaces =
serverWorkspaces.length > 0
? [...serverWorkspaces, ...desktopRemotes]
: desktopWorkspaces;
const nextWorkspaces = mergeRouteWorkspaces(list.items, desktopWorkspaces);
const sessionEntries = await Promise.all(
nextWorkspaces.map(async (workspace) => {
try {
@@ -582,7 +645,18 @@ export function SettingsRoute() {
setErrorsByWorkspaceId(Object.fromEntries(sessionEntries.map((entry) => [entry.workspaceId, entry.error])));
setSelectedWorkspaceId((current) => current || resolveWorkspaceListSelectedId(desktopList) || list.activeId?.trim() || nextWorkspaces[0]?.id || "");
} catch (error) {
setRouteError(error instanceof Error ? error.message : t("app.unknown_error"));
const message = describeRouteError(error);
console.error("[settings-route] refreshRouteState failed", error);
recordInspectorEvent("route.refresh.error", {
route: "settings",
message,
preservedWorkspaceCount: desktopWorkspaces.length,
});
setRouteError(message);
if (desktopWorkspaces.length > 0) {
setWorkspaces(desktopWorkspaces);
setSelectedWorkspaceId((current) => current || resolveWorkspaceListSelectedId(desktopList) || desktopWorkspaces[0]?.id || "");
}
} finally {
setLoading(false);
// Settings can be the first route a user lands on (direct link, deep
@@ -592,6 +666,32 @@ export function SettingsRoute() {
}
}, [markBootRouteReady]);
useEffect(() => {
workspacesRef.current = workspaces;
}, [workspaces]);
useEffect(() => {
if (!isTauriRuntime()) return;
if (loading) return;
if (openworkClient) {
reconnectAttemptedWorkspaceIdRef.current = "";
return;
}
if (!selectedWorkspace || selectedWorkspace.workspaceType !== "local") return;
const workspaceId = selectedWorkspace.id?.trim() ?? "";
if (!workspaceId || reconnectAttemptedWorkspaceIdRef.current === workspaceId) return;
reconnectAttemptedWorkspaceIdRef.current = workspaceId;
void ensureDesktopLocalOpenworkConnection({
route: "settings",
workspace: selectedWorkspace,
allWorkspaces: workspaces,
}).catch((error) => {
const message = error instanceof Error ? error.message : describeRouteError(error);
setRouteError(message);
});
}, [loading, openworkClient, selectedWorkspace, workspaces]);
useEffect(() => {
void refreshRouteState();
const handleSettingsChange = () => {
@@ -644,7 +744,7 @@ export function SettingsRoute() {
]);
useEffect(() => {
if (!opencodeClient) {
if (!activeClient) {
setProviders([]);
setProviderDefaults({});
setProviderConnectedIds([]);
@@ -653,7 +753,7 @@ export function SettingsRoute() {
}
void providerAuthStore.refreshProviders();
void connectionsStore.refreshMcpServers();
}, [connectionsStore, opencodeClient, providerAuthStore, selectedWorkspace?.id]);
}, [activeClient, connectionsStore, providerAuthStore, selectedWorkspace?.id]);
if (route.redirectPath) {
return <Navigate to={route.redirectPath} replace />;