diff --git a/wework/src/components/chat/ChatInput.test.tsx b/wework/src/components/chat/ChatInput.test.tsx index dd8f6297db..e4d162d22c 100644 --- a/wework/src/components/chat/ChatInput.test.tsx +++ b/wework/src/components/chat/ChatInput.test.tsx @@ -1070,6 +1070,56 @@ describe('ChatInput', () => { } }) + test('hides unsupported Gemini models from the desktop model menu', async () => { + const gptModel: UnifiedModel = { + name: 'overseas-gpt-5.5', + type: 'user', + displayName: '海外:gpt-5.5', + config: { + ui: { + family: 'gpt', + region: 'overseas', + modelLabel: 'gpt-5.5', + sortOrder: 10, + }, + }, + } + const geminiModel: UnifiedModel = { + name: 'overseas-gemini-3-pro', + type: 'public', + displayName: '海外:gemini-3-pro', + config: { + ui: { + family: 'gemini', + region: 'overseas', + modelLabel: 'gemini-3-pro', + sortOrder: 10, + }, + }, + } + + render( + , + ) + + await userEvent.click(screen.getByTestId('model-selector-button')) + + expect(screen.getByTestId('model-family-gpt')).toBeInTheDocument() + expect(screen.queryByTestId('model-family-gemini')).not.toBeInTheDocument() + expect(screen.queryByTestId('model-option-overseas-gemini-3-pro')).not.toBeInTheDocument() + }) + test('moves the desktop model submenu upward when the active family is near the viewport bottom', async () => { const originalInnerHeight = window.innerHeight Object.defineProperty(window, 'innerHeight', { diff --git a/wework/src/components/layout/DesktopSidebar.tsx b/wework/src/components/layout/DesktopSidebar.tsx index ec96f453a2..2742e9490a 100644 --- a/wework/src/components/layout/DesktopSidebar.tsx +++ b/wework/src/components/layout/DesktopSidebar.tsx @@ -84,7 +84,7 @@ interface DesktopSidebarProps { onGetProjectWorkspaceRoot: (deviceId: string) => Promise onListDeviceDirectories: (deviceId: string, path: string) => Promise onCreateDeviceDirectory: (deviceId: string, path: string) => Promise - onOpenSettings: () => void + onOpenSettings: (options?: { autoOpenAddCloudDeviceDialog?: boolean }) => void onLogout: () => void } @@ -1236,6 +1236,10 @@ export function DesktopSidebar({ mode={projectCreateMode ?? 'scratch'} devices={devices} onClose={() => setProjectCreateMode(null)} + onOpenCloudDeviceSettings={() => { + setProjectCreateMode(null) + onOpenSettings({ autoOpenAddCloudDeviceDialog: true }) + }} onCreateProject={onCreateProject} onCreateGitWorkspaceProject={onCreateGitWorkspaceProject} preferredDeviceId={preferredDeviceId} diff --git a/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx b/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx index 6ccd66161a..5c4fd19499 100644 --- a/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx +++ b/wework/src/components/layout/DesktopWorkbenchLayout.test.tsx @@ -220,50 +220,54 @@ describe('DesktopWorkbenchLayout', () => { onLogout: vi.fn(), } - const workspacePanelProject = { - id: 7, - name: 'project38', - tasks: [], - config: { - mode: 'workspace' as const, - execution: { - targetType: 'local' as const, - deviceId: 'device-1', - }, - workspace: { - source: 'local_path' as const, - localPath: '/workspace/projects/project38', - }, - }, - } - - const workspacePanelDevices = [ - { - id: 1, - device_id: 'device-1', - name: 'Cloud Device', + function createCloudWorkspacePanelState() { + const workspaceDevice = { + id: 11, + device_id: 'workspace-cloud-device', + name: 'Workspace Cloud Device', status: 'online' as const, is_default: false, device_type: 'cloud' as const, bind_shell: 'claudecode', - }, - ] + } + const workspaceProject = { + id: 12, + name: 'workspace-project', + tasks: [], + config: { + mode: 'workspace' as const, + execution: { + targetType: 'local' as const, + deviceId: workspaceDevice.device_id, + }, + workspace: { + source: 'local_path' as const, + localPath: '/workspace/project', + }, + }, + } + + return { + currentProject: workspaceProject, + projects: [workspaceProject], + devices: [workspaceDevice], + } + } function renderWorkspacePanelLayout() { + const workspacePanelState = createCloudWorkspacePanelState() return render( , ) @@ -1190,6 +1194,61 @@ describe('DesktopWorkbenchLayout', () => { expect(screen.getByTestId('create-project-button')).toHaveTextContent('创建项目') }) + test('opens connection settings cloud device creation from an empty sidebar project dialog', async () => { + const onRefreshDevices = vi.fn().mockResolvedValue(undefined) + + render() + + await userEvent.click(screen.getByTestId('projects-create-button')) + await userEvent.click(screen.getByTestId('project-start-from-scratch-button')) + + expect(screen.getByTestId('project-create-dialog')).toBeInTheDocument() + expect(screen.getByText('暂无可用设备')).toBeInTheDocument() + + const settingsLink = screen.getByTestId('open-cloud-device-settings-link') + expect(settingsLink).toHaveAttribute('href', '/settings') + + await userEvent.click(settingsLink) + + expect(screen.queryByTestId('project-create-dialog')).not.toBeInTheDocument() + expect(screen.getByTestId('wework-settings-page')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: '连接' })).toBeInTheDocument() + expect(screen.getByTestId('add-cloud-device-dialog')).toBeInTheDocument() + }) + + test('opens connection settings cloud device creation from an empty project dialog', async () => { + const onRefreshDevices = vi.fn().mockResolvedValue(undefined) + createDeviceApiMock.mockReturnValue({ + getAllDevices: vi.fn().mockResolvedValue([]), + getMetrics: vi.fn(), + getMetricsHistory: vi.fn(), + getVncConfig: vi.fn(), + createCloudDevice: vi.fn(), + startTerminal: vi.fn(), + startCodeServer: vi.fn(), + renameDevice: vi.fn(), + restartCloudDevice: vi.fn(), + deleteCloudDevice: vi.fn(), + deleteDevice: vi.fn(), + getHomeDirectory: vi.fn().mockResolvedValue('/home/ubuntu'), + getProjectWorkspaceRoot: vi.fn().mockResolvedValue('/workspace/projects'), + listDirectories: vi.fn().mockResolvedValue([]), + executeCommand: vi.fn(), + }) + + render() + + await userEvent.click(screen.getByTestId('project-work-button')) + await userEvent.click(screen.getByTestId('add-project-option')) + await userEvent.click(screen.getByTestId('project-start-from-scratch-option')) + await userEvent.click(screen.getByTestId('open-cloud-device-settings-link')) + + expect(screen.queryByTestId('project-create-dialog')).not.toBeInTheDocument() + expect(screen.getByTestId('wework-settings-page')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: '连接' })).toBeInTheDocument() + expect(screen.getByTestId('add-cloud-device-dialog')).toBeInTheDocument() + }) + test('creates a project from an existing folder selected in the directory tree', async () => { const onCreateProject = vi.fn().mockResolvedValue({ id: 2, name: 'repo', tasks: [] }) const onGetDeviceHomeDirectory = vi.fn().mockResolvedValue('/home/ubuntu') diff --git a/wework/src/components/layout/DesktopWorkbenchLayout.tsx b/wework/src/components/layout/DesktopWorkbenchLayout.tsx index 6f8a63a673..dc8198f3d3 100644 --- a/wework/src/components/layout/DesktopWorkbenchLayout.tsx +++ b/wework/src/components/layout/DesktopWorkbenchLayout.tsx @@ -153,6 +153,8 @@ export function DesktopWorkbenchLayout({ const [settingsOpen, setSettingsOpen] = useState(() => isSettingsRoute(stripAppBasePath(window.location.pathname)) ) + const [autoOpenAddCloudDeviceDialog, setAutoOpenAddCloudDeviceDialog] = + useState(false) const [projectWorkCreateMode, setProjectWorkCreateMode] = useState(null) const [environmentInfo, setEnvironmentInfo] = useState({ @@ -200,6 +202,12 @@ export function DesktopWorkbenchLayout({ return () => window.removeEventListener('popstate', handlePopState) }, []) + useEffect(() => { + if (settingsOpen && autoOpenAddCloudDeviceDialog) { + setAutoOpenAddCloudDeviceDialog(false) + } + }, [autoOpenAddCloudDeviceDialog, settingsOpen]) + useEffect(() => { const nextCompletedIds = new Set( messages @@ -300,7 +308,10 @@ export function DesktopWorkbenchLayout({ onGetProjectWorkspaceRoot={onGetProjectWorkspaceRoot} onListDeviceDirectories={onListDeviceDirectories} onCreateDeviceDirectory={onCreateDeviceDirectory} - onOpenSettings={() => { + onOpenSettings={options => { + setAutoOpenAddCloudDeviceDialog( + Boolean(options?.autoOpenAddCloudDeviceDialog), + ) setSettingsOpen(true) navigateTo('/settings') }} @@ -309,8 +320,10 @@ export function DesktopWorkbenchLayout({ )} {settingsOpen ? ( { setSettingsOpen(false) + setAutoOpenAddCloudDeviceDialog(false) navigateTo('/') }} onListArchivedTasks={onListArchivedTasks} @@ -361,6 +374,12 @@ export function DesktopWorkbenchLayout({ mode={projectWorkCreateMode ?? 'scratch'} devices={state.devices} onClose={() => setProjectWorkCreateMode(null)} + onOpenCloudDeviceSettings={() => { + setProjectWorkCreateMode(null) + setAutoOpenAddCloudDeviceDialog(true) + setSettingsOpen(true) + navigateTo('/settings') + }} onCreateProject={onCreateProject} onCreateGitWorkspaceProject={onCreateGitWorkspaceProject} preferredDeviceId={ diff --git a/wework/src/components/projects/ProjectCreateDialog.test.tsx b/wework/src/components/projects/ProjectCreateDialog.test.tsx index 4992db52bb..c4a072ef80 100644 --- a/wework/src/components/projects/ProjectCreateDialog.test.tsx +++ b/wework/src/components/projects/ProjectCreateDialog.test.tsx @@ -43,6 +43,34 @@ const branches: GitBranch[] = [ ] describe('ProjectCreateDialog', () => { + test('offers a settings link to create a cloud device when no project devices are available', async () => { + const onOpenCloudDeviceSettings = vi.fn() + + render( + , + ) + + expect(screen.getByText('暂无可用设备')).toBeInTheDocument() + const settingsLink = screen.getByTestId('open-cloud-device-settings-link') + + expect(settingsLink).toHaveAttribute('href', '/settings') + + await userEvent.click(settingsLink) + + expect(onOpenCloudDeviceSettings).toHaveBeenCalledTimes(1) + }) + test('hides OpenClaw devices from the project device selector', () => { render( { await waitFor(() => expect(pathInput).toHaveValue('/home/user')) await userEvent.clear(pathInput) - await userEvent.type(pathInput, '/home/user/re{Enter}') + await userEvent.type(pathInput, '/home/user/re') + await waitFor(() => expect(pathInput).toHaveValue('/home/user/re')) + fireEvent.keyDown(pathInput, { key: 'Enter' }) await waitFor(() => expect(pathInput).toHaveValue('/home/user/repo')) await waitFor(() => diff --git a/wework/src/components/projects/ProjectCreateDialog.tsx b/wework/src/components/projects/ProjectCreateDialog.tsx index fe5c3d0597..4f0d084a89 100644 --- a/wework/src/components/projects/ProjectCreateDialog.tsx +++ b/wework/src/components/projects/ProjectCreateDialog.tsx @@ -34,6 +34,7 @@ interface ProjectCreateDialogProps { ) => Promise preferredDeviceId?: string | null onSelectDevicePreference?: (deviceId: string) => void + onOpenCloudDeviceSettings?: () => void onGetDeviceHomeDirectory: (deviceId: string) => Promise onGetProjectWorkspaceRoot: (deviceId: string) => Promise onListDeviceDirectories: (deviceId: string, path: string) => Promise @@ -182,6 +183,7 @@ export function ProjectCreateDialog({ onCreateGitWorkspaceProject, preferredDeviceId, onSelectDevicePreference, + onOpenCloudDeviceSettings, onGetDeviceHomeDirectory, onGetProjectWorkspaceRoot, onListDeviceDirectories, @@ -201,6 +203,7 @@ export function ProjectCreateDialog({ onCreateGitWorkspaceProject={onCreateGitWorkspaceProject} preferredDeviceId={preferredDeviceId} onSelectDevicePreference={onSelectDevicePreference} + onOpenCloudDeviceSettings={onOpenCloudDeviceSettings} onGetDeviceHomeDirectory={onGetDeviceHomeDirectory} onGetProjectWorkspaceRoot={onGetProjectWorkspaceRoot} onListDeviceDirectories={onListDeviceDirectories} @@ -219,6 +222,7 @@ function ProjectCreateDialogContent({ onCreateGitWorkspaceProject, preferredDeviceId, onSelectDevicePreference, + onOpenCloudDeviceSettings, onGetDeviceHomeDirectory, onGetProjectWorkspaceRoot, onListDeviceDirectories, @@ -649,6 +653,23 @@ function ProjectCreateDialogContent({ ))} + {sortedDevices.length === 0 && onOpenCloudDeviceSettings && ( +

+ {t('workbench.project_no_available_devices_hint', '创建项目需要一台可用设备。')} + { + event.preventDefault() + onOpenCloudDeviceSettings() + }} + className="ml-1 font-medium text-[#14b8a6] underline underline-offset-2 hover:text-[#0f9f93]" + > + {t('workbench.project_create_cloud_device_connection', '创建云设备连接')} + +

+ )} + {mode === 'scratch' ? ( <>