dsh-hashline-edit 0.2.2 → 0.3.0

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.
Files changed (118) hide show
  1. package/README.md +57 -45
  2. package/cordis.patch.yml +9 -3
  3. package/lib/client.js +231 -0
  4. package/lib/index.js +3298 -3957
  5. package/lib/types/dsh/contracts.d.ts +12 -3
  6. package/lib/types/dsh/fs.d.ts +3 -0
  7. package/lib/types/dsh/guidance.d.ts +12 -6
  8. package/lib/types/dsh/hide.d.ts +79 -0
  9. package/lib/types/dsh/host-compat.d.ts +7 -78
  10. package/lib/types/dsh/index.d.ts +53 -10
  11. package/lib/types/dsh/metrics/events.d.ts +1 -1
  12. package/lib/types/dsh/metrics/observer.d.ts +1 -1
  13. package/lib/types/dsh/sandbox.d.ts +2 -2
  14. package/lib/types/dsh/shell-hook.d.ts +1 -1
  15. package/lib/types/dsh/tools/apply-patch.d.ts +38 -0
  16. package/lib/types/dsh/tools/read.d.ts +3 -3
  17. package/lib/types/src/apply-patch/commit.d.ts +49 -0
  18. package/lib/types/src/apply-patch/grammar.d.ts +29 -0
  19. package/lib/types/src/apply-patch/lifecycle.d.ts +32 -0
  20. package/lib/types/src/apply-patch/plan.d.ts +50 -0
  21. package/lib/types/src/apply-patch/render.d.ts +51 -0
  22. package/lib/types/src/apply-patch/resolve.d.ts +129 -0
  23. package/lib/types/src/apply-patch/tool.d.ts +54 -0
  24. package/lib/types/src/apply-patch/types.d.ts +362 -0
  25. package/lib/types/src/domain-errors.d.ts +16 -2
  26. package/lib/types/src/edit-undo.d.ts +11 -2
  27. package/lib/types/src/file-content/index.d.ts +1 -1
  28. package/lib/types/src/file-content/preview.d.ts +1 -1
  29. package/lib/types/src/fs-lifecycle.d.ts +44 -0
  30. package/lib/types/src/hashline/alphabet.d.ts +4 -3
  31. package/lib/types/src/hashline/hash-identity.d.ts +12 -7
  32. package/lib/types/src/host-utils.d.ts +63 -0
  33. package/lib/types/src/mutation-engine/edit-request.d.ts +34 -0
  34. package/lib/types/src/mutation-engine/pipeline.d.ts +8 -1
  35. package/lib/types/src/mutation-engine/types.d.ts +40 -50
  36. package/lib/types/src/served-session/serve-render.d.ts +6 -0
  37. package/lib/types/src/undo-store.d.ts +17 -1
  38. package/package.json +37 -21
  39. package/prompts/apply-patch-guidelines.md +4 -0
  40. package/prompts/apply-patch.md +1 -0
  41. package/prompts/read-guidelines.md +6 -6
  42. package/prompts/read.md +1 -1
  43. package/prompts/undo-last-edit-guidelines.md +2 -2
  44. package/prompts/undo-last-edit-snippet.md +1 -1
  45. package/prompts/undo-last-edit.md +1 -1
  46. package/prompts/zh/apply-patch-guidelines.md +4 -0
  47. package/prompts/zh/apply-patch.md +1 -0
  48. package/prompts/zh/read-guidelines.md +6 -0
  49. package/prompts/zh/read-snippet.md +1 -0
  50. package/prompts/zh/read.md +1 -0
  51. package/prompts/zh/undo-last-edit-guidelines.md +2 -0
  52. package/prompts/zh/undo-last-edit-snippet.md +1 -0
  53. package/prompts/zh/undo-last-edit.md +1 -0
  54. package/src/apply-patch/commit.ts +190 -0
  55. package/src/apply-patch/grammar.ts +620 -0
  56. package/src/apply-patch/index.ts +17 -0
  57. package/src/apply-patch/lifecycle.ts +195 -0
  58. package/src/apply-patch/plan.ts +800 -0
  59. package/src/apply-patch/render.ts +137 -0
  60. package/src/apply-patch/resolve.ts +470 -0
  61. package/src/apply-patch/tool.ts +257 -0
  62. package/src/apply-patch/types.ts +387 -0
  63. package/src/cut-repair.ts +77 -11
  64. package/src/domain-errors.ts +35 -3
  65. package/src/edit-undo.ts +379 -25
  66. package/src/file-content/index.ts +1 -1
  67. package/src/file-content/preview.ts +1 -1
  68. package/src/file-content/served-header.ts +1 -1
  69. package/src/fs-lifecycle.ts +201 -0
  70. package/src/hash-store.ts +18 -1
  71. package/src/hashline/alphabet.ts +7 -7
  72. package/src/hashline/hash-identity.ts +12 -7
  73. package/src/host-utils.ts +224 -0
  74. package/src/mutation-engine/edit-request.ts +39 -0
  75. package/src/mutation-engine/pipeline.ts +198 -27
  76. package/src/mutation-engine/types.ts +39 -57
  77. package/src/mutation-queue.ts +1 -1
  78. package/src/prompts.ts +3 -30
  79. package/src/served-session/serve-render.ts +2 -2
  80. package/src/undo-store.ts +74 -5
  81. package/lib/types/dsh/tools/edit.d.ts +0 -10
  82. package/lib/types/src/edit-tool.d.ts +0 -41
  83. package/lib/types/src/mutation-engine/engine.d.ts +0 -31
  84. package/lib/types/src/payload-contract.d.ts +0 -135
  85. package/prompts/edit-guidelines.md +0 -10
  86. package/prompts/edit-snippet.md +0 -1
  87. package/prompts/edit.md +0 -1
  88. package/prompts/read-snippet.md +0 -1
  89. package/src/bash-classifier.ts +0 -1626
  90. package/src/bash-conversion/decide.ts +0 -812
  91. package/src/bash-display-transform.ts +0 -50
  92. package/src/bash-multifile-config.ts +0 -239
  93. package/src/bash-view-config.ts +0 -158
  94. package/src/edit-normalize.ts +0 -8
  95. package/src/edit-render.ts +0 -161
  96. package/src/edit-tool.ts +0 -118
  97. package/src/edit.ts +0 -136
  98. package/src/integrations/pi-lens/command.ts +0 -712
  99. package/src/integrations/pi-lens/config.ts +0 -148
  100. package/src/integrations/pi-lens/io-bridge-adapter.ts +0 -234
  101. package/src/integrations/pi-lens/io-bridge.ts +0 -112
  102. package/src/integrations/pi-lens/settings-overlay.ts +0 -272
  103. package/src/lifecycle-hooks/index.ts +0 -1992
  104. package/src/lifecycle-hooks/types.ts +0 -64
  105. package/src/mutated-files.ts +0 -75
  106. package/src/mutation-engine/engine.ts +0 -239
  107. package/src/mutation-engine/index.ts +0 -17
  108. package/src/payload-contract.ts +0 -685
  109. package/src/preview-controller.ts +0 -84
  110. package/src/read.ts +0 -865
  111. package/src/search-hook.ts +0 -41
  112. package/src/store-config.ts +0 -73
  113. package/src/store-janitor-warning.ts +0 -169
  114. package/src/store-janitor.ts +0 -539
  115. package/src/store-kernel/settings.ts +0 -233
  116. package/src/store.ts +0 -154
  117. package/src/tui-presenter.ts +0 -134
  118. package/src/write-hook.ts +0 -130
