Skip to content

fix(sidebar): worktree 会话的根选择与地址基准(#947 / #950 / #952) - #956

Open
yanzhaohui1999 wants to merge 5 commits into
omdsh-dev:devfrom
yanzhaohui1999:fix/worktree-root-address-base
Open

yanzhaohui1999 wants to merge 5 commits into
omdsh-dev:devfrom
yanzhaohui1999:fix/worktree-root-address-base

Conversation

@yanzhaohui1999

@yanzhaohui1999 yanzhaohui1999 commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

三条 issue 一次修掉:worktree 会话的根选择与地址基准

同一场「会话 header cwd 是主检出、agent 在 linked git worktree 里干活」下暴露出来的三条:

issue 症状 失败模式
#947 聊天链接 / 改动页打开 worktree 内文件 → 路径被二次拼接,cannot read "…/wt/frontend/.worktrees/wt/CLAUDE.md": ENOENT 响亮
#950 同一相对路径经不同入口 → 两个检出的同名文件,两次都成功、无任何提示 静默,且可保存写回
#952 工作区内部嵌套仓库的 linked worktree 被当成「活动 worktree」→ explorer 被静默重定根到该子目录,兄弟目录消失 静默

根因

  1. 地址的基准是会话 header cwd,宿主两端自洽:宿主构造端 ui-chat 用 fileAddressFor(sessionId, sessions.list.byId[sid].cwd, path),解析端 api-workspace-files 用 sessions.get(sid).header.cwd —— 同一个值,所以地址里的相对路径只在那个基准下成立。
  2. 插件读的一端换成了活动 worktree 根(feat(sidebar): 文件树跟随会话实际所在的 git worktree(含跨仓库 linked worktree) #802):fs 路由与原生 tab 体跟随活动 worktree,于是同一会话有两个基准。地址里 header 相对的拼写被活动根再拼一次 → 翻倍 ENOENT;两个检出都有同名文件时静默读到另一份。只有「worktree 嵌在 header cwd 之下」才翻倍,所以 feat(sidebar): 文件树跟随会话实际所在的 git worktree(含跨仓库 linked worktree) #802 的跨仓库场景(工作区之外)没暴露它。
  3. 同一插件里还有第三、第四个基准:openSidebarFile 用冻结的 summary?.cwd,service.openTab → surface.fileAddress 用调用方手上的 scope.cwd(多数是活动根)→ 同一文件可能拿到两个地址(页签不合并),同一相对路径在不同入口落到不同检出。
  4. 根选择判定只看拓扑:activeRootFromEvents 的判据是「top ≠ base 且是 linked worktree」,不区分「会话搬到别的检出」与「工作区里本来就有另一个仓库的 worktree」(bare 仓库的 main/local 检出、容器工作区里的子项目),于是 explorer 被抢根。

口径(决策记录)

工作区内部的 linked worktree 只有属于同一仓库(--git-common-dir 相等)才算「会话搬走了」;工作区外的任何 linked worktree 照旧采纳(#802 的跨仓库场景)。

理由:.worktrees/<branch> 是 DSH 自己的约定,也是 #947 报告者的布局(他三个建议修法里两个假设它继续跟随);而「工作区里嵌套的另一个仓库」是工作区普通内容,采纳它只会把其它兄弟目录藏起来。另一条候选口径「工作区内一律不采纳」虽然更小,但会连带关掉 .worktrees/ 的跟随,故未采纳。

实现(四条 commit)

commit 1(#952) fix(active-worktree): 工作区内的 linked worktree 只在同仓库时采纳

  • 新增 gitIdentityOf(dir)(rev-parse --absolute-git-dir --git-common-dir → {gitDir, commonDir},进程级缓存,相对拼写按被探测目录归一化);
  • isLinkedWorktree 改走该身份,新增 isAdoptableTop(top, base):工作区外一律采纳,工作区内只有 commonDir 与 base 相等才采纳,base 是文件系统根时跳过该判定(免得关掉整条功能)。

commit 2(#947 / #950) fix(sidebar): 会话地址的基准统一到会话 header cwd

  • 新增 src/client/address-base.ts 的 addressBaseOf(ctx, sessionId)(会话列表里的 header cwd);
  • 消费边界:fileParamsOf(info, base?) 在切 path:line 规格之前把地址路径解析成绝对路径;native 的 editor 类型注入 paramsOf: (info) => fileParamsOf(info, addressBaseOf(ctx, fileSessionIdOf(info))) —— 下游(viewer 的 fs.read、编辑器、目录 watch)只见绝对路径,活动根再无法重定基;
  • 构造边界:surface.fileAddress 与 openClaimedNativeFile 都以 addressBaseOf 优先,调用方 cwd 只作回退 → 同一文件只有一个地址(与宿主聊天路径的地址一致,页签照旧合并);
  • 意图侧:openSidebarFile(ctx, sessionId, path, { line?, cwd? }) 的 cwd 是调用方手上的活根(EditorHost / 改动页 / markdown 面板传 useSessionRoot 的结果),相对路径按它解析,缺省才回退 header cwd。

commit 3 文档:docs/plans/2026-10-14-worktree-root-and-address-base.md(含口径 A/B 决策表与实测真值表)、AGENTS §3.4 第 13 条、接入指南 §10 同名条目(消费插件自己也解析文件地址时照同一条规则)。

commit 4(Windows,CI 抓出来的) fix(sidebar): 地址路径按基的分隔符拼写

  • 新增 resolveAddressPath(base, path)(在 address-base.ts):在地址边界把结果折叠成 base 的分隔符风格 —— Windows 上同一文件只有一个拼写(也就只有一个页签身份);POSIX 原样保留(那里 \ 是合法文件名字符,这也是 resolveSidebarPath 从不在 POSIX 折叠的原因)。
  • fileParamsOf 与 openSidebarFile 都改走它;resolveSidebarPath 与其既有断言(tests/paths.spec.ts 钉住的混合拼写)逐字未动。

验证

  • 修前复现(真 git worktree + 真函数):宿主地址构造 → 插件解析 → 真实 /sidebar/api/fs.read 返回
    cannot read "…/.worktrees/947-click-demo/.worktrees/947-click-demo/IN-WORKTREE.md": ENOENT;本机 3080 上真机点击同样命中。
    真值表(三种布局 × 三种口径)逐条实测。
  • 判别力:把新用例搬到未改动的 origin/dev 上跑 → tests/active-worktree.spec.ts 挂 2/7(正好是 [Bug] 容器工作区里嵌套仓库的 linked worktree 被当成「活动 worktree」:文件树被静默重定根到该子目录(0.25.0 / 0.26.0 同样存在) #952 的两个场景)、
    tests/worktree-address.spec.ts 挂 4/5(基准相关全部);本分支 7/7 与 6/6 全绿。
  • CI 全绿(run 38592034074):
    ci 3m04s / ci-windows 8m17s / plugin-mount 3m38s。
    第一次 CI ci-windows 红,是本 PR 新加的 tests/worktree-address.spec.ts 3/5 挂(其余 205 个文件全绿):
    地址语法在所有平台都是 / 分隔,而 resolveSidebarPath 把它原样拼到 Windows 基上得到 C:\repo\src/a.ts 的混合拼写 ——
    文件树 / git 面板 / 一切 join 来源拼的是 C:\repo\src\a.ts,而编辑器的 tab id 与 dedupeKey 都是路径派生的,
    两种拼写就是两个页签身份。已修在 commit 4(并补了一条平台无关的断言钉住它)。
  • 本地门禁:typecheck / lint 干净;pnpm test 2465 passed(唯一失败是 smoke.spec.ts:939 往 $HOME 写被沙箱 EPERM 拒,
    同一条在无关 worktree 上也红,与本 PR 无关);挂载 lane 本机 8 passed,另 2 条
    (mount.e2e.ts:689 底部终端 .xterm 60s 未挂上、tree-scroll.e2e.ts:253 的 git config not in a git directory)
    在未改动的 origin/dev 上逐条同样失败,已另记、不进本 PR。

评审指引

残留(本版不做)

worktree 会话里一条裸相对路径的聊天链接([x](docs/README.md))由宿主按 header cwd 构造并解析 → 打主检出的同名文件。宿主两端一致,插件无法在不猜的前提下区分「agent 指的是 worktree」;绝对路径修复后已无 ENOENT、也不再静默重定基。要可见性可在页签标题显示解析后的绝对路径,根因属上游 worktree 无感的聊天漏斗。

Refs #947 #950 #952(按仓库惯例,合入 dev 不自动关,等发版后关)

采纳判定原先只看「top ≠ base 且是 linked worktree」,于是容器工作区里嵌套仓库
的 worktree(bare 仓库的 main/local 检出、子项目自己的 .worktrees)也被当成
「会话搬到了别处」,explorer 被静默重定根到那个子目录,工作区其余兄弟目录全部
消失(报告者离线重放 27 个真实会话,20 个中招,最长的一次触发后 466 分钟仍生效)。

判定改为按仓库身份:`git rev-parse --git-common-dir` 相等才是同一个仓库。
- 工作区之外:任何 linked worktree 照旧采纳(PR omdsh-dev#802 的跨仓库场景不变)。
- 工作区之内:只有与会话仓库同 common dir 的才采纳 —— 同一仓库的
  `.worktrees/<branch>` 仍然跟随,嵌套仓库的 worktree 不再抢根。
- base 是文件系统根(pathKey 为空)时跳过该判定,避免整条跟随功能被关掉。
- 身份探测复用一次 spawn 与进程级缓存,`--absolute-git-dir` / `--git-common-dir`
  的相对拼写按被探测目录归一化。

tests/active-worktree.spec.ts:新增 4 例(嵌套普通仓库不采纳、嵌套 bare 仓库
不采纳、同仓库工作区内仍采纳、同仓库工作区外仍采纳);既有 3 例(跨仓库采纳、
无证据保持 cwd、真实路径探测)逐字未改。
会话地址 `dsh-resource://file/session/<sid>/<path>` 的相对路径,宿主构造端
(ui-chat 的 openFile)与解析端(api-workspace-files)都取会话存储的 header cwd;
插件自 omdsh-dev#802 起让 fs 路由与原生 tab 体跟随活动 worktree 根,于是地址里的相对拼写
被活动根再拼一次:worktree 嵌在 header cwd 之下时路径翻倍(ENOENT,聊天链接与
改动页都中招),两个检出都有同名文件时静默读到另一份(可保存写回)。

规则:构造端与消费端共用同一个基准,并在地址被读到的边界把它解析成绝对路径——
绝对路径在任何基准下都一样,下游再无可重定基的余地。

- 新增 src/client/address-base.ts 的 addressBaseOf(ctx, sessionId):会话列表里的
  header cwd(宿主地址契约的基准),取不到返回 undefined 让调用方回退。
- 消费边界:fileParamsOf(info, base?) 在切 `path:line` 规格之前把地址路径解析成
  绝对路径;native/index.ts 的 editor 类型注入
  paramsOf: (info) => fileParamsOf(info, addressBaseOf(ctx, fileSessionIdOf(info)))。
- 构造边界:native/surface.ts 的 fileAddress 与 sidebar-file.ts 的
  openClaimedNativeFile 都以 addressBaseOf 优先,调用方 cwd 只作回退——同一文件
  只有一个地址(与宿主聊天路径的地址一致,页签照旧合并)。
- 意图侧:openSidebarFile(ctx, sessionId, path, { line?, cwd? }) 的 cwd 是调用方
  手上的活根(EditorHost / 改动页 / markdown 面板传 useSessionRoot 的结果),
  相对路径按它解析,缺省才回退 header cwd——同一相对路径在各入口不再落到不同检出。
- 测试:新增 tests/worktree-address.spec.ts(基准三态 / 绝对路径不可重定基 /
  构造端基准 / 相对路径按活根解析),真 git worktree 夹具;file-open-claim 的
  `line` 位置参数随之改为 `{ line }`(唯一签名变更点)。
…dev#952)

- AGENTS §3.4 新增第 13 条:会话地址的基准是会话 header cwd,构造端与消费端都
  经 addressBaseOf 取基准、在消费边界解析成绝对路径;工作区内部的 linked worktree
  只有同一仓库(--git-common-dir 相等)才算「会话搬走了」。
- 接入指南 §10 新增同名条目(消费插件自己也解析文件地址时照同一条规则),并补上
  openSidebarFile 的新签名 `{ line?, cwd? }`。
- 新增 docs/plans/2026-10-14-worktree-root-and-address-base.md:三个症状、根因
  (宿主两端自洽 / 插件换基准 / 判定只看拓扑)、口径 A/B 决策表与实测真值表、
  逐文件实施、复现与正式用例、残留(裸相对路径聊天链接属上游 worktree 无感)。
CI 的 Windows lane 抓到:地址语法在**所有平台**都是 `/` 分隔,而
`resolveSidebarPath` 把相对路径原样拼到 Windows 基上,于是同一个文件得到
`C:\repo\src/a.ts` 这种混合拼写——文件树 / git 面板 / 一切 join 来源拼的是
`C:\repo\src\a.ts`,而编辑器的 tab id 与 dedupeKey 都是路径派生的,两种拼写
就是两个页签身份(CI 上 worktree-address.spec.ts 3/5 挂:正是三条路径相等断言)。

新增 `src/client/address-base.ts` 的 `resolveAddressPath(base, path)`:在地址边界
把结果折叠成 base 的分隔符风格(POSIX 原样保留 —— 那里 `\` 是合法文件名字符,
也是 `resolveSidebarPath` 从不在 POSIX 折叠的原因),`fileParamsOf` 与
`openSidebarFile` 都改走它。`resolveSidebarPath` 本身与其既有断言
(tests/paths.spec.ts 钉住的混合拼写)逐字未动。

用例:worktree-address.spec.ts 新增 1 条平台无关断言(`C:\repo` + `src/a.ts`
→ `C:\repo\src\a.ts`,POSIX 原样),并让既有 3 条断言在 Windows 上成立。
四处冲突,都不是「选一边」能了事的:

1) `src/active-worktree.ts` 的头注释:上游这半个月把模块重写了一遍(cwd 解析收敛:
   TTL 15s、在飞去重、三个缓存加 LRU 上界、`working-directory/change` 作为证据边界),
   本分支加的是「工作区内只有同仓库的 linked worktree 才采纳」(omdsh-dev#952)。两边都保留:
   一句讲采纳判定,一句讲证据边界。

2/3) `isLinkedWorktree` 一带:上游保留布尔缓存 + 每次探测,本分支把它换成
   `gitIdentityOf`(一次 `rev-parse` 同时给出 git dir 与 common dir,并带自己的缓存),
   由它派生出 `isLinkedWorktree` 与 `isAdoptableTop`。合并后**只留身份缓存**
   (布尔缓存成为同一趟探测的第二份副本,且负结果两边都缓存),`cachePut` 给身份缓存
   补上 LRU 上界,模块头注释的「三个缓存」正好仍是三个。

4) `AGENTS.md`:上游把 §3 整体搬进 `docs/host-contracts.md`(原 §3.4 的 1–12 条按原编号),
   本分支往旧 §3.4 追加了第 13 条(会话地址基准)。取上游的 `AGENTS.md`,把第 13 条
   按新家的体例写进 `docs/host-contracts.md` §B(编号 13 接在第 12 条后),并把四处
   「第 1–12 条」更新为「第 1–13 条」。

顺带:`tests/active-worktree.spec.ts` 的 `rmSync` 导入在上游侧被同一个文件的重排吃掉,
补回。

门禁:typecheck 干净;全量 vitest 2691 passed / 17 skipped / 1 failed(唯一红是既有的
smoke 写 `$HOME` 被本机沙箱 EPERM 拒);`active-worktree` / `worktree-address` /
`native-surface` 70 passed。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/sidebar Sidebar shell, panels, layout, tabs, workspace windows bug Something isn't working P2 中:一般功能/优化

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants