fix React startup and reload flows

This commit is contained in:
Benjamin Shafii
2026-04-23 22:43:51 -07:00
parent 9517d0397e
commit 50e350bc19
9 changed files with 542 additions and 113 deletions

View File

@@ -23,6 +23,7 @@ import {
usesChromeDevtoolsAutoConnect, usesChromeDevtoolsAutoConnect,
validateMcpServerName, validateMcpServerName,
} from "../../../app/mcp"; } from "../../../app/mcp";
import { buildOpenworkWorkspaceBaseUrl } from "../../../app/lib/openwork-server";
import type { import type {
Client, Client,
McpServerEntry, McpServerEntry,
@@ -171,7 +172,9 @@ export function createConnectionsStore(options: {
return null; return null;
} }
activeClient = createClient(`${openworkBaseUrl.replace(/\/+$/, "")}/opencode`, undefined, { const mountedBaseUrl =
buildOpenworkWorkspaceBaseUrl(openworkBaseUrl, options.runtimeWorkspaceId()) ?? openworkBaseUrl;
activeClient = createClient(`${mountedBaseUrl.replace(/\/+$/, "")}/opencode`, undefined, {
token, token,
mode: "openwork", mode: "openwork",
}); });
@@ -530,30 +533,40 @@ export function createConnectionsStore(options: {
} }
} }
const mcpAddConfig = if (canUseOpenworkServer && openworkClient && openworkWorkspaceId) {
entryType === "remote" // The OpenWork server is the source of truth for workspace-scoped MCP
? { // config in the React port. Avoid also calling the OpenCode SDK's MCP
type: "remote" as const, // hot-add endpoint here: when the SDK client is rooted at the aggregate
url: entry.url!, // `/opencode` route it can resolve to an internal `local_*` workspace
enabled: true, // id that the OpenWork server does not expose, producing a confusing
...(entry.oauth ? { oauth: {} } : {}), // `workspace_not_found` after the config write already succeeded.
} setStateField("mcpStatuses", filterConfiguredStatuses(snapshot.mcpStatuses, snapshot.mcpServers));
: { } else {
type: "local" as const, const mcpAddConfig =
command: entry.command!, entryType === "remote"
enabled: true, ? {
...(mcpEnvironment ? { environment: mcpEnvironment } : {}), type: "remote" as const,
}; url: entry.url!,
enabled: true,
...(entry.oauth ? { oauth: {} } : {}),
}
: {
type: "local" as const,
command: entry.command!,
enabled: true,
...(mcpEnvironment ? { environment: mcpEnvironment } : {}),
};
const status = unwrap( const status = unwrap(
await activeClient.mcp.add({ await activeClient.mcp.add({
directory: resolvedProjectDir, directory: resolvedProjectDir,
name: slug, name: slug,
config: mcpAddConfig, config: mcpAddConfig,
}), }),
); );
setStateField("mcpStatuses", status as McpStatusMap); setStateField("mcpStatuses", status as McpStatusMap);
}
options.markReloadRequired?.("mcp", { type: "mcp", name: slug, action }); options.markReloadRequired?.("mcp", { type: "mcp", name: slug, action });
await refreshMcpServers(); await refreshMcpServers();

View File