package/README.md CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  Hash-anchored file editing for [DeepSeek Harness](https://github.com/deepseek-ai) (dsh) on Windows.
4
4
 
5
- `dsh-hashline-edit` is a dsh bundle that shadows the host's built-in `read` and `edit` tools with hashline equivalents and adds `undo_last_edit`. Every line a served read shows carries a stable 3- or 4-character base62 anchor (`A–Z`, `a–z`, `0–9`) derived from the file's canonical path and the line's content (the width is a runtime setting; see [Anchor width](#anchor-width)). An edit names two anchors and new text; it never re-types existing code. Anchors for unchanged lines stay stable across edits, and every successful edit returns fresh anchors for the changed lines, so chained edits need no re-read.
5
+ `dsh-hashline-edit` is a dsh bundle that shadows the host's built-in `read` with hash-anchored output, registers `apply_patch` as the only file-mutation entry point, and adds `undo_last_edit`. The native `write`/`edit`/`str_replace_editor` surface is hidden; there is no pi host support, because this package targets DeepSeek Harness only. Every line a served read shows carries a stable 3- or 4-character base62 anchor (`A–Z`, `a–z`, `0–9`; the width is a build-time setting, see [Anchor width](#anchor-width)) derived from the file's canonical path and the line's content. An `apply_patch` hunk either names served anchors or matches file content; the model never re-types existing code. Anchors for unchanged lines stay stable across edits, and every successful `apply_patch` call returns fresh anchors for the changed lines, so chained calls need no re-read.
6
6
 
7
- The semantic core is the [pi-better-edit](https://github.com/Rianico/pi-better-edit) v2.16.0 hashline engine (MIT). The `dsh/` tree is this repository's DeepSeek Harness host layer: tool registration, filesystem bridge, UTF-8-only admission, prompt guidance, shell integration, write integration and telemetry. See [`dsh/README.md`](dsh/README.md) for the host-layer map.
7
+ The semantic core is the [pi-better-edit](https://github.com/Rianico/pi-better-edit) v2.16.0 hashline engine (MIT), carried as an internal engine; the pi host integration has been removed. The `dsh/` tree is this repository's DeepSeek Harness host layer: tool registration, filesystem bridge, UTF-8-only admission, prompt guidance, shell integration, write integration and telemetry. See [`dsh/README.md`](dsh/README.md) for the host-layer map.
8
8
 
9
9
  ## Requirements
10
10
 
@@ -13,13 +13,13 @@ The semantic core is the [pi-better-edit](https://github.com/Rianico/pi-better-e
13
13
  | OS | Windows — this branch targets Windows only (see [`dsh/UPSTREAM.md`](dsh/UPSTREAM.md)) |
14
14
  | Node.js | ≥ 24.0.0 |
15
15
  | pnpm | 12.x |
16
- | DeepSeek Harness | plugin-capable build; the repository is developed against `@deepseek-ai/dsh-*@0.2.1-rc.2` |
16
+ | DeepSeek Harness | plugin-capable build; the repository is developed against `@deepseek-ai/dsh-*@0.2.2-rc.1` (host packages are declared as peer dependencies) |
17
17
 
18
18
  ### Anchor width (3 or 4)
19
19
 
20
20
  The anchor width is baked into the build. Set `PI_BETTER_EDIT_ANCHOR_WIDTH` when you build or pack:
21
21
 
22
- - `PI_BETTER_EDIT_ANCHOR_WIDTH=3 pnpm run build` (or `pnpm pack` / install) produces a fully 3-char artifact.
22
+ - `PI_BETTER_EDIT_ANCHOR_WIDTH=3 pnpm run build` (and `pnpm pack`) produces a fully 3-char artifact.
23
23
  - unset or `4` builds the 4-char default.
24
24
 
25
25
  | Value | Anchor width | Usable space | Anchor generation |
@@ -52,7 +52,7 @@ For a tarball, pack first and add the archive:
52
52
 
53
53
  ```sh
54
54
  pnpm pack
55
- dsh plugin add ./dsh-hashline-edit-0.1.0.tgz
55
+ dsh plugin add ./dsh-hashline-edit-0.2.1.tgz
56
56
  ```
57
57
 
58
58
  The bundle ships `lib/index.js` plus `lib/types/**`; `prompts/` must sit next to `lib/` in the installed package (it is in `files`). Guidance override files are materialized under `<DSH_HOME>/plugins/dsh-hashline-edit/guidance/` and are shared between profiles.
@@ -71,7 +71,7 @@ p9rX│ return total;
71
71
  a1bX│}
72
72
  ```
73
73
 
74
- Copy only the 4 characters before `│` into `anchor_from` / `anchor_to`. An edit targets an inclusive range and returns a diff whose modified and context rows carry fresh anchors:
74
+ Copy only the 4 characters before `│` into `anchor_from` / `anchor_to`. An `apply_patch` identity hunk targets an inclusive served range; a successful call returns a diff whose modified and context rows carry fresh anchors:
75
75
 
76
76
  ```text
77
77
  - szJx│ let total = 0;
@@ -100,48 +100,58 @@ read({
100
100
  })
101
101
  ```
102
102
 
103
- - **Served mode** returns one `HASH│content` row per line and records every shown row as a lease for this session. A windowed read also includes a `[Lines A-B of N total]` page header. Every row shown is usable by one subsequent edit, even when the windows are far apart.
104
- - **Verbatim mode** returns the same page as plain text with no anchors and serves nothing; an edit cannot target those lines.
103
+ - **Served mode** returns one `HASH│content` row per line and records every shown row as a lease for this session. A windowed read also includes a `[Lines A-B of N total]` page header. Every row shown is usable by one subsequent `apply_patch`, even when the windows are far apart.
104
+ - **Verbatim mode** returns the same page as plain text with no anchors and serves nothing; an `apply_patch` identity hunk cannot target those lines.
105
105
  - A small requested window — up to 61 lines after folding — may widen to its enclosing function, method, class, section or paragraph; a `[widened …]` note names the block that was served. Expansion applies in both modes. Languages: TypeScript/TSX, Python, Go, Rust, Java and Vue through pinned tree-sitter wasm grammars (fetched lazily and verified against a committed SHA-256), plus Bash through the existing `unbash` parser (no wasm). Vue widens a top-level `template`/`script`/`style` block without parsing embedded JS/TS. SQL is not supported: real migrations largely fail to parse and `.tql` is a Java template, not SQL (see [`docs/spec/sql-vue-feasibility-gate.md`](docs/spec/sql-vue-feasibility-gate.md)).
106
- - `offset`/`limit` and `windows` are page requests, not anchors: only `served` output grants edit leases.
106
+ - `offset`/`limit` and `windows` are page requests, not anchors: only `served` output grants mutation leases.
107
107
 
108
- ### `edit`
108
+ ### `apply_patch`
109
109
 
110
- ```ts
111
- edit({
112
- file: string, // default target for items that omit `file`
113
- edits: [{
114
- file?: string, // optional per-item target; falls back to the call's `file`
115
- anchor_from: string, // bare 4-char anchor of the first range line (inclusive)
116
- anchor_to: string, // bare 4-char anchor of the last range line (inclusive)
117
- text?: string, // by-value content; "" deletes an in-place range
118
- text_ref?: { // by-reference content from a served span
119
- anchor_from: string,
120
- anchor_to: string,
121
- file?: string, // another served file (foreign-source copy/cut)
122
- mode: "copy" | "cut", // required: copy keeps the source, cut retires the span
123
- },
124
- at?: "in-place" | "before" | "after", // default "in-place"
125
- }],
126
- mode?: "general" | "literal", // "general" refuses served-row echoes; "literal" declares them intended
127
- })
128
- ```
110
+ The only file-mutation entry point. One call carries one Codex patch document and commits the whole
111
+ batch as one correlated transaction (pre-mirror, undo rows, deterministic member order, all-or-nothing
112
+ rollback, then per-file store commits).
129
113
 
130
- Each item carries exactly one of `text` or `text_ref`; supplying both or neither is refused. An item without `file` targets the call's top-level file; an item that names one may target another served file, so one call may edit several files. The whole call commits as one correlated transaction (pre-mirror, undo rows, ordered renames, all-or-nothing rollback, then per-file store commits) and a refusal leaves every file untouched. `before`/`after` insert at the range boundary and require a single-line resolved target. A foreign-source `cut` writes both files as one correlated transaction; `undo_last_edit` reverts both members. An edit never silently rewrites submitted text — a refused edit writes nothing.
131
-
132
- The result is the core's unified diff with fresh anchors for the changed lines, so the next edit can chain from them without a `read`. The host's built-in `write` is not replaced: the plugin guards it against served-row echoes and appends an anchored auto-read so following edits can use its output.
114
+ ```text
115
+ *** Begin Patch
116
+ *** Update File: src/config.ts
117
+ @@
118
+ - retries: 3,
119
+ + retries: 5,
120
+ *** Add File: src/new.ts
121
+ +export const x = 1;
122
+ *** End Patch
123
+ ```
133
124
 
125
+ Addressing works in two modes, and one patch may mix them:
126
+
127
+ - **Fragment hunks** use the official Codex syntax. `@@` starts the search window at the file start;
128
+ `@@ <one context line>` moves the window after that line's unique match. Body lines are
129
+ ` ` (context), `-` (remove) and `+` (add). Matching is byte-exact and must be unique: zero or
130
+ multiple matches reject the whole call and serve the candidate rows back.
131
+ - **Identity hunks** use the `@@<a>` / `@@<a>-<b>` extension: two inclusive 4-char anchors from rows
132
+ this session served. The body is `+` lines only; an empty body deletes the span. A stale, unknown
133
+ or foreign anchor is rejected with the file's current rows served.
134
+ - **Reference hunks** `*** Copy From: <path> <a>-<b>` / `*** Cut From: <path> <a>-<b>` take the
135
+ bytes of another served span; a cut also retires the source span in the same transaction.
136
+ - **Lifecycle** `*** Add File:`, `*** Delete File:` and `*** Move to:` join text edits in the same
137
+ transaction and the same undo record. `*** End of File` pins a hunk to the file tail.
138
+
139
+ `@@` followed by a space is Codex's context mode; `@@` immediately followed by four anchor
140
+ characters is identity mode. Any rejection leaves every file byte-identical and reports every
141
+ independent problem with its patch line number and `[E_*`] code. Success returns a file-level summary
142
+ (A/M/R/D) followed by fresh `HASH│content` anchors for every changed line, so the next call can chain
143
+ without a `read`. To replace a whole file, use one `*** Update File:` identity hunk spanning every served line.
134
144
  ### `undo_last_edit`
135
145
 
136
146
  ```ts
137
147
  undo_last_edit({ path: string })
138
148
  ```
139
149
 
140
- Restores the previous file content, BOM, line endings and original anchors from a snapshot-pinned undo record. The record survives agent restarts. A file changed externally after the edit is refused with `[E_UNDO_STALE]`. A correlated cut's undo restores every member of the transaction, not only the named path.
150
+ Restores the previous file content, BOM, line endings and original anchors from a snapshot-pinned undo record. The record survives agent restarts. A file changed externally after the patch is refused with `[E_UNDO_STALE]`. A correlated cut's undo restores every member of the transaction, not only the named path.
141
151
 
142
152
  ## Shell view
143
153
 
144
- The plugin installs a `tools/post-execute` listener for the host's `pwsh`, `bash` and built-in `grep` tools. When the command is a whitelisted read-only file view, the captured stdout is replaced with the aligned file's `HASH│content` rows, and those rows are served (leased) exactly like a read — a following `edit` can use them with no re-read. A built-in grep result whose matches are proven against disk is served the same way (see the table below).
154
+ The plugin installs a `tools/post-execute` listener for the host's `pwsh`, `bash` and built-in `grep` tools. When the command is a whitelisted read-only file view, the captured stdout is replaced with the aligned file's `HASH│content` rows, and those rows are served (leased) exactly like a read — a following `apply_patch` can use them with no re-read. A built-in grep result whose matches are proven against disk is served the same way (see the table below).
145
155
 
146
156
  | Command shape | Served as |
147
157
  | --- | --- |
@@ -164,10 +174,10 @@ The dsh host reads and writes UTF-8. Read admission is one rule with one exempti
164
174
 
165
175
  A file that is not valid UTF-8 is refused with `[E_UNSUPPORTED_FILE]` naming the file path and telling the model to ask the user how to handle it — for example a GBK-encoded source file or a BOM-less UTF-16LE file. The host does not guess a code page, offers no reopen-with-encoding parameter, keeps no encoding/BOM memo, and does not re-encode on save. UTF-16/UTF-32 BOMs are still recognized by kind detection: they are named in the refusal (e.g. `UTF-16LE encoded text`) instead of being decoded. Convert a non-UTF-8 file with an external tool before editing it here.
166
176
 
167
- The built-in `write` integration and `undo_last_edit` speak the same contract: they write UTF-8, and every file this plugin serves is UTF-8.
177
+ `apply_patch` and `undo_last_edit` speak the same contract: they write UTF-8, and every file this plugin serves is UTF-8. The built-in `write` integration keeps the same contract for a profile that deliberately keeps `write` visible.
168
178
  ## Sandbox behavior
169
179
 
170
- `edit` and `undo_last_edit` carry the host's optional escalation fields `sandbox_permissions` and `justification` only when a confining filesystem backend is mounted. `dsh/sandbox.ts` mirrors the built-in `@deepseek-ai/dsh-tool-fs` controller: it validates the field pairing, repeats the standing mode without approval, and resolves a strictly wider mode through the host's approval channel before touching the filesystem. A provider `FS_SANDBOX_DENIED` error is mapped to the standard model-facing denial marker plus escalation hint; other errors pass through unchanged.
180
+ `apply_patch` and `undo_last_edit` carry the host's optional escalation fields `sandbox_permissions` and `justification` only when a confining filesystem backend is mounted. `dsh/sandbox.ts` mirrors the built-in `@deepseek-ai/dsh-tool-fs` controller: it validates the field pairing, repeats the standing mode without approval, and resolves a strictly wider mode through the host's approval channel before touching the filesystem. A provider `FS_SANDBOX_DENIED` error is mapped to the standard model-facing denial marker plus escalation hint; other errors pass through unchanged.
171
181
 
172
182
  ### Host-side `0xC0000142` workspace-write finding
173
183
 
@@ -188,7 +198,7 @@ The M3/M4 smoke runs therefore used `DSH_PERMISSION_MODE=danger-full-access`; th
188
198
  | `pnpm run lint` | `oxlint .` + `knip --max-issues 0` |
189
199
  | `pnpm run verify` | lint + format + typecheck + Windows coverage |
190
200
 
191
- The build enforces its own packaging contract: host packages stay external, the pi utility package is aliased to `dsh/host-compat.ts` (the bundle must not reference pi), and no bare import outside the allowlist can slip in. See [`dsh/README.md`](dsh/README.md#build-and-packaging).
201
+ The build enforces its own packaging contract: host packages stay external, the bundle must not reference pi (there is no pi host integration), and no bare import outside the allowlist can slip in. See [`dsh/README.md`](dsh/README.md#build-and-packaging).
192
202
 
193
203
  ## Error and warning codes
194
204
 
@@ -199,7 +209,7 @@ Every refusal carries a bracketed code; `src/domain-errors.ts` is the source of
199
209
  | `[E_ACCESS]` | The filesystem denied access to the target path. |
200
210
  | `[E_BAD_PAYLOAD]` | Malformed or unknown tool payload; nothing is written. |
201
211
  | `[E_BATCH_ABORT]` | A batch item failed; the whole batch was not applied. |
202
- | `[E_EMPTY_RANGE]` | The edit would empty a non-empty file; use `write`. |
212
+ | `[E_EMPTY_RANGE]` | The patch would empty a non-empty file; use `write`. |
203
213
  | `[E_FOREIGN_ANCHOR]` | The anchor belongs to a different file. |
204
214
  | `[E_INTERNAL_INVARIANT]` | An internal invariant was violated. |
205
215
  | `[E_LARGE_FILE]` | The file exceeds the served line or byte budget. |
@@ -207,21 +217,23 @@ Every refusal carries a bracketed code; `src/domain-errors.ts` is the source of
207
217
  | `[E_MALFORMED_ANCHOR]` | The anchor token cannot be parsed. |
208
218
  | `[E_NOOP_LOOP]` | Repeated identical no-op edits; refused with the current range served. |
209
219
  | `[E_NOT_FOUND]` | The target file does not exist or is unreachable. |
220
+ | `[E_PATCH_SYNTAX]` | The apply_patch text is not valid syntax; nothing is written. |
210
221
  | `[E_STALE_ANCHOR]` | A boundary anchor no longer resolves against the file. |
211
- | `[E_STALE_RANGE]` | The served range changed between serve and edit. |
222
+ | `[E_STALE_RANGE]` | The served range changed between serve and patch. |
212
223
  | `[E_SUSPICIOUS_TEXT]` | The replacement reproduces served anchor text (a copied `HASH│` echo). |
213
224
  | `[E_TARGET_LOST]` | The target line identity can no longer be resolved. |
214
225
  | `[E_UNDO_REVERT_FAILED]` | A correlated revert failed mid-way; a repair run finishes it. |
215
226
  | `[E_UNDO_STALE]` | The undo record is stale relative to the file. |
216
227
  | `[E_UNDO_UNAVAILABLE]` | There is no undo history for the path. |
228
+ | `[E_UNIQUE_MATCH]` | A fragment hunk matched zero or multiple places; candidates are served back. |
217
229
  | `[E_UNKNOWN]` | An unclassified failure; the message carries the cause. |
218
230
  | `[E_UNKNOWN_ANCHOR]` | The anchor was never served in this session. |
219
231
  | `[E_UNSUPPORTED_FILE]` | Binary, image or directory targets are not editable text; a non-UTF-8 file is refused with its path and an instruction to ask the user how to handle it. |
220
232
  | `[E_UNVERIFIED_RANGE]` | The range interior cannot be verified (never served). |
221
- | `[W_LITERAL_BYPASS]` | Literal `HASH│` bytes were written through `edit` and accepted with a warning. |
222
- | `[W_NEVER_SERVED_SHAPE]` | A never-served hint was attached to an accepted edit. |
223
- | `[W_NOOP]` | The edit produced no content change and was accepted as a no-op. |
224
- | `[W_NOOP_INSERT]` | An insert-shaped edit produced no change. |
233
+ | `[W_LITERAL_BYPASS]` | Literal `HASH│` bytes were written through `apply_patch` and accepted with a warning. |
234
+ | `[W_NEVER_SERVED_SHAPE]` | A never-served hint was attached to an accepted patch. |
235
+ | `[W_NOOP]` | The patch produced no content change and was accepted as a no-op. |
236
+ | `[W_NOOP_INSERT]` | An insert-shaped hunk produced no change. |
225
237
  | `[W_REVERSED_ANCHORS]` | Boundaries were reversed and healed. |
226
238
  | `[W_SERVED_PREFIX_MISMATCH]` | A served prefix does not match; fresh rows were served. |
227
239
  | `[W_UNICODE_LITERAL]` | A Unicode literal in the payload looks like a copied anchor. |
@@ -230,7 +242,7 @@ Range-family rejections render their current-row evidence exactly once: the head
230
242
 
231
243
  ## Current limitations
232
244
 
233
- - **Mutation stays on the local `node:fs` seam.** The read path consumes a `CoreReadIo` port (local or mounted `ctx.fs`), but `writeAtomic`, the edit engine and undo remain local-only. `edit` and `undo_last_edit` fail closed with `E_NOT_FOUND` when the target is reachable only through `ctx.fs`. A full mutation bridge needs target-keyed store state: the host's `ctx.fs` already exposes versioned `writeText`/`editText` in `0.2.1-rc.2`, but the snapshot/lease store still keys on absolute paths and local stat identity (M6, see [`dsh/README.md`](dsh/README.md#ctxfs-deferral-m6)).
245
+ - **Mutation stays on the local `node:fs` seam.** The read path consumes a `CoreReadIo` port (local or mounted `ctx.fs`), but `writeAtomic`, the `apply_patch` engine and undo remain local-only. `apply_patch` and `undo_last_edit` fail closed with `E_NOT_FOUND` when the target is reachable only through `ctx.fs`. A full mutation bridge needs target-keyed store state: the host's `ctx.fs` already exposes versioned writes in `0.2.2-alpha.3`, but the snapshot/lease store still keys on absolute paths and local stat identity (M6, see [`dsh/README.md`](dsh/README.md#ctxfs-deferral-m6)).
234
246
  - **UTF-8 only.** Non-UTF-8 files are refused on read and must be converted with an external tool; the plugin does not decode legacy code pages, does not re-encode on save, and keeps no encoding state.
235
247
  - **Windows-only.** The branch targets Windows; POSIX-only suites are excluded in `vitest.config.windows.ts`, and that exclusion list must shrink, never grow (see [`dsh/UPSTREAM.md`](dsh/UPSTREAM.md)).
236
248
 
@@ -238,7 +250,7 @@ Range-family rejections render their current-row evidence exactly once: the head
238
250
 
239
251
  | Path | Role |
240
252
  | --- | --- |
241
- | `src/` | upstream pi-better-edit core (semantic source; upstream fixes are picked selectively, see [`dsh/UPSTREAM.md`](dsh/UPSTREAM.md)) |
253
+ | `src/` | hashline engine core derived from pi-better-edit (semantic source; upstream fixes are picked selectively, see [`dsh/UPSTREAM.md`](dsh/UPSTREAM.md)) |
242
254
  | `dsh/` | DeepSeek Harness host layer — the only code that differs from upstream; map in [`dsh/README.md`](dsh/README.md) |
243
255
  | `dsh/UPSTREAM.md` | upstream sync cursor, policy and Windows adaptation patches |
244
256
  | `dsh/baseline/` | milestone measurement baselines (M0–M5) |
package/cordis.patch.yml CHANGED
@@ -1,8 +1,14 @@
1
1
  # dsh-hashline-edit bundle patch: mount the host-plane plugin row.
2
2
  # Installed via `dsh plugin --profile <name> add dsh-hashline-edit` (or a
3
- # profile's bundle list). The plugin hooks agent/created and shadows the
4
- # built-in read/edit tools on each agent's own scope layer, so no preset
5
- # editing or user config is needed.
3
+ # profile's bundle list). The row carries three feature switches, edited on the
4
+ # Plugins page: `read` shadows the built-in read, `apply_patch` hides the native
5
+ # write/edit/apply_patch/str_replace_editor surface, `undo` exposes the
6
+ # undo_last_edit tool. Each toggles live: the host reloads this row and the plugin
7
+ # re-mounts every live agent. Defaults: read on, apply_patch on, undo off.
6
8
  - insert:
7
9
  - id: dsh-hashline-edit
8
10
  name: dsh-hashline-edit
11
+ config:
12
+ read: true
13
+ applyPatch: true
14
+ undo: false
package/lib/client.js ADDED
@@ -0,0 +1,231 @@
1
+ window.__ModuleLoader__.load({id:"dsh-hashline-edit",factory:(require)=>{const module={exports:{}};const exports=module.exports;(function(module,exports,require){
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name2 in all)
9
+ __defProp(target, name2, { get: all[name2], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // dsh/client/index.tsx
22
+ var index_exports = {};
23
+ __export(index_exports, {
24
+ apply: () => apply,
25
+ inject: () => inject,
26
+ name: () => name
27
+ });
28
+ module.exports = __toCommonJS(index_exports);
29
+ var import_react = require("react");
30
+ var import_jsx_runtime = require("react/jsx-runtime");
31
+ var SWITCHES = [
32
+ { key: "read", label: "swRead", hint: "swReadHint", fallback: true },
33
+ { key: "applyPatch", label: "swApplyPatch", hint: "swApplyPatchHint", fallback: true },
34
+ { key: "undo", label: "swUndo", hint: "swUndoHint", fallback: false }
35
+ ];
36
+ var en = {
37
+ swRead: "Read (shadow the host read)",
38
+ swReadHint: "Expose the hashline read tool with hash anchors and shadow the host's read.",
39
+ swApplyPatch: "Apply patch",
40
+ swApplyPatchHint: "Expose apply_patch and hide the native edit / write / apply_patch / str_replace_editor tools.",
41
+ swUndo: "Undo",
42
+ swUndoHint: "Expose undo_last_edit. Snapshots are always recorded; this only shows the tool to the model.",
43
+ groupTools: "Hashline tools",
44
+ groupToolsHint: "Toggle each tool live. Turning one off restores the host's own tool.",
45
+ save: "Save",
46
+ saving: "Saving\u2026",
47
+ reset: "Reset",
48
+ saved: "Saved. The host reloaded this row.",
49
+ rejected: "The host refused this save; reset and review your changes.",
50
+ loading: "Loading host settings\u2026",
51
+ unavailable: "Enable this row with the built-in switch to edit its settings.",
52
+ rowSummary: "Read, apply_patch and undo switches."
53
+ };
54
+ var zh = {
55
+ swRead: "\u8BFB\u53D6\uFF08\u906E\u853D\u5BBF\u4E3B read\uFF09",
56
+ swReadHint: "\u542F\u7528\u5E26\u54C8\u5E0C\u951A\u70B9\u7684 hashline read \u5DE5\u5177\uFF0C\u5E76\u906E\u853D\u5BBF\u4E3B\u7684 read\u3002",
57
+ swApplyPatch: "\u5E94\u7528\u8865\u4E01",
58
+ swApplyPatchHint: "\u542F\u7528 apply_patch\uFF0C\u5E76\u9690\u85CF\u539F\u751F edit / write / apply_patch / str_replace_editor\u3002",
59
+ swUndo: "\u64A4\u9500",
60
+ swUndoHint: "\u542F\u7528 undo_last_edit\u3002\u5FEB\u7167\u59CB\u7EC8\u8BB0\u5F55\uFF0C\u6B64\u9879\u4EC5\u5411\u6A21\u578B\u66B4\u9732\u5DE5\u5177\u3002",
61
+ groupTools: "Hashline \u5DE5\u5177",
62
+ groupToolsHint: "\u5B9E\u65F6\u5207\u6362\u6BCF\u4E2A\u5DE5\u5177\uFF1B\u5173\u95ED\u540E\u6062\u590D\u5BBF\u4E3B\u81EA\u5E26\u5DE5\u5177\u3002",
63
+ save: "\u4FDD\u5B58",
64
+ saving: "\u6B63\u5728\u4FDD\u5B58\u2026",
65
+ reset: "\u91CD\u7F6E",
66
+ saved: "\u5DF2\u4FDD\u5B58\uFF0C\u5BBF\u4E3B\u5DF2\u91CD\u65B0\u52A0\u8F7D\u6B64\u884C\u3002",
67
+ rejected: "\u5BBF\u4E3B\u62D2\u7EDD\u4E86\u6B64\u6B21\u4FDD\u5B58\uFF1B\u8BF7\u91CD\u7F6E\u540E\u91CD\u65B0\u68C0\u67E5\u66F4\u6539\u3002",
68
+ loading: "\u6B63\u5728\u52A0\u8F7D\u5BBF\u4E3B\u8BBE\u7F6E\u2026",
69
+ unavailable: "\u8BF7\u5148\u7528\u5185\u7F6E\u5F00\u5173\u542F\u7528\u6B64\u884C\u540E\u518D\u7F16\u8F91\u8BBE\u7F6E\u3002",
70
+ rowSummary: "\u8BFB\u53D6\u3001apply_patch \u4E0E\u64A4\u9500\u5F00\u5173\u3002"
71
+ };
72
+ var CSS = `
73
+ .hashline-settings { display:flex; flex-direction:column; gap:12px; font:inherit; color:var(--dsw-alias-label-primary,#222); }
74
+ .hashline-card { display:flex; flex-direction:column; gap:10px; padding:14px; border:.5px solid var(--dsw-alias-border-l2,#ddd); border-radius:8px; }
75
+ .hashline-title { margin:0; font-size:13px; font-weight:600; color:var(--dsw-alias-label-secondary,#555); }
76
+ .hashline-hint { margin:0; font-size:12px; line-height:1.5; color:var(--dsw-alias-label-tertiary,#888); }
77
+ .hashline-rows { display:flex; flex-direction:column; }
78
+ .hashline-row { display:flex; align-items:flex-start; gap:16px; padding:10px 0; border-top:.5px solid var(--dsw-alias-border-l2,#eee); }
79
+ .hashline-row:first-child { border-top:0; padding-top:0; }
80
+ .hashline-text { flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:4px; }
81
+ .hashline-label { font-size:13px; font-weight:500; }
82
+ .hashline-switch { flex:none; position:relative; width:36px; height:20px; border:0; padding:0; border-radius:999px; background:#c8c8c8; cursor:pointer; transition:background .12s ease; }
83
+ .hashline-switch.is-on { background:#2f6fed; }
84
+ .hashline-switch:disabled { opacity:.5; cursor:default; }
85
+ .hashline-knob { position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:#fff; transition:transform .12s ease; }
86
+ .hashline-switch.is-on .hashline-knob { transform:translateX(16px); }
87
+ .hashline-actions { display:flex; align-items:center; gap:8px; }
88
+ .hashline-primary { border:1px solid transparent; border-radius:6px; padding:5px 14px; font:inherit; font-size:13px; cursor:pointer; background:var(--dsw-alias-label-primary,#222); color:#fff; }
89
+ .hashline-primary:disabled { opacity:.4; cursor:default; }
90
+ .hashline-reset { border:0; background:none; font:inherit; font-size:12px; color:var(--dsw-alias-label-secondary,#555); cursor:pointer; }
91
+ .hashline-note { margin:0; font-size:12px; line-height:1.6; color:var(--dsw-alias-label-tertiary,#888); }
92
+ `;
93
+ function readValues(value) {
94
+ const out = {};
95
+ for (const spec of SWITCHES) {
96
+ const raw = value[spec.key];
97
+ out[spec.key] = typeof raw === "boolean" ? raw : spec.fallback;
98
+ }
99
+ return out;
100
+ }
101
+ function Switch(props) {
102
+ const { checked, label, disabled = false, onChange } = props;
103
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
104
+ "button",
105
+ {
106
+ type: "button",
107
+ role: "switch",
108
+ "aria-checked": checked,
109
+ "aria-label": label,
110
+ disabled,
111
+ className: `hashline-switch${checked ? " is-on" : ""}`,
112
+ onClick: () => onChange(!checked),
113
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "hashline-knob" })
114
+ }
115
+ );
116
+ }
117
+ function Summary(props) {
118
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: props.t("rowSummary") });
119
+ }
120
+ function RowForm(props) {
121
+ const { form, t } = props;
122
+ const ready = form?.state.status === "ready" && form.state.writable !== false;
123
+ const [draft, setDraft] = (0, import_react.useState)(
124
+ () => readValues(form?.state.value ?? {})
125
+ );
126
+ const [baseline, setBaseline] = (0, import_react.useState)(() => form?.state.value ?? {});
127
+ const [revision, setRevision] = (0, import_react.useState)(form?.state.revision);
128
+ const [dirty, setDirty] = (0, import_react.useState)(false);
129
+ const [busy, setBusy] = (0, import_react.useState)(false);
130
+ const [message, setMessage] = (0, import_react.useState)("");
131
+ (0, import_react.useEffect)(() => {
132
+ if (dirty) return;
133
+ setDraft(readValues(form?.state.value ?? {}));
134
+ setBaseline(form?.state.value ?? {});
135
+ setRevision(form?.state.revision);
136
+ }, [form?.state.revision, form?.state.status, dirty]);
137
+ if (!ready) {
138
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "hashline-note", role: "status", children: t(form?.state.status === "loading" ? "loading" : "unavailable") });
139
+ }
140
+ const toggle = (key, checked) => {
141
+ setDraft({ ...draft, [key]: checked });
142
+ setDirty(true);
143
+ setMessage("");
144
+ };
145
+ const reset = () => {
146
+ setDraft(readValues(baseline));
147
+ setDirty(false);
148
+ setMessage("");
149
+ };
150
+ const save = async () => {
151
+ if (form === void 0 || busy || revision === void 0) return;
152
+ const base = readValues(baseline);
153
+ const operations = [];
154
+ for (const spec of SWITCHES) {
155
+ if (draft[spec.key] !== base[spec.key]) {
156
+ operations.push({ op: "set", path: [spec.key], value: draft[spec.key] });
157
+ }
158
+ }
159
+ setBusy(true);
160
+ setMessage("");
161
+ try {
162
+ const accepted = await form.mutate(operations, revision);
163
+ setMessage(t(accepted ? "saved" : "rejected"));
164
+ if (accepted) setDirty(false);
165
+ } catch (error) {
166
+ setMessage(error instanceof Error ? error.message : String(error));
167
+ } finally {
168
+ setBusy(false);
169
+ }
170
+ };
171
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
172
+ "form",
173
+ {
174
+ className: "hashline-settings",
175
+ onSubmit: (event) => {
176
+ event.preventDefault();
177
+ void save();
178
+ },
179
+ children: [
180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "hashline-card", children: [
181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { className: "hashline-title", children: t("groupTools") }),
182
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "hashline-hint", children: t("groupToolsHint") }),
183
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "hashline-rows", children: SWITCHES.map((spec) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hashline-row", children: [
184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hashline-text", children: [
185
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "hashline-label", children: t(spec.label) }),
186
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "hashline-hint", children: t(spec.hint) })
187
+ ] }),
188
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
189
+ Switch,
190
+ {
191
+ checked: draft[spec.key] === true,
192
+ label: t(spec.label),
193
+ disabled: busy,
194
+ onChange: (checked) => toggle(spec.key, checked)
195
+ }
196
+ )
197
+ ] }, spec.key)) })
198
+ ] }),
199
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "hashline-actions", children: [
200
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "hashline-primary", disabled: busy || !dirty, children: t(busy ? "saving" : "save") }),
201
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "hashline-reset", onClick: reset, disabled: busy, children: t("reset") })
202
+ ] }),
203
+ message !== "" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "hashline-note", role: "status", children: message })
204
+ ]
205
+ }
206
+ );
207
+ }
208
+ var name = "dsh-hashline-edit-settings";
209
+ var inject = ["slots", "locale"];
210
+ function apply(ctx) {
211
+ ctx.effect(() => ctx.locale.register("hashlineEdit", { en, zh }));
212
+ ctx.effect(() => {
213
+ const style = document.createElement("style");
214
+ style.dataset.dshHashlineEdit = "true";
215
+ style.textContent = CSS;
216
+ document.head.append(style);
217
+ return () => style.remove();
218
+ });
219
+ ctx.slots.inject("plugins.row.config", function* () {
220
+ yield ctx.slots.register(
221
+ {
222
+ name: "plugins.row.config",
223
+ key: "dsh-hashline-edit#dsh-hashline-edit",
224
+ locale: "hashlineEdit"
225
+ },
226
+ (props) => props.view === "summary" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Summary, { ...props }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowForm, { ...props })
227
+ );
228
+ });
229
+ }
230
+
231
+ })(module,module.exports,require);return module.exports;}});