fix(desktop): stop windows from adopting each other's active project
Every window shares one server settings document, and every PUT returns the merged whole, so one window's activeProjectId write was adopted by the other on its next unrelated settings save — its sidebar then auto-selected a session in that project and wrote the pointer back, converging both windows onto one session. settings-synced now carries an adoptWorkspace flag: only bootstrap-grade syncs (startup, runtime switch) may adopt the shared workspace pointers; reconcile responses keep the window's own active project while it exists. Notification clicks and session deep links also stopped broadcasting the session switch to every window.
This commit is contained in:
@@ -4,7 +4,7 @@ import { opencodeClient } from '@/lib/opencode/client';
|
||||
import { getRegisteredRuntimeAPIs } from '@/contexts/runtimeAPIRegistry';
|
||||
import type { ProjectEntry } from '@/lib/api/types';
|
||||
import type { DesktopSettings } from '@/lib/desktop';
|
||||
import { updateDesktopSettings } from '@/lib/persistence';
|
||||
import { type SettingsSyncedDetail, updateDesktopSettings } from '@/lib/persistence';
|
||||
import { createProjectIdFromPath } from '@/lib/projectId';
|
||||
import { getDeferredSafeStorage } from './utils/safeStorage';
|
||||
import { useDirectoryStore } from './useDirectoryStore';
|
||||
@@ -68,7 +68,7 @@ interface ProjectsStore {
|
||||
reorderProjects: (fromIndex: number, toIndex: number) => void;
|
||||
resetForRuntimeSwitch: () => void;
|
||||
validateProjectPath: (path: string) => ProjectPathValidationResult;
|
||||
synchronizeFromSettings: (settings: DesktopSettings) => void;
|
||||
synchronizeFromSettings: (settings: DesktopSettings, options?: { adoptActiveProject?: boolean }) => void;
|
||||
syncVSCodeWorkspaceFolders: (folders: VSCodeWorkspaceFolderConfig[], activePath?: string | null) => ProjectEntry | null;
|
||||
getActiveProject: () => ProjectEntry | null;
|
||||
}
|
||||
@@ -809,7 +809,7 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as { settings?: DesktopSettings } | null;
|
||||
if (payload?.settings) {
|
||||
get().synchronizeFromSettings(payload.settings);
|
||||
get().synchronizeFromSettings(payload.settings, { adoptActiveProject: false });
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
@@ -838,7 +838,7 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
|
||||
const payload = (await response.json().catch(() => null)) as { settings?: DesktopSettings } | null;
|
||||
if (payload?.settings) {
|
||||
get().synchronizeFromSettings(payload.settings);
|
||||
get().synchronizeFromSettings(payload.settings, { adoptActiveProject: false });
|
||||
}
|
||||
return { ok: true };
|
||||
} catch (error) {
|
||||
@@ -874,7 +874,7 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
}
|
||||
|
||||
if (payload?.settings) {
|
||||
get().synchronizeFromSettings(payload.settings);
|
||||
get().synchronizeFromSettings(payload.settings, { adoptActiveProject: false });
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -924,32 +924,43 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
set({ projects, activeProjectId: nextActiveProjectId, manualProjectOrder: [] });
|
||||
},
|
||||
|
||||
synchronizeFromSettings: (settings: DesktopSettings) => {
|
||||
synchronizeFromSettings: (settings: DesktopSettings, options?: { adoptActiveProject?: boolean }) => {
|
||||
if (isVSCodeProjectsRuntime) {
|
||||
return;
|
||||
}
|
||||
const adoptActiveProject = options?.adoptActiveProject !== false;
|
||||
const incomingProjects = sanitizeProjects(settings.projects ?? []);
|
||||
const incomingActive = typeof settings.activeProjectId === 'string' && settings.activeProjectId.trim()
|
||||
? settings.activeProjectId.trim()
|
||||
: null;
|
||||
|
||||
const current = get();
|
||||
const incomingIds = new Set(incomingProjects.map((p) => p.id));
|
||||
|
||||
// The settings document is shared by every window on this server, so
|
||||
// outside a bootstrap sync the incoming active pointer is just another
|
||||
// window's choice — the project LIST still reconciles, but this
|
||||
// window's active project stays its own while it remains valid.
|
||||
const nextActive = adoptActiveProject
|
||||
? incomingActive
|
||||
: (current.activeProjectId && incomingIds.has(current.activeProjectId)
|
||||
? current.activeProjectId
|
||||
: incomingActive);
|
||||
|
||||
const projectsChanged = JSON.stringify(current.projects) !== JSON.stringify(incomingProjects);
|
||||
const activeChanged = current.activeProjectId !== incomingActive;
|
||||
const activeChanged = current.activeProjectId !== nextActive;
|
||||
|
||||
if (!projectsChanged && !activeChanged) {
|
||||
return;
|
||||
}
|
||||
|
||||
const incomingIds = new Set(incomingProjects.map((p) => p.id));
|
||||
const cleanedOrder = get().manualProjectOrder.filter((id) => incomingIds.has(id));
|
||||
set({ projects: incomingProjects, activeProjectId: incomingActive, manualProjectOrder: cleanedOrder });
|
||||
cacheProjects(incomingProjects, incomingActive);
|
||||
set({ projects: incomingProjects, activeProjectId: nextActive, manualProjectOrder: cleanedOrder });
|
||||
cacheProjects(incomingProjects, nextActive);
|
||||
persistManualProjectOrder(cleanedOrder);
|
||||
|
||||
if (incomingActive) {
|
||||
const activeProject = incomingProjects.find((project) => project.id === incomingActive);
|
||||
if (activeChanged && nextActive) {
|
||||
const activeProject = incomingProjects.find((project) => project.id === nextActive);
|
||||
if (activeProject) {
|
||||
opencodeClient.setDirectory(activeProject.path);
|
||||
useDirectoryStore.getState().setDirectory(activeProject.path, { showOverlay: false });
|
||||
@@ -1005,9 +1016,11 @@ export const useProjectsStore = create<ProjectsStore>()(
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('openchamber:settings-synced', (event: Event) => {
|
||||
const detail = (event as CustomEvent<DesktopSettings>).detail;
|
||||
if (detail && typeof detail === 'object') {
|
||||
useProjectsStore.getState().synchronizeFromSettings(detail);
|
||||
const detail = (event as CustomEvent<SettingsSyncedDetail>).detail;
|
||||
if (detail && typeof detail === 'object' && detail.settings) {
|
||||
useProjectsStore.getState().synchronizeFromSettings(detail.settings, {
|
||||
adoptActiveProject: detail.adoptWorkspace,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user