mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
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:
@@ -338,11 +338,12 @@ export function SessionSurface(props: SessionSurfaceProps) {
|
|||||||
setSending(true);
|
setSending(true);
|
||||||
try {
|
try {
|
||||||
const nextDraft = buildDraft(text, attachments);
|
const nextDraft = buildDraft(text, attachments);
|
||||||
props.onSendDraft(nextDraft);
|
await props.onSendDraft(nextDraft);
|
||||||
setDraft("");
|
setDraft("");
|
||||||
attachments.forEach(revokeAttachmentPreview);
|
attachments.forEach(revokeAttachmentPreview);
|
||||||
setAttachments([]);
|
setAttachments([]);
|
||||||
props.onDraftChange(buildDraft("", []));
|
props.onDraftChange(buildDraft("", []));
|
||||||
|
setSending(false);
|
||||||
} catch (nextError) {
|
} catch (nextError) {
|
||||||
setError(nextError instanceof Error ? nextError.message : "Failed to send prompt.");
|
setError(nextError instanceof Error ? nextError.message : "Failed to send prompt.");
|
||||||
setSending(false);
|
setSending(false);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import { THINKING_PREF_KEY } from "../../app/constants";
|
import { THINKING_PREF_KEY } from "../../app/constants";
|
||||||
import { coerceReleaseChannel } from "../../app/lib/release-channels";
|
import { coerceReleaseChannel } from "../../app/lib/release-channels";
|
||||||
import type { ModelRef, ReleaseChannel, SettingsTab, View } from "../../app/types";
|
import type { ModelRef, ReleaseChannel, SettingsTab, View } from "../../app/types";
|
||||||
|
import { readStoredDefaultModel } from "./model-config";
|
||||||
|
|
||||||
export type LocalUIState = {
|
export type LocalUIState = {
|
||||||
view: View;
|
view: View;
|
||||||
@@ -88,9 +89,16 @@ export function LocalProvider({ children }: LocalProviderProps) {
|
|||||||
const [ui, setUiRaw] = useState<LocalUIState>(() =>
|
const [ui, setUiRaw] = useState<LocalUIState>(() =>
|
||||||
readPersisted(UI_STORAGE_KEY, INITIAL_UI),
|
readPersisted(UI_STORAGE_KEY, INITIAL_UI),
|
||||||
);
|
);
|
||||||
const [prefs, setPrefsRaw] = useState<LocalPreferences>(() =>
|
const [prefs, setPrefsRaw] = useState<LocalPreferences>(() => {
|
||||||
readPersisted(PREFS_STORAGE_KEY, INITIAL_PREFS),
|
const persisted = readPersisted(PREFS_STORAGE_KEY, INITIAL_PREFS);
|
||||||
);
|
if (persisted.defaultModel) {
|
||||||
|
return persisted;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...persisted,
|
||||||
|
defaultModel: readStoredDefaultModel(),
|
||||||
|
};
|
||||||
|
});
|
||||||
const [ready, setReady] = useState(false);
|
const [ready, setReady] = useState(false);
|
||||||
const migratedThinkingRef = useRef(false);
|
const migratedThinkingRef = useRef(false);
|
||||||
|
|
||||||
|
|||||||
112
apps/app/src/react-app/shell/desktop-local-openwork.ts
Normal file
112
apps/app/src/react-app/shell/desktop-local-openwork.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,15 @@
|
|||||||
/** @jsxImportSource react */
|
/** @jsxImportSource react */
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
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 { listCommands, shellInSession } from "../../app/lib/opencode-session";
|
||||||
import {
|
import {
|
||||||
buildOpenworkWorkspaceBaseUrl,
|
buildOpenworkWorkspaceBaseUrl,
|
||||||
@@ -43,11 +49,11 @@ import type {
|
|||||||
TodoItem,
|
TodoItem,
|
||||||
WorkspacePreset,
|
WorkspacePreset,
|
||||||
WorkspaceConnectionState,
|
WorkspaceConnectionState,
|
||||||
|
ProviderListItem,
|
||||||
WorkspaceSessionGroup,
|
WorkspaceSessionGroup,
|
||||||
} from "../../app/types";
|
} from "../../app/types";
|
||||||
import { createClient } from "../../app/lib/opencode";
|
|
||||||
import { buildFeedbackUrl } from "../../app/lib/feedback";
|
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 { t } from "../../i18n";
|
||||||
import { useLocal } from "../kernel/local-provider";
|
import { useLocal } from "../kernel/local-provider";
|
||||||
import { usePlatform } from "../kernel/platform";
|
import { usePlatform } from "../kernel/platform";
|
||||||
@@ -75,6 +81,8 @@ import {
|
|||||||
recordInspectorEvent,
|
recordInspectorEvent,
|
||||||
} from "./app-inspector";
|
} from "./app-inspector";
|
||||||
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
|
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
|
||||||
|
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
|
||||||
|
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
||||||
|
|
||||||
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
||||||
displayNameResolved: string;
|
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(
|
function toSessionGroups(
|
||||||
workspaces: RouteWorkspace[],
|
workspaces: RouteWorkspace[],
|
||||||
sessionsByWorkspaceId: Record<string, any[]>,
|
sessionsByWorkspaceId: Record<string, any[]>,
|
||||||
@@ -228,11 +291,13 @@ export function SessionRoute() {
|
|||||||
const [workspaces, setWorkspaces] = useState<RouteWorkspace[]>([]);
|
const [workspaces, setWorkspaces] = useState<RouteWorkspace[]>([]);
|
||||||
const [sessionsByWorkspaceId, setSessionsByWorkspaceId] = useState<Record<string, any[]>>({});
|
const [sessionsByWorkspaceId, setSessionsByWorkspaceId] = useState<Record<string, any[]>>({});
|
||||||
const [errorsByWorkspaceId, setErrorsByWorkspaceId] = useState<Record<string, string | null>>({});
|
const [errorsByWorkspaceId, setErrorsByWorkspaceId] = useState<Record<string, string | null>>({});
|
||||||
|
const [routeError, setRouteError] = useState<string | null>(null);
|
||||||
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(() => readActiveWorkspaceId() ?? "");
|
const [selectedWorkspaceId, setSelectedWorkspaceId] = useState<string>(() => readActiveWorkspaceId() ?? "");
|
||||||
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
const [selectedAgent, setSelectedAgent] = useState<string | null>(null);
|
||||||
// One-way latch for "a refreshRouteState is currently running"; prevents
|
// One-way latch for "a refreshRouteState is currently running"; prevents
|
||||||
// overlapping route refreshes from queueing up when the user clicks fast.
|
// overlapping route refreshes from queueing up when the user clicks fast.
|
||||||
const refreshInFlightRef = useRef(false);
|
const refreshInFlightRef = useRef(false);
|
||||||
|
const workspacesRef = useRef<RouteWorkspace[]>([]);
|
||||||
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
|
const [createWorkspaceOpen, setCreateWorkspaceOpen] = useState(false);
|
||||||
const [createWorkspaceBusy, setCreateWorkspaceBusy] = useState(false);
|
const [createWorkspaceBusy, setCreateWorkspaceBusy] = useState(false);
|
||||||
const [createWorkspaceRemoteBusy, setCreateWorkspaceRemoteBusy] = useState(false);
|
const [createWorkspaceRemoteBusy, setCreateWorkspaceRemoteBusy] = useState(false);
|
||||||
@@ -247,6 +312,8 @@ export function SessionRoute() {
|
|||||||
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
const [modelPickerOpen, setModelPickerOpen] = useState(false);
|
||||||
const [modelPickerQuery, setModelPickerQuery] = useState("");
|
const [modelPickerQuery, setModelPickerQuery] = useState("");
|
||||||
const [modelOptions, setModelOptions] = useState<ModelOption[]>([]);
|
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
|
// Provider catalog cache. Used to compute the reasoning/thinking variant
|
||||||
// options for whichever model is currently selected so the composer's
|
// options for whichever model is currently selected so the composer's
|
||||||
// behavior pill actually shows its options (bug: was empty before).
|
// 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 [routeEngineInfo, setRouteEngineInfo] = useState<EngineInfo | null>(null);
|
||||||
const [shareRemoteAccessBusy, setShareRemoteAccessBusy] = useState(false);
|
const [shareRemoteAccessBusy, setShareRemoteAccessBusy] = useState(false);
|
||||||
const [shareRemoteAccessError, setShareRemoteAccessError] = useState<string | null>(null);
|
const [shareRemoteAccessError, setShareRemoteAccessError] = useState<string | null>(null);
|
||||||
|
const reconnectAttemptedWorkspaceIdRef = useRef("");
|
||||||
|
|
||||||
const openworkServerSettings = useMemo(
|
const openworkServerSettings = useMemo(
|
||||||
() => readOpenworkServerSettings(),
|
() => readOpenworkServerSettings(),
|
||||||
@@ -280,9 +348,25 @@ export function SessionRoute() {
|
|||||||
if (refreshInFlightRef.current) return;
|
if (refreshInFlightRef.current) return;
|
||||||
refreshInFlightRef.current = true;
|
refreshInFlightRef.current = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setRouteError(null);
|
||||||
|
let desktopList = null as Awaited<ReturnType<typeof workspaceBootstrap>> | null;
|
||||||
|
let desktopWorkspaces = workspacesRef.current;
|
||||||
try {
|
try {
|
||||||
const desktopList = isTauriRuntime() ? await workspaceBootstrap().catch(() => null) : null;
|
if (isTauriRuntime()) {
|
||||||
const desktopWorkspaces = (desktopList?.workspaces ?? []).map(mapDesktopWorkspace);
|
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();
|
const { normalizedBaseUrl, resolvedToken, hostInfo } = await resolveRouteOpenworkConnection();
|
||||||
setOpenworkServerHostInfoState(hostInfo);
|
setOpenworkServerHostInfoState(hostInfo);
|
||||||
@@ -302,50 +386,7 @@ export function SessionRoute() {
|
|||||||
token: resolvedToken,
|
token: resolvedToken,
|
||||||
});
|
});
|
||||||
const list = await openworkClient.listWorkspaces();
|
const list = await openworkClient.listWorkspaces();
|
||||||
const desktopById = new Map(desktopWorkspaces.map((workspace) => [workspace.id, workspace]));
|
const nextWorkspaces = mergeRouteWorkspaces(list.items, desktopWorkspaces);
|
||||||
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 sessionEntries = await Promise.all(
|
const sessionEntries = await Promise.all(
|
||||||
nextWorkspaces.map(async (workspace) => {
|
nextWorkspaces.map(async (workspace) => {
|
||||||
@@ -405,6 +446,21 @@ export function SessionRoute() {
|
|||||||
selectedWorkspaceId: nextWorkspaceId,
|
selectedWorkspaceId: nextWorkspaceId,
|
||||||
errors: Object.fromEntries(sessionEntries.filter((e) => e.error).map((e) => [e.workspaceId, e.error])),
|
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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
refreshInFlightRef.current = false;
|
refreshInFlightRef.current = false;
|
||||||
@@ -415,6 +471,10 @@ export function SessionRoute() {
|
|||||||
}
|
}
|
||||||
}, [markBootRouteReady, selectedSessionId]);
|
}, [markBootRouteReady, selectedSessionId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
workspacesRef.current = workspaces;
|
||||||
|
}, [workspaces]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
|
|
||||||
@@ -483,6 +543,7 @@ export function SessionRoute() {
|
|||||||
baseUrl,
|
baseUrl,
|
||||||
tokenPresent: token.length > 0,
|
tokenPresent: token.length > 0,
|
||||||
connected: Boolean(client),
|
connected: Boolean(client),
|
||||||
|
routeError,
|
||||||
selectedSessionId,
|
selectedSessionId,
|
||||||
selectedWorkspaceId,
|
selectedWorkspaceId,
|
||||||
persistedActiveWorkspaceId: readActiveWorkspaceId(),
|
persistedActiveWorkspaceId: readActiveWorkspaceId(),
|
||||||
@@ -513,6 +574,7 @@ export function SessionRoute() {
|
|||||||
loading,
|
loading,
|
||||||
selectedSessionId,
|
selectedSessionId,
|
||||||
selectedWorkspaceId,
|
selectedWorkspaceId,
|
||||||
|
routeError,
|
||||||
sessionsByWorkspaceId,
|
sessionsByWorkspaceId,
|
||||||
token,
|
token,
|
||||||
workspaces,
|
workspaces,
|
||||||
@@ -549,6 +611,28 @@ export function SessionRoute() {
|
|||||||
[selectedWorkspaceId, workspaces],
|
[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 selectedWorkspaceRoot = selectedWorkspace?.path?.trim() || "";
|
||||||
const opencodeBaseUrl = useMemo(() => {
|
const opencodeBaseUrl = useMemo(() => {
|
||||||
if (!selectedWorkspaceId || !baseUrl) return "";
|
if (!selectedWorkspaceId || !baseUrl) return "";
|
||||||
@@ -559,11 +643,83 @@ export function SessionRoute() {
|
|||||||
const opencodeClient = useMemo(
|
const opencodeClient = useMemo(
|
||||||
() =>
|
() =>
|
||||||
opencodeBaseUrl && token
|
opencodeBaseUrl && token
|
||||||
? createClient(opencodeBaseUrl, undefined, { token, mode: "openwork" })
|
? createClient(opencodeBaseUrl, selectedWorkspaceRoot || undefined, {
|
||||||
|
token,
|
||||||
|
mode: "openwork",
|
||||||
|
})
|
||||||
: null,
|
: 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
|
const modelLabel = local.prefs.defaultModel
|
||||||
? `${local.prefs.defaultModel.providerID}/${local.prefs.defaultModel.modelID}`
|
? `${local.prefs.defaultModel.providerID}/${local.prefs.defaultModel.modelID}`
|
||||||
: t("session.default_model");
|
: t("session.default_model");
|
||||||
@@ -752,6 +908,7 @@ export function SessionRoute() {
|
|||||||
const result = await opencodeClient.session.promptAsync({
|
const result = await opencodeClient.session.promptAsync({
|
||||||
sessionID: selectedSessionId,
|
sessionID: selectedSessionId,
|
||||||
parts,
|
parts,
|
||||||
|
model: local.prefs.defaultModel ?? undefined,
|
||||||
agent: selectedAgent ?? undefined,
|
agent: selectedAgent ?? undefined,
|
||||||
...(local.prefs.modelVariant ? { variant: local.prefs.modelVariant } : {}),
|
...(local.prefs.modelVariant ? { variant: local.prefs.modelVariant } : {}),
|
||||||
});
|
});
|
||||||
@@ -968,7 +1125,11 @@ export function SessionRoute() {
|
|||||||
const workspace = workspaces.find((item) => item.id === workspaceId);
|
const workspace = workspaces.find((item) => item.id === workspaceId);
|
||||||
if (!workspace || !token || !baseUrl) return;
|
if (!workspace || !token || !baseUrl) return;
|
||||||
const workspaceOpencodeBaseUrl = `${(buildOpenworkWorkspaceBaseUrl(baseUrl, workspace.id) ?? baseUrl).replace(/\/+$|\/+$/g, "")}/opencode`;
|
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(
|
const session = unwrap(
|
||||||
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
|
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")}
|
headerStatus={client ? t("status.connected") : t("status.disconnected_label")}
|
||||||
busyHint={loading ? t("session.loading_detail") : null}
|
busyHint={loading ? t("session.loading_detail") : null}
|
||||||
startupPhase={loading ? "nativeInit" : "ready"}
|
startupPhase={loading ? "nativeInit" : "ready"}
|
||||||
providerConnectedIds={[]}
|
providerConnectedIds={providerConnectedIds}
|
||||||
providers={[]}
|
providers={providers}
|
||||||
mcpConnectedCount={0}
|
mcpConnectedCount={0}
|
||||||
onSendFeedback={() => {
|
onSendFeedback={() => {
|
||||||
platform.openLink(
|
platform.openLink(
|
||||||
@@ -1193,7 +1354,11 @@ export function SessionRoute() {
|
|||||||
const workspace = workspaces.find((item) => item.id === workspaceId);
|
const workspace = workspaces.find((item) => item.id === workspaceId);
|
||||||
if (!workspace || !token || !baseUrl) return;
|
if (!workspace || !token || !baseUrl) return;
|
||||||
const workspaceOpencodeBaseUrl = `${(buildOpenworkWorkspaceBaseUrl(baseUrl, workspace.id) ?? baseUrl).replace(/\/+$|\/+$/g, "")}/opencode`;
|
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(
|
const session = unwrap(
|
||||||
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
|
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ import {
|
|||||||
import { isDesktopProviderBlocked } from "../../app/cloud/desktop-app-restrictions";
|
import { isDesktopProviderBlocked } from "../../app/cloud/desktop-app-restrictions";
|
||||||
import { useCheckDesktopRestriction } from "../domains/cloud/desktop-config-provider";
|
import { useCheckDesktopRestriction } from "../domains/cloud/desktop-config-provider";
|
||||||
import { useCloudProviderAutoSync } from "../domains/cloud/use-cloud-provider-auto-sync";
|
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 { CreateWorkspaceModal } from "../domains/workspace/create-workspace-modal";
|
||||||
import { ModelPickerModal } from "../domains/session/modals/model-picker-modal";
|
import { ModelPickerModal } from "../domains/session/modals/model-picker-modal";
|
||||||
import type { ModelOption, ModelRef } from "../../app/types";
|
import type { ModelOption, ModelRef } from "../../app/types";
|
||||||
|
import { recordInspectorEvent } from "./app-inspector";
|
||||||
|
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
||||||
|
|
||||||
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
||||||
displayNameResolved: string;
|
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) {
|
function folderNameFromPath(path: string) {
|
||||||
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||||
const parts = normalized.split("/").filter(Boolean);
|
const parts = normalized.split("/").filter(Boolean);
|
||||||
@@ -233,6 +290,8 @@ export function SettingsRoute() {
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [busyLabel, setBusyLabel] = useState<string | null>(null);
|
const [busyLabel, setBusyLabel] = useState<string | null>(null);
|
||||||
const [routeError, setRouteError] = useState<string | null>(null);
|
const [routeError, setRouteError] = useState<string | null>(null);
|
||||||
|
const workspacesRef = useRef<RouteWorkspace[]>([]);
|
||||||
|
const reconnectAttemptedWorkspaceIdRef = useRef("");
|
||||||
const [providers, setProviders] = useState<ProviderListItem[]>([]);
|
const [providers, setProviders] = useState<ProviderListItem[]>([]);
|
||||||
const [providerDefaults, setProviderDefaults] = useState<Record<string, string>>({});
|
const [providerDefaults, setProviderDefaults] = useState<Record<string, string>>({});
|
||||||
const [providerConnectedIds, setProviderConnectedIds] = useState<string[]>([]);
|
const [providerConnectedIds, setProviderConnectedIds] = useState<string[]>([]);
|
||||||
@@ -438,9 +497,12 @@ export function SettingsRoute() {
|
|||||||
const opencodeClient = useMemo(
|
const opencodeClient = useMemo(
|
||||||
() =>
|
() =>
|
||||||
opencodeBaseUrl && token
|
opencodeBaseUrl && token
|
||||||
? createClient(opencodeBaseUrl, undefined, { token, mode: "openwork" })
|
? createClient(opencodeBaseUrl, selectedWorkspaceRoot || undefined, {
|
||||||
|
token,
|
||||||
|
mode: "openwork",
|
||||||
|
})
|
||||||
: null,
|
: null,
|
||||||
[opencodeBaseUrl, token],
|
[opencodeBaseUrl, selectedWorkspaceRoot, token],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -520,9 +582,24 @@ export function SettingsRoute() {
|
|||||||
const refreshRouteState = useMemo(() => async () => {
|
const refreshRouteState = useMemo(() => async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setRouteError(null);
|
setRouteError(null);
|
||||||
|
let desktopList = null as Awaited<ReturnType<typeof workspaceBootstrap>> | null;
|
||||||
|
let desktopWorkspaces = workspacesRef.current;
|
||||||
try {
|
try {
|
||||||
const desktopList = isTauriRuntime() ? await workspaceBootstrap().catch(() => null) : null;
|
if (isTauriRuntime()) {
|
||||||
const desktopWorkspaces = (desktopList?.workspaces ?? []).map(mapDesktopWorkspace);
|
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();
|
const { normalizedBaseUrl, resolvedToken } = await resolveRouteOpenworkConnection();
|
||||||
|
|
||||||
if (!normalizedBaseUrl || !resolvedToken) {
|
if (!normalizedBaseUrl || !resolvedToken) {
|
||||||
@@ -538,21 +615,7 @@ export function SettingsRoute() {
|
|||||||
|
|
||||||
const client = createOpenworkServerClient({ baseUrl: normalizedBaseUrl, token: resolvedToken });
|
const client = createOpenworkServerClient({ baseUrl: normalizedBaseUrl, token: resolvedToken });
|
||||||
const list = await client.listWorkspaces();
|
const list = await client.listWorkspaces();
|
||||||
const serverWorkspaces = list.items.map((workspace) => ({
|
const nextWorkspaces = mergeRouteWorkspaces(list.items, desktopWorkspaces);
|
||||||
...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 sessionEntries = await Promise.all(
|
const sessionEntries = await Promise.all(
|
||||||
nextWorkspaces.map(async (workspace) => {
|
nextWorkspaces.map(async (workspace) => {
|
||||||
try {
|
try {
|
||||||
@@ -582,7 +645,18 @@ export function SettingsRoute() {
|
|||||||
setErrorsByWorkspaceId(Object.fromEntries(sessionEntries.map((entry) => [entry.workspaceId, entry.error])));
|
setErrorsByWorkspaceId(Object.fromEntries(sessionEntries.map((entry) => [entry.workspaceId, entry.error])));
|
||||||
setSelectedWorkspaceId((current) => current || resolveWorkspaceListSelectedId(desktopList) || list.activeId?.trim() || nextWorkspaces[0]?.id || "");
|
setSelectedWorkspaceId((current) => current || resolveWorkspaceListSelectedId(desktopList) || list.activeId?.trim() || nextWorkspaces[0]?.id || "");
|
||||||
} catch (error) {
|
} 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 {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
// Settings can be the first route a user lands on (direct link, deep
|
// Settings can be the first route a user lands on (direct link, deep
|
||||||
@@ -592,6 +666,32 @@ export function SettingsRoute() {
|
|||||||
}
|
}
|
||||||
}, [markBootRouteReady]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
void refreshRouteState();
|
void refreshRouteState();
|
||||||
const handleSettingsChange = () => {
|
const handleSettingsChange = () => {
|
||||||
@@ -644,7 +744,7 @@ export function SettingsRoute() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!opencodeClient) {
|
if (!activeClient) {
|
||||||
setProviders([]);
|
setProviders([]);
|
||||||
setProviderDefaults({});
|
setProviderDefaults({});
|
||||||
setProviderConnectedIds([]);
|
setProviderConnectedIds([]);
|
||||||
@@ -653,7 +753,7 @@ export function SettingsRoute() {
|
|||||||
}
|
}
|
||||||
void providerAuthStore.refreshProviders();
|
void providerAuthStore.refreshProviders();
|
||||||
void connectionsStore.refreshMcpServers();
|
void connectionsStore.refreshMcpServers();
|
||||||
}, [connectionsStore, opencodeClient, providerAuthStore, selectedWorkspace?.id]);
|
}, [activeClient, connectionsStore, providerAuthStore, selectedWorkspace?.id]);
|
||||||
|
|
||||||
if (route.redirectPath) {
|
if (route.redirectPath) {
|
||||||
return <Navigate to={route.redirectPath} replace />;
|
return <Navigate to={route.redirectPath} replace />;
|
||||||
|
|||||||
Reference in New Issue
Block a user