dsh-composer-ease 0.0.0-stage → 0.9.66

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 ADDED
@@ -0,0 +1,180 @@
1
+ - **0.9.66**(2026-10-31,`15dcf47`):**真机留证入仓 + 条目同步闸(回应「PR 总是慢一步」「没有真机复现」两条反馈)**。
2
+ ① 真机复现变成可查的产物:`npm run test:real -- <url>` 跑 33 条真机矩阵并写 `test/real/last-run.json`(时间、包版本、通过数、失败明细,token 脱敏);`npm run check:real` 检查留证新鲜度(超 14 天或版本不符就提示补跑),已进 `check:all`。
3
+ ② 首次运行就抓到两条真失败:`C2 全角括号配对`(期望 `()`,实际 `()` —— 0.9.59 改了默认)、`D1 / 菜单三行设置`(实际 5 行 —— 0.9.58 加了「全角括号」「重置插件设置」)。两条用例按新契约更新后 **33/33**。
4
+ ③ 条目同步闸 `npm run check:entry`:`docs/registry-entry.json` 成为投稿唯一来源,校验 id/repo 与 `package.json` 一致、字段齐备、描述里若写版本号必须等于当前版本 —— 直接针对「登记 PR 因描述过期被打回」这一类返工。
5
+ ④ 说明:真机矩阵不进 CI(要有活着的 dsh 宿主 + token + Playwright),改为本地跑、读数入仓,评审看得到证据。
6
+ - **0.9.65**(2026-10-29,`31b74af`):**去掉输入法分支里的重复换算(三条输入路统一到 `decideType`)**。
7
+ ① `onInput` 里那份「替换前一个全角字符」的分支删除:换算已由 beforeinput 的 `decideType` 统一给出 `convert`,两条路同时换算会互相打架。`fwKeyHandled` 只保留「清标记」的作用。
8
+ ② 真机复验七条:IME `(`/`【`(开关开)→ `()`/`[]`、`abc` 后打 `(` → `abc()`、空框 `"` → `""`、IME `(`(开关关)→ `()`、框选 `"`/`(` → `"选中这段"`/`(选中这段)`。
9
+ ③ 判据:行为套件 153 项、离线 206 项全过;散文闸干净;`check:refactor` 由上一条 `refactor:` 提交复位。
10
+ - **0.9.64**(2026-10-29,`4b2b695`):**beforeinput 也接入 `decideType`(VS Code 移植第 2 条路);`flushBeforeFix` 重写**。
11
+ ① beforeinput 不再自己判:`decideType(ch, {before, after, fwHalf})` → `insert` 时零干预,其余把 `pendingFix` 记成 `skip|convert|pair`,落地仍在 `flushBeforeFix`。
12
+ ② `flushBeforeFix` 按动作重排(surround / convert / skip / pair),convert 用梯子等原生全角字符落地后「合成 Backspace 删掉 + 插半角对子」——上一版把 convert 分支插进了 skip 块里(因而从不执行),重写后修好。
13
+ ③ 真机七条全过:IME `(`(fwHalf=1)→ `()`、IME `【` → `[]`、`abc` 后打 `(` → `abc()`、空框 `"` → `""`、IME `(`(fwHalf=0)→ `()`、框选 `"` → `"选中这段"`、框选 `(` → `(选中这段)`。
14
+ ④ 判据:行为套件 153 项、离线 206 项全过;散文闸干净。剩输入法 `onInput` 那条路仍有一份换算分支(已用 `fwKeyHandled` 防重复),下一步与另两条合并。
15
+ - **0.9.63**(2026-10-29,`b04f474`):**keydown 决策接入 `decideType`(VS Code 移植第 1 条路打通)**。
16
+ ① 键盘路径不再自己判「原字符还是换算字符」:`decideType(ch, {before, after, selection, fwHalf})` 一次给出 `open/close`(含全角→半角换算),下面各分支只按 `open/close` 落地;`fwKeyHandled` 让 beforeinput/input 两条路不重复换算(并在 input 里消费掉)。
17
+ ② 真机读数(demo 3081,六条全过):IME `(` + fwHalf=1 → `()`;`abc` 后打 `(` → `abc()`;空框 `"` → `""`;IME `(` + fwHalf=0 → `()`;选中整段打 `"` → `"选中这段"`、打 `(` → `(选中这段)`。
18
+ ③ 判据:行为套件 153 项全绿、散文闸干净。登录 3 条输入路里已切 1 条;beforeinput 与 IME 分支下一步同样改为「构造 ctx → decideType → 落地」。
19
+ - **0.9.62**(2026-10-29,`7e6d59a`):**注释瘦身回契约(散文闸干净)**。
20
+ 散文闸此前 6 处红:两处第一人称(「我们」「不是 VS Code 的」)、三处叙事词(真机/实测)、一处 9 行注释块。现全部改写成短约束(每处 ≤2 行,只写「为什么」),散文闸回到干净;行为零改动,五支套件 206 项全过。
21
+ 说明:把三条输入路切到 `buildTypeEdits` + 单 applier 的改动本轮试过一版,`(` 换算与「成对家族不叠罗汉」互相干扰(得到 `((`、`(` 两种错形状),已整体回滚,留待单独一轮做。
22
+ - **0.9.61**(2026-10-29,`664ec27`):**复刻 VS Code 的打字拦截层(模式枚举 + before 集合 + 编辑列表)**。
23
+ ① 常量照本机产物逐字搬:`DEFAULT_AUTOCLOSE_BEFORE_LANGUAGE_DEFINED_QUOTES = ";:.,=}])> \t"`、`…BRACKETS = "'\"`;:.,=}])> \t"`、`…WHITESPACE = " \t"`;模式枚举 `always|languageDefined|beforeWhitespace|never`。
24
+ ② 新增 `autoCloseMode/setAutoCloseMode`、`shouldAutoCloseBefore(prev, mode, kind)`、`buildTypeEdits(ch, ctx) → [{range,text,caret,select}]` —— 与 VS Code 的 `typeWithInterceptors` 同构:先把该做什么算成编辑列表,再由 applier 统一落地。
25
+ ③ 一处**刻意偏差**(已写进注释与测试):`languageDefined` 下我们允许词/中文后成对 —— 聊天框里 `abc` 后打 `(` 要 `abc()`,VS Code 的 before 集合里没有字母。
26
+ ④ 判据:新增 6 组复刻用例(模式语义、before 集合逐字符、编辑列表三条形状、词后成对的偏差),行为套件 147 → **153 项**。
27
+ ⑤ ctx 契约写成判据:`before` 是「选区之前」的文本,`selection` 是选中文本(整段选中时 `before` 为空)—— 这一条先写错、被测试抓住。
28
+ - **0.9.60**(2026-10-29,`13f9e46`):**配对收敛出「唯一决策入口」`decideType`(照 VS Code 的 typeWithInterceptors)**。
29
+ 本地从 VS Code 产物里挖到的路子(不是猜):① 设置是**模式**不是规则(`autoClosingBrackets`/`autoClosingQuotes` = `always|languageDefined|beforeWhitespace|never`);② 成对表来自**语言配置**(`getAutoClosingPairs` / `getSurroundingPairs`);③ 判定是一个 `shouldAutoCloseBefore(regex)` + token 类型(`_inString/_inComment/_inRegEx`);④ 编辑是 `typeWithInterceptors(...)` 一次算出编辑列表,再由模型统一 `executeEdits`。
30
+ 我们过去是「逐段在事件层打补丁」,所以选中/删除会和宿主模型脱节(全角换算栽的就是这类)。本版先落第 ④ 条的骨架:`decideType(ch, ctx)` 纯函数,返回 `wrap | skip | convert | pair | insert`,不碰 DOM;矩阵测试直接打这张表(新增 4 例,行为套件 143 → **147 项**)。
31
+ 下一步(未做):把 keydown / beforeinput / 输入法三条路都改成「先 decideType → 再由单个 applier 落地」,之后删除散落在事件层的重复判定。
32
+ - **0.9.59**(2026-10-29,`4118305`):**全角括号默认换成半角 () —— 真机落地(上一轮的真根因是变量作用域)**。
33
+ ① 上一轮失败的根因找到了:映射表 `FW_TO_ASCII` 原来写在 **input 处理器内部**,beforeinput 那条路引用它时抛 ReferenceError 被外层 try 吞掉 —— 于是既不配对也不落字,只剩输入法那个孤零零的 `(`。把表提到模块级(`10-foundation.js`)后整条路就通了。
34
+ ② 两条路各管一段:keydown 打半角/全角开符时直接换成 ASCII 再配对(并置 `fwKeyHandled` 让 beforeinput 不重复换);输入法合成(无 keydown)走 beforeinput + 梯子:等原生字符落地 → 合成 Backspace 删掉它 → 插半角对子。
35
+ ③ 默认开(`fwHalf()` 只有显式存 `0` 才关):中文态打 `(` 得 `()`、`【` 得 `[]`;想按中文标点写正文在 `/` 菜单「全角括号」关掉,得 `()`/`【】`。引号族不换算,保持输入法给的字符。
36
+ ④ 真机读数(demo 3081,两态都测):开关关 → `()`、开 → `()`、`【` → `[]`;框选回归:选中整段打 `"` → `"选中这段"`、打 `(` → `(选中这段)`。
37
+ ⑤ 判据:配对矩阵改成三列(键 / 开关开 / 开关关),空框、未闭合前缀、成对家族、上下文各表都按换算后形状断言;行为套件 142 → **143 项**,离线合计 196 项全过。
38
+ - **0.9.58**(2026-10-29,`57fdb56`):**复核上一版改动的作用域;全角括号开关如实标注(未落地真机)**。
39
+ ① 复核 `shouldPair` 放开后的副作用:真机「选中整段再打引号 / 打括号」仍是 `"选中这段"` / `(选中这段)`,框住字段的行为没有被触发;行为套件 142 项(新增全角开关一例)全过。
40
+ ② 全角括号开关(`/` 菜单「全角括号」):目标是「输入法给 ( 时输出半角 ()」。**文本域路径已实现并有单测**;真机 Lexical 路径**未落地** —— 探针读数 fwHalf=on 时仍是 `()`,且打 `(` 时 input 那条分支一次都没进(加过临时日志验证),beforeinput 接管的尝试会让它退化成孤零零一个 `(`,因此回滚。菜单文案已标「仅文本域」,不假装已支持。
41
+ ③ 结论:引号/括号两条根因修复(0.9.57)在真机成立:`"`→`""`、`abc(`→`abc()`;剩「全角强制半角」这一条是未完成项,不是已修项。
42
+ - **0.9.57**(2026-10-29,`0af9bc6`):**配对问题按根因修:旧设置粘住 + 括号规则与 VS Code 不一致**。
43
+ ① 根因一:旧版把「引号模式」默认设成标红提示,用户 localStorage 里存着的 `warn` 会一直粘着,升级后打引号仍只出一个 —— 看上去像没修。现在加设置版本(`dsh-composer-ease:schema`),迁移时清掉旧默认值留下的那一格,并在 `/` 菜单加「重置插件设置」。真机对照:预置 `quoteMode=warn` + 清 schema → 重载后存储被清(读出 null),打引号得 `""`。
44
+ ② 根因二:`shouldPair` 对括号在标识符/路径后**不补对**(`abc(` 只出一个),与 VS Code `editor.autoClosingBrackets: languageDefined` 相反 —— 这就是「括号不配对」。改成词/数字/路径后照样补对,仅在上一字符本身是成对符号时不补。真机:`abc(` → `abc()`、`(` → `()`。
45
+ ③ 判据:新增配对矩阵(10 个开符 × 期望文本,含全角家族)+ 上下文三例 + 标红模式两例 + 迁移两例;行为套件 **136 → 141 项**全过。
46
+ ④ 说明:真机打出 `()` 是输入法在中文态把 `(` 合成成 `(`,插件按全角家族配 `)` —— 属于输入法层,不是插件把半角改全角。
47
+ - **0.9.56**(2026-10-29,`1b51417`):**中文候选改成 VS Code 口径:从草稿里取词,分词交给词典**。
48
+ ① 之前的中文候选只由词典按前缀给,与「你在写什么」无关;VS Code 的 word based suggestions 是**从文档取词**。现在服务端多一个 `draft` 参数:把草稿尾巴(≤800 字)收进来,用词典做**正向最大匹配**分词(`segmentCjk`,最大词长取自词表 `CJK_MAX_LEN`,不写死数字),只把以当前前缀开头的词作为候选,来源标「本文」并排在词典候选之前;同 value 去重(草稿词优先)。
49
+ ② 客户端每次取候选都带 `&draft=`(草稿尾巴 800 字)。真机读数:请求 URL 里 `draft=` 存在且解码为「我今天去图书馆看书图书」。
50
+ ③ 判据:selftest 新增 1 条(`buildCandidates('图书','','我今天去图书馆看书…')` 首条=图书馆、detail=本文、同词只留一条);server-routes 新增 1 条(同一查询走路由同样成立)。离线 186 → **188 项**(selftest 16 / server-routes 14)。
51
+ ④ 未证实:真机弹层在「先写一句、再打前缀」这一拍里这一次读到 `display:none`(接口与路由都已证实有候选),留作待复验。
52
+ - **0.9.55**(2026-10-29,`1bb1c96`):**补领域词表:通用词典覆盖不到插件开发这套行话**(反馈:词典会不会太偏)。
53
+ 先把「偏不偏」量化:拿本仓 README / CHANGELOG / docs 里出现 ≥3 次的 1669 个中文片段去比词典,**能接出更长词的只有 5.5%**,而且排在前面的正是行话 —— 引号(108 次)、宿主(57)、弹层(44)、闭符(41)、开符、围栏、片段、标红、波浪线。jieba 那份是「日常生活」词典,不覆盖插件开发。
54
+ 因此新增 `lib/cjk-domain.js`(本仓自建,非 jieba 数据),约 70 条 3~6 字领域词(补全弹层 / 补全候选 / 引号未闭合 / 触发源 / 会核压缩 / 压缩上下文 / 插件市场 / 插件目录 / 渲染层 / 候选排序 / 弹层位置 / 宿主版本 / 闭符跳过 / 全角字符 / 标红提示 / 行内灰字 / 代码围栏 / 片段占位符 / 前缀匹配 / 快捷键冲突 / 上下文窗口 / 中文分词 / 词表命中 / 围栏标记 / 光标位置 / 选区替换 …),在 `cjk-lexicon.js` 里并进同一张表。
55
+ 读数:上述覆盖率 5.5% → **8.4%**(多数高频片段是「的了/我们」这类虚词,本来就不该补);**核心 22 个行话前缀 22/22 都能接出更长的词**(改前大半接不出)。真机:`插件` → 插件市场/开发/目录;`宿主` → 宿主版本/目录/重启;`引号` → 引号配对/未闭合;`波浪` → 波浪线/波浪线样式。
56
+ 新增判据:`wordCandidates(prefix)` 对 20 个核心前缀必须给出更长的词(selftest 14 → 15 项)。
57
+ - **0.9.54**(2026-10-29,`651de73`):**长词证明(>4 字)/ 发布包含 docs / 配对闸改成逐节解析**(三条复核意见)。
58
+ ① 上一轮只说「去掉长度上限」,但词典只有 2~4 字,等于无法证明。现在词表补 10 条 5~7 字常用词(中华人民共和国、数据库管理系统、自然语言处理、计算机科学与技术…),并加两条判据:`buildCandidates('数据库')` 必须含 7 字词、`('自然语')` 必须含 6 字词。真机读数:`数据库` → 数据库管理系统(7 字)、`自然语` → 自然语言处理(6 字)、`人工` → 人工湖 / 人工智能技术(7 字)。
59
+ ② `package.json` 的 `files` 漏了 `docs`,而 README 链到 `docs/boundaries-and-implementation.md` 与 `docs/边界与实现.md` —— 发布包里会成死链。已补 `docs`,并用 `npm pack --dry-run` 核对:两页都在包里(README 4.8kB / 4.4kB、两页 2.6kB / 2.2kB、总 38 个文件)。
60
+ ③ 双语 README 配对闸原来用「哈希字符串在文件里出现过」判定,两节哈希互换也能过。改成逐条解析成 `[{slug,en,zh}]` 后按序号比对:标题、英文正文、中文正文三格各自对齐。反向对照:互换两个小节的 en 哈希 → rc=1;复原 → 干净。
61
+ 复跑:真机矩阵 33 条全过(含 E9 两段包裹、C3 引号配对、D3 错拼兜底),离线 186 项(selftest 13 → 14)。
62
+ - **0.9.53**(2026-10-29,`27b5318`):**中文候选改为词典驱动(去掉长度上限)/ README 按官方模板瘦身并加双语配对闸 / package 元数据对齐官方插件**。
63
+ ① 边界不再写死:原来「汉字段落最多 4 字」「整串汉字不当候选」都是拍出来的上限,还得靠客户端 n 字切片兜着。现在客户端不再切汉字串(`docWords` / `contextWords` 直接跳过),中文候选只由词典(`lib/cjk-lexicon.js`,约 3 万词)按前缀给出,词长随数据走;`isCompleteWord(prefix)` 这两道前置闸(`wordCandidates` / `buildCandidates` / 路由 `wordGate`)一并换成「词典里有没有以它开头的词」。真机读数:`文档` → 文档管理;`系统` → 系统地 / 系统工程;`档管` → 弹层不开(wordGate=false);`变化` → 无更长词,收起。
64
+ ② README 从 221 / 297 行压到 **81 行**(英文主、中文副),改成 deepseek-harness 包 README 的模板:front matter `description` + `kind: package-reference`,小节 Summary / Table of Contents / Install / Use this package / Understand the implementation / Boundaries / Development and tests / Sources and licenses / Dev note;细节搬进 `docs/boundaries-and-implementation.md` 与 `docs/边界与实现.md`(各 36 行)。
65
+ ③ 新增双语配对闸 `tools/check-readme-pairing.mjs`:两份 README 的 `##` 小节数必须一致,且每节正文的 md5 记在 `README.i18n.yaml`(照官方的记录法);改一侧不重记就红,`--write` 重新生成。反向对照过:改中文某一节一个字 → rc=1;复原 → 干净。`check:all` 已纳入。
66
+ ④ `package.json` 对齐官方包:`exports` 改成条件对象(`{ "default": ... }`)、`files` 收窄并加入 `README.i18n.yaml`。改完重启 demo 宿主复跑真机:插件照常加载(`文档` → 文档管理,`档管` 不弹)。
67
+ 离线 185 项(行为 136)、真机 33 条全过。
68
+ - **0.9.52**(2026-10-29,`267751e`):**闭合引号后再打引号要成对 / 中文不再贴整句 / 标红只标那个引号**(三条一次反馈)。
69
+ ① 引号:旧逻辑在「光标前一个字符是引号且整段已闭合」时把这一键 `preventDefault` 吞掉 —— 反馈就是「其他引号都闭合的情况下,新打的引号没有成对」。现在按**奇偶**判:前一个引号是未闭合的开符才只插一个;已成对就交给 `shouldPair` 开一对新的。读数:`他说"你好"` + `"` → `他说"你好"""`(新对落下,caret 在新对中间);标红模式仍只插一个。
70
+ ② 中文候选:`docWords` / `contextWords` 原来把整串汉字也当候选,于是「上好」之后接着打「今天早上好」,弹层会把整句当提示贴回来。现在汉字串只切 2~4 字词块(docWords 补 4 字切片),整串不再入选。
71
+ ③ 标红:原来按**整行**画(实测那条线横跨 27px 甚至整行),还挂一句「引号/反引号未闭合」的 ::after 文字。现在只量**那个没闭合的引号字符**、把波浪线画在它底下(宽度=该字符,最少 6px),文字提示整条删除。读数:`"abc` 的波浪线宽度 6px、`::after` 计算值为 `none`、`data-dce-quote-hint` 为空。
72
+ 行为 133 → 135 项(新增中文整串两条),真机矩阵 33 条 × 1 轮全过。
73
+ - **0.9.51**(2026-10-29,`a2f92a5`):**多行选区能框住了**(反馈:一段话选中之后打引号框不住)。
74
+ ① 真因:0.9.46 为了避开「半截包裹」把含换行的选区直接放弃(`wrap-skip`);而用户选中的「一段话」里带换行(两段、或围栏整段)就整下失效。
75
+ ② 机制读数:整段贴 paste 时宿主会**只吃掉末尾的引号**(尾随空格/字母/零宽字符都保留,唯独引号消失);单字符 `insertText` 在宿主落定后(约 400ms)能稳定补字;同拍贴、下一拍贴、微任务贴都拿不到完整结果。
76
+ ③ 改法:多行选区分两步 —— 80ms 后贴「开符 + 内容」,再 400ms 后核对,缺闭符就单字符补上;补齐前后都做幂等检查(齐了就不动、找不到头就放手)。
77
+ ④ 读数:单段长句 `"…"`、两段 `"第一段↵第二段"`(连跑三次一致)、短词 `"abcd"` 全部包对;撤销:单行一拍、两段两拍(不再来回跳)。矩阵加一条 E9 覆盖两段选区(33 条)。
78
+ - **0.9.50**(2026-10-28,`eb6d555`):**英文 README 成主展示;用例补到 181 + 32 条**(要求:多测、README 默认英文)。
79
+ ① 双语 README 对调:`README.md` 现在是英文(GitHub 首屏、npm 页、登记表都读这一份),中文搬到 `README.zh.md`,两份首行互相链接。英文那份按当前行为重写(旧版还写着「弹层顶上那行出处」和 61 项测试)。
80
+ ② 文档闸加两件事:`README.zh.md` 也进行数预算与死链检查;新增「双语 README 必须互相链到」判据。反向对照过:把 `README.zh.md` 改名藏起来,闸立刻红(rc=1)。
81
+ ③ 离线套件 164 → **181 项**:`selftest` +3(`contextScore` 权重次序 README > docs > 普通文件、`buildCandidates('docum')` 首位是 `document`、空前缀候选封顶 ≤12)、`server-routes` +5(HEAD 与 GET 同权 / POST·PUT 405、非本机 Host 与跨站 Origin 各自单独拦截、未知子路径不命中、中文碎片过词义闸 `wordGate=false`、`lang=js` 归一到 javascript)、`client-behavior` +8(右方向键与 End 收弹层、四种注释行都不发请求、行内 `//` 照发、引号三连仍一对而反引号可三连成围栏、文字前打引号只落一个、textarea 形态多行包裹、弹层关着时 ↑↓ 不改文本、类型开关读写)。
82
+ ④ 真机矩阵 24 → **32 条**:新增边界组 8 条(`#` 注释行不弹、行内 `//` 照弹、文字前引号只落一个、反引号三连成围栏、引号三连仍一对、单行包裹撤销一拍、接受后弹层不复开、`@` 菜单 Tab 插入路径)。读数:32 条 × 4 轮 = 128 次检查,0 失败。
83
+ - **0.9.49**(2026-10-28,`4112e2f`):**真机用例矩阵入仓,六轮 144 条全过**(要求:再多轮用例测试)。
84
+ 新增 `test/real/matrix.mjs`:24 条用例 × N 轮,一条命令跑完并打印每轮的通过数与失败读数。分五组 —— 补全 6(英文/中文前缀、注释行不弹、引号内照弹、Ctrl+Space、`@` 菜单)、键位 6(Tab 接受、↓ 换灰字、← 收弹层、继续打字再提示、Home 收弹层、Esc 收弹层)、配对 7(半角/全角括号、默认引号配对、框选包裹、空对一次删两侧、末尾补闭合、未闭合波浪线贴文字行底)、`/` 菜单 3(三行设置、`/comp` 让位、`/compot` 兜底)、稳定性 2(弹层单实例、无控制台错误)。
85
+ 读数:3 轮 + 3 轮共 **6 轮 / 144 条检查 / 0 失败**;每轮 24/24。用例本身抓出一处测试口径错误(不是插件缺陷):默认配对后「未闭合引号」已经很难造出来 —— 直接打 `"` 会补成一对,得先把光标挪到已有文字**前面**再打(右边紧跟文字时不补对),波浪线才会出现;这条已写进用例注释。
86
+ - **0.9.48**(2026-10-28,`f93ff17`):**左右方向键收起提示;CI 同分支只留最新一次运行**。
87
+ ① 反馈:打 `node` 时发现前面打错,用 `←`/`→` 回车里去改,提示框一直挂在前面挡着。改法:`←` `→` `Home` `End` 一按就把弹层(含行内灰字)收掉,键本身照常交给编辑器挪光标;只收这一次,继续打字时 `onInput` 会重新拉候选。真机读数(demo 3081,打 `nod`):弹层 block → `←` 后 none、文本仍是 `nod`、caret=2 → 再 `←` caret=1 → 打 `e` 得 `neod` 且弹层重新 block;`Home` 后 none、caret=0;弹层关着时 `↑` 不动文本。
88
+ ② CI 噪音:`checks.yml` 加 `concurrency: group: checks-${{ github.ref }}, cancel-in-progress: true` —— 同一分支的新 push 会取消还在跑的旧运行,列表不再一串历史(双系统矩阵保留,Windows 那一格是上次抓出换行红的唯一手段)。
89
+ 行为 123 → 124 项(新增「左右键收弹层、继续打字再提示」)。
90
+ - **0.9.47**(2026-10-28,`9ab57b3`):**引号恢复成默认自动配对**(反馈:单独打一个引号应该得到两个)。
91
+ 改前默认是「标红提示」模式:空框打 `"` 只得一个字符,用户看到的就是「配不上对」。现在 `quoteMode()` 的默认值改成 `auto` —— 没有选区、前方也没有未闭合引号时,打一个引号补成一对、光标落在中间;「标红提示」退成 `/` 菜单里的显式开关。
92
+ 真机读数(demo 3081,localStorage 里没有任何 quoteMode):空框 `"` → `""` caret=1;`abc` 后 `"` → `abc""` caret=4;框选 `abc` 再打 → `"abc"` caret=5;`"abc` 末尾再打 → `"abc"`(补闭合,不叠第二个);反引号连按三下 → ```(围栏不受影响,反引号不在引号家族里)。
93
+ 契约用例同步改写:新增「默认 auto 空框打半角/全角引号得一对」两条,并把原来的 warn 契约用例显式设成 warn 模式再跑(矩阵那几条测的是标红档)。行为 121 → 123 项。
94
+ - **0.9.46**(2026-10-28,`de6afb2`):**多行选区不再做包裹;合成期那条用真输入法验完了**(承接 0.9.45 的两条疑点)。
95
+ ① 真输入法实测(不是合成事件):用 CDP `Input.imeSetComposition` 起一次真合成 —— 选中 `abcd` 后合成一开始,宿主就把选中内容换成合成文本(`abcd` → `ni` + 零宽空格,选区消失),随后打引号得到 `ni"`,与插件的处理无关。结论:合成期「用引号替代选中内容」是宿主/输入法的行为,插件这一侧无法保住已经被换掉的文本;0.9.44 那条补偿删除后不再加回。
96
+ ② 多行选区(围栏整段、跨行)的包裹改为**不做**:这台宿主的 paste 通道对含换行的负载会吃掉末尾闭符,补出来的闭符又要等下一次编辑器更新才重绘(实测 3 秒后仍缺,撤一下才出现),撤销还会在「有闭符 / 缺闭符」之间来回跳。读数:改前围栏整段得到 `"```js` + 换行 + `const a = 1`(缺闭符,撤销四次才回原句);改后原文一字不动。
97
+ ③ 单行、含空格、单引号、反引号、圆括号五个场景照常包裹,撤销**一拍**回原句(读数:`"abcd"` → `abcd`)。
98
+ - **0.9.45**(2026-10-28,`1e435c2`):**删掉未验证的合成期补偿;多行包裹补齐闭符**(承接 0.9.44 的三条读数复核)。
99
+ ① 0.9.44 里那条「输入法合成期放行」的补偿没有任何改前改后读数,本轮删掉。补实测:`compositionstart` 一到,宿主自己就把选中内容换成了零宽空格(事件轨迹里 `sel` 从 `abcd` 变成 ZWSP,时间点在按键之前),随后 `Shift+双引号` 自然是 `"​"​` —— 这条路径与插件的引号处理无关,之前的补偿也就不可能改变结果。
100
+ ② 多行选区的包裹原来**缺闭符**:`execCommand` 那条兜底把「开符 + 内容」落了,末尾闭符被吃掉(实测围栏里整段框起来,700ms 后仍是 `"```js` + 换行 + `const a = 1`)。新增 `finishWrap()`:落定后按 `开符 + 内层 + 闭符` 复核,缺闭符就补一个(单字符插入可靠),复核梯 60/200/450/900ms。读数:围栏整段包裹改前 `"```js\nconst a = 1`(缺闭符),改后 `"```js\nconst a = 1"`。
101
+ ③ 新增一条护栏:明明有选区却读不出文本(选区落在 chip / mention 这类非文本节点上)时,这一下**什么都不做**而不是插字符 —— 插字符必然把选区替掉,等于用引号吃掉选中内容。
102
+ ④ 九景回归(Ctrl+A / 空格串 / 单引号 / 反引号 / 圆括号 / 鼠标拖选 / 弹层开着 / 代码围栏 / 连按两下)全部包对。**残余**:多行选区(围栏整段)按 Ctrl+Z 时会在「缺闭符 / 有闭符」之间来回两拍才回到原句 —— 补闭符是第二笔历史,实测三拍序列 `"```js` → 缺闭符 → 有闭符 → 缺闭符;单行包裹仍是一拍。
103
+ - **0.9.44**(2026-10-28,`c56dc62`):**包裹只占一拍撤回 / 标红移到文字底下**(两条一次反馈,改前改后都在 demo 3081 上量过)。
104
+ ① 撤回里多出「三个引号、两个引号」两拍:包裹时插件插一次、宿主 Lexical 又按自己的键插一次,多出来的开符由 `repairWrapDup` 删掉 —— 删也是一次编辑,于是历史里留下两拍。修法:`beforeinput` 的框选分支在 `preventDefault` 之外再断传播(`stopImmediatePropagation` / `stopPropagation`),宿主那份监听收不到这次按键,就不会再插一遍。读数(输入法上屏那条路:`insertText('“')` 加全选):改前撤回序列 `“abcd”` → `““abcd”` → `“abcd”` → `abcd`;改后 `“abcd”` → `abcd` → `a`。`Shift+双引号` 同景改后同样是 `"abcd"` → `abcd`(一拍)。
105
+ ② 引号/反引号未闭合的波浪线原来画在**输入框底边**,等于给整个框描边;现在逐行画在**文字框底边**:新增覆盖层 `#dce-quote-warn-layer`,按文本节点量行盒子(行末 `br` 会撑满整行,所以只量文本节点)、同行合并成一条,`position:fixed` 贴 `bottom-3px`,随输入重画,闭合后整层清空。读数:改前波浪线 top=503、文字行底 476(差 27px);改后 top=476,宽度收到文字宽度。CSS 那条背景线撤掉,行内提示文字保留。
106
+ ③ 顺带记一条**未验证**的缓解:输入法合成期(`composing`)里选中文字再打括号,宿主会把选区替成一个字符(合成期这一拍读不到选区文本,包裹退化成用引号替代)。现在合成期只放行「有选区 + 成对符号」这一格,并借用 keydown 那拍记下的 `lastSelText`;本机 Playwright 造不出真实输入法时序,这一条只有逻辑、没有改前改后读数,不算已修。
107
+ 套件仍 162 项(行为 121),六闸全绿。
108
+ - **0.9.43**(2026-10-28,`86b7df3`):**命令名错拼兜底 / 设置行跟着查询 / Tab 不再吞原文 / 注释行不提示**(四条一次反馈,改前改后都在 demo 3081 上量过)。
109
+ ① 打 `/compot` 原来一条候选都没有:宿主的 `/` 菜单只用子序列打分(`rankByName`),`compot` 对 `compact` 的子序列断在第二个 o。插件现在向宿主的 `commandUi` 取一次真实命令表,宿主没排出来的命令按「跳 ≤2 个查询字符、≤2 个目标字符」补一行(标「错拼兜底」),选中补成 `/compact ` 交给宿主执行;正常拼写不重复出。读数:`/compot` → `/compact 压缩` 1 行(改前 0 行命令、只剩我们三条设置行);`/comp` → 宿主那行 `压缩 compact` + 技能行,我们不再插队;`/goal` 仍只有宿主一行。
110
+ ② 三条设置行原来不看查询、且立即 resolve:查 `/compot` 时宿主那组是空的,菜单里只剩我们三行;打开菜单时还先蹦出三行再被顶下去。现在三行按查询过滤、并等宿主命令组同一拍落地。读数:`/引号` → 只剩「引号模式」;`/comp` → 三条设置行让位。
111
+ ③ `Tab` 接受会把原文吞掉、要 `Ctrl+Z` 才找得回来。真因:contenteditable 这条路上「选 token → 插候选」交给 `execCommand`,而宿主 Lexical 有时不认我们摆的选区(实测打「文档」再 `Tab` 得到「文档管理文档」,多出一份原文)。改成**落一次 → 核对整段文本 → 不对就 undo 这一笔**,原文留在原地、弹层也留着换一条:接受要么正确,要么什么都没发生。读数:`文档`+`Tab` → `文档管理`(改前 `文档管理文档`);`fun`+`Tab` → `function` 照旧。
112
+ ④ 注释行里不再提示:光标所在行以 `//`、`#`、`<!--`、`--` 开头(允许缩进)时整行不弹;行内 `//` 与 `http://fun` 照旧弹,引号里的 `"fun` 也照旧弹(那一侧是字符串,正是要补的地方)。五景读数:`// fun`、` // fun`、`# fun`、`<!-- fun`、`-- fun` 全部不弹(改前全弹)。
113
+ 顺带记一条宿主编辑器的硬限制:`execCommand('insertText')` 在**含空格**的串上静默不落字(`a{}`、`a(b)` 能落,`a b`、`foo bar` 不落),`paste` 通道又会异步落到文末,所以多词片段按 `Tab` 只会保持原样(不吞原文)。已写进 README「已知边界」。套件 158 → **162** 项(行为 117 → 121:错拼兜底、查询过滤、注释行三组)。
114
+ - **0.9.42**(2026-10-22,`048e382`):**候选质量三处修正**(打 10 个常见前缀实测后改)。① 客户端:草稿/对话里出现过的词不再无条件领跑 —— 2 字前缀下它们排在服务端候选之后,实测 `de` 首条从 `DeepSeek` 变回 `delete`。② 服务端按来源分档:语言关键字 450 > 常用文件名 445 > 英文常用词 440 > html 标签/属性 425 > 生僻词典词 380;`co` 由 `col/code/come/color` 变为 `const/continue/CONTRIBUTING.md/col`。③ 短前缀(≤3)剔掉不在常用词表里的生僻词、且 2 字前缀必须前缀匹配:`ty` 由 `type/typeof/tye/tyde` 变为 `typeof/type`。3 字前缀仍保留 VS Code 的模糊语义(`abc`→`axbxc` 那条用例照旧通过)。套件 160 项全过。\n- **0.9.41**(2026-10-22,`8b5acbc`):**散文问题真正处理 + 按宿主模型补契约**(问:散文的问题真的有处理吗)。答案:之前没有。本轮量化后清零:注释里的日期 120 处、第一人称痕迹 70 处、踩坑叙事词 46 处,全部改为中性技术表述;两块超长内联注释压到 8 行内。新增 `tools/verify-comment-prose.mjs`(`npm run check:prose`)作为常驻闸。**阈值按实测校准,不是拍的**:抽 deepseek-harness 401 个源文件、3210 个注释块,块长中位数 2 行、p75=5、p90=8、p95=11,注释行均长 51 字符;因此本仓取「正文注释块 ≤8 行 / 文件头 ≤25 行 / 单行 ≤120 字符」,并豁免 vendored 的 `lib/fuzzy.js`。第一版把上限定成 3 行,按这个分布等于自造一条宿主自己都不守的规矩。按宿主的「一条规范配一个可运行 verify-*」模型补 `CONTRIBUTING.md`(契约表:每条规矩 → 执行它的命令)与 `AGENTS.md`(禁止事项);`check:all` 纳入 `check:privacy` 与 `check:prose`。多人协作项按要求暂不引入。
115
+ - **0.9.40**(2026-10-22,`c6ba057`):**重写全部历史脱敏**(决定:方案 B)。用 `git filter-repo --replace-text` 把历史里的对话原话引述、本机绝对路径、本机临时目录约定、宿主本机构建路径替换成中性表述;119 个提交全部保留(sha 全变)、tag `v0.5.0` 一并重写、CHANGELOG 里 57 处旧 sha 按 `commit-map` 重新对齐。两处踩坑:① 路径规则必须要求「盘符后 ≥2 段」,否则会把测试夹具里的假路径也换掉,`client-smoke` 的「结尾是标识符就不配对」断言当场变红;② 隐私闸与洗稿器这两个工具文件本身写着规则模板(含被替换的字面词),重写后必须从工作副本恢复,否则闸会变成搜索被替换后的词,等于闸自己失效。验证:从远端重新 clone 后跑全历史扫描,第一人称引述的残留仅 9 处且全部位于这两个工具文件里(是规则模板本身,不含任何对话内容或个人信息);`A_Workspace` / 用户目录残留 0;五闸与 160 项套件全绿。顺带把「排序」相关文档与 README 示例也脱敏(去掉内部项目名与逐字反馈)。
116
+ - **0.9.39**(2026-10-22,`2fdae8d`):**收成一份:宿主目录改成本仓的 junction**(反馈:本地工作区常出现版本不同步,要求「只保留一个可回退版本、一份云端、一份现役」)。查出真因:VS Code 工作区打开的是**宿主目录**,而提交发生在**另一个工作副本**,两者不是同一个东西,所以「永远看不到更新」。现在**宿主插件目录**(profile 里 `link:` 指向的那个)是指向本仓的 junction(`mklink /J`),物理上只有一份:改代码的地方就是被宿主加载的地方,即使忘了同步也不会不一致。真机验证:从 junction 启动 demo 宿主,打 `docum` 正常弹出三条候选(document / documents / …/documentation.md)。新增 `tools/link-host.mjs`(`npm run link:host`):报告当前关系;`--apply` 时把原目录留作 `.hostbak`、建 junction、验证读得到 `package.json` 与 `.git`,失败自动回滚。同时清理冗余副本:旧临时工作副本、废 clone、空目录三处(删前记了 bytes+sha256 清单);唯一保留的回退包在 `test/rollback-dsh-composer-ease/dsh-composer-ease-v0.9.38.zip`(65 文件 / 954 KB)。
117
+ - **0.9.38**(2026-10-22,`2aab447`):**多副本一致性闸 + VS Code 工作区任务**(反馈:更新插件后本地工作区常出现版本不同步)。本机真相:真副本只有**两份** —— 公开仓工作副本(改代码)与宿主插件目录(两个 profile 用 `link:` 指向它,真正被加载的是这份);profile 里的 `node_modules/<pkg>` 只是指向宿主的链接,不会独立漂移。新增 `tools/copies.mjs`:列出所有副本(路径 / realpath / 版本 / 是否链接),`--check` 比对版本与运行时文件内容,`--sync` 把源推进到实体副本;`npm run check:copies` 已并入 `npm run check:all`(无副本环境自动跳过,CI 不会红)。新增 `tools/host-dir.mjs`:宿主目录从 profile 的 `dependencies['dsh-composer-ease'] = 'link:<dir>'` **自动发现**,`DSH_PLUGIN_HOST` 只作覆盖 —— 不再把本机路径写进脚本(既泄隐私又不可移植)。`tools/sync-host.mjs` 同步改用该发现,并**把 `docs/**` 纳入同步**(此前宿主那份 README 引用的 banner 一直是旧的)。新增 `.vscode/tasks.json`:同步到宿主 / 检查副本漂移(打开文件夹即跑)/ 跑全部闸 / 重建 bundle。
118
+ - **0.9.37**(2026-10-22,`b462fbb`):**公开仓脱敏 + 新增发布前隐私闸**。问题:本仓是 public,而历史提交与代码注释里带着对话原话引述(`某某 … 「…」` 这类写法)与本机绝对路径,相当于把私人对话与机器信息一起推到了公网。处理:新增 `tools/scrub-public.mjs`(洗稿器,`--dry-run`/`--apply`)——整段引述吞成「某日的一次反馈」、本机绝对路径换成占位、本机临时目录约定改成中性词、私有配置路径与宿主本机构建路径同样处理;`test/` 下只洗引述、不动路径(那里的假路径是测试夹具,洗了会把用例洗红)。本次改写 **27 个文件 / 199 处**。同时移除内部的 `docs/HANDOFF-20261020.md`,并把被洗坏的四处功能性默认值(安装示例、`sync-host` 与 `record-demo` 的路径默认值)改成环境变量必填 + 明确报错。新增 `tools/check-privacy.mjs` 隐私闸(`npm run check:privacy`):公开仓不得出现对话引述、本机绝对路径、本机工作区约定、私有配置路径;`test/` 只查引述。**已知残余**:git 历史里仍有约 185 行含旧引述(HEAD 已干净);要彻底清掉需重写历史或转私有仓,待决定。
119
+ - **0.9.36**(2026-10-22,`a222a82`):**banner.svg 重画**(一次反馈)。旧 banner 是错的:标题写着 `dsh-composer-pro`(插件叫 `dsh-composer-ease`)、只讲三类候选、右下角文字溢出卡片、示例还是内部路径 `临时目录/attrib-index.diff`。新 banner 用主流首屏结构:右上身份标签(DSH 插件 · MIT)→ 名字 → 一句话 → 四个能力胶囊(自动配对/环绕选中/四类补全/标红提示)→ 一行事实(零依赖·不改源码·一条命令)→ 右侧输入框示意(`docum` + 灰字 `ent` + 光标)与补全弹层(三行、首行高亮带左侧强调条)→ 左下红色波浪线示例;1200×300,纯 SVG 无外链。另记一条判据(写进 `docs/如何做 README 图.md`):**改 SVG 必须用 Chromium 渲染成 PNG 看一眼** —— ImageMagick 对等宽/中文回退字体支持差,直接 `magick banner.svg` 本机实测 0 字节输出。
120
+ - **0.9.35**(2026-10-22,`9ed01f5`):**动图改用录制工具生成,不再手工拼**(一次反馈)。新增 `tools/record-demo.mjs`:Playwright 边操作边抓帧(帧带真实时间戳)→ **gifenc**(纯 JS GIF 编码器)用全局调色板量化写帧、`dispose: 2` 只存帧间差异;两个压缩旋钮 `--colors`(64 会偏色,实测红色波浪线变橙;128 够用)与 `--stride`(抽帧,延迟累加所以播放时长不变)。产物 `docs/demo.gif` **428 KB / 57 帧 / 648×335**,替掉旧的 349 KB 手拼版本(`demo-small.gif` 已删),内容换成真流程:打 `def` 弹补全 → ↑↓ 换选中(灰字跟着走)→ `Tab` 接受 → `docum` 英文词与文件混排 → 半个引号波浪线。**为什么不用 ffmpeg**:本机取 `ffmpeg-static` 被加速代理的 TLS 中间人挡住(`UNABLE_TO_VERIFY_LEAF_SIGNATURE`),gifenc 只走 npm registry。另记一条剧本教训:第一版用三反引号开围栏,反引号被插件自己配对,剧本跑偏(录出来只剩一个 `d`)。教程 `docs/如何做 README 图.md` 补「用专门工具录」一节;`package.json` 加 `record:demo`。
121
+ - **0.9.34**(2026-10-22,`2ce2e12`):**README 重做 + 图片瘦身 80%**。① 首屏改成「一句话 + 横幅 + 四个真机镜头表」:补全+行内灰字 / `/` 菜单(三行)/ 引号未闭合波浪线 / 括号自动配对,全部按 2 倍缩放拍真机、裁到输入框周边(`docs/shots/*.png`,共 245 KB)。② 动图 `docs/demo.gif` **1.78 MB → `docs/demo-small.gif` 349 KB(−80%)**:`-coalesce -resize 560x -fuzz 3% -colors 64 -layers Optimize`。③ README 重排:新增「快速开始」「它能做什么」,删掉与前者重复的「概览」节。④ 新增 `docs/如何做 README 图.md`:拍图脚本要点、裁剪尺寸、压 GIF 的两个坑(不 coalesce 取帧只得 1×1;先 coalesce 再 resize 反而变大)、写作约定与验收命令。⑤ `tools/check-docs.mjs` 修假阳性:行内代码(反引号)里的 `[x](y)` 语法不再被当成死链。
122
+ - **0.9.33**(2026-10-22,`3269a98`):**候选类型开关收成一行预设**(一次反馈)。四个单类型开关(词/文件/模型/片段)合并成一行「候选类型:全部 / 无文件 / 只有词 / 全关」,点一下循环切换,label 里写当前态与下一态;底层仍是四个 kind 的开关(`localStorage: dsh-composer-ease:kinds-off`),想任意组合仍可手改。`/` 菜单回到 3 行(引号模式 / 候选类型 / 严格配对),新增用例断言这个行数。行为 116 → 117。
123
+ - **0.9.32**(2026-10-22,`19f4ae4`):**候选类型开关 + 片段占位符(部分)**。① 类型开关(等价 VS Code `suggest.show*` 里我们有的四类):`/` 菜单加「候选类型:词/文件/模型/片段」四项,存 localStorage,在候选合并那一处过滤(索引/灰字/Tab 都跟同一份列表);真机验证:关掉 词+文件 后打 `console.log` 只剩片段一条。② 片段支持 `${n:默认值}` 占位符:客户端 `expandSnippet` 展开并算落点,接受后光标落到第一个占位符;真机验证:打 `console.log` 接受 → `console.log(value)`、caret=17(`value` 之后)。③ 顺带修掉两个真 bug:接受时用的是**过期的 `lastToken`**(打 `console.log` 只回删 `log`,`console.` 留在原地)→ 改为接受这一拍现算;落点基准从「插入前光标」改成「替换区间起点」。④ 片段一律改**单行** —— 多行在这台宿主上插不可靠(Lexical 模型选区不认 DOM Range)。**残余**:两个以上占位符之间的 Tab 跳还没验成(打 `function` 那条 Tab 不接受,原因未定位),已记待办。
124
+ - **0.9.31**(2026-10-22,`d8b47f0`):**行内预览跟着方向键实时走**(一次反馈)。真因:`showGhost` 固定取 `popup.items[0]`,所以上下键换了选中、灰字不动;顺带发现 `lastPrefix` 是个只声明没赋值的死变量。改法:`showGhost` 取 `popup.items[popup.index]`,token 缺省时从光标处现取(`tokenBefore(textBeforeCaret(ed))`);方向键分支与悬停分支换选中后各调一次 `showGhost`。真机读数(demo 3081,打 `docum`):初始灰字 `ent`(document) → ↓ `ents`(documents) → 再 ↓ 收掉(第三条是文件,前缀不符)→ ↑ 回 `ents`。行为 112 → 113(新增「灰字跟着选中项走」用例)。
125
+ - **0.9.30**(2026-10-22,`0ccc090`):**借 VS Code 的 suggest 语义 + 借 DSH 的「每条约定一个闸」**。输入框:对照 `microsoft/vscode` 的 `editorOptions.ts`(本机取证 44 个 `suggest.*` 选项),已跟进 8 项,本次补 `matchOnWordStartOnly` —— `kind=word` 且前缀 ≥4 时必须词首匹配(实测 `libr` 原来只给 `libra`/`libri` 这类冷词)。管理:新增 `tools/check-docs.mjs`(等价 `verify-md-links` + `verify-doc-budgets`:死链为红、README ≤400 行 / docs ≤600 行、禁 TODO);`.github/workflows/checks.yml` 合并成一份并改成 ubuntu+windows 矩阵(原 `ci.yml` 与另一条会话早建的 CI 重复,已删);新增 `.editorconfig`、PR 模板、issue 模板(bug/feature);`npm run check:all` 聚合五闸。对照表与「还缺什么」(片段占位符导航、按类型开关、shareSuggestSelections)见 `docs/借鉴-VSCode与DSH.md`。
126
+ - **0.9.29**(2026-10-22,`7fc6691`):**候选压噪三件 + 两条机制闸**。① harness 目录降权:`deepseek[-_]harness` / `dsh-(fixes|kit|shim|020rc2|017)` / `node_modules` / `plugins/` 一律 -2000(压到 0 分以下);② 上限 12 → **8**,并按类型留配额(词 ≤6、非词 ≥2)——整段分区 + 上限 8 会把文件全饿死,单测照出来了;③ **不做相对分数截断**(试过 50% 阈值,与 fuzzyScore「负分也列出」的语义冲突,还误删合法路径,已撤并写明理由)。显示层把 harness 路径折叠成 `dsh: <文件名>`(只影响显示,插入仍是完整路径)。新增两条机制闸:`tools/check-dup.mjs`(同名顶层函数/跨文件 12 行重复块,按浏览器·服务端半边分域)、`tools/check-refactor.mjs`(距上次 `refactor:` 超 40 条、或最近 40 条 fix 占比 >55% 即红);两者进 `npm test`,另加 `npm run check` 一次跑四闸。读数:`de` → def/del/debug/decide(无 harness 文件)。
127
+ - **0.9.28**(2026-10-22,`58b044a`):**client.js 拆成 8 个片段 + 打包步骤 + 结构闸**。改前读数:`lib/client.js` 1629 行 / 80 函数 / 最长 178 行,84 次提交里 `refactor` 0 次。拆法按语义:10-foundation / 20-pairing / 30-popup / 40-language / 50-caret / 60-keys / 70-input / 80-apply + `_head`/`_tail`,最长片段 353 行;`tools/build-client.mjs` 按序拼回宿主加载的那一个 `lib/client.js`(宿主只认 `exports["./client"]`,不做多文件解析),支持 `--check`;`tools/check-size.mjs` 是**结构闸**(片段 ≤400 行、手写文件 ≤600 行、生成物必须与片段一致),已进 `npm test` 第一步。切分是连续行区间切,拼接后与原文**非注释差异 0 行**;五支套件 153 全绿。
128
+ - **0.9.27**(2026-10-22,`4524d8f`):**T2 真英文词表 + T3 本机领域词 + 词优先分区**。新增 `tools/gen-lexicon.mjs`(幂等,生成物不手改):T2 取 `an-array-of-english-words@2.0.0`(MIT,274,937 词)留 3-12 字符、**用本机语料频次排序**取前 30k;T3 从文档语料统计高频词,只收「词库没有 + 不是任何词的前缀 + 不含下划线」的 66 个。手写常用词 518 条独立成 `lib/en-common.js`(唯一该手改的词表)。排序:`enMatches` 三级(常用词 > 语料名次 > 短者);词候选按整词/常用/其他给 ctx 460/440/400(整词只给 `isCompleteEnWord`,否则 `docum` 会吃满分);`rankCandidates` 末尾**词优先分区**(prefix 匹配 `^[A-Za-z][A-Za-z0-9-]{2,}$` 时词整段排在文件前)。读数:`dsh`→`dsh/本机词`(不再是 deepseek-harness 的文件)、`gh-`→`gh-archive/本机词`、`docum`→`document`、`inp`→`input`。
129
+ - **0.9.26**(2026-10-22,`94a0591`):**英文侧补词表与「完整英文词」判据**。中文靠 `isCompleteWord()` 分碎片,英文侧原先只有 words.js 关键字表、没有词也没有判据。新增 `lib/en-lexicon.js`:`EN_LEXICON`(关键字表 ∪ 手写开发语境词,**1237 词**)/ `enMatches` / `isCompleteEnWord` / `isWordPrefix`,接在 `wordCandidates()` 末尾(出处让 words.js 优先);**不做中文那种碎片清零** —— 英文前缀正是补全的用法,只在「本身已是完整词」时加 10 分。读数:`docum`→`document`(压过 documentation.md)、`dele`→`delete`、`inp`→`input`(出处仍 words.js)。`SOURCES` 补中文词典与英文词表两条出处。路由 7 → 8;扩法(T1/T2/T3、体积预算、闸)见 docs 第六节。
130
+ - **0.9.25**(2026-10-22,`020b59b`):**方向键真改选中 / 点击按行取下标 / 中文词典接入**。① 上下选原先排在 `keyAction` 之后(keyAction 对箭头返回空,`if (!act) return` 先返回),且 `renderPopup` 的签名不含 index、非 force 时首行早退 —— 两处一起改后真机高亮 0→1→2;选中行改用可见色(主题 hover 实测 `rgba(38,49,72,.06)` 等于看不见)。② `row.onmousedown` 读 `popup.index`(靠悬停更新),无 hover 时点谁都接受第一条 → 改成先设成被点那一行;悬停也不再重建整层。③ `lexiconCandidates()` 在 `lib/index.js` 里一直是死代码,中文词表从未进过候选池 → 在 `wordCandidates()` 接上(真机:「文档」→「文档管理 · 词典」)。④ 新增 `docs/参考-候选排序与CHANGELOG体例.md`。路由 6 → 7、行为 110 → 112。
131
+ - **0.9.24**(2026-10-22,`329978a`):**标红改波浪线;撇号不再误报**。① 打 `let's` 这类英文撇号会被判成「引号未闭合」——新增 `unclosedQuoteCount()`,被词字符夹着的 `'` / `’` 不计入,lint 改用它(读数:`let's` → 不标红;`"你好` → 标红;`"你好"` → 闭合)。② 标红原来是 `box-shadow: inset 0 -2px 0 0` 一条**直线**压在框底,改成 8×4 SVG 正弦波平铺(`background-image` + `repeat-x` + `left bottom`),即 VS Code 那种波浪。与 0.9.23 的两档引号模式合并:保留 quoteMode/lintQuotes,另把 `repairWrapDup`(包裹后清理 Lexical 多插的开符)留在 `__helpers__`。行为 109 → 110(新增撇号用例)。
132
+ - **0.9.23**(2026-10-23,`a29385d`)新增「常用文件名」候选池(61 条:README.md / package.json / 本地配置 / CHANGELOG.md / tsconfig.json / Dockerfile / .gitignore / src / docs …),排在语言词表之前 —— 用户向 AI 提问时被反复点名的高频词不只有语言关键字,`rea` 现在第一条就是 README.md。
133
+ - **0.9.22**(2026-10-23,`a29385d`)引号本轮三修 + 一个开关:① 闭符不再 setCaret 到全文末尾,改为在**光标处**字面插入(修「后引号跑到句子后面」);② 新增 `quoteMode`,默认 `warn` —— 不自动补闭引号、未闭合给输入框标红提示(VS Code squiggle 口径),`auto` 保留严格档补对;③ 修 `quoteLintTimer`/`lintQuotes` 的块级作用域 bug(标红之前永不生效);④ warn 模式不再吞键,连按三下引号得三个;⑤ `/` 菜单新增「引号模式」开关。
134
+ - **0.9.21**(2026-10-22,`d833a5d`):**引号恢复自动配对**(2026-10-22 的一次反馈)。改 `shouldPair`:严格配对开 → 一律补对;关(默认)→ 也补对,但按 VS Code `editor.autoClosingQuotes` 的 beforeWhitespace 口径收窄(右边紧跟文字不补、左边已是成对符号不补)。另加真机兜底:**空对里再打同一个引号一律跳过**(按文本形状判,不看光标——快速连打时上一键的复位还没落地,否则会变成三个);反引号不受影响,``` 围栏仍能连打。真机读数(demo 3081):空框 `"` → `""` caret=1、连打两下仍是 `""`、打引号后立刻打字落在两符之间、`abc` 末尾 → `abc""`、最前 → `"abc`(护栏)、中文后 → `你好""`、全角 `“` → `“”`;真机五场 6/6。离线:自检 10 / 路由 6 / 冒烟 13 / 行为 107 / 合规 10(契约相关 7 条用例按新行为改写)。
135
+ - **0.9.20**(2026-10-22,`ff15a0e`):**「严格配对」从技能下面挪到 `/` 菜单最上面**(2026-10-22 的一次反馈)。成因:宿主按 `order` 升序排触发源,内置「指令」= 1、「技能」= 2,我们那组原写 `order: 20`,必然垫底;改成 `order: 0`,排在「添加/指令」之前。`__helpers__` 暴露 `buildTriggerSources`,`client-smoke` 补一条判据(`/` 组 order 必须为 0、`@` 组必须在其后),冒烟 12 → 13。离线五支全绿(自检 10 / 路由 6 / 冒烟 13 / 行为 107 / 合规 10)。真机读数(`dsh --profile demo --port 3081 --no-open` + Playwright,探针 `test/real/menu-order.mjs`):`__DSE_TRIGGER_SOURCES__ = 2`;`/` 菜单自上而下 `便捷输入框(top 88) → 严格配对(122) → 添加(152) → 指令(321) → 技能(487)`,我们那组已是第一组。注:3080 主宿主没重启(使用中 2026-10-21 定),线上要等它下次重启才生效。
136
+ - **0.9.19**(2026-10-21,`2bd8fdf`):按上游登记表 PR #40 的评审补齐发布元数据 —— `package.json` 增加 `engines.node >=20` 与 `dsh.compatibility.dsh >=0.2.0-rc.2`(宿主可据此对不兼容版本告警);`cordis.patch.yml` 的安装示例从本机绝对路径改为占位符;README「已知边界」补一条判据:空输入框里的 `<p><br data-lexical-managed-linebreak>` 是编辑器的空文档表示,**判空要看 `textContent`,不要用 `innerText`**(后者会把空段落报成换行),手动删除该段落会在 100 ms 内被重建(三次取样实测)。行为无变化,离线五支 145 项全绿。
137
+ - **0.9.18**(2026-10-21,`a7dbb23`):**引号不再自动补闭符**(真机 4 个引号 → **2 个**)。原因:自动补对要插件插两个字符、再用 DOM Range 把光标塞回两符之间,而 DSH 输入框的 Lexical **模型选区不认 DOM Range**,快速连打时下一键按它自己的选区落字,于是冒出 `“”你好""` 这种多出来的引号(本轮实测:`__lexicalEditor` 在节点上确实存在,但模型操作要用内部 `$` 函数,浏览器侧插件拿不到)。改法:`shouldPair(before, after, ch)` 对引号返回 `pairStrict()` —— **默认只插一个**,闭符由用户打时按家族奇偶判定再补;开了「严格配对」(`/` 菜单里我们那组)才恢复老行为。测试契约整套重写:空框/Shift/beforeinput 三条改为单插、行为矩阵按家族分账(括号补对、引号单插),新增「严格配对开启后恢复补对」。真机读数:`全角“ + 你好 + 半角"` → `“你好"`(2 个);回归 `"abc"` 再打 `"` → 仍 2 个;框选四景 W1–W4 全部不变。离线五支 **145 项全绿**(behavior 103 → 107)。README「已知边界」已写明原因与开启方式。
138
+ - **0.9.17**(2026-10-21,`023636a`):**注册 `/` 触发源,我们那组进宿主命令菜单**。`buildTriggerSources()` 增加 `slashSource`(trigger `/`、分组名「便捷输入框」、order 20);组内首条是**「严格配对」开关**:读 `localStorage['dsh-composer-ease:pairStrict']`(默认 false),描述里显示当前状态,选中即翻转并返回 `{ text: '' }` 清掉输入框里的 `/`。`pairStrict` / `setPairStrict` 已暴露进 `__helpers__` 并加一条离线用例(默认关、读写不抛错,client-smoke 11 → 12)。真机读数(demo 3081):`__DSE_TRIGGER_SOURCES__ = 2`;`/` 菜单分组标题 `["添加","指令","技能","便捷输入框"]`;点「严格配对」后 `localStorage` 由 `null` → `1`,再次打开菜单文案变为「当前:开」。**已知待修**:点击后输入框残留一个换行;同一菜单内第二次点击未生效。离线五支 141 项全绿。
139
+ - **0.9.16**(2026-10-21,`b36787a`):**按被录用插件的做法注册宿主输入触发源**。`@` 这一路不再由我们自己的弹层提供,而是在宿主菜单里新增一组「便捷输入框」:`package.json` 的 `dsh.client.inject` 声明 `@deepseek-ai/dsh-client-ui-input-trigger`、`exports.inject = ['inputTriggers']`、用 `ctx.inject(['inputTriggers'], scope => …)` 取服务(照 `dsh-client-ui-input-trigger/lib/client.js:1254` 的官方用法;`ctx.get` 在 apply 时取不到),候选仍走既有只读路由。同时修掉一个真 bug:路由返回的键是 `candidates`,`fetchItems()` 误读 `items`,导致候选恒为 0 条。真机读数(demo 3081):`__DSE_TRIGGER_SOURCES__ = 1`,`@rea` 菜单分组标题 `["文件与文件夹","对话","便捷输入框"]`,条目 81 → **129**(我们贡献 48 条)。离线五支 140 项全绿(client-smoke 的 inject 期望同步改为 `['inputTriggers']`)。
140
+ - **0.9.15**(2026-10-21,`3ed5c10`):**行首 `/` 让给 DSH 原生命令系统**,插件不再抢。一次反馈。查证:DSH 原生命令链路是 `dsh-commands`(宿主侧命令注册表)+ `dsh-native-command` + `dsh-command-goal/compact/feedback` + `dsh-client-ui-commands`,会话侧入口为 `remote.commands.execute(sessionId, line, [])`(事件 `command/run`、`command/done`),而插件此前在 `onInput` 里对 `/` 弹自己的候选,与原生菜单叠在一起。现在 `/` 与 `/xxx` 一律不拉候选、不弹层,`@` 与普通词照旧。真机读数(demo 3081):输入 `/` 与 `/dsh` 插件路由请求 **0 次**、我们的弹层不可见;`rea` 仍弹层、`@rea` 仍发请求。离线五支 **138 项全绿**(behavior 100 → 101)。
141
+ - **0.9.14**(2026-10-21,`5777575`):**框选(环绕选区)当"一个动作"处理**。使用中反馈两条:① 框选时会先露出一个引号再冒出补全提醒;② 框选应当可以反选(同一个引号再打一次就取消这次框选)。修法:新增 `wrapSelection()`,框选期间压住补全(`suppressInput` 用后立刻恢复,不泄漏给下一键)并记下范围;新增 `tryUnwrap()`,再打同一个对称引号时**一次选区替换**成内层内容(不做两步删除,避免 Lexical 中间态错位),内层重新选中,位置口径对不上时按「开符 + 内层 + 闭符」在全文兜底定位、被改过就放弃;`deleteAt()` 拿不到 `HTMLTextAreaElement.prototype.value` 描述符时退化为直接赋值(原先抛异常被吞 = 什么都没删)。真机读数(demo 3081):选中 `abc` 打 `"` → `"abc"` 期间 **popup/灰字全 false**;再打 `"` → `abc`;反选后打 `X` → `X`(内层确实重新选中);`"你好"` → `你好`。离线五支 137 项全绿(behavior 97 → 100)。
142
+ - **0.9.13**(2026-10-21,`f36e947`):文件候选改成「**当前工作区优先**」。上一版只按「常用作上下文」打分,真机打 `rea` 时排上来的还是别的项目的 README —— 真因有两处:`workspaceRoots()` 按对象顺序取前 3 (不是最近动过的那个),且 `pickCandidates` 排序只按 `fuzzyScore`,跨工作区的上下文分在查询那一步就丢了。现在:`workspaceRoots()` 按 `workspace.json` 的 `updatedAt` 倒序;当前工作区文件 +200;`ref_repo`/`third_party`/`external`/`_upstream` 里的 README 扣 500 且整目录进 `SKIP_DIRS`;排序加 `ctx` 作为并列次序。真机读数(demo 3081):打 `proj` → 候选 2 条、第 1 条是当前工作区的 `PROJECT_MEMORY.md`;打 `rea` → 8 条全是真项目文件、含中间态目录 0 条、仓库副本已消失。`selftest` 里 `collectFiles` 的断言改成按 `rel` 取值(返回结构变了)。离线五支 134 项全绿。
143
+ - **0.9.12**(2026-10-21,`770ad8e`):文件候选改成「常用作上下文优先」。`lib/index.js` 新增 `contextScore()`:顶层 README/CHANGELOG/AGENTS/CONTRIBUTING/package 1000 分、同名非顶层 700、顶层其它 md 500、`docs/**/*.md` 400、其它 md 300、其余按修改时间;`collectFiles()` 先排序再取上限。`SKIP_DIRS` 增加 `临时目录`/`临时目录`/`coverage`/`out`/`vendor`/`.cache`/`.turbo`/`backups`/`storages`/`sessions` 等 —— 一次反馈。README 补「概览」「两个常用场景」「文件上下文候选怎么排」三节。真机读数:打 `rea` 后首位就是 `README.md`,含中间态目录的候选 0 条。离线五支 134 项全绿。
144
+ - **0.9.11**(2026-10-21,`8de8b35`):修「快速回车后修字提示残留」——补全候选的响应比回车晚到时,旧实现会把浮层与灰字画回已被清空的输入框;现在回车无条件 `closePopup()`(代数 +1 作废在途响应),`showGhost` 在浮层已收起时直接返回。离线 134 项 + 真机 3081(/words 延迟 900ms)6/6,两处都做了反向对照。
145
+ - **0.9.10**(2026-10-21,`4d93819`):治**手快那一拍的顺序问题**(一次反馈)。成因是一类**读到旧状态**:上一键挂起的光标复位还没落地,下一键就来读 `caret`/`before` 了 —— 空框打 `"` 得 `""` 之后紧接着再打 `"`,因为光标还没挪到两符中间,第二下会当成字面插入,于是多出一个。修法:`keydown` 与 `beforeinput` **先就地 flush 上一次挂起的光标复位,再读状态**(`flushCaretFix` 本身幂等:文本不匹配就什么都不做)。真机回归补 4 条**手速**场景(0ms 连按两下 / 0ms 连按三下 / 打完 `"` 立刻上屏中文 / 打完两下立刻上屏中文),15 → **23 项**。
146
+ **说清楚**:这一条在本机用 Playwright **复现不出来**(它的两键间隔总比真实手速宽),所以只能算「堵住一类已知会读旧状态的顺序」,不是「已复现的故障被修好」。判据上我做了反向对照(把两处 flush 去掉)——在 Playwright 的时序下 15/15 仍然全绿,说明**这条修复没有被我手上的仪器证伪过,也没有被它证实过**。如果你再看到多出来的引号,请回我一句「还在」,我会改用「配对插入整体原子化」那条更重的路。
147
+ - **0.9.9**(2026-10-21,`e86daf1`):修**使用中报的那条真正的路径**——「先打『你好』→ 前面打 `"` → 再打 `"`」。真机复现出两种落法,都会堆出多余引号:**A 回最前打 `"`(没选中)**,光标紧贴一个没闭合的开符、后面还有正文,此时再打同一个得到 `""你好`(两个没闭合的开引号,谁都用不上);**B 选中文字打 `"`**,会被框成 `"你好"`(光标落在闭符之后),再打 `"` 就堆出使用中报的 `"你好""`。修法:A 认作**补闭合**,闭符落到这一段的末尾(`"|你好` + `"` → `"你好"`);B 认作**已经框好**,不再加一个(`"你好"` + `"` → `"你好"`)。两条都有边界:**空对不算已框好**(连打三下引号仍是 `"""`,写 ``` 围栏那种用法不能被挡)、`a(` 后打 `(` 仍只插一个。用例 `client-behavior` 90 → **96 项**、真机回归 15 → **19 项**(4 条就是使用中这条路径的两种落法 × 快慢);**反向对照过**:把这两条规则关掉,B 立刻红成 `"你好""`、A 红成 `""你好`。离线 133 项、真机 6+19 项,各连跑 3 轮全绿
148
+ - **0.9.8**(2026-10-21,`1874756`):把 0.9.7 的「落地后按真文本复核」也补到**输入法那条路**(`beforeinput` 补闭符)上——之前只有半角 `keydown` 那条有这道网。判据:配对被落地之后,按真文本重算一次规则,本该「只插一个」的就把多出来的闭符撤掉。离线 127 项、真机 21 项复跑全绿(真机结果与 0.9.7 一致,这一版只加网、不改行为)
149
+ - **0.9.7**(2026-10-21,`69b87cc`):修 0.9.6 **自己引入的回归**——「配对后光标复位」当时靠盲定时器撒一串(0/16/48/110ms),两头都出错:**要么没抢在输入法前面**(`"` 配好对之后紧接着上屏中文,文字落到闭引号**外面**成 `""你好`,末尾再打就是使用中报的 `"你好""`),**要么在用户已经打完字之后才开火**,把光标拽回原位(读数 `caret=1`)。逐点采样的真机读数说清了时机:我们自己插入的文本节点在 microtask / rAF / `setTimeout 0` 那一刻**都还不存在**,约 **2ms** 才出现,从那一刻起写光标就粘得住——所以这个时刻只能由 DOM 变化来定,不能猜。修法:① **MutationObserver** 等文本真的落地再写(不再撒盲定时器);② **连续两拍都读到目标位置、文本也没再变**才算落地(Lexical 的重渲染比我们晚一拍,只写一次会被它冲掉);③ **`expect` 文本护栏**:文本被别人(用户/输入法)改过就立刻放手,绝不抢光标;④ **落地后按真文本复核**一次,判据本该「只插一个」的就把多出来的闭符撤掉。用例补 3 条**手速**场景(每步 10ms 与零等待)进 `test/real/real-regress.mjs`;**反向对照过**:去掉 observer 立刻红成 `""你好""`,正是使用中报的形状。离线 127 项、真机 6+15 项全绿
150
+ - **0.9.6**(2026-10-20,`2f6e3fc`):修**配对之后光标跑掉**与**空框打全角开符不补闭符**,两条是同一个根因的两面。真机取证(`dsh --profile demo --port 3081` + Playwright,在 `window` 捕获阶段记录「按键那一刻」的读数,绕开插件自身处理器)推翻了交接文档里的三条旧假设:① keydown 里读 DOM **不是空的**(`text='"' + '你好'`、`caret=3` 全对)——影子状态不需要;② `preventDefault()` **拦不住 Lexical**,它照自己的模型再插一遍,所以「拦 + 自己插」必然双份;③ `input` 事件在**空框首次插入时根本不派发**(探针里只有 `beforeinput`),这才是「补闭符时灵时不灵」的真因。实测的两条缺陷:**配对之后光标停在开符之前**(空框打 `"` 值对但 `caret=0`,接着打字打到引号外面、连按第三下被当成「跳过闭符」吞掉,`"""` 只出 `""`);**空框打 `“` 只得到 `“`**(补闭符挂在 `onInput` 上,而那一场没有 `input`)。修法:① 光标改成**按绝对偏移写 + 跨帧校验**;② `beforeinput` 改成**判定在这里、落地等原生插完再收尾**——不拦默认、不自己插开符,「只插一个」的情况零干预,要补闭符/要跳过/要框住的才记一条 pending,事后读状态确认再动,且**幂等**(`onInput` 那条先到也不会补两次);③ 选区框完之后把原生多插的那一个开符删掉(真机读数:选中 `abc` 打 `“` 曾得 `““abc”`)。假 DOM 顺带补齐「浏览器那半步」(`beforeinput` 没被拦住就照默认落字,可模拟 `input` 缺席),六条目标规格转正、替换掉 0.9.3 挂的 4 条旧规格;`client-behavior` **84 → 90 项**。另:安装地址换成规范路径 `mengge237/dsh-composer-ease`(GitHub 已把 `mengge237-labs` 迁走,旧地址只剩重定向)
151
+ - **0.9.5**(2026-10-20,`5166ecf`):补上**单书名号 `〈〉`**(U+3008/3009)到成对家族。取证时发现它不在表里,所以 `〈〉` 空对按 Backspace 完全不生效、空框打 `〈` 也不配对;与 `《》` 同属中文成对标点,不该漏。用例 79 → **84 项**(`〈〉` 的配对、跳闭符、空对删除,外加 `《》` 的空对删除),全套 121 项全绿
152
+ - **0.9.4**(2026-10-20,`5c83100`):修**全角/中文标点的空对里按 Backspace 只删一侧**。取证读数:`()`(码点 ff08 ff09)删完残留 `ff09`、`“”`(201c 201d)残留 `201d`,而半角 `()`、`[]` 正常。成因是 `pairbackspace` 两次删除各派发一次 `input`,`onInput` 的「全角开符自动补闭符」在前删之后看到孤零零的 `(` 就把 `)` 补了回来(半角因 `charCode < 127` 不走那条分支)。修法:删除期间把 `suppressInput` 计数 +2,让这两次 input 被吞掉。用例 75 → **79 项**(五个家族各一条),全套 116 项全绿
153
+ - **0.9.3**(2026-10-20,`3a16975`):**只加测试,不改行为**。`client-behavior` 从 42 增到 **75 项**——新增 29 条「成对家族行为矩阵」(九种开符空框配对、五种前方未闭合只插一个、三种右边已是闭符则跳过、四种框住选区、连按两/三下、五种标识符或路径后不配对、中文正文后仍配对)与 4 条基础用法(`/` 与 `@` 触发候选、全角闭符跳过与放行)。另挂 4 条「目标规格/已知缺口」,默认跳过,`CE_PENDING=1 node test/client-behavior.mjs` 才跑:beforeinput 路线要补的三条,以及这次新测出来的真 bug——**全角空对()里按 Backspace 只删一侧**(ASCII `()` 正常)
154
+ - **0.9.2**(2026-10-20,`c05be49`):修**「前方有引号、后方再打还是一对」**——原来只看光标前一个字符,当前面是「好」这种正文时看不到更早那个没闭合的开符,于是又补一对:`"你好` 末尾打 `"` 得 `"你好""`、`“文字` 末尾打 `“` 得 `“文字“”`(后者是 onInput 里的中文自动补闭符又补了一次)。改为按**未闭合**判定(对称引号看奇偶、非对称看开符多于闭符),命中就只插一个字符;「框住 + 打引号」同一套判定跟进(keydown 与 beforeinput 两条路都改),不再变成 `""abc"`。假 DOM 用例从 36 增到 42,五支测试全绿
155
+ - **0.9.1**(2026-10-20,`ef86aae`):修**框住之后的光标落点**——用符号把选区框起来后,光标原来停在闭符之前(回选到内层内容),现在**落在闭符之后**,接着打字就接在 `"内容"` 后面;正常打一对引号/括号那条路不变,光标仍在成对符号中间。落点按「选区后面的剩余文本长度」反推,多行/单行都验过(真机:`"AAA\nBBB\nCCC"` 后打 X 得 `"AAA\nBBB\nCCC"X`)
156
+ - **0.9.0**(2026-10-20,`6093447`):三件事。① **配对守卫扩到整个成对家族**(`( [ { ( 【 「 《 “ ‘` ` " '`),光标前是其中任意一个时只插一个字符、不补对——修掉括号类同样的「多出一个」;② **多行选区不再被压成一行**:真机实测 Lexical 只认 paste 通道,原来的 `insertText` 会把 `AAA\nBBB\nCCC` 清成 `\n\n`,现在合成 ClipboardEvent 走粘贴,三行原样保留(选区仍还原到内层内容);③ 弹层顶部那行「据对话推断 · java」**整行删除**,改成右下角一块**语言水印**:只写语言名(`java`、`java · python`),斜排、透明当底纹,角度随语言种数增长(1→14°、2→26°、3→38°、封顶 45°),不吃点击、无语言时不渲染
157
+ - **0.8.1**(2026-10-20,`f670f2d`):修**中文引号重复**——光标前已经是引号(半角 `"` `'` 反引号,以及中文标点的 `“”‘’「」『』`)时,再打引号**只插一个字符**,不再补成一对(一次反馈「中文模式打引号变成多个」两条同源)。连带收益:` ``` ` 围栏现在能直接连打三下,不再被配对吞掉一个。三条行为测试钉住,并做过反向对照(去掉守卫即变红)
158
+ - **0.8.0**(2026-10-20,`c2c6751`):改名「**更便捷的输入框**」,包名 `dsh-composer-ease`。上下文候选改为**只读对话正文区**(宿主的 `[data-conversation-scroll]` 与 `[data-slot="conversation.chat.node"]`):左侧栏、设置面板里的词不再进补全池子,也不再自我喂养已经画出来的候选;弹层顶上新增**依据行**(`据对话推断 · java` / `本文围栏 · python` / `历史习惯 · javascript`),让「凭什么给这张词表」看得见;回车发送时同步收掉弹层与灰字
159
+ - **0.7.1**(2026-10-17,`70b240f`):碎片前缀**整条候选清零**(不只词典源:文件/模型/词表全不给),客户端消费 `wordGate` 后连弹层都不开;碰撞测试改为断言整条为空
160
+ - **0.7.0**(2026-10-17,`ff6a776`):中文补全改用**词典判定完整词义**(`lib/cjk-lexicon.js`,jieba MIT 的常用词子集):前缀必须是词表里的词或更长词的前缀,**碎片(如「档管」)一律不提示**;n-gram 只留给草稿/上下文兜底。新增碰撞测试(文档/档管/单字三种输入)
161
+ - **0.6.3**(2026-10-17,`0fb1761`):中文补全**至少要 2 个字**且必须是更长词的前缀才提示,单字不再弹(使用中:要符合完整词义)
162
+ - **0.6.2**(2026-10-17,`7ecc16e`):粘贴(Ctrl+V)立刻收起弹层与灰字、不为粘贴内容补全;插件被装两份时复用同一弹层/灰字元素(原来两套实例各画一个,出现「一个在输入框左上、一个在光标后」的双弹层)
163
+ - **0.6.1**(2026-10-17,`acb9e31`):**IME 合成期间不补全**——打拼音时(如 `ye`)不再被 JS 关键字 `yield` 抢,
164
+ 弹层与行内灰字一起收起,上屏后再按中文内容重新判一次(2026-10-16 的一次反馈)
165
+ - **0.6.0**(2026-10-17,`4ed0943`):① **中文词补全**——草稿/上下文里的汉字词按二元·三元组拆开后参与前缀匹配(中文没有空格分词;
166
+ 长句尾巴按 1~4 字后缀各试一次),写过「文档」后面打「文」就能提示;触发条件也认汉字;
167
+ ② **引号成串修复**——补上 VS Code 的引号规则(前一个字符是同引号时只插一个),并按「一次按键只认最后一次」
168
+ 守住延后的光标复位、按住键的自动重复不再配对、apply 加单例防止重复挂载(重复挂载会让一次按键执行两遍)
169
+ - **0.5.0**(2026-10-17,`0ef3225`):① **上下文关联**——新增「上下文候选」(把页面上可见的对话文本里的标识符按前缀补出来,类型标「上下文」)与「语言推断」(没写围栏时按 def/import/.py、<div>、const/await 这些线索猜语言,再退到习惯);② **行内排版照 VS Code**:左边类型图标、中间标签(命中加粗)、右边淡类型词,类型不再跑到前面
170
+ - **0.4.3**(2026-10-17,`403ac6d`):修引号手感——① 光标右边紧跟文字时**只插一个开符**(在已有文字前打前引号不再被补成一对);② 右边已经是同一个引号时**跳过**而不是再套一对;③ 插入后光标**立刻**摆到中间(原来只延后一拍,快速连打两个引号会看不到右边已有闭符、再插一对),延后那一拍改成按绝对位置复位(不然会把光标挪到开符之前)
171
+ - **0.4.2**(2026-10-16,`e155f42`):`lib/fuzzy.js` 顶部**原样补回上游逐字的版权与许可头**(MIT 要求保留声明,之前被我自己的出处说明顶掉了);
172
+ 合规闸加一条判据盯死它。排序实现仍直接用上游那份(MIT 合规,不存在侵权,无需另写一套)
173
+ - **0.4.1**(2026-10-16,`9a6c8c4`):加**合规自检闸** `test/compliance.mjs`(8 项:署名/出处/许可证原文/出处头/零依赖/无密钥/版本一致/登记表条例),挂进 `npm test`;仓公开前先跑它
174
+ - **0.4.0**(2026-10-16,`b20b742`):排序换成 **VS Code 的 fuzzyScore**(lib/fuzzy.js 逐字取自 microsoft/vscode,MIT):前缀/词边界/驼峰/连续加分、间隔与分散扣分;列表按它的样子重画(命中字符加粗、右侧一个淡类型词),并去掉「本文出现 N 次」这类啰嗦标注
175
+ - **0.3.0**(2026-10-16,`bb1c393`):框选 + 打符号修复(Shift 组合键不再被挡、全角/中文输入法改走 beforeinput);新增**行内灰字提示**(首位候选的后缀贴在光标后);新增**语言习惯**(没写围栏时按你实际用过的语言上词表)
176
+ - **0.2.3**(2026-10-16,`c24fc88`):补全角/中文成对符(`(` `【` `「` `《` `“` `‘`,中文输入法下走 input 事件补配对);补全弹层改成**贴光标定位**(优先下方、放不下才翻上方 6px、宽度固定、高度封顶可滚、列表没变不重画)
177
+ - **0.2.2**(2026-10-16,`ddefacb`):回车不再被弹层吃掉;接受候选后弹层不再自动复开;文件候选改「路径段前缀」匹配;文件索引与模型 id 加 20s 缓存
178
+ - **0.2.1**(2026-10-16,`33b0d26`):修 Lexical 选区(改显式 Range)与捕获阶段时序(选区调整推一拍);环绕选区后选区还原到内层
179
+ - **0.2.0**(2026-10-16,无 sha:早于本仓首个提交 1481f82):新增本文词补全、语言词表(HTML/JS/Python/Java/CSS)、打两个字符自动弹、围栏语言提示
180
+ - **0.1.0**(2026-10-15,无 sha:早于本仓首个提交 1481f82):首版——自动配对、环绕选区、跳闭合、删空对、Ctrl+Space 弹层(文件与模型名)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 mengge237
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ # 双语 README 成对记录:每个小节正文的 md5(改一边就要同步另一边并重记)。
2
+ # 重新生成:node tools/check-readme-pairing.mjs --write
3
+ sections:
4
+ - slug: "Summary"
5
+ en: be542914d99851bc
6
+ zh: 67e3702491d4019a
7
+ - slug: "Table of Contents"
8
+ en: 82c6bbdf150c80b2
9
+ zh: 30d6d790b7277aa8
10
+ - slug: "Install"
11
+ en: c1a2732a91e030b7
12
+ zh: 3ee5fce83ff09935
13
+ - slug: "Use this package"
14
+ en: 84b72090a79e426d
15
+ zh: 1be5360456ea7a4f
16
+ - slug: "Understand the implementation"
17
+ en: 00544fe8f996f04b
18
+ zh: 3e18cc21df4d0447
19
+ - slug: "Boundaries and known limits"
20
+ en: ebd2eab4a5e734e7
21
+ zh: 6823f694a307f48b
22
+ - slug: "Development and tests"
23
+ en: 52d63fabe0d7917f
24
+ zh: 3155a27edf0960c8
25
+ - slug: "Sources and licenses"
26
+ en: 41d2f7be47179ae9
27
+ zh: e8ffa4c9b31d8260
28
+ - slug: "Dev note"
29
+ en: 44d1e52f610c2210
30
+ zh: 6fc8f565e2284fb4
package/README.md CHANGED
@@ -1,3 +1,80 @@
1
- # Temporary Holding Version
1
+ ---
2
+ description: "VS Code-feel composer for the DeepSeek Harness Web GUI: bracket/quote pairing, wrapping a selection, and a completion popup fed by draft words, language tables, workspace files, model ids and snippets; for DSH users and maintainers of composer plugins."
3
+ kind: "package-reference"
4
+ ---
2
5
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
6
+ # dsh-composer-ease
7
+
8
+ English | [中文](README.zh.md)
9
+
10
+ ## Summary
11
+
12
+ `dsh-composer-ease` is a DSH bundle plugin that gives the Web composer the editing feel of VS Code: brackets and quotes pair up, typing an opener over a selection wraps it, a closer over an existing closer is skipped, backspace removes an empty pair, and a completion popup opens after two characters. The browser half is plain DOM; the server half exposes two read-only loopback routes and writes nothing.
13
+
14
+ ![overview](docs/banner.svg)
15
+
16
+ ## Table of Contents
17
+
18
+ - [Install](#install)
19
+ - [Use this package](#use-this-package)
20
+ - [Understand the implementation](#understand-the-implementation)
21
+ - [Boundaries and known limits](docs/boundaries-and-implementation.md)
22
+ - [Development and tests](#development-and-tests)
23
+ - [Sources and licenses](#sources-and-licenses)
24
+ - [Dev note](#dev-note)
25
+
26
+ ## Install
27
+
28
+ ```bash
29
+ dsh plugin --profile web add https://github.com/mengge237/dsh-composer-ease
30
+ dsh plugin --profile web remove dsh-composer-ease
31
+ ```
32
+
33
+ Desktop (Electron) uses its own plugin manager; `dsh plugin --profile desktop` is rejected by the CLI. When developing, the browser half is read from disk on every request (refresh applies), while the server half needs a `dsh web` restart.
34
+
35
+ ## Use this package
36
+
37
+ | Action | Key / trigger | Behaviour |
38
+ |---|---|---|
39
+ | Auto pair | `(` `[` `{` `"` `'` backtick, full-width family | caret between the pair; no pairing after a path/identifier character, or when text follows the caret |
40
+ | Wrap selection | type any opener over a selection | `text` becomes `(text)`; single-line selections only (see boundaries) |
41
+ | Type-over / delete empty pair | closer already to the right / backspace inside `()` | no duplicate; both sides removed |
42
+ | Complete | two characters, or `Ctrl+Space` | popup: draft words, language tables, workspace files, model ids, snippets |
43
+ | Accept / navigate | `Tab` or click / `↑` `↓` | Enter stays the send key; `←` `→` `Home` `End` dismiss the popup and only move the caret |
44
+ | Dismiss | `Esc` | `Shift+Enter` still inserts a newline |
45
+
46
+ Completion follows the context: a fenced block selects the language table, otherwise the draft and the visible conversation vote, then the language you have been accepting candidates from.
47
+
48
+ ## Understand the implementation
49
+
50
+ - `lib/index.js` — server half: `/dsh-composer-ease/words` and `/config.json`, both behind a loopback trust fence; ranking uses `lib/fuzzy.js` (VS Code's `fuzzyScore`, copied verbatim).
51
+ - `lib/client/*.js` — browser half, nine fragments concatenated into `lib/client.js` by `tools/build-client.mjs` (the host loads exactly one file).
52
+ - Chinese candidates come from the dictionary (`lib/cjk-lexicon.js`) only: the prefix is matched against the word list, so word length follows the data instead of a fixed cap.
53
+ - The popup is plain DOM anchored to the caret; it never touches the host editor's data model.
54
+
55
+ ## Boundaries and known limits
56
+
57
+ See [docs/boundaries-and-implementation.md](docs/boundaries-and-implementation.md) for the measured list (multiline wrapping, insertion limits, IME behaviour, squiggle geometry) and the reason behind each.
58
+
59
+ ## Development and tests
60
+
61
+ ```bash
62
+ npm run build # fragments → lib/client.js
63
+ npm test # five offline suites, 185 items, zero network
64
+ npm run check:all # structure, duplicates, refactor budget, docs, privacy, prose, copies
65
+ node test/real/matrix.mjs "http://127.0.0.1:3081/?token=…" 3 --verbose # live matrix, 33 cases per round
66
+ ```
67
+
68
+ The host composer is Lexical: it ignores `sel.modify(...)`, ignores `beforeinput`'s `preventDefault`, and does not guarantee an `input` event — selection work uses flattened offsets plus explicit Ranges and cross-frame verification.
69
+
70
+ ## Sources and licenses
71
+
72
+ - Interaction design follows Visual Studio Code (MIT); no source copied except `lib/fuzzy.js` (verbatim, see [THIRD-PARTY.md](THIRD-PARTY.md)).
73
+ - Word-table data comes from monaco-editor, codemirror/lang-html, codemirror/lang-css, MDN browser-compat-data and jieba's dictionary; each entry in THIRD-PARTY.md records repo, commit, files and the full license text.
74
+ - This plugin: MIT (see [LICENSE](LICENSE)).
75
+
76
+ ## Dev note
77
+
78
+ - Change `lib/client/*.js`, then `npm run build`; never edit `lib/client.js`.
79
+ - Every convention has a runnable check under `tools/check-*.mjs`; CI only runs them (`.github/workflows/checks.yml`, ubuntu + windows, one run per branch via `concurrency`).
80
+ - Release: bump `package.json`, add a CHANGELOG entry with the commit sha, then `npm run check:all`.
package/README.zh.md ADDED
@@ -0,0 +1,80 @@
1
+ ---
2
+ description: "给 DeepSeek Harness Web 输入框加一套 VS Code 手感:括号与引号成对、选中即环绕,以及由本文词 / 语言词表 / 工作区文件 / 模型名 / 片段喂出来的补全弹层;供 DSH 用户与输入框插件维护者阅读。"
3
+ kind: "package-reference"
4
+ ---
5
+
6
+ # dsh-composer-ease
7
+
8
+ [English](README.md) | 中文
9
+
10
+ ## 概述
11
+
12
+ `dsh-composer-ease` 是一个 DSH bundle 插件,把 Web 输入框的手感对齐 VS Code:括号与引号成对、选中一段再打开符就环绕、闭符跳过、空对一次删两侧,打满两个字符弹补全。浏览器半边是纯 DOM;服务端半边只有两条只读的本机路由,一个字节都不写。
13
+
14
+ ![概览](docs/banner.svg)
15
+
16
+ ## 目录
17
+
18
+ - [安装](#安装)
19
+ - [使用本包](#使用本包)
20
+ - [理解实现](#理解实现)
21
+ - [边界与已知限制](docs/边界与实现.md)
22
+ - [开发与测试](#开发与测试)
23
+ - [来源与许可](#来源与许可)
24
+ - [开发说明](#开发说明)
25
+
26
+ ## 安装
27
+
28
+ ```bash
29
+ dsh plugin --profile web add https://github.com/mengge237/dsh-composer-ease
30
+ dsh plugin --profile web remove dsh-composer-ease
31
+ ```
32
+
33
+ 桌面端(Electron)用它自己的插件管理器装,`dsh plugin --profile desktop` 会被 CLI 拒绝。开发时:浏览器半边每次请求现读磁盘(刷新即生效),服务端半边要重启 `dsh web`。
34
+
35
+ ## 使用本包
36
+
37
+ | 操作 | 键 / 触发 | 行为 |
38
+ |---|---|---|
39
+ | 自动配对 | `(` `[` `{` `"` `'` 反引号与全角家族 | 光标落在中间;光标前是路径/标识符字符、或右边紧跟文字时不配对 |
40
+ | 环绕选区 | 选中后打任意开符 | `内容` → `(内容)`;只支持单行选区(见边界) |
41
+ | 跳闭合 / 删空对 | 右边已是闭符 / 光标夹在 `()` 里按退格 | 不重复插;一次删两侧 |
42
+ | 补全 | 打满两个字符,或 `Ctrl+Space` | 弹层:本文词、语言词表、工作区文件、模型名、片段 |
43
+ | 接受 / 选择 | `Tab` 或点击 / `↑` `↓` | 回车仍是发送;`←` `→` `Home` `End` 只收弹层、挪光标 |
44
+ | 关闭 | `Esc` | `Shift+Enter` 仍是换行 |
45
+
46
+ 补全跟着上下文走:写了代码围栏就上该语言词表;否则由草稿与可见对话投票,再退到你最近接受过候选的语言。
47
+
48
+ ## 理解实现
49
+
50
+ - `lib/index.js` —— 服务端半边:`/dsh-composer-ease/words` 与 `/config.json`,都在本机信任栅栏之后;排序用 `lib/fuzzy.js`(VS Code 的 `fuzzyScore`,逐字取用)。
51
+ - `lib/client/*.js` —— 浏览器半边,九个片段由 `tools/build-client.mjs` 拼成 `lib/client.js`(宿主只认一个文件)。
52
+ - 中文候选只来自词典(`lib/cjk-lexicon.js`):前缀在词表里匹配,词长由数据决定,不再写死一个长度上限。
53
+ - 弹层是纯 DOM、锚在光标上;从不碰宿主编辑器的数据模型。
54
+
55
+ ## 边界与已知限制
56
+
57
+ 逐条阅读见 [docs/边界与实现.md](docs/边界与实现.md)(多行包裹、插入限制、输入法行为、波浪线几何),每条都写了为什么。
58
+
59
+ ## 开发与测试
60
+
61
+ ```bash
62
+ npm run build # 片段 → lib/client.js
63
+ npm test # 五支离线套件,185 项,0 网络
64
+ npm run check:all # 结构 / 重复 / 重构预算 / 文档 / 隐私 / 散文 / 副本
65
+ node test/real/matrix.mjs "http://127.0.0.1:3081/?token=…" 3 --verbose # 真机矩阵,每轮 33 条
66
+ ```
67
+
68
+ 宿主 composer 是 Lexical:不认 `sel.modify(...)`、不认 `beforeinput` 的 `preventDefault`、`input` 事件不保证派发 —— 选区操作一律走「摊平偏移 + 显式 Range + 跨帧校验」。
69
+
70
+ ## 来源与许可
71
+
72
+ - 交互设计参照 Visual Studio Code(MIT);除 `lib/fuzzy.js` 逐字取用外没有复制源码(见 [THIRD-PARTY.md](THIRD-PARTY.md))。
73
+ - 词表数据借用自 monaco-editor、codemirror/lang-html、codemirror/lang-css、MDN browser-compat-data 与 jieba 词典;每条的仓库、commit、取用文件与许可证原文都记在 THIRD-PARTY.md。
74
+ - 本插件自身:MIT(见 [LICENSE](LICENSE))。
75
+
76
+ ## 开发说明
77
+
78
+ - 只改 `lib/client/*.js`,然后 `npm run build`;不要手改 `lib/client.js`。
79
+ - 每条约定都有可执行判据放在 `tools/check-*.mjs`;CI 只负责把它们拉起来(`.github/workflows/checks.yml`,ubuntu + windows,`concurrency` 保证每分支只留最新一次)。
80
+ - 发版:改 `package.json` 版本 → CHANGELOG 写条目并回填 commit sha → `npm run check:all`。