fix: address Copilot review (9 comments)

#1 GitView: hide/disable 'open in editor' for external linked worktrees
   (path outside session workspace is rejected by the fence); add
   isWithinWorkspace client helper for the gate.
#2 DiffTab: untracked diff read resolves against diff.repoRoot (child-repo
   paths are repo-root-relative, not cwd-relative); fs.read route threads
   selectedRepoOf into resolveGitPath so the host resolves the right base.
#3 GitView: chooseRepo handler mirrors chooseWorktree — clears status/
   history/log and bumps generation before async refresh, preventing stale
   rows from acting on the newly selected repository.
#4 git.ts: repoRoots/repoRoot compare roots via pathIdentity() (platform-
   aware) instead of raw includes(), fixing win32 forward/backslash
   mismatch that silently fell back to roots[0].
#5 markdown-images: mask fenced code blocks and inline code spans before
   rewriting; only rewrite reference definitions whose label is actually
   referenced by image syntax (not plain links). +3 tests.
#6 package.json: downgrade unrun ^0.3.1 → ^0.2.39 (0.3.x requires Node
   ^22.13, incompatible with declared engines >=20).
#7 market-manifest.spec: shell:true on win32 so execFileSync resolves the
   pnpm .cmd shim.
#8 smoke.spec: build html-route URL via encodeHtmlUrl() (production
   encoder) so the Windows drive separator is correct.
#9 index.ts git.worktrees route: thread selectedRepoOf so a workspace
   container's selected child repo's linked worktrees appear.
