From ee33c8da4234a4ba43daee4499175745f3cd67df Mon Sep 17 00:00:00 2001 From: hongyu9 Date: Mon, 8 Jun 2026 19:21:54 +0800 Subject: [PATCH] feat(wework): improve mobile project controls --- wework/src/components/chat/ChatInput.test.tsx | 107 ++++++ wework/src/components/chat/ChatInput.tsx | 6 + .../chat/composer/ProjectChatComposer.tsx | 6 + .../chat/composer/ProjectWorkBar.tsx | 116 +++++- .../src/components/common/BranchSelector.tsx | 339 ++++++++++++++++++ .../components/common/SearchableSelect.tsx | 159 ++++++++ .../layout/DesktopWorkbenchLayout.test.tsx | 17 +- .../layout/DesktopWorkbenchLayout.tsx | 23 +- .../layout/EnvironmentInfoPopover.tsx | 249 +------------ .../layout/MobileWorkbenchLayout.test.tsx | 104 +++++- .../layout/MobileWorkbenchLayout.tsx | 72 +++- .../projects/ProjectCreateDialog.test.tsx | 46 ++- .../projects/ProjectCreateDialog.tsx | 121 ++++--- wework/src/i18n/locales/en/common.json | 5 + wework/src/i18n/locales/zh-CN/common.json | 5 + 15 files changed, 1056 insertions(+), 319 deletions(-) create mode 100644 wework/src/components/common/BranchSelector.tsx create mode 100644 wework/src/components/common/SearchableSelect.tsx diff --git a/wework/src/components/chat/ChatInput.test.tsx b/wework/src/components/chat/ChatInput.test.tsx index 03f33847bf7..6a261291a63 100644 --- a/wework/src/components/chat/ChatInput.test.tsx +++ b/wework/src/components/chat/ChatInput.test.tsx @@ -1633,6 +1633,113 @@ describe('ChatInput', () => { expect(screen.queryByTestId('project-execution-mode-menu')).not.toBeInTheDocument() }) + test('switches branches immediately from the new project chat work bar', async () => { + const onListBranches = vi.fn().mockResolvedValue(['feature/chat', 'main']) + const onCheckoutBranch = vi.fn().mockResolvedValue(undefined) + const onRefreshBranch = vi.fn().mockResolvedValue(undefined) + const projects: ProjectWithTasks[] = [ + { + id: 7, + name: 'Wegent', + config: { + mode: 'workspace', + device_id: 'device-1', + workspace: { + source: 'local_path', + localPath: '/workspace/projects/Wegent', + }, + }, + tasks: [], + }, + ] + + render( + , + ) + + expect(screen.getByTestId('project-branch-button')).toHaveTextContent('main') + await userEvent.click(screen.getByTestId('project-branch-button')) + + await waitFor(() => { + expect(onRefreshBranch).toHaveBeenCalledTimes(1) + expect(onListBranches).toHaveBeenCalledTimes(1) + }) + + const options = await screen.findAllByTestId('project-branch-option') + await userEvent.click(options[0]) + + expect(onCheckoutBranch).toHaveBeenCalledWith('feature/chat') + await waitFor(() => { + expect(screen.queryByTestId('project-branch-menu')).not.toBeInTheDocument() + }) + }) + + test('creates and checks out a branch from the new project chat work bar', async () => { + const onCreateBranch = vi.fn().mockResolvedValue(undefined) + + render( + , + ) + + await userEvent.click(screen.getByTestId('project-branch-button')) + await screen.findAllByTestId('project-branch-option') + await userEvent.click(screen.getByTestId('project-open-new-branch-button')) + await userEvent.type(screen.getByTestId('project-new-branch-input'), 'feature/new-chat') + await userEvent.click(screen.getByTestId('project-confirm-new-branch-button')) + + expect(onCreateBranch).toHaveBeenCalledWith('feature/new-chat') + }) + + test('hides branch switching when the project conversation is locked', () => { + render( + , + ) + + expect(screen.queryByTestId('project-branch-button')).not.toBeInTheDocument() + }) + test('opens project work menu upward when below cannot fit the four-row menu', async () => { vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function () { if ((this as HTMLElement).dataset.testid === 'project-work-button') { diff --git a/wework/src/components/chat/ChatInput.tsx b/wework/src/components/chat/ChatInput.tsx index 84f7f157d10..293dc22c19e 100644 --- a/wework/src/components/chat/ChatInput.tsx +++ b/wework/src/components/chat/ChatInput.tsx @@ -47,6 +47,12 @@ export interface ProjectWorkControls { onSelectStandaloneDevice: (deviceId: string | null) => void onExecutionModeChange: (mode: ProjectExecutionMode) => void onCreateProjectMode?: (mode: ProjectCreateMode) => void + branchName?: string + branchLoading?: boolean + onRefreshBranch?: () => Promise + onListBranches?: () => Promise + onCheckoutBranch?: (branchName: string) => Promise + onCreateBranch?: (branchName: string) => Promise } interface ChatInputProps { diff --git a/wework/src/components/chat/composer/ProjectChatComposer.tsx b/wework/src/components/chat/composer/ProjectChatComposer.tsx index 877a8d63480..c6cc7421242 100644 --- a/wework/src/components/chat/composer/ProjectChatComposer.tsx +++ b/wework/src/components/chat/composer/ProjectChatComposer.tsx @@ -114,6 +114,12 @@ export function ProjectChatComposer({ onSelectStandaloneDevice={projectWork.onSelectStandaloneDevice} onExecutionModeChange={projectWork.onExecutionModeChange} onCreateProjectMode={projectWork.onCreateProjectMode} + branchName={projectWork.branchName} + branchLoading={projectWork.branchLoading} + onRefreshBranch={projectWork.onRefreshBranch} + onListBranches={projectWork.onListBranches} + onCheckoutBranch={projectWork.onCheckoutBranch} + onCreateBranch={projectWork.onCreateBranch} className="min-h-11 px-4" /> )} diff --git a/wework/src/components/chat/composer/ProjectWorkBar.tsx b/wework/src/components/chat/composer/ProjectWorkBar.tsx index 43f88bee643..166d5763e2a 100644 --- a/wework/src/components/chat/composer/ProjectWorkBar.tsx +++ b/wework/src/components/chat/composer/ProjectWorkBar.tsx @@ -12,6 +12,7 @@ import { Laptop, Plus, Search, + X, } from 'lucide-react' import { useCallback, @@ -22,6 +23,8 @@ import { useState, } from 'react' import { ProjectFolderIcon } from '@/components/projects/ProjectFolderIcon' +import { BranchSelector } from '@/components/common/BranchSelector' +import { useIsMobile } from '@/hooks/useIsMobile' import { useTranslation } from '@/hooks/useTranslation' import { getPreferredStandaloneDeviceId, @@ -120,6 +123,12 @@ interface ProjectWorkBarProps { onSelectStandaloneDevice: (deviceId: string | null) => void onExecutionModeChange: (mode: ProjectExecutionMode) => void onCreateProjectMode?: (mode: ProjectCreateMode) => void + branchName?: string + branchLoading?: boolean + onRefreshBranch?: () => Promise + onListBranches?: () => Promise + onCheckoutBranch?: (branchName: string) => Promise + onCreateBranch?: (branchName: string) => Promise className?: string buttonClassName?: string menuClassName?: string @@ -137,12 +146,19 @@ export function ProjectWorkBar({ onSelectStandaloneDevice, onExecutionModeChange, onCreateProjectMode, + branchName, + branchLoading, + onRefreshBranch, + onListBranches, + onCheckoutBranch, + onCreateBranch, className, buttonClassName, menuClassName, emptyLabel, }: ProjectWorkBarProps) { const { t } = useTranslation('common') + const isMobile = useIsMobile() const containerRef = useRef(null) const executionModeContainerRef = useRef(null) const triggerButtonRef = useRef(null) @@ -306,9 +322,10 @@ export function ProjectWorkBar({ useEffect(() => { if (!open) return + if (isMobile) return searchInputRef.current?.focus() - }, [open]) + }, [isMobile, open]) const getDeviceForProject = useCallback((project: ProjectWithTasks): DeviceInfo | undefined => { const deviceId = project.config?.execution?.deviceId ?? project.config?.device_id @@ -404,6 +421,30 @@ export function ProjectWorkBar({ const ExecutionModeTriggerIcon = executionMode === 'git_worktree' ? GitBranch : Laptop + const renderMobileSheetHeader = (title: string, subtitle: string, onClose: () => void) => ( + <> +
+
+
+

{title}

+

{subtitle}

+
+ +
+ + ) + + const renderMobileBackdrop = (onClose: () => void) => +
+ const getCompactDeviceStatusLabel = (device: DeviceInfo) => { if (device.status === 'online') { return t('workbench.project_device_status_online', '在线') @@ -423,17 +464,31 @@ export function ProjectWorkBar({ return (
+ {open && isMobile && renderMobileBackdrop(closeMenu)} {open && (
-