Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/desktop/src/main/ipc/workspace-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ export function registerWorkspaceHandlers(): void {
await migrateWorkspace(oldPath, newWorkspacePath);
reinitDatabase(newWorkspacePath);
updateConfig({ workspacePath: newWorkspacePath });
const win = BrowserWindow.getAllWindows()[0];
if (win) win.webContents.send('workspace:changed', newWorkspacePath);
Comment on lines +59 to +60
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using BrowserWindow.getAllWindows()[0] is unreliable as it only targets the first window found by the runtime. In a multi-window environment (e.g., if the user has detached chat windows or multiple instances), other windows will not receive the workspace:changed event and will continue to display stale data from the previous workspace. It is better to broadcast the event to all open windows.

Suggested change
const win = BrowserWindow.getAllWindows()[0];
if (win) win.webContents.send('workspace:changed', newWorkspacePath);
BrowserWindow.getAllWindows().forEach((win) => {
win.webContents.send('workspace:changed', newWorkspacePath);
});

return { ok: true };
} catch (err) {
reinitDatabase(oldPath);
Expand Down
1 change: 1 addition & 0 deletions packages/desktop/src/preload/clawwork.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ export interface ClawWorkAPI {
browseWorkspace: () => Promise<string | null>;
setupWorkspace: (path: string) => Promise<IpcResult>;
changeWorkspace: (path: string) => Promise<IpcResult>;
onWorkspaceChanged: (callback: (newPath: string) => void) => () => void;

getSettings: () => Promise<AppSettings | null>;
updateSettings: (partial: Partial<AppSettings>) => Promise<{ ok: boolean; config: AppSettings }>;
Expand Down
10 changes: 10 additions & 0 deletions packages/desktop/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,16 @@ function buildApi(): ClawWorkAPI {

setWindowButtonVisibility: (visible: boolean) => ipcRenderer.send('ui:set-window-button-visibility', visible),

onWorkspaceChanged: (callback) => {
const listener = (_event: Electron.IpcRendererEvent, newPath: string): void => {
callback(newPath);
};
ipcRenderer.on('workspace:changed', listener);
return () => {
ipcRenderer.removeListener('workspace:changed', listener);
};
},

getDeviceId: () => ipcRenderer.invoke('workspace:get-device-id') as Promise<string>,

selectContextFolder: () => ipcRenderer.invoke('context:select-folder'),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { useState, useEffect, useCallback } from 'react';
import { useTaskStore } from '../../../platform';
import { useMessageStore } from '../../../platform';
import { useDashboardStore } from '@/stores/dashboardStore';
import { useUsageStore } from '@/stores/usageStore';
import { useApprovalStore } from '@/stores/approvalStore';
import { MonitorDot, Zap, FolderOpen, Loader2, ExternalLink } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { toast } from '@/lib/toast';
Expand Down Expand Up @@ -64,6 +69,26 @@ export default function SystemSection() {
[quickLaunchShortcut, t],
);

// When main process signals workspace changed, clear and re-hydrate stores
useEffect(() => {
return window.clawwork.onWorkspaceChanged(async () => {
// Clear task store and re-hydrate from new DB
useTaskStore.setState({ tasks: [], activeTaskId: null, hydrated: false });
useTaskStore.getState().hydrate();

// Clear message store
useMessageStore.setState({ messagesByTask: {}, activeTurnBySession: {}, processingBySession: new Set() });

// Clear dashboard, usage, and approval caches
useDashboardStore.getState().clear();
useUsageStore.getState().clear();
useApprovalStore.getState().clear();

// Refresh settings to pick up new workspace config
await refreshSettings().catch(() => {});
});
}, [refreshSettings]);
Comment on lines +73 to +90
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The onWorkspaceChanged listener is currently tied to the lifecycle of the SystemSection component. This presents two architectural issues:

  1. Lifecycle Dependency: The stores will only refresh if the user is actively viewing the "System" settings section when the workspace change event is received. If they navigate away or close the settings before the IPC event arrives, the stores remain stale.
  2. Multi-window Consistency: In a multi-window setup, only the window that has the settings page open will refresh its stores. Other windows will remain out of sync.

Consider moving this subscription to a global location (e.g., a root layout component or a dedicated initialization hook) to ensure the application state is consistently updated across all windows and views. Once moved, the manual refreshSettings() call in handleChangeWorkspace (line 107) should be removed to avoid redundant network requests.

References
  1. Verify dependency direction and ensure architectural invariants are maintained across layers. (link)


const handleChangeWorkspace = useCallback(async () => {
let selected: string | null = null;
try {
Expand Down
Loading