@flotiarenor/dsh-tool-text-editor 1.1.0 → 1.1.2

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/README.md CHANGED
@@ -17,9 +17,10 @@ byte by default) and `writeText` does not restore a file's line-ending style.
17
17
 
18
18
  On top of fidelity: **unified diffs** (with a `dry_run` preview), **automatic backups**, an **edit
19
19
  ledger**, **`grep` / `lines` anchors** so old text never has to be copied by hand, **ambiguity
20
- refusal**, **near-miss candidates** when an anchor does not match, and a **token-budgeted result
21
- text**: the model sees one stat line plus a line-budgeted diff body, while the full diff goes to the
22
- UI card (see "Result text and the token budget").
20
+ refusal**, and **near-miss candidates** when an anchor does not match.
21
+
22
+ The canonical return value of both tools, the composition of the model-facing text, and the UI card
23
+ projection are documented under "Return value".
23
24
 
24
25
  ## Implementation and requirements
25
26
 
@@ -92,23 +93,42 @@ siblings (same extension first) with no BOM by default.
92
93
 
93
94
  Both **write by default** (like the built-ins); pass `dry_run: true` to preview.
94
95
 
95
- ### Result text and the token budget
96
+ ### Return value
96
97
 
97
- Tool results land in the **append-only session history, which is never prefix-cached**, so every byte
98
- returned per call accumulates. The result is therefore split into layers:
98
+ Both tools return the same canonical value (`OUTPUT_SCHEMA`). Field contents and destinations:
99
99
 
100
- | Layer | Content | Where it goes |
100
+ | Field | Content | Destination |
101
101
  |---|---|---|
102
- | Model-facing (`brief` / `diff`) | warnings + one stat line (e.g. `replace@60 +1/-1`) + a budgeted diff body (0 context lines) | the model context |
103
- | Full record (`stdout`) | path header, the complete diff with context lines, the backup name | UI / logs / human triage |
104
- | UI card (`presentationMeta`) | the same `{ path, oldText, newText }` hunk list the built-in `edit` / `write` project | the Web UI, never the model context |
102
+ | `path` | the target path as supplied by the caller, echoed back | |
103
+ | `ok` / `wrote` / `dryRun` | outcome flags | |
104
+ | `brief` | warning lines plus one stat line, e.g. `replace@60 +1/-1` | model context |
105
+ | `diff` | a unified diff of the changed lines only (`@@` hunk headers, 0 context lines, no `---` / `+++` file headers), limited by `maxDiffLines` | model context |
106
+ | `stdout` | the full human record: path header, complete diff with context lines, backup filename | UI / logs / triage |
107
+ | `stderr` | failure reason (non-empty on failure) | model context |
108
+
109
+ The model-facing text consists of `brief` and `diff`, with the path appearing once in the leading
110
+ line; the `diff` body carries no `---` / `+++` file headers, so the path never recurs inside it. The
111
+ complete diff is additionally projected by `output.presentationMeta` into a list of
112
+ `{ path, oldText, newText }`, the same card vocabulary the built-in `edit` / `write` tools use, and
113
+ handed to the Web UI by `presentResult`; that metadata is persisted with `tool/result` and never
114
+ enters the model context.
115
+
116
+ On failure neither `brief` nor `diff` is returned: the model-facing text is `FAIL` plus the target
117
+ path, followed by the complete failure reason. That reason is produced by the core and usually
118
+ contains the workspace-relative path once more (a failure favours a complete reason).
119
+
120
+ The `diff` argument selects the detail level of the `diff` field:
121
+
122
+ | Value | Behavior |
123
+ |---|---|
124
+ | `auto` | default. The body is returned when it fits within `maxDiffLines`; otherwise it is omitted with a one-line note |
125
+ | `full` | the body is always returned; it is truncated with a one-line note when it exceeds `maxDiffLines` |
126
+ | `none` | no body is returned |
105
127
 
