mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
fix React cloud sign-in and workspace reload state
This commit is contained in:
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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":
|
||||||
|
|||||||
Reference in New Issue
Block a user