feat: validate and persist openInAppId updates from settings

This commit is contained in:
Bohdan Triapitsyn
2026-02-08 15:11:20 +02:00
parent b4f4ee58e5
commit 148b55f66b
5 changed files with 47 additions and 24 deletions
@@ -47,7 +47,7 @@ const OPEN_IN_APPS: OpenInAppOption[] = [
{ id: 'trae', label: 'Trae', appName: 'Trae' }, { id: 'trae', label: 'Trae', appName: 'Trae' },
]; ];
const DEFAULT_APP_ID = 'vscode'; const DEFAULT_APP_ID = 'finder';
const ALWAYS_AVAILABLE_APP_IDS = new Set(['finder', 'terminal']); const ALWAYS_AVAILABLE_APP_IDS = new Set(['finder', 'terminal']);
const getAlwaysAvailableApps = () => OPEN_IN_APPS.filter((app) => ALWAYS_AVAILABLE_APP_IDS.has(app.id)); const getAlwaysAvailableApps = () => OPEN_IN_APPS.filter((app) => ALWAYS_AVAILABLE_APP_IDS.has(app.id));
@@ -121,9 +121,17 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
const handler = (event: Event) => { const handler = (event: Event) => {
const detail = (event as CustomEvent<DesktopSettings>).detail; const detail = (event as CustomEvent<DesktopSettings>).detail;
if (detail && typeof detail.openInAppId === 'string' && detail.openInAppId.length > 0) { const nextId = detail
setSelectedAppId(detail.openInAppId); && typeof detail.openInAppId === 'string'
&& detail.openInAppId.length > 0
&& OPEN_IN_APPS.some((app) => app.id === detail.openInAppId)
? detail.openInAppId
: null;
if (!nextId) {
return;
} }
window.localStorage.setItem('openInAppId', nextId);
setSelectedAppId(nextId);
}; };
window.addEventListener('openchamber:settings-synced', handler); window.addEventListener('openchamber:settings-synced', handler);
return () => window.removeEventListener('openchamber:settings-synced', handler); return () => window.removeEventListener('openchamber:settings-synced', handler);
@@ -174,10 +182,7 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
retryTimeoutRef.current = null; retryTimeoutRef.current = null;
} }
if (force) { if (force) {
console.info('[open-in] manual refresh requested');
setLoadedState(false); setLoadedState(false);
} else {
console.info('[open-in] load installed apps');
} }
isLoadingRef.current = true; isLoadingRef.current = true;
setIsScanning(true); setIsScanning(true);
@@ -191,7 +196,6 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
} = await fetchDesktopInstalledApps(appNames, force); } = await fetchDesktopInstalledApps(appNames, force);
if (!isMountedRef.current) return; if (!isMountedRef.current) return;
setIsCacheStale(hasCache ? nextCacheStale : false); setIsCacheStale(hasCache ? nextCacheStale : false);
console.info('[open-in] installed apps returned', installed.map((app) => app.name));
applyInstalledApps(installed); applyInstalledApps(installed);
if (success) { if (success) {
if (!hasCache && installed.length === 0 && retryAttemptRef.current < 3) { if (!hasCache && installed.length === 0 && retryAttemptRef.current < 3) {
@@ -230,14 +234,12 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
void loadInstalledApps(); void loadInstalledApps();
const handler = () => { const handler = () => {
console.info('[open-in] app ready, starting installed app scan');
void loadInstalledApps(); void loadInstalledApps();
}; };
window.addEventListener('openchamber:app-ready', handler); window.addEventListener('openchamber:app-ready', handler);
const updateHandler = (event: Event) => { const updateHandler = (event: Event) => {
const detail = (event as CustomEvent<InstalledDesktopAppInfo[]>).detail; const detail = (event as CustomEvent<InstalledDesktopAppInfo[]>).detail;
if (Array.isArray(detail)) { if (Array.isArray(detail)) {
console.info('[open-in] received installed app update', detail.length);
retryAttemptRef.current = 3; retryAttemptRef.current = 3;
keepScanningRef.current = false; keepScanningRef.current = false;
setIsScanning(false); setIsScanning(false);
@@ -248,7 +250,6 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
window.addEventListener('openchamber:installed-apps-updated', updateHandler); window.addEventListener('openchamber:installed-apps-updated', updateHandler);
const flag = (window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady; const flag = (window as unknown as { __openchamberAppReady?: boolean }).__openchamberAppReady;
if (flag) { if (flag) {
console.info('[open-in] app ready flag already set');
void loadInstalledApps(); void loadInstalledApps();
} }
return () => { return () => {
@@ -262,22 +263,22 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
if (typeof window === 'undefined') return; if (typeof window === 'undefined') return;
const fallbackTimer = window.setTimeout(() => { const fallbackTimer = window.setTimeout(() => {
if (!hasLoadedAppsRef.current) { if (!hasLoadedAppsRef.current) {
console.info('[open-in] fallback scan triggered');
void loadInstalledApps(); void loadInstalledApps();
} }
}, 5000); }, 5000);
return () => window.clearTimeout(fallbackTimer); return () => window.clearTimeout(fallbackTimer);
}, [isDesktopLocal, loadInstalledApps]); }, [isDesktopLocal, loadInstalledApps]);
const selectedApp = availableApps.find((app) => app.id === selectedAppId) ?? availableApps[0]; const selectedApp = React.useMemo(() => {
const known = OPEN_IN_APPS.find((app) => app.id === selectedAppId)
React.useEffect(() => { ?? OPEN_IN_APPS.find((app) => app.id === DEFAULT_APP_ID)
if (!selectedApp) return; ?? OPEN_IN_APPS[0];
if (selectedAppId !== selectedApp.id) { if (known) {
setSelectedAppId(selectedApp.id); const iconDataUrl = availableApps.find((app) => app.appName === known.appName)?.iconDataUrl;
void updateDesktopSettings({ openInAppId: selectedApp.id }); return iconDataUrl ? { ...known, iconDataUrl } : known;
} }
}, [selectedApp, selectedAppId]); return availableApps[0];
}, [availableApps, selectedAppId]);
if (!isDesktopLocal || !directory) { if (!isDesktopLocal || !directory) {
return null; return null;
@@ -293,6 +294,9 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
const handleSelect = async (app: OpenInAppOption) => { const handleSelect = async (app: OpenInAppOption) => {
setSelectedAppId(app.id); setSelectedAppId(app.id);
if (typeof window !== 'undefined') {
window.localStorage.setItem('openInAppId', app.id);
}
await updateDesktopSettings({ openInAppId: app.id }); await updateDesktopSettings({ openInAppId: app.id });
await handleOpen(app); await handleOpen(app);
}; };
@@ -348,7 +352,9 @@ export const OpenInAppButton = ({ directory, className }: OpenInAppButtonProps)
iconDataUrl={selectedApp.iconDataUrl} iconDataUrl={selectedApp.iconDataUrl}
fallbackIconDataUrl={selectedApp.fallbackIconDataUrl} fallbackIconDataUrl={selectedApp.fallbackIconDataUrl}
/> />
<span className={cn(isScanning ? 'animate-pulse text-muted-foreground' : undefined)}>Open</span> <span className={cn('header-open-label', isScanning ? 'animate-pulse text-muted-foreground' : undefined)}>
Open
</span>
</button> </button>
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
+9 -2
View File
@@ -237,9 +237,16 @@ export const Header: React.FC = () => {
return normalize(raw || ''); return normalize(raw || '');
}, [currentSession?.directory]); }, [currentSession?.directory]);
const draftDirectory = useSessionStore((state) => {
if (!state.newSessionDraft?.open) {
return '';
}
return normalize(state.newSessionDraft.directoryOverride ?? '');
});
const openDirectory = React.useMemo(() => { const openDirectory = React.useMemo(() => {
return worktreeDirectory || sessionDirectory; return worktreeDirectory || sessionDirectory || draftDirectory;
}, [sessionDirectory, worktreeDirectory]); }, [draftDirectory, sessionDirectory, worktreeDirectory]);
const [planTabAvailable, setPlanTabAvailable] = React.useState(false); const [planTabAvailable, setPlanTabAvailable] = React.useState(false);
+6
View File
@@ -463,11 +463,17 @@ html:not(.dark) .chat-scroll {
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label { :root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
display: inline; display: inline;
} }
:root:not(.mobile-pointer):not(.vscode-runtime) .header-open-label {
display: inline;
}
@media (max-width: 940px) { @media (max-width: 940px) {
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label { :root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-label {
display: none; display: none;
} }
:root:not(.mobile-pointer):not(.vscode-runtime) .header-open-label {
display: none;
}
:root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-badge { :root:not(.mobile-pointer):not(.vscode-runtime) .header-tab-badge {
margin-left: -0.125rem; margin-left: -0.125rem;
} }
-2
View File
@@ -73,8 +73,6 @@ const persistToLocalStorage = (settings: DesktopSettings) => {
} }
if (typeof settings.openInAppId === 'string' && settings.openInAppId.length > 0) { if (typeof settings.openInAppId === 'string' && settings.openInAppId.length > 0) {
localStorage.setItem('openInAppId', settings.openInAppId); localStorage.setItem('openInAppId', settings.openInAppId);
} else {
localStorage.removeItem('openInAppId');
} }
}; };
+6
View File
@@ -1094,6 +1094,12 @@ const sanitizeSettingsUpdate = (payload) => {
if (typeof candidate.filesViewShowGitignored === 'boolean') { if (typeof candidate.filesViewShowGitignored === 'boolean') {
result.filesViewShowGitignored = candidate.filesViewShowGitignored; result.filesViewShowGitignored = candidate.filesViewShowGitignored;
} }
if (typeof candidate.openInAppId === 'string') {
const trimmed = candidate.openInAppId.trim();
if (trimmed.length > 0) {
result.openInAppId = trimmed;
}
}
// Memory limits for message viewport management // Memory limits for message viewport management
if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) { if (typeof candidate.memoryLimitHistorical === 'number' && Number.isFinite(candidate.memoryLimitHistorical)) {