@@ -143,6 +143,14 @@ export function ServerProvider({ children, defaultUrl }: ServerProviderProps) {
useEffect(() => { useEffect(() => {
if (!active) return; if (!active) return;
if (isDesktopRuntime() && !active.includes("/opencode")) {
// Desktop React routes now talk to OpenWork server workspace-mounted
// `/opencode` URLs directly. Ignore old persisted raw OpenCode daemon
// URLs here; their ephemeral ports go stale across restarts and otherwise
// produce noisy `/global/health` connection-refused polling forever.
setHealthy(undefined);
return;
}
setHealthy(undefined); setHealthy(undefined);
let cancelled = false; let cancelled = false;

View File

@@ -49,7 +49,7 @@ const PHASE_MESSAGES: Record<BootPhaseId, string> = {
idle: "", idle: "",
"bootstrapping-workspaces": "Loading your workspaces", "bootstrapping-workspaces": "Loading your workspaces",
"starting-openwork-server": "Starting the OpenWork server", "starting-openwork-server": "Starting the OpenWork server",
"starting-engine": "Starting OpenCode", "starting-engine": "Preparing workspace",
"activating-workspace": "Activating your workspace", "activating-workspace": "Activating your workspace",
ready: "Ready", ready: "Ready",
error: "Something went wrong", error: "Something went wrong",

View File

@@ -130,7 +130,7 @@ export function useDesktopRuntimeBoot() {
if (!workspacePaths.includes(path)) workspacePaths.push(path); if (!workspacePaths.includes(path)) workspacePaths.push(path);
} }
setPhase("starting-engine", "Launching the OpenCode engine"); setPhase("starting-engine", "Starting your workspace");
const engineStartResult = await engineStart(workspaceRoot, { const engineStartResult = await engineStart(workspaceRoot, {
runtime: "openwork-orchestrator", runtime: "openwork-orchestrator",
workspacePaths, workspacePaths,

View File

@@ -13,6 +13,7 @@ import { BootStateProvider } from "./boot-state";
import { DesktopRuntimeBoot } from "./desktop-runtime-boot"; import { DesktopRuntimeBoot } from "./desktop-runtime-boot";
import { startDebugLogger, stopDebugLogger } from "./debug-logger"; import { startDebugLogger, stopDebugLogger } from "./debug-logger";
import { MigrationPrompt } from "./migration-prompt"; import { MigrationPrompt } from "./migration-prompt";
import { ReloadCoordinatorProvider } from "./reload-coordinator";
function resolveDefaultServerUrl(): string { function resolveDefaultServerUrl(): string {
if (isDesktopRuntime()) return "http://127.0.0.1:4096"; if (isDesktopRuntime()) return "http://127.0.0.1:4096";
@@ -63,7 +64,9 @@ export function AppProviders({ children }: AppProvidersProps) {
<DenAuthProvider> <DenAuthProvider>
<DesktopConfigProvider> <DesktopConfigProvider>
<RestrictionNoticeProvider> <RestrictionNoticeProvider>
<LocalProvider>{children}</LocalProvider> <LocalProvider>
<ReloadCoordinatorProvider>{children}</ReloadCoordinatorProvider>
</LocalProvider>
</RestrictionNoticeProvider> </RestrictionNoticeProvider>
</DesktopConfigProvider> </DesktopConfigProvider>
</DenAuthProvider> </DenAuthProvider>

View File

@@ -0,0 +1,149 @@
/** @jsxImportSource react */
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode,
} from "react";
import type { ReloadReason, ReloadTrigger } from "../../app/types";
import { t } from "../../i18n";
import { ReloadWorkspaceToast } from "../domains/shell-feedback/reload-workspace-toast";
import { useSystemState } from "../kernel/system-state";
type ReloadSession = { id: string; title: string };
export type WorkspaceReloadControls = {
canReloadWorkspaceEngine: () => boolean;
reloadWorkspaceEngine: () => Promise<boolean>;
activeSessions?: () => ReloadSession[];
stopSession?: (sessionId: string) => void | Promise<void>;
};
type ReloadCoordinatorContextValue = {
markReloadRequired: (reason: ReloadReason, trigger?: ReloadTrigger) => void;
clearReloadRequired: () => void;
reloadWorkspaceEngine: () => Promise<void>;
canReloadWorkspaceEngine: boolean;
registerWorkspaceReloadControls: (controls: WorkspaceReloadControls | null) => () => void;
};
const ReloadCoordinatorContext = createContext<ReloadCoordinatorContextValue | null>(null);
export function ReloadCoordinatorProvider({ children }: { children: ReactNode }) {
const controlsRef = useRef<WorkspaceReloadControls | null>(null);
const [activeSessions, setActiveSessions] = useState<ReloadSession[]>([]);
const registerWorkspaceReloadControls = useCallback((controls: WorkspaceReloadControls | null) => {
controlsRef.current = controls;
setActiveSessions(controls?.activeSessions?.() ?? []);
return () => {
if (controlsRef.current === controls) {
controlsRef.current = null;
setActiveSessions([]);
}
};
}, []);
const hasActiveRuns = useCallback(() => activeSessions.length > 0, [activeSessions.length]);
const canReloadWorkspaceEngine = useCallback(
() => controlsRef.current?.canReloadWorkspaceEngine() === true,
[],
);
const reloadWorkspaceEngine = useCallback(async () => {
const controls = controlsRef.current;
if (!controls?.reloadWorkspaceEngine) return false;
return controls.reloadWorkspaceEngine();
}, []);
const ignoreError = useCallback(() => {}, []);
const systemStateOptions = useMemo(
() => ({
hasActiveRuns,
canReloadWorkspaceEngine,
reloadWorkspaceEngine,
setError: ignoreError,
}),
[canReloadWorkspaceEngine, hasActiveRuns, ignoreError, reloadWorkspaceEngine],
);
const systemState = useSystemState(systemStateOptions);
useEffect(() => {
const handler = (event: Event) => {
const detail = (event as CustomEvent<{ reason?: ReloadReason; trigger?: ReloadTrigger }>).detail;
systemState.markReloadRequired(detail?.reason ?? "config", detail?.trigger);
};
window.addEventListener("openwork-reload-required", handler);
return () => window.removeEventListener("openwork-reload-required", handler);
}, [systemState.markReloadRequired]);
const forceStopActiveSessionsAndReload = useCallback(async () => {
const controls = controlsRef.current;
if (controls?.stopSession) {
for (const session of activeSessions) {
await Promise.resolve(controls.stopSession(session.id)).catch(() => undefined);
}
}
await systemState.reloadWorkspaceEngine();
}, [activeSessions, systemState.reloadWorkspaceEngine]);
const value = useMemo<ReloadCoordinatorContextValue>(
() => ({
markReloadRequired: systemState.markReloadRequired,
clearReloadRequired: systemState.clearReloadRequired,
reloadWorkspaceEngine: systemState.reloadWorkspaceEngine,
canReloadWorkspaceEngine: systemState.canReloadWorkspaceEngine,
registerWorkspaceReloadControls,
}),
[
registerWorkspaceReloadControls,
systemState.canReloadWorkspaceEngine,
systemState.clearReloadRequired,
systemState.markReloadRequired,
systemState.reloadWorkspaceEngine,
],
);
return (
<ReloadCoordinatorContext.Provider value={value}>
{children}
<div className="pointer-events-none fixed right-4 top-4 z-50 w-[min(24rem,calc(100vw-1.5rem))] max-w-full sm:right-6 sm:top-6">
<div className="pointer-events-auto">
<ReloadWorkspaceToast
open={systemState.reload.reloadPending}
title={systemState.reloadCopy.title}
description={systemState.reloadCopy.body}
trigger={systemState.reload.reloadTrigger}
error={systemState.reload.reloadError}
reloadLabel={
activeSessions.length > 0 ? t("app.reload_stop_tasks") : t("app.reload_now")
}
dismissLabel={t("app.reload_later")}
busy={systemState.reload.reloadBusy}
canReload={systemState.canReloadWorkspaceEngine}
hasActiveRuns={activeSessions.length > 0}
onReload={() => {
void (activeSessions.length > 0
? forceStopActiveSessionsAndReload()
: systemState.reloadWorkspaceEngine());
}}
onDismiss={systemState.clearReloadRequired}
/>
</div>
</div>
</ReloadCoordinatorContext.Provider>
);
}
export function useReloadCoordinator(): ReloadCoordinatorContextValue {
const value = useContext(ReloadCoordinatorContext);
if (!value) {
throw new Error("useReloadCoordinator must be used inside <ReloadCoordinatorProvider>");
}
return value;
}

View File

@@ -85,6 +85,7 @@ import { useReactRenderWatchdog } from "./react-render-watchdog";
import { getModelBehaviorSummary } from "../../app/lib/model-behavior"; import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers"; import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork"; import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
import { useReloadCoordinator } from "./reload-coordinator";
type RouteWorkspace = OpenworkWorkspaceInfo & { type RouteWorkspace = OpenworkWorkspaceInfo & {
displayNameResolved: string; displayNameResolved: string;
@@ -133,6 +134,44 @@ async function resolveRouteOpenworkConnection() {
return { normalizedBaseUrl, resolvedToken, hostInfo }; return { normalizedBaseUrl, resolvedToken, hostInfo };
} }
async function checkWorkspaceOpencodeReady(
baseUrl: string,
workspaceId: string,
token: string,
timeoutMs = 1_500,
) {
const mounted = buildOpenworkWorkspaceBaseUrl(baseUrl, workspaceId) ?? baseUrl;
const url = `${mounted.replace(/\/+$/, "")}/opencode/global/health`;
const controller = typeof AbortController !== "undefined" ? new AbortController() : null;
let timeoutId: number | null = null;
try {
if (controller) {
timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);
}
const response = await fetch(url, {
headers: token ? { Authorization: `Bearer ${token}` } : undefined,
signal: controller?.signal,
});
if (!response.ok) return false;
const data = await response.json().catch(() => null);
return data?.healthy === true;
} catch {
return false;
} finally {
if (timeoutId) window.clearTimeout(timeoutId);
}
}
function isTransientStartupError(message: string | null | undefined) {
const value = (message ?? "").toLowerCase();
return (
value.includes("timed out") ||
value.includes("failed to fetch") ||
value.includes("connection") ||
value.includes("not ready")
);
}
function workspaceLabel(workspace: OpenworkWorkspaceInfo) { function workspaceLabel(workspace: OpenworkWorkspaceInfo) {
return ( return (
workspace.displayName?.trim() || workspace.displayName?.trim() ||
@@ -202,11 +241,16 @@ function toSessionGroups(
workspaces: RouteWorkspace[], workspaces: RouteWorkspace[],
sessionsByWorkspaceId: Record<string, any[]>, sessionsByWorkspaceId: Record<string, any[]>,
errorsByWorkspaceId: Record<string, string | null>, errorsByWorkspaceId: Record<string, string | null>,
loadingWorkspaceIds: Set<string>,
): WorkspaceSessionGroup[] { ): WorkspaceSessionGroup[] {
return workspaces.map((workspace) => ({ return workspaces.map((workspace) => ({
workspace, workspace,
sessions: (sessionsByWorkspaceId[workspace.id] ?? []) as WorkspaceSessionGroup["sessions"], sessions: (sessionsByWorkspaceId[workspace.id] ?? []) as WorkspaceSessionGroup["sessions"],
status: errorsByWorkspaceId[workspace.id] ? "error" : "ready", status: loadingWorkspaceIds.has(workspace.id)
? "loading"
: errorsByWorkspaceId[workspace.id]
? "error"
: "ready",
error: errorsByWorkspaceId[workspace.id], error: errorsByWorkspaceId[workspace.id],
})); }));
} }
@@ -280,6 +324,7 @@ export function SessionRoute() {
const navigate = useNavigate(); const navigate = useNavigate();
const platform = usePlatform(); const platform = usePlatform();
const local = useLocal(); const local = useLocal();
const reloadCoordinator = useReloadCoordinator();
const checkDesktopRestriction = useCheckDesktopRestriction(); const checkDesktopRestriction = useCheckDesktopRestriction();
const restrictionNotice = useRestrictionNotice(); const restrictionNotice = useRestrictionNotice();
const params = useParams<{ sessionId?: string }>(); const params = useParams<{ sessionId?: string }>();
@@ -299,7 +344,11 @@ export function SessionRoute() {
// 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 reloadEventCursorByWorkspaceRef = useRef<Record<string, number | null>>({});
const workspacesRef = useRef<RouteWorkspace[]>([]); const workspacesRef = useRef<RouteWorkspace[]>([]);
const sessionsByWorkspaceIdRef = useRef<Record<string, any[]>>({});
const startupRetryTimerRef = useRef<number | null>(null);
const [retryingWorkspaceIds, setRetryingWorkspaceIds] = useState<string[]>([]);
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);
@@ -362,6 +411,7 @@ export function SessionRoute() {
setRouteError(null); setRouteError(null);
let desktopList = null as Awaited<ReturnType<typeof workspaceBootstrap>> | null; let desktopList = null as Awaited<ReturnType<typeof workspaceBootstrap>> | null;
let desktopWorkspaces = workspacesRef.current; let desktopWorkspaces = workspacesRef.current;
let routeReadyAfterRefresh = true;
try { try {
if (isDesktopRuntime()) { if (isDesktopRuntime()) {
try { try {
@@ -402,6 +452,27 @@ export function SessionRoute() {
const sessionEntries = await Promise.all( const sessionEntries = await Promise.all(
nextWorkspaces.map(async (workspace) => { nextWorkspaces.map(async (workspace) => {
try { try {
const opencodeReady = await checkWorkspaceOpencodeReady(
normalizedBaseUrl,
workspace.id,
resolvedToken,
);
if (!opencodeReady) {
return {
workspaceId: workspace.id,
sessions: sessionsByWorkspaceIdRef.current[workspace.id] ?? [],
error: null as string | null,
loading: true,
};
}
if (!selectedSessionId) {
return {
workspaceId: workspace.id,
sessions: sessionsByWorkspaceIdRef.current[workspace.id] ?? [],
error: null as string | null,
loading: false,
};
}
const response = await openworkClient.listSessions(workspace.id, { limit: 200 }); const response = await openworkClient.listSessions(workspace.id, { limit: 200 });
// The underlying opencode instance is shared across all local // The underlying opencode instance is shared across all local
// workspaces attached to the same openwork-server, so `listSessions` // workspaces attached to the same openwork-server, so `listSessions`
@@ -414,16 +485,41 @@ export function SessionRoute() {
normalizeDirectoryPath(session?.directory ?? "") === workspaceRoot, normalizeDirectoryPath(session?.directory ?? "") === workspaceRoot,
) )
: (response.items ?? []); : (response.items ?? []);
return { workspaceId: workspace.id, sessions: items, error: null as string | null }; return { workspaceId: workspace.id, sessions: items, error: null as string | null, loading: false };
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : t("app.unknown_error");
if (isTransientStartupError(message)) {
return {
workspaceId: workspace.id,
sessions: sessionsByWorkspaceIdRef.current[workspace.id] ?? [],
error: null as string | null,
loading: true,
};
}
return { return {
workspaceId: workspace.id, workspaceId: workspace.id,
sessions: [], sessions: [],
error: error instanceof Error ? error.message : t("app.unknown_error"), error: message,
loading: false,
}; };
} }
}), }),
); );
const retryingIds = sessionEntries
.filter((entry) => entry.loading)
.map((entry) => entry.workspaceId);
routeReadyAfterRefresh = retryingIds.length === 0;
setRetryingWorkspaceIds(retryingIds);
if (retryingIds.length > 0 && typeof window !== "undefined" && startupRetryTimerRef.current === null) {
startupRetryTimerRef.current = window.setTimeout(() => {
startupRetryTimerRef.current = null;
refreshInFlightRef.current = false;
void refreshRouteState();
}, 1_000);
} else if (retryingIds.length === 0 && startupRetryTimerRef.current !== null) {
window.clearTimeout(startupRetryTimerRef.current);
startupRetryTimerRef.current = null;
}
// Prefer, in order: the URL-selected workspace (if it owns the session), // Prefer, in order: the URL-selected workspace (if it owns the session),
// the user's last-active workspace from localStorage, the desktop's // the user's last-active workspace from localStorage, the desktop's
@@ -478,14 +574,112 @@ export function SessionRoute() {
// Tell the boot overlay the first route data load has completed so // Tell the boot overlay the first route data load has completed so
// the overlay dismisses after BOTH the desktop boot and the workspace // the overlay dismisses after BOTH the desktop boot and the workspace
// list/sessions are ready. // list/sessions are ready.
markBootRouteReady(); if (routeReadyAfterRefresh) {
markBootRouteReady();
}
} }
}, [markBootRouteReady, selectedSessionId]); }, [markBootRouteReady, selectedSessionId]);
const reloadWorkspaceEngineFromUi = useCallback(async () => {
if (!client || !selectedWorkspaceId) {
setRouteError(t("app.error_connect_first"));
return false;
}
await client.reloadEngine(selectedWorkspaceId);
try {
window.dispatchEvent(new CustomEvent("openwork-server-settings-changed"));
} catch {
// ignore browser event dispatch failures
}
await refreshRouteState();
return true;
}, [client, refreshRouteState, selectedWorkspaceId]);
useEffect(() => {
return reloadCoordinator.registerWorkspaceReloadControls({
canReloadWorkspaceEngine: () => Boolean(client && selectedWorkspaceId),
reloadWorkspaceEngine: reloadWorkspaceEngineFromUi,
activeSessions: () => [],
});
}, [client, reloadCoordinator, reloadWorkspaceEngineFromUi, selectedWorkspaceId]);
useEffect(() => {
if (!client || !selectedWorkspaceId) return;
let cancelled = false;
const pollReloadEvents = async () => {
const currentCursor = reloadEventCursorByWorkspaceRef.current[selectedWorkspaceId];
try {
const response = await client.listReloadEvents(
selectedWorkspaceId,
typeof currentCursor === "number" ? { since: currentCursor } : undefined,
);
if (cancelled) return;
reloadEventCursorByWorkspaceRef.current[selectedWorkspaceId] =
typeof response.cursor === "number"
? response.cursor
: Math.max(currentCursor ?? 0, ...((response.items ?? []).map((item: any) => Number(item.seq) || 0)));
// The first poll establishes the server cursor so historical reload
// events don't show a stale toast on route entry. Subsequent polls mark
// new filesystem/server-side mutations, including skills created by an
// agent while the session page is open.
if (currentCursor === undefined || currentCursor === null) return;
for (const event of response.items ?? []) {
reloadCoordinator.markReloadRequired(event.reason, event.trigger);
}
} catch {
// Reload-event polling is best-effort; normal route health checks still
// surface connection failures.
}
};
void pollReloadEvents();
const interval = window.setInterval(() => void pollReloadEvents(), 3000);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [client, reloadCoordinator, selectedWorkspaceId]);
useEffect(() => {
if (!client || !selectedWorkspaceId || !selectedSessionId) return;
let cancelled = false;
const refreshSelectedSessionTitle = async () => {
try {
const response = await client.getSession(selectedWorkspaceId, selectedSessionId);
if (cancelled || !response.item) return;
setSessionsByWorkspaceId((current) => {
const list = current[selectedWorkspaceId] ?? [];
const index = list.findIndex((session: any) => session?.id === selectedSessionId);
if (index < 0) return current;
const nextSession = { ...list[index], ...response.item };
if (JSON.stringify(nextSession) === JSON.stringify(list[index])) return current;
const nextList = [...list];
nextList[index] = nextSession;
return { ...current, [selectedWorkspaceId]: nextList };
});
} catch {
// Best-effort title sync; the session surface still owns messages.
}
};
void refreshSelectedSessionTitle();
const interval = window.setInterval(() => void refreshSelectedSessionTitle(), 3_000);
return () => {
cancelled = true;
window.clearInterval(interval);
};
}, [client, selectedSessionId, selectedWorkspaceId]);
useEffect(() => { useEffect(() => {
workspacesRef.current = workspaces; workspacesRef.current = workspaces;
}, [workspaces]); }, [workspaces]);
useEffect(() => {
sessionsByWorkspaceIdRef.current = sessionsByWorkspaceId;
}, [sessionsByWorkspaceId]);
useEffect(() => { useEffect(() => {
let cancelled = false; let cancelled = false;
@@ -522,6 +716,10 @@ export function SessionRoute() {
return () => { return () => {
cancelled = true; cancelled = true;
if (startupRetryTimerRef.current !== null) {
window.clearTimeout(startupRetryTimerRef.current);
startupRetryTimerRef.current = null;
}
window.removeEventListener("openwork-server-settings-changed", handleSettingsChange); window.removeEventListener("openwork-server-settings-changed", handleSettingsChange);
if (typeof document !== "undefined") { if (typeof document !== "undefined") {
document.removeEventListener("visibilitychange", handleVisibility); document.removeEventListener("visibilitychange", handleVisibility);
@@ -551,6 +749,7 @@ export function SessionRoute() {
useEffect(() => { useEffect(() => {
const dispose = publishInspectorSlice("route", () => ({ const dispose = publishInspectorSlice("route", () => ({
loading, loading,
retryingWorkspaceIds,
baseUrl, baseUrl,
tokenPresent: token.length > 0, tokenPresent: token.length > 0,
connected: Boolean(client), connected: Boolean(client),
@@ -564,6 +763,7 @@ export function SessionRoute() {
workspaceType: workspace.workspaceType, workspaceType: workspace.workspaceType,
path: workspace.path, path: workspace.path,
sessionCount: (sessionsByWorkspaceId[workspace.id] ?? []).length, sessionCount: (sessionsByWorkspaceId[workspace.id] ?? []).length,
loading: retryingWorkspaceIds.includes(workspace.id),
error: errorsByWorkspaceId[workspace.id] ?? null, error: errorsByWorkspaceId[workspace.id] ?? null,
})), })),
sessionsByWorkspaceId: Object.fromEntries( sessionsByWorkspaceId: Object.fromEntries(
@@ -583,6 +783,7 @@ export function SessionRoute() {
client, client,
errorsByWorkspaceId, errorsByWorkspaceId,
loading, loading,
retryingWorkspaceIds,
selectedSessionId, selectedSessionId,
selectedWorkspaceId, selectedWorkspaceId,
routeError, routeError,
@@ -613,8 +814,8 @@ export function SessionRoute() {
// the onboarding flow, not from the route effect loop. // the onboarding flow, not from the route effect loop.
const workspaceSessionGroups = useMemo( const workspaceSessionGroups = useMemo(
() => toSessionGroups(workspaces, sessionsByWorkspaceId, errorsByWorkspaceId), () => toSessionGroups(workspaces, sessionsByWorkspaceId, errorsByWorkspaceId, new Set(retryingWorkspaceIds)),
[errorsByWorkspaceId, sessionsByWorkspaceId, workspaces], [errorsByWorkspaceId, retryingWorkspaceIds, sessionsByWorkspaceId, workspaces],
); );
const selectedWorkspace = useMemo( const selectedWorkspace = useMemo(
@@ -650,16 +851,22 @@ export function SessionRoute() {
const mounted = buildOpenworkWorkspaceBaseUrl(baseUrl, selectedWorkspaceId) ?? baseUrl; const mounted = buildOpenworkWorkspaceBaseUrl(baseUrl, selectedWorkspaceId) ?? baseUrl;
return `${mounted.replace(/\/+$|\/+$/g, "")}/opencode`; return `${mounted.replace(/\/+$|\/+$/g, "")}/opencode`;
}, [baseUrl, selectedWorkspaceId]); }, [baseUrl, selectedWorkspaceId]);
const selectedWorkspaceIsLoading = retryingWorkspaceIds.includes(selectedWorkspaceId);
const selectedWorkspaceError = errorsByWorkspaceId[selectedWorkspaceId] ?? null;
const effectiveLoading = loading || selectedWorkspaceIsLoading;
const opencodeClient = useMemo( const opencodeClient = useMemo(
() => () =>
opencodeBaseUrl && token opencodeBaseUrl && token && !effectiveLoading && !selectedWorkspaceError
? createClient(opencodeBaseUrl, selectedWorkspaceRoot || undefined, { ? createClient(opencodeBaseUrl, selectedWorkspaceRoot || undefined, {
token, token,
mode: "openwork", mode: "openwork",
}) })
: null, : null,
[opencodeBaseUrl, selectedWorkspaceRoot, token], [effectiveLoading, opencodeBaseUrl, selectedWorkspaceError, selectedWorkspaceRoot, token],
);
const canCreateTask = Boolean(
opencodeClient && selectedWorkspaceId && !effectiveLoading && !selectedWorkspaceError,
); );
useEffect(() => { useEffect(() => {
@@ -1132,18 +1339,42 @@ export function SessionRoute() {
const handleCreateTaskInWorkspace = useCallback(async (workspaceId: string) => { const handleCreateTaskInWorkspace = useCallback(async (workspaceId: string) => {
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 ||
loading ||
retryingWorkspaceIds.includes(workspaceId) ||
errorsByWorkspaceId[workspaceId]
) {
return;
}
const workspaceOpencodeBaseUrl = `${(buildOpenworkWorkspaceBaseUrl(baseUrl, workspace.id) ?? baseUrl).replace(/\/+$|\/+$/g, "")}/opencode`; const workspaceOpencodeBaseUrl = `${(buildOpenworkWorkspaceBaseUrl(baseUrl, workspace.id) ?? baseUrl).replace(/\/+$|\/+$/g, "")}/opencode`;
const workspaceClient = createClient( const workspaceClient = createClient(
workspaceOpencodeBaseUrl, workspaceOpencodeBaseUrl,
workspace.path?.trim() || undefined, workspace.path?.trim() || undefined,
{ token, mode: "openwork" }, { token, mode: "openwork" },
); );
const session = unwrap( try {
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }), const session = unwrap(
); await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
navigate(`/session/${session.id}`); );
}, [baseUrl, navigate, token, workspaces]); navigate(`/session/${session.id}`);
} catch (error) {
const message = describeRouteError(error);
setRouteError(message);
if (isTransientStartupError(message)) {
setRetryingWorkspaceIds((current) => Array.from(new Set([...current, workspaceId])));
if (startupRetryTimerRef.current === null) {
startupRetryTimerRef.current = window.setTimeout(() => {
startupRetryTimerRef.current = null;
refreshInFlightRef.current = false;
void refreshRouteState();
}, 1_000);
}
}
}
}, [baseUrl, errorsByWorkspaceId, loading, navigate, refreshRouteState, retryingWorkspaceIds, token, workspaces]);
// Global shortcuts: // Global shortcuts:
// Cmd/Ctrl+N -> new task in selected workspace // Cmd/Ctrl+N -> new task in selected workspace
@@ -1165,7 +1396,7 @@ export function SessionRoute() {
const key = event.key?.toLowerCase(); const key = event.key?.toLowerCase();
if (key === "n" && !inEditable) { if (key === "n" && !inEditable) {
event.preventDefault(); event.preventDefault();
if (selectedWorkspaceId) { if (canCreateTask && selectedWorkspaceId) {
void handleCreateTaskInWorkspace(selectedWorkspaceId); void handleCreateTaskInWorkspace(selectedWorkspaceId);
} }
return; return;
@@ -1177,7 +1408,7 @@ export function SessionRoute() {
}; };
window.addEventListener("keydown", handler); window.addEventListener("keydown", handler);
return () => window.removeEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler);
}, [handleCreateTaskInWorkspace, selectedWorkspaceId]); }, [canCreateTask, handleCreateTaskInWorkspace, selectedWorkspaceId]);
const paletteSessionOptions = useMemo<PaletteSessionOption[]>(() => { const paletteSessionOptions = useMemo<PaletteSessionOption[]>(() => {
const out: PaletteSessionOption[] = []; const out: PaletteSessionOption[] = [];
@@ -1290,7 +1521,7 @@ export function SessionRoute() {
return ( return (
<> <>
{selectedWorkspaceId && opencodeBaseUrl && token ? ( {opencodeClient && selectedWorkspaceId && opencodeBaseUrl && token ? (
<ReactSessionRuntime <ReactSessionRuntime
workspaceId={selectedWorkspaceId} workspaceId={selectedWorkspaceId}
sessionId={selectedSessionId} sessionId={selectedSessionId}
@@ -1310,14 +1541,14 @@ export function SessionRoute() {
selectedWorkspaceRoot={selectedWorkspaceRoot} selectedWorkspaceRoot={selectedWorkspaceRoot}
runtimeWorkspaceId={selectedWorkspaceId || null} runtimeWorkspaceId={selectedWorkspaceId || null}
workspaces={workspaces} workspaces={workspaces}
clientConnected={Boolean(opencodeClient)} clientConnected={canCreateTask}
openworkServerStatus={client ? "connected" : "disconnected"} openworkServerStatus={client ? "connected" : "disconnected"}
openworkServerClient={client} openworkServerClient={client}
openworkServerToken={token} openworkServerToken={token}
developerMode={false} developerMode={false}
headerStatus={client ? t("status.connected") : t("status.disconnected_label")} headerStatus={canCreateTask ? t("status.connected") : t("session.loading_detail")}
busyHint={loading ? t("session.loading_detail") : null} busyHint={effectiveLoading ? t("session.loading_detail") : null}
startupPhase={loading ? "nativeInit" : "ready"} startupPhase={effectiveLoading ? "nativeInit" : "ready"}
providerConnectedIds={providerConnectedIds} providerConnectedIds={providerConnectedIds}
providers={providers} providers={providers}
mcpConnectedCount={0} mcpConnectedCount={0}
@@ -1338,9 +1569,9 @@ export function SessionRoute() {
sessionStatusById: {}, sessionStatusById: {},
connectingWorkspaceId: null, connectingWorkspaceId: null,
workspaceConnectionStateById: {}, workspaceConnectionStateById: {},
newTaskDisabled: !Boolean(opencodeClient), newTaskDisabled: !canCreateTask,
sidebarHydratedFromCache: false, sidebarHydratedFromCache: Object.values(sessionsByWorkspaceId).some((list) => list.length > 0),
startupPhase: loading ? "nativeInit" : "ready", startupPhase: effectiveLoading ? "nativeInit" : "ready",
onSelectWorkspace: async (workspaceId) => { onSelectWorkspace: async (workspaceId) => {
if (workspaceId === selectedWorkspaceId) return true; if (workspaceId === selectedWorkspaceId) return true;
setSelectedWorkspaceId(workspaceId); setSelectedWorkspaceId(workspaceId);
@@ -1371,7 +1602,9 @@ export function SessionRoute() {
}, },
onPrefetchSession: () => {}, onPrefetchSession: () => {},
onCreateTaskInWorkspace: async (workspaceId) => { onCreateTaskInWorkspace: async (workspaceId) => {
const workspace = workspaces.find((item) => item.id === workspaceId); void handleCreateTaskInWorkspace(workspaceId);
return;
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( const workspaceClient = createClient(
@@ -1415,7 +1648,7 @@ export function SessionRoute() {
onRedo: () => {}, onRedo: () => {},
}} }}
todos={[] satisfies TodoItem[]} todos={[] satisfies TodoItem[]}
sessionLoadingById={(sessionId) => loading && Boolean(sessionId && sessionId === selectedSessionId)} sessionLoadingById={(sessionId) => effectiveLoading && Boolean(sessionId && sessionId === selectedSessionId)}
shareWorkspaceModal={ shareWorkspaceModal={
shareWorkspaceState.shareWorkspaceOpen shareWorkspaceState.shareWorkspaceOpen
? { ? {
@@ -1509,6 +1742,13 @@ export function SessionRoute() {
} }
: undefined : undefined
} }
statusBar={effectiveLoading ? {
statusLabel: "Preparing workspace",
statusDetail: t("session.loading_detail"),
statusDotClass: "bg-amber-9",
statusPingClass: "bg-amber-9/35 animate-ping",
statusPulse: true,
} : undefined}
/> />
<CreateWorkspaceModal <CreateWorkspaceModal
open={createWorkspaceOpen} open={createWorkspaceOpen}

View File

@@ -21,7 +21,6 @@ import { createOpenworkServerStore, useOpenworkServerStoreSnapshot } from "../do
import { createProviderAuthStore, useProviderAuthStoreSnapshot } from "../domains/connections/provider-auth/store"; import { createProviderAuthStore, useProviderAuthStoreSnapshot } from "../domains/connections/provider-auth/store";
import ProviderAuthModal from "../domains/connections/provider-auth/provider-auth-modal"; import ProviderAuthModal from "../domains/connections/provider-auth/provider-auth-modal";
import ConnectionsModals from "../domains/connections/modals"; import ConnectionsModals from "../domains/connections/modals";
import { ReloadWorkspaceToast } from "../domains/shell-feedback/reload-workspace-toast";
import { GeneralSettingsView } from "../domains/settings/pages/general-view"; import { GeneralSettingsView } from "../domains/settings/pages/general-view";
import { AdvancedView } from "../domains/settings/pages/advanced-view"; import { AdvancedView } from "../domains/settings/pages/advanced-view";
import { AppearanceView } from "../domains/settings/pages/appearance-view"; import { AppearanceView } from "../domains/settings/pages/appearance-view";
@@ -39,7 +38,6 @@ import { SettingsShell } from "../domains/settings/shell/settings-shell";
import { createAutomationsStore, useAutomationsStoreSnapshot } from "../domains/settings/state/automations-store"; import { createAutomationsStore, useAutomationsStoreSnapshot } from "../domains/settings/state/automations-store";
import { createExtensionsStore, useExtensionsStoreSnapshot } from "../domains/settings/state/extensions-store"; import { createExtensionsStore, useExtensionsStoreSnapshot } from "../domains/settings/state/extensions-store";
import { usePlatform } from "../kernel/platform"; import { usePlatform } from "../kernel/platform";
import { useSystemState } from "../kernel/system-state";
import { useLocal } from "../kernel/local-provider"; import { useLocal } from "../kernel/local-provider";
import { import {
DEFAULT_WORKSPACE_LEFT_SIDEBAR_WIDTH, DEFAULT_WORKSPACE_LEFT_SIDEBAR_WIDTH,
@@ -68,6 +66,7 @@ import type { ModelOption, ModelRef } from "../../app/types";
import { recordInspectorEvent } from "./app-inspector"; import { recordInspectorEvent } from "./app-inspector";
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork"; import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
import { abortSessionSafe } from "../../app/lib/opencode-session"; import { abortSessionSafe } from "../../app/lib/opencode-session";
import { useReloadCoordinator } from "./reload-coordinator";
type RouteWorkspace = OpenworkWorkspaceInfo & { type RouteWorkspace = OpenworkWorkspaceInfo & {
displayNameResolved: string; displayNameResolved: string;
@@ -139,6 +138,35 @@ function mergeRouteWorkspaces(
return [...mergedServer, ...missingDesktop]; return [...mergedServer, ...missingDesktop];
} }
function reconcileSelectedWorkspaceId(
currentId: string,
serverList: { activeId?: string | null },
desktopList: Awaited<ReturnType<typeof workspaceBootstrap>> | null,
workspaces: RouteWorkspace[],
) {
const current = currentId.trim();
const serverIds = new Set(workspaces.map((workspace) => workspace.id));
if (current && serverIds.has(current)) return current;
const desktopSelectedId = resolveWorkspaceListSelectedId(desktopList);
const desktopSelected = desktopSelectedId
? desktopList?.workspaces?.find((workspace) => workspace.id === desktopSelectedId)
: null;
const currentDesktop = current
? desktopList?.workspaces?.find((workspace) => workspace.id === current)
: null;
const selectedPath = normalizeDirectoryPath((currentDesktop ?? desktopSelected)?.path ?? "");
if (selectedPath) {
const pathMatch = workspaces.find(
(workspace) => normalizeDirectoryPath(workspace.path ?? "") === selectedPath,
);
if (pathMatch) return pathMatch.id;
}
return serverList.activeId?.trim() || desktopSelectedId || workspaces[0]?.id || "";
}
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);
@@ -287,6 +315,7 @@ export function SettingsRoute() {
const local = useLocal(); const local = useLocal();
const platform = usePlatform(); const platform = usePlatform();
const checkDesktopRestriction = useCheckDesktopRestriction(); const checkDesktopRestriction = useCheckDesktopRestriction();
const reloadCoordinator = useReloadCoordinator();
const route = parseSettingsPath(location.pathname); const route = parseSettingsPath(location.pathname);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -419,21 +448,25 @@ export function SettingsRoute() {
return true; return true;
}, [openworkClient, selectedWorkspaceId]); }, [openworkClient, selectedWorkspaceId]);
const systemState = useSystemState({ useEffect(() => {
hasActiveRuns: () => activeReloadBlockingSessions.length > 0, return reloadCoordinator.registerWorkspaceReloadControls({
reloadWorkspaceEngine: reloadWorkspaceEngineFromUi, canReloadWorkspaceEngine: () => Boolean(openworkClient && (selectedWorkspace?.id || selectedWorkspaceId)),
canReloadWorkspaceEngine: () => Boolean(openworkClient && (selectedWorkspace?.id || selectedWorkspaceId)), reloadWorkspaceEngine: reloadWorkspaceEngineFromUi,
setError: setRouteError, activeSessions: () => activeReloadBlockingSessions,
}); stopSession: async (sessionId) => {
if (!activeClient) return;
const forceStopActiveSessionsAndReload = useCallback(async () => { await abortSessionSafe(activeClient, sessionId);
if (activeClient) { },
for (const session of activeReloadBlockingSessions) { });
await abortSessionSafe(activeClient, session.id).catch(() => undefined); }, [
} activeClient,
} activeReloadBlockingSessions,
await systemState.reloadWorkspaceEngine(); openworkClient,
}, [activeClient, activeReloadBlockingSessions, systemState.reloadWorkspaceEngine]); reloadCoordinator,
reloadWorkspaceEngineFromUi,
selectedWorkspace?.id,
selectedWorkspaceId,
]);
const shellLayout = useWorkspaceShellLayout({ const shellLayout = useWorkspaceShellLayout({
expandedRightWidth: 320, expandedRightWidth: 320,
@@ -474,9 +507,9 @@ export function SettingsRoute() {
openworkServer: openworkServerStore, openworkServer: openworkServerStore,
runtimeWorkspaceId: () => routeStateRef.current.runtimeWorkspaceId, runtimeWorkspaceId: () => routeStateRef.current.runtimeWorkspaceId,
developerMode: () => routeStateRef.current.developerMode, developerMode: () => routeStateRef.current.developerMode,
markReloadRequired: systemState.markReloadRequired, markReloadRequired: reloadCoordinator.markReloadRequired,
}), }),
[openworkServerStore, systemState.markReloadRequired], [openworkServerStore, reloadCoordinator.markReloadRequired],
); );
const providerAuthStore = useMemo( const providerAuthStore = useMemo(
() => () =>
@@ -496,14 +529,14 @@ export function SettingsRoute() {
setDisabledProviders, setDisabledProviders,
markOpencodeConfigReloadRequired: () => { markOpencodeConfigReloadRequired: () => {
setConfigActionStatus(t("settings.config_updated")); setConfigActionStatus(t("settings.config_updated"));
systemState.markReloadRequired("config", { reloadCoordinator.markReloadRequired("config", {
type: "config", type: "config",
name: "opencode.json", name: "opencode.json",
action: "updated", action: "updated",
}); });
}, },
}), }),
[openworkServerStore, systemState.markReloadRequired], [openworkServerStore, reloadCoordinator.markReloadRequired],
); );
const extensionsStore = useMemo( const extensionsStore = useMemo(
() => () =>
@@ -519,9 +552,9 @@ export function SettingsRoute() {
setBusyLabel, setBusyLabel,
setBusyStartedAt: () => {}, setBusyStartedAt: () => {},
setError: setRouteError, setError: setRouteError,
markReloadRequired: systemState.markReloadRequired, markReloadRequired: reloadCoordinator.markReloadRequired,
}), }),
[openworkServerStore, systemState.markReloadRequired], [openworkServerStore, reloadCoordinator.markReloadRequired],
); );
const automationsStore = useMemo( const automationsStore = useMemo(
() => () =>
@@ -710,7 +743,9 @@ export function SettingsRoute() {
setWorkspaces(nextWorkspaces); setWorkspaces(nextWorkspaces);
setSessionsByWorkspaceId(Object.fromEntries(sessionEntries.map((entry) => [entry.workspaceId, entry.sessions]))); setSessionsByWorkspaceId(Object.fromEntries(sessionEntries.map((entry) => [entry.workspaceId, entry.sessions])));
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) =>
reconcileSelectedWorkspaceId(current, list, desktopList, nextWorkspaces),
);
} catch (error) { } catch (error) {
const message = describeRouteError(error); const message = describeRouteError(error);
console.error("[settings-route] refreshRouteState failed", error); console.error("[settings-route] refreshRouteState failed", error);
@@ -1004,9 +1039,9 @@ export function SettingsRoute() {
addPlugin={async () => { addPlugin={async () => {
setRouteError("Scheduler plugin install is not wired into the React settings route yet."); setRouteError("Scheduler plugin install is not wired into the React settings route yet.");
}} }}
reloadWorkspaceEngine={systemState.reloadWorkspaceEngine} reloadWorkspaceEngine={reloadCoordinator.reloadWorkspaceEngine}
reloadBusy={systemState.reload.reloadBusy} reloadBusy={false}
canReloadWorkspace={systemState.canReloadWorkspaceEngine} canReloadWorkspace={reloadCoordinator.canReloadWorkspaceEngine}
openLink={(url) => platform.openLink(url)} openLink={(url) => platform.openLink(url)}
/> />
); );
@@ -1130,10 +1165,10 @@ export function SettingsRoute() {
updateOpenworkServerSettings: openworkServerStore.updateOpenworkServerSettings, updateOpenworkServerSettings: openworkServerStore.updateOpenworkServerSettings,
resetOpenworkServerSettings: openworkServerStore.resetOpenworkServerSettings, resetOpenworkServerSettings: openworkServerStore.resetOpenworkServerSettings,
testOpenworkServerConnection: openworkServerStore.testOpenworkServerConnection, testOpenworkServerConnection: openworkServerStore.testOpenworkServerConnection,
canReloadWorkspace: systemState.canReloadWorkspaceEngine, canReloadWorkspace: reloadCoordinator.canReloadWorkspaceEngine,
reloadWorkspaceEngine: systemState.reloadWorkspaceEngine, reloadWorkspaceEngine: reloadCoordinator.reloadWorkspaceEngine,
reloadBusy: systemState.reload.reloadBusy, reloadBusy: false,
reloadError: systemState.reload.reloadError ?? routeError, reloadError: routeError,
developerMode, developerMode,
}} }}
/> />
@@ -1299,32 +1334,6 @@ export function SettingsRoute() {
remoteSubmitting={createWorkspaceRemoteBusy} remoteSubmitting={createWorkspaceRemoteBusy}
remoteError={createWorkspaceRemoteError} remoteError={createWorkspaceRemoteError}
/> />
<div className="pointer-events-none fixed right-4 top-4 z-50 w-[min(24rem,calc(100vw-1.5rem))] max-w-full sm:right-6 sm:top-6">
<div className="pointer-events-auto">
<ReloadWorkspaceToast
open={systemState.reload.reloadPending}
title={systemState.reloadCopy.title}
description={systemState.reloadCopy.body}
trigger={systemState.reload.reloadTrigger}
error={systemState.reload.reloadError}
reloadLabel={
activeReloadBlockingSessions.length > 0
? t("app.reload_stop_tasks")
: t("app.reload_now")
}
dismissLabel={t("app.reload_later")}
busy={systemState.reload.reloadBusy}
canReload={systemState.canReloadWorkspaceEngine}
hasActiveRuns={activeReloadBlockingSessions.length > 0}
onReload={() => {
void (activeReloadBlockingSessions.length > 0
? forceStopActiveSessionsAndReload()
: systemState.reloadWorkspaceEngine());
}}
onDismiss={systemState.clearReloadRequired}
/>
</div>
</div>
<ConnectionsModals <ConnectionsModals
client={activeClient} client={activeClient}
projectDir={selectedWorkspaceRoot} projectDir={selectedWorkspaceRoot}
@@ -1336,7 +1345,7 @@ export function SettingsRoute() {
if (!activeClient) return undefined; if (!activeClient) return undefined;
return abortSessionSafe(activeClient, sessionId); return abortSessionSafe(activeClient, sessionId);
}} }}
onReloadEngine={systemState.reloadWorkspaceEngine} onReloadEngine={reloadCoordinator.reloadWorkspaceEngine}
modalState={{ modalState={{
mcpAuthModalOpen: connectionsSnapshot.mcpAuthModalOpen, mcpAuthModalOpen: connectionsSnapshot.mcpAuthModalOpen,
mcpAuthEntry: connectionsSnapshot.mcpAuthEntry, mcpAuthEntry: connectionsSnapshot.mcpAuthEntry,

View File

@@ -486,23 +486,30 @@ async function fetchOpencodeJson(
const url = new URL(baseUrl); const url = new URL(baseUrl);
url.pathname = path.startsWith("/") ? path : `/${path}`; url.pathname = path.startsWith("/") ? path : `/${path}`;
const directory = resolveOpencodeDirectory(workspace);
if (init.query instanceof URLSearchParams) { if (init.query instanceof URLSearchParams) {
url.search = init.query.toString(); const params = new URLSearchParams(init.query);
if (directory && !params.has("directory")) {
params.set("directory", directory);
}
url.search = params.toString();
} else if (init.query) { } else if (init.query) {
const params = new URLSearchParams(); const params = new URLSearchParams();
for (const [key, value] of Object.entries(init.query)) { for (const [key, value] of Object.entries(init.query)) {
if (value === undefined || value === null) continue; if (value === undefined || value === null) continue;
params.set(key, String(value)); params.set(key, String(value));
} }
if (directory && !params.has("directory")) {
params.set("directory", directory);
}
url.search = params.toString(); url.search = params.toString();
} else { } else {
url.search = ""; url.search = directory ? new URLSearchParams({ directory }).toString() : "";
} }
const headers = new Headers(); const headers = new Headers();
headers.set("Content-Type", "application/json"); headers.set("Content-Type", "application/json");
const directory = resolveOpencodeDirectory(workspace);
if (directory) { if (directory) {
headers.set("x-opencode-directory", directory); headers.set("x-opencode-directory", directory);
} }