refactor: remove legacy Tauri desktop support
Electron updater now uses Electron release metadata only Removed legacy Tauri package and migration workflow Replaced Tauri shim usage with the desktop bridge
This commit is contained in:
@@ -221,7 +221,7 @@ export const debugUtils = {
|
||||
})();
|
||||
|
||||
const runtimeApis = getRegisteredRuntimeAPIs();
|
||||
const isTauriShell = typeof window !== 'undefined' && Boolean((window as any).__TAURI__);
|
||||
const isDesktopRuntime = typeof window !== 'undefined' && Boolean((window as { __OPENCHAMBER_ELECTRON__?: unknown }).__OPENCHAMBER_ELECTRON__);
|
||||
|
||||
const safeJson = async (resp: Response) => {
|
||||
try {
|
||||
@@ -327,7 +327,7 @@ export const debugUtils = {
|
||||
const report = {
|
||||
runtime: {
|
||||
platform: runtimeApis?.runtime?.platform ?? null,
|
||||
isDesktop: isTauriShell,
|
||||
isDesktop: isDesktopRuntime,
|
||||
isVSCode: Boolean(runtimeApis?.runtime?.isVSCode),
|
||||
hasRuntimeApis: Boolean(runtimeApis),
|
||||
desktopServerOrigin: null,
|
||||
|
||||
@@ -185,19 +185,14 @@ export type DesktopSettings = {
|
||||
draftStarters?: DraftStarterRef[];
|
||||
};
|
||||
|
||||
type TauriGlobal = {
|
||||
core?: {
|
||||
invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
};
|
||||
dialog?: {
|
||||
open?: (options: Record<string, unknown>) => Promise<unknown>;
|
||||
};
|
||||
event?: {
|
||||
listen?: (
|
||||
event: string,
|
||||
handler: (evt: { payload?: unknown }) => void,
|
||||
) => Promise<() => void>;
|
||||
};
|
||||
type DesktopBridgeGlobal = {
|
||||
invoke?: (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
openDialog?: (options: Record<string, unknown>) => Promise<unknown>;
|
||||
openExternal?: (url: string) => Promise<unknown>;
|
||||
listen?: (
|
||||
event: string,
|
||||
handler: (evt: { payload?: unknown }) => void,
|
||||
) => Promise<() => void>;
|
||||
};
|
||||
|
||||
type ElectronRuntimeGlobal = {
|
||||
@@ -209,27 +204,23 @@ const getElectronRuntime = (): ElectronRuntimeGlobal | null => {
|
||||
return (window as unknown as { __OPENCHAMBER_ELECTRON__?: ElectronRuntimeGlobal }).__OPENCHAMBER_ELECTRON__ ?? null;
|
||||
};
|
||||
|
||||
export const isTauriShell = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
return typeof tauri?.core?.invoke === 'function';
|
||||
const getDesktopBridge = (): DesktopBridgeGlobal | null => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__ ?? null;
|
||||
};
|
||||
|
||||
export const isElectronShell = (): boolean => getElectronRuntime()?.runtime === 'electron';
|
||||
|
||||
export const hasDesktopInvoke = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
return typeof tauri?.core?.invoke === 'function';
|
||||
return typeof getDesktopBridge()?.invoke === 'function';
|
||||
};
|
||||
|
||||
export const canUseElectronDesktopIPC = (): boolean => isElectronShell() && hasDesktopInvoke();
|
||||
|
||||
export const invokeDesktop = async <T = unknown>(command: string, args?: Record<string, unknown>): Promise<T | null> => {
|
||||
if (typeof window === 'undefined') return null;
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
if (typeof tauri?.core?.invoke !== 'function') return null;
|
||||
return tauri.core.invoke(command, args ?? {}) as Promise<T>;
|
||||
const bridge = getDesktopBridge();
|
||||
if (typeof bridge?.invoke !== 'function') return null;
|
||||
return bridge.invoke(command, args ?? {}) as Promise<T>;
|
||||
};
|
||||
|
||||
type LaunchAtLoginStatus = {
|
||||
@@ -367,18 +358,16 @@ export const isDesktopLocalOriginActive = (): boolean => {
|
||||
|
||||
export const isDesktopShell = (): boolean => {
|
||||
if (typeof window === 'undefined') return false;
|
||||
return isTauriShell() || isElectronShell();
|
||||
return isElectronShell();
|
||||
};
|
||||
|
||||
export const startDesktopWindowDrag = async (): Promise<boolean> => {
|
||||
if (!isDesktopShell() || !isTauriShell()) {
|
||||
if (!isDesktopShell()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
const appWindow = getCurrentWindow();
|
||||
await appWindow.startDragging();
|
||||
await invokeDesktop('desktop_start_window_drag');
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -418,10 +407,9 @@ export const requestDirectoryAccess = async (
|
||||
directoryPath: string
|
||||
): Promise<{ success: boolean; path?: string; projectId?: string; error?: string }> => {
|
||||
// Desktop shell on local instance: use native folder picker.
|
||||
if (isTauriShell() && isDesktopLocalOriginActive()) {
|
||||
if (hasDesktopInvoke() && isDesktopLocalOriginActive()) {
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const selected = await tauri?.dialog?.open?.({
|
||||
const selected = await getDesktopBridge()?.openDialog?.({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
title: 'Select Working Directory',
|
||||
@@ -431,7 +419,7 @@ export const requestDirectoryAccess = async (
|
||||
}
|
||||
return { success: true, path: selected };
|
||||
} catch (error) {
|
||||
console.warn('Failed to request directory access (tauri)', error);
|
||||
console.warn('Failed to request directory access', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
@@ -442,10 +430,9 @@ export const requestDirectoryAccess = async (
|
||||
export const requestFileAccess = async (
|
||||
options?: { filters?: Array<{ name: string; extensions: string[] }>; defaultPath?: string }
|
||||
): Promise<{ success: boolean; path?: string; error?: string }> => {
|
||||
if (isTauriShell() && isDesktopLocalOriginActive()) {
|
||||
if (hasDesktopInvoke() && isDesktopLocalOriginActive()) {
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const selected = await tauri?.dialog?.open?.({
|
||||
const selected = await getDesktopBridge()?.openDialog?.({
|
||||
directory: false,
|
||||
multiple: false,
|
||||
title: 'Select File',
|
||||
@@ -457,7 +444,7 @@ export const requestFileAccess = async (
|
||||
}
|
||||
return { success: true, path: selected };
|
||||
} catch (error) {
|
||||
console.warn('Failed to request file access (tauri)', error);
|
||||
console.warn('Failed to request file access', error);
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
@@ -482,10 +469,9 @@ export const stopAccessingDirectory = async (
|
||||
export const sendAssistantCompletionNotification = async (
|
||||
payload?: AssistantNotificationPayload
|
||||
): Promise<boolean> => {
|
||||
if (isTauriShell()) {
|
||||
if (hasDesktopInvoke()) {
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
await tauri?.core?.invoke?.('desktop_notify', {
|
||||
await invokeDesktop('desktop_notify', {
|
||||
payload: {
|
||||
title: payload?.title,
|
||||
body: payload?.body,
|
||||
@@ -494,7 +480,7 @@ export const sendAssistantCompletionNotification = async (
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to send assistant completion notification (tauri)', error);
|
||||
console.warn('Failed to send assistant completion notification', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -503,16 +489,15 @@ export const sendAssistantCompletionNotification = async (
|
||||
};
|
||||
|
||||
export const checkForDesktopUpdates = async (): Promise<UpdateInfo | null> => {
|
||||
if (!isTauriShell()) {
|
||||
if (!hasDesktopInvoke()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const info = await tauri?.core?.invoke?.('desktop_check_for_updates');
|
||||
const info = await invokeDesktop<UpdateInfo>('desktop_check_for_updates');
|
||||
return info as UpdateInfo;
|
||||
} catch (error) {
|
||||
console.warn('Failed to check for updates (tauri)', error);
|
||||
console.warn('Failed to check for updates', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -520,18 +505,18 @@ export const checkForDesktopUpdates = async (): Promise<UpdateInfo | null> => {
|
||||
export const downloadDesktopUpdate = async (
|
||||
onProgress?: (progress: UpdateProgress) => void
|
||||
): Promise<boolean> => {
|
||||
if (!isTauriShell()) {
|
||||
if (!hasDesktopInvoke()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const bridge = getDesktopBridge();
|
||||
let unlisten: null | (() => void | Promise<void>) = null;
|
||||
let downloaded = 0;
|
||||
let total: number | undefined;
|
||||
|
||||
try {
|
||||
if (typeof onProgress === 'function' && tauri?.event?.listen) {
|
||||
unlisten = await tauri.event.listen('openchamber:update-progress', (evt) => {
|
||||
if (typeof onProgress === 'function' && bridge?.listen) {
|
||||
unlisten = await bridge.listen('openchamber:update-progress', (evt) => {
|
||||
const payload = evt?.payload;
|
||||
if (!payload || typeof payload !== 'object') return;
|
||||
const data = payload as { event?: unknown; data?: unknown };
|
||||
@@ -560,10 +545,10 @@ export const downloadDesktopUpdate = async (
|
||||
});
|
||||
}
|
||||
|
||||
await tauri?.core?.invoke?.('desktop_download_and_install_update');
|
||||
await invokeDesktop('desktop_download_and_install_update');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to download update (tauri)', error);
|
||||
console.warn('Failed to download update', error);
|
||||
return false;
|
||||
} finally {
|
||||
if (unlisten) {
|
||||
@@ -580,7 +565,7 @@ export const downloadDesktopUpdate = async (
|
||||
};
|
||||
|
||||
export const restartToApplyUpdate = async (): Promise<boolean> => {
|
||||
if (!isTauriShell()) {
|
||||
if (!hasDesktopInvoke()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -588,37 +573,35 @@ export const restartToApplyUpdate = async (): Promise<boolean> => {
|
||||
};
|
||||
|
||||
export const restartDesktopApp = async (): Promise<boolean> => {
|
||||
if (!isTauriShell()) {
|
||||
if (!hasDesktopInvoke()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
await tauri?.core?.invoke?.('desktop_restart');
|
||||
await invokeDesktop('desktop_restart');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to restart desktop app (tauri)', error);
|
||||
console.warn('Failed to restart desktop app', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getDesktopLanAddress = async (): Promise<string | null> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const result = await tauri?.core?.invoke?.('desktop_get_lan_address');
|
||||
const result = await invokeDesktop<string>('desktop_get_lan_address');
|
||||
return typeof result === 'string' && result.trim().length > 0 ? result.trim() : null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to get desktop LAN address (tauri)', error);
|
||||
console.warn('Failed to get desktop LAN address', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const openDesktopPath = async (path: string, app?: string | null): Promise<boolean> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -628,20 +611,19 @@ export const openDesktopPath = async (path: string, app?: string | null): Promis
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
await tauri?.core?.invoke?.('desktop_open_path', {
|
||||
await invokeDesktop('desktop_open_path', {
|
||||
path: trimmed,
|
||||
app: typeof app === 'string' && app.trim().length > 0 ? app.trim() : undefined,
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to open path (tauri)', error);
|
||||
console.warn('Failed to open path', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const revealDesktopPath = async (path: string): Promise<boolean> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -651,8 +633,7 @@ export const revealDesktopPath = async (path: string): Promise<boolean> => {
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
await tauri?.core?.invoke?.('desktop_reveal_path', {
|
||||
await invokeDesktop('desktop_reveal_path', {
|
||||
path: trimmed,
|
||||
});
|
||||
return true;
|
||||
@@ -665,7 +646,7 @@ export const saveDesktopMarkdownFile = async (
|
||||
defaultFileName: string,
|
||||
content: string,
|
||||
): Promise<string | null> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -675,14 +656,13 @@ export const saveDesktopMarkdownFile = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const result = await tauri?.core?.invoke?.('desktop_save_markdown_file', {
|
||||
const result = await invokeDesktop<string>('desktop_save_markdown_file', {
|
||||
defaultFileName: trimmedFileName,
|
||||
content,
|
||||
});
|
||||
return typeof result === 'string' && result.trim().length > 0 ? result : null;
|
||||
} catch (error) {
|
||||
console.warn('Failed to save markdown file (tauri)', error);
|
||||
console.warn('Failed to save markdown file', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -692,7 +672,7 @@ export const openDesktopProjectInApp = async (
|
||||
appId: string,
|
||||
appName: string,
|
||||
): Promise<boolean> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -705,8 +685,7 @@ export const openDesktopProjectInApp = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
await tauri?.core?.invoke?.('desktop_open_in_app', {
|
||||
await invokeDesktop('desktop_open_in_app', {
|
||||
projectPath: trimmedProjectPath,
|
||||
appId: trimmedAppId,
|
||||
appName: trimmedAppName,
|
||||
@@ -723,7 +702,7 @@ export const openDesktopFileInApp = async (
|
||||
appId: string,
|
||||
appName: string,
|
||||
): Promise<boolean> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -736,8 +715,7 @@ export const openDesktopFileInApp = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
await tauri?.core?.invoke?.('desktop_open_file_in_app', {
|
||||
await invokeDesktop('desktop_open_file_in_app', {
|
||||
filePath: trimmedFilePath,
|
||||
appId: trimmedAppId,
|
||||
appName: trimmedAppName,
|
||||
@@ -750,7 +728,7 @@ export const openDesktopFileInApp = async (
|
||||
};
|
||||
|
||||
export const filterInstalledDesktopApps = async (apps: string[]): Promise<string[]> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -760,19 +738,18 @@ export const filterInstalledDesktopApps = async (apps: string[]): Promise<string
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const result = await tauri?.core?.invoke?.('desktop_filter_installed_apps', {
|
||||
const result = await invokeDesktop<string[]>('desktop_filter_installed_apps', {
|
||||
apps: candidate,
|
||||
});
|
||||
return Array.isArray(result) ? result.filter((value) => typeof value === 'string') : [];
|
||||
} catch (error) {
|
||||
console.warn('Failed to check installed apps (tauri)', error);
|
||||
console.warn('Failed to check installed apps', error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<string, string>> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -782,8 +759,7 @@ export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<strin
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const result = await tauri?.core?.invoke?.('desktop_fetch_app_icons', {
|
||||
const result = await invokeDesktop<unknown[]>('desktop_fetch_app_icons', {
|
||||
apps: candidate,
|
||||
});
|
||||
if (!Array.isArray(result)) {
|
||||
@@ -798,7 +774,7 @@ export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<strin
|
||||
}
|
||||
return map;
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch installed app icons (tauri)', error);
|
||||
console.warn('Failed to fetch installed app icons', error);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
@@ -819,7 +795,7 @@ export const fetchDesktopInstalledApps = async (
|
||||
apps: string[],
|
||||
force?: boolean
|
||||
): Promise<FetchDesktopInstalledAppsResult> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return { apps: [], success: false, hasCache: false, isCacheStale: false };
|
||||
}
|
||||
|
||||
@@ -829,8 +805,7 @@ export const fetchDesktopInstalledApps = async (
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const result = await tauri?.core?.invoke?.('desktop_get_installed_apps', {
|
||||
const result = await invokeDesktop<unknown>('desktop_get_installed_apps', {
|
||||
apps: candidate,
|
||||
force: force === true ? true : undefined,
|
||||
});
|
||||
@@ -858,19 +833,18 @@ export const fetchDesktopInstalledApps = async (
|
||||
isCacheStale: payload.isCacheStale === true,
|
||||
};
|
||||
} catch (error) {
|
||||
console.warn('Failed to fetch installed apps (tauri)', error);
|
||||
console.warn('Failed to fetch installed apps', error);
|
||||
return { apps: [], success: false, hasCache: false, isCacheStale: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const clearDesktopCache = async (): Promise<boolean> => {
|
||||
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
|
||||
if (!hasDesktopInvoke() || !isDesktopLocalOriginActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
await tauri?.core?.invoke?.('desktop_clear_cache');
|
||||
await invokeDesktop('desktop_clear_cache');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('Failed to clear cache', error);
|
||||
|
||||
@@ -194,7 +194,7 @@ describe('shouldRestartDesktopBootFlow', () => {
|
||||
test('restarts the desktop app when boot UI is running in the startup window', () => {
|
||||
expect(
|
||||
shouldRestartDesktopBootFlow({
|
||||
isTauriShell: true,
|
||||
isDesktopShell: true,
|
||||
isDesktopLocalOriginActive: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
@@ -203,16 +203,16 @@ describe('shouldRestartDesktopBootFlow', () => {
|
||||
test('does not restart when the local desktop origin is already active', () => {
|
||||
expect(
|
||||
shouldRestartDesktopBootFlow({
|
||||
isTauriShell: true,
|
||||
isDesktopShell: true,
|
||||
isDesktopLocalOriginActive: true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
test('does not restart outside the tauri shell', () => {
|
||||
test('does not restart outside the desktop shell', () => {
|
||||
expect(
|
||||
shouldRestartDesktopBootFlow({
|
||||
isTauriShell: false,
|
||||
isDesktopShell: false,
|
||||
isDesktopLocalOriginActive: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
|
||||
@@ -222,14 +222,14 @@ export type InitialLoadingState = {
|
||||
};
|
||||
|
||||
export type DesktopBootFlowRestartInput = {
|
||||
isTauriShell: boolean;
|
||||
isDesktopShell: boolean;
|
||||
isDesktopLocalOriginActive: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Whether the initial loading screen can be dismissed.
|
||||
*
|
||||
* Desktop shells must wait until a valid boot outcome is injected by Rust.
|
||||
* Desktop shells must wait until a valid boot outcome is injected by the native host.
|
||||
* For non-main views (chooser, recovery), the splash can dismiss as soon as
|
||||
* the outcome is known — `isInitialized` is not required because OpenCode
|
||||
* may not be available in those flows.
|
||||
@@ -254,16 +254,16 @@ export function canDismissInitialLoading(state: InitialLoadingState): boolean {
|
||||
}
|
||||
|
||||
/**
|
||||
* Boot/recovery UI can render in the Tauri startup window before the local
|
||||
* Boot/recovery UI can render in the desktop startup window before the local
|
||||
* desktop HTTP origin is active. In that state, same-origin reloads and
|
||||
* `/api/*` requests cannot recover the app, so callers must restart Tauri.
|
||||
* `/api/*` requests cannot recover the app, so callers must restart desktop.
|
||||
*/
|
||||
export function shouldRestartDesktopBootFlow(input: DesktopBootFlowRestartInput): boolean {
|
||||
return input.isTauriShell && !input.isDesktopLocalOriginActive;
|
||||
return input.isDesktopShell && !input.isDesktopLocalOriginActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the boot outcome injected by the Rust backend.
|
||||
* Read the boot outcome injected by the native desktop host.
|
||||
* Returns `null` when not in desktop, when the outcome has not been set yet,
|
||||
* or when the injected payload is malformed.
|
||||
*/
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { isTauriShell } from '@/lib/desktop';
|
||||
import { hasDesktopInvoke, invokeDesktop } from '@/lib/desktop';
|
||||
|
||||
type TauriInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
type TauriGlobal = {
|
||||
core?: {
|
||||
invoke?: TauriInvoke;
|
||||
};
|
||||
};
|
||||
type DesktopInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
export type DesktopHost = {
|
||||
id: string;
|
||||
@@ -176,10 +170,9 @@ export const getDesktopHostApiUrl = (host: DesktopHost): string => {
|
||||
return normalizeHostUrl(host.apiUrl || host.url) || host.apiUrl || host.url;
|
||||
};
|
||||
|
||||
const getInvoke = (): TauriInvoke | null => {
|
||||
if (!isTauriShell()) return null;
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null;
|
||||
const getInvoke = (): DesktopInvoke | null => {
|
||||
if (!hasDesktopInvoke()) return null;
|
||||
return (command, args) => invokeDesktop(command, args) as Promise<unknown>;
|
||||
};
|
||||
|
||||
export const desktopHostsGet = async (): Promise<DesktopHostsConfig> => {
|
||||
|
||||
@@ -1,30 +1,15 @@
|
||||
import { isDesktopShell } from '@/lib/desktop';
|
||||
import { hasDesktopInvoke, invokeDesktop, isDesktopShell } from '@/lib/desktop';
|
||||
|
||||
type InvokeArgs = Record<string, unknown>;
|
||||
|
||||
const isElectronDesktop = (): boolean => {
|
||||
return typeof window !== 'undefined' && Boolean((window as { __OPENCHAMBER_ELECTRON__?: unknown }).__OPENCHAMBER_ELECTRON__);
|
||||
};
|
||||
|
||||
const getInvoke = () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const tauri = (window as unknown as {
|
||||
__TAURI__?: { core?: { invoke?: (cmd: string, args?: InvokeArgs) => Promise<unknown> } };
|
||||
}).__TAURI__;
|
||||
return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null;
|
||||
};
|
||||
|
||||
export const invokeDesktopCommand = async <TValue = unknown>(
|
||||
command: string,
|
||||
args?: InvokeArgs,
|
||||
): Promise<TValue> => {
|
||||
const invoke = getInvoke();
|
||||
if (!invoke) {
|
||||
if (!hasDesktopInvoke()) {
|
||||
throw new Error('Desktop runtime is not available');
|
||||
}
|
||||
return invoke(command, args) as Promise<TValue>;
|
||||
return invokeDesktop<TValue>(command, args) as Promise<TValue>;
|
||||
};
|
||||
|
||||
export const startDesktopWindowDrag = async (): Promise<void> => {
|
||||
@@ -33,13 +18,7 @@ export const startDesktopWindowDrag = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
try {
|
||||
if (isElectronDesktop()) {
|
||||
await invokeDesktopCommand('desktop_start_window_drag');
|
||||
return;
|
||||
}
|
||||
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
await getCurrentWindow().startDragging();
|
||||
await invokeDesktopCommand('desktop_start_window_drag');
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
@@ -51,11 +30,6 @@ export const isDesktopWindowFullscreen = async (): Promise<boolean> => {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isElectronDesktop()) {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
return await getCurrentWindow().isFullscreen();
|
||||
}
|
||||
|
||||
return Boolean(await invokeDesktopCommand('desktop_is_window_fullscreen'));
|
||||
} catch {
|
||||
return false;
|
||||
@@ -77,12 +51,6 @@ export const setDesktopWindowTitle = async (title: string): Promise<void> => {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isElectronDesktop()) {
|
||||
const { getCurrentWindow } = await import('@tauri-apps/api/window');
|
||||
await getCurrentWindow().setTitle(title);
|
||||
return;
|
||||
}
|
||||
|
||||
await invokeDesktopCommand('desktop_set_window_title', { title });
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -98,12 +66,6 @@ export const setDesktopWindowTheme = async (
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isElectronDesktop()) {
|
||||
const { invoke } = await import('@tauri-apps/api/core');
|
||||
await invoke('desktop_set_window_theme', { themeMode, themeVariant });
|
||||
return;
|
||||
}
|
||||
|
||||
await invokeDesktopCommand('desktop_set_window_theme', { themeMode, themeVariant });
|
||||
} catch {
|
||||
// ignore
|
||||
@@ -116,11 +78,6 @@ export const getDesktopAppVersion = async (): Promise<string | null> => {
|
||||
}
|
||||
|
||||
try {
|
||||
if (!isElectronDesktop()) {
|
||||
const { getVersion } = await import('@tauri-apps/api/app');
|
||||
return await getVersion();
|
||||
}
|
||||
|
||||
const version = await invokeDesktopCommand('desktop_get_app_version');
|
||||
return typeof version === 'string' && version.trim().length > 0 ? version : null;
|
||||
} catch {
|
||||
@@ -146,17 +103,6 @@ export const listenDesktopNativeDragDrop = async (
|
||||
return null;
|
||||
}
|
||||
|
||||
// Electron uses the renderer's native DOM drag/drop events instead of a
|
||||
// separate webview drag listener.
|
||||
if (isElectronDesktop()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const { getCurrentWebviewWindow } = await import('@tauri-apps/api/webviewWindow');
|
||||
const webviewWindow = getCurrentWebviewWindow();
|
||||
return await webviewWindow.onDragDropEvent(handler as never);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
void handler;
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import { isTauriShell } from '@/lib/desktop';
|
||||
import { hasDesktopInvoke, invokeDesktop } from '@/lib/desktop';
|
||||
|
||||
type TauriInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
type DesktopInvoke = (cmd: string, args?: Record<string, unknown>) => Promise<unknown>;
|
||||
|
||||
type TauriGlobal = {
|
||||
core?: {
|
||||
invoke?: TauriInvoke;
|
||||
};
|
||||
event?: {
|
||||
listen?: (
|
||||
event: string,
|
||||
handler: (evt: { payload?: unknown }) => void,
|
||||
) => Promise<() => void>;
|
||||
};
|
||||
type DesktopBridgeGlobal = {
|
||||
listen?: (
|
||||
event: string,
|
||||
handler: (evt: { payload?: unknown }) => void,
|
||||
) => Promise<() => void>;
|
||||
};
|
||||
|
||||
export type DesktopSshRemoteMode = 'managed' | 'external';
|
||||
@@ -126,10 +121,9 @@ const asStringArray = (value: unknown): string[] => {
|
||||
return value.filter((item): item is string => typeof item === 'string');
|
||||
};
|
||||
|
||||
const getInvoke = (): TauriInvoke | null => {
|
||||
if (!isTauriShell()) return null;
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
return typeof tauri?.core?.invoke === 'function' ? tauri.core.invoke : null;
|
||||
const getInvoke = (): DesktopInvoke | null => {
|
||||
if (!hasDesktopInvoke()) return null;
|
||||
return (command, args) => invokeDesktop(command, args) as Promise<unknown>;
|
||||
};
|
||||
|
||||
const parseStoredSecret = (value: unknown): DesktopSshStoredSecret | undefined => {
|
||||
@@ -432,12 +426,12 @@ export const desktopSshLogsClear = async (id: string): Promise<void> => {
|
||||
export const listenDesktopSshStatus = async (
|
||||
listener: (status: DesktopSshInstanceStatus) => void,
|
||||
): Promise<() => Promise<void>> => {
|
||||
if (!isTauriShell()) {
|
||||
if (!hasDesktopInvoke()) {
|
||||
return async () => {};
|
||||
}
|
||||
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
|
||||
const listen = tauri?.event?.listen;
|
||||
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
|
||||
const listen = desktop?.listen;
|
||||
if (typeof listen !== 'function') {
|
||||
return async () => {};
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ const getNavigatorDeviceHints = (maxTouchPoints: number) => {
|
||||
};
|
||||
|
||||
const setRootDeviceAttributes = (
|
||||
isTauriShellRuntime: boolean,
|
||||
isDesktopShellRuntime: boolean,
|
||||
deviceType: DeviceType,
|
||||
hasTouchInput: boolean,
|
||||
) => {
|
||||
@@ -66,7 +66,7 @@ const setRootDeviceAttributes = (
|
||||
: 'device-desktop'
|
||||
);
|
||||
|
||||
if (isTauriShellRuntime) {
|
||||
if (isDesktopShellRuntime) {
|
||||
root.classList.add('desktop-runtime');
|
||||
root.style.setProperty('--is-mobile', '0');
|
||||
root.style.setProperty('--device-type', 'desktop');
|
||||
|
||||
@@ -5,10 +5,9 @@ import type React from 'react';
|
||||
* Uses both `isComposing` and the `keyCode === 229` fallback.
|
||||
*
|
||||
* Note: `keyCode` is deprecated, but `229` remains a practical fallback for
|
||||
* some WebKit-based environments (including Tauri WebView) where composition
|
||||
* some WebKit-based environments where composition
|
||||
* events can be ordered unexpectedly.
|
||||
*/
|
||||
export const isIMECompositionEvent = (e: React.KeyboardEvent): boolean => {
|
||||
return e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { snapdom } from '@zumer/snapdom';
|
||||
import { getFontEmbedCSS, toJpeg } from 'html-to-image';
|
||||
import { invokeDesktop } from '@/lib/desktop';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
|
||||
export type PreviewElementMetadata = {
|
||||
@@ -97,18 +98,16 @@ export const renderPreviewScreenshot = async (
|
||||
iframe: HTMLIFrameElement,
|
||||
target: PreviewElementMetadata,
|
||||
): Promise<File | null> => {
|
||||
const tauri = typeof window !== 'undefined'
|
||||
? (window as unknown as { __TAURI__?: { core?: { invoke?: <T>(cmd: string, args?: Record<string, unknown>) => Promise<T> } } }).__TAURI__
|
||||
: undefined;
|
||||
if (typeof tauri?.core?.invoke === 'function') {
|
||||
if (typeof window !== 'undefined') {
|
||||
try {
|
||||
const rect = iframe.getBoundingClientRect();
|
||||
const capture = await tauri.core.invoke<{ mime: string; base64: string; width: number; height: number }>('desktop_capture_page_rect', {
|
||||
const capture = await invokeDesktop<{ mime: string; base64: string; width: number; height: number }>('desktop_capture_page_rect', {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
if (!capture) throw new Error('Desktop screenshot capture is not available');
|
||||
const image = new Image();
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
image.onload = () => resolve();
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Router module for URL-based navigation in OpenChamber.
|
||||
*
|
||||
* Provides bidirectional sync between URL query parameters and application state.
|
||||
* Works across web, desktop (Tauri), and VS Code (state-only mode).
|
||||
* Works across web, desktop, and VS Code (state-only mode).
|
||||
*
|
||||
* URL Schema:
|
||||
* - `?session=<id>` - Navigate to specific session
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isMacOS } from '@/lib/utils';
|
||||
import { isTauriShell } from '@/lib/desktop';
|
||||
import { isDesktopShell } from '@/lib/desktop';
|
||||
|
||||
export type ShortcutModifier = 'mod' | 'shift' | 'alt' | 'option' | 'ctrl';
|
||||
export type ShortcutKey = string;
|
||||
@@ -32,7 +32,7 @@ const MODIFIER_KEY_MAP: Record<string, ShortcutModifier> = {
|
||||
};
|
||||
|
||||
const DISPLAY_LABEL_MAP: Record<ShortcutModifier, string> = {
|
||||
'mod': isMacOS() && isTauriShell() ? '⌘' : 'Ctrl',
|
||||
'mod': isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl',
|
||||
'shift': '⇧',
|
||||
'alt': '⌥',
|
||||
'option': '⌥',
|
||||
@@ -555,7 +555,7 @@ export function eventMatchesShortcut(
|
||||
const expectedShift = parsed.modifiers.has('shift');
|
||||
const expectedAlt = parsed.modifiers.has('alt');
|
||||
const expectedCtrl = parsed.modifiers.has('ctrl');
|
||||
const isDesktopMac = isMacOS() && isTauriShell();
|
||||
const isDesktopMac = isMacOS() && isDesktopShell();
|
||||
const isMac = isMacOS();
|
||||
|
||||
const modMatches = isDesktopMac
|
||||
@@ -615,5 +615,5 @@ export function getShortcutLabel(id: string): string {
|
||||
}
|
||||
|
||||
export function getModifierLabel(): string {
|
||||
return isMacOS() && isTauriShell() ? '⌘' : 'Ctrl';
|
||||
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
|
||||
}
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
|
||||
/**
|
||||
* Utility for opening external URLs with Tauri shell support.
|
||||
* In desktop runtime, uses tauri.shell.open() for proper system browser handling.
|
||||
* Falls back to window.open() for web runtime.
|
||||
*/
|
||||
|
||||
type TauriShell = {
|
||||
shell?: {
|
||||
open?: (url: string) => Promise<unknown>;
|
||||
};
|
||||
type DesktopBridgeGlobal = {
|
||||
openExternal?: (url: string) => Promise<unknown>;
|
||||
};
|
||||
|
||||
const parseUrlSafely = (value: string): URL | null => {
|
||||
@@ -90,7 +82,7 @@ export const extractLoopbackUrls = (text: string): string[] => {
|
||||
|
||||
/**
|
||||
* Opens an external URL in the system browser.
|
||||
* In Tauri desktop runtime, uses tauri.shell.open() for proper handling.
|
||||
* In desktop runtime, uses the native shell for proper handling.
|
||||
* Falls back to window.open() for web runtime.
|
||||
*
|
||||
* @param url - The URL to open
|
||||
@@ -127,10 +119,10 @@ export const openExternalUrl = async (url: string): Promise<boolean> => {
|
||||
}
|
||||
}
|
||||
|
||||
const tauri = (window as unknown as { __TAURI__?: TauriShell }).__TAURI__;
|
||||
if (tauri?.shell?.open) {
|
||||
const desktop = (window as unknown as { __OPENCHAMBER_DESKTOP__?: DesktopBridgeGlobal }).__OPENCHAMBER_DESKTOP__;
|
||||
if (desktop?.openExternal) {
|
||||
try {
|
||||
await tauri.shell.open(normalizedTarget);
|
||||
await desktop.openExternal(normalizedTarget);
|
||||
return true;
|
||||
} catch {
|
||||
// Fall through to window.open
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { isTauriShell } from "@/lib/desktop";
|
||||
import { isDesktopShell } from "@/lib/desktop";
|
||||
import { matchesFuzzyQuery } from "@/lib/search/fuzzySearch";
|
||||
import type { I18nKey } from "@/lib/i18n";
|
||||
|
||||
@@ -31,19 +31,19 @@ export const getRevealLabelKey = (): I18nKey => {
|
||||
/**
|
||||
* Checks if the platform-appropriate modifier key is pressed.
|
||||
* On macOS desktop app: Cmd (metaKey), on other platforms or web: Ctrl (ctrlKey).
|
||||
* Browser intercepts Cmd shortcuts, so we only use Cmd in Tauri desktop app.
|
||||
* Browser intercepts Cmd shortcuts, so we only use Cmd in the desktop app.
|
||||
*/
|
||||
export const hasModifier = (e: KeyboardEvent | React.KeyboardEvent): boolean => {
|
||||
return isMacOS() && isTauriShell() ? e.metaKey : e.ctrlKey;
|
||||
return isMacOS() && isDesktopShell() ? e.metaKey : e.ctrlKey;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the platform-appropriate modifier key label.
|
||||
* On macOS desktop app: "⌘", on other platforms or web: "Ctrl"
|
||||
* Browser intercepts Cmd shortcuts, so we only show Cmd in Tauri desktop app.
|
||||
* Browser intercepts Cmd shortcuts, so we only show Cmd in the desktop app.
|
||||
*/
|
||||
export const getModifierLabel = (): string => {
|
||||
return isMacOS() && isTauriShell() ? '⌘' : 'Ctrl';
|
||||
return isMacOS() && isDesktopShell() ? '⌘' : 'Ctrl';
|
||||
};
|
||||
|
||||
export const truncatePathMiddle = (
|
||||
|
||||
Reference in New Issue
Block a user