106
- - The path appears **exactly once** in the model-facing text, and the backup filename is no longer
107
- echoed (it stays in `stdout` and in the ledger).
108
- - The body is returned only when the change is genuinely small (`diff: auto`, threshold
109
- `maxDiffLines`); otherwise the model gets the stat line plus a one-line hint and decides for itself
110
- whether to `read`. `diff: full` forces the body, but **it is still capped** — uncapped, a 400-line
111
- rewrite echoes 7.5k tokens straight back (a measured ~1.0x amplification of the content just sent).
128
+ The body always uses 0 context lines; the `context` setting affects `stdout` and the UI card only.
129
+ `maxDiffLines` bounds the bytes returned to the model context by a single call: tool results are
130
+ appended to the session history and are not prefix-cached, so without a bound a full-file rewrite
131
+ produces a return of the same order as the content just sent (a measured ~1.0x amplification).
112
132
 
113
133
  ## Deliberate limitations
114
134
 
@@ -117,9 +137,9 @@ the tools.
117
137
 
118
138
  - **Writes bypass `ctx.fs`.** The file is written by the plugin itself, so the fs-observation policy
119
139
  (read-before-write, version freshness), the sandbox, `sandbox_permissions` escalation and Windows
120
- DACL preservation are all skipped. The atomic write is the plugin's own (same-directory temp file +
121
- fsync + rename) and the diff card comes from its own `presentationMeta` (the built-ins project the
122
- `ctx.fs` `before` / `after` instead).
140
+ DACL preservation are all skipped. The atomic write is implemented by the plugin (same-directory
141
+ temp file + fsync + rename), and the diff card is projected by the plugin's `presentationMeta`
142
+ whereas the built-ins use the `before` / `after` returned by `ctx.fs`.
123
143
  - **Line anchors are not content-verified.** `lines` and `before` / `after <line>` locate text by line
124
144
  number alone: a wrong number does not fail, it edits somewhere else. When the anchor has to be
125
145
  verifiable, use `old_text` or `grep`.
@@ -139,9 +159,9 @@ There is no Config schema: the preset row's `config:` mapping is passed through
139
159
  | `ledger` | `true` | append a JSONL record to `artifactsDir/edits.log` |
140
160
  | `artifactsDir` | `<workspace>/.dsh` | where backups and the ledger live |
141
161
  | `newFileBom` | `false` | write a UTF-8 BOM when creating a new file |
142
- | `context` | `3` | context lines in the **human-facing** diff (affects `stdout` and the UI card only; the model-facing body always uses 0) |
143
- | `diff` | `'auto'` | default policy for the model-facing diff body; a per-call `diff` argument overrides it |
144
- | `maxDiffLines` | `30` | line budget for the model-facing diff body: `auto` drops it when exceeded, `full` truncates at it |
162
+ | `context` | `3` | context lines in the diff of `stdout` and the UI card (the model-facing body always uses 0) |
163
+ | `diff` | `'auto'` | default policy for the `diff` field (`auto` / `full` / `none`); a per-call `diff` argument takes precedence |
164
+ | `maxDiffLines` | `30` | line limit for the `diff` field: `auto` omits the body when exceeded, `full` truncates at it |
145
165
  | `root` | `process.cwd()` | fallback workspace when a call has no agent session |
146
166
 
147
167
  `DSH_TEXT_EDITOR_EOL` (`lf` \| `crlf`) overrides the line-ending inference for **new** files.
@@ -163,11 +183,10 @@ refusal, usage errors, binary/invalid-UTF-8 refusal, `.dsh/` and outside-workspa
163
183
  EOL inference, multi-hunk diffs, end-of-file newline changes and concurrent writes — **plus a
164
184
  plugin-layer suite** that drives `apply()` with a fake context and asserts tool registration, the
165
185
  guidance section, that every returned value satisfies `OUTPUT_SCHEMA`, the `render()` text, and the
