fix React cloud sign-in and workspace reload state

This commit is contained in:
Benjamin Shafii
2026-04-24 11:35:09 -07:00
parent b8cccf10d2
commit 20fcb84fd4
6 changed files with 131 additions and 11 deletions

View File

@@ -88,7 +88,23 @@ export const desktopBridge: DesktopBridge = new Proxy({} as DesktopBridge, {
export const desktopFetch: typeof globalThis.fetch = (input, init) => { export const desktopFetch: typeof globalThis.fetch = (input, init) => {
if (isElectronDesktopRuntime()) { if (isElectronDesktopRuntime()) {
return globalThis.fetch(input, init); return invokeElectronHelper<{
status: number;
statusText: string;
headers: [string, string][];
body: string;
}>("__fetch", typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url, {
method: init?.method,
headers: init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : undefined,
body: typeof init?.body === "string" ? init.body : undefined,
}).then(
(result) =>
new Response(result.body, {
status: result.status,
statusText: result.statusText,
headers: result.headers,
}),
);
} }
return tauriBridge.desktopFetch(input, init); return tauriBridge.desktopFetch(input, init);
}; };

View File

@@ -16,9 +16,19 @@ import {
DenApiError, DenApiError,
ensureDenActiveOrganization, ensureDenActiveOrganization,
readDenSettings, readDenSettings,
writeDenSettings,
type DenUser, type DenUser,
} from "../../../app/lib/den"; } from "../../../app/lib/den";
import { denSessionUpdatedEvent } from "../../../app/lib/den-session-events"; import {
denSessionUpdatedEvent,
dispatchDenSessionUpdated,
} from "../../../app/lib/den-session-events";
import {
deepLinkBridgeEvent,
drainPendingDeepLinks,
type DeepLinkBridgeDetail,
} from "../../../app/lib/deep-link-bridge";
import { parseDenAuthDeepLink } from "../../../app/lib/openwork-links";
export type DenAuthStatus = "checking" | "signed_in" | "signed_out"; export type DenAuthStatus = "checking" | "signed_in" | "signed_out";
@@ -48,6 +58,7 @@ export function DenAuthProvider({ children }: DenAuthProviderProps) {
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Monotonic token so stale async refreshes can't clobber a newer result. // Monotonic token so stale async refreshes can't clobber a newer result.
const refreshTokenRef = useRef(0); const refreshTokenRef = useRef(0);
const handledGrantsRef = useRef<Set<string>>(new Set());
const refresh = useCallback(async () => { const refresh = useCallback(async () => {
const currentRun = ++refreshTokenRef.current; const currentRun = ++refreshTokenRef.current;
@@ -114,6 +125,60 @@ export function DenAuthProvider({ children }: DenAuthProviderProps) {
}; };
}, [refresh]); }, [refresh]);
useEffect(() => {
if (typeof window === "undefined") return;
const handleUrls = (urls: readonly string[]) => {
for (const rawUrl of urls) {
const parsed = parseDenAuthDeepLink(rawUrl);
if (!parsed || handledGrantsRef.current.has(parsed.grant)) continue;
handledGrantsRef.current.add(parsed.grant);
void createDenClient({ baseUrl: parsed.denBaseUrl })
.exchangeDesktopHandoff(parsed.grant)
.then((result) => {
if (!result.token) {
throw new Error("Failed to sign in to OpenWork Cloud.");
}
writeDenSettings({
baseUrl: parsed.denBaseUrl,
authToken: result.token,
activeOrgId: null,
activeOrgSlug: null,
activeOrgName: null,
});
dispatchDenSessionUpdated({
status: "success",
baseUrl: parsed.denBaseUrl,
token: result.token,
user: result.user,
email: result.user?.email ?? null,
});
})
.catch((error) => {
handledGrantsRef.current.delete(parsed.grant);
dispatchDenSessionUpdated({
status: "error",
message:
error instanceof Error
? error.message
: "Failed to sign in to OpenWork Cloud.",
});
});
}
};
handleUrls(drainPendingDeepLinks(window));
const handleDeepLink = (event: Event) => {
handleUrls(((event as CustomEvent<DeepLinkBridgeDetail>).detail?.urls ?? []) as string[]);
};
window.addEventListener(deepLinkBridgeEvent, handleDeepLink);
return () => window.removeEventListener(deepLinkBridgeEvent, handleDeepLink);
}, []);
const value = useMemo<DenAuthStore>( const value = useMemo<DenAuthStore>(
() => ({ () => ({
status, status,

View File

@@ -115,7 +115,7 @@ export function ReloadCoordinatorProvider({ children }: { children: ReactNode })
<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-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"> <div className="pointer-events-auto">
<ReloadWorkspaceToast <ReloadWorkspaceToast
open={systemState.reload.reloadPending} open={systemState.reload.reloadPending && activeSessions.length === 0}
title={systemState.reloadCopy.title} title={systemState.reloadCopy.title}
description={systemState.reloadCopy.body} description={systemState.reloadCopy.body}
trigger={systemState.reload.reloadTrigger} trigger={systemState.reload.reloadTrigger}

View File

@@ -180,9 +180,9 @@ function mergeRouteWorkspaces(
const merged = match const merged = match
? { ? {
...workspace, ...workspace,
displayName: match.displayName?.trim() displayName: workspace.displayName?.trim()
? match.displayName ? workspace.displayName
: workspace.displayName, : match.displayName,
name: match.name?.trim() ? match.name : workspace.name, name: match.name?.trim() ? match.name : workspace.name,
} }
: workspace; : workspace;
@@ -227,6 +227,14 @@ function toSessionGroups(
})); }));
} }
function isActiveSessionStatus(status: unknown) {
return status === "running" || status === "retry" || status === "busy";
}
function getSessionStatus(session: any) {
return session?.status ?? session?.state ?? session?.runStatus ?? null;
}
async function fileToDataUrl(file: File) { async function fileToDataUrl(file: File) {
return await new Promise<string>((resolve, reject) => { return await new Promise<string>((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
@@ -372,6 +380,21 @@ export function SessionRoute() {
workspaceLabel, workspaceLabel,
}); });
const activeReloadBlockingSessions = useMemo(
() =>
Object.values(sessionsByWorkspaceId)
.flat()
.filter((session) => isActiveSessionStatus(getSessionStatus(session)))
.map((session: any) => ({
id: String(session?.id ?? ""),
title:
String(session?.title ?? session?.slug ?? session?.id ?? "").trim() ||
t("session.untitled"),
}))
.filter((session) => session.id.length > 0),
[sessionsByWorkspaceId],
);
const backgroundSessionLoadInFlight = useRef<Set<string>>(new Set()); const backgroundSessionLoadInFlight = useRef<Set<string>>(new Set());
const loadWorkspaceSessionsInBackground = useCallback( const loadWorkspaceSessionsInBackground = useCallback(
async (openworkClient: OpenworkServerClient, workspaces: RouteWorkspace[]) => { async (openworkClient: OpenworkServerClient, workspaces: RouteWorkspace[]) => {
@@ -575,9 +598,9 @@ export function SessionRoute() {
return reloadCoordinator.registerWorkspaceReloadControls({ return reloadCoordinator.registerWorkspaceReloadControls({
canReloadWorkspaceEngine: () => Boolean(client && selectedWorkspaceId), canReloadWorkspaceEngine: () => Boolean(client && selectedWorkspaceId),
reloadWorkspaceEngine: reloadWorkspaceEngineFromUi, reloadWorkspaceEngine: reloadWorkspaceEngineFromUi,
activeSessions: () => [], activeSessions: () => activeReloadBlockingSessions,
}); });
}, [client, reloadCoordinator, reloadWorkspaceEngineFromUi, selectedWorkspaceId]); }, [activeReloadBlockingSessions, client, reloadCoordinator, reloadWorkspaceEngineFromUi, selectedWorkspaceId]);
useEffect(() => { useEffect(() => {
if (!client || !selectedWorkspaceId) return; if (!client || !selectedWorkspaceId) return;

View File

@@ -109,9 +109,9 @@ function mergeRouteWorkspaces(
const merged = match const merged = match
? { ? {
...workspace, ...workspace,
displayName: match.displayName?.trim() displayName: workspace.displayName?.trim()
? match.displayName ? workspace.displayName
: workspace.displayName, : match.displayName,
name: match.name?.trim() ? match.name : workspace.name, name: match.name?.trim() ? match.name : workspace.name,
} }
: workspace; : workspace;

View File

@@ -1140,6 +1140,22 @@ async function handleDesktopInvoke(event, command, ...args) {
shell.showItemInFolder(target); shell.showItemInFolder(target);
return undefined; return undefined;
} }
case "__fetch": {
const url = String(args[0] ?? "").trim();
const init = args[1] ?? {};
if (!url) throw new Error("URL is required.");
const response = await fetch(url, {
method: typeof init.method === "string" ? init.method : undefined,
headers: init.headers && typeof init.headers === "object" ? init.headers : undefined,
body: typeof init.body === "string" ? init.body : undefined,
});
return {
status: response.status,
statusText: response.statusText,
headers: Array.from(response.headers.entries()),
body: await response.text(),
};
}
case "__homeDir": case "__homeDir":
return os.homedir(); return os.homedir();
case "__joinPath": case "__joinPath":