@young1lin/dsh-ui-gitworkbench 0.1.16 → 0.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  本文件记录面向使用者的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
4
4
 
5
+ ## [0.1.17] - 2026-09-10
6
+
7
+ ### 新增
8
+
9
+ - **`worktree_enter` 可以指定分支了:新增 `branch` 参数,缺省仍是名字。** 名字兼任分支的旧约定把最通行的分支拼写锁死了——`feature/foo` 是完全合法的 ref,但 `/` 永远进不了 Windows 目录名,于是 worktree 仿真建不出任何斜杠分支。参数把两件事拆开:目录仍由 name 决定,全新创建的分支用 branch(缺省回落到 name,旧调用行为分毫不变)。校验按分支的规矩走——`isRefName` 之外再拒 git 自己也会拒的三类拼写(`.lock` 结尾、首尾点、`head` 大小写碰撞),非法**直接报错**而不是像 name 那样静默换随机名:目录标签是任意的,分支名是有语义的,偷偷换掉等于把活干上错误的分支。参数只在全新创建时生效——复用已注册的 worktree 照旧保留它自己的分支(显式请求了不一致的分支时 hint 注明未采用),`-b` 撞上既有分支时照旧回退为检出该分支,这也让「目录叫 X、落在既有分支 Y」第一次成为可能。
10
+
11
+ ### 修复
12
+
13
+ - **CRLF 文件「有时候能显示、有时候不能」的两张面孔都说清楚了。** 第一张是行尾归一化幻影:仓库存 LF、工作区 CRLF(`core.autocrlf=true` 或 `eol=crlf`)时,git 的 stat 检查永远把文件列为 modified、clean 过滤又永远认为内容无差异——树里挂着 M 的文件点开只剩一行无人解释的「无文本差异」,大空面板里读起来就是显示不出来;它还有第二层:`fileDiff` 的 untracked fallback 此前不查 tracked 状态,会给幻影文件合成出 git 自己都看不见的「整文件新增」段(统一视图下整个文件都成了新增行)。现在这种文件点开是一句完整解释(行尾幻影 + 建议统一 LF,`phantomNotice` 双语),fileDiff 对 tracked 文件如实返回空。第二张是 Compare 的三点方向陷阱:`A...B` 比较的是分叉点到 B,两端选反时整个文件树为空、一个字不说;现在空结果给一行「想看另一方向的变化请交换两端」(`compareEmptyHint` 双语)。真有内容差异的 CRLF 文件(含仅行尾不同的 Compare)始终照常渲染,␍ 标记不变。活体探针 5 项断言全过(`scripts/verify_crlf_display.py`,本地)。
14
+
5
15
  ## [0.1.16] - 2026-09-04
6
16
 
7
17
  ### 新增
package/CHANGELOG_EN.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  User-facing changes, newest first. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning follows SemVer.
4
4
 
5
+ ## [0.1.17] - 2026-09-10
6
+
7
+ ### Added
8
+
9
+ - **`worktree_enter` can name the branch: a new `branch` parameter, still defaulting to the name.** The name-doubles-as-branch contract had locked out the most common branch spelling there is — `feature/foo` is a perfectly legal ref, but `/` can never appear in a Windows directory name, so worktree emulation could not create a single slash branch. The parameter splits the two: the directory still comes from `name`, a fresh worktree's branch from `branch` (falling back to `name` when absent — old callers behave identically). Validation follows branch rules: besides `isRefName` it refuses the three spellings git itself refuses (a `.lock` ending, leading/trailing dots, `head` in any case), and an illegal value is **rejected outright** rather than silently replaced the way an illegal name is — a directory label is arbitrary, a branch name is semantic, and substituting one lands work on the wrong branch. The parameter applies to a fresh create only: a reused registered worktree keeps its own branch (the hint says so when an explicit request was set aside), and an existing-branch collision still falls back to checking that branch out — which also makes "directory named X, on existing branch Y" possible for the first time.
10
+
11
+ ### Fixed
12
+
13
+ - **Both faces of "CRLF content sometimes will not display" now explain themselves.** The first is the line-ending phantom: with LF in the repository and CRLF in the working tree (`core.autocrlf=true` or `eol=crlf`), git's stat check lists the file as modified forever while its clean filter finds no content difference — clicking a file the tree shows as M produced one unexplained "No text changes" line that reads as a blank, broken pane. It had a second layer: `fileDiff`'s untracked fallback never checked tracked status, so it synthesized a whole-file addition git itself does not see (which the unified view then painted as every line added). Such a file now opens to a full explanation (the phantom, plus the advice to normalise to LF — `phantomNotice`, both languages), and `fileDiff` returns an honest empty for tracked files. The second face is Compare's three-dot direction trap: `A...B` diffs from the fork point up to B, so picking the ends the other way round empties the whole file tree without a word; an empty result now gets one line suggesting to swap the two ends (`compareEmptyHint`, both languages). CRLF files with real differences — including a Compare that differs in endings only — render as before, ␍ marks unchanged. All five live-probe assertions pass (`scripts/verify_crlf_display.py`, local).
14
+
5
15
  ## [0.1.16] - 2026-09-04
