mirror of
https://github.com/different-ai/openwork
synced 2026-04-25 17:15:34 +02:00
* feat(desktop): electron 1:1 port alongside Tauri, fix workspace-create visibility Adds an Electron shell that mirrors the Tauri desktop runtime (bridge, dialogs, deep links, runtime supervision for openwork-server / opencode / opencode-router / orchestrator, packaging via electron-builder). Tauri dev/build scripts remain the default; Electron runs via dev:electron and package:electron. Also fixes the "workspace I just created is invisible until I restart the app" bug: the React routes only wrote to desktop-side state, so the running openwork-server never learned about the new workspace and the sidebar (which is populated from the server list) dropped it. The create flow now also calls openworkClient.createLocalWorkspace so POST /workspaces/local registers the workspace at runtime. Other small fixes included: - Clears the "OpenWork server Disconnected" flash caused by React 18 StrictMode double-invoking the connection stores' start/dispose pair. - Real app icon wired into Electron (dock + BrowserWindow + builder). - Fix a latent rm-import bug in runtime.mjs that silently skipped orchestrator auth cleanup. - Locale copy updated to say "OpenWork desktop app" instead of "Tauri app". - Adds description/author to apps/desktop/package.json to silence electron-builder warnings. * docs(prds): Tauri → Electron migration plan Describes how we'll cut every current Tauri user over to the Electron build via the existing Tauri updater (one last migration release that downloads + launches the Electron installer), how we unify app identity so Electron reads the same userData Tauri wrote (zero-copy data migration), and how ongoing auto-updates switch to electron-updater publishing to the same GitHub releases. --------- Co-authored-by: Benjamin Shafii <benjamin@openworklabs.com>
125 lines
3.5 KiB
TypeScript
125 lines
3.5 KiB
TypeScript
/** @jsxImportSource react */
|
|
import { createContext, useContext, type ReactNode } from "react";
|
|
|
|
import { openDesktopUrl, relaunchDesktopApp } from "../../app/lib/desktop";
|
|
import { isDesktopRuntime } from "../../app/utils";
|
|
|
|
export type SyncStorage = {
|
|
getItem(key: string): string | null;
|
|
setItem(key: string, value: string): void;
|
|
removeItem(key: string): void;
|
|
};
|
|
|
|
export type AsyncStorage = {
|
|
getItem(key: string): Promise<string | null>;
|
|
setItem(key: string, value: string): Promise<void>;
|
|
removeItem(key: string): Promise<void>;
|
|
};
|
|
|
|
export type Platform = {
|
|
platform: "web" | "desktop";
|
|
os?: "macos" | "windows" | "linux";
|
|
version?: string;
|
|
openLink(url: string): void;
|
|
restart(): Promise<void>;
|
|
notify(title: string, description?: string, href?: string): Promise<void>;
|
|
storage?: (name?: string) => SyncStorage | AsyncStorage;
|
|
checkUpdate?: () => Promise<{ updateAvailable: boolean; version?: string }>;
|
|
update?: () => Promise<void>;
|
|
fetch?: typeof fetch;
|
|
getDefaultServerUrl?: () => Promise<string | null>;
|
|
setDefaultServerUrl?: (url: string | null) => Promise<void>;
|
|
};
|
|
|
|
const PlatformContext = createContext<Platform | undefined>(undefined);
|
|
|
|
type PlatformProviderProps = {
|
|
value: Platform;
|
|
children: ReactNode;
|
|
};
|
|
|
|
export function PlatformProvider({ value, children }: PlatformProviderProps) {
|
|
return (
|
|
<PlatformContext.Provider value={value}>
|
|
{children}
|
|
</PlatformContext.Provider>
|
|
);
|
|
}
|
|
|
|
export function usePlatform(): Platform {
|
|
const context = useContext(PlatformContext);
|
|
if (!context) {
|
|
throw new Error("Platform context is missing");
|
|
}
|
|
return context;
|
|
}
|
|
|
|
function shouldOpenInCurrentTab(url: string) {
|
|
return /^(mailto|tel):/i.test(url.trim());
|
|
}
|
|
|
|
export function createDefaultPlatform(): Platform {
|
|
return {
|
|
platform: isDesktopRuntime() ? "desktop" : "web",
|
|
openLink(url: string) {
|
|
if (isDesktopRuntime()) {
|
|
void openDesktopUrl(url).catch(() => undefined);
|
|
return;
|
|
}
|
|
|
|
if (shouldOpenInCurrentTab(url)) {
|
|
window.location.href = url;
|
|
return;
|
|
}
|
|
|
|
window.open(url, "_blank");
|
|
},
|
|
restart: async () => {
|
|
if (isDesktopRuntime()) {
|
|
await relaunchDesktopApp();
|
|
return;
|
|
}
|
|
|
|
window.location.reload();
|
|
},
|
|
notify: async (title, description, href) => {
|
|
if (!("Notification" in window)) return;
|
|
|
|
const permission =
|
|
Notification.permission === "default"
|
|
? await Notification.requestPermission().catch(() => "denied")
|
|
: Notification.permission;
|
|
|
|
if (permission !== "granted") return;
|
|
|
|
const inView = document.visibilityState === "visible" && document.hasFocus();
|
|
if (inView) return;
|
|
|
|
await Promise.resolve()
|
|
.then(() => {
|
|
const notification = new Notification(title, {
|
|
body: description ?? "",
|
|
});
|
|
notification.onclick = () => {
|
|
window.focus();
|
|
if (href) {
|
|
window.history.pushState(null, "", href);
|
|
window.dispatchEvent(new PopStateEvent("popstate"));
|
|
}
|
|
notification.close();
|
|
};
|
|
})
|
|
.catch(() => undefined);
|
|
},
|
|
storage: (name) => {
|
|
const prefix = name ? `${name}:` : "";
|
|
return {
|
|
getItem: (key) => window.localStorage.getItem(prefix + key),
|
|
setItem: (key, value) => window.localStorage.setItem(prefix + key, value),
|
|
removeItem: (key) => window.localStorage.removeItem(prefix + key),
|
|
};
|
|
},
|
|
fetch,
|
|
};
|
|
}
|