dsh-hashline-edit 0.2.3 → 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.
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, shadows the built-in
4
- # read tool, and hides the native write/edit/str_replace_editor surface on each
5
- # agent's own scope layer, so no preset 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;}});
package/lib/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // dsh/index.ts
2
+ import Schema from "@deepseek-ai/schemastery";
3
+
1
4
  // dsh/fs.ts
2
5
  import { constants } from "node:fs";
3
6
  import {
@@ -45,7 +48,7 @@ import {
45
48
  import { dirname as dirname2, join as join4, parse, resolve, sep } from "node:path";
46
49
 
47
50
  // src/hashline/alphabet.ts
48
- var HASH_LEN = true ? 3 : resolveAnchorWidth(process.env);
51
+ var HASH_LEN = true ? 4 : resolveAnchorWidth(process.env);
49
52
  var ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
50
53
  var ALPHA_SAFE = ALPHA.replace(/-/g, "\\-");
51
54
  var ALPHA_RE = new RegExp(`^[${ALPHA_SAFE}]+$`);
@@ -1672,7 +1675,7 @@ function toDshToolError(error, fallbackCode = "E_UNKNOWN") {
1672
1675
  // dsh/metrics/instrument.ts
1673
1676
  var WARNING_CODE = /\b(W_[A-Z0-9_]+)\b/u;
1674
1677
  var RESULT_CODE = /\b([EW]_[A-Z0-9_]+)\b/u;
1675
- var HASH_ROW = /^[A-Za-z0-9]{4}│/u;
1678
+ var HASH_ROW = new RegExp(`^${HASH_CLASS}\u2502`, "u");
1676
1679
  function jsonChars(value) {
1677
1680
  if (value === void 0) return 0;
1678
1681
  try {
@@ -2236,7 +2239,14 @@ function resolveSectionText(rootCtx, section, presetId, homeDir, orderFor, warne
2236
2239
  return section.renderDefault();
2237
2240
  }
2238
2241
  }
2239
- function registerGuidanceSections(rootCtx, agentCtx) {
2242
+ function sectionEnabled(name2, toggles) {
2243
+ if (toggles === void 0) return true;
2244
+ if (name2 === "tool:read") return toggles.read;
2245
+ if (name2 === "tool:apply_patch") return toggles.applyPatch;
2246
+ if (name2 === "tool:undo_last_edit") return toggles.undo;
2247
+ return true;
2248
+ }
2249
+ function registerGuidanceSections(rootCtx, agentCtx, enabled) {
2240
2250
  const registry = registryFor(agentCtx) ?? registryFor(rootCtx);
2241
2251
  if (registry === void 0) {
2242
2252
  warn2(rootCtx, "systemPrompt registry unavailable; hashline guidance not registered");
@@ -2267,6 +2277,7 @@ function registerGuidanceSections(rootCtx, agentCtx) {
2267
2277
  for (let index = 0; index < sections.length; index++) {
2268
2278
  const section = sections[index];
2269
2279
  const definition = GUIDANCE_SECTIONS2[index];
2280
+ if (!sectionEnabled(definition.name, enabled)) continue;
2270
2281
  if (section.malformed !== void 0) {
2271
2282
  warn2(
2272
2283
  rootCtx,
@@ -8957,7 +8968,7 @@ async function publishPluginWrites(deps, requestPaths, ctx, exec) {
8957
8968
 
8958
8969
  // dsh/tools/read.ts
8959
8970
  var READ_TOOL_NAME = "read";
8960
- var READ_DESCRIPTION = "Read a file as hash-anchored rows (`HASH\u2502content`). In the default `served` mode the 4-character anchor before each line is the line identity an `apply_patch` identity hunk must use, and every row shown is recorded as served for this session. `verbatim` mode returns the same page as plain text with no anchors, so nothing is served. Supports a single offset/limit page, several line windows (offset/limit or around_anchor \xB1 radius). UTF-8 only: a file whose bytes are not valid UTF-8 is refused instead of decoded. A small requested window may widen to its enclosing function, method or section; a `[widened \u2026]` note names the block that was served.";
8971
+ var READ_DESCRIPTION = 'Read a UTF-8 text file as hash-anchored `HASH\u2502content` rows; each anchor is the `apply_patch` identity for that line. Served mode is the default; `mode: "verbatim"` returns plain text with no anchors. Supports offset/limit pages and `windows` (offset/limit or around_anchor).';
8961
8972
  var RENDER_MODES = ["served", "verbatim"];
8962
8973
  function createDefaultReadSpanProvider(io) {
8963
8974
  const readTextFile = io === void 0 ? void 0 : async (path) => {
@@ -9415,7 +9426,7 @@ function buildReadTool(deps, options = {}) {
9415
9426
  },
9416
9427
  windows: {
9417
9428
  type: "array",
9418
- description: `Optional array of up to ${MAX_READ_WINDOWS} line windows to read in a single call. Each window is either an offset/limit range or an around_anchor radius around a 4-character anchor this session served; every served row is usable in one edit.`,
9429
+ description: `Optional array of up to ${MAX_READ_WINDOWS} windows, each an offset/limit range or an around_anchor + radius around a served anchor.`,
9419
9430
  items: {
9420
9431
  oneOf: [
9421
9432
  {
@@ -9425,7 +9436,7 @@ function buildReadTool(deps, options = {}) {
9425
9436
  around_anchor: {
9426
9437
  type: "string",
9427
9438
  required: true,
9428
- description: "A 4-character anchor this session served: the window centres on its line"
9439
+ description: "A served anchor: the window centres on its line"
9429
9440
  },
9430
9441
  radius: {
9431
9442
  type: "number",
@@ -9455,7 +9466,7 @@ function buildReadTool(deps, options = {}) {
9455
9466
  mode: {
9456
9467
  type: "string",
9457
9468
  enum: RENDER_MODES,
9458
- description: 'Render mode: "served" (default) returns each line as a 4-character anchor plus content and records every shown row as served; "verbatim" returns the same lines as plain text with no anchors and serves nothing, so an edit cannot target those lines.'
9469
+ description: "`served` (default) returns `HASH\u2502content` rows and records them as served; `verbatim` returns plain text with no anchors."
9459
9470
  }
9460
9471
  },
9461
9472
  output: {
@@ -13126,7 +13137,7 @@ var TOOL_RESULT_SCHEMA = {
13126
13137
  details: { type: "json", required: true }
13127
13138
  }
13128
13139
  };
13129
- var DIFF_ROW = /^([ +-]).{4}│(.*)$/u;
13140
+ var DIFF_ROW = new RegExp(`^([ +-]).{${HASH_LEN}}\u2502(.*)$`, "u");
13130
13141
  var DIFF_FILE_HEADER = /^--- (.+) ---$/u;
13131
13142
  var MAX_META_HUNKS = 24;
13132
13143
  var MAX_META_CHARS = 8192;
@@ -13240,9 +13251,14 @@ var APPLY_PATCH_PARAMETERS = {
13240
13251
  input: {
13241
13252
  type: "string",
13242
13253
  required: true,
13243
- description: "Complete patch text: *** Begin Patch ... *** End Patch"
13254
+ description: "Patch text: start with *** Begin Patch and end with *** End Patch. `*** Add File: path` with + lines; `*** Update File: path`, optionally `*** Move to: path`, with fragment hunks (`@@` or `@@ <context>`, body lines prefixed space/+/-) or identity hunks (`@@<a>` / `@@<a>-<b>`, + lines only; empty body deletes); `*** Copy From: path <a>-<b>` / `*** Cut From: path <a>-<b>` reuse a served span; `*** End of File` pins a hunk to the file end; `*** Delete File: path` deletes. Paths are workspace-relative or absolute."
13244
13255
  }
13245
13256
  };
13257
+ var REFERENCE_HEADER = new RegExp(
13258
+ `^\\*\\*\\* (?:Copy From|Cut From): (.+) (${HASH_CLASS}(?:-${HASH_CLASS})?)$`,
13259
+ "u"
13260
+ );
13261
+ var CUT_HEADER = new RegExp(`^\\*\\*\\* Cut From: (.+) (${HASH_CLASS}(?:-${HASH_CLASS})?)$`, "u");
13246
13262
  function previewPatch(input) {
13247
13263
  const diffs = [];
13248
13264
  const locations = [];
@@ -13271,7 +13287,7 @@ function previewPatch(input) {
13271
13287
  locations.push(move2[1].trim());
13272
13288
  continue;
13273
13289
  }
13274
- const reference = /^\*\*\* (?:Copy From|Cut From): (.+) ([A-Za-z0-9]{4}(?:-[A-Za-z0-9]{4})?)$/u.exec(line);
13290
+ const reference = REFERENCE_HEADER.exec(line);
13275
13291
  if (reference !== null) {
13276
13292
  locations.push(reference[1].trim());
13277
13293
  continue;
@@ -13302,7 +13318,7 @@ function writtenPatchPaths(input) {
13302
13318
  add(move2[1]);
13303
13319
  continue;
13304
13320
  }
13305
- const cut = /^\*\*\* Cut From: (.+) [A-Za-z0-9]{4}(?:-[A-Za-z0-9]{4})?$/u.exec(line);
13321
+ const cut = CUT_HEADER.exec(line);
13306
13322
  if (cut !== null) add(cut[1]);
13307
13323
  }
13308
13324
  return paths;
@@ -13779,8 +13795,34 @@ function registerWriteHook(rootCtx, agentCtx, io, observations) {
13779
13795
  // dsh/index.ts
13780
13796
  var name = "dsh-hashline-edit";
13781
13797
  var inject = ["tools", "systemPrompt"];
13798
+ var Config = Schema.object({
13799
+ read: Schema.boolean().default(true).volatile(),
13800
+ applyPatch: Schema.boolean().default(true).volatile(),
13801
+ undo: Schema.boolean().default(false).volatile()
13802
+ });
13803
+ function switchValue(value, fallback) {
13804
+ if (typeof value === "boolean") return value;
13805
+ if (value !== null && typeof value === "object" && typeof value.get === "function") {
13806
+ try {
13807
+ const inner = value.get();
13808
+ return typeof inner === "boolean" ? inner : fallback;
13809
+ } catch {
13810
+ return fallback;
13811
+ }
13812
+ }
13813
+ return fallback;
13814
+ }
13815
+ function resolveSwitches(config = {}) {
13816
+ const fields = config;
13817
+ return {
13818
+ read: switchValue(fields.read, true),
13819
+ applyPatch: switchValue(fields.applyPatch, true),
13820
+ undo: switchValue(fields.undo, false)
13821
+ };
13822
+ }
13782
13823
  function installAgentTools(rootCtx, agent, metrics, config = {}) {
13783
- agent.ctx.effect(() => {
13824
+ const switches = resolveSwitches(config);
13825
+ return agent.ctx.effect(() => {
13784
13826
  const io = createLocalFileIO();
13785
13827
  const sandbox = new FsSandboxController(rootCtx);
13786
13828
  const fsService = rootCtx.get("fs");
@@ -13795,27 +13837,31 @@ function installAgentTools(rootCtx, agent, metrics, config = {}) {
13795
13837
  };
13796
13838
  const disposers = [];
13797
13839
  try {
13798
- const spanProvider = createDefaultReadSpanProvider(coreIo);
13799
- const readDefinition = buildReadTool(deps, { spanProvider });
13800
- const undoDefinition = buildUndoTool(deps);
13801
- disposers.push(agent.ctx.tools.register(readDefinition));
13802
- const applyPatch = registerApplyPatchTool(rootCtx, agent, deps);
13803
- if (applyPatch !== null) {
13804
- disposers.push(applyPatch.dispose);
13805
- disposers.push(
13806
- registerNativeToolHiding(rootCtx, agent.ctx, {
13807
- toolName: applyPatch.name,
13808
- scope: agent,
13809
- hideNativeWriteTools: config.hideNativeWriteTools ?? true,
13810
- ...config.hiddenToolNames === void 0 ? {} : { hiddenToolNames: config.hiddenToolNames }
13811
- })
13812
- );
13840
+ if (switches.read) {
13841
+ const spanProvider = createDefaultReadSpanProvider(coreIo);
13842
+ disposers.push(agent.ctx.tools.register(buildReadTool(deps, { spanProvider })));
13843
+ }
13844
+ if (switches.applyPatch) {
13845
+ const applyPatch = registerApplyPatchTool(rootCtx, agent, deps);
13846
+ if (applyPatch !== null) {
13847
+ disposers.push(applyPatch.dispose);
13848
+ disposers.push(
13849
+ registerNativeToolHiding(rootCtx, agent.ctx, {
13850
+ toolName: applyPatch.name,
13851
+ scope: agent
13852
+ })
13853
+ );
13854
+ }
13855
+ }
13856
+ if (switches.undo) {
13857
+ disposers.push(agent.ctx.tools.register(buildUndoTool(deps)));
13858
+ }
13859
+ disposers.push(registerGuidanceSections(rootCtx, agent.ctx, switches));
13860
+ if (switches.read) {
13861
+ disposers.push(registerWriteHook(rootCtx, agent.ctx, io, bridge?.observations));
13862
+ disposers.push(registerShellHook(rootCtx, agent.ctx, deps));
13813
13863
  }
13814
- disposers.push(agent.ctx.tools.register(undoDefinition));
13815
- disposers.push(registerGuidanceSections(rootCtx, agent.ctx));
13816
- disposers.push(registerWriteHook(rootCtx, agent.ctx, io, bridge?.observations));
13817
13864
  disposers.push(registerToolObserver(agent.ctx, metrics));
13818
- disposers.push(registerShellHook(rootCtx, agent.ctx, deps));
13819
13865
  } catch (error) {
13820
13866
  disposeReverse(disposers);
13821
13867
  throw error;
@@ -13846,6 +13892,15 @@ function disposeReverse(disposers) {
13846
13892
  function warnVia3(rootCtx, message) {
13847
13893
  rootCtx.logger.warn(`dsh-hashline-edit: ${message}`);
13848
13894
  }
13895
+ var pluginStates = /* @__PURE__ */ new WeakMap();
13896
+ function liveAgentsFrom(rootCtx) {
13897
+ try {
13898
+ const service = rootCtx.get("agents");
13899
+ return typeof service?.list === "function" ? service.list() : [];
13900
+ } catch {
13901
+ return [];
13902
+ }
13903
+ }
13849
13904
  function apply(rootCtx, config = {}) {
13850
13905
  const metrics = createToolMetrics(
13851
13906
  metricSinkFromEnv(process.env, (error) => {
@@ -13854,22 +13909,76 @@ function apply(rootCtx, config = {}) {
13854
13909
  );
13855
13910
  })
13856
13911
  );
13857
- const installed = /* @__PURE__ */ new WeakSet();
13858
- rootCtx.on("agent/created", ({ agent }) => {
13859
- if (installed.has(agent)) return;
13860
- installed.add(agent);
13912
+ let state = pluginStates.get(rootCtx);
13913
+ if (state === void 0) {
13914
+ state = { agents: /* @__PURE__ */ new Set(), installs: /* @__PURE__ */ new Map() };
13915
+ pluginStates.set(rootCtx, state);
13916
+ }
13917
+ const current = state;
13918
+ const reconcile = (agent) => {
13919
+ const previous = current.installs.get(agent);
13920
+ if (previous !== void 0) {
13921
+ current.installs.delete(agent);
13922
+ try {
13923
+ previous();
13924
+ } catch {
13925
+ }
13926
+ }
13861
13927
  try {
13862
- installAgentTools(rootCtx, agent, metrics, config);
13928
+ current.installs.set(agent, installAgentTools(rootCtx, agent, metrics, config));
13863
13929
  } catch (error) {
13864
13930
  rootCtx.logger.warn(
13865
13931
  `dsh-hashline-edit: failed to install tools for agent ${agent.id}: ${error instanceof Error ? error.message : String(error)}`
13866
13932
  );
13867
13933
  }
13934
+ };
13935
+ const live = liveAgentsFrom(rootCtx);
13936
+ if (live.length > 0) {
13937
+ const alive = new Set(live);
13938
+ const gone = [];
13939
+ for (const known of current.agents) if (!alive.has(known)) gone.push(known);
13940
+ for (const agent of gone) current.agents.delete(agent);
13941
+ for (const agent of live) current.agents.add(agent);
13942
+ }
13943
+ for (const agent of current.agents) reconcile(agent);
13944
+ rootCtx.on("agent/created", ({ agent }) => {
13945
+ current.agents.add(agent);
13946
+ reconcile(agent);
13868
13947
  });
13948
+ rootCtx.on("agent/disposed", ({ agent }) => {
13949
+ current.agents.delete(agent);
13950
+ const dispose = current.installs.get(agent);
13951
+ if (dispose !== void 0) {
13952
+ current.installs.delete(agent);
13953
+ try {
13954
+ dispose();
13955
+ } catch {
13956
+ }
13957
+ }
13958
+ });
13959
+ const onEvent = rootCtx.on;
13960
+ if (typeof onEvent === "function") {
13961
+ onEvent.call(rootCtx, "loader/volatile-update", () => {
13962
+ for (const agent of current.agents) reconcile(agent);
13963
+ });
13964
+ }
13965
+ if (typeof rootCtx.effect === "function") {
13966
+ rootCtx.effect(() => () => {
13967
+ for (const dispose of current.installs.values()) {
13968
+ try {
13969
+ dispose();
13970
+ } catch {
13971
+ }
13972
+ }
13973
+ current.installs.clear();
13974
+ });
13975
+ }
13869
13976
  }
13870
13977
  export {
13978
+ Config,
13871
13979
  apply,
13872
13980
  inject,
13873
13981
  installAgentTools,
13874
- name
13982
+ name,
13983
+ resolveSwitches
13875
13984
  };
@@ -116,4 +116,10 @@ export declare function guidanceHomeDir(env?: Record<string, string | undefined>
116
116
  * come from the shared prompt sources, so the plugin package must ship
117
117
  * `prompts/` for them to load.
118
118
  */
119
- export declare function registerGuidanceSections(rootCtx: Context, agentCtx: Context): () => void;
119
+ /** Which feature tools are enabled, so a disabled tool's guidance section is not registered. */
120
+ export interface GuidanceToggles {
121
+ readonly read: boolean;
122
+ readonly applyPatch: boolean;
123
+ readonly undo: boolean;
124
+ }
125
+ export declare function registerGuidanceSections(rootCtx: Context, agentCtx: Context, enabled?: GuidanceToggles): () => void;
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import type { Context } from "@deepseek-ai/cordis";
19
19
  import type { Agent } from "@deepseek-ai/dsh-agent";
20
+ import Schema from "@deepseek-ai/schemastery";
20
21
  import { type ToolMetrics } from "./metrics/instrument.js";
21
22
  /** Cordis plugin name used by loader diagnostics. */
22
23
  export declare const name = "dsh-hashline-edit";
@@ -25,17 +26,47 @@ export declare const name = "dsh-hashline-edit";
25
26
  * services are resolved with `ctx.get` inside `dsh/sandbox.ts` instead.
26
27
  */
27
28
  export declare const inject: string[];
29
+ /**
30
+ * The row's editable config schema. Exporting `Config` is what makes the host
31
+ * expose `dsh-hashline-edit#dsh-hashline-edit` as a writable settings namespace,
32
+ * so the Plugins-page card can render its three switches and write them back.
33
+ */
34
+ export declare const Config: Schema<Schemastery.ObjectS<NoInfer<{
35
+ read: Schema<boolean, boolean, "volatile-defined">;
36
+ applyPatch: Schema<boolean, boolean, "volatile-defined">;
37
+ undo: Schema<boolean, boolean, "volatile-defined">;
38
+ }>>, Schemastery.ObjectT<NoInfer<{
39
+ read: Schema<boolean, boolean, "volatile-defined">;
40
+ applyPatch: Schema<boolean, boolean, "volatile-defined">;
41
+ undo: Schema<boolean, boolean, "volatile-defined">;
42
+ }>>, "plain">;
28
43
  /** The plugin configuration the loader passes as the second `apply` argument. */
29
44
  export interface PluginConfig {
30
45
  /**
31
- * Hide the native `write`/`edit`/`str_replace_editor` tools so every file
32
- * mutation goes through `apply_patch`. Defaults to true. A whole-file
33
- * replacement is one `*** Update File:` identity hunk over the served range.
46
+ * Expose the hashline `read` tool, shadowing the host's native `read`.
47
+ * Defaults to true; off leaves the native `read` in place.
48
+ */
49
+ read?: boolean;
50
+ /**
51
+ * Expose the hashline `apply_patch` tool and hide the native
52
+ * `edit`/`write`/`apply_patch`/`str_replace_editor` tools. Defaults to true;
53
+ * off keeps every native mutation tool.
34
54
  */
35
- hideNativeWriteTools?: boolean;
36
- /** Extra global tool names to hide alongside the native write set. */
37
- hiddenToolNames?: readonly string[];
55
+ applyPatch?: boolean;
56
+ /**
57
+ * Expose the `undo_last_edit` tool. Defaults to false. The undo snapshot row is
58
+ * still written on every commit; only the model-facing tool is opt-in.
59
+ */
60
+ undo?: boolean;
61
+ }
62
+ /** The three feature switches after the shipped defaults are applied. */
63
+ export interface ResolvedSwitches {
64
+ readonly read: boolean;
65
+ readonly applyPatch: boolean;
66
+ readonly undo: boolean;
38
67
  }
68
+ /** Resolve the plugin's three switches with the shipped defaults: read and apply_patch on, undo off. */
69
+ export declare function resolveSwitches(config?: PluginConfig): ResolvedSwitches;
39
70
  /**
40
71
  * Install the hashline toolset for one agent inside its own effect, so the
41
72
  * registrations are disposed in reverse order when the agent scope unwinds.
@@ -43,6 +74,6 @@ export interface PluginConfig {
43
74
  * Exported separately from {@link apply} so the host test can drive one fake
44
75
  * agent directly and integration probes can reuse the exact install path.
45
76
  */
46
- export declare function installAgentTools(rootCtx: Context, agent: Agent, metrics?: ToolMetrics, config?: PluginConfig): void;
47
- /** Mount the plugin: install the hashline toolset on every new agent. */
77
+ export declare function installAgentTools(rootCtx: Context, agent: Agent, metrics?: ToolMetrics, config?: PluginConfig): () => void;
78
+ /** Mount the plugin and reconcile live agents on every (re)apply. */
48
79
  export declare function apply(rootCtx: Context, config?: PluginConfig): void;
@@ -54,7 +54,7 @@ export declare function createDefaultReadSpanProvider(io?: CoreReadIo): SpanProv
54
54
  export interface ServedFileLine {
55
55
  /** 1-based line number the anchor addresses. */
56
56
  readonly line: number;
57
- /** The line's 4-character anchor, usable by `apply_patch`. */
57
+ /** The line's served anchor, usable by `apply_patch`. */
58
58
  readonly hash: string;
59
59
  /** The line's decoded text, without the anchor prefix. */
60
60
  readonly text: string;
@@ -20,7 +20,7 @@ export interface ParsePatchOptions {
20
20
  export declare function parseApplyPatch(input: string, options?: ParsePatchOptions): ParsePatchResult;
21
21
  /**
22
22
  * Parse an anchor token: `<a>` (single line) or `<a>-<b>` (closed span).
23
- * Returns `undefined` when the token is not a valid four-character anchor pair; the
23
+ * Returns `undefined` when the token is not a valid anchor pair; the
24
24
  * grammar layer turns that into an `E_PATCH_SYNTAX` problem at the patch line.
25
25
  *
26
26
  * @param token - the text after `@@` (no leading space).
@@ -8,7 +8,7 @@
8
8
  * (`PatchPlan`, `PlannedFile`, `PlannedMutation`),
9
9
  * - the **result / error** types the renderer and the host adapter consume.
10
10
  *
11
- * The engine underneath is the existing hashline kernel: content-addressed 4-char
11
+ * The engine underneath is the existing hashline kernel: content-addressed
12
12
  * anchors, served leases, MVCC snapshots, correlated multi-file transactions and
13
13
  * the undo store. Nothing here re-declares those primitives; the plan IR only
14
14
  * adds the addressing and lifecycle vocabulary apply_patch needs.
@@ -23,7 +23,7 @@ import type { LineEnding } from "../edit-diff.js";
23
23
  import type { HashStore } from "../hash-store.js";
24
24
  import type { TransactionRecoveryReport } from "../mutation-engine/types.js";
25
25
  /** Patch AST (grammar layer output). */
26
- /** A 4-character anchor: `[A-Za-z0-9]{4}` (the hashline alphabet). */
26
+ /** An anchor token: `HASH_LEN` characters of the hashline alphabet. */
27
27
  export type Anchor = string;
28
28
  /** A 1-based line number in the submitted patch text, used by every diagnostic. */
29
29
  export type PatchLineNumber = number;
@@ -164,7 +164,7 @@ export interface FileBaseline {
164
164
  readonly absolutePath: string;
165
165
  /** LF-normalized text; `""` for an Add File target. */
166
166
  readonly text: string;
167
- /** 4-char anchors parallel to the file's lines; empty for an Add File target. */
167
+ /** Anchors parallel to the file's lines; empty for an Add File target. */
168
168
  readonly hashes: readonly string[];
169
169
  /** Byte-order mark the file carried ("" when none). */
170
170
  readonly bom: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hashline-edit",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Hash-anchored read/apply_patch tools for DeepSeek Harness (dsh). Every line gets a unique 3- or 4-char hash (A-Za-z0-9, configurable) that stays stable across edits; apply_patch is the only file-mutation entry point and commits one patch batch per transaction. Undo persists across restarts.",
5
5
  "keywords": [
6
6
  "anchors",
@@ -42,6 +42,9 @@
42
42
  "types": "./lib/types/dsh/index.d.ts",
43
43
  "default": "./lib/index.js"
44
44
  },
45
+ "./client": {
46
+ "default": "./lib/client.js"
47
+ },
45
48
  "./package.json": "./package.json"
46
49
  },
47
50
  "scripts": {
@@ -49,6 +52,7 @@
49
52
  "test:watch": "vitest",
50
53
  "test:coverage": "vitest run --coverage --coverage.thresholds.lines=85 --coverage.thresholds.statements=85 --coverage.thresholds.functions=85 --coverage.thresholds.branches=80",
51
54
  "test:win": "vitest run --config vitest.config.windows.ts",
55
+ "test:win:3": "node scripts/test-width.mjs 3 dsh",
52
56
  "test:win:coverage": "vitest run --config vitest.config.windows.ts --coverage --coverage.thresholds.lines=85 --coverage.thresholds.statements=85 --coverage.thresholds.functions=85 --coverage.thresholds.branches=80",
53
57
  "lint": "oxlint . && knip --max-issues 0",
54
58
  "lint:knip": "knip --max-issues 0",
@@ -78,16 +82,19 @@
78
82
  "@deepseek-ai/dsh-home-paths": "0.2.2-rc.1",
79
83
  "@deepseek-ai/dsh-sandbox": "0.2.2-rc.1",
80
84
  "@deepseek-ai/dsh-tools": "0.2.2-rc.1",
85
+ "@deepseek-ai/schemastery": "3.19.0",
81
86
  "@semantic-release/git": "^11.0.1",
82
87
  "@semantic-release/github": "^12.0.9",
83
88
  "@semantic-release/npm": "^13.1.5",
84
89
  "@types/node": "^24.0.0",
90
+ "@types/react": "18.3.1",
85
91
  "@vitest/coverage-v8": "^4.1.10",
86
92
  "esbuild": "0.28.2",
87
93
  "husky": "^9.1.7",
88
94
  "knip": "6.40.0",
89
95
  "oxfmt": "^0.66.0",
90
96
  "oxlint": "^1.81.0",
97
+ "react": "18.3.1",
91
98
  "semantic-release": "^25.0.9",
92
99
  "typescript": "^7.0.0",
93
100
  "vite": "^8.3.5",
@@ -112,6 +119,18 @@
112
119
  "dsh": {
113
120
  "bundle": {
114
121
  "patch": "./cordis.patch.yml"
122
+ },
123
+ "client": {
124
+ "inject": [
125
+ "@deepseek-ai/dsh-api-gateway",
126
+ "@deepseek-ai/dsh-client-ui-renderer",
127
+ "@deepseek-ai/dsh-client-locale",
128
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
129
+ "@deepseek-ai/dsh-client-ui-slots",
130
+ "@deepseek-ai/dsh-client-ui-primitives"
131
+ ],
132
+ "platform": "web",
133
+ "immediately": true
115
134
  }
116
135
  }
117
136
  }
@@ -1,10 +1,4 @@
1
- - apply_patch: one call = one transaction. Collect every related file change (code, tests, docs, config) into one patch; split only when an earlier RESULT must decide a later edit.
2
- - apply_patch: two addressing modes. Fragment mode (`@@` or `@@ <context>`) locates by exact content and needs no served state. Identity mode (`@@<a>` / `@@<a>-<b>`) targets rows this session served and never writes the old text back.
3
- - apply_patch: the character after `@@` selects the mode. `@@` followed immediately by exactly {{HASH_LEN}} anchor characters is identity mode; `@@` followed by a space is Codex context mode (`@@ <line>`); a bare `@@` starts the search at the file start. Any other token is a syntax rejection.
4
- - apply_patch: fragment mode must match exactly once. Zero or several hits reject and serve the candidate rows; add surrounding context to disambiguate, never guess.
5
- - apply_patch: identity mode accepts only `+` lines; an empty body deletes the span. A `-` or context line in an identity hunk, or body lines in a Copy/Cut hunk, rejects at admission with its patch line number.
6
- - apply_patch: every hunk resolves against the file as it was before the call; later windows begin after the previous hunk's end. Overlapping target ranges in one file reject the whole batch.
7
- - apply_patch: lifecycle joins the same transaction and undo. `*** Add File:` content lines all start with `+`; `*** Delete File:` removes the file; `*** Move to:` must immediately follow `*** Update File:` and renames before its hunks apply; an existing move target rejects.
8
- - apply_patch: a rejection writes nothing. Read the `- patch line N ([E_*], path): ...` lines, then use the immediately following `HASH│content` block — those are the current rows and anchors; retry directly without issuing a separate `read`.
9
- - apply_patch: a success starts with `Updated N files:` and ends with the changed lines' fresh anchors per file; use those anchors directly in the next identity hunk. `[W_*]` lines are informational.
10
- - apply_patch: paths are workspace-relative or workspace-absolute; `..`, `.git`, and symlink targets reject. Whole-file replacement is one `*** Update File:` identity hunk spanning every served line with `+` lines for the new content.
1
+ - apply_patch: one call = one transaction; batch related changes and split only when an earlier RESULT decides the next.
2
+ - apply_patch: hunks are validated against the pre-call file in order (overlapping ranges reject the batch); a fragment hunk must match exactly once — add context to disambiguate, never guess — while identity hunks target served rows and a stale or unknown anchor is rejected; both serve the current rows back.
3
+ - apply_patch: a rejection writes nothing; retry from the served `HASH│content` rows without another `read`.
4
+ - apply_patch: success returns fresh anchors for changed lines; whole-file replacement is one identity hunk over every served line.
@@ -1 +1 @@
1
- Apply one Codex-format patch to one or more files in a single transaction: `{ input: string }`. `input` is the complete patch text: `*** Begin Patch` ... `*** End Patch`, with `*** Add File:`, `*** Delete File:`, `*** Update File:` (plus optional `*** Move to:`) and hunks addressed either by content (`@@` or `@@ <context>` with ` `/`-`/`+` lines) or by served anchors (`@@<a>` / `@@<a>-<b>` with `+` lines only). `*** Copy From:` / `*** Cut From:` copy or move a served span from another file. Every target validates before anything is written; a rejection changes nothing and returns the current rows with anchors. On success the result lists the changed files and the new anchors for the changed lines, so a follow-up call needs no `read`. To replace a whole file, use `*** Update File:` with one identity hunk spanning every served line and `+` lines for the new content.
1
+ Create, update, delete, move, copy, or cut UTF-8 text files with a single transactional patch; prefer it over multiple single-file edits when a change spans files or separated regions. Pass the complete patch as `input`.
@@ -1,6 +1,6 @@
1
- - read: call it only for information the tool never served you, such as content past the auto-read diff. A bash view or an auto-read already served the file, so apply_patch from those rows.
2
- - read: when you need several blocks or both boundaries of one replacement, put every range in ONE call. Use `{ file, windows: [{ offset, limit }] }`. Each window is served in that turn, so its anchors work in the same apply_patch call. A window can also be `{ around_anchor, radius }` to centre on a line this session already served. An anchor outside this session's served rows is omitted with an inline `[Window warning: ...]`.
3
- - read: a requested window of 61 lines or fewer can widen to its enclosing block; overlapping or adjacent windows fold before this test. Caps are 80 lines for code and Markdown, 40 for plain text. Widened rows begin with `[widened <requested> -> <served>, <label>]`; above three requested windows the note is `[widened N windows: <ranges>]`. An over-cap Markdown section clamps and names `(section <extent>)`; over-cap code or plain text refuses with no note. `mode: "verbatim"` widens too, with no anchors.
4
- - read: when you will not apply_patch the file, pass `mode: "verbatim"` for plain content with no anchors. Examples are a config value, docs, a README, or skill content (a SKILL.md or any file in its directory). Omit `mode` when you intend to use the anchors with apply_patch, so the rows carry anchors.
5
- - read: send one file per call as `{ file, windows?, mode? }`, the same way `apply_patch` does. The single top-level `mode` applies to the file. To read several files, make parallel `read` calls.
6
- - read: PNG/JPG images return as image attachments for multimodal models. Request one with `file` like any other file.
1
+ - read: call it for content the tool has never served you; a bash view or an auto-read already served the file, so `apply_patch` can use those rows.
2
+ - read: batch several disjoint ranges in ONE call with `windows` — an offset/limit range, or `around_anchor` plus `radius` around a line this session served.
3
+ - read: pass `mode: "verbatim"` when you will not `apply_patch` the file (config, docs, a README, skill files); otherwise omit it so the rows carry anchors.
4
+ - read: a window of 61 lines or fewer may widen to its enclosing block; widened rows carry a `[widened …]` note. Caps are 80 lines for code/Markdown and 40 for plain text.
5
+ - read: read several files with parallel `read` calls, one file per call.
6
+ - read: PNG/JPG/GIF/WebP files return as an image attachment for multimodal models; binary files and directories are refused.
package/prompts/read.md CHANGED
@@ -1 +1 @@
1
- Read one text file via `file`, with optional `windows` and top-level `mode`. To read several files, make parallel `read` calls. Served mode (default) returns each line as HASH│content with a {{HASH_LEN}}-char alphanumeric hash. That hash is the `apply_patch` identity anchor, so copy only the {{HASH_LEN}} chars before `│`. Pass `mode: "verbatim"` for plain text with no anchors when you will not apply_patch. `mode: "verbatim"` is not capped by line budget. Name ranges with `windows`: `[{ offset, limit }]` (offset is 1-indexed, limit is a line count, maximum 16 windows) or `[{ around_anchor: "<anchor>", radius: 10 }]` for a line this session served. When a page stops short, continue from the next unshown line with `windows: [{ offset, limit }]`. A requested window of 61 lines or fewer can widen to its enclosing block before serving; overlapping or adjacent windows fold before this test. Caps are 80 lines for code and Markdown, 40 for plain text. A widened page begins with `[widened <requested> -> <served>, <label>]`; more than three requested windows collapse to `[widened N windows: <ranges>]`. An over-cap Markdown section clamps to a cap-sized window and names `(section <extent>)`; an over-cap code or plain-text block refuses, serving the requested window. `mode: "verbatim"` widens the same way but adds no anchors. Inline `[...]` notices describe the served result, not file content. Empty files return `HASH│` (use an apply_patch identity hunk to insert). PNG/JPG/GIF/WebP return as image attachments for multimodal models. Binary and directory files are rejected. Hashes are file-scoped: an anchor is bound to the file it was served from.
1
+ Reads a UTF-8 file as `HASH│content` rows in served mode; each row's anchor is the `apply_patch` identity anchor for that line. Pass `mode: "verbatim"` for plain text with no anchors when you will not apply_patch; verbatim is not capped by line budget. Batch disjoint ranges in one call with `windows` (offset/limit or `around_anchor`).
@@ -0,0 +1,4 @@
1
+ - apply_patch:一次调用 = 一个事务;把相关改动成批处理,只有当先前的结果决定下一步时才拆分。
2
+ - apply_patch:各 hunk 按顺序针对调用前的文件校验(重叠范围会拒绝整批);片段 hunk 必须恰好匹配一次——补充上下文以消歧,绝不猜测——身份 hunk 定位已服务的行,陈旧或未知锚点会被拒绝;两者都会回送当前行。
3
+ - apply_patch:拒绝时不写入任何内容;从回送的 `HASH│content` 行重试,无需再次 `read`。
4
+ - apply_patch:成功时返回改动行的新锚点;整文件替换是用一个身份 hunk 覆盖每一行已服务的行。
@@ -0,0 +1 @@
1
+ 用单个事务补丁创建、更新、删除、移动、复制或剪切 UTF-8 文本文件;当改动跨多个文件或分散区域时,优先于多次单文件编辑。把完整补丁作为 `input` 传入。
@@ -0,0 +1,6 @@
1
+ - read:仅在工具尚未服务给你信息时调用,例如自动读取 diff 之外的内容。bash 视图或自动读取已经服务过该文件,因此直接用那些行 apply_patch。
2
+ - read:当一次替换需要多个块或两端边界时,把每个范围放进同一次调用。用 `{ file, windows: [{ offset, limit }] }`。每个窗口都在当轮被服务,因此其锚点可在同一次 apply_patch 调用中使用。窗口也可以是 `{ around_anchor, radius }`,以本会话已服务的某行为中心。本会话未服务过的锚点会被省略,并内联标注 `[Window warning: ...]`。
3
+ - read:61 行及以内的请求窗口可扩展到其所在块;重叠或相邻窗口在此判定前合并。上限为:代码与 Markdown 80 行,纯文本 40 行。扩展的行以 `[widened <requested> -> <served>, <label>]` 开头;超过三个请求窗口的提示为 `[widened N windows: <ranges>]`。超限的 Markdown 章节会裁剪并标注 `(section <extent>)`;超限的代码或纯文本则拒绝且无提示。`mode: "verbatim"` 也会扩展,但不加锚点。
4
+ - read:当你不打算对该文件 apply_patch 时,用 `mode: "verbatim"` 获取无锚点的纯内容。例如配置值、文档、README 或技能内容(SKILL.md 或其所处目录中的任意文件)。当你打算用锚点配合 apply_patch 时,就不要指定 `mode`,让这些行带上锚点。
5
+ - read:每次调用只发送一个文件,形式为 `{ file, windows?, mode? }`,与 `apply_patch` 相同。单个顶层 `mode` 作用于该文件。要读多个文件就并行发起多次 `read` 调用。
6
+ - read:PNG/JPG 图像以图像附件形式返回,供多模态模型使用。像其他文件一样用 `file` 请求。
@@ -0,0 +1 @@
1
+ 通过 `file`、`windows?`、`mode?` 读取文件;默认返回可直接编辑的 HASH│content 行,`mode: "verbatim"` 在你不编辑时返回纯文本;用 `windows` 在一次调用中批量读取多个范围
@@ -0,0 +1 @@
1
+ 通过 `file` 读取一个文本文件,可选 `windows` 与顶层 `mode`。要读多个文件就并行发起多次 `read` 调用。服务模式(默认)把每行返回为 HASH│content,带一个 {{HASH_LEN}} 字符的字母数字哈希。该哈希就是 `apply_patch` 的身份锚点,因此只复制 `│` 之前的 {{HASH_LEN}} 个字符。当你不打算 apply_patch 该文件时,用 `mode: "verbatim"` 获取无锚点的纯文本。`mode: "verbatim"` 不受行数上限限制。用 `windows` 指定范围:`[{ offset, limit }]`(offset 从 1 开始,limit 为行数,最多 16 个窗口)或 `[{ around_anchor: "<anchor>", radius: 10 }]`(针对本会话已服务的某行)。当页面未取全时,用 `windows: [{ offset, limit }]` 从下一个未显示行继续。61 行及以内的请求窗口在返回前可能扩展到其所在块;重叠或相邻的窗口在此判定前会合并。上限为:代码与 Markdown 80 行,纯文本 40 行。扩展页以 `[widened <requested> -> <served>, <label>]` 开头;超过三个请求窗口时折叠为 `[widened N windows: <ranges>]`。超限的 Markdown 章节会裁剪为上限大小的窗口并标注 `(section <extent>)`;超限的代码或纯文本块则拒绝,只返回所请求的窗口。`mode: "verbatim"` 同样会扩展,但不加锚点。行内的 `[...]` 提示描述的是返回结果,而非文件内容。空文件返回 `HASH│`(用 apply_patch 身份 hunk 插入)。PNG/JPG/GIF/WebP 以图像附件形式返回,供多模态模型使用。二进制与目录文件会被拒绝。哈希是文件作用域的:一个锚点绑定于它被服务时所在的文件。
@@ -0,0 +1,2 @@
1
+ - undo_last_edit:还原最近一次触及 `path` 处文件的 `apply_patch` 批次及其每个成员:文本 hunk 加文件生命周期操作(增/删/移动)。例外是外来 `cut`:此时撤销也会还原源文件。文件必须仍保有被编辑后的字节。带外写入会让调用以 `E_UNDO_STALE` 失败,且文件保持字节不变。一旦结果不对就立刻调用它。
2
+ - undo_last_edit:对该文件的 `write`,或之后的一次 `apply_patch` 批次,会替换撤销记录。一次读取、bash 视图或带外写入会保留该记录。当一次 apply_patch 的 diff 显示出你本想保留的行的 `-HASH│` 行时,该结果就是错的。例子包括一个收尾花括号、一句 import 或一个声明。
@@ -0,0 +1 @@
1
+ 撤销对 `path` 处文件最近一次的 `apply_patch` 批次
@@ -0,0 +1 @@
1
+ 撤销最近一次触及 `path` 处文件的 `apply_patch` 批次,把整批——每个文本 hunk 加上文件生命周期操作(增/删/移动)——还原到先前状态。当某次 `apply_patch` 产生了错误结果(例如内容错误、重复行、语法损坏)时使用。该文件自那批之后必须未被改动。
@@ -101,7 +101,7 @@ export function parseApplyPatch(input: string, options?: ParsePatchOptions): Par
101
101
 
102
102
  /**
103
103
  * Parse an anchor token: `<a>` (single line) or `<a>-<b>` (closed span).
104
- * Returns `undefined` when the token is not a valid four-character anchor pair; the
104
+ * Returns `undefined` when the token is not a valid anchor pair; the
105
105
  * grammar layer turns that into an `E_PATCH_SYNTAX` problem at the patch line.
106
106
  *
107
107
  * @param token - the text after `@@` (no leading space).
@@ -8,7 +8,7 @@
8
8
  * (`PatchPlan`, `PlannedFile`, `PlannedMutation`),
9
9
  * - the **result / error** types the renderer and the host adapter consume.
10
10
  *
11
- * The engine underneath is the existing hashline kernel: content-addressed 4-char
11
+ * The engine underneath is the existing hashline kernel: content-addressed
12
12
  * anchors, served leases, MVCC snapshots, correlated multi-file transactions and
13
13
  * the undo store. Nothing here re-declares those primitives; the plan IR only
14
14
  * adds the addressing and lifecycle vocabulary apply_patch needs.
@@ -25,7 +25,7 @@ import type { TransactionRecoveryReport } from "../mutation-engine/types.js";
25
25
 
26
26
  /** Patch AST (grammar layer output). */
27
27
 
28
- /** A 4-character anchor: `[A-Za-z0-9]{4}` (the hashline alphabet). */
28
+ /** An anchor token: `HASH_LEN` characters of the hashline alphabet. */
29
29
  export type Anchor = string;
30
30
 
31
31
  /** A 1-based line number in the submitted patch text, used by every diagnostic. */
@@ -175,7 +175,7 @@ export interface FileBaseline {
175
175
  readonly absolutePath: string;
176
176
  /** LF-normalized text; `""` for an Add File target. */
177
177
  readonly text: string;
178
- /** 4-char anchors parallel to the file's lines; empty for an Add File target. */
178
+ /** Anchors parallel to the file's lines; empty for an Add File target. */
179
179
  readonly hashes: readonly string[];
180
180
  /** Byte-order mark the file carried ("" when none). */
181
181
  readonly bom: string;
@@ -1 +0,0 @@
1
- Read a file via `file`, `windows?`, `mode?`; default returns HASH│content rows for editing, `mode: "verbatim"` returns plain text when you will not edit; batch several ranges in one call via `windows`