@oh-my-pi/pi-coding-agent 13.0.1 → 13.1.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 (52) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +33 -3
  4. package/src/commit/prompts/analysis-system.md +3 -3
  5. package/src/commit/prompts/changelog-system.md +3 -3
  6. package/src/commit/prompts/summary-system.md +5 -5
  7. package/src/extensibility/custom-tools/wrapper.ts +1 -0
  8. package/src/extensibility/extensions/wrapper.ts +2 -0
  9. package/src/extensibility/hooks/tool-wrapper.ts +1 -0
  10. package/src/lsp/index.ts +1 -0
  11. package/src/patch/diff.ts +2 -2
  12. package/src/patch/hashline.ts +88 -119
  13. package/src/patch/index.ts +88 -124
  14. package/src/patch/shared.ts +14 -21
  15. package/src/prompts/system/agent-creation-architect.md +2 -2
  16. package/src/prompts/system/system-prompt.md +9 -9
  17. package/src/prompts/tools/ask.md +3 -18
  18. package/src/prompts/tools/{poll-jobs.md → await.md} +1 -1
  19. package/src/prompts/tools/bash.md +4 -3
  20. package/src/prompts/tools/browser.md +11 -18
  21. package/src/prompts/tools/fetch.md +2 -5
  22. package/src/prompts/tools/find.md +10 -1
  23. package/src/prompts/tools/grep.md +1 -4
  24. package/src/prompts/tools/hashline.md +131 -155
  25. package/src/prompts/tools/lsp.md +6 -16
  26. package/src/prompts/tools/read.md +3 -6
  27. package/src/prompts/tools/task.md +93 -275
  28. package/src/prompts/tools/web-search.md +0 -7
  29. package/src/prompts/tools/write.md +0 -5
  30. package/src/sdk.ts +12 -2
  31. package/src/system-prompt.ts +5 -0
  32. package/src/task/index.ts +1 -0
  33. package/src/tools/ask.ts +1 -0
  34. package/src/tools/{poll-jobs.ts → await-tool.ts} +20 -19
  35. package/src/tools/bash.ts +1 -0
  36. package/src/tools/browser.ts +19 -4
  37. package/src/tools/calculator.ts +1 -0
  38. package/src/tools/cancel-job.ts +1 -0
  39. package/src/tools/exit-plan-mode.ts +1 -0
  40. package/src/tools/fetch.ts +1 -0
  41. package/src/tools/find.ts +1 -0
  42. package/src/tools/grep.ts +1 -0
  43. package/src/tools/index.ts +3 -3
  44. package/src/tools/notebook.ts +2 -1
  45. package/src/tools/plan-mode-guard.ts +2 -2
  46. package/src/tools/python.ts +1 -0
  47. package/src/tools/read.ts +1 -0
  48. package/src/tools/ssh.ts +1 -0
  49. package/src/tools/submit-result.ts +1 -0
  50. package/src/tools/todo-write.ts +1 -0
  51. package/src/tools/write.ts +1 -0
  52. package/src/web/search/index.ts +1 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [13.1.0] - 2026-02-23
6
+ ### Breaking Changes
7
+
8
+ - Renamed `file` parameter to `path` in replace, patch, and hashline edit operations
9
+
10
+ ### Added
11
+
12
+ - Added clarification in hashline edit documentation that the `end` tag must include closing braces/brackets when replacing blocks to prevent syntax errors
13
+
14
+ ### Changed
15
+
16
+ - Restructured task tool documentation for clarity, moving parameter definitions into a dedicated section and consolidating guidance on context, assignments, and parallelization
17
+ - Reformatted system prompt template to use markdown headings instead of XML tags for skills, preloaded skills, and rules sections
18
+ - Renamed `deviceScaleFactor` parameter to `device_scale_factor` in browser viewport configuration for consistency with snake_case naming convention
19
+ - Moved intent field documentation from per-tool JSON schema descriptions into a single system prompt block, reducing token overhead proportional to tool count
20
+
5
21
  ## [13.0.1] - 2026-02-22
6
22
  ### Changed