166
- config plumbing (`root` / `backup` / `ledger` / `newFileBom`) — **and a token-budget suite**: a
167
- full-file rewrite must not echo the content back, a small edit still shows the changed lines, the
168
- `diff: none` / `full` boundaries hold, the path appears once, and the complete diff flows only into
169
- the UI card. The token budget is a design constraint rather than an implementation detail, so it
170
- carries its own regression test; otherwise one "let me print a bit more here" quietly removes it.
186
+ config plumbing (`root` / `backup` / `ledger` / `newFileBom`) — **and a return-value suite**: a
187
+ full-file rewrite must not echo the content back, a small edit must still report the changed lines,
188
+ the three `diff` values must hold their documented boundaries, the path must appear once, and the
189
+ complete diff must be projected only through `presentationMeta`.
171
190
 
172
191
  `tools/gen-schema.mjs` needs an installed `@deepseek-ai/dsh-tools`: it looks for one under the dsh
173
192
  profile's `node_modules` and under the npm global prefix, and `DSH_TOOLS_ENTRY` overrides that lookup.
@@ -176,7 +195,7 @@ It exits 2 when it cannot find one.
176
195
  ## Layout
177
196
 
178
197
  ```
179
- lib/core.mjs # the core: BOM/EOL, anchors, matching, diff (with the result budget), backups, ledger, atomic write, per-target lock
198
+ lib/core.mjs # the core: BOM/EOL, anchors, matching, diff, backups, ledger, atomic write, per-target lock
180
199
  lib/editor.mjs # the plugin: schemas, validation, tool registration (zero-dep ESM, no build)
181
200
  preset/preset.yml # preset name/description, as dsh lists it
182
201
  scripts/install-preset.mjs # derives the user preset from the local dsh installation
package/README.zh.md CHANGED
@@ -16,9 +16,9 @@
16
16
  BOM 字节),且 `writeText` 不按原文件风格还原行尾。
17
17
 
18
18
  在保真之外,本插件还提供:**统一 diff**(可用 dry-run 预览)、**写入前自动备份**、**编辑台账**、
19
- **`grep` / `lines` 锚点**(无需人工誊抄原文)、**歧义时拒绝写入**、**最接近候选**提示,以及
20
- **面向 token 预算的结果文本**:模型只看到一行统计加受行数预算约束的 diff 正文,完整 diff 走 UI 卡片
21
- (见「结果文本与 token 预算」)。
19
+ **`grep` / `lines` 锚点**(无需人工誊抄原文)、**歧义时拒绝写入**,以及**最接近候选**提示。
20
+
21
+ 两个工具的规范返回值、模型可见文本的构成与 UI 卡片的投影方式见「返回值」。
22
22
 
23
23
  ## 实现与依赖
24
24
 
@@ -94,30 +94,47 @@ dsh --profile web --dump-config # 应当能看到 "# == @flotiarenor/dsh-tool-
94
94
  `file_path` + `content`(另接受与 `edit_text` 相同的 `diff` / `dry_run` / `note`);目标不存在时自动
95
95
  新建,覆盖前先备份。新建文件的行尾风格取自同目录的多数派(同扩展名优先),默认不写 BOM。
96
96
 
97
- ### 结果文本与 token 预算
97
+ ### 返回值
98
+
99
+ 两个工具返回同一份规范值(`OUTPUT_SCHEMA`)。字段内容与去向如下:
100
+
101
+ | 字段 | 内容 | 去向 |
102
+ | --------------------- | ------------------------------------------------------------------------ | ---------------- |
103
+ | `path` | 调用方给出的目标路径(原样回填) | — |
104
+ | `ok` / `wrote` / `dryRun` | 执行结果标志 | — |
105
+ | `brief` | 警告行与一行统计,如 `replace@60 +1/-1` | 模型上下文 |
106
+ | `diff` | 只含 `@@` 块头与改动行的 unified diff(0 上下文行、无 `---` / `+++` 文件头),受 `maxDiffLines` 限制 | 模型上下文 |
107
+ | `stdout` | 人读全文:路径头、含上下文行的完整 diff、备份文件名 | UI / 日志 / 排查 |
108
+ | `stderr` | 失败原因(失败时非空) | 模型上下文 |
109
+
110
+ 模型可见文本由 `brief` 与 `diff` 组成,路径在起始行出现一次;`diff` 正文不含 `---` / `+++` 文件头,
111
+ 因此路径不会在正文中再次出现。完整 diff 另经 `output.presentationMeta` 投影为
112
+ `{ path, oldText, newText }` 列表,与原生 `edit` / `write` 的卡片词汇同形,由 `presentResult` 交给
113
+ Web UI;该元数据随 `tool/result` 持久化,不进入模型上下文。
114
+
115
+ 失败时不返回 `brief` 与 `diff`:模型可见文本为 `FAIL` 加目标路径,其后是完整的失败原因;原因文本由
116
+ 核心生成,其中通常再包含一次工作区相对路径(失败路径优先保证原因完整)。
98
117
 
99
- 工具结果会进入 **append-only 的会话历史,永远不会被前缀缓存**,所以每次调用回吐的字节逐次累积。
100
- 因此本插件把结果分成两层:
118
+ `diff` 参数决定 `diff` 字段的详细程度:
101
119
 
102
- | | 内容 | 去向 |
103
- | ------------------------ | ------------------------------------------------------------------------------- | --------------------------- |
104
- | 模型可见(`brief` / `diff`) | 警告 + 一行统计(如 `replace@60 +1/-1`)+ 受预算约束的 diff 正文(0 上下文行) | 模型上下文 |
105
- | 完整版(`stdout`) | 路径头、含上下文行的完整 diff、备份名 | UI / 日志 / 人工排查 |
106
- | UI 卡片(`presentationMeta`) | 与原生 `edit` / `write` 同形的 `{ path, oldText, newText }` hunk 列表 | Web UI(**不进模型上下文**) |
120
+ | 取值 | 行为 |
121
+ | --------- | -------------------------------------------------------------------------------- |
122
+ | `auto` | 默认。正文不超过 `maxDiffLines` 行时给出;超出时不给出正文,附一行省略提示 |
123
+ | `full` | 始终给出正文;超过 `maxDiffLines` 行时截断,附一行截断提示 |
124
+ | `none` | 不给出正文 |
107
125
 
108
- - 路径在模型可见文本里**只出现一次**,备份文件名不再回吐(它留在 `stdout` 与编辑台账里)。
109
- - 正文默认只在改动确实很小时才给(`diff: auto`,阈值见 `maxDiffLines`);否则只给统计行并提供一行提示,
110
- 让模型自己决定要不要 `read`。`diff: full` 强制给正文,但**仍然封顶** —— 没有封顶时,一次"整体重写
111
- 400 行"会把 7.5k tokens 原样回吐(实测回吐量 ≈ 输入内容的 1.0x)。
126
+ 正文固定使用 0 上下文行;`context` 配置只影响 `stdout` 与 UI 卡片。`maxDiffLines` 的作用是限制单次
127
+ 调用回吐到模型上下文的字节数——工具结果按追加方式进入会话历史,不参与前缀缓存,因此不含上限时,
128
+ 整文件重写会产生与输入内容等量级的回吐(实测放大率 1.0x)。
112
129
 
113
130
  ## 已知限制
114
131
 
115
132
  以下均为有意的设计取舍,而非缺陷;采用前请对照自身场景确认。
116
133
 
117
134
  - **写入不经由 `ctx.fs`。** 文件由本插件直接写入,因此不经过 fs 观察策略(先读后写、版本新鲜度校验)、
118
- 沙箱与 `sandbox_permissions` 审批升权,也不保留 Windows DACL。原子写由本插件自己保证
119
- (同目录临时文件 + fsync + rename),diff 卡片则由 `presentationMeta` 自行提供(原生工具用的是
120
- `ctx.fs` `before` / `after`)。
135
+ 沙箱与 `sandbox_permissions` 审批升权,也不保留 Windows DACL。原子写由本插件自行实现
136
+ (同目录临时文件 + fsync + rename);diff 卡片由本插件的 `presentationMeta` 提供,原生工具则取自
137
+ `ctx.fs` 返回的 `before` / `after`。
121
138
  - **行号锚点不做内容校验。** `lines` 与 `before` / `after <行号>` 仅按行号定位:行号有误不会报错,
122
139
  改动会落在非预期位置;定位需要可校验时,请改用 `old_text` 或 `grep`。
123
140
  - **同目标串行仅限本进程。** 进程内按目标路径排队,并配合原子写,故并行的工具调用不会相互覆盖;
@@ -135,9 +152,9 @@ dsh --profile web --dump-config # 应当能看到 "# == @flotiarenor/dsh-tool-
135
152
  | `ledger` | `true` | 往 `artifactsDir/edits.log` 追加一条 JSONL 记录 |
136
153
  | `artifactsDir` | `<工作区>/.dsh` | 备份与台账所在目录 |
137
154
  | `newFileBom` | `false` | 新建文件时是否写 UTF-8 BOM |
138
- | `context` | `3` | **人读** diff 的上下文行数(只影响 `stdout` 与 UI 卡片;模型可见正文固定 0 行) |
139
- | `diff` | `'auto'` | 模型可见 diff 正文的默认策略,可被逐调用的 `diff` 参数覆盖 |
140
- | `maxDiffLines` | `30` | 模型可见 diff 正文的行数预算:`auto` 超了就只给统计行,`full` 按它截断 |
155
+ | `context` | `3` | `stdout` 与 UI 卡片中 diff 的上下文行数(模型可见正文固定 0 行) |
156
+ | `diff` | `'auto'` | `diff` 字段的默认策略(`auto` / `full` / `none`);逐调用的 `diff` 参数优先 |
157
+ | `maxDiffLines` | `30` | `diff` 字段的行数上限:`auto` 超出时不给出正文,`full` 超出时截断 |
141
158
  | `root` | `process.cwd()` | 无 agent 会话时的回退工作区 |
142
159
 
143
160
  环境变量 `DSH_TEXT_EDITOR_EOL`(`lf` \| `crlf`)可覆盖**新建文件**的行尾推断。
@@ -155,9 +172,8 @@ node tools/gen-schema.mjs # 内嵌 schema 是否仍与作者 DSL 一致
155
172
  二进制与非法 UTF-8、路径护栏(`.dsh/`、工作区之外)、行尾多数派推断、跨多个 hunk、末尾换行差异、
156
173
  并发写入不产生半截文件;**并含一层插件层断言**:以模拟 ctx 驱动 `apply()`,验证工具注册、引导段
157
174
  身份、每个返回值均满足 `OUTPUT_SCHEMA`、`render()` 输出,以及 config 透传(`root` / `backup` /
158
- `ledger` / `newFileBom`);**还有一层回吐预算断言**:整体重写不得回吐内容、小改动仍给改动行、
159
- `diff: none` / `full` 的边界、路径只出现一次、完整 diff 只走 UI 卡片。token 预算是设计约束而非实现
160
- 细节,因此它自带回归测试 —— 否则一次"顺手多打印一点"就能把它悄悄取消掉。
175
+ `ledger` / `newFileBom`);**以及一层返回值约束断言**:整文件重写不回显内容、小改动仍给出改动行、
176
+ `diff` 三种取值的行为边界、路径仅出现一次、完整 diff 只经 `presentationMeta` 投影。
161
177
 
162
178
  `tools/gen-schema.mjs` 需要一份装有 `@deepseek-ai/dsh-tools` 的 dsh:它会在 dsh profile 的
163
179
  `node_modules` 与 npm 全局目录中自动查找,也可用 `DSH_TOOLS_ENTRY` 显式指定;找不到入口时退出码为 2。
@@ -165,7 +181,7 @@ node tools/gen-schema.mjs # 内嵌 schema 是否仍与作者 DSL 一致
165
181
  ## 目录结构
166
182
 
167
183
  ```
