Skip to content
Merged
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
50 changes: 50 additions & 0 deletions wework/src/components/chat/ChatInput.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ChatInput
value=""
onChange={vi.fn()}
onSubmit={vi.fn()}
disabled={false}
variant="desktop"
projectChat={projectChatControls({
models: [gptModel, geminiModel],
selectedModel: gptModel,
selectedModelOptions: {},
})}
/>,
)

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', {
Expand Down
6 changes: 5 additions & 1 deletion wework/src/components/layout/DesktopSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ interface DesktopSidebarProps {
onGetProjectWorkspaceRoot: (deviceId: string) => Promise<string>
onListDeviceDirectories: (deviceId: string, path: string) => Promise<string[]>
onCreateDeviceDirectory: (deviceId: string, path: string) => Promise<void>
onOpenSettings: () => void
onOpenSettings: (options?: { autoOpenAddCloudDeviceDialog?: boolean }) => void
onLogout: () => void
}

Expand Down Expand Up @@ -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}
Expand Down
119 changes: 89 additions & 30 deletions wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<DesktopWorkbenchLayout
{...baseProps}
state={{
...baseProps.state,
projects: [workspacePanelProject],
devices: workspacePanelDevices,
currentProject: workspacePanelProject,
...workspacePanelState,
}}
projectWork={{
...baseProps.projectWork,
projects: [workspacePanelProject],
devices: workspacePanelDevices,
currentProjectId: workspacePanelProject.id,
projects: workspacePanelState.projects,
devices: workspacePanelState.devices,
currentProjectId: workspacePanelState.currentProject.id,
}}
/>,
)
Expand Down Expand Up @@ -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(<DesktopWorkbenchLayout {...baseProps} onRefreshDevices={onRefreshDevices} />)

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(<DesktopWorkbenchLayout {...baseProps} onRefreshDevices={onRefreshDevices} />)

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')
Expand Down
21 changes: 20 additions & 1 deletion wework/src/components/layout/DesktopWorkbenchLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProjectCreateMode | null>(null)
const [environmentInfo, setEnvironmentInfo] = useState<EnvironmentInfo>({
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -300,7 +308,10 @@ export function DesktopWorkbenchLayout({
onGetProjectWorkspaceRoot={onGetProjectWorkspaceRoot}
onListDeviceDirectories={onListDeviceDirectories}
onCreateDeviceDirectory={onCreateDeviceDirectory}
onOpenSettings={() => {
onOpenSettings={options => {
setAutoOpenAddCloudDeviceDialog(
Boolean(options?.autoOpenAddCloudDeviceDialog),
)
setSettingsOpen(true)
navigateTo('/settings')
}}
Expand All @@ -309,8 +320,10 @@ export function DesktopWorkbenchLayout({
)}
{settingsOpen ? (
<ConnectionsSettingsPage
autoOpenAddCloudDeviceDialog={autoOpenAddCloudDeviceDialog}
onBack={() => {
setSettingsOpen(false)
setAutoOpenAddCloudDeviceDialog(false)
navigateTo('/')
}}
onListArchivedTasks={onListArchivedTasks}
Expand Down Expand Up @@ -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={
Expand Down
32 changes: 31 additions & 1 deletion wework/src/components/projects/ProjectCreateDialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<ProjectCreateDialog
open
mode="scratch"
devices={[]}
onClose={vi.fn()}
onCreateProject={vi.fn()}
onOpenCloudDeviceSettings={onOpenCloudDeviceSettings}
onGetDeviceHomeDirectory={vi.fn().mockResolvedValue('/home/user')}
onGetProjectWorkspaceRoot={vi.fn().mockResolvedValue('/workspace/projects')}
onListDeviceDirectories={vi.fn().mockResolvedValue([])}
onCreateDeviceDirectory={vi.fn()}
/>,
)

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(
<ProjectCreateDialog
Expand Down Expand Up @@ -218,7 +246,9 @@ describe('ProjectCreateDialog', () => {
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(() =>
Expand Down
21 changes: 21 additions & 0 deletions wework/src/components/projects/ProjectCreateDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ interface ProjectCreateDialogProps {
) => Promise<ProjectWithTasks>
preferredDeviceId?: string | null
onSelectDevicePreference?: (deviceId: string) => void
onOpenCloudDeviceSettings?: () => void
onGetDeviceHomeDirectory: (deviceId: string) => Promise<string>
onGetProjectWorkspaceRoot: (deviceId: string) => Promise<string>
onListDeviceDirectories: (deviceId: string, path: string) => Promise<string[]>
Expand Down Expand Up @@ -182,6 +183,7 @@ export function ProjectCreateDialog({
onCreateGitWorkspaceProject,
preferredDeviceId,
onSelectDevicePreference,
onOpenCloudDeviceSettings,
onGetDeviceHomeDirectory,
onGetProjectWorkspaceRoot,
onListDeviceDirectories,
Expand All @@ -201,6 +203,7 @@ export function ProjectCreateDialog({
onCreateGitWorkspaceProject={onCreateGitWorkspaceProject}
preferredDeviceId={preferredDeviceId}
onSelectDevicePreference={onSelectDevicePreference}
onOpenCloudDeviceSettings={onOpenCloudDeviceSettings}
onGetDeviceHomeDirectory={onGetDeviceHomeDirectory}
onGetProjectWorkspaceRoot={onGetProjectWorkspaceRoot}
onListDeviceDirectories={onListDeviceDirectories}
Expand All @@ -219,6 +222,7 @@ function ProjectCreateDialogContent({
onCreateGitWorkspaceProject,
preferredDeviceId,
onSelectDevicePreference,
onOpenCloudDeviceSettings,
onGetDeviceHomeDirectory,
onGetProjectWorkspaceRoot,
onListDeviceDirectories,
Expand Down Expand Up @@ -649,6 +653,23 @@ function ProjectCreateDialogContent({
))}
</select>

{sortedDevices.length === 0 && onOpenCloudDeviceSettings && (
<p className="mt-2 text-xs leading-5 text-[#606368]">
{t('workbench.project_no_available_devices_hint', '创建项目需要一台可用设备。')}
<a
href="/settings"
data-testid="open-cloud-device-settings-link"
onClick={event => {
event.preventDefault()
onOpenCloudDeviceSettings()
}}
className="ml-1 font-medium text-[#14b8a6] underline underline-offset-2 hover:text-[#0f9f93]"
>
{t('workbench.project_create_cloud_device_connection', '创建云设备连接')}
</a>
</p>
)}

{mode === 'scratch' ? (
<>
<label className="mt-5 block text-[13px] font-semibold text-[#202124]">
Expand Down
Loading
Loading