mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
fix React startup and reload flows
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
usesChromeDevtoolsAutoConnect,
|
||||
validateMcpServerName,
|
||||
} from "../../../app/mcp";
|
||||
import { buildOpenworkWorkspaceBaseUrl } from "../../../app/lib/openwork-server";
|
||||
import type {
|
||||
Client,
|
||||
McpServerEntry,
|
||||
@@ -171,7 +172,9 @@ export function createConnectionsStore(options: {
|
||||
return null;
|
||||
}
|
||||
|
||||
activeClient = createClient(`${openworkBaseUrl.replace(/\/+$/, "")}/opencode`, undefined, {
|
||||
const mountedBaseUrl =
|
||||
buildOpenworkWorkspaceBaseUrl(openworkBaseUrl, options.runtimeWorkspaceId()) ?? openworkBaseUrl;
|
||||
activeClient = createClient(`${mountedBaseUrl.replace(/\/+$/, "")}/opencode`, undefined, {
|
||||
token,
|
||||
mode: "openwork",
|
||||
});
|
||||
@@ -530,30 +533,40 @@ export function createConnectionsStore(options: {
|
||||
}
|
||||
}
|
||||
|
||||
const mcpAddConfig =
|
||||
entryType === "remote"
|
||||
? {
|
||||
type: "remote" as const,
|
||||
url: entry.url!,
|
||||
enabled: true,
|
||||
...(entry.oauth ? { oauth: {} } : {}),
|
||||
}
|
||||
: {
|
||||
type: "local" as const,
|
||||
command: entry.command!,
|
||||
enabled: true,
|
||||
...(mcpEnvironment ? { environment: mcpEnvironment } : {}),
|
||||
};
|
||||
if (canUseOpenworkServer && openworkClient && openworkWorkspaceId) {
|
||||
// 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
|
||||
// hot-add endpoint here: when the SDK client is rooted at the aggregate
|
||||
// `/opencode` route it can resolve to an internal `local_*` workspace
|
||||
// id that the OpenWork server does not expose, producing a confusing
|
||||
// `workspace_not_found` after the config write already succeeded.
|
||||
setStateField("mcpStatuses", filterConfiguredStatuses(snapshot.mcpStatuses, snapshot.mcpServers));
|
||||
} else {
|
||||
const mcpAddConfig =
|
||||
entryType === "remote"
|
||||
? {
|
||||
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(
|
||||
await activeClient.mcp.add({
|
||||
directory: resolvedProjectDir,
|
||||
name: slug,
|
||||
config: mcpAddConfig,
|
||||
}),
|
||||
);
|
||||
const status = unwrap(
|
||||
await activeClient.mcp.add({
|
||||
directory: resolvedProjectDir,
|
||||
name: slug,
|
||||
config: mcpAddConfig,
|
||||
}),
|
||||
);
|
||||
|
||||
setStateField("mcpStatuses", status as McpStatusMap);
|
||||
setStateField("mcpStatuses", status as McpStatusMap);
|
||||
}
|
||||
options.markReloadRequired?.("mcp", { type: "mcp", name: slug, action });
|
||||
await refreshMcpServers();
|
||||
|
||||
|
||||
@@ -143,6 +143,14 @@ export function ServerProvider({ children, defaultUrl }: ServerProviderProps) {
|
||||
|
||||
useEffect(() => {
|
||||
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);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
@@ -49,7 +49,7 @@ const PHASE_MESSAGES: Record<BootPhaseId, string> = {
|
||||
idle: "",
|
||||
"bootstrapping-workspaces": "Loading your workspaces",
|
||||
"starting-openwork-server": "Starting the OpenWork server",
|
||||
"starting-engine": "Starting OpenCode",
|
||||
"starting-engine": "Preparing workspace",
|
||||
"activating-workspace": "Activating your workspace",
|
||||
ready: "Ready",
|
||||
error: "Something went wrong",
|
||||
|
||||
@@ -130,7 +130,7 @@ export function useDesktopRuntimeBoot() {
|
||||
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, {
|
||||
runtime: "openwork-orchestrator",
|
||||
workspacePaths,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { BootStateProvider } from "./boot-state";
|
||||
import { DesktopRuntimeBoot } from "./desktop-runtime-boot";
|
||||
import { startDebugLogger, stopDebugLogger } from "./debug-logger";
|
||||
import { MigrationPrompt } from "./migration-prompt";
|
||||
import { ReloadCoordinatorProvider } from "./reload-coordinator";
|
||||
|
||||
function resolveDefaultServerUrl(): string {
|
||||
if (isDesktopRuntime()) return "http://127.0.0.1:4096";
|
||||
@@ -63,7 +64,9 @@ export function AppProviders({ children }: AppProvidersProps) {
|
||||
<DenAuthProvider>
|
||||
<DesktopConfigProvider>
|
||||
<RestrictionNoticeProvider>
|
||||
<LocalProvider>{children}</LocalProvider>
|
||||
<LocalProvider>
|
||||
<ReloadCoordinatorProvider>{children}</ReloadCoordinatorProvider>
|
||||
</LocalProvider>
|
||||
</RestrictionNoticeProvider>
|
||||
</DesktopConfigProvider>
|
||||
</DenAuthProvider>
|
||||
|
||||
149
apps/app/src/react-app/shell/reload-coordinator.tsx
Normal file
149
apps/app/src/react-app/shell/reload-coordinator.tsx
Normal 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;
|
||||
}
|
||||
@@ -85,6 +85,7 @@ import { useReactRenderWatchdog } from "./react-render-watchdog";
|
||||
import { getModelBehaviorSummary } from "../../app/lib/model-behavior";
|
||||
import { filterProviderList, mapConfigProvidersToList } from "../../app/utils/providers";
|
||||
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
||||
import { useReloadCoordinator } from "./reload-coordinator";
|
||||
|
||||
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
||||
displayNameResolved: string;
|
||||
@@ -133,6 +134,44 @@ async function resolveRouteOpenworkConnection() {
|
||||
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) {
|
||||
return (
|
||||
workspace.displayName?.trim() ||
|
||||
@@ -202,11 +241,16 @@ function toSessionGroups(
|
||||
workspaces: RouteWorkspace[],
|
||||
sessionsByWorkspaceId: Record<string, any[]>,
|
||||
errorsByWorkspaceId: Record<string, string | null>,
|
||||
loadingWorkspaceIds: Set<string>,
|
||||
): WorkspaceSessionGroup[] {
|
||||
return workspaces.map((workspace) => ({
|
||||
workspace,
|
||||
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],
|
||||
}));
|
||||
}
|
||||
@@ -280,6 +324,7 @@ export function SessionRoute() {
|
||||
const navigate = useNavigate();
|
||||
const platform = usePlatform();
|
||||
const local = useLocal();
|
||||
const reloadCoordinator = useReloadCoordinator();
|
||||
const checkDesktopRestriction = useCheckDesktopRestriction();
|
||||
const restrictionNotice = useRestrictionNotice();
|
||||
const params = useParams<{ sessionId?: string }>();
|
||||
@@ -299,7 +344,11 @@ export function SessionRoute() {
|
||||
// One-way latch for "a refreshRouteState is currently running"; prevents
|
||||
// overlapping route refreshes from queueing up when the user clicks fast.
|
||||
const refreshInFlightRef = useRef(false);
|
||||
const reloadEventCursorByWorkspaceRef = useRef<Record<string, number | null>>({});
|
||||
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 [createWorkspaceBusy, setCreateWorkspaceBusy] = useState(false);
|
||||
const [createWorkspaceRemoteBusy, setCreateWorkspaceRemoteBusy] = useState(false);
|
||||
@@ -362,6 +411,7 @@ export function SessionRoute() {
|
||||
setRouteError(null);
|
||||
let desktopList = null as Awaited<ReturnType<typeof workspaceBootstrap>> | null;
|
||||
let desktopWorkspaces = workspacesRef.current;
|
||||
let routeReadyAfterRefresh = true;
|
||||
try {
|
||||
if (isDesktopRuntime()) {
|
||||
try {
|
||||
@@ -402,6 +452,27 @@ export function SessionRoute() {
|
||||
const sessionEntries = await Promise.all(
|
||||
nextWorkspaces.map(async (workspace) => {
|
||||
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 });
|
||||
// The underlying opencode instance is shared across all local
|
||||
// workspaces attached to the same openwork-server, so `listSessions`
|
||||
@@ -414,16 +485,41 @@ export function SessionRoute() {
|
||||
normalizeDirectoryPath(session?.directory ?? "") === workspaceRoot,
|
||||
)
|
||||
: (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) {
|
||||
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 {
|
||||
workspaceId: workspace.id,
|
||||
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),
|
||||
// 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
|
||||
// the overlay dismisses after BOTH the desktop boot and the workspace
|
||||
// list/sessions are ready.
|
||||
markBootRouteReady();
|
||||
if (routeReadyAfterRefresh) {
|
||||
markBootRouteReady();
|
||||
}
|
||||
}
|
||||
}, [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(() => {
|
||||
workspacesRef.current = workspaces;
|
||||
}, [workspaces]);
|
||||
|
||||
useEffect(() => {
|
||||
sessionsByWorkspaceIdRef.current = sessionsByWorkspaceId;
|
||||
}, [sessionsByWorkspaceId]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
|
||||
@@ -522,6 +716,10 @@ export function SessionRoute() {
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
if (startupRetryTimerRef.current !== null) {
|
||||
window.clearTimeout(startupRetryTimerRef.current);
|
||||
startupRetryTimerRef.current = null;
|
||||
}
|
||||
window.removeEventListener("openwork-server-settings-changed", handleSettingsChange);
|
||||
if (typeof document !== "undefined") {
|
||||
document.removeEventListener("visibilitychange", handleVisibility);
|
||||
@@ -551,6 +749,7 @@ export function SessionRoute() {
|
||||
useEffect(() => {
|
||||
const dispose = publishInspectorSlice("route", () => ({
|
||||
loading,
|
||||
retryingWorkspaceIds,
|
||||
baseUrl,
|
||||
tokenPresent: token.length > 0,
|
||||
connected: Boolean(client),
|
||||
@@ -564,6 +763,7 @@ export function SessionRoute() {
|
||||
workspaceType: workspace.workspaceType,
|
||||
path: workspace.path,
|
||||
sessionCount: (sessionsByWorkspaceId[workspace.id] ?? []).length,
|
||||
loading: retryingWorkspaceIds.includes(workspace.id),
|
||||
error: errorsByWorkspaceId[workspace.id] ?? null,
|
||||
})),
|
||||
sessionsByWorkspaceId: Object.fromEntries(
|
||||
@@ -583,6 +783,7 @@ export function SessionRoute() {
|
||||
client,
|
||||
errorsByWorkspaceId,
|
||||
loading,
|
||||
retryingWorkspaceIds,
|
||||
selectedSessionId,
|
||||
selectedWorkspaceId,
|
||||
routeError,
|
||||
@@ -613,8 +814,8 @@ export function SessionRoute() {
|
||||
// the onboarding flow, not from the route effect loop.
|
||||
|
||||
const workspaceSessionGroups = useMemo(
|
||||
() => toSessionGroups(workspaces, sessionsByWorkspaceId, errorsByWorkspaceId),
|
||||
[errorsByWorkspaceId, sessionsByWorkspaceId, workspaces],
|
||||
() => toSessionGroups(workspaces, sessionsByWorkspaceId, errorsByWorkspaceId, new Set(retryingWorkspaceIds)),
|
||||
[errorsByWorkspaceId, retryingWorkspaceIds, sessionsByWorkspaceId, workspaces],
|
||||
);
|
||||
|
||||
const selectedWorkspace = useMemo(
|
||||
@@ -650,16 +851,22 @@ export function SessionRoute() {
|
||||
const mounted = buildOpenworkWorkspaceBaseUrl(baseUrl, selectedWorkspaceId) ?? baseUrl;
|
||||
return `${mounted.replace(/\/+$|\/+$/g, "")}/opencode`;
|
||||
}, [baseUrl, selectedWorkspaceId]);
|
||||
const selectedWorkspaceIsLoading = retryingWorkspaceIds.includes(selectedWorkspaceId);
|
||||
const selectedWorkspaceError = errorsByWorkspaceId[selectedWorkspaceId] ?? null;
|
||||
const effectiveLoading = loading || selectedWorkspaceIsLoading;
|
||||
|
||||
const opencodeClient = useMemo(
|
||||
() =>
|
||||
opencodeBaseUrl && token
|
||||
opencodeBaseUrl && token && !effectiveLoading && !selectedWorkspaceError
|
||||
? createClient(opencodeBaseUrl, selectedWorkspaceRoot || undefined, {
|
||||
token,
|
||||
mode: "openwork",
|
||||
})
|
||||
: null,
|
||||
[opencodeBaseUrl, selectedWorkspaceRoot, token],
|
||||
[effectiveLoading, opencodeBaseUrl, selectedWorkspaceError, selectedWorkspaceRoot, token],
|
||||
);
|
||||
const canCreateTask = Boolean(
|
||||
opencodeClient && selectedWorkspaceId && !effectiveLoading && !selectedWorkspaceError,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1132,18 +1339,42 @@ export function SessionRoute() {
|
||||
|
||||
const handleCreateTaskInWorkspace = useCallback(async (workspaceId: string) => {
|
||||
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 workspaceClient = createClient(
|
||||
workspaceOpencodeBaseUrl,
|
||||
workspace.path?.trim() || undefined,
|
||||
{ token, mode: "openwork" },
|
||||
);
|
||||
const session = unwrap(
|
||||
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
|
||||
);
|
||||
navigate(`/session/${session.id}`);
|
||||
}, [baseUrl, navigate, token, workspaces]);
|
||||
try {
|
||||
const session = unwrap(
|
||||
await workspaceClient.session.create({ directory: workspace.path?.trim() || undefined }),
|
||||
);
|
||||
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:
|
||||
// Cmd/Ctrl+N -> new task in selected workspace
|
||||
@@ -1165,7 +1396,7 @@ export function SessionRoute() {
|
||||
const key = event.key?.toLowerCase();
|
||||
if (key === "n" && !inEditable) {
|
||||
event.preventDefault();
|
||||
if (selectedWorkspaceId) {
|
||||
if (canCreateTask && selectedWorkspaceId) {
|
||||
void handleCreateTaskInWorkspace(selectedWorkspaceId);
|
||||
}
|
||||
return;
|
||||
@@ -1177,7 +1408,7 @@ export function SessionRoute() {
|
||||
};
|
||||
window.addEventListener("keydown", handler);
|
||||
return () => window.removeEventListener("keydown", handler);
|
||||
}, [handleCreateTaskInWorkspace, selectedWorkspaceId]);
|
||||
}, [canCreateTask, handleCreateTaskInWorkspace, selectedWorkspaceId]);
|
||||
|
||||
const paletteSessionOptions = useMemo<PaletteSessionOption[]>(() => {
|
||||
const out: PaletteSessionOption[] = [];
|
||||
@@ -1290,7 +1521,7 @@ export function SessionRoute() {
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedWorkspaceId && opencodeBaseUrl && token ? (
|
||||
{opencodeClient && selectedWorkspaceId && opencodeBaseUrl && token ? (
|
||||
<ReactSessionRuntime
|
||||
workspaceId={selectedWorkspaceId}
|
||||
sessionId={selectedSessionId}
|
||||
@@ -1310,14 +1541,14 @@ export function SessionRoute() {
|
||||
selectedWorkspaceRoot={selectedWorkspaceRoot}
|
||||
runtimeWorkspaceId={selectedWorkspaceId || null}
|
||||
workspaces={workspaces}
|
||||
clientConnected={Boolean(opencodeClient)}
|
||||
clientConnected={canCreateTask}
|
||||
openworkServerStatus={client ? "connected" : "disconnected"}
|
||||
openworkServerClient={client}
|
||||
openworkServerToken={token}
|
||||
developerMode={false}
|
||||
headerStatus={client ? t("status.connected") : t("status.disconnected_label")}
|
||||
busyHint={loading ? t("session.loading_detail") : null}
|
||||
startupPhase={loading ? "nativeInit" : "ready"}
|
||||
headerStatus={canCreateTask ? t("status.connected") : t("session.loading_detail")}
|
||||
busyHint={effectiveLoading ? t("session.loading_detail") : null}
|
||||
startupPhase={effectiveLoading ? "nativeInit" : "ready"}
|
||||
providerConnectedIds={providerConnectedIds}
|
||||
providers={providers}
|
||||
mcpConnectedCount={0}
|
||||
@@ -1338,9 +1569,9 @@ export function SessionRoute() {
|
||||
sessionStatusById: {},
|
||||
connectingWorkspaceId: null,
|
||||
workspaceConnectionStateById: {},
|
||||
newTaskDisabled: !Boolean(opencodeClient),
|
||||
sidebarHydratedFromCache: false,
|
||||
startupPhase: loading ? "nativeInit" : "ready",
|
||||
newTaskDisabled: !canCreateTask,
|
||||
sidebarHydratedFromCache: Object.values(sessionsByWorkspaceId).some((list) => list.length > 0),
|
||||
startupPhase: effectiveLoading ? "nativeInit" : "ready",
|
||||
onSelectWorkspace: async (workspaceId) => {
|
||||
if (workspaceId === selectedWorkspaceId) return true;
|
||||
setSelectedWorkspaceId(workspaceId);
|
||||
@@ -1371,7 +1602,9 @@ export function SessionRoute() {
|
||||
},
|
||||
onPrefetchSession: () => {},
|
||||
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;
|
||||
const workspaceOpencodeBaseUrl = `${(buildOpenworkWorkspaceBaseUrl(baseUrl, workspace.id) ?? baseUrl).replace(/\/+$|\/+$/g, "")}/opencode`;
|
||||
const workspaceClient = createClient(
|
||||
@@ -1415,7 +1648,7 @@ export function SessionRoute() {
|
||||
onRedo: () => {},
|
||||
}}
|
||||
todos={[] satisfies TodoItem[]}
|
||||
sessionLoadingById={(sessionId) => loading && Boolean(sessionId && sessionId === selectedSessionId)}
|
||||
sessionLoadingById={(sessionId) => effectiveLoading && Boolean(sessionId && sessionId === selectedSessionId)}
|
||||
shareWorkspaceModal={
|
||||
shareWorkspaceState.shareWorkspaceOpen
|
||||
? {
|
||||
@@ -1509,6 +1742,13 @@ export function SessionRoute() {
|
||||
}
|
||||
: 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
|
||||
open={createWorkspaceOpen}
|
||||
|
||||
@@ -21,7 +21,6 @@ import { createOpenworkServerStore, useOpenworkServerStoreSnapshot } from "../do
|
||||
import { createProviderAuthStore, useProviderAuthStoreSnapshot } from "../domains/connections/provider-auth/store";
|
||||
import ProviderAuthModal from "../domains/connections/provider-auth/provider-auth-modal";
|
||||
import ConnectionsModals from "../domains/connections/modals";
|
||||
import { ReloadWorkspaceToast } from "../domains/shell-feedback/reload-workspace-toast";
|
||||
import { GeneralSettingsView } from "../domains/settings/pages/general-view";
|
||||
import { AdvancedView } from "../domains/settings/pages/advanced-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 { createExtensionsStore, useExtensionsStoreSnapshot } from "../domains/settings/state/extensions-store";
|
||||
import { usePlatform } from "../kernel/platform";
|
||||
import { useSystemState } from "../kernel/system-state";
|
||||
import { useLocal } from "../kernel/local-provider";
|
||||
import {
|
||||
DEFAULT_WORKSPACE_LEFT_SIDEBAR_WIDTH,
|
||||
@@ -68,6 +66,7 @@ import type { ModelOption, ModelRef } from "../../app/types";
|
||||
import { recordInspectorEvent } from "./app-inspector";
|
||||
import { ensureDesktopLocalOpenworkConnection } from "./desktop-local-openwork";
|
||||
import { abortSessionSafe } from "../../app/lib/opencode-session";
|
||||
import { useReloadCoordinator } from "./reload-coordinator";
|
||||
|
||||
type RouteWorkspace = OpenworkWorkspaceInfo & {
|
||||
displayNameResolved: string;
|
||||
@@ -139,6 +138,35 @@ function mergeRouteWorkspaces(
|
||||
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) {
|
||||
const normalized = path.replace(/\\/g, "/").replace(/\/+$/, "");
|
||||
const parts = normalized.split("/").filter(Boolean);
|
||||
@@ -287,6 +315,7 @@ export function SettingsRoute() {
|
||||
const local = useLocal();
|
||||
const platform = usePlatform();
|
||||
const checkDesktopRestriction = useCheckDesktopRestriction();
|
||||
const reloadCoordinator = useReloadCoordinator();
|
||||
const route = parseSettingsPath(location.pathname);
|
||||
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -419,21 +448,25 @@ export function SettingsRoute() {
|
||||
return true;
|
||||
}, [openworkClient, selectedWorkspaceId]);
|
||||
|
||||
const systemState = useSystemState({
|
||||
hasActiveRuns: () => activeReloadBlockingSessions.length > 0,
|
||||
reloadWorkspaceEngine: reloadWorkspaceEngineFromUi,
|
||||
canReloadWorkspaceEngine: () => Boolean(openworkClient && (selectedWorkspace?.id || selectedWorkspaceId)),
|
||||
setError: setRouteError,
|
||||
});
|
||||
|
||||
const forceStopActiveSessionsAndReload = useCallback(async () => {
|
||||
if (activeClient) {
|
||||
for (const session of activeReloadBlockingSessions) {
|
||||
await abortSessionSafe(activeClient, session.id).catch(() => undefined);
|
||||
}
|
||||
}
|
||||
await systemState.reloadWorkspaceEngine();
|
||||
}, [activeClient, activeReloadBlockingSessions, systemState.reloadWorkspaceEngine]);
|
||||
useEffect(() => {
|
||||
return reloadCoordinator.registerWorkspaceReloadControls({
|
||||
canReloadWorkspaceEngine: () => Boolean(openworkClient && (selectedWorkspace?.id || selectedWorkspaceId)),
|
||||
reloadWorkspaceEngine: reloadWorkspaceEngineFromUi,
|
||||
activeSessions: () => activeReloadBlockingSessions,
|
||||
stopSession: async (sessionId) => {
|
||||
if (!activeClient) return;
|
||||
await abortSessionSafe(activeClient, sessionId);
|
||||
},
|
||||
});
|
||||
}, [
|
||||
activeClient,
|
||||
activeReloadBlockingSessions,
|
||||
openworkClient,
|
||||
reloadCoordinator,
|
||||
reloadWorkspaceEngineFromUi,
|
||||
selectedWorkspace?.id,
|
||||
selectedWorkspaceId,
|
||||
]);
|
||||
|
||||
const shellLayout = useWorkspaceShellLayout({
|
||||
expandedRightWidth: 320,
|
||||
@@ -474,9 +507,9 @@ export function SettingsRoute() {
|
||||
openworkServer: openworkServerStore,
|
||||
runtimeWorkspaceId: () => routeStateRef.current.runtimeWorkspaceId,
|
||||
developerMode: () => routeStateRef.current.developerMode,
|
||||
markReloadRequired: systemState.markReloadRequired,
|
||||
markReloadRequired: reloadCoordinator.markReloadRequired,
|
||||
}),
|
||||
[openworkServerStore, systemState.markReloadRequired],
|
||||
[openworkServerStore, reloadCoordinator.markReloadRequired],
|
||||
);
|
||||
const providerAuthStore = useMemo(
|
||||
() =>
|
||||
@@ -496,14 +529,14 @@ export function SettingsRoute() {
|
||||
setDisabledProviders,
|
||||
markOpencodeConfigReloadRequired: () => {
|
||||
setConfigActionStatus(t("settings.config_updated"));
|
||||
systemState.markReloadRequired("config", {
|
||||
reloadCoordinator.markReloadRequired("config", {
|
||||
type: "config",
|
||||
name: "opencode.json",
|
||||
action: "updated",
|
||||
});
|
||||
},
|
||||
}),
|
||||
[openworkServerStore, systemState.markReloadRequired],
|
||||
[openworkServerStore, reloadCoordinator.markReloadRequired],
|
||||
);
|
||||
const extensionsStore = useMemo(
|
||||
() =>
|
||||
@@ -519,9 +552,9 @@ export function SettingsRoute() {
|
||||
setBusyLabel,
|
||||
setBusyStartedAt: () => {},
|
||||
setError: setRouteError,
|
||||
markReloadRequired: systemState.markReloadRequired,
|
||||
markReloadRequired: reloadCoordinator.markReloadRequired,
|
||||
}),
|
||||
[openworkServerStore, systemState.markReloadRequired],
|
||||
[openworkServerStore, reloadCoordinator.markReloadRequired],
|
||||
);
|
||||
const automationsStore = useMemo(
|
||||
() =>
|
||||
@@ -710,7 +743,9 @@ export function SettingsRoute() {
|
||||
setWorkspaces(nextWorkspaces);
|
||||
setSessionsByWorkspaceId(Object.fromEntries(sessionEntries.map((entry) => [entry.workspaceId, entry.sessions])));
|
||||
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) {
|
||||
const message = describeRouteError(error);
|
||||
console.error("[settings-route] refreshRouteState failed", error);
|
||||
@@ -1004,9 +1039,9 @@ export function SettingsRoute() {
|
||||
addPlugin={async () => {
|
||||
setRouteError("Scheduler plugin install is not wired into the React settings route yet.");
|
||||
}}
|
||||
reloadWorkspaceEngine={systemState.reloadWorkspaceEngine}
|
||||
reloadBusy={systemState.reload.reloadBusy}
|
||||
canReloadWorkspace={systemState.canReloadWorkspaceEngine}
|
||||
reloadWorkspaceEngine={reloadCoordinator.reloadWorkspaceEngine}
|
||||
reloadBusy={false}
|
||||
canReloadWorkspace={reloadCoordinator.canReloadWorkspaceEngine}
|
||||
openLink={(url) => platform.openLink(url)}
|
||||
/>
|
||||
);
|
||||
@@ -1130,10 +1165,10 @@ export function SettingsRoute() {
|
||||
updateOpenworkServerSettings: openworkServerStore.updateOpenworkServerSettings,
|
||||
resetOpenworkServerSettings: openworkServerStore.resetOpenworkServerSettings,
|
||||
testOpenworkServerConnection: openworkServerStore.testOpenworkServerConnection,
|
||||
canReloadWorkspace: systemState.canReloadWorkspaceEngine,
|
||||
reloadWorkspaceEngine: systemState.reloadWorkspaceEngine,
|
||||
reloadBusy: systemState.reload.reloadBusy,
|
||||
reloadError: systemState.reload.reloadError ?? routeError,
|
||||
canReloadWorkspace: reloadCoordinator.canReloadWorkspaceEngine,
|
||||
reloadWorkspaceEngine: reloadCoordinator.reloadWorkspaceEngine,
|
||||
reloadBusy: false,
|
||||
reloadError: routeError,
|
||||
developerMode,
|
||||
}}
|
||||
/>
|
||||
@@ -1299,32 +1334,6 @@ export function SettingsRoute() {
|
||||
remoteSubmitting={createWorkspaceRemoteBusy}
|
||||
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
|
||||
client={activeClient}
|
||||
projectDir={selectedWorkspaceRoot}
|
||||
@@ -1336,7 +1345,7 @@ export function SettingsRoute() {
|
||||
if (!activeClient) return undefined;
|
||||
return abortSessionSafe(activeClient, sessionId);
|
||||
}}
|
||||
onReloadEngine={systemState.reloadWorkspaceEngine}
|
||||
onReloadEngine={reloadCoordinator.reloadWorkspaceEngine}
|
||||
modalState={{
|
||||
mcpAuthModalOpen: connectionsSnapshot.mcpAuthModalOpen,
|
||||
mcpAuthEntry: connectionsSnapshot.mcpAuthEntry,
|
||||
|
||||
@@ -486,23 +486,30 @@ async function fetchOpencodeJson(
|
||||
|
||||
const url = new URL(baseUrl);
|
||||
url.pathname = path.startsWith("/") ? path : `/${path}`;
|
||||
const directory = resolveOpencodeDirectory(workspace);
|
||||
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) {
|
||||
const params = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(init.query)) {
|
||||
if (value === undefined || value === null) continue;
|
||||
params.set(key, String(value));
|
||||
}
|
||||
if (directory && !params.has("directory")) {
|
||||
params.set("directory", directory);
|
||||
}
|
||||
url.search = params.toString();
|
||||
} else {
|
||||
url.search = "";
|
||||
url.search = directory ? new URLSearchParams({ directory }).toString() : "";
|
||||
}
|
||||
|
||||
const headers = new Headers();
|
||||
headers.set("Content-Type", "application/json");
|
||||
|
||||
const directory = resolveOpencodeDirectory(workspace);
|
||||
if (directory) {
|
||||
headers.set("x-opencode-directory", directory);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user