6
16
 
7
17
  ### Added
package/README.md CHANGED
@@ -169,7 +169,7 @@ export function apply(ctx) {
169
169
  ### 2.4 worktree 仿真(`src/worktree.ts` 纯逻辑 + `src/index.ts` 里的 RPC/工具)
170
170
 
171
171
  - **宿主 RPC**(同一 `GitWorkbenchService` 上多挂 4 个 `@Remote`,参数照 §6.8 裸标识符、signal 最后):
172
- - `worktreeEnter(sessionId, repoPath, name, signal)`——`repoRootOf` 解析仓库根;在 `<repoRoot>/.agents/worktrees/<name>` 创建(或复用)worktree、**分支 = 名字本身**(不加强制前缀),写绑定;返回 `{ok, worktreePath, branch, hint}`,hint 教模型怎么用相对路径(会话 cwd 不可变)。复用判定走 **realpath**:目标目录已是注册 worktree(别的工具建的、或经 Junction 映射进来的,git 登记的是另一种拼写)→ 直接绑定并保留**它自己的分支**,不再 `worktree add`。
172
+ - `worktreeEnter(sessionId, repoPath, name, branchName, signal)`——`repoRootOf` 解析仓库根;在 `<repoRoot>/.agents/worktrees/<name>` 创建(或复用)worktree、**分支 = branchName ?? 名字**(都不加强制前缀),写绑定;返回 `{ok, worktreePath, branch, hint}`,hint 教模型怎么用相对路径(会话 cwd 不可变)。`branchName` 是给斜杠分支留的口子:`feature/foo` 是合法 ref、却是 Windows 目录名拼不出的拼写,名字兼任分支时这类最通行的分支永远建不出来。它的校验按分支的规矩走(`isRefName` 加上 git 自己也会拒的 `.lock` 结尾、首尾点、`head` 大小写碰撞),**非法直接拒绝、绝不静默换名**——目录标签可以随机生成,分支名有语义;且只在全新创建时生效。复用判定走 **realpath**:目标目录已是注册 worktree(别的工具建的、或经 Junction 映射进来的,git 登记的是另一种拼写)→ 直接绑定并保留**它自己的分支**(显式传了不一致的 branchName 时 hint 注明未采用),不再 `worktree add`。
173
173
  - `worktreeExit(sessionId, remove, signal)`——解绑;`remove:true` 且树干净才 `git worktree remove`,脏树拒绝。
174
174
  - `worktreeStatus(sessionId, repoPath, signal)`——**有效绑定**(自有优先,否则沿谱系借最近绑定祖先;`bindingInherited` 标明是否借来)+ 仓库全部 worktree 列表。
175
175
  - `sessionWorktree(sessionId, signal)`——`{worktreePath, name, inherited}`,只读绑定 JSON、零 git spawn;无自有绑定时借最近绑定祖先(`inherited:true`),连祖先也无绑定才是双 `null`。客户端轮询已改用 `worktreeStatus`(绑定+列表一次拿全),这个 RPC 保留作轻量单查。
@@ -363,7 +363,7 @@ window.__ModuleLoader__.load({ id: "@young1lin/dsh-ui-gitworkbench", factory: (r
363
363
  - **输出 schema 必须容纳所有早退返回形状**:`worktree_status` 的无会话早退 `{ok:false, error}` 与正常 `{ok, binding, worktrees}` 共用一个 schema,所以 `ok`/`error` 声明为可选、`binding` 用 oneOf——否则真实调用时校验失败。
364
364
 
365
365
  ### 6.12 worktree 的 Windows 细节
366
- - **`git worktree remove` 保留分支**(exit 从不删 `<name>`——可能有未合并提交)。之后再 enter:`worktree add -b <name> <dir>` 会因分支已存在而失败 → 先 `rev-parse --verify --quiet refs/heads/<name>` 探测,幸存则改用 `worktree add <dir> <name>` **检出既有分支**(hint 注明 reused,提醒模型里面有旧提交)。
366
+ - **`git worktree remove` 保留分支**(exit 从不删 `<name>`——可能有未合并提交)。之后再 enter:`worktree add -b <branch> <dir>`(`<branch>` = branchName ?? 名字)会因分支已存在而失败 → 先 `rev-parse --verify --quiet refs/heads/<branch>` 探测,幸存则改用 `worktree add <dir> <branch>` **检出既有分支**(hint 注明 reused,提醒模型里面有旧提交)——这也是「目录叫 X、落在既有分支 Y」的通路。
367
367
  - **绑定文件的 rename 在 Windows 可能 EPERM**:页面 15s 轮询短暂持有读句柄/杀毒扫描,rename 撞上就 EPERM。做法:tmp + rename,EPERM 按 25/50/100/200/400ms 退避重试后再抛(见 `src/worktree.ts` 的 `saveBindings`)。
368
368
  - **路径一律正斜杠规范化**:`rev-parse --show-toplevel` 的输出、porcelain 的 worktree path 都要做 `.replace(/\\/g,'/')` 再比对——宿主在 Windows 返回反斜杠,两边不统一就匹配不上(复用判定会失灵)。
369
369
 
@@ -482,6 +482,30 @@ git 的两种「路径」只在仓库根相等:`git status --porcelain` 和 `g
482
482
  `tests/host-rooted-paths.test.ts` 源码扫描钉布线(先剥注释;断言带 path 的 @Remote
483
483
  恰好十个、每个方法体内必须出现 `rootedDirOf`;已做变异测试,改掉一个方法它会点名)。
484
484
 
485
+ ### 6.23 「永远 modified」的 CRLF 幻影:status 列着 M、diff 永远为空
486
+
487
+ 仓库字节 + `core.autocrlf=true`(或 `eol=crlf` 属性)的组合下,git 的 stat 检查与
488
+ clean 过滤对同一文件给出**相反答案**:`git status` 永远报 modified(smudge 方向认为
489
+ 重新检出会不一样),`git diff` / `--numstat` 永远为空(clean 方向认为内容一致)。
490
+ 实测两种形态稳定复现(LF 入库 + autocrlf=true + CRLF 工作区;`v.txt eol=crlf` 属性 +
491
+ LF 入库 + CRLF 工作区),touch 失效 stat 缓存后反复 status **不会**自愈。注意反例:
492
+ CRLF 字节**入库**(autocrlf 翻转之前提交的)反而报干净——不是「有 CRLF 就有幻影」,
493
+ 条件是「入库字节经 clean 后与工作区一致、经 smudge 后与工作区不一致」。
494
+
495
+ 抽屉此前把它显示成一行无人解释的「无文本差异」——树里挂着 M、点开却什么都不说,
496
+ 读起来就是显示坏了;unified 视图更糟:`fileDiff` 的 untracked fallback **不查 tracked
497
+ 状态**,会给幻影文件合成出 git 自己都看不见的「整文件新增」段。修复三层:
498
+ 空 diff + 树行状态为 modified(`isPhantomModified`,diff-model.ts——fully-staged 文件
499
+ 的 unstaged 层也空,但整文件 HEAD-diff 非空,所以判据必须用**整文件**段而不是当前层)
500
+ → 面板解释这是行尾归一化幻影并建议统一 LF(locale `phantomNotice`;cr-visible 的
501
+ LF 建议守卫计数 2→3);`fileDiff` 合成前先过 `isUntracked`;Compare 的另一半是
502
+ **三点语义方向**——`A...B` 比较分叉点到 B,两端选反时**整个文件树为空**(RPC 的
503
+ files/numstat 全 0),此前一个字不说,现在给一行「想看另一方向请交换两端」
504
+ (`compareEmptyHint`)。守卫:`tests/phantom-notice.test.ts`(判定)、
505
+ `tests/crlf-pipeline.test.ts`(CRLF 行在解析与两遍着色中逐行自对齐——排查时管线已
506
+ 被排除,钉住它继续被排除);活体验证 `scripts/verify_crlf_display.py`(本地,5 项断言:
507
+ 幻影提示出现、fileDiff 空返回、真差异照常渲染、方向提示出现、仅行尾对比带 ␍ 渲染)。
508
+
485
509
  ---
486
510
 
487
511
  ## 7. dsh 仓库里的关键参考文件(去哪里抄)
package/README_EN.md CHANGED
@@ -13,7 +13,7 @@ Every session header gets a small status card showing the current branch, ahead/
13
13
  - **Commit & sync** — ticking a file in the tree is a real `git add` / `git restore --staged`; with the commit box and the fetch / pull / push bar, a full commit-and-push never leaves the panel.
14
14
  - **Appearance** — seven theme families in light and dark (following the OS by default), plus a blurred background image and custom CSS, stored per project and globally with the project scope winning.
15
15
 
16
- It also ships **worktree emulation**: the model can call the `worktree_enter` / `worktree_exit` / `worktree_status` agent tools to create or leave an isolated worktree under `.agents/worktrees/<name>` and bind the session to it. **A subagent session never writes a binding of its own — it borrows the nearest bound ancestor's worktree along its lineage**: the standing prompt, the chip, and `worktree_status` all resolve that effective binding for a session without one, and the borrow disappears when the parent exits. The status card lights up its binding marker, the panel header gains a worktree switcher listing every worktree in the repository, and the stats follow the binding.
16
+ It also ships **worktree emulation**: the model can call the `worktree_enter` / `worktree_exit` / `worktree_status` agent tools to create or leave an isolated worktree under `.agents/worktrees/<name>` and bind the session to it. The name doubles as the branch by default; pass `branch` to choose another — unlike the name it may contain slashes (`feature/foo`), the spelling a Windows directory name can never hold. **A subagent session never writes a binding of its own — it borrows the nearest bound ancestor's worktree along its lineage**: the standing prompt, the chip, and `worktree_status` all resolve that effective binding for a session without one, and the borrow disappears when the parent exits. The status card lights up its binding marker, the panel header gains a worktree switcher listing every worktree in the repository, and the stats follow the binding.
17
17
 
18
18
  <div align="center">
19
19
  <video src="https://github.com/user-attachments/assets/c6a73c7b-bf69-4b97-80a2-9175bc293d7d" muted autoplay loop playsinline controls width="100%"></video>