Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion docs/en/wework/workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ When a conversation is taller than the current viewport, turn markers appear alo

## Switch conversations and restore position

When switching conversations, the desktop workbench saves runtime state, recent messages, right-workspace tabs, and panel state, so returning restores the workspace as it was left. Ordinary conversations do not retain a hidden full-page DOM, which bounds WebView memory growth from long conversations.
When switching conversations, the desktop workbench saves runtime state, recent messages, right-workspace tabs, and panel state, so returning restores the workspace as it was left. The Files tab restores the selected file and its actual directory, including absolute paths opened from assistant messages outside the workspace root. The Review tab restores the selected review scope and loaded diff. Ordinary conversations do not retain a hidden full-page DOM, which bounds WebView memory growth from long conversations.

Conversation panes with a running Terminal or Wework built-in browser remain mounted. Terminal processes, tabs, and output buffers stay live, while built-in browser pages, addresses, and tab state are preserved. After those resources close, the pane can be released while its restorable panel state remains. Hidden conversations do not handle shortcuts or browser-open events intended for the active conversation.

Expand Down
2 changes: 1 addition & 1 deletion docs/zh/wework/workbench.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Wework 桌面版使用顶部标签页承载任务、项目空间、智能体和

## 切换对话与恢复位置

切换对话时,桌面工作台会保存运行状态、最近消息、右侧工作区标签和面板状态;切回后会立即恢复离开时的工作区。普通对话不会保留隐藏的完整页面 DOM,以限制长对话带来的 WebView 内存增长。
切换对话时,桌面工作台会保存运行状态、最近消息、右侧工作区标签和面板状态;切回后会立即恢复离开时的工作区。文件标签会恢复离开前选中的文件及其实际目录,包括从助手消息打开的工作区外绝对路径;审核标签会恢复所选审核范围和已经加载的 Diff。普通对话不会保留隐藏的完整页面 DOM,以限制长对话带来的 WebView 内存增长。

包含运行中 Terminal 或 Wework 内置浏览器的对话页面会保持挂载。Terminal 进程、标签和输出缓冲会继续保留,内置浏览器的页面、地址和标签状态也会保持;关闭对应资源后,页面即可释放,仅保留可恢复的面板状态。隐藏对话不会响应当前对话的快捷键或浏览器打开事件。

