feat: Add desktop Open In button (#350)

* feat: add OpenInAppButton and macOS path opener

* feat: filter installed apps on macOS and use in OpenInAppButton

* feat: fetch and display macOS app icons in OpenInAppButton

* feat(desktop): cache and fetch installed macOS apps

* feat: add force refresh and retry for installed apps

* feat(OpenInAppButton): add Copy Path action in dropdown header

* feat: enhance caching mechanism for apps discovery

---------

Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
Iuliia Ivashko
2026-02-08 04:32:14 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 9534e3d016
commit b432437b02
14 changed files with 1140 additions and 9 deletions
+1
View File
@@ -451,6 +451,7 @@ export interface SettingsPayload {
diffViewMode?: 'single' | 'stacked';
directoryShowHidden?: boolean;
filesViewShowGitignored?: boolean;
openInAppId?: string;
[key: string]: unknown;
}
+138
View File
@@ -59,6 +59,7 @@ export type DesktopSettings = {
defaultVariant?: string;
defaultAgent?: string;
defaultGitIdentityId?: string; // ''/undefined = unset, 'global' or profile id
openInAppId?: string;
autoCreateWorktree?: boolean;
queueModeEnabled?: boolean;
gitmojiEnabled?: boolean;
@@ -322,3 +323,140 @@ export const restartToApplyUpdate = async (): Promise<boolean> => {
return false;
}
};
export const openDesktopPath = async (path: string, app?: string | null): Promise<boolean> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
return false;
}
const trimmed = path?.trim();
if (!trimmed) {
return false;
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
await tauri?.core?.invoke?.('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);
return false;
}
};
export const filterInstalledDesktopApps = async (apps: string[]): Promise<string[]> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
return [];
}
const candidate = Array.isArray(apps) ? apps.filter((value) => typeof value === 'string') : [];
if (candidate.length === 0) {
return [];
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('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);
return [];
}
};
export const fetchDesktopAppIcons = async (apps: string[]): Promise<Record<string, string>> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
return {};
}
const candidate = Array.isArray(apps) ? apps.filter((value) => typeof value === 'string') : [];
if (candidate.length === 0) {
return {};
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('desktop_fetch_app_icons', {
apps: candidate,
});
if (!Array.isArray(result)) {
return {};
}
const map: Record<string, string> = {};
for (const entry of result) {
if (!entry || typeof entry !== 'object') continue;
const candidateEntry = entry as { app?: unknown; data_url?: unknown };
if (typeof candidateEntry.app !== 'string' || typeof candidateEntry.data_url !== 'string') continue;
map[candidateEntry.app] = candidateEntry.data_url;
}
return map;
} catch (error) {
console.warn('Failed to fetch installed app icons (tauri)', error);
return {};
}
};
export type InstalledDesktopAppInfo = {
name: string;
iconDataUrl?: string | null;
};
export type FetchDesktopInstalledAppsResult = {
apps: InstalledDesktopAppInfo[];
success: boolean;
hasCache: boolean;
isCacheStale: boolean;
};
export const fetchDesktopInstalledApps = async (
apps: string[],
force?: boolean
): Promise<FetchDesktopInstalledAppsResult> => {
if (!isTauriShell() || !isDesktopLocalOriginActive()) {
return { apps: [], success: false, hasCache: false, isCacheStale: false };
}
const candidate = Array.isArray(apps) ? apps.filter((value) => typeof value === 'string') : [];
if (candidate.length === 0) {
return { apps: [], success: true, hasCache: false, isCacheStale: false };
}
try {
const tauri = (window as unknown as { __TAURI__?: TauriGlobal }).__TAURI__;
const result = await tauri?.core?.invoke?.('desktop_get_installed_apps', {
apps: candidate,
force: force === true ? true : undefined,
});
if (!result || typeof result !== 'object') {
return { apps: [], success: false, hasCache: false, isCacheStale: false };
}
const payload = result as { apps?: unknown; hasCache?: unknown; isCacheStale?: unknown };
if (!Array.isArray(payload.apps)) {
return { apps: [], success: false, hasCache: false, isCacheStale: false };
}
const installedApps = payload.apps
.filter((entry) => entry && typeof entry === 'object')
.map((entry) => {
const record = entry as { name?: unknown; iconDataUrl?: unknown };
return {
name: typeof record.name === 'string' ? record.name : '',
iconDataUrl: typeof record.iconDataUrl === 'string' ? record.iconDataUrl : null,
};
})
.filter((entry) => entry.name.length > 0);
return {
apps: installedApps,
success: true,
hasCache: payload.hasCache === true,
isCacheStale: payload.isCacheStale === true,
};
} catch (error) {
console.warn('Failed to fetch installed apps (tauri)', error);
return { apps: [], success: false, hasCache: false, isCacheStale: false };
}
};
+8
View File
@@ -71,6 +71,11 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
if (typeof settings.filesViewShowGitignored === 'boolean') {
localStorage.setItem('filesViewShowGitignored', settings.filesViewShowGitignored ? 'true' : 'false');
}
if (typeof settings.openInAppId === 'string' && settings.openInAppId.length > 0) {
localStorage.setItem('openInAppId', settings.openInAppId);
} else {
localStorage.removeItem('openInAppId');
}
};
type PersistApi = {
@@ -471,6 +476,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
if (typeof candidate.filesViewShowGitignored === 'boolean') {
result.filesViewShowGitignored = candidate.filesViewShowGitignored;
}
if (typeof candidate.openInAppId === 'string' && candidate.openInAppId.length > 0) {
result.openInAppId = candidate.openInAppId;
}
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {
result.memoryLimitHistorical = candidate.memoryLimitHistorical;