@young1lin/dsh-ui-gitworkbench 0.1.15 → 0.1.16
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 +16 -0
- package/CHANGELOG_EN.md +16 -0
- package/README.md +4 -3
- package/README_EN.md +1 -1
- package/lib/client.js +1519 -477
- package/lib/dir-listing.js +34 -0
- package/lib/fs-remove.js +5 -36
- package/lib/index.js +192 -41
- package/lib/path-lock.js +54 -0
- package/lib/worktree.js +83 -0
- package/lib/write-checked.js +1 -1
- package/package.json +1 -1
- package/src/client/ChromeGlyph.tsx +5 -0
- package/src/client/CodeEditor.tsx +19 -1
- package/src/client/DiffViews.tsx +116 -121
- package/src/client/FileBrowser.tsx +196 -23
- package/src/client/GitWorkbenchPanel.module.css +1 -0
- package/src/client/GitWorkbenchPanel.tsx +100 -12
- package/src/client/SideRails.tsx +106 -0
- package/src/client/diff-cells.tsx +147 -0
- package/src/client/diff-nav.ts +4 -1
- package/src/client/dir-tree.ts +31 -1
- package/src/client/file-rows.ts +40 -0
- package/src/client/h-rail.ts +70 -0
- package/src/client/ignored-cache.ts +193 -0
- package/src/client/index.ts +22 -3
- package/src/client/locales.ts +12 -2
- package/src/client/row-heights.ts +225 -0
- package/src/client/styles/changes.css +33 -2
- package/src/client/styles/controls.css +5 -0
- package/src/client/styles/files.css +5 -0
- package/src/client/styles/rails.css +72 -0
- package/src/client/use-row-window.ts +7 -3
- package/src/client/use-variable-row-window.ts +210 -0
- package/src/dir-listing.ts +47 -0
- package/src/fs-remove.ts +5 -36
- package/src/index.ts +217 -43
- package/src/path-lock.ts +56 -0
- package/src/types/dsh-shim.d.ts +12 -2
- package/src/worktree.ts +97 -0
- package/src/write-checked.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录面向使用者的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
|
4
4
|
|
|
5
|
+
## [0.1.16] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- **被 .gitignore 的文件能在 Files 里浏览、搜索、打开和编辑了。** `application-local.yml` 这类文件此前是文件浏览器唯一看不到的:路径全部来自 `ls-tree HEAD`(只认已跟踪)和 status(从不报告被忽略项),于是浏览器最该给你看的那个文件恰好缺席。不列它们本来是怕 `node_modules`,但 git 自己就把这件事变便宜了——`ls-files --others --ignored --exclude-standard --directory` 把每个完全被忽略的目录塌缩成一行,列表规模跟着 `.gitignore` 的覆盖面走而不是仓库大小;本仓库带 `node_modules` 实测 55 条、30ms。展开一个塌缩目录不能走 git(实测过:`--directory` 加 pathspec 仍然塌缩成目录自己那一行,去掉 `--directory` 一次吐 11,877 行),所以新增的 `ignoredDir` RPC 每次点击读一层文件系统,pnpm 式的软链包目录用一次跟随 `stat` 分类,条目数封顶并把「被截断」如实报出来。被忽略的行显示为暗色并带一句说明为什么;打开、编辑、保存复用既有的未跟踪文件通路。懒加载的子项不并入路径列表,而是进一个会淘汰、会报告自身大小的独立缓存,树从它嫁接:并回去的话每次展开都要重建一个 Set、用 `localeCompare` 重排整份路径列表、再重建整棵树——在已发布的 50,000 路径上限下是同步 118.9ms 一次;嫁接只重建被点目录那条脊,同一次展开 1.09ms,109 倍,屏幕上仍是那 1,500 行。这个缓存永远不淘汰读者正打开的目录:跟视口对着干的缓存会让效果层永远重读那个目录。失败的忽略列表现在报告错误而不是给一个空列表(空列表和「这仓库不忽略任何东西」无法区分);`ls-tree` 的退出码仍然故意不检查,没有 HEAD 的仓库理应让它失败,那里「还没有文件」才是诚实答案。
|
|
10
|
+
|
|
11
|
+
- **一个自动换行开关,管编辑器和两种差异视图。** 抽屉头部多了一个按钮,同时管 Files 编辑器、统一差异视图和左右对照,按浏览器记住,默认关——代码是按列写的,换行是针对某个长文件的选择,不是对所有文件都更好的默认值。编辑器那半便宜:CodeMirror 自己管变高行,一个 compartment 进出,不丢光标也不丢撤销栈。差异面板不是:行窗口把第 i 行放在 `i × 20px` 且一次测量都不做,而它自己写明了前提——面板写着 `white-space: pre`,永不换行。所以现在是两套高度模型、同时只活一套:关的时候原来的算术一行没动,新代码不挂监听也不分配内存;开的时候每行高度存在 Fenwick 树里,因为滚动路径上要问的三件事都得是 O(log n)——某行的起点是前缀和,y 处是哪行是找前缀,一次测量改一行。行是**测量**的,但只测在 DOM 里的那些;其余按文本估算(tab 跳到下一个制表位、全角字符算两列),所以第一帧滚动条就基本准。测量替换视口**上方**的估算会把整篇文档从读者眼下推走,于是同一个 layout pass 里把同样的差值还给滚动容器。左右对照最贵:两栏是两个独立的 grid(单个 grid 的轨道会被全文件最宽的行撑开,那样分隔条就拖不动了),而两个独立 grid 只在每行两边等高时才对齐——换行恰好破坏这一点;现在两边的格子都取较高一侧的高度做 `min-height`,被测量的盒子在格子**内部**,所以强加的高度不会反馈进测量,窗口拉宽时行还能缩回去。实测:编辑器最宽行 1165px → 1106px(窗格 1151px);654 行的差异开着换行时 DOM 里 81 行,14 次跳跃滚动逐行偏移全程 0px,最差帧间隔 0ms(同一个文件关掉换行是 182ms,那是定高路径重新着色的代价)。
|
|
12
|
+
|
|
13
|
+
- **子 Agent 会话继承父血缘的 worktree 绑定。** 子 Agent 自己从不调用 `worktree_enter`,于是它的状态卡、抽屉和常驻提示都在说「外层仓库」,而它的父会话正在一个 worktree 里干活。dsh 的压缩不会改变会话 id(在 0.1.1-rc.2 与当前源码上都验证过;日志里的 session/end-seed 标记的是恢复而不是分叉),所以要修的是**血缘**而不是 id 稳定性。新的解析顺序是:先看自己的绑定,没有就沿 `parentOf` 向上找最近一个有绑定的祖先(带环检测,8 跳封顶)。这张图由 `agent/session-start` 喂(子会话头部会写出父级),并在提示回调里按实时头部自愈。设计上只读:父级退出只改自己那个键,后代下次读取时重新解析(可能解析到更上层祖先的绑定),不会留下悬空引用。`sessionWorktree` 多了 `inherited`,`worktreeStatus` 多了 `bindingInherited`;在没有自身绑定的会话里调 `worktree_exit` 会说明绑定属于父级。客户端半未改动。
|
|
14
|
+
|
|
15
|
+
### 修复
|
|
16
|
+
|
|
17
|
+
- **左右对照的横向滚动条浮到面板底部了。** 一行 200 列的代码,此前只能先滚到文件**末尾**才够得着横向滚动条:面板竖着滚、两栏各自横着滚,所以浏览器把滚动条画在**栏**的底部——而栏跟整个 diff 一样高。650 行的文件,那是一个你必须先越过全部想读的内容才能碰到、碰到之后还得滚回去看它干了什么的控件。两栏仍然各滚各的,因为那正是分隔条有意义的前提(一个横跨两侧的 grid 会被全文件最宽的行撑开轨道,拖它什么都不动);变的是滚动条画在哪。每栏自己的条藏起来,取而代之的是钉在**面板**底部的一条轨——`position: sticky` 装在面板的竖向滚动容器里,所以无论读者在文件哪个位置,它都贴着视口底边。每条轨是真的滚动容器,内容宽度正好等于它那栏的滚动宽度,所以两个滚动位置一比一对应、滑块也是应有的大小。双向同步,因为两端都是真的:轨是用来拖的,栏也还能被触控板横划或 shift+滚轮推动;每边只在值不同时才写,这是两者不会来回回声的原因。这条轨永远渲染、不需要时塌成 `height: 0` 而不是卸载,于是轨保住自己的元素、同步效果保住稳定的依赖;它的负外边距是它不占布局高度的原因——没有它,面板会永远比文件多滚 12px——而它盖住的是网格自己的 16px 下内边距,不是任何一行代码。开着软换行时没有溢出,整条自动塌掉。实测:轨距面板底边 0px,第一行就够得着;轨的行程与栏的行程相等,互相驱动精确一致。
|
|
18
|
+
|
|
19
|
+
- **宿主的文件读取不再能走出工作区。** `fileSides` 和 `fileImage` 用 `join(root, path)` 直接读浏览器给的路径。`isSafePathArg` 只拒绝空串和开头的 `-`,这对**pathspec** 是够的——git 拿到什么都不会走出仓库;但这两个不把路径交给 git,它们自己 `stat` 和 `readFile`,于是 `../../../` 走出工作区,抽屉照样把文件端回来:能读的返回文本、图片返回 base64、其余靠 `missing` / `notImage` / `tooLarge` 三态充当存在性探针。这道锁其实已经写过两遍(`fs-remove.ts` 为唯一那次文件系统删除写的,`writeChecked` 借的它),所以它移到 `path-lock.ts`,所有裸读都过它。两道检查而不是一道:先查拼写(绝对路径、盘符、UNC 前缀、NUL 字节、任何位置的 `..` 段——包括埋在路径中间的,两种分隔符都算),再查**解析后**的路径,那才是系统调用真正作用的形式。`ignoredDir` 原本手写了同样两道,现在读共享的那个。未跟踪文件的两个度量辅助函数拿的是 git 自己的输出而不是客户端字符串,本来就不是漏洞,但也一并走锁:带例外的规则是没人会执行的规则,而且这样「宿主里没有任何文件系统路径是用裸 `join` 拼出来的」才成为一条测试能检查的性质。**没有关掉的**:`resolve` 是词法的,所以工作区内一个指向外面的符号链接仍然解析在内。跟进链接意味着每次读每个文件都要逐段 `realpath`,而这是 git 自己都不防的场景,且前提是攻击者已经能写你打开的那个仓库。
|
|
20
|
+
|
|
5
21
|
## [0.1.15] - 2026-08-31
|
|
6
22
|
|
|
7
23
|
### 修复
|
package/CHANGELOG_EN.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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.16] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **Files ignored by .gitignore can be browsed, searched, opened and edited in the Files tab.** A file like `application-local.yml` was the one file a browser owes the reader and the one file it could not show: the paths all came from `ls-tree HEAD`, which only knows tracked paths, and from status, which never reports an ignored file. Not listing them was a performance decision that feared `node_modules` — but git already bounds that. `ls-files --others --ignored --exclude-standard --directory` collapses every fully-ignored directory to ONE line, so the listing scales with the `.gitignore`'s coverage rather than with the repository: measured on this checkout with `node_modules` present, 55 entries in 30ms. Expanding a collapsed directory cannot go through git (probed: a pathspec under `--directory` still collapses to the directory's own line, and dropping `--directory` answers 11,877 lines at once), so the new `ignoredDir` RPC reads one level from the filesystem instead — one readdir per click, pnpm-style symlinked package directories classified by a follow-up stat, entries capped with the cut reported. Ignored rows are dimmed with a title saying why; open, edit and save ride the existing untracked-file machinery. The lazily read children are kept OFF the repository path list, in a bounded, evicting, self-reporting cache of their own that the tree is grafted from: merging them back in meant every expansion rebuilt a Set, re-sorted the whole path list through `localeCompare` and rebuilt the whole tree — 118.9ms per click at the shipped 50,000-path cap, synchronously. Grafting rebuilds only the spine down to the directory clicked: 1.09ms for the same expansion, 109x, with the same 1,500 rows on screen. The one thing that cache will never evict is a directory the reader has open — a cache that fights the viewport makes the effect read that directory again, forever. A failing ignored listing now reports its error rather than serving an empty list, which is indistinguishable from a repository that ignores nothing; `ls-tree`'s exit code stays unchecked on purpose, since a repository with no HEAD legitimately fails it and "no files yet" is the honest answer there.
|
|
10
|
+
|
|
11
|
+
- **One soft-wrap switch, for the editor and both diff views.** A button in the drawer header now covers the Files editor, the unified diff and the side-by-side pane, remembered per reader, default OFF — code is written in columns, and wrapping it is a choice about one long file rather than a better default for every file. The Files editor is the cheap half: CodeMirror owns variable line heights, so wrap is one compartment it can be reconfigured in and out of without dropping the caret or the undo stack. The diff panes are not. The row window places row i at `i * 20px` with no measuring at all, and says why: the panes write `white-space: pre`, so nothing ever wraps. So there are now two height models, one live at a time. Off, the arithmetic is untouched and the new code attaches nothing and allocates nothing. On, per-row heights live in a Fenwick tree, because all three things asked of it happen on the scroll path and must each be O(log n): where a row starts is a prefix sum, which row is at y is a search for one, and a measurement changes one row. Rows are MEASURED, but only the ones in the DOM; the rest carry an estimate from their text (tabs to the next stop, full-width characters as two columns), so the scrollbar is close from the first paint. A measurement that replaces an estimate ABOVE the viewport moves the document under the reader, so the scroller is given the same delta back in the same layout pass. Side by side costs the most: the two columns are two separate grids on purpose — one grid's tracks are sized by the widest line in the file, so a dragged divider would move nothing — and separate grids align only while every row is the same height in both, which wrap ends. Both cells now take the taller side's height as a `min-height`, and the measured box is INSIDE the cell, so imposing a height cannot feed back into what is measured and a row widened again can shrink. Measured: the editor's widest line 1165px in a 1151px pane became 1106px; a 654-row diff with wrap on kept 81 rows in the DOM with a worst row-to-row drift of 0px across 14 scroll jumps and a worst frame gap of 0ms (the same file with wrap off costs 182ms, which is the fixed-height path re-highlighting).
|
|
12
|
+
|
|
13
|
+
- **A subagent session borrows its parent lineage's worktree binding.** A subagent never calls `worktree_enter` itself, so its chip, drawer and standing prompt all read "the outer repo" while its parent conversation works in a worktree. dsh's compaction never changes the session id (verified on 0.1.1-rc.2 and current source; a session/end-seed in a log marks a resume, not a fork), so lineage — not id stability — is the surface to fix on. Resolution is now: the session's own binding first, else the nearest bound ancestor up the `parentOf` map, cycle-guarded with an 8-hop cap. That map is fed by `agent/session-start` (child headers name their parent) and self-healed from the live header in the prompt callback. Read-only by design: a parent exiting changes only its own key, and descendants re-resolve on the next read — possibly to a higher ancestor's binding — so nothing dangles. `sessionWorktree` gains `inherited`, `worktreeStatus` gains `bindingInherited`, and `worktree_exit` without an own binding says the binding belongs to the parent. The client half is unchanged.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
- **The side-by-side pane's horizontal scrollbars float at the bottom of the pane.** A 200-column line could only be reached by scrolling to the END of the file first: the pane scrolls vertically and each column scrolls horizontally on its own, so the browser draws a column's scrollbar at the bottom of the COLUMN — and a column is as tall as the diff. On a 650-row file that is a control you have to travel past everything it was going to help you read, and then travel back to use what it did. The columns keep scrolling independently, because that is what lets the divider mean something: one grid across both sides is sized by the widest line in the file, so dragging it moves nothing. What changes is where the scrollbar is drawn. Each column's own bar is hidden and a rail is stuck to the bottom of the PANE in its place — `position: sticky` inside the pane's vertical scroller, so it rides the viewport's bottom edge wherever the reader is in the file. Each rail is a real scroller whose content is exactly as wide as its column's, so the two positions map one to one with no arithmetic and the thumb comes out the size the reader expects. Both directions, because both ends are real: the rail is what gets dragged, and the column still scrolls from a trackpad swipe or a shift-wheel over the code; each writes only when the value differs, which is what stops the pair from echoing one scroll back and forth. The strip is always rendered and collapses to `height: 0` rather than unmounting, so the rails keep their elements and the scroll-sync effects keep stable dependencies. Its negative margin is why it costs no layout height — without it the pane would scroll 12px past the end of every file, forever — and what it overlays is the grid's own 16px of bottom padding, not a line of code. With soft wrap on nothing overflows and the strip collapses on its own. Measured: the rail sits 0px from the pane's bottom edge and is reachable from row 1; rail travel equals column travel, and each drives the other exactly.
|
|
18
|
+
|
|
19
|
+
- **The host's file reads can no longer leave the worktree.** `fileSides` and `fileImage` read a file the browser named with `join(root, path)` and nothing else. `isSafePathArg` refuses an empty string and a leading dash, which is all a PATHSPEC needs — git will not step outside the repository whatever it is handed. Those two do not hand the path to git. They stat and read it directly, so `../../../` walked out of the worktree and the drawer answered with the file: a text pane for anything readable, base64 for any image, and the missing/notImage/tooLarge split as an existence oracle for the rest. The lock already existed twice over — `fs-remove.ts` wrote it for the one filesystem delete, and `writeChecked` borrowed it from there — so it moves to `path-lock.ts` and every raw read now goes through it. Two checks, not one: the spelling (absolute paths, drive letters, UNC prefixes, NUL bytes, any `..` segment including one buried mid-path, either separator), and then the RESOLVED path, which is the form the syscall acts on. `ignoredDir` had open-coded both; it reads the shared one instead. The two untracked-file measurement helpers take git's own output rather than a client string, so they were never the hole, but they go through the lock anyway: a rule with an exception is a rule nobody applies, and it makes "no filesystem path in the host is built by a bare join" a property a test can check. What this does NOT close: `resolve` is lexical, so a symlink inside the worktree pointing outward still resolves inside. Following links means a realpath per segment on every read of every file, for a case git itself does not defend against and that already presupposes write access to the repository the reader opened.
|
|
20
|
+
|
|
5
21
|
## [0.1.15] - 2026-08-31
|
|
6
22
|
|
|
7
23
|
### Fixed
|
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
- **提交与同步**:树上勾选文件就是真实的 `git add` / `git restore --staged`,配合提交框和 fetch / pull / push 同步条,一次提交加推送全程不用离开面板;
|
|
14
14
|
- **外观**:七套主题族各带亮暗,默认跟随系统;支持虚化背景图和自定义 CSS,按「项目 / 全局」两个作用域保存,项目优先。
|
|
15
15
|
|
|
16
|
-
另带 **worktree 仿真**:模型在会话里调用 `worktree_enter` / `worktree_exit` / `worktree_status` 三个工具,即可在 `.agents/worktrees/<name>` 下建立或退出隔离 worktree
|
|
16
|
+
另带 **worktree 仿真**:模型在会话里调用 `worktree_enter` / `worktree_exit` / `worktree_status` 三个工具,即可在 `.agents/worktrees/<name>` 下建立或退出隔离 worktree,并把会话绑定过去。**子代理会话不写自己的绑定,而是沿谱系借用最近绑定祖先的 worktree**——standing 提示、芯片与 `worktree_status` 对无自有绑定的会话统一解析「有效绑定」,外层退出后子树自动失去借用。绑定后状态卡点亮绑定标记,面板头部出现 worktree 切换器(按分支列出仓库全部 worktree),统计随之切换。
|
|
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>
|
|
@@ -171,8 +171,8 @@ export function apply(ctx) {
|
|
|
171
171
|
- **宿主 RPC**(同一 `GitWorkbenchService` 上多挂 4 个 `@Remote`,参数照 §6.8 裸标识符、signal 最后):
|
|
172
172
|
- `worktreeEnter(sessionId, repoPath, name, signal)`——`repoRootOf` 解析仓库根;在 `<repoRoot>/.agents/worktrees/<name>` 创建(或复用)worktree、**分支 = 名字本身**(不加强制前缀),写绑定;返回 `{ok, worktreePath, branch, hint}`,hint 教模型怎么用相对路径(会话 cwd 不可变)。复用判定走 **realpath**:目标目录已是注册 worktree(别的工具建的、或经 Junction 映射进来的,git 登记的是另一种拼写)→ 直接绑定并保留**它自己的分支**,不再 `worktree add`。
|
|
173
173
|
- `worktreeExit(sessionId, remove, signal)`——解绑;`remove:true` 且树干净才 `git worktree remove`,脏树拒绝。
|
|
174
|
-
- `worktreeStatus(sessionId, signal)
|
|
175
|
-
- `sessionWorktree(sessionId, signal)`——`{worktreePath, name}
|
|
174
|
+
- `worktreeStatus(sessionId, repoPath, signal)`——**有效绑定**(自有优先,否则沿谱系借最近绑定祖先;`bindingInherited` 标明是否借来)+ 仓库全部 worktree 列表。
|
|
175
|
+
- `sessionWorktree(sessionId, signal)`——`{worktreePath, name, inherited}`,只读绑定 JSON、零 git spawn;无自有绑定时借最近绑定祖先(`inherited:true`),连祖先也无绑定才是双 `null`。客户端轮询已改用 `worktreeStatus`(绑定+列表一次拿全),这个 RPC 保留作轻量单查。
|
|
176
176
|
- **绑定持久化** `~/.dsh/gitworkbench-worktree-bindings.json`(`{v:1, bindings:{<sessionId>:{repoRoot,worktreePath,name,enteredAt}}}`)。写法是**先写 `.tmp` 再 rename**(崩溃不留半截文件);Windows 上 rename 可能 EPERM → 25/50/100/200/400ms 退避重试;所有 load→save 段落经 promise 队列互斥(`withBindings`),并发 enter/exit 不会互相覆盖。
|
|
177
177
|
- **agent 工具**:同一份逻辑用 `ctx.tools.register(defineTool({...}))` 注册成 `worktree_enter/exit/status`,sessionId/cwd 取自 `exec.agent?.session`(**不接受**模型传参)——注册要点见 §6.10,schema 限制见 §6.11。
|
|
178
178
|
- **客户端跟随**:`GitWorkbenchPanel` 每轮拉 stats 的同时拉 `worktreeStatus(sessionId, cwd)`(绑定 + 仓库全部 worktree 一次拿到,agent 在 dsh 外面建的 worktree 也会跟进列表);有绑定 → 状态卡亮出绑定标记(树形图标;分支与徽标文字重名时省略后者)、stats 改传绑定的 worktree 绝对路径;面板头部的 worktree 选择器按分支列出所有源,**只切显示对象、不动绑定**。树的展开状态跨切换、跨轮询保留;选中在切换源时**有意重置**——旧 worktree 的路径不能漏进新树的选中(§6.0c)。
|
|
@@ -533,4 +533,5 @@ git 的两种「路径」只在仓库根相等:`git status --porcelain` 和 `g
|
|
|
533
533
|
- **会话 cwd 不可变**(dsh 本体约束):enter 不切 cwd,而是返回 hint 指引模型——file 工具用 `.agents/worktrees/<name>/` 前缀的相对路径,shell 命令传 per-call workdir `.agents/worktrees/<name>`(相对会话 cwd 解析)。
|
|
534
534
|
- **再进入**:目录仍是注册 worktree(含外部工具建的、经 Junction 映射的——realpath 判定)→ 直接复用、只补绑定并保留其分支;目录已删但分支 `<name>` 幸存 → `worktree add <dir> <name>` 检出旧分支(hint 注明 reused)。
|
|
535
535
|
- **绑定**(per-session)持久化于 `~/.dsh/gitworkbench-worktree-bindings.json`;损坏/缺失视为无绑定并重建。写入原子(tmp+rename)+ 互斥(promise 队列)+ EPERM 退避重试(§6.12)。
|
|
536
|
+
- **子代理借绑定、不写键**:`agent/session-start` 事件把子会话 header 的 `parentSession` 喂进宿主 `parentOf` 表(提示回调还会从活 header 自愈补第一跳,兜插件重载);standing 提示、芯片/抽屉、`worktree_status`、`sessionWorktree` 统一经 `resolveEffectiveBinding`(worktree.ts:**自有绑定优先**,miss 沿父链借最近绑定祖先,环检测 + 8 跳上限)解析有效绑定。只读不写——外层 `worktree_exit` 后子树下次读取自动失去借用(更高祖先仍绑定时向上翻转);子会话自己 `worktree_enter` 以自有绑定遮蔽继承。`worktree_exit` 对无自有绑定的会话报错并指明绑定属父会话。宿主重启后空闲会话的谱系边要等其 loop 恢复才有——查询退化为无继承(即旧版行为,fail-soft)。
|
|
536
537
|
- **状态卡纪律例外**:有绑定时即使 bound worktree 干净也显示状态卡——绑定标记(树形图标)是绑定指示器与面板入口;面板打开期间空视图也保持挂载(可从空源切走)。头部选择器只改显示对象,不动绑定。
|
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. 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. **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>
|