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
4 changes: 4 additions & 0 deletions docs/en/wework/workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ The first main window starts with three default tabs: Task, Project spaces, and

When many tabs are open, the tab list scrolls horizontally while the **+** and the rightmost feedback button remain visible. A tab can also be moved to a separate window from its context menu. After the move succeeds, the source window removes the tab and the destination window contains only the moved tab and its state; it does not create the three default tabs again. If destination-window creation fails, the source tab remains unchanged.

## Move between project-space tasks and runtime tasks

When a runtime task is linked to a specific project-space task, select the task name in the runtime task's Environment information to open the matching project-space tab and task details. The task details' **Local execution** section lists the linked Wework runtime tasks; select a record to return to its runtime task tab. Both directions reuse existing tabs and preserve a restorable project-space or runtime-task route.

## Start a new task

The new-task page uses compact suggestion buttons to help choose a task direction. Selecting a direction reveals more specific prompts. Selecting a prompt writes it into the composer, where it can still be edited before sending.
Expand Down
4 changes: 4 additions & 0 deletions docs/zh/wework/workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ Wework 桌面版使用顶部标签页承载任务、项目空间、智能体和

标签页较多时,标签列表会水平滚动,顶部 **+** 和最右侧的反馈按钮始终保持可见。标签页也可以通过右键菜单移动到独立窗口。移动成功后,原窗口会移除该标签页;新窗口只包含被移动的标签页及其状态,不会再次创建三个默认标签页。创建新窗口失败时,原窗口中的标签页保持不变。

## 在项目空间和运行任务之间往返

如果运行任务已经关联到项目空间中的具体任务,可以在运行任务的 Environment 信息中点击任务名称,打开对应项目空间标签并定位到该任务详情。项目空间任务详情的“本地执行”区域会列出关联的 Wework 运行任务;点击其中一条记录可以返回对应的运行任务标签。两次跳转都会复用已有标签页,并保留项目空间或运行任务的可恢复路由。

## 开始新任务

新任务页使用紧凑的建议按钮帮助选择任务方向。点击一个方向后,可以继续选择更具体的提示;选中的提示会写入下方输入框,仍可在发送前编辑。
Expand Down
40 changes: 36 additions & 4 deletions wework/src/components/layout/DesktopWorkbenchLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
RuntimeIMNotificationSettingsResponse,
} from '@/types/api'
import { stripAppBasePath } from '@/config/runtime'
import { isSettingsRoute, navigateTo } from '@/lib/navigation'
import { buildRuntimeTaskRoute, isSettingsRoute, navigateTo } from '@/lib/navigation'
import { shouldUseNativeProjectDirectoryPicker } from '@/e2e/automation'
import { cn } from '@/lib/utils'
import { DesktopSidebar } from './DesktopSidebar'
Expand Down Expand Up @@ -53,10 +53,10 @@ function getPermanentWorktreeError(error: unknown, fallback: string) {
return fallback
}