This commit is contained in:
HuanLinOTO
2026-08-23 22:02:47 +08:00
parent 9410e5bb2a
commit 751fb954a2
12 changed files with 793 additions and 28 deletions
+408
View File
@@ -0,0 +1,408 @@
# Bug PR 一次性合并方案(merge train + 并行 subagent)
> 仓库:`omdsh-dev/DSH-better-sidebar`(本地 clone:`D:\Projects\deepseek-harness\DSH-better-sidebar`)
> 基准:main `d6d8db0` (= 0.15.2 + #339 locale ja)
> 目标:把 12 个 bug PR 一次性合入 main,**保留每个原作者的贡献归属**
> 输入:`bug-pr-review.md` 中 12 个 NOT_FIXED PR
> 日期:2026-08-23
---
## 0. 设计原则
| 原则 | 说明 |
|---|---|
| 保留作者归属 | 用 `git merge --no-ff`(**不 squash**),每个 PR 的原始 commits 全部进 main 历史;GitHub 通过 commit author email 匹配到 12 位作者 |
| 跨平台 commit trailer | #285 作者邮箱 `fix@local.invalid` 不匹配 GitHub → 在 merge commit message 加 `Co-authored-by: xxxg0001 <xxxg0001@users.noreply.github.com>` |
| 零源码 patch | train 分支所有改动都来自 PR 本身或冲突解决;**不修改任何 PR 的 commits** |
| 并行加速 | 4 个 subagent 各自 worktree 独立合并簇内 PR;跨簇冲突由主 agent 串行解决 |
| 每步验证 | 每个 subagent 完成簇内合并后跑 `pnpm typecheck`;主 agent 跨簇合并后跑 `pnpm typecheck && pnpm test` |
---
## 1. PR 清单与作者
| PR | 标题 | 作者 | author email |
|---|---|---|---|
| #345 | 限制文件 API 访问会话工作区 | wang-kaopu | 1970138194@qq.com |
| #342 | 搜索跳过 node_modules 等噪声目录 | sunrain520 | sunrain520@users.noreply.github.com |
| #341 | mermaid 抑制全局错误渲染 | duyiliu | 278950578@qq.com |
| #338 | 移除公开版 cordis peer,迁移类型基底 | Menghuan1918 | menghuan2003@outlook.com |
| #336 | tsdown 配置加载缺 unrun devDependency | MilchstraBe-Endless10 | milchstrasse.endless10@users.noreply.github.com |
| #330 | panel host 降到 DSH dynamic-plugin 层下 | ZgblKylin | 13114351+ZgblKylin@users.noreply.github.com |
| #326 | 支持选择子 git 仓库 | loeanxi | 2012065136@qq.com |
| #310 | layout push 下保 composer 在视口内 | Zzy-min | zzy19812007@gmail.com |
| #292 | markdown 预览本地/相对路径图片 | huzilin | huzilinha@gmail.com |
| #285 | linked worktrees 移植到 RC8 | xxxg0001 | fix@local.invalid ⚠ |
| #278 | 面板宿主裁剪视口、收起面板不再撑出双向滚动 | ch3n4y | ch3n4y@gmail.com |
| #254 | mobile 说明不可用侧边栏并防溢出 | SparkElf | laoyh@chinatelecom.cn |
⚠ #285 author email 不匹配 GitHub 账号,需在 merge commit 加 `Co-authored-by` trailer 补 credit。
---
## 2. 文件冲突矩阵(跨簇)
> 仅列出被 ≥2 个 PR 修改、且**跨簇**的文件。簇内冲突由 subagent 解决。
| 文件 | 涉及 PR(按簇) |
|---|---|
| `README.md` / `README_EN.md` | A: #338 #345;B: #292;C: #254 |
| `AGENTS.md` | A: #338;B: #342;C: #330 |
| `docs/external-plugin-guide.md` | A: #338 #345(簇内解决) |
| `package.json` | A: #338 #336;C: #254;D: #326 |
| `dsh.plugin.json` | A: #338;C: #254 |
| `src/index.ts` | A: #338 #345;D: #285 #326 |
| `src/client/sidebar.module.css` | C: #330 #278 #254(簇内解决);D: #285 |
| `src/client/Sidebar.tsx` | B: #310;C: #254 |
| `src/client/service.ts` | A: #338;C: #254 |
| `src/client/DiffTab.tsx` / `GitView.tsx` / `api.ts` / `state.ts` / `src/git.ts` | D: #285 #326(簇内解决) |
---
## 3. 分簇(簇内串行,跨簇并行)
### Cluster A — 基础设施(package.json / cordis / path security)
**分支**:`train/base`
**worktree**:`D:\Projects\deepseek-harness\dsb-train-base`
**合并顺序**:
1. `#338` (cordis peer 迁移) — 改 package.json/dsh.plugin.json/src/index.ts/service.ts/AGENTS.md/README/docs
2. `#336` (unrun devDep) — 只改 package.json
3. `#345` (path security) — 改 README/docs/src/index.ts/fs-operations/html-route/path-security 新增
**簇内冲突点**:
- #336 vs #338:package.json(#338 移除 cordis peer,#336 加 unrun devDep,行不冲突但邻近)
- #345 vs #338:README.md / README_EN.md / docs/external-plugin-guide.md / src/index.ts(#338 迁移基底,#345 加 path-security 引用,可能 src/index.ts 注册新路由撞)
### Cluster B — 独立修复
**分支**:`train/misc`
**worktree**:`D:\Projects\deepseek-harness\dsb-train-misc`
**合并顺序**:
1. `#341` (mermaid suppressErrorRendering) — 改 src/client/mermaid.tsx + 测试
2. `#310` (layout-push composer clip) — 改 Sidebar.tsx + layout-push.ts + breakpoints.ts + layout.css + 测试
3. `#342` (fs-search skip dirs) — 改 fs-search.ts + AGENTS.md + 测试
4. `#292` (markdown images) — 改 TextEditor.tsx + markdown-images.ts + README + 测试
**簇内冲突点**:基本无(彼此文件不重叠)。#342 的 AGENTS.md 和 #292 的 README 是跨簇冲突,本 worktree 内基于 main,无冲突。
### Cluster C — CSS 簇(sidebar.module.css)
**分支**:`train/css`
**worktree**:`D:\Projects\deepseek-harness\dsb-train-css`
**合并顺序**:
1. `#330` (z-index drop) — 改 sidebar.module.css:35 + AGENTS.md + docs/plans + e2e 测试
2. `#278` (overflow hidden + panelHidden transform) — 改 sidebar.module.css + e2e 测试
3. `#254` (mobile no-session feedback + overflow) — 改 Sidebar.tsx + sidebar.module.css + README + dsh.plugin.json + package.json + service.ts
**簇内冲突点**:
- #278 vs #330:sidebar.module.css(#330 改 z-index 行,#278 改 overflow/transform 行,可能行邻近但不直接冲突)
- #254 vs #330/#278:sidebar.module.css(mobile media query 区域,可能撞 e2e 测试)
### Cluster D — git 簇
**分支**:`train/git`
**worktree**:`D:\Projects\deepseek-harness\dsb-train-git`
**合并顺序**:
1. `#285` (worktree RC8 forward-port) — 改 DiffTab/GitView/api/state/git.ts/index.ts/builtins/tabs.tsx/locales.ts/sidebar.module.css + 测试
2. `#326` (multi-repo selector) — 改 DiffTab/GitView/api/state/git.ts/index.ts/package.json + 截图 + 测试
**簇内冲突点**(严重):
- #326 vs #285:5 个核心文件全撞(DiffTab/GitView/api/state/git.ts)+ src/index.ts
- #326 应建立在 #285 的 worktree 支持基础上(多仓选择是 worktree list 的扩展)
- 合并 #326 时需要手工缝合:保留 #285 的 worktree list 逻辑 + #326 的子仓选择 UI
---
## 4. 执行流程
### Phase 0 — 准备(主 agent,串行)
```powershell
$repo = "D:\Projects\deepseek-harness\DSH-better-sidebar"
# 0.1 拉所有 PR 分支到本地 ref(避免 subagent 重复 fetch)
$prs = 345,342,341,338,336,330,326,310,292,285,278,254
foreach ($n in $prs) {
git -C $repo fetch origin "pull/$n/head:pr-$n"
}
# 0.2 创建 4 个 worktree(每个独立目录,独立分支基于 origin/main)
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-base" -b train/base origin/main
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-misc" -b train/misc origin/main
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-css" -b train/css origin/main
git -C $repo worktree add "D:\Projects\deepseek-harness\dsb-train-git" -b train/git origin/main
# 0.3 每个 worktree 都 pnpm install(独立 node_modules,避免锁文件竞争)
foreach ($wt in @("dsb-train-base","dsb-train-misc","dsb-train-css","dsb-train-git")) {
Push-Location "D:\Projects\deepseek-harness\$wt"
pnpm install --prefer-offline
Pop-Location
}
```
### Phase 1 — 簇内并行合并(4 个 subagent 同时跑)
每个 subagent 接收:
- 自己的 worktree 路径
- 簇内 PR 编号和合并顺序
- PR 的预期改动文件清单(见 §3)
- 验证命令:`pnpm typecheck`(必跑)+ `pnpm test`(如簇内有相互依赖的测试)
#### Subagent A(base)
```
worktree: D:\Projects\deepseek-harness\dsb-train-base
合并顺序: #338 → #336 → #345
命令:
git merge --no-ff pr-338 -m "merge #338: 移除公开版 cordis peer,迁移类型基底到 @deepseek-ai/cordis"
# 解决冲突(README/docs/package.json/src/index.ts)
pnpm typecheck
git merge --no-ff pr-336 -m "merge #336: add unrun devDependency for tsdown config loading"
# 解决 package.json 冲突
pnpm typecheck
git merge --no-ff pr-345 -m "merge #345: 限制文件 API 访问会话工作区"
# 解决 README/docs/src/index.ts 冲突
pnpm typecheck
完成后输出:
- train/base 的 HEAD SHA
- 解决的冲突文件清单
- 跨簇预期冲突点(README.md / package.json / src/index.ts / docs/external-plugin-guide.md / AGENTS.md / dsh.plugin.json / src/client/service.ts)
```
#### Subagent B(misc)
```
worktree: D:\Projects\deepseek-harness\dsb-train-misc
合并顺序: #341 → #310 → #342 → #292
命令:
git merge --no-ff pr-341 -m "merge #341: fix(mermaid): suppress global error rendering"
pnpm typecheck
git merge --no-ff pr-310 -m "merge #310: keep conversation composer in viewport under layout push"
pnpm typecheck
git merge --no-ff pr-342 -m "merge #342: fs-search skip node_modules and other noise dirs"
# AGENTS.md 可能无冲突(基于 main,A 簇的 AGENTS.md 改动在另一个 worktree)
pnpm typecheck
git merge --no-ff pr-292 -m "merge #292: markdown preview local/relative images"
pnpm typecheck
完成后输出:
- train/misc 的 HEAD SHA
- 跨簇预期冲突点(AGENTS.md 来自 #342;README.md 来自 #292;Sidebar.tsx 来自 #310)
```
#### Subagent C(css)
```
worktree: D:\Projects\deepseek-harness\dsb-train-css
合并顺序: #330 → #278 → #254
命令:
git merge --no-ff pr-330 -m "merge #330: drop panel host below DSH cordis dynamic-plugin layer"
pnpm typecheck
git merge --no-ff pr-278 -m "merge #278: panel host clip viewport, hidden panel no longer causes双向 overflow"
# sidebar.module.css 可能冲突
pnpm typecheck
git merge --no-ff pr-254 -m "merge #254: mobile explain unavailable sidebar and prevent overflow"
# sidebar.module.css 可能冲突;其他文件基于 main 无冲突
pnpm typecheck
完成后输出:
- train/css 的 HEAD SHA
- 跨簇预期冲突点(sidebar.module.css 来自 #285;README/package.json/dsh.plugin.json/Sidebar.tsx/service.ts 来自 #254)
```
#### Subagent D(git)
```
worktree: D:\Projects\deepseek-harness\dsb-train-git
合并顺序: #285 → #326
命令:
git merge --no-ff pr-285 -m "merge #285: forward-port linked worktrees to RC8
Co-authored-by: xxxg0001 <xxxg0001@users.noreply.github.com>"
# 注意:#285 author email = fix@local.invalid,必须加 Co-authored-by trailer
pnpm typecheck
git merge --no-ff pr-326 -m "merge #326: support selecting child git repositories"
# 严重冲突:DiffTab/GitView/api/state/git.ts/src/index.ts
# 缝合策略:保留 #285 的 worktree list 逻辑 + #326 的子仓选择 UI
pnpm typecheck
完成后输出:
- train/git 的 HEAD SHA
- 跨簇预期冲突点(sidebar.module.css 来自 #285;src/index.ts 与 base 簇撞;package.json 与 base 簇撞)
- #326 缝合的关键决策(哪些函数来自 #285,哪些来自 #326)
```
### Phase 2 — 跨簇串行合并(主 agent)
```powershell
$repo = "D:\Projects\deepseek-harness\DSH-better-sidebar"
# 切回主 worktree
# 2.1 创建 mega-train 分支
git -C $repo checkout -b merge/bug-pr-train origin/main
# 2.2 merge train/base(应该 ff,因为 base 直接基于 main)
git -C $repo merge --no-ff train/base -m "integrate train/base: cordis peer migration + unrun devDep + path security"
# 2.3 merge train/css
git -C $repo merge --no-ff train/css -m "integrate train/css: panel z-index + overflow clip + mobile sidebar"
# 解决冲突:
# - sidebar.module.css(train/base 没改,应该无冲突)
# - README.md(train/base 改了,train/css 的 #254 也改了 → 冲突)
# - package.json(train/base 加了 unrun devDep,train/css 的 #254 可能加 mobile deps → 冲突)
# - dsh.plugin.json(train/base 改了,train/css 的 #254 也改了 → 冲突)
# - src/client/service.ts(train/base 迁移了 cordis,train/css 的 #254 可能改了 mobile 检测 → 冲突)
pnpm install # 合并 package.json 后必须重装
pnpm typecheck
# 2.4 merge train/git
git -C $repo merge --no-ff train/git -m "integrate train/git: worktree RC8 forward-port + multi-repo selector"
# 解决冲突:
# - src/index.ts(train/base 改了 cordis 迁移,train/git 改了 worktree 注册 → 冲突)
# - src/client/sidebar.module.css(train/css 改了 z-index/overflow/mobile,train/git 的 #285 可能改了 git 面板样式 → 冲突)
# - package.json(train/git 的 #326 可能加了依赖 → 冲突)
pnpm install
pnpm typecheck
# 2.5 merge train/misc
git -C $repo merge --no-ff train/misc -m "integrate train/misc: mermaid + layout-push + fs-search + markdown-images"
# 解决冲突:
# - AGENTS.md(train/base 改了,train/misc 的 #342 也改了 → 冲突)
# - README.md(train/base + train/css 都改了,train/misc 的 #292 又改 → 三方合并冲突)
# - src/client/Sidebar.tsx(train/misc 的 #310 改了 layout,train/css 的 #254 改了 mobile → 冲突)
pnpm typecheck
# 2.6 全量验证
pnpm typecheck
pnpm test
```
### Phase 3 — 推送与开 PR(主 agent)
```powershell
# 3.1 推 mega-train 到 fork(不是 origin,因为 origin 是 omdsh-dev 上游)
git -C $repo push fork merge/bug-pr-train
# 3.2 开 mega-PR(base = main,head = HuanLinOTO:merge/bug-pr-train)
gh pr create `
--repo omdsh-dev/DSH-better-sidebar `
--base main `
--head HuanLinOTO:merge/bug-pr-train `
--title "merge: bug PR train (12 PRs, --no-ff to preserve authors)" `
--body "Closes #345, #342, #341, #338, #336, #330, #326, #310, #292, #285, #278, #254
一次性合并 12 个 bug PR,使用 --no-ff 保留每个 PR 的原始 commits 和作者归属。
合并顺序与分簇:
- Cluster A (base): #338 → #336 → #345
- Cluster B (misc): #341 → #310 → #342 → #292
- Cluster C (css): #330 → #278 → #254
- Cluster D (git): #285 → #326
并行 subagent 在 4 个独立 worktree 中合并簇内 PR,主 agent 串行合并 4 个 train 子分支。
#285 author email 不匹配 GitHub,merge commit 加了 Co-authored-by: xxxg0001 trailer。
详细方案:docs/plans/2026-08-23-bug-pr-merge-train.md"
# 3.3 PR 创建后,13 个 PR(12 个原始 + 1 个 mega)会显示在 PR 列表
# 等 CI 通过后用 --merge 合并(不要 squash):
gh pr merge <mega-PR-number> --repo omdsh-dev/DSH-better-sidebar --merge
# 合并后 12 个原始 PR 因 Closes 关键字自动 close
```
---
## 5. 验证清单
### Phase 1 完成后(每个 subagent 自查)
- [ ] 簇内所有 PR 已 `--no-ff` 合并
- [ ] `pnpm typecheck` 通过
- [ ] git log 显示每个 PR 的原始 commits 保留
- [ ] 输出 train 子分支的 HEAD SHA 给主 agent
- [ ] 输出解决的冲突文件清单
- [ ] 输出跨簇预期冲突点
### Phase 2 完成后(主 agent 自查)
- [ ] 4 个 train 子分支全部并入 `merge/bug-pr-train`
- [ ] `pnpm install` 成功
- [ ] `pnpm typecheck` 通过
- [ ] `pnpm test` 通过(win32 平台已知失败项可记录,不算回归)
- [ ] `git log --oneline d6d8db0..HEAD` 显示 12 个 merge commit + 各 PR 的原始 commits
### Phase 3 完成后
- [ ] mega-PR 在 omdsh-dev/DSH-better-sidebar 创建成功
- [ ] PR body 包含 12 个 `Closes #N`
- [ ] CI 通过(或失败项是已知非回归)
- [ ] 用 `--merge` 合并(**不是 squash**)
- [ ] 合并后 12 个原始 PR 全部 close
- [ ] main 上 `git log --author=<每位作者>` 都能查到 commits
---
## 6. 风险点与应对
| 风险 | 概率 | 应对 |
|---|---|---|
| Phase 1 某个 subagent 簇内冲突解决失败 | 中 | 主 agent 介入协助;最坏退回该簇重做 |
| Phase 2 跨簇冲突比预期复杂(特别是 README 三方合并) | 高 | 手工缝合;以 #338 的 cordis 迁移为基底,叠加其他 PR 的章节 |
| Phase 2 #326 缝合 #285 后逻辑不一致(worktree list + 子仓选择) | 中 | 优先保 #285 的 RC8 forward-port 完整性;#326 的 UI 适配其 API |
| `pnpm test` 在 win32 平台已知失败 | 中 | 记录为已知非回归,不阻塞合并(参考 AGENTS.md "上游测试可能有 win32 平台假设失败") |
| GitHub SSL 抖动(fetch 失败) | 低 | 重试 / 用 `gh pr diff` 替代 fetch / 走 fork remote |
| `--merge` 合并 mega-PR 后 contributors 列表未立即更新 | 低 | GitHub 异步计算 contributors,等 24h;只要 commit author email 匹配即可 |
| #285 的 `fix@local.invalid` 邮箱即使加 Co-authored-by 也不被 GitHub 识别 | 中 | 已用 noreply 邮箱作为 Co-authored-by,GitHub 会识别;最坏在 PR body 注明 |
---
## 7. 时间预估
| 阶段 | 串行耗时 | 并行耗时 |
|---|---|---|
| Phase 0 准备 | 5 min | 5 min |
| Phase 1 簇内合并 | 60 min | **20 min**(4 个 subagent 并行,取最慢的 git 簇) |
| Phase 2 跨簇合并 | 30 min | 30 min(必须串行) |
| Phase 3 推送 + PR | 5 min | 5 min |
| **总计** | 100 min | **60 min** |
并行加速比 ≈ 1.7×(受 Phase 2 串行合并限制)。
---
## 8. 回滚
```powershell
# 任意阶段失败,回滚到 main
git -C $repo checkout main
git -C $repo branch -D merge/bug-pr-train train/base train/misc train/css train/git
# 清理 worktree
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-base" --force
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-misc" --force
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-css" --force
git -C $repo worktree remove "D:\Projects\deepseek-harness\dsb-train-git" --force
# 删除本地 PR ref
$prs = 345,342,341,338,336,330,326,310,292,285,278,254
foreach ($n in $prs) { git -C $repo branch -D "pr-$n" }
```
如果 mega-PR 已合并到 main 但发现严重回归:
```powershell
# Revert mega-PR 的 merge commit(保留历史,不删除 PR 记录)
gh pr merge <mega-PR-number> --repo omdsh-dev/DSH-better-sidebar --revert
```
+1 -1
View File
@@ -169,7 +169,7 @@
"react-dom": "18.2.0",
"tsdown": "^0.22.2",
"typescript": "^5.6.0",
"unrun": "^0.3.1",
"unrun": "^0.2.39",
"vitest": "^4.1.8"
}
}
+237 -10
View File
@@ -182,13 +182,13 @@ importers:
version: 18.2.0(react@18.2.0)
tsdown:
specifier: ^0.22.2
version: 0.22.14(typescript@5.6.2)(unrun@0.3.1)
version: 0.22.14(typescript@5.6.2)(unrun@0.2.39)
typescript:
specifier: ^5.6.0
version: 5.6.2
unrun:
specifier: ^0.3.1
version: 0.3.1
specifier: ^0.2.39
version: 0.2.39
vitest:
specifier: ^4.1.8
version: 4.1.11(@types/node@24.13.3)(jsdom@29.1.1)(vite@8.2.2(@types/node@24.13.3))
@@ -1046,6 +1046,15 @@ packages:
'@deepseek-ai/schemastery@3.18.1':
resolution: {integrity: sha512-Qn0FCSwCQnpnj6SB31I6i2sIKgKWnkbJM8O0EU91Gv2UsYVvtZTl6IA0sCwk2e2MZf5S8w5hpq9QkeVvK9qwxg==}
'@emnapi/core@1.10.0':
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
'@emnapi/runtime@1.10.0':
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
'@emnapi/wasi-threads@1.2.1':
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
'@exodus/bytes@1.15.1':
resolution: {integrity: sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
@@ -1149,6 +1158,16 @@ packages:
'@mermaid-js/parser@1.2.1':
resolution: {integrity: sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw==}
'@napi-rs/wasm-runtime@1.2.3':
resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==}
engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0}
peerDependencies:
'@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4
'@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4
'@oxc-project/types@0.127.0':
resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==}
'@oxc-project/types@0.146.0':
resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==}
@@ -1166,36 +1185,73 @@ packages:
cpu: [arm]
os: [android]
'@rolldown/binding-android-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-android-arm64@1.2.5':
resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-arm64@1.2.5':
resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-darwin-x64@1.2.5':
resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-freebsd-x64@1.2.5':
resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm-gnueabihf@1.2.5':
resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-gnu@1.2.5':
resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1203,6 +1259,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-arm64-musl@1.2.5':
resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1210,6 +1273,13 @@ packages:
os: [linux]
libc: [musl]
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [ppc64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-ppc64-gnu@1.2.5':
resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1217,6 +1287,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-s390x-gnu@1.2.5':
resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1224,6 +1301,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-gnu@1.2.5':
resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1231,6 +1315,13 @@ packages:
os: [linux]
libc: [glibc]
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
'@rolldown/binding-linux-x64-musl@1.2.5':
resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -1238,24 +1329,50 @@ packages:
os: [linux]
libc: [musl]
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-openharmony-arm64@1.2.5':
resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [openharmony]
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [wasm32]
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-arm64-msvc@1.2.5':
resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/binding-win32-x64-msvc@1.2.5':
resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
'@rolldown/pluginutils@1.0.0-rc.17':
resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==}
'@rolldown/pluginutils@1.0.1':
resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==}
@@ -1293,6 +1410,9 @@ packages:
'@standard-schema/spec@1.1.0':
resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@tybys/wasm-util@0.10.3':
resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==}
'@types/chai@5.2.3':
resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==}
@@ -2389,6 +2509,11 @@ packages:
vue-tsc:
optional: true
rolldown@1.0.0-rc.17:
resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
rolldown@1.2.5:
resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2562,9 +2687,9 @@ packages:
unist-util-visit@5.1.0:
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
unrun@0.3.1:
resolution: {integrity: sha512-onIck/oNnCaytwths1ZVp1LK2Gq2hPoyFhiHebObuUXqR3S0uHuLLaBK8K6mRRgV7Ptip8AnNvaUsgzwWwBZuA==}
engines: {node: ^22.13.0 || >=24.0.0}
unrun@0.2.39:
resolution: {integrity: sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==}
engines: {node: '>=20.19.0'}
hasBin: true
peerDependencies:
synckit: ^0.11.11
@@ -3720,6 +3845,22 @@ snapshots:
'@deepseek-ai/cosmokit': 1.8.2
'@standard-schema/spec': 1.1.0
'@emnapi/core@1.10.0':
dependencies:
'@emnapi/wasi-threads': 1.2.1
tslib: 2.8.1
optional: true
'@emnapi/runtime@1.10.0':
dependencies:
tslib: 2.8.1
optional: true
'@emnapi/wasi-threads@1.2.1':
dependencies:
tslib: 2.8.1
optional: true
'@exodus/bytes@1.15.1': {}
'@huanlin/dsh-plugin-better-locale@0.1.0(cb831dcffe1fb561624a57e14d05c62c)':
@@ -3838,6 +3979,15 @@ snapshots:
dependencies:
'@chevrotain/types': 11.1.2
'@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@tybys/wasm-util': 0.10.3
optional: true
'@oxc-project/types@0.127.0': {}
'@oxc-project/types@0.146.0': {}
'@playwright/test@1.62.1':
@@ -3851,48 +4001,99 @@ snapshots:
'@rolldown/binding-android-arm-eabi@1.2.5':
optional: true
'@rolldown/binding-android-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-android-arm64@1.2.5':
optional: true
'@rolldown/binding-darwin-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-darwin-arm64@1.2.5':
optional: true
'@rolldown/binding-darwin-x64@1.0.0-rc.17':
optional: true
'@rolldown/binding-darwin-x64@1.2.5':
optional: true
'@rolldown/binding-freebsd-x64@1.0.0-rc.17':
optional: true
'@rolldown/binding-freebsd-x64@1.2.5':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm-gnueabihf@1.2.5':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm64-gnu@1.2.5':
optional: true
'@rolldown/binding-linux-arm64-musl@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-arm64-musl@1.2.5':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-ppc64-gnu@1.2.5':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-s390x-gnu@1.2.5':
optional: true
'@rolldown/binding-linux-x64-gnu@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-x64-gnu@1.2.5':
optional: true
'@rolldown/binding-linux-x64-musl@1.0.0-rc.17':
optional: true
'@rolldown/binding-linux-x64-musl@1.2.5':
optional: true
'@rolldown/binding-openharmony-arm64@1.0.0-rc.17':
optional: true
'@rolldown/binding-openharmony-arm64@1.2.5':
optional: true
'@rolldown/binding-wasm32-wasi@1.0.0-rc.17':
dependencies:
'@emnapi/core': 1.10.0
'@emnapi/runtime': 1.10.0
'@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
optional: true
'@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17':
optional: true
'@rolldown/binding-win32-arm64-msvc@1.2.5':
optional: true
'@rolldown/binding-win32-x64-msvc@1.0.0-rc.17':
optional: true
'@rolldown/binding-win32-x64-msvc@1.2.5':
optional: true
'@rolldown/pluginutils@1.0.0-rc.17': {}
'@rolldown/pluginutils@1.0.1': {}
'@shikijs/core@4.4.3':
@@ -3937,6 +4138,11 @@ snapshots:
'@standard-schema/spec@1.1.0': {}
'@tybys/wasm-util@0.10.3':
dependencies:
tslib: 2.8.1
optional: true
'@types/chai@5.2.3':
dependencies:
'@types/deep-eql': 4.0.2
@@ -5174,6 +5380,27 @@ snapshots:
transitivePeerDependencies:
- oxc-resolver
rolldown@1.0.0-rc.17:
dependencies:
'@oxc-project/types': 0.127.0
'@rolldown/pluginutils': 1.0.0-rc.17
optionalDependencies:
'@rolldown/binding-android-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-arm64': 1.0.0-rc.17
'@rolldown/binding-darwin-x64': 1.0.0-rc.17
'@rolldown/binding-freebsd-x64': 1.0.0-rc.17
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17
'@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17
'@rolldown/binding-linux-x64-musl': 1.0.0-rc.17
'@rolldown/binding-openharmony-arm64': 1.0.0-rc.17
'@rolldown/binding-wasm32-wasi': 1.0.0-rc.17
'@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17
'@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17
rolldown@1.2.5:
dependencies:
'@oxc-project/types': 0.146.0
@@ -5288,7 +5515,7 @@ snapshots:
ts-dedent@2.3.0: {}
tsdown@0.22.14(typescript@5.6.2)(unrun@0.3.1):
tsdown@0.22.14(typescript@5.6.2)(unrun@0.2.39):
dependencies:
ansis: 4.3.1
cac: 7.0.0
@@ -5307,7 +5534,7 @@ snapshots:
verkit: 0.3.2
optionalDependencies:
typescript: 5.6.2
unrun: 0.3.1
unrun: 0.2.39
transitivePeerDependencies:
- '@typescript/native-preview'
- '@volar/typescript'
@@ -5355,9 +5582,9 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
unrun@0.3.1:
unrun@0.2.39:
dependencies:
rolldown: 1.2.5
rolldown: 1.0.0-rc.17
use-sync-external-store@1.2.0(react@18.2.0):
dependencies:
+4 -1
View File
@@ -61,7 +61,10 @@ export function DiffTab(props: { sessionId: string; cwd: string | undefined; dif
// Empty diff: an untracked file (git diff never lists it) falls back
// to a full-file addition; anything else is a genuine no-text-change.
if (diff.untracked === true && !diff.staged) {
const text = await api.fsRead(scope, resolveSidebarPath(diff.worktree ?? cwd, diff.path))
// A child-repo path is relative to diff.repoRoot, not the session
// cwd or the linked-worktree root; resolve against whichever the
// diff ref carries so the untracked fallback reads the right file.
const text = await api.fsRead(scope, resolveSidebarPath(diff.repoRoot ?? diff.worktree ?? cwd, diff.path))
if (!cancelled) {
setData(text.kind === 'text' ? { diff: '', untracked: text.content } : { diff: '' })
}
+33 -4
View File
@@ -16,7 +16,7 @@ import {
} from '@deepseek-ai/dsh-client-ui-primitives'
import type { GitLogEntry, GitStatusEntry, GitStatusResult, GitWorktree, SessionScope } from './api.ts'
import { api } from './api.ts'
import { relativeTo } from './paths.ts'
import { isWithinWorkspace, relativeTo } from './paths.ts'
import { resolveSidebarPath } from './produced-files.ts'
import { relativeTime, t } from './locales.ts'
import type { SidebarTab } from './state.ts'
@@ -231,6 +231,23 @@ export function GitView(props: {
const generation = refreshGeneration.current += 1
void refreshTarget(target, { loading: true, generation })
}
/** Switching the selected child repository must invalidate every
* target-derived surface (status/history/log) before the asynchronous
* refresh resolves; otherwise stale rows remain actionable while their
* handlers already address the new repository. Mirrors chooseWorktree. */
const chooseRepo = (target: string): void => {
setRepoRoot(target)
setStatus(null)
setBranchNames([])
setLogEntries([])
setLogEnded(false)
setLogLoadingMore(false)
// Re-list worktrees for the selected child (a workspace container's
// own worktree list is empty); keep the current linked-checkout choice
// unless it does not belong to the new repository.
const generation = refreshGeneration.current += 1
void refreshTarget(selectedRef.current ?? '', { loading: true, generation })
}
useEffect(() => {
if (!visible) return
const timer = window.setInterval(() => { void refresh(true) }, 2_000)
@@ -420,7 +437,7 @@ export function GitView(props: {
className={css.gitBranchSelect}
value={repoRoot ?? ''}
title={repoRoot}
onChange={(event) => { setRepoRoot(event.target.value) }}
onChange={(event) => { chooseRepo(event.target.value) }}
disabled={busy}
>
{status!.repositories!.map(root => <option key={root} value={root}>{baseName(root)}</option>)}
@@ -551,7 +568,13 @@ export function GitView(props: {
open={fileMenu !== null}
onClose={() => { setFileMenu(null) }}
items={[
{ id: 'open', label: t('openEditor'), icon: <IconCodeOutline16 size={14} /> },
// A linked worktree outside the session workspace cannot be
// opened in the editor: the host's workspace fence rejects
// every path under it. Hide the action for that checkout so
// the menu does not offer a no-op that confuses the user.
...(fileMenu !== null && isWithinWorkspace(scope.cwd ?? '', resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, fileMenu.entry.path))
? [{ id: 'open', label: t('openEditor'), icon: <IconCodeOutline16 size={14} /> }]
: []),
fileMenu?.staged === true
? { id: 'stage', label: t('unstage'), icon: <IconTrashOutline16 size={14} /> }
: { id: 'stage', label: t('stage'), icon: <IconBranchOutline16 size={14} /> },
@@ -567,7 +590,13 @@ export function GitView(props: {
if (target === null) return
setFileMenu(null)
if (id === 'open') {
onOpenFile(resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, target.entry.path))
const resolved = resolveSidebarPath(repoRoot ?? selectedWorktree ?? scope.cwd, target.entry.path)
// Defense-in-depth: the menu hides this action when the
// resolved path escapes the session workspace, but a
// racing repo switch could still reach here with a path
// the host would reject. No-op in that case.
if (!isWithinWorkspace(scope.cwd ?? '', resolved)) return
onOpenFile(resolved)
return
}
if (id === 'stage') {
+38 -3
View File
@@ -50,6 +50,14 @@ function normalizeLocalPath(path: string): string {
* local paths pass through. Remote (http/https/data/mailto) and `#`-anchor
* destinations are left untouched for `MarkdownText`. Reference-style images
* (`![x][id]` + `[id]: url`) are covered by rewriting their definition lines.
*
* Code spans (`` `...` ``) and fenced code blocks (``` ```...``` ```) are
* masked before rewriting so documentation that demonstrates `![alt](./img.png)`
* is not mutated into a `/sidebar/file` URL. Reference definitions are only
* rewritten when their label is actually referenced by an image (collapsed
* `[![][id]]`, full `![alt][id]`, or shortcut `![]` referencing the next
* definition) — a plain link `[text][id]` must not have its destination
* redirected to the media route.
* @param text - The raw markdown source (inline + reference images).
* @param scope - The session scope (sessionId + cwd) for the media route.
* @param filePath - The absolute path of the opened `.md` file.
@@ -79,11 +87,38 @@ export function rewriteLocalImageUrls(
const candidate = isAbsolutePath(trimmed) ? trimmed : directory + trimmed
return mediaUrl(normalizeLocalPath(candidate))
}
const inline = text.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_match, alt, dest) => {
// Mask fenced code blocks and inline code spans so image-looking text
// inside documentation examples is never rewritten. The sentinel uses a
// character unlikely to appear in prose; the original spans are restored
// after the image rewrite.
const masks: string[] = []
const masked = text
.replace(/```[\s\S]*?```/g, (block) => { masks.push(block); return `\u0000${masks.length - 1}\u0000` })
.replace(/`[^`\n]*`/g, (span) => { masks.push(span); return `\u0000${masks.length - 1}\u0000` })
const inline = masked.replace(/!\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g, (_match, alt, dest) => {
return `![${alt}](${resolve(dest)})`
})
// Reference-style images: rewrite their `[id]: <url>` definitions.
return inline.replace(/^(\s*\[[^\]]+\]:\s*)(<[^>]+>|[^\s]+)/gm, (_match, head, dest) => {
// Collect labels referenced by image syntax (full `![alt][id]` and
// collapsed `![][id]`) so only those reference definitions are rewritten.
// A shortcut reference (`![alt]` with no `[id]`) resolves to the label
// text `alt` itself; include it too. Plain links `[text][id]` never match
// the leading `!` and are left untouched.
const imageLabels = new Set<string>()
const labelRe = /!\[([^\]]*)\](?:\[((?:[^\][]|\[[^\]]*\])*)\])?/g
let labelMatch: RegExpExecArray | null
while ((labelMatch = labelRe.exec(inline)) !== null) {
const alt = labelMatch[1] ?? ''
const ref = labelMatch[2]
imageLabels.add(ref !== undefined && ref !== '' ? ref.toLowerCase() : alt.toLowerCase())
}
const refsRewritten = inline.replace(/^(\s*\[([^\]]+)\]:\s*)(<[^>]+>|[^\s]+)/gm, (match, head: string, label: string, dest: string) => {
if (!imageLabels.has(label.toLowerCase())) return match
return `${head}${resolve(dest.replace(/^<|>$/g, ''))}`
})
return refsRewritten.replace(/\u0000(\d+)\u0000/g, (_m, index: string) => masks[Number(index)] ?? '')
}
+16
View File
@@ -45,3 +45,19 @@ export function relativeTo(cwd: string, path: string): string {
if (nPath.toLowerCase().startsWith(`${nBase.toLowerCase()}/`)) return nPath.slice(nBase.length + 1)
return path
}
/**
* Whether `target` lies under `base` (or equals it), tolerant of separator
* style and — on Windows-style drive paths — of letter case. A client-side
* mirror of the host's `isWithin` (fs-tree.ts) used to decide whether a
* git-derived path can be opened in the editor (a linked worktree outside
* the session workspace cannot: the host's workspace fence would reject it).
*/
export function isWithinWorkspace(base: string, target: string): boolean {
const norm = (value: string): string => value.replace(/[\\/]+/g, '/').replace(/\/$/, '')
const b = norm(base)
const t = norm(target)
const lb = b.toLowerCase()
const lt = t.toLowerCase()
return lt === lb || lt.startsWith(`${lb}/`)
}
+8 -2
View File
@@ -212,7 +212,7 @@ export async function repoRoots(cwd: string): Promise<string[]> {
.sort((left, right) => left.name.localeCompare(right.name))) {
try {
const root = await directRepoRoot(join(cwd, entry.name))
if (!roots.includes(root)) roots.push(root)
if (!roots.some(existing => pathIdentity(existing) === pathIdentity(root))) roots.push(root)
} catch {
// Ordinary child directory; keep discovering sibling repositories.
}
@@ -225,7 +225,13 @@ export async function repoRoots(cwd: string): Promise<string[]> {
export async function repoRoot(cwd: string, selected?: string): Promise<string> {
const roots = await repoRoots(cwd)
if (roots.length === 0) throw new GitCommandError('not a git repository', 'not-repo', 'rev-parse')
if (selected !== undefined && roots.includes(selected)) return selected
// Git for Windows may return forward-slash roots while callers pass
// backslashes (or vice-versa); compare via the platform-aware identity.
if (selected !== undefined) {
const identity = pathIdentity(selected)
const match = roots.find(root => pathIdentity(root) === identity)
if (match !== undefined) return match
}
return roots[0]!
}
+12 -4
View File
@@ -286,8 +286,11 @@ function buildApi(
'fs.read': async (payload) => {
const { cwd } = cwdOf(payload)
// Relative paths are git-derived (status/diff report repo-root-relative
// names; the untracked diff view reads the file through this route).
const path = await ensureWorkspacePath(cwd, await resolveGitPath(cwd, requireString(payload, 'path')))
// names; the untracked diff view reads the file through this route). A
// child-repo path is relative to the selected repoRoot, not the session
// cwd; thread it so the path resolves inside the authorized workspace.
const selected = selectedRepoOf(payload)
const path = await ensureWorkspacePath(cwd, await resolveGitPath(cwd, requireString(payload, 'path'), selected))
const { content, truncated, binary, size, head } = await readText(path, resolved.readLimit)
if (binary) return { kind: 'binary', size, truncated, head }
return { kind: 'text', content, truncated }
@@ -308,8 +311,13 @@ function buildApi(
return { ok: true }
},
'git.worktrees': async (payload) => {
const { cwd } = cwdOf(payload)
return git.worktrees(cwd)
const { cwd } = await gitCwdOf(payload)
const selected = selectedRepoOf(payload)
// A workspace container (no repo at cwd) has child repos; the worktree
// list belongs to the SELECTED child, not the container. Thread the
// validated repoRoot so linked checkouts of a chosen child appear.
const base = selected !== undefined ? await git.repoRoot(cwd, selected).catch(() => cwd) : cwd
return git.worktrees(base)
},
'git.status': async (payload) => {
const { cwd } = await gitCwdOf(payload)
+27
View File
@@ -70,4 +70,31 @@ describe('rewriteLocalImageUrls', () => {
expect(url).toContain('/sidebar/file?')
expect(decodeURIComponent(url)).toContain('C:\\repo\\img.png')
})
it('does not rewrite image-looking text inside fenced code blocks', () => {
const md = '```\n![alt](./img.png)\n```\n![real](./real.png)'
const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN)
// The fenced example must be preserved verbatim; only the real image is rewritten.
expect(out).toContain('```\n![alt](./img.png)\n```')
expect(out).toContain('/sidebar/file?')
})
it('does not rewrite image-looking text inside inline code spans', () => {
const md = 'Use `![alt](./img.png)` syntax.\n![real](./real.png)'
const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN)
expect(out).toContain('`![alt](./img.png)`')
expect(out).toContain('/sidebar/file?')
})
it('does not rewrite a reference definition used only by a plain link', () => {
// The `[manual][docs]` link references `[docs]: ./docs.md`, but that
// definition must NOT be redirected to /sidebar/file because it is not
// referenced by any image. Only image-referenced definitions are rewritten.
const md = '[manual][docs]\n\n![pic][pic-def]\n\n[docs]: ./docs.md\n[pic-def]: ./pic.png'
const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN)
expect(out).toContain('[docs]: ./docs.md')
expect(out).not.toMatch(/\[docs\]:.*\/sidebar\/file/)
// The image-referenced definition IS rewritten.
expect(out).toMatch(/\[pic-def\]:.*\/sidebar\/file/)
})
})
+4 -1
View File
@@ -50,7 +50,10 @@ const STABLE_SEMVER = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u
function packedManifest(): Record<string, unknown> {
const dir = mkdtempSync(join(tmpdir(), 'market-pack-'))
try {
execFileSync('pnpm', ['pack', '--pack-destination', dir], { cwd: ROOT, stdio: 'pipe' })
// On Windows `pnpm` ships as a `.cmd` shim that `execFileSync` cannot
// spawn directly (ENOENT). Run it through the shell so the same call
// works on every platform; `tar` is available on all supported OSes.
execFileSync('pnpm', ['pack', '--pack-destination', dir], { cwd: ROOT, stdio: 'pipe', shell: process.platform === 'win32' })
execFileSync('tar', ['-xzf', join(dir, `${pkg.name}-${pkg.version}.tgz`), '-C', dir, 'package/package.json'], { stdio: 'pipe' })
return JSON.parse(readFileSync(join(dir, 'package/package.json'), 'utf8')) as Record<string, unknown>
} finally {
+5 -2
View File
@@ -12,6 +12,7 @@ import { SettingsConflictError, settingsNamespace } from '@deepseek-ai/dsh-setti
import { apply, mediaTypeForPath } from '../src/index.ts'
import * as git from '../src/git.ts'
import { listDirectory } from '../src/fs-tree.ts'
import { encodeHtmlUrl } from '../src/html-route.ts'
import { defaultShell, PtyManager, type SidebarPty } from '../src/pty-manager.ts'
import type { SidebarWebRoute, SidebarWebUpgradeRoute } from '../src/context-types.ts'
@@ -636,8 +637,10 @@ describe('session cwd resolution over the API route', () => {
const media = routes.find(route => route.path === '/sidebar/file')!
const html = routes.find(route => route.path === '/sidebar/html')!
const mediaResult = await invokeGet(media, `/sidebar/file?sessionId=security&path=${encodeURIComponent(join(workspace, 'link', 'secret.png'))}`)
const htmlPathname = join(workspace, 'link', 'secret.html').replace(/\\/g, '/').split('/').map(encodeURIComponent).join('/')
const htmlResult = await invokeGet(html, `/sidebar/html/security${htmlPathname}`)
// Use the production encoder so the URL is well-formed on every
// platform (a Windows drive path needs the leading slash separator
// that a naive join-without-separator drops).
const htmlResult = await invokeGet(html, encodeHtmlUrl('security', join(workspace, 'link', 'secret.html')))
expect(mediaResult).toMatchObject({ status: 403 })
expect(JSON.parse(mediaResult.body)).toMatchObject({ ok: false, error: { code: 'forbidden' } })
expect(htmlResult).toMatchObject({ status: 403 })