168
- lib/core.mjs # 编辑核心:BOM/行尾、锚点、匹配、diff(含回吐预算)、备份、台账、原子写、同目标串行
184
+ lib/core.mjs # 编辑核心:BOM/行尾、锚点、匹配、diff、备份、台账、原子写、同目标串行
169
185
  lib/editor.mjs # 插件本体:schema、参数校验、工具注册(零依赖 ESM,无构建)
170
186
  preset/preset.yml # preset 的名字/描述(dsh 列表里显示的内容)
171
187
  scripts/install-preset.mjs # 从本机 dsh 派生用户 preset
package/lib/core.mjs CHANGED
@@ -20,11 +20,11 @@
20
20
  * 备份与台账的格式:`<工作区>/.dsh/backups/<扁平化绝对路径>@<时间戳>` 与
21
21
  * `<工作区>/.dsh/edits.log` 里的 JSONL 记录(字段见 `appendLedger`)。
22
22
  *
23
- * 为什么要给 diff 定预算(`diff` / `maxDiffLines`):工具结果是 **append-only 的会话历史,
24
- * 永远不会被前缀缓存**,所以每次调用回吐的字节会逐次累积。而"新建/整体重写"的 unified diff
25
- * 是每一行都带 `+` 的整文件回显 —— 实测回吐量 输入内容的 1.0x,等于把刚发出去的内容再发
26
- * 一遍。因此正文默认只在改动确实很小时才给(`auto`),其余情况只给统计行,让模型自己决定
27
- * 要不要 `read` 文件;`none` 完全不回吐,`full` 强制回吐但仍受行数封顶。
23
+ * `diff` / `maxDiffLines` 的由来:工具结果按追加方式进入会话历史,不参与前缀缓存,因此单次调用
24
+ * 回吐的字节随调用次数累积。"新建 / 整体重写" 的 unified diff 每一行都带 `+`,等同于整文件回显
25
+ * ——实测回吐量与输入内容同量级(放大率 ≈ 1.0x)。故模型可见正文默认仅在改动较小时给出
26
+ * (`auto`),其余情况只给统计行,由调用方决定是否另行读取文件;`none` 不给出正文,`full` 始终
27
+ * 给出正文,但同样受行数上限约束。
28
28
  */
