14 KiB
14 KiB
edit
Applies source edits; default mode is the hashline patch language consumed from a single
inputstring.
Source
- Entry:
packages/coding-agent/src/edit/index.ts - Model-facing prompt:
packages/coding-agent/src/prompts/tools/hashline.md - Key collaborators:
packages/coding-agent/src/utils/edit-mode.ts— selects active edit modepackages/coding-agent/src/hashline/grammar.lark— custom-tool grammar for hashline modepackages/coding-agent/src/hashline/input.ts— splits@PATHsectionspackages/coding-agent/src/hashline/parser.ts— parses ops and payload linespackages/coding-agent/src/hashline/apply.ts— validates anchors and applies editspackages/coding-agent/src/hashline/anchors.ts— stale-anchor mismatch formattingpackages/coding-agent/src/hashline/recovery.ts— cache-based stale-anchor recoverypackages/coding-agent/src/hashline/hash.ts— computesLINEhh|anchors shared withread/searchpackages/coding-agent/src/edit/file-read-cache.ts— per-session read snapshot cachepackages/coding-agent/src/tools/read.ts— emits anchored lines and records read snapshotspackages/coding-agent/src/tools/search.ts— records sparse snapshots from matches/contextpackages/coding-agent/src/tools/fs-cache-invalidation.ts— invalidates FS scan caches after writespackages/coding-agent/src/edit/streaming.ts— computes in-flight diff previews for the TUI
Inputs
Hashline mode (default)
| Field | Type | Required | Description |
|---|---|---|---|
input |
string |
Yes | One or more edit sections. First non-blank line must be @PATH unless the caller supplies the legacy fallback path outside the model schema and the body already looks like hashline ops (packages/coding-agent/src/hashline/input.ts). Optional *** Begin Patch / *** End Patch envelope is ignored if present. |
Patch language inside input:
- Section header:
@PATH - Insert after:
+ ANCHOR - Insert before:
< ANCHOR - Delete range:
- A..B - Replace range:
= A..B - Payload line:
~TEXTby default; separator isHL_EDIT_SEPand can be overridden once at process start byPI_HL_SEP(packages/coding-agent/src/hashline/hash.ts) - Special anchors:
BOF,EOF - Anchor token:
<line><2-char-hash>, for example41th
Anchors come from read/search output. read formats lines as LINEhh|TEXT via formatHashLine / formatHashLines in packages/coding-agent/src/hashline/hash.ts; copy only the token left of | into op lines.
Other edit modes exist (replace, patch, vim, apply_patch) and are selected outside the tool payload by resolveEditMode() in packages/coding-agent/src/utils/edit-mode.ts. Their schemas are different; this document covers the default hashline mode.
Outputs
- Single-shot tool result; hashline mode does not use a
resolvepreview/apply handshake. contentcontains one text block per call. For a successful single-file edit it is either:<path>:plus a compact diff preview frompackages/coding-agent/src/hashline/diff-preview.ts, orUpdated <path>/Created <path>when no compact preview text is emitted.
- Parse or recovery warnings are appended as:
Warnings:
...
detailsisEditToolDetailsfrompackages/coding-agent/src/edit/renderer.ts:diff: unified diff stringfirstChangedLine: first changed post-edit linediagnostics: LSP/format result if availableop:"create"or"update"for hashline modemeta: output metadataperFileResults: present for multi-section input
- Multi-section input returns one aggregated result with combined text and per-file details.
- While the model is still typing arguments, the TUI can compute a diff preview with
packages/coding-agent/src/edit/streaming.ts; that preview is not a deferred action and does not block execution.
Flow
EditTool.execute()inpackages/coding-agent/src/edit/index.tsresolves the active mode. Default ishashline;customFormatexposespackages/coding-agent/src/hashline/grammar.larkwith$HFMT$/$HSEP$placeholders filled frompackages/coding-agent/src/hashline/hash.ts.executeHashlineSingle()inpackages/coding-agent/src/hashline/execute.tssplits the rawinputinto@PATHsections withsplitHashlineInputs().- If multiple sections target the same path,
mergeSamePathSections()concatenates them before execution so every op still refers to the original file snapshot. - Multi-section calls run a preflight pass (
preflightHashlineSection()): parse ops, enforce plan-mode write rules, load the current file, reject anchor-scoped edits against missing files, reject auto-generated files, apply edits in memory, and fail if the result is a no-op. This prevents partial batches. parseHashlineWithWarnings()inpackages/coding-agent/src/hashline/parser.tstokenizes the diff body:- ignores blank lines and optional
*** Begin Patch - stops at
*** End Patch - stops at
*** Abortand emitsABORT_WARNING - turns
+/<payload runs into oneinsertedit per payload line - turns
- A..Binto onedeleteedit per line in the range - turns
= A..Binto inserts beforeA, then deletes forA..B; no payload means replace with a single empty line
- ignores blank lines and optional
applyHashlineEdits()inpackages/coding-agent/src/hashline/apply.tsvalidates every referenced anchor before mutating anything. Each anchor hash is recomputed from current file content withcomputeLineHash().- If any anchor hash differs,
applyHashlineEdits()throwsHashlineMismatchError.execute.tscatches only that class and callstryRecoverHashlineWithCache(). - Recovery replays the edits against the most recent cached read/search snapshot for that path (
packages/coding-agent/src/edit/file-read-cache.ts), then 3-way merges the result onto current disk content usingDiff.applyPatch(..., { fuzzFactor: 3 })inpackages/coding-agent/src/hashline/recovery.ts. On success the edit proceeds with a warning; on failure the original mismatch error is re-thrown. - Before splicing lines,
absorbReplacementBoundaryDuplicates()normalizes some malformed-but-recoverable ranges:- duplicate prefix/suffix lines adjacent to a replacement can be absorbed by widening the delete range
- pure inserts can auto-drop duplicated leading/trailing payload lines when
edit.hashlineAutoDropPureInsertDuplicatesis enabled - all such fixes append warnings
after_anchorinserts are normalized tobefore_anchorof the next line, orEOFif the anchor was the last line.- Anchor-targeted edits are bucketed by target line and applied bottom-up so earlier splices do not invalidate later original line numbers.
BOFandEOFinserts are applied after that. - The edited text is restored to the original BOM and line ending style with helpers from
packages/coding-agent/src/edit/normalize.tsand persisted viaserializeEditFileText()inpackages/coding-agent/src/edit/read-file.ts. - The writethrough callback from
createLspWritethrough()may format the file and fetch diagnostics. Late diagnostics are queued back into session state as a hidden deferred message byEditTool.#injectLateDiagnostics()inpackages/coding-agent/src/edit/index.ts. invalidateFsScanAfterWrite()callsinvalidateFsScanCache(path)so filesystem-backed tools do not serve stale scan results.- The session file-read cache is refreshed with the post-edit file text via
recordContiguous(), making the just-written content the new recovery base for subsequent stale-anchor merges. - The final response is built from a unified diff (
generateDiffString()), a compact preview, and any accumulated warnings.
Modes / Variants
hashline— default mode; line-anchored patch language described here (packages/coding-agent/src/utils/edit-mode.ts).replace— exact/fuzzy old/new text replacement (packages/coding-agent/src/edit/modes/replace.ts).patch— structured JSON diff-hunk mode (packages/coding-agent/src/edit/modes/patch.ts).apply_patch— freeform Codex-style*** Begin Patchenvelope, internally expanded into patch-mode entries (packages/coding-agent/src/edit/modes/apply-patch.ts).vim— persistent modal editing buffer (packages/coding-agent/src/tools/vim.ts).
Hashline op examples:
@src/a.ts
+ 4fb
~const added = true;
@src/a.ts
< 4fb
~const addedBefore = true;
@src/a.ts
- 4fb..6qx
@src/a.ts
= 4fb..5dm
~const clean = (name || DEF).trim();
~return clean.length === 0 ? DEF : clean.toUpperCase();
BOF/EOF examples:
@src/a.ts
+ BOF
~const HEADER = true;
@src/a.ts
+ EOF
~export const done = true;
Side Effects
- Filesystem
- Reads target files with
readEditFileText(). - Writes full updated file contents with
serializeEditFileText(). - Preserves BOM and original line-ending style.
- Reads target files with
- Subprocesses / native bindings
createLspWritethrough()may trigger formatter / diagnostics work through the LSP subsystem.invalidateFsScanAfterWrite()calls nativeinvalidateFsScanCache()from@oh-my-pi/pi-natives.
- Session state
- Reads and updates the per-session
FileReadCacheused for stale-anchor recovery. - Stores pending deferred-diagnostics abort controllers per path inside
EditTool. - Queues late diagnostics back into the session transcript as a hidden custom message.
- Reads and updates the per-session
- Background work / cancellation
- A new edit to the same path aborts the prior deferred diagnostics fetch for that path (
packages/coding-agent/src/edit/index.ts). - The tool itself is marked
nonAbortable = trueandconcurrency = "exclusive"inpackages/coding-agent/src/edit/index.ts.
- A new edit to the same path aborts the prior deferred diagnostics fetch for that path (
Limits & Caps
- Default mode is
hashline(DEFAULT_EDIT_MODE) inpackages/coding-agent/src/utils/edit-mode.ts. - Anchor hashes are always 2 lowercase letters from a stable 647-entry bigram table (
HL_BIGRAMS_COUNT) inpackages/coding-agent/src/hashline/hash.ts. - The visible mismatch report shows 2 lines of context on each side (
MISMATCH_CONTEXT) inpackages/coding-agent/src/hashline/constants.ts. - Stale-anchor recovery uses
fuzzFactor: 3(HASHLINE_RECOVERY_FUZZ_FACTOR) inpackages/coding-agent/src/hashline/recovery.ts. - The per-session read cache keeps at most 30 paths (
MAX_PATHS_PER_SESSION) inpackages/coding-agent/src/edit/file-read-cache.ts. - Hashline streaming chunk defaults are 200 lines or 64 KiB per chunk (
packages/coding-agent/src/hashline/types.ts, consumed bypackages/coding-agent/src/hashline/stream.ts). HL_EDIT_SEPdefaults to~;HL_BODY_SEPis always|(packages/coding-agent/src/hashline/hash.ts).
Errors
- Missing section header:
input must begin with "@PATH" on the first non-blank line; got: ... Example: "@src/foo.ts" then edit ops.
- Empty header:
Input header "@" is empty; provide a file path.
- Bad anchor token:
line N: expected a full anchor such as "119sr"; got "...".
- Bad range syntax:
line N: explicit ranges are required for delete/replace...line N: range must include exactly two full anchors separated by "..".line N: range A..B ends before it starts.line N: range A..B uses two different hashes for the same line.
- Missing payload for
+/<:line N: + and < operations require at least one ~TEXT payload line.
- Stray payload line:
line N: payload line has no preceding +, <, or = operation.
- Unknown op:
line N: unrecognized op. Use < ANCHOR..., + ANCHOR..., - A..B..., = A..B...
- Missing file for anchor-scoped edits:
File not found: <path>
- Out-of-range anchor:
Line N does not exist (file has M lines)
- Stale anchors throw
HashlineMismatchError. The error message contains re-read guidance and reprints nearby current file lines asLINEhh|TEXT; mismatched lines are marked*.displayMessagerenders the same information in a code-frame style. - No-op edit:
Edits to <path> resulted in no changes being made.
- Recovery failure is silent internally: if cache-based merge cannot prove a valid result, the original mismatch error is surfaced unchanged.
Notes
readandsearchare the authoritative source of anchors. The edit parser does not want the trailing|TEXT; copy only theLINEhhtoken.- Multi-op patches are parsed against the original file snapshot. Do not renumber later anchors after earlier ops;
applyHashlineEdits()buckets and applies them bottom-up. = A..Bis not a primitive replace in the parser. It expands to inserts beforeAplus deletes forA..B, which is why stale-anchor checking still happens on the original range lines.- Interior lines of a multi-line range use hash
**(RANGE_INTERIOR_HASH) and are not individually verified; only the first and last anchor hashes are checked. computeLineHash()trims trailing whitespace before hashing. Anchors survive line-ending changes and trailing-space-only changes, but not substantive line edits.- For punctuation-only lines, the hash mixes in the line number; identical
}lines on different lines intentionally get different anchors. splitHashlineInputs()normalizes absolute@PATHheaders back to a cwd-relative path when the file is inside the current working tree.- Optional
*** Begin Patch/*** End Patchmarkers are accepted in hashline mode, but the file sections are still@PATH-based, not Codex*** Update File:hunks. *** Abortterminates parsing early and returnsABORT_WARNING; ops parsed before the marker still apply.- File-read cache invalidation is conflict-based, not write-through invalidation. If
readlater records content for a line that disagrees with the cached snapshot, the entire snapshot for that path is replaced with the newly observed lines (packages/coding-agent/src/edit/file-read-cache.ts). - There is no resolve-style apply/discard phase for hashline edits. The only preview path is the transient TUI diff preview in
packages/coding-agent/src/edit/streaming.ts.