Repository navigation
Releases: hyperion2144/dsh-hashline-edittool
Release list
v0.10.7
Added
- README 增加 dsh-plugin.org 收录徽章(Listed on dsh-plugin.org)。
- 新增
screenshots.json:在插件仓库里声明市场截图,dsh-market 等店面按此展示。
Changed
- npm 包的
description对齐当前工具集:去掉已在 0.4 契约中移除的batch_edit,补上 write/grep/delete/rename/ast_grep/ast_edit/lsp/format,并把标记形态改成<anchor>(可选<anchor>:<line>)。
Fixed
grep的命中数与截断不再静默:每个文件一节末尾报该文件的真命中数与本次返回数,被每文件上限或响应预算截断时写明是哪一刀截的(续取段同样逐文件报);canonicaltotal改为真命中数(触到上限时补一趟不限额计数),并新增truncatedBy(#386,ADR-0029)。grep卡片折叠行标题尾显示命中数与截断原因(双语,与 read 行窗口同一槽位)(#386,ADR-0029)。- LSP 自动诊断不再投递过期内容:同一文件的后续写入落地后,先前写入的延迟诊断会被丢弃,诊断探针也不再把旧文本回灌给语言服务器;已进入队列的旧报告带写入代际标注,与新报告自证先后(#388,ADR-0030)。
- 会话切换工作目录后,read/write/edit 等工具的相对路径跟随 DSH 的当前工作目录,不再落回会话创建时的旧检出(#394,ADR-0031)。
v0.10.6
Fixed
grep字面模式(regex: false)搜目录不再恒返回「No matches」:模式作为一个参数原样传给 ripgrep,字面量改用独立的--fixed-strings,不再把-F拼在模式文本前面(#384)。grep改由 ripgrep 直接产出命中(rg --json),不再逐行用 JS 匹配;锚点、上下文行、预算续取、二进制保真等契约不变(#384,ADR-0028)。grep的模式方言改为 ripgrep 正则(原先是 JavaScript 正则):ripgrep 拒收的模式即坏模式(E_BAD_SHAPE),与官方 grep 工具同口径(#384,ADR-0028)。
v0.10.5
v0.10.4
Added
-
lsp.idle_timeout_ms设置:语言服务器空闲超过该窗口(默认 5 分钟,0 = 永不)即被回收,下次用到再拉起(#306)。 -
format.fallback_full_file设置(默认关):range 模式下服务器不支持局部格式化时的回退必须是显式选择——自动路径静默跳过,format工具显式带 range 则以E_FORMAT_UNSUPPORTED拒绝而不是悄悄格式化整文件(#315)。 -
新增
delete工具:删除普通文件并回收该路径的anchor_meta/anchor_lines/ 所有会话的served/ 所有深度的undo,报告逻辑释放量;物理空间在存储关闭时 VACUUM 归还(#322)。 -
新增
rename工具:重命名/移动普通文件时把旧路径的四族锚点行迁到新路径,已分配与服务过的锚点无需重读继续可用;目标已存在即拒绝,批量按请求顺序逐对执行(#337)。 -
新增
write.preview设置(默认diff):write写后回显可选统一 diff / 整文件回读 / 仅一行确认;confirmation不回显内容也不分配锚点,卡片 gutter 只有行号(#354 / #359)。 -
五类卡片新增换行开关:read / grep / diff(edit、write、undo、ast_edit、format)/ LSP / error 卡片默认自动换行,一键切回横向滚动;error 卡无回显内容时不出现按钮(#373)。
-
read 折叠行标题行尾显示读取窗口(
L240–319 · 共 374 行),预算截断附 resume 提示(#374,ADR-0027)。
Changed
- web 卡片改用插件自有词表命名空间
hashline.card(zh/en,跟随 DSH 语言设置,其它语言回落英文):卡片界面文字随语言切换,diff / LSP 卡不再显示 key 字面量,delete 卡与 error 卡的硬编码英文一并收编(#372)。 - host 拼装的文案(read 退化注记、折叠行标题、LSP 状态与安装提示)改为稳定 key 过 wire,客户端按界面语言渲染;插件设置页界面文字全部双语化,delete / rename 卡的标题、摘要与明细随之跟随语言(#375,ADR-0026)。
format.editorconfig默认改为开启:项目.editorconfig的缩进声明优先于文件自身的缩进推断,显式设 false 退回推断(#336)。- 编辑 diff 改为 unified diff 形态:文件头
--- path/+++ path,段头@@ -old,count +new,count @@ 函数名()给出改动前后的行区间与所在函数;删除行留原位,其失效的旧锚点以~标记(#293,ADR-0020)。 - 段头的作用域名由纯语法作用域检测给出(
src/render/scope/):检测不到就留空,不猜(#294)。 - 作用域检测按文件扩展名/文件名选语言与声明模式表(git 的做法),覆盖 TS/JS、Python、Go、Rust、Java 系、C 系、Ruby、Lua/Elixir/Julia 等语言;表驱动,加一种语言是一行(#301)。
- 段头列出该段所有改动落入的作用域名(
first() / second()),不再只报第一个(#302)。 - 作用域检测对同一份文本只扫一次(缓存键含路径):写大文件时不再按改动行重复全文扫描(#303)。
- web diff 卡片同步新形态:段头作分组标题,折叠只数代码行,复制文本不再带上段头(#295)。
format.auto从布尔改为三态模式off(默认)/full/range,存量true→full、false→off自动迁移;range 模式只格式化本次调用改到的行,write无改动区间视同full(#315)。- 自动格式化段的 restored 折叠:被格式化器改回编辑前原文的行不再引用内容,每段连续行折叠为一条
@@ ~start,count @@ scope定位行加一句 restored 摘要,仅锚点真漂移的行保留内容行;header 增加r restored计数(#315)
Fixed
format不再把 4 空格深层嵌套文件推断成 8 空格:缩进单位改按「行间步长 + 重复宽度网格」联合识别,个别错缩进行不再改变单位,全文与范围格式化不再产生 4/8 混排(#336)。rename/delete卡片标题修复:工具名与路径/数量之间补分隔点;rename标题统一列出 from -> to,delete列出实际路径(#337)。- format 工具的 range 边界修复:
<line>:<anchor>组合形式(数字在前的行号提示)的行号曾被当锚点查找而误报E_RANGE_UNVERIFIED,现在两种行号顺序都正确解析(#316) - 关闭哈希存储时在写完关闭标记后再做一次 WAL 检查点:干净关机后不留未落盘的 WAL 帧,下一次打开是普通读而不是恢复(#303)。
- 关闭哈希存储时真正等待 VACUUM 完成;此前的
pending_vacuum没有生产者、VACUUM 又是异步的,标记在完成前就被清掉。现在删除/淘汰在逻辑字节下降时写标记,关机同步 VACUUM 成功才清(#322)。 - 语言服务器子进程不再常驻到插件卸载:握手失败的进程立即关闭,空闲超时的会话被回收(#306)。
grep命中区超过单次响应预算时不再返回空壳:改为按行切分,先返回预算内放得下的行(带锚点、可直接编辑,切点不会落在首个命中之前),其余行经 resume 提示续取(#307)。- 锚点库冷开与每次写的预算判定不再随
anchor_lines行数线性变慢:行数改为随写入同事务维护的值(#305)。 - 锚点库的路径数改为随写入维护的值,预算判定不再随路径数变慢(#311)。
- 自动格式化段不再逐行重复主 diff 已展示的行:这类行只在 header 计数(
k in main diff),write更新路径同样塌缩为计数,手动format全量输出不变(#312)。 lsp错误响应不再被框架拒绝:语言服务器启动失败/超时等转出的错误值补齐了 output schema 必填字段(#317)。- LSP 启动失败的槽位不再永久卡死:按指数退避自动重试(5 秒起、上限 1 分钟),成功后计数清零;启动失败报错附服务器 stderr 尾巴(#318)。
lsp工具新增status(只读查询已启动语言的服务器状态)与warm(跳过退避立即启动/重拉)操作(#318)。lsp的status/warm出专属状态卡(状态点 + 语言 + 失败原因),词汇表之外的意外错误包装为[E_LSP_UNEXPECTED]走错误卡片,不再因宿主无法无损序列化而整次调用失败(#320)。lsp的错误值不再携带字面path: undefined:status/warm无path,宿主无损 JSON 校验会整次拒绝这类拒绝值,改为省略该字段(#320)。- client 半区九个 toolview 接管注册收进一个
TAKEOVER_PRIORITY常量并改为-2:与dsh-claude-style写死的-1不再同值,两个插件同时启用时不再抛keyed slot "tool.call.toolview" ... already has an entry(#321,ADR-0022)。 - 同一
edit调用里释放的锚点不再被该调用的后续行重新分配;多 item 批次的释放池与 served 清理不再被批次收尾的早退吞掉(#351)。 - PTC preset 会话里的卡片不再回退成原始输入/输出:卡片模型不再一律拒绝
parentCallId,run_code子调用带同一份 presentationMeta,和原生调用一样渲染(#353)。
v0.10.3
v0.10.2
Changed
- SDK 开发依赖对齐 0.2.0-rc.2 运行时线(17 个
@deepseek-ai/dsh-*devDependencies);类型与测试期依赖,源码零改动。
Fixed
read/edit不再仅凭 NUL 字节判定「二进制」:含 NUL 的合法 UTF-8 文件可读可改(读服务锚点、编辑写回字节一致),非法 UTF-8 仍可读但不可编辑([E_LOSSY_BYTES]),grep的 lossless 命中同步升级为可编辑(#273,ADR-0018)。- 后端「读取超上限」错误码映射为
[E_TOO_LARGE],不再裸冒FS_TOO_LARGE(#271)。 grep不再因一个 NUL 字节把整个文件静默跳过(#268)。- 冷开性能预算改判「三个新进程取中位数」,共享 runner 上的单样本抖动不再误杀(无票)。
- 工具目录下嵌套检出的测试不再被主检出收进测试集合(#266)。
v0.10.1
Fixed
read游标收纯数字字符串(#256,ADR-0014 Amendment):模型工具调用里行号以字符串形态到达是常态(本单会话内 8 次自然复现:offset"1"/"145"/"148"、limit"8"/"14"/"40"等),而parseReadCursor的DIGITS_RE分支把纯数字串一律当「row marker 的行号 hint 被当锚点抄」抛[E_BAD_REF]—— 同一语义的 number 形态却照收,错误文案指路「Re-read that line to get its anchor」对想要行号的调用方完全无效。修复(#238 决议 6 之修订,维护者拍板):字符串 trim 后命中/^\d+$/即按数字语义解析(offset=起始行号、limit=行数,与 number 同义);沿用 number 同款正整数校验("0"→[E_BAD_SHAPE],顺带修复其原先报E_BAD_REF的同语义两种报法);前导零("088")与空白(" 88 ")容忍;line#hash/line:anchor混合形态维持[E_BAD_REF],真锚点行为不变;presentCall卡片同步归一(数字串不再显示为幽灵锚点)。read-window 套件 13 → 20 例(单元级边界矩阵 + 集成级同结果/JSON 同码)。
v0.10.0
Changed
-
续读/分页文案收口成一句话(#246):B 线只统一了
read自己的尾巴,其它通道仍在各自拼同一句「还有更多」。本轮按每个通道的消费者逐个裁决:括号版通知(Omitted N lines[ (~C chars)]. Use <consumer> {resume: "TOKEN"} to continue.)收进共享生成器formatOmittedNotice(src/infra/response-stream.ts),由spillModelTextOverflow(edit / lsp / ast_grep / ast_edit / undo 六处调用者输出字节不变)、grep 相邻两处(原先在同一个文件里拼了两遍)与 read 的[Continued report]页脚共用 —— 后者是唯一一处可见变化:more lines→lines。write的自动预览不再自造第四种尾巴:它就是「第 1..K 行 / 共 N 行」的文件窗口,改用read的窗口句[Lines X-Y of N. Omitted K lines. Use read {resume: …} to continue.](formatWindowSummary);JSON 模式下也不再是「JSON 后面粘一句散文」——window与continuation进 payload,顺带关掉一条真实崩溃路径(JSON 模式 + 有 diagnostics + 预览被截断时,diagnostics 合并会对粘了尾巴的文本再JSON.parse),续读令牌也不再只活在 modelText 里。删除hashline/anchor-pipeline.ts里已无人调用的paginationHint(第三个生成器,死的)与write预览里一条裸表达式死代码。此外修掉readguidance 里描述已退役旧尾巴的引文,并给 grep guidance 的引文补上真实的(~C chars)。裁决记录:grep 无窗口数字可报(只有 spill)、edit 的拒绝回声今天本就没有分页句,两者语义各自保留。已知未改(诚实记账):read 的 report-segment 续读在 JSON 模式下仍是散文(totalLines:0/lines:[]占位),与 #245 的 JSON 纯度承诺不符,另票处理。新增test/core/issue-246-tail-copy.test.ts(8 例)钉住上述字面量。 -
模型侧文案改成逐文件口径:prompt 不再宣称「整批原子 / 整批拒绝」(#242):
edit的 description 与两态 guidance 原先写着「The batch is ATOMIC — any hunk failure rejects the WHOLE batch and nothing is written」,而 ADR-0003 的语义是每个文件一个原子批次、文件之间互不牵连、部分成功逐文件上报 —— 模型据此会把一次多文件调用当成单一事务,一处失败就以为整次什么都没发生、也就不会去重试失败的那个文件。现在editDescription两态的开头、两态 guidance 的原子性 bullet、edits参数 description、README / README.zh 的同款 bullet 与[E_BATCH_ABORT]错误码行都改成「该文件的批次被拒绝、该文件什么都没写;其它文件不受影响,部分成功逐文件上报,失败文件各带自己的错误码」。顺带查明并收窄同族的另外两处夸大:申报不匹配([E_CONTENT_MISMATCH])与Classification: noop也都只在文件范围内成立。引擎抛的[E_BATCH_ABORT]尾巴逐字保留 —— 它的作用域就是「一个文件的批次」,单文件调用里字字为真;多文件响应由 ADR-0004 规定剥掉该尾巴(test/core/multi-file-atomicity-contract.test.ts两处已锁)。另外,两处只在文件范围内成立的守卫消息([E_ANCHOR_AMBIGUOUS]的两条抛出文案与 README / README.zh 错误码表对应行)原先也收尾于「nothing was written / 未写入任何内容」—— 多文件调用里别的文件可能已经落盘,同一批收窄为「该文件」。新增test/core/issue-242-atomicity-wording.test.ts钉住这些文案不再出现整调用口径。 -
设置文件通道整体删除:插件不再自己读写设置文件(#237):
src/infra/legacy-migration.ts(0.1.7 的settings.yaml/settings.yaml.imported一次性导入与.legacy-settings-migrated标记)、src/index.ts的调用点、src/config.ts的settingsYamlPath()与手写 YAML 解析器parseSettingsYaml()(约 145 行)、两个 dev 脚本里的直读段全部删除。设置只剩一条路:profile 的插件配置(Config+apply(ctx, config)活引用 +settings/document-updated)—— 0.2.1 的设置服务面没有任何 API 能把遗留文档交给插件(importLegacyDocument私有),而运行时自己会做一次性导入;旧模块既重复,也可能与运行时并发写同一份配置。README / README.zh 的迁移段改写为「导入由 dsh 自己做」;test/core/legacy-migration.test.ts删除,ast-settings/auto-diag的解析型用例改为经 schema +applyEffective断言同一行为。 -
多文件编辑的部分失败,卡片终于看得见(#247):
edit的presentationMeta新增并列的failures通道 —— 每个失败文件一条{ path, code?, message, context?, hint? }(与ErrorMeta同源、在src/infra/error-result.ts只声明一次;path必填、空表不出现键),顺序与输入一致。它与既有的error互斥且穷尽:error= 这次调用零变更,failures= 至少一个文件落盘、至少一个没落;全失败只发error(其context已逐行列出每个文件),全成功两者都不发;不新增派生计数 —— 卡片自己用diffRowGroups与failures的长度算「N of M files failed」,计数不可能与清单打架。per-file 的code改取内层失败的头部[E_*](原先取消息里最后一个字面量:拒绝会原样带回 ±3 回声,被回显的源码行自带[E_BAD_SHAPE]这类字面量时会把卡片显示的码劫持掉),message/context与单文件错误走同一个切分splitErrorText。卡片侧:失败文件与成功文件同为 tab(红点 +ErrorCard面板),上方一条role="alert"横幅(「N of M files failed」,每条失败可点进对应 tab),行摘要显示第一个失败路径与计数,而行状态仍是成功(确有变更落盘)。同时修掉两处会让失败整块消失的陷阱:presentationMeta的空 diff 兜底分支漏发diffRowGroups(唯一成功文件是整文件 no-op 时会连失败一起丢),以及客户端metaDiffRowGroups对零行分组整表返 null(同一种丢失);ErrorCard的 alert 改为可关(部分失败时由横幅独占播报)。ADR-0015;新增test/core/partial-failure-meta.test.ts(6 例,含回声劫持夹具)。 -
read 的行窗口改吃锚点,尾巴只剩一句(#245):
offset/limit现在各收正整数或活锚点(闭区间:锚点所在行包含在窗口内;limit给锚点即“读到这一行为止”,给数字仍是行数),数字与锚点可混用 —— 行号关闭时 grep 只回裸锚点,模型从此能直接说“从这个锚点读到那一行”,续读不再依赖会漂移的行号。窗口总结改为宿主算一次、只有一句:[Lines X-Y of N. End of file.]/[Lines X-Y of N. Use offset="<末行锚点>" to continue.]/ 预算截断时[Lines X-Y of N. Omitted K lines. Use read {resume: …} to continue.];散在渲染器 footer、formatPaginationHint与字符截断正则三处的旧句式([Showing lines …]、[End of file - total N lines.])全部删除,JSON 与 web 卡片同读宿主投射的window: {start, end, totalLines}(start是实际服务的第一行;越界不产出window;空文件是{1,1,1}的真实服务,末行仍是空文件提示)。游标失败复用编辑侧的 served 探针并按edit的口径报码:锚点已死 →[E_STALE],served 类异常(没服务过 / 行移了 / 内容变了)→[E_RANGE_UNVERIFIED]([E_RANGE_STALE]仍只留给校验和与版本守卫),旧式line#hash标记 →[E_BAD_REF];resume与offset/limit同时出现不再被静默偏向resume,改为[E_RESUME_CONFLICT]。三个既有E_RESUME_*码此前由字符串插值生成、互锁测试看不见,也从未进 README —— 本轮改为字面量表并补进错误码表。ADR-0014;新增test/core/read-window.test.ts(13 例)钉住两种游标、闭区间、唯一新增硬拒绝与四条尾巴。 -
⚠️ BREAKING:line_numbers从工具参数变成用户设置(#244):read/edit/grep/undo_last_edit不再接受该参数 —— 现在是设置卡片里的一个开关(行号 / Show line numbers,默认关闭),决定模型侧每一行是<anchor>:<line>还是裸<anchor>。行号不该由被观察的一方决定自己看到多少文件结构,这个旋钮属于读者。默认值同时从「开」翻成「关」,所以仍带该字段的调用一律[E_BAD_SHAPE]硬拒绝(静默忽略等于用旧语义回答新问题),提示直接指向设置位置。开关只作用于模型侧行渲染:text 行与 JSON 的{"<marker>": content}键同源,read 的表头图例与ANCHOR:LINE|CONTENT形态一并跟随;web 卡片读结构化presentationMeta(整数lineNumber),因而不受开关影响。覆盖 read / grep / edit 文本 diff / undo / ast_grep / write 的自动预览 / lsp —— 全部经同一个标记落点fmtMarker。新增test/core/line-numbers-switch.test.ts(8 例)钉住开关两态与四工具的拒绝路径。 -
ADR-0013: refusals by size are retired — all tools stream oversized results in segments (per-response char budget, default 48,000) with resume tokens instead of refusing.
GREP_MAX_TOTAL_BYTESand the 100 MiB read gate are gone.grepnever returns "No matches" for a file it hasn't searched. -
grep default context is now
context_lines(3) instead of 0 — text and JSON contracts are aligned. -
锚点事实回归唯一持久化真相源(契约 §3/§8):
hashline/session-anchors.ts里那个Map<string, SparseState>+ LRU 整体删除(连同ANCHOR_CACHE_LIMIT、dropAllAnchorState、applyEditToState)。它让纯域层持有 store 作用域的可变状态,并把两个只应由库回答的问题从内存回答:「该文件已分配了哪些锚点」(§2.1 第3条)与「这个锚点还活着吗」(§2.2 条件1)。现在ensureState每次都读anchor_lines,anchorsFor是纯读(不再 realign、不再写库)。 -
store 耦合的实现搬出
hashline/(契约 §3):hashline/session-anchors.ts(705 行)→domain/session/anchor-state.ts(持久化端口、事务、anchor_lines读写、对齐通知);纯逻辑留在hashline/alloc.ts(SparseState/AnchorEntry/allocateInto/anchorsPure)。hashline/index.ts的 barrel 不再转发 store 耦合的一半 —— 那正是tools/当初能跨层的口子。解析引擎对状态的反向依赖改为注入式读取器(registerAnchorReader,由hash-store装配时接上,无会话时回退纯推导)。验证:grep确认hashline/内零domain/import、全库零session-anchors引用;6 个 tools/lsp 文件改从domain/session/引入。 -
6 处整文件级冗余库读删除:
ast_edit/ast_grep(两处)/lsp(两处)/auto_diag原先先做一次「把整文件锚点读成稠密数组」,随后又把其中实际会被读取的那几行全部覆盖成铸造结果 —— 数组里被真正用到的项无一来自那读。改为按行数建空数组、由铸造填。ast_grep的槽宽也改为按已服务的行计算(原先按整文件算,静默把每条槽拉宽)。 -
write的 diff 不再二次记 served:allocateForRenderedRows现在把全部渲染行交给唯一入口(复用的行本就不会重铸),于是一行的锚点与它在 served 里的存在同一事务落定 ——write工具里那段补记的recordServed随之删除(那是同一事实的第二次写,也是 #187 两半可能不一致的入口)。 -
宿主的
FS_STALE_VERSION与锚点的E_RANGE_STALE共用一码这件事已在契约 / README / README.zh 写明:两者补救手段相同(重新 read 拿新锚点),但只有后者是「校验和 ≠anchor_meta」的重映射信号。原先文档只说了后者,读码的人会以为标签只有一种含义。
Added
read/grep/ast_grep/lsp:resumeparameter + continuation tokens. Oversized results return a first segment and a continuation footer; pass the token back to get the next segment.- Settings:
max_response_chars(per-response char budget, default 48,000, clamp [8,000, 49,984]). - New module
src/infra/response-stream.ts: spill store + resume tokens + segment assembler.
Fixed
-
diff 渲染在超大单块上抛
RangeError(#229):partsFromRuns把游程拼回行数组时用push(...slice)—— 展开会把每个元素当成实参,单块同向连续行达到约 15 万行就撞上 V8 的实参上限(Node 26 实测:10 万行通过、15 万行抛)。爆点在渲染,差分本身已算完(degraded仍为false)——write一个超大文件、undo一次超大回滚会直接得到栈溢出而不是 diff。改为逐元素追加(新pushSlice),输出逐位不变;新增test/core/line-diff-bounds.test.ts以 100 万行纯插入/纯删除钉住。 -
Myers
maxD加上硬上限 1,024(#230):trace 是O(maxD²)(maxD + 1条2·maxD + 1的 int32),而maxD可由调用方指定且原本无上限 —— 实测 16,384 时单次调用要 ~1.3 GB。现在在分配点(myersRange)夹取,任何调用方都无法把 trace 撑过 ~8.4 MB;巨型maxD与上限本身的行为逐位一致(不再靠碰运气撞RangeError)。⚠️ 不采纳 #230 的另一半(默认值 256 → 64):该结论基于单个 800k 行 / 50% 变更的输入测得「快 6 倍且输出逐位相同」,但不普遍成立。反例是 #190 一致性契约里的「整文件重写、尾部幸存」:maxD=64时预算先耗尽 → 区间被切分 → 幸存行被渲染成-/+而不是 context。test/core/line-diff.test.ts把这一形状当作与 jsdiff 的逐字段一致性要求,所以 64 是渲染缺陷而非白赚的提速。默认值保持 256;两个数字的不同职责(默认值是质量底线、上限是安全网)写进源码注释,反例由test/core/line-diff-bounds.test.ts保存。
-
取锚收敛为唯一入口(#223):
read/grep/lsp/ast_grep/ast_edit/undo/write各自直接调allocateForLines/allocateInWorkspace,再另外调一次recordServed的时代结束 —— 全部改走anchorForInWorkspace,取锚与 served 记录在同一事务提交。两次调用之间失败就留下「模型看见了但没有任何会话见过」的锚点(下一次编辑拿它直接被判 never-served),这是#136/#143那一类缺陷的孳生地。- 会话层的 serve 拆成两半:
anchorForInWorkspace(进作用域取锚 + 记 served,并返回锚点供渲染 —— 模型看到的必须就是它能用的那批)与serveRowsInWorkspace(只剩 observe)。session-view.allocateInWorkspace删除,已被前者取代。
- 会话层的 serve 拆成两半:
-
allocateInto不再在同一次调用里释放旧锚点:那条used.delete(existing.anchor)会把刚释放的锚点交给同一批次里的另一行,而模型手里那份仍在自己的 served 里 → 校验放行 → 静默错行编辑(#217§1 已构造性复现)。 -
锚点状态写入改为差量:原先每次写回都是「整路径 DELETE + 逐行重插」(
#217实测约 187× 写放大);现在对照 store 现状只 upsert/delete 变化的行,并把写路径收成一条(persistAnchorState)。 -
关库时同时清掉内存里的锚点状态(已被取代:内存里的锚点状态已整体删除 —— 见### Changed第一条。当时那条修的是「缓存跑在库前面」,而现在根本没有缓存可跑;shutdownHashStore只需关库,dropAllAnchorState随之消失)。 -
编辑路径补全「释放三清」(#223 §4 / #224):原先被替换行的锚点只做了一清 —— 从
anchor_lines丢掉就结束,既没告诉发起释放的那个会话(served 里那条还在),也没进本次调用的释放池。于是它还能被重新发给另一行,而模型手里那份仍然「活着且已 served」→ 校验放行 → 静默错行编辑。现在updateAnchorsAfterEdit报告丢掉的行,reconcileServed把摘掉的锚点送入释放池,edit 引擎据此同步本会话的 served 镜像。 -
probeLines的四类 reason 改为按「哪一条判定失败」给出:原先靠「那行现在挂着谁的锚点」反推,那回答的是行的状态而非拒绝原因 —— 一个已被释放、其旧行现在挂着别人锚点的锚点会被报成「文件在你会话期间被改过」(line-changed),而真相是「这个锚点已经没了」(not-live)。 -
undo改为快照回退(#224):内容、行↔锚点绑定、校验和三者一起退回。原先只退内容,重新拿到的是被撤销那次编辑铸的锚点 —— 模型撤销一次误操作、再重提它本来要做的编辑时,会因为「锚点不是你会话见过的」被拒。现在UndoRecord里本就保存的编辑前绑定被整体写回。 -
undo栈深度 10 → 3:栈是用来从一次误编辑里恢复的,不是历...
v0.9.5
Added
grep默认按.gitignore取候选清单(新设置grep_respect_gitignore,默认开):候选文件清单改由 ripgrep 出(--files --no-require-git --no-ignore-parent),被忽略的树(target/、build/、vendored 副本)根本不进读取集,从根上避开 64 MiB 读取预算那道墙。--no-ignore-parent是刻意的:只认被搜目录自身的忽略规则,祖先目录(恰好包含该路径的那个仓库)的规则不得把清单悄悄清空。关掉该设置即退回插件自己的走查(隐藏项 +node_modules跳过、不读忽略文件),rg 不可用时是同一回退;且预算被撑爆而预过滤没参与时,notice 会点名这一点。
Fixed
lines是行数组:一个元素就是一行,[""]不再变成删除(#198):载荷在进入引擎前被join("\n")压成字符串,而字符串形态用同一个""同时表达「没有行(删除)」与空行族 —— 于是[""]变成零行(replace静默删掉该行、ins变成无操作),["",""]静默少一行。修法是把数组→文本的桥做成无损编码(新增encodeText,即parseText在数组面的逆):只有全空族改用"\n".repeat(n)的写法,其余数组一字不变;op:"del"仍独占删除语义。resolveIns的第二处 join 一并修正(锚点行自身为空时,插入空行同样会丢行)。- diff 的
-行改为指向引擎真正移除的那一行(#198):删除行的锚点原本取自文本 diff 的自身对齐(前向贪心),相邻同内容行时它挑中后面那个孪生行 —— 也就是幸存行 —— 于是-行报出幸存行的锚点与旧行号,而被指定行的锚点整份 diff 都看不到。现在渲染器接收域层算好的候选范围(replacedOriginalRanges:按toAnchorHunk的既有规则排除ins—— 它的锚点行是被保留的、不构成被移除的行 —— 并升序返回),删除行取其中最早未被认领且内容相同的一行;无 hunk 的全文 diff(write/undo)不带该信息,行为不变。评审时正是这里查出过缺陷:ins的锚点行一度混进候选池,端到端用例靠 hunk 顺序恰好有利而假绿;修好后用变异测试(临时去掉排除 → 2 例失败)钉住。 - 新增
test/core/issue-198-lines-semantics.test.ts(16 例):[""]/["",""]/["\n"]的行语义、ins空行(含锚点行自身为空的形状)、[""]与del可区分、候选池排除ins且升序,以及相邻同内容行下del/replace的删除行归属;另含穷举用例 —— 长度 ≤ 3、字母表含空串 / 文本 / 裸换行 / CRLF 的全部 156 个数组,期望值由规则推导而非逐例手写。根套件 114 文件 / 1,346 例、client 6 文件 / 148 例、两侧tsc与npm run build(含 client bundle 校验)均绿。 - grep 的 ripgrep 预过滤:零命中不再当成失败(
rg -l的退出码 1):rg --files-with-matches用退出码 1 表示「一个都没匹配」,而代码把任何非零退出都当作失败 → 调用方保留全量候选列表 → 用 JS 引擎把整棵树读一遍,正好在最该省的那一种情形(无命中)烧光 64 MiB 读取预算并报「N 个文件未搜索」。现在退出码 1 是一种答案(收窄为空列表,不读任何文件);只有 ENOENT / 超时 / 退出码 2(rg 自身的错误,如模式被拒)才回退 JS 引擎。 - rg 解析加固 + 降级可观测:
@vscode/ripgrep的平台二进制若未落地(install script 被跳过、可选平台包缺失),rgPath会指向一个不存在的路径,过去每次调用都 spawn 失败;现在先校验存在性再采用,否则退回 PATH 上的rg。当预算真的被撑爆、而预过滤没有参与时,notice 会点名这一点,不再把「全量 JS 扫描」伪装成普通结果。 - 清掉已废弃的 per-file 过滤(#169 只关了开关、没删代码):
GREP_MAX_FILE_BYTES、ReadBudget的maxFileBytes维度与"too-large"跳过原因一并删除,预算只剩「整次扫描的总量」一个口径;tool-grep里那句「超限文件会被跳过」的陈旧注释也修正了。 - 新增
test/core/grep-rg-prefilter.test.ts:退出码分类(1 ≠ 失败)、依赖声明与二进制确实可用、以及真实 rg 下「零命中收窄为空列表而非 undefined」。issue-167 的集成用例同步重写:不命中的巨型文件不再进读取集(这正是报障场景),而命中且总量超预算时仍给出诚实的预算提示。
Docs
- README 版本支持表与实测数据纠正:三处陈述已被仓库现状证伪 —— ①当前构建/测试 SDK 基线写的是
0.1.6-alpha.1,实际@deepseek-ai/dsh-*全部为0.1.7-alpha.1(0.9.0 起仅支持 0.1.7,0.8.x 留给 0.1.6);②兼容性仍写“由@deepseek-ai/dsh-settingspeer 依赖 + npm 强制”,该依赖已在 0.9.0 移除,现仅@deepseek-ai/schemastery >=3.18.3;③test/注为 1,210 个测试,实测 113 文件 / 1,330 例。中英两版同步修正。 - 建立 Agent 记忆层:新增根
AGENTS.md(Level 0 入口 +l0_domains导航表)与docs/workflows/release.md(tag-first 发布流程从CLAUDE.md迁出,单一归属);CLAUDE.md退化为指向AGENTS.md的薄指针,消除双入口;知识变更记入docs/CHANGELOG-MEMORY.md。同时删去CLAUDE.md里早已移除的batch_edit工具名。
v0.9.4
Fixed
- 拒绝回显不再改写已分配的锚点(#187 现场故障):回显窗口的分配曾用重建串
fileLines.join("\n")(丢了尾换行)→ensureState判 checksum 不符 → 触发一次虚假 realign,把已分配的有效锚点释放、给同一行重铸新锚。现场表现:read给ai:405,edit 被拒后回显变成WQ:405并反过来报ai是 stale。现两处回显分配都改用文件真实内容(verifyServedRange新增content参数、由已有该参数的applyEdit传入),拿不到真实内容时跳过分配而不是近似重建。全 src 排查:无重建串调用点残留;四个锚点入口内部规范化,raw/规范化混用无害。