@young1lin/dsh-ui-gitworkbench 0.1.9 → 0.1.11
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/AGENTS.md +40 -1
- package/CHANGELOG.md +26 -0
- package/CHANGELOG_EN.md +26 -0
- package/README.md +2 -0
- package/lib/client.js +784 -474
- package/package.json +21 -1
- package/src/client/CodeEditor.tsx +174 -48
- package/src/client/FileBrowser.tsx +27 -25
- package/src/client/GitWorkbenchPanel.tsx +16 -14
- package/src/client/cm-diff.ts +24 -2
- package/src/client/cm-tokens.ts +34 -14
- package/src/client/highlight.ts +100 -54
- package/src/client/idle-value.ts +0 -21
- package/src/client/locales.ts +1 -3
- package/src/client/token-cache.ts +0 -0
package/AGENTS.md
CHANGED
|
@@ -33,6 +33,45 @@ This split shapes everything else:
|
|
|
33
33
|
- `@Remote` method params must be bare identifiers with `signal` last (the gateway reads `Function.prototype.toString`), and return values must be JSON-safe — no `undefined` property values, omit the key entirely.
|
|
34
34
|
- Host git calls use `ctx.subprocess.spawn` with `stdout: 'pipe'`, accumulating chunks manually — never `ctx.shell` (PTY scrollback silently truncates large output: lost branch headers, lost files).
|
|
35
35
|
|
|
36
|
+
## Performance first, and no leaks
|
|
37
|
+
|
|
38
|
+
This is the drawer's first requirement, ahead of features. It reads
|
|
39
|
+
repositories that are large, files that are long, and diffs that are dense, and
|
|
40
|
+
every one of those has, at some point, frozen it for seconds. The rules below
|
|
41
|
+
are what those freezes cost to find.
|
|
42
|
+
|
|
43
|
+
- **Nothing may be proportional to the file, the diff, or the repository.**
|
|
44
|
+
Work is proportional to the VIEWPORT: the rows on screen, plus a bounded
|
|
45
|
+
overscan. `row-window.ts` decides which rows are in the DOM; `token-cache.ts`
|
|
46
|
+
decides which lines get tokenized. A new pane wires into both — it does not
|
|
47
|
+
invent a third answer, and it does not render "just this once" over
|
|
48
|
+
everything.
|
|
49
|
+
- **A cap that turns a feature off is not a fix.** The Files tab used to stop
|
|
50
|
+
colouring past 2,000 lines and print a notice about it; that is a freeze
|
|
51
|
+
traded for a missing feature. Bound the WORK, keep the feature.
|
|
52
|
+
- **Never put an O(document) pass on the keystroke path.** The live diff tint
|
|
53
|
+
cost 709ms per keystroke at 4,000 lines. Map what is already computed through
|
|
54
|
+
the change and recompute when the typing stops (`IdleLayer` in
|
|
55
|
+
`CodeEditor.tsx`).
|
|
56
|
+
- **Caches are bounded, evicting, and able to report their own size**, so a
|
|
57
|
+
test can prove the bound instead of trusting it — `ChunkedTokens.size()`,
|
|
58
|
+
`LineTokens.size()`, `CommitPayloadCache`. An unbounded cache is a leak with
|
|
59
|
+
a nicer name.
|
|
60
|
+
- **Everything acquired is released**: `addEventListener` /
|
|
61
|
+
`removeEventListener`, `setInterval` / `clearInterval`, every observer
|
|
62
|
+
`disconnect()`ed, every timer cleared in the view's `destroy()` or the
|
|
63
|
+
effect's cleanup. `tests/no-leaks.test.ts` counts the pairs per file, so an
|
|
64
|
+
unbalanced one fails the suite.
|
|
65
|
+
- **Measure before and after, on real code, and put the numbers in the commit
|
|
66
|
+
message.** Generated fixtures lie: 4,000 identical lines with one change
|
|
67
|
+
opened in 200ms while the reporter's 800-line file took two seconds. The
|
|
68
|
+
live probes under `scripts/` (`verify_perf_profile.py`,
|
|
69
|
+
`verify_paint_window.py`) take a CDP CPU profile and count painted spans, so
|
|
70
|
+
the answer is a function name and a span count, not an impression. A
|
|
71
|
+
"performance fix" with no before/after number is a guess.
|
|
72
|
+
- **A fast pane that shows nothing is not fast.** Every perf probe asserts what
|
|
73
|
+
is ON SCREEN as well as what it cost.
|
|
74
|
+
|
|
36
75
|
## Testing pattern
|
|
37
76
|
|
|
38
77
|
Pure rules live in React/CSS-free modules so vitest can load them — `src/client/stage-tree.ts`, `diff-model.ts`, `worktree-view.ts`, `commit-graph.ts`, `op-feedback.ts`, `themes.ts`, `highlight.ts` (client); `worktree.ts`, `style-store.ts`, `atomic-json.ts`, `commit-cache.ts`, `git-ops.ts`, `git-log.ts` (host). React state stays in `GitWorkbenchPanel.tsx` (~3400 lines: chip + drawer + diff rendering). New behavior = pure helper + unit tests first, component wiring after.
|
|
@@ -48,7 +87,7 @@ Pure rules live in React/CSS-free modules so vitest can load them — `src/clien
|
|
|
48
87
|
- **UI copy is bilingual** (`src/client/locales.ts`, zh + en — add both keys); code comments and commit messages are English.
|
|
49
88
|
- **Commits:** conventional prefix, lowercase subject, a body explaining *why*, ending with a `Co-Authored-By:` trailer per model that worked on the change, ranked by token usage — credit the model(s) that actually wrote it, never a fixed name (history shows the spellings: `Claude Opus 5 (1M context) <noreply@anthropic.com>`, `GLM-5.3 <noreply@zhipuai.cn>`, `Grok-4.6 <noreply@x.ai>`). Remote is `origin` = github.com/young1lin/dsh-ui-gitworkbench; publishing is tag-driven — push `vX.Y.Z` and `.github/workflows/publish.yml` publishes to npm via Trusted Publishing (no local npm login; the workflow re-runs typecheck/tests/build itself).
|
|
50
89
|
- **Other agents may have uncommitted work in this tree** (e.g. `scripts/verify_worktree_ui.py`). Check `git status` first and commit with `git commit --only <paths>` — never sweep the tree, and don't touch files that aren't yours.
|
|
51
|
-
- `.npmrc` must keep `auto-install-peers=false` (dev-time weight control: the harness peers resolve from the web profile / linked checkout; letting pnpm auto-install the whole `@deepseek-ai/*` tree locally adds nothing). The peers
|
|
90
|
+
- `.npmrc` must keep `auto-install-peers=false` (dev-time weight control: the harness peers resolve from the web profile / linked checkout; letting pnpm auto-install the whole `@deepseek-ai/*` tree locally adds nothing). The peers are declared **optional** on purpose: every one of them is in `tsdown.config.ts`'s `CLIENT_EXTERNALS`, so the dsh shell shares them into its frozen module table at runtime and they are never meant to land in a consumer's tree — a dsh profile holds only the plugin, with `@deepseek-ai/*` one directory up. Keep the RANGES current anyway; they are what still checks compatibility when the peer is present. Do not trust the `latest` dist-tag of any `@deepseek-ai/*` package: it still points at the unusable `0.0.1-rc.1` line (README 6.14). `@deepseek-ai/*` imports in client code must be `import type` only — the bundle purity gate rejects value imports.
|
|
52
91
|
|
|
53
92
|
<!-- business-logic-skill: explore-first (managed by install_hooks.py) -->
|
|
54
93
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录面向使用者的变更。格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
|
4
4
|
|
|
5
|
+
## [0.1.11] - 2026-08-21
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **安装插件时不再打印一屏 `missing peer` 告警**。`dsh plugin --profile web add` 会对六个 peer 全部报 missing,而插件其实完全正常。原因是这些 peer 一个都不该被安装:它们全在 `tsdown.config.ts` 的 `CLIENT_EXTERNALS` 里,dsh 外壳把它们共享进自己那张冻结模块表,由**加载插件的进程**在运行时提供。目录结构可以直接看出来——`~/.dsh/profiles/web/node_modules/` 里只有插件自己,`@deepseek-ai/*` 全在上一层 `~/.dsh/profiles/node_modules/`,Node 逐级向上解析所以运行时找得到,而 pnpm 的 peer 检查只看本层,于是全报 missing。这不是本包特有的:官方自己的 `dsh-client-ui-file-reference` 与 `dsh-file-reference` 在同一次安装里打印一模一样的告警。既然消费者不该安装它们,那按 npm 自己的定义它们就是 optional,现在 `peerDependenciesMeta` 如实声明了这一点。**版本范围一个字没改**——peer 真的在场时仍然照常校验兼容性。空目录装 `npm pack` 出的 tarball 验证(`auto-install-peers=false`,与 dsh profile 一致):改前 6 行 missing peer,改后零告警、exit 0,17 个 host 模块加 `client.js` 一个不少。
|
|
10
|
+
|
|
11
|
+
## [0.1.10] - 2026-08-20
|
|
12
|
+
|
|
13
|
+
性能专项:抽屉的开销从此与文件长度无关。
|
|
14
|
+
|
|
15
|
+
### 性能
|
|
16
|
+
|
|
17
|
+
- **打开长文件不再卡,而且长文件重新有了语法着色**。此前编辑器(Files 页签,以及变更页进入编辑时)会把**整个文件**交给 shiki 分析一遍——1837 行真实 TypeScript 实测 1637ms,而超过 2000 行的文件干脆**不上色**,并弹一条提示说明原因。那不是修好,那是拿"少一个功能"换"不卡"。现在编辑器只要它**即将显示的那几十行**,跟着 CodeMirror 自己的视口走,2000 行上限和那条提示一起删了。实测 1837 行:**1461ms、连续 1274ms 画面不动**变成 **254ms、60ms**;6065 行的文件此前"很快但没有颜色",现在 **255ms 且有颜色**。
|
|
18
|
+
- **同一行永远不分析第二遍**。此前窗口每移动一次,都要重新扫描 240 行上下文加窗口内的行,什么都不记——连滚十屏 3021ms。新的 `token-cache.ts` 把文档切成 128 行一块,每块只分析一次,并且**接着前一块结束时的语法状态**继续(shiki 4.3 会把这个状态和 token 一起交回来,也接受它作为入参,所以续接是精确的,不是靠上下文猜的);只有从中间直接跳入时才读一次 240 行上下文。diff 视图那第二遍"逐行单独重新分析"——它必须存在,因为一个 hunk 不是真文件,否则新增的语句会被涂成对象键——只取决于该行文本,所以按文本缓存。**往回滚实测 77ms、零掉帧**。两个缓存都是 LRU、按**行数**限量、能自报大小,测试直接验证这个上限而不是相信它。
|
|
19
|
+
- **换引擎没有用,这条是实测排除的**。试了五种 shiki 引擎配置(预编译、ES2024 / ES2018 目标、内部 cache),300 行真实代码全部落在 130–180ms。每行半毫秒是这个分析器的地板,所以唯一的解法是少分析、并且记住——上面两条。
|
|
20
|
+
|
|
21
|
+
### 修复
|
|
22
|
+
|
|
23
|
+
- **打开第二个文件卡 7 到 9 秒**(这也是此前"从 22 行文件点到 4000 行文件几乎卡死"的真正原因)。CPU 采样直接点名 `@codemirror/merge` 的 diff。切换文件时,新文件的正文和"被比较的另一侧"是**两个独立的事务**进入编辑器的,中间那一帧里,**新文件的正文正对着旧文件的另一侧**——两份毫不相干的文档,做一次没有上限的 diff。现在实时底色只在两边都到齐之后才重算(而那时两边通常完全相同,第一行就返回了),并且给 `presentableDiff` 加上了 CodeMirror 自己的合并视图所用的上限(`scanLimit: 500`,外加 100ms 超时)。去掉这个上限,新增测试里那一对文本要跑 **292 秒**。实测:切换文件 **6952–8974ms 变成 270–357ms**,且文字落地即有颜色。
|
|
24
|
+
- **打字时不再有整文档级别的计算**。实时底色此前在**每一个事务**上重算全文 diff——4000 行的文件每敲一个键 709ms。现在着色和底色都先跟着改动平移(颜色跟着文字走,不会糊),停手之后再重算。
|
|
25
|
+
|
|
26
|
+
### 内部
|
|
27
|
+
|
|
28
|
+
- 新增 `tests/no-leaks.test.ts`:逐个客户端源文件核对 `addEventListener`/`removeEventListener`、`setInterval`/`clearInterval`、observer/`disconnect` 的配对,以及 CodeMirror 图层那个 React 清理够不着的延时器是否在 `destroy()` 里清除。扫描前先剥掉注释和字符串字面量。
|
|
29
|
+
- `AGENTS.md` 新增「性能与内存优先」一节:不许有正比于文件或仓库的开销;用上限关掉功能不算修好;按键路径上不许有整文档级别的计算;缓存必须有界、会淘汰、能自报大小;性能改动必须有真实代码上的前后数字。
|
|
30
|
+
|
|
5
31
|
## [0.1.9] - 2026-08-20
|
|
6
32
|
|
|
7
33
|
### 性能
|
package/CHANGELOG_EN.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
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.11] - 2026-08-21
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Installing the plugin no longer prints a screen of `missing peer` warnings.** `dsh plugin --profile web add` reported all six peers as missing while the plugin worked perfectly. None of them should be installed: every one is in `tsdown.config.ts`'s `CLIENT_EXTERNALS`, shared into the dsh shell's frozen module table and provided at runtime by the process that loads the plugin. The directory layout shows it — `~/.dsh/profiles/web/node_modules/` holds only the plugin, while `@deepseek-ai/*` lives one level up in `~/.dsh/profiles/node_modules/`, which Node's upward resolution finds and pnpm's peer check (which only inspects the profile's own tree) does not. This is not specific to this package: DeepSeek's own `dsh-client-ui-file-reference` and `dsh-file-reference` print the identical warnings in the same install. A peer a consumer must not install is, by npm's own definition, optional, and `peerDependenciesMeta` now says so. **The version ranges are unchanged** — they still check compatibility when the peer IS present. Verified by installing the packed tarball into an empty directory with `auto-install-peers=false`, as a dsh profile has: six missing-peer lines before, no warnings and exit 0 after, with all 17 host modules and `client.js` present.
|
|
10
|
+
|
|
11
|
+
## [0.1.10] - 2026-08-20
|
|
12
|
+
|
|
13
|
+
A performance release: what the drawer costs no longer depends on how long the file is.
|
|
14
|
+
|
|
15
|
+
### Performance
|
|
16
|
+
|
|
17
|
+
- **Long files open without freezing, and long files have syntax colour again.** The editor — the Files tab, and the side pane once editing is armed — used to hand shiki the WHOLE file: 1,637ms on 1,837 lines of real TypeScript, and files past 2,000 lines were simply not coloured, with a notice explaining why. That is not a fix; it is a freeze traded for a missing feature. The editor now asks only for the lines it is about to show, driven by CodeMirror's own viewport, and the 2,000-line cap went with the notice. Measured at 1,837 lines: **1,461ms with a 1,274ms frozen frame becomes 254ms with 60ms**. A 6,065-line file used to be fast and colourless; it is now **255ms, with colour**.
|
|
18
|
+
- **No line is ever tokenized twice.** Every window move used to re-lex a 240-line lead-in plus its rows and remember nothing — ten screenfuls cost 3,021ms. The new `token-cache.ts` cuts a document into 128-line chunks, tokenizes each once, and **continues from the grammar state the previous chunk ended in** (shiki 4.3 returns that state with the tokens and accepts it back, so the continuation is exact rather than a lead-in's guess). Only a cold jump into the middle of a file reads a lead-in, and only once. The diff panes' second pass — re-lexing each line alone, which has to exist because a hunk is not a real file and would otherwise paint added statements as object keys — depends on nothing but the line's text, so it is cached by it. **Scrolling back over a file: 77ms, no dropped frame.** Both caches are LRU, bounded in LINES, and report their own size, so the tests prove the bound rather than trusting it.
|
|
19
|
+
- **Changing the engine does not help — that was measured, not assumed.** Five shiki engine configurations (eager compilation, ES2024 and ES2018 targets, an internal cache) all landed between 130ms and 180ms for 300 lines of real code. Half a millisecond per line is this tokenizer's floor, so the only lever is to tokenize less and remember it — the two entries above.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **Opening a second file froze for seven to nine seconds** — which is also what was really behind "clicking a 4,000-line file after a 22-line one nearly locks up". A CPU profile named `@codemirror/merge`'s diff. Switching files replaces the buffer and the side it is compared against in two separate transactions, so for one frame the new file's text sits opposite the OLD file's: two unrelated documents, diffed with no ceiling. The live tint now recomputes only once both sides have settled (by which point they are usually identical and it returns immediately), and `presentableDiff` is given the bound CodeMirror's own merge view uses (`scanLimit: 500`, plus a 100ms timeout). Without that bound, the pair in the new test takes **292 seconds**. Measured: a file switch goes from **6,952-8,974ms to 270-357ms**, and the text arrives painted.
|
|
24
|
+
- **Nothing document-sized runs on the keystroke path any more.** The live tint used to diff the whole document on EVERY transaction — 709ms per keystroke at 4,000 lines. Colour and tint now map through the edit (so they ride along with the text instead of smearing) and recompute once the typing stops.
|
|
25
|
+
|
|
26
|
+
### Internal
|
|
27
|
+
|
|
28
|
+
- `tests/no-leaks.test.ts`: per client source, every `addEventListener` has a `removeEventListener`, every `setInterval` a `clearInterval`, every observer a `disconnect`, and the CodeMirror layer's deferred timer — the one React's cleanup cannot reach — is cleared in `destroy()`. Comments and string literals are stripped before counting.
|
|
29
|
+
- `AGENTS.md` gains a "Performance first, and no leaks" section: nothing proportional to the file or the repository; a cap that turns a feature off is not a fix; nothing document-sized on the keystroke path; caches bounded, evicting, and able to report their size; and a performance change ships with before/after numbers taken on real code.
|
|
30
|
+
|
|
5
31
|
## [0.1.9] - 2026-08-20
|
|
6
32
|
|
|
7
33
|
### Performance
|
package/README.md
CHANGED
|
@@ -370,6 +370,8 @@ window.__ModuleLoader__.load({ id: "@young1lin/dsh-ui-gitworkbench", factory: (r
|
|
|
370
370
|
### 6.14 发布的 peer 范围不能写 `*`——`*` 按 `latest` dist-tag 解析
|
|
371
371
|
npm 7+ 自动安装 peer 时,`*` 走 **`latest` dist-tag**,不是「取版本列表最高」。`@deepseek-ai/*` 全系的 `latest` 长期停在 8 月 10 日的 `0.0.1-rc.1` 老线(那条线依赖**从未发布**的 `@deepseek-ai/dsh-compact`,公开安装必 404),能用的 `0.1.0-rc.x` 全挂 `next`。于是 0.1.2 之前任何不在 dsh profile 工作区里的裸 `npm i` 都炸 E404。规则:**peer 写显式区间**(cordis `^4.0.1-rc.1`、dsh 系 `^0.1.0-rc.2`),dsh 发新线时同步抬范围并验证 `npm pack` 出的 tarball 在空目录可装。注意 6.5 的 `auto-install-peers=false` 只管本仓库 pnpm 开发态,管不了用户侧 npm。
|
|
372
372
|
|
|
373
|
+
**补充(0.1.11)**:范围要留,但这些 peer 同时是 **optional**。它们全在 `tsdown.config.ts` 的 `CLIENT_EXTERNALS` 里——dsh 外壳把它们共享进自己那张冻结模块表,运行时由**加载插件的进程**提供,从来不该落进 profile 自己那层 `node_modules`。实测目录结构可以证明:`~/.dsh/profiles/web/node_modules/` 里只有插件自己,`@deepseek-ai/*` 全在上一层 `~/.dsh/profiles/node_modules/`,Node 逐级向上解析所以能找到;而 pnpm 的 peer 检查只看本层,于是把每一个都报成 missing——**官方自己的 `dsh-client-ui-file-reference` / `dsh-file-reference` 在同一次安装里打印一模一样的告警**,这条才是「这是平台常态,不是本包的缺陷」的证据。「消费者不该安装的 peer」按 npm 自己的定义就是 optional,所以 0.1.11 起 `peerDependenciesMeta` 把六个全标为 optional:告警消失,而**范围一个字没动**,peer 真的在场时仍然照常校验版本。验证方式是空目录装 `npm pack` 出的 tarball(`auto-install-peers=false`,与 dsh profile 一致):改前 6 行 missing peer,改后零告警、exit 0、17 个 host 模块加 `client.js` 一个不少。
|
|
374
|
+
|
|
373
375
|
### 6.15 Windows 上裸 `--since=2026-08-18` 可能吃掉当天的提交
|
|
374
376
|
git 对裸 `yyyy-mm-dd` 的 `--since`/`--until` 解析带时刻语义,Windows 上一整天的提交可能全被排掉,且无任何报错。host 把裸日期展开为 `T00:00:00` / `T23:59:59` 再交给 git(`log-filter.ts`)——选中一天即指一整天,不赌平台行为。
|
|
375
377
|
|