7
23
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "13.0.1",
4
+ "version": "13.1.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/can1357/oh-my-pi",
7
7
  "author": "Can Boluk",
@@ -41,12 +41,12 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@mozilla/readability": "^0.6",
44
- "@oh-my-pi/omp-stats": "13.0.1",
45
- "@oh-my-pi/pi-agent-core": "13.0.1",
46
- "@oh-my-pi/pi-ai": "13.0.1",
47
- "@oh-my-pi/pi-natives": "13.0.1",
48
- "@oh-my-pi/pi-tui": "13.0.1",
49
- "@oh-my-pi/pi-utils": "13.0.1",
44
+ "@oh-my-pi/omp-stats": "13.1.0",
45
+ "@oh-my-pi/pi-agent-core": "13.1.0",
46
+ "@oh-my-pi/pi-ai": "13.1.0",
47
+ "@oh-my-pi/pi-natives": "13.1.0",
48
+ "@oh-my-pi/pi-tui": "13.1.0",
49
+ "@oh-my-pi/pi-utils": "13.1.0",
50
50
  "@sinclair/typebox": "^0.34",
51
51
  "@xterm/headless": "^6.0",
52
52
  "ajv": "^8.18",
@@ -6,7 +6,7 @@
6
6
  * 1. No blank line before list items
7
7
  * 2. No blank line after opening XML tag or Handlebars block
8
8
  * 3. No blank line before closing XML tag or Handlebars block
