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:
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+237
-10
@@ -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:
|
||||
|
||||
@@ -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
@@ -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') {
|
||||
|
||||
@@ -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 ``
|
||||
* 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 `})`
|
||||
})
|
||||
// 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)] ?? '')
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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\n```\n'
|
||||
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\n```')
|
||||
expect(out).toContain('/sidebar/file?')
|
||||
})
|
||||
|
||||
it('does not rewrite image-looking text inside inline code spans', () => {
|
||||
const md = 'Use `` syntax.\n'
|
||||
const out = rewriteLocalImageUrls(md, scope, '/repo/readme.md', ORIGIN)
|
||||
expect(out).toContain('``')
|
||||
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/)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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 })
|
||||
|
||||
Reference in New Issue
Block a user