mirror of
https://github.com/ValueCell-ai/ClawX.git
synced 2026-08-14 08:53:09 +00:00
fix(chat): inherit workspace when creating a new chat (#1186)
This commit is contained in:
@@ -1,13 +1,29 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { resolveEffectiveWorkspace } from '@/lib/workspace-context';
|
||||
import { useChatStore } from '@/stores/chat';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
|
||||
export function useNewChatAction(): () => void {
|
||||
const navigate = useNavigate();
|
||||
const newSession = useChatStore((state) => state.newSession);
|
||||
const setChatWorkspacePath = useSettingsStore((state) => state.setChatWorkspacePath);
|
||||
|
||||
return useCallback(() => {
|
||||
const { currentSessionKey, sessions } = useChatStore.getState();
|
||||
const selectedSession = sessions.find((session) => session.key === currentSessionKey);
|
||||
|
||||
// Start the draft in the selected conversation's effective workspace while
|
||||
// keeping the workspace picker editable until the first message creates it.
|
||||
if (selectedSession) {
|
||||
const selectedWorkspacePath = resolveEffectiveWorkspace({
|
||||
session: selectedSession,
|
||||
globalWorkspace: useSettingsStore.getState().chatWorkspacePath,
|
||||
}).cwd;
|
||||
setChatWorkspacePath(selectedWorkspacePath);
|
||||
}
|
||||
|
||||
newSession();
|
||||
navigate('/');
|
||||
}, [navigate, newSession]);
|
||||
}, [navigate, newSession, setChatWorkspacePath]);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ const SESSION_KEY = 'agent:main:session-a';
|
||||
const SESSION_WORKSPACE = '/Users/e2e/workspace/ClawX';
|
||||
const SESSION_WORKSPACE_LABEL = '~/workspace/ClawX';
|
||||
const GLOBAL_WORKSPACE = '/Users/e2e/workspace/GlobalProject';
|
||||
const GLOBAL_WORKSPACE_LABEL = '~/workspace/GlobalProject';
|
||||
const DEFAULT_WORKSPACE = '~/.openclaw/workspace';
|
||||
const AUTO_TITLE_WITH_CWD = `[Working directory: ${DEFAULT_WORKSPACE}]\n\nWorkspace chat`;
|
||||
const SESSIONS_LIST_PAYLOAD = {
|
||||
@@ -106,11 +105,16 @@ async function installWorkspaceMocks(app: ElectronApplication, options: Workspac
|
||||
const nowMs = Date.now();
|
||||
const gatewayStatus = { state: 'running', gatewayReady: true, port: 18789, pid: 12345, connectedAt: nowMs };
|
||||
const sessionHistory = options.sessionHistory ?? [];
|
||||
const recentWorkspacePaths = options.recentWorkspacePaths ?? [DEFAULT_WORKSPACE];
|
||||
const inheritedRecentWorkspacePaths = [
|
||||
SESSION_WORKSPACE,
|
||||
...recentWorkspacePaths.filter((path) => path !== SESSION_WORKSPACE),
|
||||
].slice(0, 10);
|
||||
const settingsSnapshot = {
|
||||
language: 'en',
|
||||
setupComplete: true,
|
||||
chatWorkspacePath: options.chatWorkspacePath ?? DEFAULT_WORKSPACE,
|
||||
recentWorkspacePaths: options.recentWorkspacePaths ?? [DEFAULT_WORKSPACE],
|
||||
recentWorkspacePaths,
|
||||
workspaceLabels: options.workspaceLabels ?? {},
|
||||
};
|
||||
const sessionRow = {
|
||||
@@ -163,6 +167,12 @@ async function installWorkspaceMocks(app: ElectronApplication, options: Workspac
|
||||
[stableStringify(['settings', 'setMany', {
|
||||
patch: { workspaceLabels: { [SESSION_WORKSPACE]: 'Renamed workspace' } },
|
||||
}])]: { success: true },
|
||||
[stableStringify(['settings', 'setMany', {
|
||||
patch: {
|
||||
chatWorkspacePath: SESSION_WORKSPACE,
|
||||
recentWorkspacePaths: inheritedRecentWorkspacePaths,
|
||||
},
|
||||
}])]: { success: true },
|
||||
[stableStringify(['settings', 'setMany', {
|
||||
patch: {
|
||||
chatWorkspacePath: options.chatWorkspacePath ?? DEFAULT_WORKSPACE,
|
||||
@@ -285,6 +295,44 @@ test.describe('ClawX chat workspace context', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('new chat inherits the selected conversation workspace', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
await installWorkspaceMocks(app);
|
||||
|
||||
const page = await getStableWindow(app);
|
||||
try {
|
||||
await page.reload();
|
||||
} catch (error) {
|
||||
if (!String(error).includes('ERR_FILE_NOT_FOUND')) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceSelector = page.getByTestId('chat-workspace-selector');
|
||||
await expect(workspaceSelector).toHaveText(SESSION_WORKSPACE_LABEL, { timeout: 30_000 });
|
||||
await expect(workspaceSelector).toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
await page.getByTestId('sidebar-new-chat').click();
|
||||
|
||||
await expect.poll(async () => {
|
||||
const invocations = await getRecordedHostInvocations(app);
|
||||
return invocations.some((entry) => (
|
||||
entry.module === 'settings'
|
||||
&& entry.action === 'setMany'
|
||||
&& entry.payload?.patch?.chatWorkspacePath === SESSION_WORKSPACE
|
||||
));
|
||||
}).toBe(true);
|
||||
await expect(page.getByTestId('acp-chat-empty-state')).toBeVisible();
|
||||
await expect(workspaceSelector).toHaveText(SESSION_WORKSPACE_LABEL);
|
||||
await expect(workspaceSelector).toHaveAttribute('title', SESSION_WORKSPACE);
|
||||
await expect(workspaceSelector).not.toHaveAttribute('aria-disabled', 'true');
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('host summary title stays clean when a derived title is unavailable', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
@@ -313,7 +361,7 @@ test.describe('ClawX chat workspace context', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('new unbound chat stays hidden until it has content and then appears under the selected global workspace group', async ({ launchElectronApp }) => {
|
||||
test('new unbound chat stays hidden until it has content and prefers the selected conversation workspace over the global workspace', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
@@ -335,17 +383,17 @@ test.describe('ClawX chat workspace context', () => {
|
||||
await expect(workspaceSelector).toHaveText(SESSION_WORKSPACE_LABEL, { timeout: 30_000 });
|
||||
|
||||
const sidebar = page.getByTestId('sidebar');
|
||||
const globalWorkspaceGroup = sidebar.getByTestId(workspaceSessionGroupTestId(GLOBAL_WORKSPACE));
|
||||
const selectedWorkspaceGroup = sidebar.getByTestId(workspaceSessionGroupTestId(SESSION_WORKSPACE));
|
||||
|
||||
await expect(async () => {
|
||||
await page.getByTestId('sidebar-new-chat').click();
|
||||
await expect(globalWorkspaceGroup.getByText(/agent:main:session-/)).toHaveCount(0, { timeout: 500 });
|
||||
await expect(selectedWorkspaceGroup.getByText(/agent:main:session-/)).toHaveCount(0, { timeout: 500 });
|
||||
}).toPass({ timeout: 30_000 });
|
||||
|
||||
await expect(page.getByTestId('acp-chat-empty-state')).toBeVisible();
|
||||
|
||||
await expect(workspaceSelector).toHaveText(GLOBAL_WORKSPACE_LABEL);
|
||||
await expect(workspaceSelector).toHaveAttribute('title', GLOBAL_WORKSPACE);
|
||||
await expect(workspaceSelector).toHaveText(SESSION_WORKSPACE_LABEL);
|
||||
await expect(workspaceSelector).toHaveAttribute('title', SESSION_WORKSPACE);
|
||||
await expect(workspaceSelector).not.toHaveAttribute('aria-disabled', 'true');
|
||||
|
||||
const defaultWorkspaceGroupWithPendingSession = sidebar.getByTestId(workspaceSessionGroupTestId(DEFAULT_WORKSPACE))
|
||||
@@ -356,14 +404,14 @@ test.describe('ClawX chat workspace context', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('missing global workspace prompts for another folder without attempting ACP creation', async ({ launchElectronApp }) => {
|
||||
test('missing inherited workspace prompts for another folder without attempting ACP creation', async ({ launchElectronApp }) => {
|
||||
const app = await launchElectronApp({ skipSetup: true });
|
||||
|
||||
try {
|
||||
await installWorkspaceMocks(app, {
|
||||
chatWorkspacePath: GLOBAL_WORKSPACE,
|
||||
recentWorkspacePaths: [GLOBAL_WORKSPACE, DEFAULT_WORKSPACE],
|
||||
unavailableWorkspacePath: GLOBAL_WORKSPACE,
|
||||
unavailableWorkspacePath: SESSION_WORKSPACE,
|
||||
});
|
||||
|
||||
const page = await getStableWindow(app);
|
||||
@@ -378,7 +426,7 @@ test.describe('ClawX chat workspace context', () => {
|
||||
});
|
||||
await expect(async () => {
|
||||
await page.getByTestId('sidebar-new-chat').click();
|
||||
await expect(page.getByTestId('chat-workspace-selector')).toHaveText(GLOBAL_WORKSPACE_LABEL, {
|
||||
await expect(page.getByTestId('chat-workspace-selector')).toHaveText(SESSION_WORKSPACE_LABEL, {
|
||||
timeout: 1_000,
|
||||
});
|
||||
}).toPass({ timeout: 30_000 });
|
||||
@@ -386,7 +434,7 @@ test.describe('ClawX chat workspace context', () => {
|
||||
const banner = page.getByTestId('workspace-unavailable-banner');
|
||||
await expect(banner).toBeVisible();
|
||||
await expect(banner).toContainText('Workspace unavailable');
|
||||
await expect(banner).toContainText(GLOBAL_WORKSPACE);
|
||||
await expect(banner).toContainText(SESSION_WORKSPACE);
|
||||
await expect(banner.getByRole('button', { name: 'Choose workspace' })).toBeVisible();
|
||||
|
||||
await expect.poll(async () => {
|
||||
@@ -394,7 +442,7 @@ test.describe('ClawX chat workspace context', () => {
|
||||
return invocations.filter((entry) => (
|
||||
entry.module === 'chat'
|
||||
&& entry.action === 'loadAcpSession'
|
||||
&& entry.payload?.cwd === GLOBAL_WORKSPACE
|
||||
&& entry.payload?.cwd === SESSION_WORKSPACE
|
||||
)).length;
|
||||
}).toBe(0);
|
||||
} finally {
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { chatState, navigateMock } = vi.hoisted(() => ({
|
||||
const { chatState, navigateMock, settingsState } = vi.hoisted(() => ({
|
||||
chatState: {
|
||||
messages: [] as unknown[],
|
||||
currentSessionKey: 'agent:main:main',
|
||||
sessions: [] as Array<{ key: string; workspacePath?: string; createdLocally?: boolean }>,
|
||||
newSession: vi.fn(),
|
||||
},
|
||||
navigateMock: vi.fn(),
|
||||
settingsState: {
|
||||
chatWorkspacePath: '~/.openclaw/workspace',
|
||||
setChatWorkspacePath: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('react-router-dom', () => ({
|
||||
@@ -21,10 +27,22 @@ vi.mock('@/stores/chat', () => {
|
||||
return { useChatStore };
|
||||
});
|
||||
|
||||
vi.mock('@/stores/settings', () => {
|
||||
const useSettingsStore = Object.assign(
|
||||
(selector: (state: typeof settingsState) => unknown) => selector(settingsState),
|
||||
{ getState: () => settingsState },
|
||||
);
|
||||
return { useSettingsStore };
|
||||
});
|
||||
|
||||
describe('useNewChatAction', () => {
|
||||
beforeEach(() => {
|
||||
chatState.messages = [];
|
||||
chatState.currentSessionKey = 'agent:main:main';
|
||||
chatState.sessions = [];
|
||||
chatState.newSession.mockReset();
|
||||
settingsState.chatWorkspacePath = '~/.openclaw/workspace';
|
||||
settingsState.setChatWorkspacePath.mockReset();
|
||||
navigateMock.mockReset();
|
||||
});
|
||||
|
||||
@@ -34,6 +52,24 @@ describe('useNewChatAction', () => {
|
||||
|
||||
act(() => result.current());
|
||||
|
||||
expect(settingsState.setChatWorkspacePath).not.toHaveBeenCalled();
|
||||
expect(chatState.newSession).toHaveBeenCalledTimes(1);
|
||||
expect(navigateMock).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
it('inherits the selected conversation workspace', async () => {
|
||||
chatState.currentSessionKey = 'agent:main:session-a';
|
||||
chatState.sessions = [{
|
||||
key: chatState.currentSessionKey,
|
||||
workspacePath: '/Users/e2e/workspace/ClawX',
|
||||
}];
|
||||
|
||||
const { useNewChatAction } = await import('@/components/layout/use-new-chat-action');
|
||||
const { result } = renderHook(() => useNewChatAction());
|
||||
|
||||
act(() => result.current());
|
||||
|
||||
expect(settingsState.setChatWorkspacePath).toHaveBeenCalledWith('/Users/e2e/workspace/ClawX');
|
||||
expect(chatState.newSession).toHaveBeenCalledTimes(1);
|
||||
expect(navigateMock).toHaveBeenCalledWith('/');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user