function boardProjectIdFromRoute(contentRoute: string): string | null {
function boardRouteParam(contentRoute: string, name: string): string | null {
const searchIndex = contentRoute.indexOf('?')
if (searchIndex < 0) return null
return new URLSearchParams(contentRoute.slice(searchIndex + 1)).get('projectId')
return new URLSearchParams(contentRoute.slice(searchIndex + 1)).get(name)
}

interface DesktopWorkbenchLayoutProps {
Expand Down Expand Up @@ -180,6 +180,20 @@ export function DesktopWorkbenchLayout({ routeActive = true }: DesktopWorkbenchL
const [projectWorkEditProject, setProjectWorkEditProject] = useState<ProjectWithTasks | null>(
null
)
const openProjectSpaceRuntimeTask = useCallback(
async (address: RuntimeTaskAddress) => {
await onOpenRuntimeTask(address)
if (!workspaceTabs) return
const contentRoute = buildRuntimeTaskRoute(address)
const taskTab = workspaceTabs.tabs.find(tab => tab.kind === 'task')
if (taskTab) {
workspaceTabs.selectTab(taskTab.id, { contentRoute })
return
}
workspaceTabs.openTab('task', { contentRoute })
},
[onOpenRuntimeTask, workspaceTabs]
)
const [searchOpen, setSearchOpen] = useState(false)
const [imNotificationDialogMode, setImNotificationDialogMode] =
useState<ImNotificationDialogMode | null>(null)
Expand Down Expand Up @@ -688,11 +702,29 @@ export function DesktopWorkbenchLayout({ routeActive = true }: DesktopWorkbenchL
user={state.user}
localProjects={localTodoProjects}
services={services}
onOpenRuntimeTask={openProjectSpaceRuntimeTask}
activeProjectId={
workspaceTabs?.activeTab.kind === 'board'
? boardProjectIdFromRoute(workspaceTabs.activeTab.contentRoute)
? boardRouteParam(workspaceTabs.activeTab.contentRoute, 'projectId')
: undefined
}
focusedItemId={
workspaceTabs?.activeTab.kind === 'board'
? boardRouteParam(workspaceTabs.activeTab.contentRoute, 'itemId')
: undefined
}
onFocusedItemHandled={() => {
if (!workspaceTabs || workspaceTabs.activeTab.kind !== 'board') return
const projectId = boardRouteParam(
workspaceTabs.activeTab.contentRoute,
'projectId'
)
const params = new URLSearchParams()
if (projectId) params.set('projectId', projectId)
workspaceTabs.updateActiveTab({
contentRoute: `/todo${params.size ? `?${params.toString()}` : ''}`,
})
}}
onActiveProjectChange={project => {
if (!workspaceTabs || workspaceTabs.activeTab.kind !== 'board') return
if (!project) {
Expand Down
36 changes: 32 additions & 4 deletions wework/src/components/layout/DesktopWorkbenchMain.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ import { useWorkbenchProjectWorkControls } from './useWorkbenchProjectWorkContro
import { useRuntimeTaskContinueInIm } from './useRuntimeTaskContinueInIm'
import { requestOpenCloudDeviceSettings } from './workbenchShellEvents'
import { SubagentStatusIndicator } from './SubagentStatusIndicator'
import { useOptionalWorkspaceTabs } from '@/features/workspace-tabs/workspaceTabsContextValue'
import {
SupervisorSuggestionCards,
TaskSupervisorControl,
Expand Down Expand Up @@ -528,6 +529,7 @@ const DesktopWorkbenchPane = memo(function DesktopWorkbenchPane({
startNewChat,
} = useWorkbenchPaneContext()
const { services } = useWorkbench()
const workspaceTabs = useOptionalWorkspaceTabs()
const { t } = useTranslation('common')
const { t: tChat } = useTranslation('chat')
const currentRuntimeTask = pane.currentRuntimeTask
Expand Down Expand Up @@ -1005,6 +1007,30 @@ const DesktopWorkbenchPane = memo(function DesktopWorkbenchPane({
? deliveryItem
: null

const openBoundProjectSpaceTask = useCallback(() => {
if (!boundCloudProject || !boundCloudItem) return
const params = new URLSearchParams()
params.set('projectId', String(boundCloudProject.id))
params.set('itemId', boundCloudItem.id)
const contentRoute = `/todo?${params.toString()}`
const boardTab = workspaceTabs?.tabs.find(tab => tab.kind === 'board')
if (boardTab && workspaceTabs) {
workspaceTabs.selectTab(boardTab.id, {
title: boundCloudProject.name,
contentRoute,
})
return
}
if (workspaceTabs) {
workspaceTabs.openTab('board', {
title: boundCloudProject.name,
contentRoute,
})
return
}
navigateTo(contentRoute)
}, [boundCloudItem, boundCloudProject, workspaceTabs])

const finishLocalDelivery = useCallback(async () => {
if (!activeDeliveryItem) return
const items = await loadLocalWorkItems(state.user?.id)
Expand Down Expand Up @@ -2143,10 +2169,12 @@ const DesktopWorkbenchPane = memo(function DesktopWorkbenchPane({
}
onManageTodo={
experimentalFeaturesEnabled && currentRuntimeTask && services?.deliveryApi
? () => {
setDeliverAfterBinding(false)
setTodoBindingPickerOpen(true)
}
? boundCloudItem
? openBoundProjectSpaceTask
: () => {
setDeliverAfterBinding(false)
setTodoBindingPickerOpen(true)
}
: undefined
}
supervisor={supervisorFeatureAvailable ? supervisor : null}
Expand Down
39 changes: 39 additions & 0 deletions wework/src/features/todo/CloudTodoWorkspace.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,45 @@ describe('CloudTodoWorkspace', () => {
expect(screen.getByTestId('cloud-projects-home-manage')).toBeInTheDocument()
})

it('opens a routed project task in the board detail drawer', async () => {
const user = { id: 1, user_name: 'local', email: 'local@example.com' } as User
const workbenchServices = services()
const controlledProject = { ...project, id: String(project.id) }
const controlledItem = {
...item,
cloud_project_id: controlledProject.id,
}
vi.mocked(workbenchServices.deliveryApi!.listCloudProjects).mockResolvedValue({
items: [controlledProject],
})
vi.mocked(workbenchServices.deliveryApi!.listLoopItems).mockResolvedValue({
items: [controlledItem],
})
const onFocusedItemHandled = vi.fn()
const onOpenRuntimeTask = vi.fn()

render(
<CloudTodoWorkspace
user={user}
localProjects={[]}
services={workbenchServices}
activeProjectId={controlledProject.id}
focusedItemId={controlledItem.id}
onFocusedItemHandled={onFocusedItemHandled}
onOpenRuntimeTask={onOpenRuntimeTask}
/>
)

expect(await screen.findByTestId('cloud-todo-detail')).toBeInTheDocument()
expect(screen.getByTestId('cloud-todo-detail-title')).toHaveValue(controlledItem.title)
expect(onFocusedItemHandled).toHaveBeenCalledOnce()
await userEvent.click(await screen.findByTestId('cloud-todo-execution-1'))
expect(onOpenRuntimeTask).toHaveBeenCalledWith({
deviceId: 'local-device',
taskId: 'runtime-248868498',
})
})

it('renames and archives a project from the sidebar menu', async () => {
const workbenchServices = services()

Expand Down
34 changes: 33 additions & 1 deletion wework/src/features/todo/CloudTodoWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ import { useTranslation } from '@/hooks/useTranslation'
import { copyTextToClipboard } from '@/lib/clipboard'
import { cn } from '@/lib/utils'
import { AITableView } from '@/features/todo/AITableView'
import type { ProjectWithTasks, User as UserProfile } from '@/types/api'
import type { ProjectWithTasks, RuntimeTaskAddress, User as UserProfile } from '@/types/api'
import { CloudTodoModal as Modal } from './CloudTodoModal'
import { CloudMyWorkView } from './CloudMyWorkView'
import {
Expand Down Expand Up @@ -212,7 +212,10 @@ interface CloudTodoWorkspaceProps {
localProjects: ProjectWithTasks[]
services: WorkbenchServices
activeProjectId?: string | null
focusedItemId?: string | null
onFocusedItemHandled?: () => void
onActiveProjectChange?: (project: LocatedCloudProject | null) => void
onOpenRuntimeTask?: (address: RuntimeTaskAddress) => Promise<void> | void
}

const columnEmptyHints: Record<CloudLoopItem['status'], string> = {
Expand Down Expand Up @@ -661,7 +664,10 @@ export function CloudTodoWorkspace({
localProjects,
services,
activeProjectId,
focusedItemId,
onFocusedItemHandled,
onActiveProjectChange,
onOpenRuntimeTask,
}: CloudTodoWorkspaceProps) {
const { t } = useTranslation('common')
const projectSpaceApis = useMemo(() => {
Expand Down Expand Up @@ -726,6 +732,7 @@ export function CloudTodoWorkspace({
const [projectSearchFilters, setProjectSearchFilters] =
useState<TaskSearchFilters>(emptyTaskSearchFilters)
const locallyRequestedProjectIdRef = useRef<string | null | undefined>(undefined)
const focusedItemRequestRef = useRef<string | null>(null)
const projectHeaderRef = useRef<HTMLElement>(null)
const projectHeaderContentRef = useRef<HTMLDivElement>(null)
const projectHeaderTabsRef = useRef<HTMLElement>(null)
Expand Down Expand Up @@ -1306,6 +1313,30 @@ export function CloudTodoWorkspace({
window.clearInterval(interval)
}
}, [applyBoardItems, selectedProject, selectedProjectApi, selectedProjectId, services.aitableApi])
useEffect(() => {
if (!focusedItemId) {
focusedItemRequestRef.current = null
return
}
if (!selectedProjectId || itemsProjectId !== selectedProjectId) return
const requestKey = `${selectedProjectId}:${focusedItemId}`
if (focusedItemRequestRef.current === requestKey) return
const focusedItem = items.find(item => item.id === focusedItemId)
if (!focusedItem || focusedItem.can_view_detail === false) return
focusedItemRequestRef.current = requestKey
let active = true
queueMicrotask(() => {
if (!active) return
setRootView('projects')
setProjectView('board')
setBoardParentId(focusedItem.parent_id)
setSelectedItem(focusedItem)
onFocusedItemHandled?.()
})
return () => {
active = false
}
Comment on lines +1326 to +1338

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## Locate file"
fd -a 'CloudTodoWorkspace\.tsx$' . || true

echo "## File excerpt around referenced lines"
file="$(fd 'CloudTodoWorkspace\.tsx$' . | head -n 1)"
if [ -n "${file:-}" ]; then
  sed -n '1290,1365p' "$file" | cat -n
  echo "## Search focusedItemRequestRef usages"
  rg -n "focusedItemRequestRef|onFocusedItemHandled|queueMicrotask" "$file" || true
fi

echo "## Behavioral model for StrictMode-like setup-cleanup setup"
python3 - <<'PY'
# Standalone model of the reported Race in React Strict Mode:
# effect mounts => sets ref and active => cleanup runs => second mount runs.
request_key = "k"
focused_item_request_ref_current = None
active = None
opened = False
called_handler = False

def old_effect():
    global focused_item_request_ref_current, active, opened, called_handler
    focused_item_request_ref_current = request_key
    active = True
    def cleanup():
        global active
        active = False
    return cleanup

def strict_setup_cleanup_setup():
    cleanup1 = old_effect()
    # cleanup from first effect before second effect
    cleanup1()
    cleanup2 = old_effect()
    cleanup2()
    return focused_item_request_ref_current, active

ref_after, active_after = strict_setup_cleanup_setup()
print("old_effect_ref_after=", ref_after)
print("old_effect_active_after=", active_after)
print("old_effect_opens_task_after_2nd_cleanup=", not active_after)
print("old_effect_skips_onFocusedItemHandled_after_2nd_cleanup=", not active_after)

# Proposed model: ref set inside queued microtask only, after active test.
ref_after2 = None
active2 = True  # second effect's active local after cleanup of first effect?
def proposed_effect():
    global ref_after2, active2
    if not active2:
        return
    ref_after2 = request_key
    # microtask would run while active2 is true
    # then active2 would become false during cleanup before second effect, but ref isn't set
def proposed_cleanup():
    global active2
    active2 = False
cleanup_prop = proposed_effect()
proposed_cleanup()
proposed_effect()
print("proposed_effect_ref_after=", ref_after2)
PY

Repository: wecode-ai/Wegent

Length of output: 4401


🌐 Web query:

React Strict Mode double invokes effects setup cleanup before second setup documentation

💡 Result:

In React Strict Mode (development only), React intentionally performs an extra mount-unmount-remount cycle to verify that your components and effects are resilient and properly handle setup and cleanup [1][2][3]. The specific sequence of execution in Strict Mode for a component mount is: 1. Setup (First mount) [3] 2. Cleanup (First unmount) [3] 3. Setup (Second mount) [3] This behavior was introduced in React 18 to help developers identify bugs caused by missing cleanup functions or impure logic [2][4]. Because Effects are expected to be synchronized with the component's lifecycle, they should always have a cleanup function if they perform side effects (like subscriptions, network requests, or timers) [1][2]. When your component mounts in development, this sequence runs: - React calls your Effect's setup code [1]. - React immediately runs your Effect's cleanup function [1][3]. - React calls your Effect's setup code again [1]. This cycle ensures that if your code has a bug—such as a missing cleanup function that leaves an event listener or a WebSocket connection open—you will notice the issue immediately in development [1][2]. In production builds, Strict Mode checks are stripped out, and the component mounts exactly once [2][3]. If you are concerned about side effects occurring twice (e.g., duplicate API requests), the recommended approach is to implement proper cleanup (such as using an AbortController to cancel fetches or a boolean flag to ignore results) rather than disabling Strict Mode [4][5][3]. Top results: [1][2][3]

Citations:


Set focusedItemRequestRef after the active microtask guard.

React Strict Mode can run setup, cleanup, then setup again before the queued microtask. The first cleanup sets active to false, and the second setup exits at focusedItemRequestRef.current === requestKey, so the focused task does not open and onFocusedItemHandled is not called. Move the ref update into the microtask after the !active return.

Proposed fix
-    focusedItemRequestRef.current = requestKey
     let active = true
     queueMicrotask(() => {
       if (!active) return
+      focusedItemRequestRef.current = requestKey
       setRootView('projects')
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
focusedItemRequestRef.current = requestKey
let active = true
queueMicrotask(() => {
if (!active) return
setRootView('projects')
setProjectView('board')
setBoardParentId(focusedItem.parent_id)
setSelectedItem(focusedItem)
onFocusedItemHandled?.()
})
return () => {
active = false
}
let active = true
queueMicrotask(() => {
if (!active) return
focusedItemRequestRef.current = requestKey
setRootView('projects')
setProjectView('board')
setBoardParentId(focusedItem.parent_id)
setSelectedItem(focusedItem)
onFocusedItemHandled?.()
})
return () => {
active = false
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/features/todo/CloudTodoWorkspace.tsx` around lines 1326 - 1338,
Move the focusedItemRequestRef.current assignment in the focused-item effect so
it occurs inside the queued microtask, immediately after the active guard.
Preserve the existing requestKey deduplication and state updates, ensuring
Strict Mode’s second setup can process the request and invoke
onFocusedItemHandled.

}, [focusedItemId, items, itemsProjectId, onFocusedItemHandled, selectedProjectId])
useEffect(() => {
if (rootView !== 'my-work' && !(rootView === 'projects' && !selectedProjectId)) return
void Promise.all(availableProjectSpaceApis.map(({ api }) => api.listMyWork())).then(responses =>
Expand Down Expand Up @@ -2277,6 +2308,7 @@ export function CloudTodoWorkspace({
item={selectedItem}
project={projects.find(project => project.id === selectedItem.cloud_project_id)}
allItems={detailAllItems}
onOpenRuntimeTask={onOpenRuntimeTask}
onClose={() => setSelectedItem(null)}
onAddChild={() => openTodoCreation(selectedItem)}
onStartConversation={() => openTaskConversation(selectedItem)}
Expand Down
20 changes: 16 additions & 4 deletions wework/src/features/todo/TodoEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import type {
} from '@/api/deliveries'
import type { AITableApi } from '@/api/aitable'
import type { WorkbenchServices } from '@/features/workbench/workbenchServices'
import type { RuntimeTaskAddress } from '@/types/api'
import { cn } from '@/lib/utils'
import { TaskDescriptionEditor } from './TaskDescriptionEditor'
import { TagEditor } from './TagEditor'
Expand Down Expand Up @@ -235,14 +236,15 @@ export type TodoEditorProps = {
aitableApi?: AITableApi
allItems: CloudLoopItem[]
onClose: () => void
onOpenRuntimeTask?: (address: RuntimeTaskAddress) => Promise<void> | void
} & (TodoEditorCreateProps | TodoEditorEditProps)

// Single panel for creating, viewing, and editing a todo. Create mode keeps a
// local draft and stages attachments until the item exists; edit mode loads the
// sections that require an item id (children, collaborators, executions,
// deliveries) and saves through a versioned update.
export function TodoEditor(props: TodoEditorProps) {
const { api, allItems, onClose } = props
const { api, allItems, onClose, onOpenRuntimeTask } = props
const createProps = props.mode === 'create' ? props : null
const editProps = props.mode === 'edit' ? props : null
const isCreate = createProps !== null
Expand Down Expand Up @@ -1024,9 +1026,19 @@ export function TodoEditor(props: TodoEditorProps) {
) : (
<div className="mt-1">
{tasks.map(task => (
<div
<button
key={task.id}
className="flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-xs transition-colors hover:bg-muted/60"
type="button"
data-testid={`cloud-todo-execution-${task.id}`}
onClick={() =>
void onOpenRuntimeTask?.({
deviceId: task.device_id,
taskId: task.task_id,
})
}
disabled={!onOpenRuntimeTask}
className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-2 text-left text-xs transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-focus disabled:cursor-default"
aria-label={`打开本地执行 ${task.task_title || task.task_id}`}
Comment on lines +1029 to +1041

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Provide a 44px mobile touch target.

This new local-execution button is about 32px high from its text and py-2 padding. It has no mobile override. On screens at or below 767px, it does not meet the required 44px × 44px minimum control size.

Add a 44px minimum height for mobile. Preserve the compact height at desktop only if needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wework/src/features/todo/TodoEditor.tsx` around lines 1029 - 1041, Update the
button rendered in the cloud todo execution list, identified by the
`data-testid` value `cloud-todo-execution-${task.id}`, to use a minimum 44px
height on screens at or below 767px. Keep its current compact sizing for desktop
through an appropriate responsive class override, without changing its click
behavior or other styling.

Source: Coding guidelines

>
<Link2 className="h-4 w-4 shrink-0 text-text-muted" />
<span
Expand All @@ -1036,7 +1048,7 @@ export function TodoEditor(props: TodoEditorProps) {
{task.task_title || task.task_id}
</span>
<span className="shrink-0 text-text-muted">{task.device_id}</span>
</div>
</button>
))}
</div>
)}
Expand Down
30 changes: 29 additions & 1 deletion wework/src/features/workspace-tabs/WorkspaceTabsContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const labels = {
}

function TabsState() {
const { activeTab, openTab, tabs } = useWorkspaceTabs()
const { activeTab, openTab, selectTab, tabs } = useWorkspaceTabs()
const boardTab = tabs.find(tab => tab.kind === 'board')

return (
<>
Expand All @@ -32,6 +33,18 @@ function TabsState() {
<button type="button" onClick={() => openTab('board')}>
新建项目空间标签
</button>
<button
type="button"
onClick={() =>
boardTab &&
selectTab(boardTab.id, {
title: 'Wegent V4',
contentRoute: '/todo?projectId=project-1&itemId=WEG-1',
})
}
>
打开项目任务
</button>
</>
)
}
Expand Down Expand Up @@ -92,4 +105,19 @@ describe('WorkspaceTabsProvider routing', () => {
expect(screen.getByTestId('active-tab-kind')).toHaveTextContent('board')
expect(window.location.search).toContain('workspaceTab=board-')
})

test('selects and updates an existing board tab for a concrete project task', () => {
render(<RoutingHarness />)

act(() => screen.getByRole('button', { name: '打开项目任务' }).click())

expect(screen.getByTestId('tab-count')).toHaveTextContent('3')
expect(screen.getByTestId('active-tab-kind')).toHaveTextContent('board')
expect(screen.getByTestId('active-tab-title')).toHaveTextContent('Wegent V4')
expect(screen.getByTestId('active-tab-route')).toHaveTextContent(
'/todo?projectId=project-1&itemId=WEG-1'
)
expect(window.location.search).toContain('projectId=project-1')
expect(window.location.search).toContain('itemId=WEG-1')
})
})
Loading
Loading