Files
oh-my-pi/docs/tools/edit.md
T
can1357 6fbb93b017 fix(coding-agent): allowed hashline parsing to accept flexible @@ section headers
- Updated hashline section parsing to accept headers with any leading `@` characters, normalizing them to the path before validation.
- Updated the hashline grammar and fallback errors to use canonical `@@ PATH` section headers.
- Added coverage for mixed `@@` and `@@@` headers across multiple file sections in hashline parsing tests.
2026-05-12 14:40:42 +02:00

14 KiB

edit

Applies source edits; default mode is the hashline patch language consumed from a single input string.

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 mode
    • packages/coding-agent/src/hashline/grammar.lark — custom-tool grammar for hashline mode
    • packages/coding-agent/src/hashline/input.ts — splits @@ PATH sections (legacy single-@ headers are still accepted)
    • packages/coding-agent/src/hashline/parser.ts — parses ops and payload lines
    • packages/coding-agent/src/hashline/apply.ts — validates anchors and applies edits
    • packages/coding-agent/src/hashline/anchors.ts — stale-anchor mismatch formatting
    • packages/coding-agent/src/hashline/recovery.ts — cache-based stale-anchor recovery
    • packages/coding-agent/src/hashline/hash.ts — computes LINEhh| anchors shared with read/search
    • packages/coding-agent/src/edit/file-read-cache.ts — per-session read snapshot cache
    • packages/coding-agent/src/tools/read.ts — emits anchored lines and records read snapshots
    • packages/coding-agent/src/tools/search.ts — records sparse snapshots from matches/context
    • packages/coding-agent/src/tools/fs-cache-invalidation.ts — invalidates FS scan caches after writes
    • packages/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 (legacy single-@ is still accepted) 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: ~TEXT by default; separator is HL_EDIT_SEP and can be overridden once at process start by PI_HL_SEP (packages/coding-agent/src/hashline/hash.ts)
  • Special anchors: BOF, EOF
  • Anchor token: <line><2-char-hash>, for example 41th

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 resolve preview/apply handshake.
  • content contains one text block per call. For a successful single-file edit it is either:
    • <path>: plus a compact diff preview from packages/coding-agent/src/hashline/diff-preview.ts, or
    • Updated <path> / Created <path> when no compact preview text is emitted.
  • Parse or recovery warnings are appended as:
Warnings:
...
  • details is EditToolDetails from packages/coding-agent/src/edit/renderer.ts:
    • diff: unified diff string
    • firstChangedLine: first changed post-edit line
    • diagnostics: LSP/format result if available
    • op: "create" or "update" for hashline mode
    • meta: output metadata
    • perFileResults: 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

  1. EditTool.execute() in packages/coding-agent/src/edit/index.ts resolves the active mode. Default is hashline; customFormat exposes packages/coding-agent/src/hashline/grammar.lark with $HFMT$ / $HSEP$ placeholders filled from packages/coding-agent/src/hashline/hash.ts.
  2. executeHashlineSingle() in packages/coding-agent/src/hashline/execute.ts splits the raw input into @PATH sections with splitHashlineInputs().
  3. If multiple sections target the same path, mergeSamePathSections() concatenates them before execution so every op still refers to the original file snapshot.
  4. 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.
  5. parseHashlineWithWarnings() in packages/coding-agent/src/hashline/parser.ts tokenizes the diff body:
    • ignores blank lines and optional *** Begin Patch
    • stops at *** End Patch
    • stops at *** Abort and emits ABORT_WARNING
    • turns + / < payload runs into one insert edit per payload line
    • turns - A..B into one delete edit per line in the range
    • turns = A..B into inserts before A, then deletes for A..B; no payload means replace with a single empty line
  6. applyHashlineEdits() in packages/coding-agent/src/hashline/apply.ts validates every referenced anchor before mutating anything. Each anchor hash is recomputed from current file content with computeLineHash().
  7. If any anchor hash differs, applyHashlineEdits() throws HashlineMismatchError. execute.ts catches only that class and calls tryRecoverHashlineWithCache().
  8. 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 using Diff.applyPatch(..., { fuzzFactor: 3 }) in packages/coding-agent/src/hashline/recovery.ts. On success the edit proceeds with a warning; on failure the original mismatch error is re-thrown.
  9. 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.hashlineAutoDropPureInsertDuplicates is enabled
    • all such fixes append warnings
  10. after_anchor inserts are normalized to before_anchor of the next line, or EOF if the anchor was the last line.
  11. Anchor-targeted edits are bucketed by target line and applied bottom-up so earlier splices do not invalidate later original line numbers. BOF and EOF inserts are applied after that.
  12. The edited text is restored to the original BOM and line ending style with helpers from packages/coding-agent/src/edit/normalize.ts and persisted via serializeEditFileText() in packages/coding-agent/src/edit/read-file.ts.
  13. 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 by EditTool.#injectLateDiagnostics() in packages/coding-agent/src/edit/index.ts.
  14. invalidateFsScanAfterWrite() calls invalidateFsScanCache(path) so filesystem-backed tools do not serve stale scan results.
  15. 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.
  16. 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 Patch envelope, 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.
  • Subprocesses / native bindings
    • createLspWritethrough() may trigger formatter / diagnostics work through the LSP subsystem.
    • invalidateFsScanAfterWrite() calls native invalidateFsScanCache() from @oh-my-pi/pi-natives.
  • Session state
    • Reads and updates the per-session FileReadCache used 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.
  • 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 = true and concurrency = "exclusive" in packages/coding-agent/src/edit/index.ts.

Limits & Caps

  • Default mode is hashline (DEFAULT_EDIT_MODE) in packages/coding-agent/src/utils/edit-mode.ts.
  • Anchor hashes are always 2 lowercase letters from a stable 647-entry bigram table (HL_BIGRAMS_COUNT) in packages/coding-agent/src/hashline/hash.ts.
  • The visible mismatch report shows 2 lines of context on each side (MISMATCH_CONTEXT) in packages/coding-agent/src/hashline/constants.ts.
  • Stale-anchor recovery uses fuzzFactor: 3 (HASHLINE_RECOVERY_FUZZ_FACTOR) in packages/coding-agent/src/hashline/recovery.ts.
  • The per-session read cache keeps at most 30 paths (MAX_PATHS_PER_SESSION) in packages/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 by packages/coding-agent/src/hashline/stream.ts).
  • HL_EDIT_SEP defaults to ~; HL_BODY_SEP is 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 as LINEhh|TEXT; mismatched lines are marked *. displayMessage renders 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

  • read and search are the authoritative source of anchors. The edit parser does not want the trailing |TEXT; copy only the LINEhh token.
  • 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..B is not a primitive replace in the parser. It expands to inserts before A plus deletes for A..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 @@ PATH headers back to a cwd-relative path when the file is inside the current working tree. Headers with any run of leading @ chars (e.g. @ foo.ts, @@ foo.ts, @@@foo.ts) are accepted to absorb unified-diff-style drift; the canonical form is @@ PATH.
  • Optional *** Begin Patch / *** End Patch markers are accepted in hashline mode, but the file sections are still @@ PATH-based, not Codex *** Update File: hunks.
  • *** Abort terminates parsing early and returns ABORT_WARNING; ops parsed before the marker still apply.
  • File-read cache invalidation is conflict-based, not write-through invalidation. If read later 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.