Expand Down
177 changes: 170 additions & 7 deletions wework/e2e/desktop/task-flow.e2e.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ const CHECKPOINT_TASK_COMPLETION_TEXT = 'WEWORK_DESKTOP_E2E_CHECKPOINT_TASK_COMP
const FILE_PANEL_ANCHOR_PROMPT =
'WEWORK_DESKTOP_E2E_FILE_PANEL_ANCHOR: create a long response with a file link in the middle.'
const FILE_PANEL_ANCHOR_MARKER = 'WEWORK_DESKTOP_E2E_FILE_PANEL_ANCHOR_MARKER'
const FILE_PREVIEW_RESTORE_MARKER = 'WEWORK_DESKTOP_E2E_FILE_PREVIEW_RESTORED'
const REVIEW_RESTORE_MARKER = 'WEWORK_DESKTOP_E2E_REVIEW_RESTORED'
const FILE_PANEL_ANCHOR_RESPONSE = [
'WEWORK_DESKTOP_E2E_FILE_PANEL_ANCHOR_RESPONSE',
...Array.from({ length: 30 }, (_, index) =>
Expand Down Expand Up @@ -13803,8 +13805,44 @@ last_updated = "2026-07-30T00:00:00Z"`
)

phase = 'workspace-resources-across-conversation-switch'
const activeBrowserInputSelector = `${ACTIVE_WORKBENCH_SELECTOR} [data-testid="workspace-browser-url-input"]`
const activeTerminalSelector = `${ACTIVE_WORKBENCH_SELECTOR} [data-testid="workspace-terminal-window"]`
await writeFile(
join(workspacePath, GIT_SEED_NAME),
`${GIT_SEED_CONTENT}${FILE_PREVIEW_RESTORE_MARKER}\n`
)
const firstTaskDebugSnapshot = JSON.parse(
await control.command('getWorkbenchDebugSnapshot', 'body')
)
const firstTaskWorkspacePath =
firstTaskDebugSnapshot.workbench?.currentRuntimeTask?.workspacePath
assert.ok(
firstTaskWorkspacePath,
'The first task did not expose a workspace path for review restoration'
)
const activeWorkspaceTabSelector =
'[data-testid^="workspace-tab-select-task-"][aria-selected="true"]'
await control.command('waitFor', activeWorkspaceTabSelector, {
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
})
const activeWorkspaceTabTestId = await control.command(
'getAttribute',
activeWorkspaceTabSelector,
{ value: 'data-testid' }
)
const activeWorkspaceTabId = activeWorkspaceTabTestId.replace('workspace-tab-select-', '')
assert.ok(
activeWorkspaceTabId.startsWith('task-'),
`Expected an active task workspace tab, received ${activeWorkspaceTabTestId}`
)
const activeTaskWorkbenchSelector =
`[data-testid="workspace-tab-content-${activeWorkspaceTabId}"] ` +
'[data-testid="desktop-workbench-main"]'
const firstTaskReadme = join(firstTaskWorkspacePath, GIT_SEED_NAME)
await writeFile(
firstTaskReadme,
`${await readFile(firstTaskReadme, 'utf8')}${REVIEW_RESTORE_MARKER}\n`
)
const activeBrowserInputSelector = `${activeTaskWorkbenchSelector} [data-testid="workspace-browser-url-input"]`
const activeTerminalSelector = `${activeTaskWorkbenchSelector} [data-testid="workspace-terminal-window"]`
const rightPanelToggleSelector = '[data-testid="toggle-right-workspace-panel-button"]'
const bottomPanelToggleSelector = '[data-testid="toggle-bottom-workspace-panel-button"]'
const bottomWorkspaceTabCloseSelector = '[data-testid="close-bottom-workspace-tab-button"]'
Expand All @@ -13817,7 +13855,7 @@ last_updated = "2026-07-30T00:00:00Z"`
await control.command('click', rightPanelToggleSelector)
await control.command(
'click',
`${ACTIVE_WORKBENCH_SELECTOR} [data-testid="right-workspace-browser-option"]`
`${activeTaskWorkbenchSelector} [data-testid="right-workspace-browser-option"]`
)
await control.command('waitFor', activeBrowserInputSelector, {
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
Expand All @@ -13826,7 +13864,7 @@ last_updated = "2026-07-30T00:00:00Z"`
await control.command('submit', activeBrowserInputSelector)
await control.command(
'waitFor',
`${ACTIVE_WORKBENCH_SELECTOR} [data-testid="workspace-browser-native-view"]`,
`${activeTaskWorkbenchSelector} [data-testid="workspace-browser-native-view"]`,
{ timeoutMs: DEFAULT_STEP_TIMEOUT_MS }
)
await control.command('click', bottomPanelToggleSelector)
Expand All @@ -13843,14 +13881,71 @@ last_updated = "2026-07-30T00:00:00Z"`
},
'The first task bottom workspace panel did not open a terminal or limited-tools launcher',
DEFAULT_STEP_TIMEOUT_MS,
ACTIVE_WORKBENCH_SELECTOR
activeTaskWorkbenchSelector
)
const firstTaskOpenedTerminal = firstTaskBottomWorkspaceSnapshot.testIds.includes(
'workspace-terminal-window'
)
await control.command(
'click',
`${filePanelAnchorSelector} [data-testid="assistant-markdown-link"]`
)
await control.command(
'waitFor',
`${activeTaskWorkbenchSelector} [data-testid="workspace-markdown-preview"]`,
{
text: FILE_PREVIEW_RESTORE_MARKER,
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
}
)
assert.equal(
await control.command(
'getText',
`${activeTaskWorkbenchSelector} [data-testid="workspace-file-path"]`
),
join(workspacePath, GIT_SEED_NAME),
'The linked absolute file opened from the wrong workspace target'
)
await control.command('click', '[data-testid="right-workspace-new-tab-button"]')
await control.command('click', '[data-testid="right-workspace-review-option"]')
await control.command(
'waitFor',
`${activeTaskWorkbenchSelector} [data-testid="review-view-switcher-button"]`,
{
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
}
)
await control.command(
'click',
`${activeTaskWorkbenchSelector} [data-testid="review-view-switcher-button"]`
)
await control.command('click', '[data-testid="review-view-switcher-option"]:first-child')
await control.command(
'waitFor',
`${activeTaskWorkbenchSelector} [data-testid="file-changes-review-file-tree"]`,
{
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
}
)
await control.command(
'waitFor',
`${activeTaskWorkbenchSelector} [data-testid="file-changes-review-file-diff-toggle"]`,
{
text: GIT_SEED_NAME,
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
}
)
assert.match(
await control.command(
'getText',
`${activeTaskWorkbenchSelector} [data-testid="file-changes-review-toolbar"]`
),
/\+2\s*-0/,
'The review fixture did not expose both README additions before switching tasks'
)
await control.command('click', `[data-testid="${secondTaskRowTestId}"]`)
const secondTaskWorkspaceSnapshot = JSON.parse(
await control.command('snapshot', ACTIVE_WORKBENCH_SELECTOR)
await control.command('snapshot', activeTaskWorkbenchSelector)
Comment on lines 13948 to +13956

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 | 🟠 Major | ⚡ Quick win

Snapshot the second task workbench after the task switch.

activeTaskWorkbenchSelector still identifies the first task tab. Line 13948 therefore snapshots the first task after Line 13946 selects the second task. The following assertions cannot detect resources that leak into the second task.

Resolve the selected workspace tab again after the switch. Build a second-task selector from that tab before taking the snapshot.

Proposed fix
 await control.command('click', `[data-testid="${secondTaskRowTestId}"]`)
+await control.command('waitFor', activeWorkspaceTabSelector, {
+  timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
+})
+const secondTaskWorkspaceTabTestId = await control.command(
+  'getAttribute',
+  activeWorkspaceTabSelector,
+  { value: 'data-testid' }
+)
+const secondTaskWorkspaceTabId = secondTaskWorkspaceTabTestId.replace(
+  'workspace-tab-select-',
+  ''
+)
+const secondTaskWorkbenchSelector =
+  `[data-testid="workspace-tab-content-${secondTaskWorkspaceTabId}"] ` +
+  '[data-testid="desktop-workbench-main"]'
 const secondTaskWorkspaceSnapshot = JSON.parse(
-  await control.command('snapshot', activeTaskWorkbenchSelector)
+  await control.command('snapshot', secondTaskWorkbenchSelector)
 )
📝 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
await control.command('click', `[data-testid="${secondTaskRowTestId}"]`)
const secondTaskWorkspaceSnapshot = JSON.parse(
await control.command('snapshot', ACTIVE_WORKBENCH_SELECTOR)
await control.command('snapshot', activeTaskWorkbenchSelector)
await control.command('click', `[data-testid="${secondTaskRowTestId}"]`)
await control.command('waitFor', activeWorkspaceTabSelector, {
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
})
const secondTaskWorkspaceTabTestId = await control.command(
'getAttribute',
activeWorkspaceTabSelector,
{ value: 'data-testid' }
)
const secondTaskWorkspaceTabId = secondTaskWorkspaceTabTestId.replace(
'workspace-tab-select-',
''
)
const secondTaskWorkbenchSelector =
`[data-testid="workspace-tab-content-${secondTaskWorkspaceTabId}"] ` +
'[data-testid="desktop-workbench-main"]'
const secondTaskWorkspaceSnapshot = JSON.parse(
await control.command('snapshot', secondTaskWorkbenchSelector)
🤖 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/e2e/desktop/task-flow.e2e.mjs` around lines 13946 - 13948, After the
task switch performed by control.command('click',
`[data-testid="${secondTaskRowTestId}"]`), resolve the currently selected
workspace tab again instead of reusing activeTaskWorkbenchSelector. Build a
selector for the second task workbench from that newly selected tab, then pass
it to the snapshot command used to create secondTaskWorkspaceSnapshot.

)
assert.equal(
secondTaskWorkspaceSnapshot.testIds.includes('workspace-terminal-window'),
Expand All @@ -13862,6 +13957,16 @@ last_updated = "2026-07-30T00:00:00Z"`
false,
'The first task browser leaked into the second task'
)
assert.equal(
secondTaskWorkspaceSnapshot.testIds.includes('workspace-markdown-preview'),
false,
'The first task file preview leaked into the second task'
)
assert.equal(
secondTaskWorkspaceSnapshot.testIds.includes('file-changes-review-panel'),
false,
'The first task review leaked into the second task'
)
assert.equal(
secondTaskWorkspaceSnapshot.testIds.includes('workspace-tool-launcher'),
false,
Expand All @@ -13882,9 +13987,57 @@ last_updated = "2026-07-30T00:00:00Z"`
value.testIds.includes('workspace-local-device-limited-tools'),
'The first task bottom workspace limited-tools state was not restored',
DEFAULT_STEP_TIMEOUT_MS,
ACTIVE_WORKBENCH_SELECTOR
activeTaskWorkbenchSelector
)
}
await control.command(
'waitFor',
`${activeTaskWorkbenchSelector} [data-testid="file-changes-review-file-tree"]`,
{
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
}
)
await control.command(
'waitFor',
`${activeTaskWorkbenchSelector} [data-testid="file-changes-review-file-diff-toggle"]`,
{
text: GIT_SEED_NAME,
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
}
)
assert.match(
await control.command(
'getText',
`${activeTaskWorkbenchSelector} [data-testid="file-changes-review-toolbar"]`
),
/\+2\s*-0/,
'The restored review lost the README diff statistics'
)
assert.equal(
await control.command('getAttribute', '[data-testid="right-workspace-review-tab"]', {
value: 'aria-selected',
}),
'true',
'The review tab was not active after switching back to the first task'
)
await control.command('click', '[data-testid="right-workspace-file-tab"]')
await control.command(
'waitFor',
`${activeTaskWorkbenchSelector} [data-testid="workspace-markdown-preview"]`,
{
text: FILE_PREVIEW_RESTORE_MARKER,
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
}
)
assert.equal(
await control.command(
'getText',
`${activeTaskWorkbenchSelector} [data-testid="workspace-file-path"]`
),
join(workspacePath, GIT_SEED_NAME),
'The linked absolute file path was lost after switching conversations'
)
await control.command('click', '[data-testid="right-workspace-browser-tab"]')
await control.command('waitFor', activeBrowserInputSelector, {
timeoutMs: DEFAULT_STEP_TIMEOUT_MS,
})
Expand All @@ -13898,6 +14051,14 @@ last_updated = "2026-07-30T00:00:00Z"`
restoredWorkspaceSnapshot.testIds.includes('right-workspace-browser-tab'),
'The browser tab was not restored after switching conversations'
)
assert.ok(
restoredWorkspaceSnapshot.testIds.includes('right-workspace-file-tab'),
'The file tab was not restored after switching conversations'
)
assert.ok(
restoredWorkspaceSnapshot.testIds.includes('right-workspace-review-tab'),
'The review tab was not restored after switching conversations'
)
await control.command('finishAnimations', 'body')
await captureVerificationScreenshot(control, 'workspace-panel-01-default-split.png')

Expand Down Expand Up @@ -13998,6 +14159,8 @@ last_updated = "2026-07-30T00:00:00Z"`
await captureVerificationScreenshot(control, 'workspace-panel-04-restored-split.png')
await control.command('click', bottomWorkspaceTabCloseSelector)
await control.command('click', rightBrowserTabCloseSelector)
await control.command('click', '[data-testid="right-workspace-file-tab-close-button"]')
await control.command('click', '[data-testid="right-workspace-review-tab-close-button"]')

await control.command('fill', composerSelector, { value: '' })
await control.command('click', `[data-testid="${secondTaskRowTestId}"]`)
Expand Down
112 changes: 112 additions & 0 deletions wework/src/components/layout/DesktopWorkbenchLayout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7995,6 +7995,118 @@ describe('DesktopWorkbenchLayout', () => {
expect(workspaceFileApi.readWorkspaceTextFile).toHaveBeenCalledTimes(4)
})

test('preserves an absolute file opened from a message when switching runtime tasks', async () => {
const { propsForTask, taskA, taskB } = createLocalRuntimeTaskPanelFixture()
const workspaceFileApi = {
listWorkspaceEntries: vi.fn().mockImplementation((_deviceId: string, path: string) =>
Promise.resolve({
path,
entries: [],
})
),
readWorkspaceTextFile: vi.fn().mockImplementation((_deviceId: string, path: string) =>
Promise.resolve({
path,
name: 'outside.md',
content: 'outside workspace preview',
editable: true,
revision: 'sha256:outside',
truncated: false,
size: 25,
modifiedAt: null,
})
),
}
const messages = [
{
id: 'assistant-edited-absolute-file',
taskId: 101,
role: 'assistant' as const,
content: '',
status: 'completed' as const,
createdAt: '2026-08-05T00:00:00.000Z',
blocks: [
{
id: 'edit-absolute-file',
subtaskId: 101,
type: 'tool' as const,
toolName: 'edit_file',
toolInput: {
path: '/tmp/outside.md',
old_string: 'before',
new_string: 'after',
},
status: 'completed' as const,
createdAt: 1770000000000,
},
],
},
]
const propsWithFileMessage = (task: typeof taskA) => ({
...propsForTask(task),
messages,
workspaceFileApi,
})
const activePane = () => within(screen.getByTestId('desktop-workbench-main'))
const { rerender } = render(<DesktopWorkbenchLayout {...propsWithFileMessage(taskA)} />)

await userEvent.click(activePane().getByRole('button', { name: /正在编辑 outside\.md/ }))
expect(await activePane().findByTestId('workspace-markdown-preview')).toHaveTextContent(
'outside workspace preview'
)

rerender(<DesktopWorkbenchLayout {...propsWithFileMessage(taskB)} />)
rerender(<DesktopWorkbenchLayout {...propsWithFileMessage(taskA)} />)

await waitFor(() => {
expect(activePane().getByTestId('right-workspace-file-tab')).toHaveAttribute(
'aria-selected',
'true'
)
expect(activePane().getByTestId('workspace-file-path')).toHaveTextContent('/tmp/outside.md')
expect(activePane().getByTestId('workspace-markdown-preview')).toHaveTextContent(
'outside workspace preview'
)
})
expect(workspaceFileApi.readWorkspaceTextFile).toHaveBeenCalledTimes(2)
})

test('preserves the review when switching runtime tasks', async () => {
const { propsForTask, taskA, taskB } = createLocalRuntimeTaskPanelFixture()
const onLoadEnvironmentDiff = vi
.fn()
.mockResolvedValue(
'diff --git a/src/task-a.ts b/src/task-a.ts\n--- a/src/task-a.ts\n+++ b/src/task-a.ts\n@@ -1 +1 @@\n-old\n+restored\n'
)
const propsWithReview = (task: typeof taskA) => ({
...propsForTask(task),
onLoadEnvironmentDiff,
})
const activePane = () => within(screen.getByTestId('desktop-workbench-main'))
const { rerender } = render(<DesktopWorkbenchLayout {...propsWithReview(taskA)} />)

await userEvent.click(activePane().getByTestId('toggle-right-workspace-panel-button'))
await userEvent.click(activePane().getByTestId('right-workspace-review-option'))
expect(await activePane().findByTestId('file-changes-review-panel')).toHaveTextContent(
'src/task-a.ts'
)

rerender(<DesktopWorkbenchLayout {...propsWithReview(taskB)} />)
rerender(<DesktopWorkbenchLayout {...propsWithReview(taskA)} />)

await waitFor(() => {
expect(activePane().getByTestId('right-workspace-review-tab')).toHaveAttribute(
'aria-selected',
'true'
)
expect(activePane().getByTestId('file-changes-review-panel')).toHaveTextContent(
'src/task-a.ts'
)
expect(activePane().getByTestId('file-changes-review-panel')).toHaveTextContent('restored')
})
expect(onLoadEnvironmentDiff).toHaveBeenCalledTimes(1)
})

test('preserves the open directory when switching runtime tasks', async () => {
const { propsForTask, taskA, taskB } = createLocalRuntimeTaskPanelFixture()
const workspaceFileApi = {
Expand Down
Loading
Loading