9
- * 4. Strip leading whitespace from closing XML tags and Handlebars (lines starting with {{)
9
+ * 4. Strip leading whitespace from top-level closing XML tags (opened at col 0) and Handlebars (lines starting with {{)
10
10
  * 5. Compact markdown tables (remove padding)
11
11
  * 6. Collapse 2+ blank lines to single blank line
12
12
  * 7. Trim trailing whitespace (preserve indentation)
@@ -62,9 +62,17 @@ function compactTableSep(line: string): string {
62
62
  }
63
63
 
64
64
  function formatPrompt(content: string): string {
65
+ // Replace common ascii ellipsis and arrow patterns with their unicode equivalents
66
+ content = content
67
+ .replace(/\.{3}/g, "…")
68
+ .replace(/->/g, "→")
69
+ .replace(/<-/g, "←")
70
+ .replace(/<->/g, "↔");
65
71
  const lines = content.split("\n");
66
72
  const result: string[] = [];
67
73
  let inCodeBlock = false;
74
+ // Stack of tag names whose opening tag was at column 0 (top-level)
75
+ const topLevelTags: string[] = [];
68
76
 
69
77
  for (let i = 0; i < lines.length; i++) {
70
78
  let line = lines[i];
@@ -83,8 +91,30 @@ function formatPrompt(content: string): string {
83
91
  continue;
84
92
  }
85
93
 
86
- // Strip leading whitespace from closing XML tags and Handlebars
87
- if (CLOSING_XML.test(trimmed) || trimmed.startsWith("{{")) {
94
+ // Track top-level XML opening tags for depth-aware indent stripping
95
+ const isOpeningXml =
96
+ OPENING_XML.test(trimmed) && !trimmed.endsWith("/>");
97
+ if (isOpeningXml && line.length === trimmed.length) {
98
+ // Opening tag at column 0 — track as top-level
99
+ const match = OPENING_XML.exec(trimmed);
100
+ if (match) topLevelTags.push(match[1]);
101
+ }
102
+
103
+ // Strip leading whitespace from top-level closing XML tags and Handlebars
104
+ const closingMatch = CLOSING_XML.exec(trimmed);
105
+ if (closingMatch) {
106
+ const tagName = closingMatch[1];
107
+ if (
108
+ topLevelTags.length > 0 &&
109
+ topLevelTags[topLevelTags.length - 1] === tagName
110
+ ) {
111
+ // Closing tag matches a top-level opener — strip indent
112
+ line = trimmed;
113
+ topLevelTags.pop();
114
+ } else {
115
+ line = line.trimEnd();
116
+ }
117
+ } else if (trimmed.startsWith("{{")) {
88
118
  line = trimmed;
89
119
  } else if (TABLE_SEP.test(trimmed)) {
90
120
  // Compact table separator
@@ -7,8 +7,8 @@ Classify git diff into conventional commit format.
7
7
  ## 1. Determine Scope
8
8
 
9
9
  Apply scope when 60%+ line changes target single component:
10
- - 150 lines in src/api/, 30 in src/lib.rs -> "api"
11
- - 50 lines in src/api/, 50 in src/types/ -> null (50/50 split)
10
+ - 150 lines in src/api/, 30 in src/lib.rs "api"
11
+ - 50 lines in src/api/, 50 in src/types/ null (50/50 split)
12
12
 
13
13
  Use null for: cross-cutting changes, project-wide refactoring.
14
14
 
@@ -32,7 +32,7 @@ Group 3+ similar changes: "Updated 5 test files for new API." (not five bullets)
32
32
 
33
33
  Issue references inline: (#123), (#123, #456), (#123-#125).
34
34
 
35
- Priority: user-visible -> perf/security -> architecture -> internal.
35
+ Priority: user-visible perf/security architecture internal.
36
36
 
37
37
  Exclude: import changes, whitespace, formatting, trivial renames, debug prints, comment-only, file moves without modification.
38
38
 
@@ -30,9 +30,9 @@ Good:
30
30
  - Changed default timeout from 30s to 60s for slow connections
31
31
 
32
32
  Bad:
33
- - **cli**: Added dry-run flag -> redundant scope prefix
34
- - Added new feature. -> vague, trailing period
35
- - Refactored parser internals -> not user-visible
33
+ - **cli**: Added dry-run flag redundant scope prefix
34
+ - Added new feature. vague, trailing period
35
+ - Refactored parser internals not user-visible
36
36
 
37
37
  Breaking Changes:
38
38
  - Removed legacy auth flow; users must re-authenticate with OAuth tokens
@@ -23,15 +23,15 @@ Output: ONLY description after "{{ commit_type }}{{ scope_prefix }}:"; max {{ ch
23
23
  </verb-reference>
24
24
  <examples>
25
25
  feat | TLS encryption added to HTTP client for MITM prevention
26
- -> added TLS support to prevent man-in-the-middle attacks
26
+ added TLS support to prevent man-in-the-middle attacks
27
27
  refactor | Consolidated HTTP transport into unified builder pattern
28
- -> migrated HTTP transport to unified builder API
28
+ migrated HTTP transport to unified builder API
29
29
  fix | Race condition in connection pool causing exhaustion under load
30
- -> corrected race condition causing connection pool exhaustion
30
+ corrected race condition causing connection pool exhaustion
31
31
  perf | Batch processing optimized to reduce memory allocations
32
- -> eliminated allocation overhead in batch processing
32
+ eliminated allocation overhead in batch processing
33
33
  build | Updated serde to fix CVE-2024-1234
34
- -> upgraded serde to 1.0.200 for CVE-2024-1234
34
+ upgraded serde to 1.0.200 for CVE-2024-1234
35
35
  </examples>
36
36
  <banned-words>
37
37
  comprehensive, various, several, improved, enhanced, quickly, simply, basically, this change, this commit, now
@@ -14,6 +14,7 @@ export class CustomToolAdapter<TParams extends TSchema = TSchema, TDetails = any
14
14
  declare label: string;
15
15
  declare description: string;
16
16
  declare parameters: TParams;
17
+ readonly strict = true;
17
18
 
18
19
  constructor(
19
20
  private tool: CustomTool<TParams, TDetails>,
@@ -17,6 +17,7 @@ export class RegisteredToolAdapter implements AgentTool<any, any, any> {
17
17
  declare description: string;
18
18
  declare parameters: any;
19
19
  declare label: string;
20
+ declare strict: boolean;
20
21
 
21
22
  renderCall?: (args: any, options: any, theme: any) => any;
22
23
  renderResult?: (result: any, options: any, theme: any, args?: any) => any;
@@ -83,6 +84,7 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
83
84
  declare description: string;
84
85
  declare parameters: TParameters;
85
86
  declare label: string;
87
+ declare strict: boolean;
86
88
 
87
89
  constructor(
88
90
  private tool: AgentTool<TParameters, TDetails>,
@@ -22,6 +22,7 @@ export class HookToolWrapper<TParameters extends TSchema = TSchema, TDetails = u
22
22
  declare description: string;
23
23
  declare parameters: TParameters;
24
24
  declare label: string;
25
+ declare strict: boolean;
25
26
 
26
27
  constructor(
27
28
  private tool: AgentTool<TParameters, TDetails>,
package/src/lsp/index.ts CHANGED
@@ -866,6 +866,7 @@ export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Them
866
866
  readonly label = "LSP";
867
867
  readonly description: string;
868
868
  readonly parameters = lspSchema;
869
+ readonly strict = true;
869
870
  readonly renderCall = renderCall;
870
871
  readonly renderResult = renderResult;
871
872
  readonly mergeCallAndResult = true;
package/src/patch/diff.ts CHANGED
@@ -414,11 +414,11 @@ export async function computeHashlineDiff(
414
414
  const normalizedContent = normalizeToLF(content);
415
415
 
416
416
  const result = applyHashlineEdits(normalizedContent, edits);
417
- if (normalizedContent === result.content) {
417
+ if (normalizedContent === result.lines) {
418
418
  return { error: `No changes would be made to ${path}. The edits produce identical content.` };
419
419
  }
420
420
 
421
- return generateDiffString(normalizedContent, result.content);
421
+ return generateDiffString(normalizedContent, result.lines);
422
422
  } catch (err) {
423
423
  return { error: err instanceof Error ? err.message : String(err) };
424
424
  }
@@ -1,26 +1,24 @@
1
1
  /**
2
- * Hashline edit mode — a line-addressable edit format using content hashes.
2
+ * Hashline edit mode — a line-addressable edit format using text hashes.
3
3
  *
4
4
  * Each line in a file is identified by its 1-indexed line number and a short
5
- * hexadecimal hash derived from the normalized line content (xxHash32, truncated to 2
5
+ * hexadecimal hash derived from the normalized line text (xxHash32, truncated to 2
6
6
  * hex chars).
7
7
  * The combined `LINE#ID` reference acts as both an address and a staleness check:
8
8
  * if the file has changed since the caller last read it, hash mismatches are caught
9
9
  * before any mutation occurs.
10
10
  *
11
- * Displayed format: `LINENUM#HASH:CONTENT`
11
+ * Displayed format: `LINENUM#HASH:TEXT`
12
12
  * Reference format: `"LINENUM#HASH"` (e.g. `"5#aa"`)
13
13
  */
14
14
 
15
15
  import type { HashMismatch } from "./types";
16
16
 
17
- export type LineTag = { line: number; hash: string };
17
+ export type Anchor = { line: number; hash: string };
18
18
  export type HashlineEdit =
19
- | { op: "replace"; tag: LineTag; content: string[] }
20
- | { op: "replace"; first: LineTag; last: LineTag; content: string[] }
21
- | { op: "append"; after?: LineTag; content: string[] }
22
- | { op: "prepend"; before?: LineTag; content: string[] }
23
- | { op: "insert"; after: LineTag; before: LineTag; content: string[] };
19
+ | { op: "replace"; pos: Anchor; end?: Anchor; lines: string[] }
20
+ | { op: "append"; pos?: Anchor; lines: string[] }
21
+ | { op: "prepend"; pos?: Anchor; lines: string[] };
24
22
 
25
23
  const NIBBLE_STR = "ZPMQVRWSNKTXJBYH";
26
24
 
@@ -30,12 +28,14 @@ const DICT = Array.from({ length: 256 }, (_, i) => {
30
28
  return `${NIBBLE_STR[h]}${NIBBLE_STR[l]}`;
31
29
  });
32
30
 
31
+ const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
32
+
33
33
  /**
34
34
  * Compute a short hexadecimal hash of a single line.
35
35
  *
36
- * Uses xxHash32 on a whitespace-normalized line, truncated to {@link HASH_LEN}
37
- * hex characters. The `idx` parameter is accepted for compatibility with older
38
- * call sites, but is not currently mixed into the hash.
36
+ * Uses xxHash32 on a whitespace-normalized line, truncated to 2 chars from
37
+ * {@link NIBBLE_STR}. For lines containing no alphanumeric characters (only
38
+ * punctuation/symbols/whitespace), the line number is mixed in to reduce hash collisions.
39
39
  * The line input should not include a trailing newline.
40
40
  */
41
41
  export function computeLineHash(idx: number, line: string): string {
@@ -43,23 +43,27 @@ export function computeLineHash(idx: number, line: string): string {
43
43
  line = line.slice(0, -1);
44
44
  }
45
45
  line = line.replace(/\s+/g, "");
46
- void idx; // Might use line, but for now, let's not.
47
- return DICT[Bun.hash.xxHash32(line) & 0xff];
46
+
47
+ let seed = 0;
48
+ if (!RE_SIGNIFICANT.test(line)) {
49
+ seed = idx;
50
+ }
51
+ return DICT[Bun.hash.xxHash32(line, seed) & 0xff];
48
52
  }
49
53
 
50
54
  /**
51
- * Formats a tag given the line number and content.
55
+ * Formats a tag given the line number and text.
52
56
  */
53
- export function formatLineTag(line: number, content: string): string {
54
- return `${line}#${computeLineHash(line, content)}`;
57
+ export function formatLineTag(line: number, lines: string): string {
58
+ return `${line}#${computeLineHash(line, lines)}`;
55
59
  }
56
60
 
57
61
  /**
58
- * Format file content with hashline prefixes for display.
62
+ * Format file text with hashline prefixes for display.
59
63
  *
60
- * Each line becomes `LINENUM#HASH:CONTENT` where LINENUM is 1-indexed.
64
+ * Each line becomes `LINENUM#HASH:TEXT` where LINENUM is 1-indexed.
61
65
  *
62
- * @param content - Raw file content string
66
+ * @param text - Raw file text string
63
67
  * @param startLine - First line number (1-indexed, defaults to 1)
64
68
  * @returns Formatted string with one hashline-prefixed line per input line
65
69
  *
@@ -69,8 +73,8 @@ export function formatLineTag(line: number, content: string): string {
69
73
  * // "1#HH:function hi() {\n2#HH: return;\n3#HH:}"
70
74
  * ```
71
75
  */
72
- export function formatHashLines(content: string, startLine = 1): string {
73
- const lines = content.split("\n");
76
+ export function formatHashLines(text: string, startLine = 1): string {
77
+ const lines = text.split("\n");
74
78
  return lines
75
79
  .map((line, i) => {
76
80
  const num = startLine + i;
@@ -375,14 +379,14 @@ export class HashlineMismatchError extends Error {
375
379
  }
376
380
  prevLine = lineNum;
377
381
 
378
- const content = fileLines[lineNum - 1];
379
- const hash = computeLineHash(lineNum, content);
382
+ const text = fileLines[lineNum - 1];
383
+ const hash = computeLineHash(lineNum, text);
380
384
  const prefix = `${lineNum}#${hash}`;
381
385
 
382
386
  if (mismatchSet.has(lineNum)) {
383
- lines.push(`>>> ${prefix}:${content}`);
387
+ lines.push(`>>> ${prefix}:${text}`);
384
388
  } else {
385
- lines.push(` ${prefix}:${content}`);
389
+ lines.push(` ${prefix}:${text}`);
386
390
  }
387
391
  }
388
392
  return lines.join("\n");
@@ -415,7 +419,7 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
415
419
  * Apply an array of hashline edits to file content.
416
420
  *
417
421
  * Each edit operation identifies target lines directly (`replace`,
418
- * `insert`). Line references are resolved via {@link parseTag}
422
+ * `append`, `prepend`). Line references are resolved via {@link parseTag}
419
423
  * and hashes validated before any mutation.
420
424
  *
421
425
  * Edits are sorted bottom-up (highest effective line first) so earlier
@@ -424,22 +428,22 @@ export function validateLineRef(ref: { line: number; hash: string }, fileLines:
424
428
  * @returns The modified content and the 1-indexed first changed line number
425
429
  */
426
430
  export function applyHashlineEdits(
427
- content: string,
431
+ text: string,
428
432
  edits: HashlineEdit[],
429
433
  ): {
430
- content: string;
434
+ lines: string;
431
435
  firstChangedLine: number | undefined;
432
436
  warnings?: string[];
433
- noopEdits?: Array<{ editIndex: number; loc: string; currentContent: string }>;
437
+ noopEdits?: Array<{ editIndex: number; loc: string; current: string }>;
434
438
  } {
435
439
  if (edits.length === 0) {
436
- return { content, firstChangedLine: undefined };
440
+ return { lines: text, firstChangedLine: undefined };
437
441
  }
438
442
 
439
- const fileLines = content.split("\n");
443
+ const fileLines = text.split("\n");
440
444
  const originalFileLines = [...fileLines];
441
445
  let firstChangedLine: number | undefined;
442
- const noopEdits: Array<{ editIndex: number; loc: string; currentContent: string }> = [];
446
+ const noopEdits: Array<{ editIndex: number; loc: string; current: string }> = [];
443
447
 
444
448
  // Pre-validate: collect all hash mismatches before mutating
445
449
  const mismatches: HashMismatch[] = [];
@@ -457,42 +461,30 @@ export function applyHashlineEdits(
457
461
  for (const edit of edits) {
458
462
  switch (edit.op) {
459
463
  case "replace": {
460
- if ("tag" in edit) {
461
- if (!validateRef(edit.tag)) continue;
462
- } else {
463
- if (edit.first.line > edit.last.line) {
464
- throw new Error(`Range start line ${edit.first.line} must be <= end line ${edit.last.line}`);
465
- }
466
- const startValid = validateRef(edit.first);
467
- const endValid = validateRef(edit.last);
464
+ if (edit.end) {
465
+ const startValid = validateRef(edit.pos);
466
+ const endValid = validateRef(edit.end);
468
467
  if (!startValid || !endValid) continue;
468
+ if (edit.pos.line > edit.end.line) {
469
+ throw new Error(`Range start line ${edit.pos.line} must be <= end line ${edit.end.line}`);
470
+ }
471
+ } else {
472
+ if (!validateRef(edit.pos)) continue;
469
473
  }
470
474
  break;
471
475
  }
472
476
  case "append": {
473
- if (edit.content.length === 0) {
474
- throw new Error('Insert-after edit (src "N#HH..") requires non-empty dst');
477
+ if (edit.pos && !validateRef(edit.pos)) continue;
478
+ if (edit.lines.length === 0) {
479
+ edit.lines = [""]; // insert an empty line
475
480
  }
476
- if (edit.after && !validateRef(edit.after)) continue;
477
481
  break;
478
482
  }
479
483
  case "prepend": {
480
- if (edit.content.length === 0) {
481
- throw new Error('Insert-before edit (src "N#HH..") requires non-empty dst');
482
- }
483
- if (edit.before && !validateRef(edit.before)) continue;
484
- break;
485
- }
486
- case "insert": {
487
- if (edit.content.length === 0) {
488
- throw new Error('Insert-between edit (src "A#HH.. B#HH..") requires non-empty dst');
489
- }
490
- if (edit.before.line <= edit.after.line) {
491
- throw new Error(`insert requires after (${edit.after.line}) < before (${edit.before.line})`);
484
+ if (edit.pos && !validateRef(edit.pos)) continue;
485
+ if (edit.lines.length === 0) {
486
+ edit.lines = [""]; // insert an empty line
492
487
  }
493
- const afterValid = validateRef(edit.after);
494
- const beforeValid = validateRef(edit.before);
495
- if (!afterValid || !beforeValid) continue;
496
488
  break;
497
489
  }
498
490
  }
@@ -508,31 +500,28 @@ export function applyHashlineEdits(
508
500
  let lineKey: string;
509
501
  switch (edit.op) {
510
502
  case "replace":
511
- if ("tag" in edit) {
512
- lineKey = `s:${edit.tag.line}`;
503
+ if (!edit.end) {
504
+ lineKey = `s:${edit.pos.line}`;
513
505
  } else {
514
- lineKey = `r:${edit.first.line}:${edit.last.line}`;
506
+ lineKey = `r:${edit.pos.line}:${edit.end.line}`;
515
507
  }
516
508
  break;
517
509
  case "append":
518
- if (edit.after) {
519
- lineKey = `i:${edit.after.line}`;
510
+ if (edit.pos) {
511
+ lineKey = `i:${edit.pos.line}`;
520
512
  break;
521
513
  }
522
514
  lineKey = "ieof";
523
515
  break;
524
516
  case "prepend":
525
- if (edit.before) {
526
- lineKey = `ib:${edit.before.line}`;
517
+ if (edit.pos) {
518
+ lineKey = `ib:${edit.pos.line}`;
527
519
  break;
528
520
  }
529
521
  lineKey = "ibef";
530
522
  break;
531
- case "insert":
532
- lineKey = `ix:${edit.after.line}:${edit.before.line}`;
533
- break;
534
523
  }
535
- const dstKey = `${lineKey}:${edit.content.join("\n")}`;
524
+ const dstKey = `${lineKey}:${edit.lines.join("\n")}`;
536
525
  if (seenEditKeys.has(dstKey)) {
537
526
  dedupIndices.add(i);
538
527
  } else {
@@ -551,25 +540,21 @@ export function applyHashlineEdits(
551
540
  let precedence: number;
552
541
  switch (edit.op) {
553
542
  case "replace":
554
- if ("tag" in edit) {
555
- sortLine = edit.tag.line;
543
+ if (!edit.end) {
544
+ sortLine = edit.pos.line;
556
545
  } else {
557
- sortLine = edit.last.line;
546
+ sortLine = edit.end.line;
558
547
  }
559
548
  precedence = 0;
560
549
  break;
561
550
  case "append":
562
- sortLine = edit.after ? edit.after.line : fileLines.length + 1;
551
+ sortLine = edit.pos ? edit.pos.line : fileLines.length + 1;
563
552
  precedence = 1;
564
553
  break;
565
554
  case "prepend":
566
- sortLine = edit.before ? edit.before.line : 0;
555
+ sortLine = edit.pos ? edit.pos.line : 0;
567
556
  precedence = 2;
568
557
  break;
569
- case "insert":
570
- sortLine = edit.before.line;
571
- precedence = 3;
572
- break;
573
558
  }
574
559
  return { edit, idx, sortLine, precedence };
575
560
  });
@@ -580,40 +565,40 @@ export function applyHashlineEdits(
580
565
  for (const { edit, idx } of annotated) {
581
566
  switch (edit.op) {
582
567
  case "replace": {
583
- if ("tag" in edit) {
584
- const origLines = originalFileLines.slice(edit.tag.line - 1, edit.tag.line);
585
- const newLines = edit.content;
568
+ if (!edit.end) {
569
+ const origLines = originalFileLines.slice(edit.pos.line - 1, edit.pos.line);
570
+ const newLines = edit.lines;
586
571
  if (origLines.every((line, i) => line === newLines[i])) {
587
572
  noopEdits.push({
588
573
  editIndex: idx,
589
- loc: `${edit.tag.line}#${edit.tag.hash}`,
590
- currentContent: origLines.join("\n"),
574
+ loc: `${edit.pos.line}#${edit.pos.hash}`,
575
+ current: origLines.join("\n"),
591
576
  });
592
577
  break;
593
578
  }
594
- fileLines.splice(edit.tag.line - 1, 1, ...newLines);
595
- trackFirstChanged(edit.tag.line);
579
+ fileLines.splice(edit.pos.line - 1, 1, ...newLines);
580
+ trackFirstChanged(edit.pos.line);
596
581
  } else {
597
- const count = edit.last.line - edit.first.line + 1;
598
- const newLines = edit.content;
599
- fileLines.splice(edit.first.line - 1, count, ...newLines);
600
- trackFirstChanged(edit.first.line);
582
+ const count = edit.end.line - edit.pos.line + 1;
583
+ const newLines = edit.lines;
584
+ fileLines.splice(edit.pos.line - 1, count, ...newLines);
585
+ trackFirstChanged(edit.pos.line);
601
586
  }
602
587
  break;
603
588
  }
604
589
  case "append": {
605
- const inserted = edit.content;
590
+ const inserted = edit.lines;
606
591
  if (inserted.length === 0) {
607
592
  noopEdits.push({
608
593
  editIndex: idx,
609
- loc: edit.after ? `${edit.after.line}#${edit.after.hash}` : "EOF",
610
- currentContent: edit.after ? originalFileLines[edit.after.line - 1] : "",
594
+ loc: edit.pos ? `${edit.pos.line}#${edit.pos.hash}` : "EOF",
595
+ current: edit.pos ? originalFileLines[edit.pos.line - 1] : "",
611
596
  });
612
597
  break;
613
598
  }
614
- if (edit.after) {
615
- fileLines.splice(edit.after.line, 0, ...inserted);
616
- trackFirstChanged(edit.after.line + 1);
599
+ if (edit.pos) {
600
+ fileLines.splice(edit.pos.line, 0, ...inserted);
601
+ trackFirstChanged(edit.pos.line + 1);
617
602
  } else {
618
603
  if (fileLines.length === 1 && fileLines[0] === "") {
619
604
  fileLines.splice(0, 1, ...inserted);
@@ -626,18 +611,18 @@ export function applyHashlineEdits(
626
611
  break;
627
612
  }
628
613
  case "prepend": {
629
- const inserted = edit.content;
614
+ const inserted = edit.lines;
630
615
  if (inserted.length === 0) {
631
616
  noopEdits.push({
632
617
  editIndex: idx,
633
- loc: edit.before ? `${edit.before.line}#${edit.before.hash}` : "BOF",
634
- currentContent: edit.before ? originalFileLines[edit.before.line - 1] : "",
618
+ loc: edit.pos ? `${edit.pos.line}#${edit.pos.hash}` : "BOF",
619
+ current: edit.pos ? originalFileLines[edit.pos.line - 1] : "",
635
620
  });
636
621
  break;
637
622
  }
638
- if (edit.before) {
639
- fileLines.splice(edit.before.line - 1, 0, ...inserted);
640
- trackFirstChanged(edit.before.line);
623
+ if (edit.pos) {
624
+ fileLines.splice(edit.pos.line - 1, 0, ...inserted);
625
+ trackFirstChanged(edit.pos.line);
641
626
  } else {
642
627
  if (fileLines.length === 1 && fileLines[0] === "") {
643
628
  fileLines.splice(0, 1, ...inserted);
@@ -648,27 +633,11 @@ export function applyHashlineEdits(
648
633
  }
649
634
  break;
650
635
  }
651
- case "insert": {
652
- const afterLine = originalFileLines[edit.after.line - 1];
653
- const beforeLine = originalFileLines[edit.before.line - 1];
654
- const inserted = edit.content;
655
- if (inserted.length === 0) {
656
- noopEdits.push({
657
- editIndex: idx,
658
- loc: `${edit.after.line}#${edit.after.hash}..${edit.before.line}#${edit.before.hash}`,
659
- currentContent: `${afterLine}\n${beforeLine}`,
660
- });
661
- break;
662
- }
663
- fileLines.splice(edit.before.line - 1, 0, ...inserted);
664
- trackFirstChanged(edit.before.line);
665
- break;
666
- }
667
636
  }
668
637
  }
669
638
 
670
639
  return {
671
- content: fileLines.join("\n"),
640
+ lines: fileLines.join("\n"),
672
641
  firstChangedLine,
673
642
  ...(noopEdits.length > 0 ? { noopEdits } : {}),
674
643
  };