Repository navigation
fix(sidebar): worktree 会话的根选择与地址基准(#947 / #950 / #952) - #956
Open
yanzhaohui1999 wants to merge 5 commits into
Open
yanzhaohui1999 wants to merge 5 commits into
yanzhaohui1999 wants to merge 5 commits into
Conversation
采纳判定原先只看「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 上成立。
This was referenced Oct 18, 2026
四处冲突,都不是「选一边」能了事的: 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。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
三条 issue 一次修掉:worktree 会话的根选择与地址基准
同一场「会话 header cwd 是主检出、agent 在 linked git worktree 里干活」下暴露出来的三条:
cannot read "…/wt/frontend/.worktrees/wt/CLAUDE.md": ENOENT根因
ui-chat用fileAddressFor(sessionId, sessions.list.byId[sid].cwd, path),解析端api-workspace-files用sessions.get(sid).header.cwd—— 同一个值,所以地址里的相对路径只在那个基准下成立。openSidebarFile用冻结的summary?.cwd,service.openTab → surface.fileAddress用调用方手上的scope.cwd(多数是活动根)→ 同一文件可能拿到两个地址(页签不合并),同一相对路径在不同入口落到不同检出。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 cwdsrc/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 全绿。ci3m04s /ci-windows8m17s /plugin-mount3m38s。第一次 CI
ci-windows红,是本 PR 新加的tests/worktree-address.spec.ts3/5 挂(其余 205 个文件全绿):地址语法在所有平台都是
/分隔,而resolveSidebarPath把它原样拼到 Windows 基上得到C:\repo\src/a.ts的混合拼写 ——文件树 / git 面板 / 一切
join来源拼的是C:\repo\src\a.ts,而编辑器的 tab id 与 dedupeKey 都是路径派生的,两种拼写就是两个页签身份。已修在 commit 4(并补了一条平台无关的断言钉住它)。
typecheck/lint干净;pnpm test2465 passed(唯一失败是smoke.spec.ts:939往$HOME写被沙箱EPERM拒,同一条在无关 worktree 上也红,与本 PR 无关);挂载 lane 本机
8 passed,另 2 条(
mount.e2e.ts:689底部终端.xterm60s 未挂上、tree-scroll.e2e.ts:253的git config not in a git directory)在未改动的
origin/dev上逐条同样失败,已另记、不进本 PR。评审指引
src/active-worktree.ts(commit 1,判定)→src/client/address-base.ts与native/index.ts的paramsOf注入(commit 2,基准)→ commit 4 的分隔符折叠;口径 A/B 的决策表与实测真值表在
docs/plans/2026-10-14-worktree-root-and-address-base.md。openSidebarFile(ctx, sessionId, path, line?)→openSidebarFile(ctx, sessionId, path, { line?, cwd? })(4 个调用点 + 1 条既有用例同步)。
「修了 A 让 B 的复现条件消失或变形」的中间态。若维护者偏好一 issue 一 PR,可按 commit 拆开:
commit 1 = [Bug] 容器工作区里嵌套仓库的 linked worktree 被当成「活动 worktree」:文件树被静默重定根到该子目录(0.25.0 / 0.26.0 同样存在) #952;commit 2 + 4 = [Bug] worktree 内文件的聊天链接路径被二次拼接(ENOENT);相对路径会静默打开另一个 worktree 的同名文件 #947 / [Bug] 客户端入口与文件路由使用不同工作区根:同一相对路径静默解析到另一检出的同名文件(worktree 场景) #950;commit 3 = 文档。
残留(本版不做)
worktree 会话里一条裸相对路径的聊天链接(
[x](docs/README.md))由宿主按 header cwd 构造并解析 → 打主检出的同名文件。宿主两端一致,插件无法在不猜的前提下区分「agent 指的是 worktree」;绝对路径修复后已无 ENOENT、也不再静默重定基。要可见性可在页签标题显示解析后的绝对路径,根因属上游 worktree 无感的聊天漏斗。Refs #947 #950 #952(按仓库惯例,合入
dev不自动关,等发版后关)