From 534c01add1149d9fadce06a92872467eb77d3d5e Mon Sep 17 00:00:00 2001 From: AlphaCat Date: Wed, 16 Sep 2026 23:55:02 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(chat):=20=E4=BE=A7=E6=A0=8F=E7=82=B9?= =?UTF-8?q?=E9=80=89=E8=B7=A8=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=97=B6=E5=90=8C=E6=AD=A5=E6=BF=80=E6=B4=BB=E5=85=B6?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E7=A9=BA=E9=97=B4=20(#787)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 普通点选会话只走 openController.open,没有把会话所属工作空间置为当前, 右侧 dock 的文件树/终端/Git 仍停在上一个工作空间。从搜索路径抽出 activateConversationWorkspace,Desktop 与 WebUI 的普通点选分支在打开前 按会话 cwd 激活对应工作空间;同工作空间或无 cwd 时不动。 Refs #787 --- .../test/webui/history-chat-ui.test.mjs | 65 +++++++++++++++++++ .../agent-gateway/web/src/app/GatewayApp.tsx | 2 + .../web/src/app/gatewayConversationActions.ts | 11 ++++ .../app/hooks/useGatewayWorkspaceProjects.ts | 41 +++++++----- crates/agent-gui/src/pages/ChatPage.tsx | 14 ++++ .../chat/workspace/useWorkspaceProjects.ts | 41 +++++++----- ...sue-787-sidebar-filetree-workspace-sync.md | 52 +++++++++++++++ 7 files changed, 194 insertions(+), 32 deletions(-) create mode 100644 docs/worklog/issue-787-sidebar-filetree-workspace-sync.md diff --git a/crates/agent-gateway/test/webui/history-chat-ui.test.mjs b/crates/agent-gateway/test/webui/history-chat-ui.test.mjs index 1711a7379..24a69780a 100644 --- a/crates/agent-gateway/test/webui/history-chat-ui.test.mjs +++ b/crates/agent-gateway/test/webui/history-chat-ui.test.mjs @@ -214,6 +214,71 @@ test("web edit-resend retains only persisted conversation references", async () assert.equal(calls[0].options.editMessageRef, messageRef); }); +// #787: picking a conversation that belongs to another workspace from the +// sidebar session tree must bring that workspace to the front so the right +// dock (file tree / terminal / git) follows; search already did this via +// beforeCommit, the plain click path did not. +function createSidebarSelectHarness({ isAgentMode, sidebarCwd, runtimeWorkdir }) { + const activated = []; + const opened = []; + const actions = createGatewayConversationActions({ + isAgentMode, + activeView: "chat", + activateConversationWorkspace: (cwd) => activated.push(cwd), + activateSearchConversationWorkspace: () => {}, + sidebarStore: { + peek: (id) => (id === "conversation-b" && sidebarCwd ? { id, cwd: sidebarCwd } : undefined), + upsertLocal: () => {}, + }, + conversationWorkdirsRef: { + current: new Map(runtimeWorkdir ? [["conversation-b", runtimeWorkdir]] : []), + }, + openController: { + cancel: () => {}, + open: (id, options) => opened.push({ id, options }), + }, + getVisibleComposerConversationId: () => "conversation-a", + isLocalDraftConversationId: () => false, + prepareComposerForConversationChange: () => {}, + restoreCachedComposerDraft: () => {}, + setActiveView: () => {}, + setSidebarOpen: () => {}, + pendingDisplayedConversationAutoBottomRef: { current: null }, + }); + return { actions, activated, opened }; +} + +test("sidebar select activates the conversation's workspace before opening it", () => { + const { actions, activated, opened } = createSidebarSelectHarness({ + isAgentMode: true, + sidebarCwd: "/workspace/project-b", + }); + actions.handleSidebarSelectConversation("conversation-b"); + assert.deepEqual(activated, ["/workspace/project-b"]); + assert.equal(opened.length, 1); + assert.equal(opened[0].id, "conversation-b"); +}); + +test("sidebar select falls back to the runtime workdir when the sidebar row has no cwd", () => { + const { actions, activated } = createSidebarSelectHarness({ + isAgentMode: true, + sidebarCwd: "", + runtimeWorkdir: "/workspace/project-runtime", + }); + actions.handleSidebarSelectConversation("conversation-b"); + assert.deepEqual(activated, ["/workspace/project-runtime"]); +}); + +test("sidebar select leaves the workspace alone outside agent mode", () => { + const { actions, activated, opened } = createSidebarSelectHarness({ + isAgentMode: false, + sidebarCwd: "/workspace/project-b", + }); + actions.handleSidebarSelectConversation("conversation-b"); + assert.deepEqual(activated, []); + assert.equal(opened.length, 1); +}); + test("parseHistoryMessagesJson preserves Image tool result image content", () => { const entries = chatUi.parseHistoryMessagesJson(JSON.stringify([ { diff --git a/crates/agent-gateway/web/src/app/GatewayApp.tsx b/crates/agent-gateway/web/src/app/GatewayApp.tsx index 096ab2b49..84598ff30 100644 --- a/crates/agent-gateway/web/src/app/GatewayApp.tsx +++ b/crates/agent-gateway/web/src/app/GatewayApp.tsx @@ -461,6 +461,7 @@ function useGatewayAppController() { >(() => ""); const { activateWorkspaceProject, + activateConversationWorkspace, activateSearchConversationWorkspace, clearSearchConversationWorkspace, searchConversationWorkdir, @@ -1138,6 +1139,7 @@ function useGatewayAppController() { handleSidebarSelectConversation, startNewConversation, } = createGatewayConversationActions({ + activateConversationWorkspace, activateSearchConversationWorkspace, clearSearchConversationWorkspace, activeView, diff --git a/crates/agent-gateway/web/src/app/gatewayConversationActions.ts b/crates/agent-gateway/web/src/app/gatewayConversationActions.ts index e85fa511a..b4d20d194 100644 --- a/crates/agent-gateway/web/src/app/gatewayConversationActions.ts +++ b/crates/agent-gateway/web/src/app/gatewayConversationActions.ts @@ -25,6 +25,7 @@ import { isMobileSidebarLayout } from "./historyUtils"; import type { SendChatFn } from "./types"; type CreateGatewayConversationActionsOptions = { + activateConversationWorkspace: (cwd?: string) => void; activateSearchConversationWorkspace: (cwd?: string) => void; clearSearchConversationWorkspace: () => void; activeView: ApplicationViewId; @@ -168,6 +169,16 @@ export function createGatewayConversationActions(options: CreateGatewayConversat options.restoreCachedComposerDraft(targetConversationId); return; } + // 侧栏会话树跨工作空间点选:先把会话所属工作空间置为当前,右侧文件树/ + // 终端/Git 才会跟着切到该工作空间根目录(#787)。搜索入口已在 + // beforeCommit 里做同样的事,这里补齐普通点选通路。 + if (options.isAgentMode) { + options.activateConversationWorkspace( + options.sidebarStore.peek(targetConversationId)?.cwd?.trim() || + options.conversationWorkdirsRef.current.get(targetConversationId)?.trim() || + "", + ); + } options.openController.open(targetConversationId); options.restoreCachedComposerDraft(targetConversationId); }; diff --git a/crates/agent-gateway/web/src/app/hooks/useGatewayWorkspaceProjects.ts b/crates/agent-gateway/web/src/app/hooks/useGatewayWorkspaceProjects.ts index 7a839e8bc..8333714d7 100644 --- a/crates/agent-gateway/web/src/app/hooks/useGatewayWorkspaceProjects.ts +++ b/crates/agent-gateway/web/src/app/hooks/useGatewayWorkspaceProjects.ts @@ -264,30 +264,38 @@ export function useGatewayWorkspaceProjects({ ], ); - // Reading persisted history does not require the original directory to exist. - const activateSearchConversationWorkspace = useCallback( + // Bring the workspace that owns `cwd` to the front so the right dock (file + // tree / terminal / git) and the sidebar highlight follow the conversation + // being opened. Reading persisted history does not require the original + // directory to exist, so a missing directory is preserved rather than + // cleared. No-op when the conversation already belongs to the active + // workspace or has no cwd. + const activateConversationWorkspace = useCallback( (cwd?: string) => { const path = cwd?.trim() ?? ""; if ( - path && - workspaceProjectPathKey(path) !== workspaceProjectPathKey(activeWorkspaceProjectPath) + !path || + workspaceProjectPathKey(path) === workspaceProjectPathKey(activeWorkspaceProjectPath) ) { - const project = - workspaceProjects.find( - (item) => workspaceProjectPathKey(item.path) === workspaceProjectPathKey(path), - ) ?? createWorkspaceProjectFromPath(path, "history"); - activateWorkspaceProject(project, { preserveMissing: true }); + return; } + const project = + workspaceProjects.find( + (item) => workspaceProjectPathKey(item.path) === workspaceProjectPathKey(path), + ) ?? createWorkspaceProjectFromPath(path, "history"); + activateWorkspaceProject(project, { preserveMissing: true }); + }, + [activeWorkspaceProjectPath, workspaceProjects, activateWorkspaceProject], + ); + + const activateSearchConversationWorkspace = useCallback( + (cwd?: string) => { + const path = cwd?.trim() ?? ""; + activateConversationWorkspace(path); setSearchNavigation({ mode: settings.system.executionMode, cwd: path }); sidebarStore.setScope(path ? { kind: "workdir", cwd: path } : { kind: "unscoped" }); }, - [ - activeWorkspaceProjectPath, - workspaceProjects, - activateWorkspaceProject, - settings.system.executionMode, - sidebarStore, - ], + [activateConversationWorkspace, settings.system.executionMode, sidebarStore], ); const handleSelectWorkspaceProject = useCallback( @@ -631,6 +639,7 @@ export function useGatewayWorkspaceProjects({ return { activateWorkspaceProject, + activateConversationWorkspace, activateSearchConversationWorkspace, clearSearchConversationWorkspace, searchConversationWorkdir: searchCwd, diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index edaae4545..eb89b5359 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -353,6 +353,7 @@ export function ChatPage(props: ChatPageProps) { sidebarScope, historyScopeKey, activateWorkspaceProject, + activateConversationWorkspace, activateSearchConversationWorkspace, clearSearchConversationWorkspace, searchConversationWorkdir, @@ -1796,11 +1797,24 @@ export function ChatPage(props: ChatPageProps) { }); } else { prepareComposerForConversationChange(); + // 侧栏会话树跨工作空间点选:先把会话所属工作空间置为当前,右侧 + // 文件树/终端/Git 才会跟着切到该工作空间根目录(#787)。搜索入口 + // 已在 beforeCommit 里做同样的事,这里补齐普通点选通路。 + if (isAgentMode) { + const targetWorkdir = + sidebarStore.peek(targetConversationId)?.cwd?.trim() || + conversationRuntimeCacheRef.current.get(targetConversationId)?.workdir?.trim() || + ""; + activateConversationWorkspace(targetWorkdir); + } openController.open(targetConversationId); } }, [ + activateConversationWorkspace, activateSearchConversationWorkspace, + conversationRuntimeCacheRef, + isAgentMode, openController, prepareComposerForConversationChange, sidebarStore, diff --git a/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts b/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts index fda26c9a5..21651c9d6 100644 --- a/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts +++ b/crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts @@ -303,30 +303,38 @@ export function useWorkspaceProjects(params: UseWorkspaceProjectsParams) { [setSettings, workspaceProjects, activeWorkspaceProjectId, settings.system], ); - // Reading persisted history does not require the original directory to exist. - const activateSearchConversationWorkspace = useCallback( + // Bring the workspace that owns `cwd` to the front so the right dock (file + // tree / terminal / git) and the sidebar highlight follow the conversation + // being opened. Reading persisted history does not require the original + // directory to exist, so a missing directory is preserved rather than + // cleared. No-op when the conversation already belongs to the active + // workspace or has no cwd. + const activateConversationWorkspace = useCallback( (cwd?: string) => { const path = cwd?.trim() ?? ""; if ( - path && - workspaceProjectPathKey(path) !== workspaceProjectPathKey(activeWorkspaceProjectPath) + !path || + workspaceProjectPathKey(path) === workspaceProjectPathKey(activeWorkspaceProjectPath) ) { - const project = - workspaceProjects.find( - (item) => workspaceProjectPathKey(item.path) === workspaceProjectPathKey(path), - ) ?? createWorkspaceProjectFromPath(path, "history"); - activateWorkspaceProject(project, { preserveMissing: true }); + return; } + const project = + workspaceProjects.find( + (item) => workspaceProjectPathKey(item.path) === workspaceProjectPathKey(path), + ) ?? createWorkspaceProjectFromPath(path, "history"); + activateWorkspaceProject(project, { preserveMissing: true }); + }, + [activeWorkspaceProjectPath, workspaceProjects, activateWorkspaceProject], + ); + + const activateSearchConversationWorkspace = useCallback( + (cwd?: string) => { + const path = cwd?.trim() ?? ""; + activateConversationWorkspace(path); setSearchNavigation({ mode: isAgentMode, cwd: path }); sidebarStore.setScope(path ? { kind: "workdir", cwd: path } : { kind: "unscoped" }); }, - [ - activeWorkspaceProjectPath, - workspaceProjects, - activateWorkspaceProject, - isAgentMode, - sidebarStore, - ], + [activateConversationWorkspace, isAgentMode, sidebarStore], ); const handleSelectWorkspaceProject = useCallback( @@ -736,6 +744,7 @@ export function useWorkspaceProjects(params: UseWorkspaceProjectsParams) { historyScopeKey, checkWorkspaceProjectDirectory, activateWorkspaceProject, + activateConversationWorkspace, activateSearchConversationWorkspace, clearSearchConversationWorkspace, searchConversationWorkdir: searchCwd, diff --git a/docs/worklog/issue-787-sidebar-filetree-workspace-sync.md b/docs/worklog/issue-787-sidebar-filetree-workspace-sync.md new file mode 100644 index 000000000..474db1660 --- /dev/null +++ b/docs/worklog/issue-787-sidebar-filetree-workspace-sync.md @@ -0,0 +1,52 @@ +# Issue #787:切换工作空间会话时右侧文件树未同步切换 + +日期:2026-09-16。状态:本地实现与自动验证完成,已在分支提交,尚未推送或创建 PR。 + +## 起因 + +v1.3.5(main @ 4eb6a4a8)复现:左侧会话树里点选另一个工作空间下的会话,会话内容切换了,但右侧文件树、终端、Git 面板仍停留在上一个工作空间。上游已有 Issue #787(v1.3.2 报的,open,无关联 PR),所以没有另开 issue,只在 #787 下留言确认复现并说明分支。 + +## 分支 + +fix/sidebar-filetree-sync,基线 main @ 4eb6a4a8。 + +## 根因 + +右侧 dock 的 `cwd` / `projectPathKey` 来自 `activeWorkspaceProjectPath`(Desktop 的 `useWorkspaceProjects`、WebUI 的 `useGatewayWorkspaceProjects`),而不是当前会话的 cwd。它只在用户点项目行、Workbench 聚焦 Pane、或者从搜索弹窗打开会话(#752 修的 `beforeCommit` 路径)时才被更新。PR #770 把侧栏改成跨工作空间的会话树以后,普通点选会话走的是 `openController.open(id)` 直开,没有任何一步把会话所属工作空间置为当前,dock 自然不动。 + +## 修复 + +两端一致: + +- 从 `activateSearchConversationWorkspace` 里抽出 `activateConversationWorkspace(cwd)`,只做一件事,按 cwd 找到(或以 history 类型临时建)对应项目并 `activateWorkspaceProject(project, { preserveMissing: true })`;同工作空间或无 cwd 时直接返回,不写 settings。搜索路径改为调用它再做自己的 scope 处理,行为不变。 +- Desktop `ChatPage.handleSelectConversation` 与 WebUI `handleSidebarSelectConversation` 的普通点选分支,在 `openController.open` 之前调用它。cwd 取侧栏行的持久化 cwd,取不到再退到运行时缓存的 workdir。只在 agent 模式下做,text 模式不动工作空间。 + +不改 `activateWorkspaceProject` 本身,不动搜索路径与 Workbench 路径的语义。 + +## 涉及文件 + +- crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts +- crates/agent-gui/src/pages/ChatPage.tsx +- crates/agent-gateway/web/src/app/hooks/useGatewayWorkspaceProjects.ts +- crates/agent-gateway/web/src/app/gatewayConversationActions.ts +- crates/agent-gateway/web/src/app/GatewayApp.tsx +- crates/agent-gateway/test/webui/history-chat-ui.test.mjs(新增 3 个用例) + +## 自动验证 + +| 检查 | 结果 | +| --- | --- | +| tsc --noEmit(agent-gui) | 通过 | +| tsc --noEmit(gateway-webui) | 通过 | +| biome check 本次 5 个源码文件 | 通过 | +| pnpm test:webui | 721/721 通过(含新增 3 个) | +| pnpm check:fast | Shared UI boundaries 项失败,报的 3 个文件本次未改动,是基线已有问题 | + +## 已知边界 + +- 若当前是一个空白草稿会话,点选另一工作空间的会话时,工作空间先切换,`ChatPage` 里那条"空草稿跟随当前工作空间"的 effect 会把旧草稿的 workdir 也改过去。和用户直接点项目行时的行为一致,搜索路径同样如此,没有额外处理。 +- 归档项目的会话不会在侧栏渲染,所以不会经这条路径把归档项目激活回来。 + +## 待办 + +- 用户在客户端验收后推送分支、开 PR 关联 #787。 From c65d2547582ea9e1c0186d2af9d4cb663f6adc40 Mon Sep 17 00:00:00 2001 From: AlphaCat Date: Thu, 17 Sep 2026 01:19:17 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(chat):=20=E5=B7=A5=E4=BD=9C=E7=A9=BA?= =?UTF-8?q?=E9=97=B4=E6=BF=80=E6=B4=BB=E6=94=B9=E6=8C=82=E5=88=B0=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E6=89=93=E5=BC=80=E7=9A=84=20afterCommit,=E6=B6=88?= =?UTF-8?q?=E9=99=A4=E4=B8=A4=E6=AC=A1=E7=82=B9=E5=87=BB=20(#787)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 点选前先激活工作空间会让侧栏作用域先行刷新,旧的当前会话从列表消失 触发兜底新建草稿,把这次打开顶掉——表现为要点两次。改为会话提交后 (afterCommit)再激活其工作空间;Desktop 的 cache-hit 分支补调 afterCommit, 热路径同样生效。cwd 优先取运行时/历史响应的权威 workdir。 Refs #787 --- .../test/webui/history-chat-ui.test.mjs | 18 ++++++++----- .../web/src/app/gatewayConversationActions.ts | 23 +++++++++------- crates/agent-gui/src/pages/ChatPage.tsx | 26 +++++++++++-------- .../history/useConversationHistoryActions.ts | 4 +++ ...sue-787-sidebar-filetree-workspace-sync.md | 13 +++++++--- 5 files changed, 54 insertions(+), 30 deletions(-) diff --git a/crates/agent-gateway/test/webui/history-chat-ui.test.mjs b/crates/agent-gateway/test/webui/history-chat-ui.test.mjs index 24a69780a..67b31d60c 100644 --- a/crates/agent-gateway/test/webui/history-chat-ui.test.mjs +++ b/crates/agent-gateway/test/webui/history-chat-ui.test.mjs @@ -248,24 +248,29 @@ function createSidebarSelectHarness({ isAgentMode, sidebarCwd, runtimeWorkdir }) return { actions, activated, opened }; } -test("sidebar select activates the conversation's workspace before opening it", () => { +test("sidebar select opens immediately and activates the workspace only at commit", () => { const { actions, activated, opened } = createSidebarSelectHarness({ isAgentMode: true, sidebarCwd: "/workspace/project-b", }); actions.handleSidebarSelectConversation("conversation-b"); - assert.deepEqual(activated, ["/workspace/project-b"]); + // 点选瞬间只发起打开,不先切工作空间(否则作用域先行刷新会顶掉本次打开)。 assert.equal(opened.length, 1); assert.equal(opened[0].id, "conversation-b"); + assert.deepEqual(activated, []); + // 会话提交后才激活其工作空间。 + opened[0].options.afterCommit(); + assert.deepEqual(activated, ["/workspace/project-b"]); }); -test("sidebar select falls back to the runtime workdir when the sidebar row has no cwd", () => { - const { actions, activated } = createSidebarSelectHarness({ +test("sidebar select prefers the authoritative runtime workdir at commit time", () => { + const { actions, activated, opened } = createSidebarSelectHarness({ isAgentMode: true, - sidebarCwd: "", + sidebarCwd: "/workspace/project-stale", runtimeWorkdir: "/workspace/project-runtime", }); actions.handleSidebarSelectConversation("conversation-b"); + opened[0].options.afterCommit(); assert.deepEqual(activated, ["/workspace/project-runtime"]); }); @@ -275,8 +280,9 @@ test("sidebar select leaves the workspace alone outside agent mode", () => { sidebarCwd: "/workspace/project-b", }); actions.handleSidebarSelectConversation("conversation-b"); - assert.deepEqual(activated, []); assert.equal(opened.length, 1); + opened[0].options.afterCommit(); + assert.deepEqual(activated, []); }); test("parseHistoryMessagesJson preserves Image tool result image content", () => { diff --git a/crates/agent-gateway/web/src/app/gatewayConversationActions.ts b/crates/agent-gateway/web/src/app/gatewayConversationActions.ts index b4d20d194..e4b805909 100644 --- a/crates/agent-gateway/web/src/app/gatewayConversationActions.ts +++ b/crates/agent-gateway/web/src/app/gatewayConversationActions.ts @@ -169,17 +169,20 @@ export function createGatewayConversationActions(options: CreateGatewayConversat options.restoreCachedComposerDraft(targetConversationId); return; } - // 侧栏会话树跨工作空间点选:先把会话所属工作空间置为当前,右侧文件树/ - // 终端/Git 才会跟着切到该工作空间根目录(#787)。搜索入口已在 - // beforeCommit 里做同样的事,这里补齐普通点选通路。 - if (options.isAgentMode) { - options.activateConversationWorkspace( - options.sidebarStore.peek(targetConversationId)?.cwd?.trim() || + // 侧栏会话树跨工作空间点选:会话提交后(afterCommit)再把它所属的 + // 工作空间置为当前,右侧文件树/终端/Git 随之切到该工作空间根目录 + // (#787)。等提交而不是点选瞬间,避免作用域先行刷新与打开流程互相 + // 打架;cwd 优先取本次历史响应写入的权威 workdir。 + options.openController.open(targetConversationId, { + afterCommit: () => { + if (!options.isAgentMode) return; + options.activateConversationWorkspace( options.conversationWorkdirsRef.current.get(targetConversationId)?.trim() || - "", - ); - } - options.openController.open(targetConversationId); + options.sidebarStore.peek(targetConversationId)?.cwd?.trim() || + "", + ); + }, + }); options.restoreCachedComposerDraft(targetConversationId); }; diff --git a/crates/agent-gui/src/pages/ChatPage.tsx b/crates/agent-gui/src/pages/ChatPage.tsx index eb89b5359..7d2ab9064 100644 --- a/crates/agent-gui/src/pages/ChatPage.tsx +++ b/crates/agent-gui/src/pages/ChatPage.tsx @@ -1797,17 +1797,21 @@ export function ChatPage(props: ChatPageProps) { }); } else { prepareComposerForConversationChange(); - // 侧栏会话树跨工作空间点选:先把会话所属工作空间置为当前,右侧 - // 文件树/终端/Git 才会跟着切到该工作空间根目录(#787)。搜索入口 - // 已在 beforeCommit 里做同样的事,这里补齐普通点选通路。 - if (isAgentMode) { - const targetWorkdir = - sidebarStore.peek(targetConversationId)?.cwd?.trim() || - conversationRuntimeCacheRef.current.get(targetConversationId)?.workdir?.trim() || - ""; - activateConversationWorkspace(targetWorkdir); - } - openController.open(targetConversationId); + // 侧栏会话树跨工作空间点选:会话提交后再把它所属的工作空间置为 + // 当前,右侧 dock(文件树/终端/Git)随 activeWorkspaceProjectPath + // 切到该工作空间根目录(#787)。必须等 afterCommit——先切工作空间 + // 会让侧栏作用域先行刷新,旧的当前会话从列表消失会触发 + // "会话被删"兜底新建草稿,反把这次打开顶掉(表现为要点两次)。 + openController.open(targetConversationId, { + afterCommit: () => { + if (!isAgentMode) return; + const targetWorkdir = + conversationRuntimeCacheRef.current.get(targetConversationId)?.workdir?.trim() || + sidebarStore.peek(targetConversationId)?.cwd?.trim() || + ""; + activateConversationWorkspace(targetWorkdir); + }, + }); } }, [ diff --git a/crates/agent-gui/src/pages/chat/history/useConversationHistoryActions.ts b/crates/agent-gui/src/pages/chat/history/useConversationHistoryActions.ts index 8f18b47ab..5b4026cbc 100644 --- a/crates/agent-gui/src/pages/chat/history/useConversationHistoryActions.ts +++ b/crates/agent-gui/src/pages/chat/history/useConversationHistoryActions.ts @@ -272,6 +272,10 @@ export function useConversationHistoryActions(params: UseConversationHistoryActi entry: cached, clearError: true, }); + // Cache-hit is a real commit: afterCommit must fire here too, or + // callers that defer work to the commit point (e.g. workspace + // activation for #787) silently skip on the warm path. + request?.afterCommit?.(); return "cache-hit"; } conversationRuntimeCacheRef.current.delete(id); diff --git a/docs/worklog/issue-787-sidebar-filetree-workspace-sync.md b/docs/worklog/issue-787-sidebar-filetree-workspace-sync.md index 474db1660..5a3f9ddb4 100644 --- a/docs/worklog/issue-787-sidebar-filetree-workspace-sync.md +++ b/docs/worklog/issue-787-sidebar-filetree-workspace-sync.md @@ -19,14 +19,20 @@ fix/sidebar-filetree-sync,基线 main @ 4eb6a4a8。 两端一致: - 从 `activateSearchConversationWorkspace` 里抽出 `activateConversationWorkspace(cwd)`,只做一件事,按 cwd 找到(或以 history 类型临时建)对应项目并 `activateWorkspaceProject(project, { preserveMissing: true })`;同工作空间或无 cwd 时直接返回,不写 settings。搜索路径改为调用它再做自己的 scope 处理,行为不变。 -- Desktop `ChatPage.handleSelectConversation` 与 WebUI `handleSidebarSelectConversation` 的普通点选分支,在 `openController.open` 之前调用它。cwd 取侧栏行的持久化 cwd,取不到再退到运行时缓存的 workdir。只在 agent 模式下做,text 模式不动工作空间。 +- Desktop `ChatPage.handleSelectConversation` 与 WebUI `handleSidebarSelectConversation` 的普通点选分支,把该激活挂在 `openController.open` 的 `afterCommit` 上:会话先提交(当前会话已切成目标会话),提交后再激活其工作空间。cwd 优先取运行时缓存/历史响应的权威 workdir,退到侧栏行 cwd。只在 agent 模式下做,text 模式不动工作空间。 +- Desktop `useConversationHistoryActions.openInitial` 的 cache-hit 分支补调 `request?.afterCommit?.()`(热路径同样是一次真实提交,此前只有 painted 路径调用)。搜索路径不走 cache-hit 分支,不受影响。 -不改 `activateWorkspaceProject` 本身,不动搜索路径与 Workbench 路径的语义。 +### 为什么必须等 afterCommit(第一版返工的原因) + +第一版在 `open` 之前同步激活工作空间,实测表现为"点一次只切工作空间,要再点一次才进会话"。机制:激活 → `sidebarScope` 先行切换 → 侧栏 store 重载后旧的当前会话从列表消失 → ChatPage 里"当前会话被删则新建草稿"的兜底 effect 触发 `startNewConversation` → `cancelConversationLoad` 顶掉了正在进行的这次打开,用户落在新工作空间的空白草稿上。改为提交后激活,此时当前会话已是目标会话(存在于新作用域列表中),兜底 effect 不再误判。 + +不改 `activateWorkspaceProject` 本身,不动搜索路径与 Workbench 路径的语义。文件树面板关闭时同样更新 `activeWorkspaceProjectPath`(RightDockPanel 的 `cwd`/`projectPathKey` 常驻传入),下次展开即为对应工作空间根目录。 ## 涉及文件 - crates/agent-gui/src/pages/chat/workspace/useWorkspaceProjects.ts - crates/agent-gui/src/pages/ChatPage.tsx +- crates/agent-gui/src/pages/chat/history/useConversationHistoryActions.ts - crates/agent-gateway/web/src/app/hooks/useGatewayWorkspaceProjects.ts - crates/agent-gateway/web/src/app/gatewayConversationActions.ts - crates/agent-gateway/web/src/app/GatewayApp.tsx @@ -38,8 +44,9 @@ fix/sidebar-filetree-sync,基线 main @ 4eb6a4a8。 | --- | --- | | tsc --noEmit(agent-gui) | 通过 | | tsc --noEmit(gateway-webui) | 通过 | -| biome check 本次 5 个源码文件 | 通过 | +| biome check 本次源码文件 | 通过 | | pnpm test:webui | 721/721 通过(含新增 3 个) | +| pnpm test:gui | 3087/3095,8 个失败在基线 main @ 4eb6a4a8 上复测同样失败,与本次改动无关 | | pnpm check:fast | Shared UI boundaries 项失败,报的 3 个文件本次未改动,是基线已有问题 | ## 已知边界