feat: validate and persist openInAppId updates from settings
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user