29
29
 
30
30
  import {
@@ -577,15 +577,15 @@ export function diffStat(before, after) {
577
577
  }
578
578
 
579
579
  /**
580
- * 按预算裁剪**模型可见**的 diff 正文。
580
+ * 按上限裁剪**模型可见**的 diff 正文。
581
581
  *
582
- * 只有 `full` 会强制给正文,且仍然封顶 —— 封顶是这里存在的全部理由:没有它,一次"整体重写
583
- * 400 行"就会把 7.5k tokens 原样回吐(实测放大率 1.02x),而模型刚刚才把这段内容发出去。
582
+ * `none` 不返回正文;`auto` 仅在正文不超过上限时返回;`full` 始终返回正文,超过上限时截断并附
583
+ * 一行说明。上限的作用是避免整文件重写产生与输入内容同量级的回吐(实测放大率 1.0x)。
584
584
  *
585
585
  * @param text - 已生成的 unified diff 文本(调用方传 0 上下文行的那份,只保留改动行)。
586
586
  * @param mode - `auto` | `full` | `none`(见 `DIFF_MODES`)。
587
- * @param maxLines - 正文行数预算,默认 `DEFAULT_MAX_DIFF_LINES`。
588
- * @returns 模型可见的正文;裁剪时带一行提示(省略或截断),可能为空字符串。
587
+ * @param maxLines - 正文行数上限,默认 `DEFAULT_MAX_DIFF_LINES`。
588
+ * @returns 模型可见的正文;裁剪时附一行说明(省略或截断),可能为空字符串。
589
589
  */
590
590
  export function diffBudget(text, mode, maxLines = DEFAULT_MAX_DIFF_LINES) {
591
591
  if (mode === 'none' || text === '') return ''
@@ -601,11 +601,29 @@ export function diffBudget(text, mode, maxLines = DEFAULT_MAX_DIFF_LINES) {
601
601
  }
602
602
 
603
603
  /**
604
- * unified diff 文本解析成 UI diff 卡片的词汇:`{ path, oldText, newText }` 列表,
604
+ * 去掉 unified diff 顶部的 `--- a/…` `+++ b/…` 头两行。
605
+ *
606
+ * 模型可见正文里路径已在结果首行出现过一次,头部两行属于重复;标准形式仍完整保留在 `stdout`
607
+ * 与 UI 卡片中。
608
+ *
609
+ * @param text - unified diff 文本。
610
+ * @returns 去掉文件头之后的文本(没有头部时原样返回)。
611
+ */
612
+ function withoutDiffHeaders(text) {
613
+ const lines = text.split('\n')
614
+ let from = 0
615
+ while (from < lines.length && (lines[from].startsWith('---') || lines[from].startsWith('+++'))) {
616
+ from += 1
617
+ }
618
+ return lines.slice(from).join('\n')
619
+ }
620
+
621
+ /**
622
+ * 把 unified diff 文本解析为 UI diff 卡片的词汇:`{ path, oldText, newText }` 列表,
605
623
  * 与原生 `edit` / `write` 的 `presentationMeta` 同形(纯新增的 hunk 用 `oldText: null`)。
606
624
  *
607
- * 只服务**人类**:这条路径不经过模型上下文,所以完整 diff(含上下文行)走这里,模型拿到的
608
- * `diffBudget` 裁过的那份。
625
+ * 该投影面向人类,不经过模型上下文:完整 diff(含上下文行)由此提供,模型侧使用经
626
+ * `diffBudget` 裁剪的正文。
609
627
  *
610
628
  * @param text - unified diff 文本。
611
629
  * @param path - 卡片上标注的路径。
@@ -995,11 +1013,11 @@ export async function applyPlan(plan, context) {
995
1013
  const kinds = [...new Set(applied.map(([k, s, e]) => (k === 'replace' ? `replace@${s}${e !== s ? `-${e}` : ''}` : k)))]
996
1014
  .join('、')
997
1015
 
998
- // 模型可见的那一份:警告 + 一行统计(+A/-B),加受预算约束的正文。
999
- // 正文用 0 上下文行 —— 模型刚做完这次编辑,周边内容它要么已经读过,要么可以再 read;
1000
- // 完整 diff(带 context 行)只走 stdout 与 UI 卡片,不进这条路径。
1016
+ // 模型可见部分:警告 + 一行统计(+A/-B)+ 经上限裁剪的正文。
1017
+ // 正文使用 0 上下文行:调用方刚完成这次编辑,周边内容或已读取过,或可自行读取。
1018
+ // 完整 diff(含上下文行)只出现在 stdout 与 UI 卡片中,不进入这条路径。
1001
1019
  const brief = [...warnings, `${kinds} +${stat.added}/-${stat.removed}`].join('\n')
1002
- const visibleDiff = diffBudget(unifiedDiff(label, original, output, 0), diffMode, maxDiffLines)
1020
+ const visibleDiff = diffBudget(withoutDiffHeaders(unifiedDiff(label, original, output, 0)), diffMode, maxDiffLines)
1003
1021
 
1004
1022
  if (plan.dryRun) {
1005
1023
  return {
package/lib/editor.mjs CHANGED
@@ -22,9 +22,9 @@
22
22
  * * 写盘**不经过** `ctx.fs`:绕过 fs 观察策略(先读后写 / 版本新鲜度)、沙箱与
23
23
  * `sandbox_permissions` 审批升权、原生原子写与 Windows DACL 保留。原生原子写与本插件的
24
24
  * 临时文件 + fsync + rename 等价,这段绕过的代价已在 README 写明。
25
- * * 模型可见的结果文本**刻意很省**:路径只出现一次,只有一行统计加受预算约束的 diff 正文
26
- * (`brief` / `diff`);备份名等细节只留在台账与 `stdout` 里。完整 diff
27
- * `presentationMeta` UI 卡片 —— 那条路径不进模型上下文。理由见 `lib/core.mjs` 头部。
25
+ * * 模型可见文本受上限约束:路径仅出现一次,正文为一行统计加经 `diffBudget` 裁剪的 diff
26
+ * (`brief` / `diff`);备份文件名等细节只保留在台账与 `stdout` 中。完整 diff
27
+ * `presentationMeta` 投影为 UI 卡片,该路径不进入模型上下文。理由见 `lib/core.mjs` 头部。
28
28
  * * `lines` / `before <行号>` 是**盲锚点**:行号错了不会报错,会改在别的地方。
29
29
  * * 同目标串行只覆盖**本进程内**;跨进程(另一个 dsh 实例、你手边的编辑器)仍可能互相覆盖。
30
30
  *
@@ -127,11 +127,10 @@ export const OUTPUT_SCHEMA = {
127
127
  }
128
128
 
129
129
  /**
130
- * 模型可见的结果文本:**一行统计(+ 受预算约束的 diff 正文)**。
130
+ * 模型可见的结果文本:**一行统计、受上限约束的 diff 正文**。
131
131
  *
132
- * 这里刻意不复述任何东西:路径只出现一次,备份名只留在台账与 `stdout` 里,完整 diff 交给
133
- * UI 卡片。工具结果是 append-only 的会话历史、永不缓存,所以每个字节都要问一句"模型真需要吗";
134
- * 失败时相反 —— 原因必须完整,因为下一次调用要靠它自救。
132
+ * 成功路径只返回 `brief` `diff`:路径仅出现一次,备份文件名只保留在台账与 `stdout` 中,
133
+ * 完整 diff UI 卡片承载。失败路径相反——原因必须完整,它决定调用方的下一次调用。
135
134
  */
136
135
  function renderResult(_args, value) {
137
136
  const where = value.path === '' ? '' : ' ' + value.path
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flotiarenor/dsh-tool-text-editor",
3
- "version": "1.1.0",
4
- "description": "Byte-faithful text editing tools (edit_text / write_text) for DeepSeek Harness: they preserve a UTF-8 BOM and the file's own CRLF/LF style (the built-in write/edit tools do not), back the previous content up, accept grep/lines anchors, and return a token-budgeted result (one stat line plus a bounded diff body, with the full diff going to the UI card instead of the model context). Pure Node and in-process: no external runtime, no child process, no third-party package, no build step, no imports beyond node: builtins.",
3
+ "version": "1.1.2",
4
+ "description": "Byte-faithful text editing tools (edit_text / write_text) for DeepSeek Harness: they preserve a UTF-8 BOM and the file's own CRLF/LF style (the built-in write/edit tools do not), back the previous content up, accept grep/lines anchors, and return a bounded result: one stat line plus a diff body limited by maxDiffLines, while the complete diff is projected to the UI card and never enters the model context. Pure Node and in-process: no external runtime, no child process, no third-party package, no build step, no imports beyond node: builtins.",
5
5
  "type": "module",
6
6
  "main": "lib/editor.mjs",
7
7
  "exports": {