@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.
- package/CHANGELOG.md +16 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +33 -3
- package/src/commit/prompts/analysis-system.md +3 -3
- package/src/commit/prompts/changelog-system.md +3 -3
- package/src/commit/prompts/summary-system.md +5 -5
- package/src/extensibility/custom-tools/wrapper.ts +1 -0
- package/src/extensibility/extensions/wrapper.ts +2 -0
- package/src/extensibility/hooks/tool-wrapper.ts +1 -0
- package/src/lsp/index.ts +1 -0
- package/src/patch/diff.ts +2 -2
- package/src/patch/hashline.ts +88 -119
- package/src/patch/index.ts +88 -124
- package/src/patch/shared.ts +14 -21
- package/src/prompts/system/agent-creation-architect.md +2 -2
- package/src/prompts/system/system-prompt.md +9 -9
- package/src/prompts/tools/ask.md +3 -18
- package/src/prompts/tools/{poll-jobs.md → await.md} +1 -1
- package/src/prompts/tools/bash.md +4 -3
- package/src/prompts/tools/browser.md +11 -18
- package/src/prompts/tools/fetch.md +2 -5
- package/src/prompts/tools/find.md +10 -1
- package/src/prompts/tools/grep.md +1 -4
- package/src/prompts/tools/hashline.md +131 -155
- package/src/prompts/tools/lsp.md +6 -16
- package/src/prompts/tools/read.md +3 -6
- package/src/prompts/tools/task.md +93 -275
- package/src/prompts/tools/web-search.md +0 -7
- package/src/prompts/tools/write.md +0 -5
- package/src/sdk.ts +12 -2
- package/src/system-prompt.ts +5 -0
- package/src/task/index.ts +1 -0
- package/src/tools/ask.ts +1 -0
- package/src/tools/{poll-jobs.ts → await-tool.ts} +20 -19
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +19 -4
- package/src/tools/calculator.ts +1 -0
- package/src/tools/cancel-job.ts +1 -0
- package/src/tools/exit-plan-mode.ts +1 -0
- package/src/tools/fetch.ts +1 -0
- package/src/tools/find.ts +1 -0
- package/src/tools/grep.ts +1 -0
- package/src/tools/index.ts +3 -3
- package/src/tools/notebook.ts +2 -1
- package/src/tools/plan-mode-guard.ts +2 -2
- package/src/tools/python.ts +1 -0
- package/src/tools/read.ts +1 -0
- package/src/tools/ssh.ts +1 -0
- package/src/tools/submit-result.ts +1 -0
- package/src/tools/todo-write.ts +1 -0
- package/src/tools/write.ts +1 -0
- 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
|
|
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
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.0
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.0
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.0
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.0
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.0
|
|
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
|
-
//
|
|
87
|
-
|
|
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
|
|
11
|
-
- 50 lines in src/api/, 50 in src/types/
|
|
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
|
|
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
|
|
34
|
-
- Added new feature.
|
|
35
|
-
- Refactored parser internals
|
|
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
|
-
|
|
26
|
+
→ added TLS support to prevent man-in-the-middle attacks
|
|
27
27
|
refactor | Consolidated HTTP transport into unified builder pattern
|
|
28
|
-
|
|
28
|
+
→ migrated HTTP transport to unified builder API
|
|
29
29
|
fix | Race condition in connection pool causing exhaustion under load
|
|
30
|
-
|
|
30
|
+
→ corrected race condition causing connection pool exhaustion
|
|
31
31
|
perf | Batch processing optimized to reduce memory allocations
|
|
32
|
-
|
|
32
|
+
→ eliminated allocation overhead in batch processing
|
|
33
33
|
build | Updated serde to fix CVE-2024-1234
|
|
34
|
-
|
|
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.
|
|
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.
|
|
421
|
+
return generateDiffString(normalizedContent, result.lines);
|
|
422
422
|
} catch (err) {
|
|
423
423
|
return { error: err instanceof Error ? err.message : String(err) };
|
|
424
424
|
}
|
package/src/patch/hashline.ts
CHANGED
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hashline edit mode — a line-addressable edit format using
|
|
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
|
|
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:
|
|
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
|
|
17
|
+
export type Anchor = { line: number; hash: string };
|
|
18
18
|
export type HashlineEdit =
|
|
19
|
-
| { op: "replace";
|
|
20
|
-
| { op: "
|
|
21
|
-
| { op: "
|
|
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
|
|
37
|
-
*
|
|
38
|
-
*
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
55
|
+
* Formats a tag given the line number and text.
|
|
52
56
|
*/
|
|
53
|
-
export function formatLineTag(line: number,
|
|
54
|
-
return `${line}#${computeLineHash(line,
|
|
57
|
+
export function formatLineTag(line: number, lines: string): string {
|
|
58
|
+
return `${line}#${computeLineHash(line, lines)}`;
|
|
55
59
|
}
|
|
56
60
|
|
|
57
61
|
/**
|
|
58
|
-
* Format file
|
|
62
|
+
* Format file text with hashline prefixes for display.
|
|
59
63
|
*
|
|
60
|
-
* Each line becomes `LINENUM#HASH:
|
|
64
|
+
* Each line becomes `LINENUM#HASH:TEXT` where LINENUM is 1-indexed.
|
|
61
65
|
*
|
|
62
|
-
* @param
|
|
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(
|
|
73
|
-
const lines =
|
|
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
|
|
379
|
-
const hash = computeLineHash(lineNum,
|
|
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}:${
|
|
387
|
+
lines.push(`>>> ${prefix}:${text}`);
|
|
384
388
|
} else {
|
|
385
|
-
lines.push(` ${prefix}:${
|
|
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
|
-
* `
|
|
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
|
-
|
|
431
|
+
text: string,
|
|
428
432
|
edits: HashlineEdit[],
|
|
429
433
|
): {
|
|
430
|
-
|
|
434
|
+
lines: string;
|
|
431
435
|
firstChangedLine: number | undefined;
|
|
432
436
|
warnings?: string[];
|
|
433
|
-
noopEdits?: Array<{ editIndex: number; loc: string;
|
|
437
|
+
noopEdits?: Array<{ editIndex: number; loc: string; current: string }>;
|
|
434
438
|
} {
|
|
435
439
|
if (edits.length === 0) {
|
|
436
|
-
return {
|
|
440
|
+
return { lines: text, firstChangedLine: undefined };
|
|
437
441
|
}
|
|
438
442
|
|
|
439
|
-
const fileLines =
|
|
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;
|
|
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 (
|
|
461
|
-
|
|
462
|
-
|
|
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.
|
|
474
|
-
|
|
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.
|
|
481
|
-
|
|
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 (
|
|
512
|
-
lineKey = `s:${edit.
|
|
503
|
+
if (!edit.end) {
|
|
504
|
+
lineKey = `s:${edit.pos.line}`;
|
|
513
505
|
} else {
|
|
514
|
-
lineKey = `r:${edit.
|
|
506
|
+
lineKey = `r:${edit.pos.line}:${edit.end.line}`;
|
|
515
507
|
}
|
|
516
508
|
break;
|
|
517
509
|
case "append":
|
|
518
|
-
if (edit.
|
|
519
|
-
lineKey = `i:${edit.
|
|
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.
|
|
526
|
-
lineKey = `ib:${edit.
|
|
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.
|
|
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 (
|
|
555
|
-
sortLine = edit.
|
|
543
|
+
if (!edit.end) {
|
|
544
|
+
sortLine = edit.pos.line;
|
|
556
545
|
} else {
|
|
557
|
-
sortLine = edit.
|
|
546
|
+
sortLine = edit.end.line;
|
|
558
547
|
}
|
|
559
548
|
precedence = 0;
|
|
560
549
|
break;
|
|
561
550
|
case "append":
|
|
562
|
-
sortLine = edit.
|
|
551
|
+
sortLine = edit.pos ? edit.pos.line : fileLines.length + 1;
|
|
563
552
|
precedence = 1;
|
|
564
553
|
break;
|
|
565
554
|
case "prepend":
|
|
566
|
-
sortLine = edit.
|
|
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 (
|
|
584
|
-
const origLines = originalFileLines.slice(edit.
|
|
585
|
-
const newLines = edit.
|
|
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.
|
|
590
|
-
|
|
574
|
+
loc: `${edit.pos.line}#${edit.pos.hash}`,
|
|
575
|
+
current: origLines.join("\n"),
|
|
591
576
|
});
|
|
592
577
|
break;
|
|
593
578
|
}
|
|
594
|
-
fileLines.splice(edit.
|
|
595
|
-
trackFirstChanged(edit.
|
|
579
|
+
fileLines.splice(edit.pos.line - 1, 1, ...newLines);
|
|
580
|
+
trackFirstChanged(edit.pos.line);
|
|
596
581
|
} else {
|
|
597
|
-
const count = edit.
|
|
598
|
-
const newLines = edit.
|
|
599
|
-
fileLines.splice(edit.
|
|
600
|
-
trackFirstChanged(edit.
|
|
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.
|
|
590
|
+
const inserted = edit.lines;
|
|
606
591
|
if (inserted.length === 0) {
|
|
607
592
|
noopEdits.push({
|
|
608
593
|
editIndex: idx,
|
|
609
|
-
loc: edit.
|
|
610
|
-
|
|
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.
|
|
615
|
-
fileLines.splice(edit.
|
|
616
|
-
trackFirstChanged(edit.
|
|
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.
|
|
614
|
+
const inserted = edit.lines;
|
|
630
615
|
if (inserted.length === 0) {
|
|
631
616
|
noopEdits.push({
|
|
632
617
|
editIndex: idx,
|
|
633
|
-
loc: edit.
|
|
634
|
-
|
|
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.
|
|
639
|
-
fileLines.splice(edit.
|
|
640
|
-
trackFirstChanged(edit.
|
|
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
|
-
|
|
640
|
+
lines: fileLines.join("\n"),
|
|
672
641
|
firstChangedLine,
|
|
673
642
|
...(noopEdits.length > 0 ? { noopEdits } : {}),
|
|
674
643
|
};
|