@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.2
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 +110 -0
- package/dist/types/cli/file-processor.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +45 -3
- package/dist/types/config/settings.d.ts +1 -1
- package/dist/types/debug/raw-sse.d.ts +2 -0
- package/dist/types/edit/file-read-cache.d.ts +15 -4
- package/dist/types/edit/index.d.ts +3 -8
- package/dist/types/edit/renderer.d.ts +1 -2
- package/dist/types/eval/__tests__/shared-executors.test.d.ts +1 -0
- package/dist/types/eval/js/shared/local-module-loader.d.ts +16 -0
- package/dist/types/eval/js/shared/rewrite-imports.d.ts +4 -0
- package/dist/types/eval/js/shared/runtime.d.ts +14 -8
- package/dist/types/eval/py/executor.d.ts +1 -2
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -5
- package/dist/types/eval/session-id.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +1 -3
- package/dist/types/hashline/anchors.d.ts +15 -9
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/diff.d.ts +1 -2
- package/dist/types/hashline/executor.d.ts +52 -0
- package/dist/types/hashline/hash.d.ts +44 -93
- package/dist/types/hashline/index.d.ts +2 -1
- package/dist/types/hashline/input.d.ts +2 -9
- package/dist/types/hashline/recovery.d.ts +3 -9
- package/dist/types/hashline/tokenizer.d.ts +91 -0
- package/dist/types/hashline/types.d.ts +5 -7
- package/dist/types/modes/components/extensions/types.d.ts +0 -4
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/sdk.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +11 -15
- package/dist/types/session/agent-storage.d.ts +11 -10
- package/dist/types/slash-commands/acp-builtins.d.ts +3 -3
- package/dist/types/slash-commands/types.d.ts +0 -5
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tool-discovery/tool-index.d.ts +0 -50
- package/dist/types/tools/index.d.ts +2 -8
- package/dist/types/tools/match-line-format.d.ts +4 -4
- package/dist/types/tools/output-schema-validator.d.ts +64 -0
- package/dist/types/tools/review.d.ts +13 -0
- package/dist/types/tools/search-tool-bm25.d.ts +1 -1
- package/dist/types/tools/search.d.ts +4 -3
- package/dist/types/utils/edit-mode.d.ts +1 -1
- package/dist/types/web/kagi.d.ts +4 -2
- package/dist/types/web/parallel.d.ts +4 -3
- package/dist/types/web/scrapers/types.d.ts +2 -1
- package/dist/types/web/search/index.d.ts +12 -4
- package/dist/types/web/search/provider.d.ts +2 -1
- package/dist/types/web/search/providers/anthropic.d.ts +9 -4
- package/dist/types/web/search/providers/base.d.ts +34 -2
- package/dist/types/web/search/providers/brave.d.ts +8 -1
- package/dist/types/web/search/providers/codex.d.ts +13 -9
- package/dist/types/web/search/providers/exa.d.ts +10 -1
- package/dist/types/web/search/providers/gemini.d.ts +20 -23
- package/dist/types/web/search/providers/jina.d.ts +2 -1
- package/dist/types/web/search/providers/kagi.d.ts +4 -1
- package/dist/types/web/search/providers/kimi.d.ts +10 -1
- package/dist/types/web/search/providers/parallel.d.ts +3 -2
- package/dist/types/web/search/providers/perplexity.d.ts +5 -2
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +5 -8
- package/dist/types/web/search/providers/tavily.d.ts +11 -4
- package/dist/types/web/search/providers/utils.d.ts +8 -6
- package/dist/types/web/search/providers/zai.d.ts +12 -3
- package/package.json +7 -7
- package/src/cli/file-processor.ts +12 -2
- package/src/cli.ts +0 -8
- package/src/commands/commit.ts +8 -8
- package/src/config/prompt-templates.ts +6 -6
- package/src/config/settings-schema.ts +47 -3
- package/src/config/settings.ts +5 -5
- package/src/debug/raw-sse.ts +68 -3
- package/src/edit/file-read-cache.ts +68 -25
- package/src/edit/index.ts +6 -37
- package/src/edit/renderer.ts +9 -47
- package/src/edit/streaming.ts +43 -56
- package/src/eval/__tests__/shared-executors.test.ts +520 -0
- package/src/eval/js/context-manager.ts +64 -53
- package/src/eval/js/shared/local-module-loader.ts +265 -0
- package/src/eval/js/shared/prelude.txt +4 -0
- package/src/eval/js/shared/rewrite-imports.ts +85 -0
- package/src/eval/js/shared/runtime.ts +129 -86
- package/src/eval/js/worker-core.ts +23 -38
- package/src/eval/py/executor.ts +155 -84
- package/src/eval/py/kernel.ts +10 -1
- package/src/eval/py/prelude.py +22 -24
- package/src/eval/py/runner.py +203 -85
- package/src/eval/py/tool-bridge.ts +17 -10
- package/src/eval/session-id.ts +8 -0
- package/src/exec/bash-executor.ts +27 -16
- package/src/extensibility/extensions/runner.ts +0 -1
- package/src/extensibility/extensions/types.ts +1 -3
- package/src/hashline/anchors.ts +56 -65
- package/src/hashline/apply.ts +29 -31
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff-preview.ts +4 -5
- package/src/hashline/diff.ts +30 -4
- package/src/hashline/execute.ts +91 -26
- package/src/hashline/executor.ts +239 -0
- package/src/hashline/grammar.lark +12 -10
- package/src/hashline/hash.ts +69 -114
- package/src/hashline/index.ts +2 -1
- package/src/hashline/input.ts +48 -41
- package/src/hashline/prefixes.ts +21 -11
- package/src/hashline/recovery.ts +63 -71
- package/src/hashline/stream.ts +2 -2
- package/src/hashline/tokenizer.ts +467 -0
- package/src/hashline/types.ts +6 -8
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/components/extensions/types.ts +0 -5
- package/src/modes/components/session-observer-overlay.ts +11 -2
- package/src/modes/components/settings-selector.ts +10 -1
- package/src/modes/components/tree-selector.ts +10 -2
- package/src/modes/controllers/command-controller.ts +1 -3
- package/src/modes/controllers/extension-ui-controller.ts +10 -11
- package/src/modes/controllers/selector-controller.ts +5 -5
- package/src/modes/theme/theme.ts +4 -2
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/ui-helpers.ts +4 -0
- package/src/prompts/agents/explore.md +1 -1
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/eval.md +1 -1
- package/src/prompts/tools/hashline.md +73 -94
- package/src/prompts/tools/read.md +4 -4
- package/src/prompts/tools/search.md +3 -3
- package/src/sdk.ts +33 -26
- package/src/session/agent-session.ts +59 -66
- package/src/session/agent-storage.ts +13 -14
- package/src/slash-commands/acp-builtins.ts +3 -3
- package/src/slash-commands/types.ts +0 -6
- package/src/task/executor.ts +26 -57
- package/src/task/index.ts +8 -4
- package/src/tool-discovery/tool-index.ts +0 -134
- package/src/tools/ast-edit.ts +36 -13
- package/src/tools/ast-grep.ts +45 -4
- package/src/tools/browser/tab-worker.ts +3 -2
- package/src/tools/eval.ts +2 -1
- package/src/tools/fetch.ts +23 -14
- package/src/tools/index.ts +2 -8
- package/src/tools/irc.ts +59 -5
- package/src/tools/match-line-format.ts +5 -7
- package/src/tools/output-schema-validator.ts +132 -0
- package/src/tools/read.ts +142 -31
- package/src/tools/review.ts +23 -0
- package/src/tools/search-tool-bm25.ts +3 -30
- package/src/tools/search.ts +48 -16
- package/src/tools/write.ts +3 -3
- package/src/tools/yield.ts +32 -41
- package/src/utils/edit-mode.ts +1 -2
- package/src/utils/file-mentions.ts +2 -2
- package/src/web/kagi.ts +15 -6
- package/src/web/parallel.ts +9 -6
- package/src/web/scrapers/types.ts +7 -1
- package/src/web/scrapers/youtube.ts +13 -7
- package/src/web/search/index.ts +37 -11
- package/src/web/search/provider.ts +5 -3
- package/src/web/search/providers/anthropic.ts +30 -21
- package/src/web/search/providers/base.ts +35 -2
- package/src/web/search/providers/brave.ts +4 -4
- package/src/web/search/providers/codex.ts +118 -89
- package/src/web/search/providers/exa.ts +3 -2
- package/src/web/search/providers/gemini.ts +58 -155
- package/src/web/search/providers/jina.ts +4 -4
- package/src/web/search/providers/kagi.ts +17 -11
- package/src/web/search/providers/kimi.ts +29 -13
- package/src/web/search/providers/parallel.ts +171 -23
- package/src/web/search/providers/perplexity.ts +38 -37
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +16 -19
- package/src/web/search/providers/tavily.ts +23 -18
- package/src/web/search/providers/utils.ts +11 -17
- package/src/web/search/providers/zai.ts +16 -8
- package/dist/types/hashline/parser.d.ts +0 -7
- package/dist/types/mcp/discoverable-tool-metadata.d.ts +0 -7
- package/dist/types/tools/vim.d.ts +0 -58
- package/dist/types/vim/buffer.d.ts +0 -41
- package/dist/types/vim/commands.d.ts +0 -6
- package/dist/types/vim/engine.d.ts +0 -47
- package/dist/types/vim/parser.d.ts +0 -3
- package/dist/types/vim/render.d.ts +0 -25
- package/dist/types/vim/types.d.ts +0 -182
- package/src/hashline/parser.ts +0 -246
- package/src/mcp/discoverable-tool-metadata.ts +0 -24
- package/src/prompts/tools/vim.md +0 -98
- package/src/tools/vim.ts +0 -949
- package/src/vim/buffer.ts +0 -309
- package/src/vim/commands.ts +0 -382
- package/src/vim/engine.ts +0 -2409
- package/src/vim/parser.ts +0 -134
- package/src/vim/render.ts +0 -252
- package/src/vim/types.ts +0 -197
package/src/hashline/input.ts
CHANGED
|
@@ -1,15 +1,10 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import type { SplitHashlineOptions } from "./types";
|
|
2
|
+
import { HL_FILE_HASH_SEP, HL_FILE_PREFIX } from "./hash";
|
|
3
|
+
import { HashlineTokenizer } from "./tokenizer";
|
|
4
|
+
import type { HashlineInputSection, SplitHashlineOptions } from "./types";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
export interface HashlineInputSection {
|
|
10
|
-
path: string;
|
|
11
|
-
diff: string;
|
|
12
|
-
}
|
|
6
|
+
// Pure classification — single shared tokenizer is safe.
|
|
7
|
+
const TOKENIZER = new HashlineTokenizer();
|
|
13
8
|
|
|
14
9
|
function unquoteHashlinePath(pathText: string): string {
|
|
15
10
|
if (pathText.length < 2) return pathText;
|
|
@@ -27,27 +22,30 @@ function normalizeHashlinePath(rawPath: string, cwd?: string): string {
|
|
|
27
22
|
return isWithinCwd ? relative || "." : unquoted;
|
|
28
23
|
}
|
|
29
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Parse a `¶PATH[#hash]` header line. Returns `null` for lines that do not
|
|
27
|
+
* begin with the `¶` prefix; throws the existing "Input header must be …"
|
|
28
|
+
* error when a `¶`-prefixed line fails the strict shape (so malformed paths
|
|
29
|
+
* surface immediately instead of being silently re-classified as payload).
|
|
30
|
+
*/
|
|
30
31
|
function parseHashlineHeaderLine(line: string, cwd?: string): HashlineInputSection | null {
|
|
31
32
|
const trimmed = line.trimEnd();
|
|
32
33
|
if (!trimmed.startsWith(HL_FILE_PREFIX)) return null;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
throw new Error(`Input header "${HL_FILE_PREFIX}" is empty; provide a file path.`);
|
|
34
|
+
|
|
35
|
+
const token = TOKENIZER.tokenize(trimmed);
|
|
36
|
+
if (token.kind !== "header") {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Input header must be ${HL_FILE_PREFIX}PATH or ${HL_FILE_PREFIX}PATH${HL_FILE_HASH_SEP}HASH with a 4-hex file hash; got ${JSON.stringify(trimmed)}.`,
|
|
39
|
+
);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
|
|
42
|
+
const parsedPath = normalizeHashlinePath(token.path, cwd);
|
|
42
43
|
if (parsedPath.length === 0) {
|
|
43
44
|
throw new Error(`Input header "${HL_FILE_PREFIX}" is empty; provide a file path.`);
|
|
44
45
|
}
|
|
45
|
-
return
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isPatchEnvelopeMarker(line: string): boolean {
|
|
49
|
-
const trimmed = line.trimEnd();
|
|
50
|
-
return trimmed === BEGIN_PATCH_MARKER || trimmed === END_PATCH_MARKER;
|
|
46
|
+
return token.fileHash !== undefined
|
|
47
|
+
? { path: parsedPath, fileHash: token.fileHash, diff: "" }
|
|
48
|
+
: { path: parsedPath, diff: "" };
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
function stripLeadingBlankLines(input: string): string {
|
|
@@ -55,7 +53,7 @@ function stripLeadingBlankLines(input: string): string {
|
|
|
55
53
|
const lines = stripped.split("\n");
|
|
56
54
|
while (lines.length > 0) {
|
|
57
55
|
const head = lines[0].replace(/\r$/, "");
|
|
58
|
-
if (head.trim().length === 0 ||
|
|
56
|
+
if (head.trim().length === 0 || TOKENIZER.tokenize(head).kind === "envelope-begin") {
|
|
59
57
|
lines.shift();
|
|
60
58
|
continue;
|
|
61
59
|
}
|
|
@@ -66,7 +64,7 @@ function stripLeadingBlankLines(input: string): string {
|
|
|
66
64
|
|
|
67
65
|
export function containsRecognizableHashlineOperations(input: string): boolean {
|
|
68
66
|
for (const line of input.split(/\r?\n/)) {
|
|
69
|
-
if (
|
|
67
|
+
if (TOKENIZER.isOp(line)) return true;
|
|
70
68
|
}
|
|
71
69
|
return false;
|
|
72
70
|
}
|
|
@@ -84,7 +82,7 @@ function normalizeFallbackInput(input: string, options: SplitHashlineOptions): s
|
|
|
84
82
|
return `${HL_FILE_PREFIX}${fallbackPath}\n${input}`;
|
|
85
83
|
}
|
|
86
84
|
|
|
87
|
-
export function splitHashlineInput(input: string, options: SplitHashlineOptions = {}):
|
|
85
|
+
export function splitHashlineInput(input: string, options: SplitHashlineOptions = {}): HashlineInputSection {
|
|
88
86
|
const [section] = splitHashlineInputs(input, options);
|
|
89
87
|
return section;
|
|
90
88
|
}
|
|
@@ -97,33 +95,42 @@ export function splitHashlineInputs(input: string, options: SplitHashlineOptions
|
|
|
97
95
|
if (parseHashlineHeaderLine(firstLine, options.cwd) === null) {
|
|
98
96
|
const preview = JSON.stringify(firstLine.slice(0, 120));
|
|
99
97
|
throw new Error(
|
|
100
|
-
`input must begin with "${HL_FILE_PREFIX}PATH" on the first non-blank line; got: ${preview}. ` +
|
|
101
|
-
`Example: "${HL_FILE_PREFIX}src/foo.ts" then edit ops.`,
|
|
98
|
+
`input must begin with "${HL_FILE_PREFIX}PATH${HL_FILE_HASH_SEP}HASH" on the first non-blank line for anchored edits; got: ${preview}. ` +
|
|
99
|
+
`Example: "${HL_FILE_PREFIX}src/foo.ts${HL_FILE_HASH_SEP}1a2b" then edit ops.`,
|
|
102
100
|
);
|
|
103
101
|
}
|
|
104
102
|
|
|
105
103
|
const sections: HashlineInputSection[] = [];
|
|
106
|
-
let
|
|
104
|
+
let current: HashlineInputSection | undefined;
|
|
107
105
|
let currentLines: string[] = [];
|
|
108
106
|
|
|
109
107
|
const flush = () => {
|
|
110
|
-
if (
|
|
108
|
+
if (!current) return;
|
|
111
109
|
const hasOps = currentLines.some(line => line.trim().length > 0);
|
|
112
|
-
if (hasOps) sections.push({
|
|
110
|
+
if (hasOps) sections.push({ ...current, diff: currentLines.join("\n") });
|
|
113
111
|
currentLines = [];
|
|
114
112
|
};
|
|
115
113
|
|
|
116
114
|
for (const line of lines) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
115
|
+
const trimmed = line.trimEnd();
|
|
116
|
+
const token = TOKENIZER.tokenize(line);
|
|
117
|
+
if (token.kind === "envelope-end" || token.kind === "abort") break;
|
|
118
|
+
if (token.kind === "envelope-begin") continue;
|
|
119
|
+
|
|
120
|
+
// Route every `¶`-prefixed line through parseHashlineHeaderLine so
|
|
121
|
+
// malformed headers still raise the strict "Input header must be …"
|
|
122
|
+
// diagnostic (the tokenizer alone would silently classify them as
|
|
123
|
+
// payload).
|
|
124
|
+
if (trimmed.startsWith(HL_FILE_PREFIX)) {
|
|
125
|
+
const header = parseHashlineHeaderLine(line, options.cwd);
|
|
126
|
+
if (header !== null) {
|
|
127
|
+
flush();
|
|
128
|
+
current = header;
|
|
129
|
+
currentLines = [];
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
126
132
|
}
|
|
133
|
+
currentLines.push(line);
|
|
127
134
|
}
|
|
128
135
|
flush();
|
|
129
136
|
return sections;
|
package/src/hashline/prefixes.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
const HL_PREFIX_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*(?:[+*]\\s*)?\\d+[a-z]{2}${HL_OUTPUT_PREFIX_SEPARATOR_RE}`);
|
|
5
|
-
const HL_PREFIX_PLUS_RE = new RegExp(`^\\s*(?:>>>|>>)?\\s*\\+\\s*\\d+[a-z]{2}${HL_OUTPUT_PREFIX_SEPARATOR_RE}`);
|
|
1
|
+
const HL_PREFIX_RE = /^\s*(?:>>>|>>)?\s*(?:[+*-]\s*)?\d+:/;
|
|
2
|
+
const HL_PREFIX_PLUS_RE = /^\s*(?:>>>|>>)?\s*\+\s*\d+:/;
|
|
3
|
+
const HL_HEADER_RE = /^\s*¶\S+#[0-9a-f]{4}\s*$/;
|
|
6
4
|
const DIFF_PLUS_RE = /^[+](?![+])/;
|
|
7
5
|
const READ_TRUNCATION_NOTICE_RE = /^\[(?:Showing lines \d+-\d+ of \d+|\d+ more lines? in (?:file|\S+))\b.*\bUse :L?\d+/;
|
|
8
6
|
|
|
@@ -20,12 +18,14 @@ function stripLeadingHashlinePrefixes(line: string): string {
|
|
|
20
18
|
// 5. Read-output prefix stripping
|
|
21
19
|
//
|
|
22
20
|
// When a model echoes back content from a `read` or `search` response, every
|
|
23
|
-
// line is prefixed with either a hashline
|
|
24
|
-
// echoes, a leading `+`. These helpers detect that and recover the
|
|
21
|
+
// line is prefixed with either a hashline-mode line number (`123:`) or, for
|
|
22
|
+
// diff-style echoes, a leading `+`. These helpers detect that and recover the
|
|
23
|
+
// raw text.
|
|
25
24
|
// ───────────────────────────────────────────────────────────────────────────
|
|
26
25
|
|
|
27
26
|
type LinePrefixStats = {
|
|
28
27
|
nonEmpty: number;
|
|
28
|
+
headerCount: number;
|
|
29
29
|
hashPrefixCount: number;
|
|
30
30
|
diffPlusHashPrefixCount: number;
|
|
31
31
|
diffPlusCount: number;
|
|
@@ -35,6 +35,7 @@ type LinePrefixStats = {
|
|
|
35
35
|
function collectLinePrefixStats(lines: string[]): LinePrefixStats {
|
|
36
36
|
const stats: LinePrefixStats = {
|
|
37
37
|
nonEmpty: 0,
|
|
38
|
+
headerCount: 0,
|
|
38
39
|
hashPrefixCount: 0,
|
|
39
40
|
diffPlusHashPrefixCount: 0,
|
|
40
41
|
diffPlusCount: 0,
|
|
@@ -47,6 +48,11 @@ function collectLinePrefixStats(lines: string[]): LinePrefixStats {
|
|
|
47
48
|
stats.truncationNoticeCount++;
|
|
48
49
|
continue;
|
|
49
50
|
}
|
|
51
|
+
if (HL_HEADER_RE.test(line)) {
|
|
52
|
+
stats.nonEmpty++;
|
|
53
|
+
stats.headerCount++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
50
56
|
stats.nonEmpty++;
|
|
51
57
|
if (HL_PREFIX_RE.test(line)) stats.hashPrefixCount++;
|
|
52
58
|
if (HL_PREFIX_PLUS_RE.test(line)) stats.diffPlusHashPrefixCount++;
|
|
@@ -59,7 +65,8 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
59
65
|
const stats = collectLinePrefixStats(lines);
|
|
60
66
|
if (stats.nonEmpty === 0) return lines;
|
|
61
67
|
|
|
62
|
-
const
|
|
68
|
+
const contentLineCount = stats.nonEmpty - stats.headerCount;
|
|
69
|
+
const stripHash = contentLineCount > 0 && stats.hashPrefixCount === contentLineCount;
|
|
63
70
|
const stripPlus =
|
|
64
71
|
!stripHash &&
|
|
65
72
|
stats.diffPlusHashPrefixCount === 0 &&
|
|
@@ -69,7 +76,7 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
69
76
|
if (!stripHash && !stripPlus && stats.diffPlusHashPrefixCount === 0) return lines;
|
|
70
77
|
|
|
71
78
|
return lines
|
|
72
|
-
.filter(line => !READ_TRUNCATION_NOTICE_RE.test(line))
|
|
79
|
+
.filter(line => !READ_TRUNCATION_NOTICE_RE.test(line) && !(stripHash && HL_HEADER_RE.test(line)))
|
|
73
80
|
.map(line => {
|
|
74
81
|
if (stripHash) return stripLeadingHashlinePrefixes(line);
|
|
75
82
|
if (stripPlus) return line.replace(DIFF_PLUS_RE, "");
|
|
@@ -83,8 +90,11 @@ export function stripNewLinePrefixes(lines: string[]): string[] {
|
|
|
83
90
|
export function stripHashlinePrefixes(lines: string[]): string[] {
|
|
84
91
|
const stats = collectLinePrefixStats(lines);
|
|
85
92
|
if (stats.nonEmpty === 0) return lines;
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
const contentLineCount = stats.nonEmpty - stats.headerCount;
|
|
94
|
+
if (contentLineCount === 0 || stats.hashPrefixCount !== contentLineCount) return lines;
|
|
95
|
+
return lines
|
|
96
|
+
.filter(line => !READ_TRUNCATION_NOTICE_RE.test(line) && !HL_HEADER_RE.test(line))
|
|
97
|
+
.map(line => stripLeadingHashlinePrefixes(line));
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
/**
|
package/src/hashline/recovery.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import * as Diff from "diff";
|
|
2
2
|
import { generateDiffString } from "../edit/diff";
|
|
3
|
-
import type { FileReadCache } from "../edit/file-read-cache";
|
|
4
|
-
import { HashlineMismatchError } from "./anchors";
|
|
3
|
+
import type { FileReadCache, FileReadSnapshot } from "../edit/file-read-cache";
|
|
5
4
|
import { applyHashlineEdits, type HashlineApplyResult } from "./apply";
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
5
|
+
import { computeFileHash } from "./hash";
|
|
6
|
+
import type { HashlineApplyOptions, HashlineEdit } from "./types";
|
|
8
7
|
|
|
9
8
|
export interface HashlineRecoveryArgs {
|
|
10
9
|
cache: FileReadCache;
|
|
11
10
|
absolutePath: string;
|
|
12
11
|
currentText: string;
|
|
12
|
+
fileHash: string;
|
|
13
13
|
edits: HashlineEdit[];
|
|
14
14
|
options: HashlineApplyOptions;
|
|
15
15
|
}
|
|
@@ -20,76 +20,28 @@ export interface HashlineRecoveryResult {
|
|
|
20
20
|
warnings: string[];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
//
|
|
24
|
-
// duplicate closer 100+ lines away. If
|
|
25
|
-
//
|
|
23
|
+
// Section hashes are line-precise; never let Diff.applyPatch slide a hunk onto a
|
|
24
|
+
// duplicate closer 100+ lines away. If snapshot replay does not align exactly,
|
|
25
|
+
// refuse and let the model re-read.
|
|
26
26
|
const HASHLINE_RECOVERY_FUZZ_FACTOR = 0;
|
|
27
27
|
|
|
28
|
-
const
|
|
29
|
-
"Recovered from stale
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
function collectEditAnchors(edits: HashlineEdit[]): Anchor[] {
|
|
33
|
-
const anchors: Anchor[] = [];
|
|
34
|
-
for (const edit of edits) {
|
|
35
|
-
if (edit.kind === "delete") {
|
|
36
|
-
anchors.push(edit.anchor);
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
const cursor = edit.cursor;
|
|
40
|
-
if (cursor.kind === "before_anchor" || cursor.kind === "after_anchor") {
|
|
41
|
-
anchors.push(cursor.anchor);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return anchors;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Attempt to recover from a `HashlineMismatchError` by replaying the edits
|
|
49
|
-
* against a cached pre-edit snapshot of the file and 3-way-merging the result
|
|
50
|
-
* onto the current on-disk content. Returns `null` when no recovery is
|
|
51
|
-
* possible — callers should propagate the original mismatch error in that
|
|
52
|
-
* case.
|
|
53
|
-
*
|
|
54
|
-
* Recovery is gated on a strict precondition: every line the model anchored
|
|
55
|
-
* MUST be present in the cached snapshot AND its content MUST hash to the
|
|
56
|
-
* model-supplied hash. This prevents 3-way merges from silently sliding onto
|
|
57
|
-
* the wrong site when only tangential parts of the file went stale.
|
|
58
|
-
*/
|
|
59
|
-
export function tryRecoverHashlineWithCache(args: HashlineRecoveryArgs): HashlineRecoveryResult | null {
|
|
60
|
-
const { cache, absolutePath, currentText, edits, options } = args;
|
|
61
|
-
const snapshot = cache.get(absolutePath);
|
|
62
|
-
if (!snapshot || snapshot.lines.size === 0) return null;
|
|
63
|
-
|
|
64
|
-
// Precondition: the model's anchors must be vouched-for by the cache. If
|
|
65
|
-
// even one anchored line is missing from the snapshot, or its cached
|
|
66
|
-
// content hashes to a different value than the model supplied, refuse —
|
|
67
|
-
// any merge from here is a guess.
|
|
68
|
-
const anchors = collectEditAnchors(edits);
|
|
69
|
-
for (const anchor of anchors) {
|
|
70
|
-
const cachedLine = snapshot.lines.get(anchor.line);
|
|
71
|
-
if (cachedLine === undefined) return null;
|
|
72
|
-
if (computeLineHash(anchor.line, cachedLine) !== anchor.hash) return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const overlaid = currentText.split("\n");
|
|
76
|
-
let maxCachedLine = 0;
|
|
77
|
-
for (const lineNum of snapshot.lines.keys()) {
|
|
78
|
-
if (lineNum > maxCachedLine) maxCachedLine = lineNum;
|
|
79
|
-
}
|
|
80
|
-
while (overlaid.length < maxCachedLine) overlaid.push("");
|
|
81
|
-
for (const [lineNum, content] of snapshot.lines) {
|
|
82
|
-
overlaid[lineNum - 1] = content;
|
|
83
|
-
}
|
|
84
|
-
const previousText = overlaid.join("\n");
|
|
85
|
-
if (previousText === currentText) return null;
|
|
28
|
+
const HASHLINE_RECOVERY_EXTERNAL_WARNING =
|
|
29
|
+
"Recovered from a stale file hash using a previous read snapshot (file changed externally between read and edit).";
|
|
30
|
+
const HASHLINE_RECOVERY_SESSION_CHAIN_WARNING =
|
|
31
|
+
"Recovered from a stale file hash using an earlier in-session snapshot (the file hash advanced after a prior edit in this session).";
|
|
86
32
|
|
|
33
|
+
function applyEditsToSnapshot(
|
|
34
|
+
previousText: string,
|
|
35
|
+
currentText: string,
|
|
36
|
+
edits: HashlineEdit[],
|
|
37
|
+
options: HashlineApplyOptions,
|
|
38
|
+
recoveryWarning: string,
|
|
39
|
+
): HashlineRecoveryResult | null {
|
|
87
40
|
let applied: HashlineApplyResult;
|
|
88
41
|
try {
|
|
89
42
|
applied = applyHashlineEdits(previousText, edits, options);
|
|
90
|
-
} catch
|
|
91
|
-
|
|
92
|
-
throw err;
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
93
45
|
}
|
|
94
46
|
if (applied.lines === previousText) return null;
|
|
95
47
|
|
|
@@ -98,11 +50,9 @@ export function tryRecoverHashlineWithCache(args: HashlineRecoveryArgs): Hashlin
|
|
|
98
50
|
if (typeof merged !== "string" || merged === currentText) return null;
|
|
99
51
|
|
|
100
52
|
const mergedDiff = generateDiffString(currentText, merged);
|
|
101
|
-
// Only surface the recovery warning when the merge actually changed
|
|
102
|
-
// something visible. A no-op merge (e.g. trailing-newline only) is noise.
|
|
103
53
|
const hasNetChange = mergedDiff.firstChangedLine !== undefined;
|
|
104
54
|
const recoveryWarnings = hasNetChange
|
|
105
|
-
? [
|
|
55
|
+
? [recoveryWarning, ...(applied.warnings ?? [])]
|
|
106
56
|
: [...(applied.warnings ?? [])];
|
|
107
57
|
|
|
108
58
|
return {
|
|
@@ -111,3 +61,45 @@ export function tryRecoverHashlineWithCache(args: HashlineRecoveryArgs): Hashlin
|
|
|
111
61
|
warnings: recoveryWarnings,
|
|
112
62
|
};
|
|
113
63
|
}
|
|
64
|
+
|
|
65
|
+
function buildSparseOverlayText(currentText: string, snapshotLines: ReadonlyMap<number, string>): string {
|
|
66
|
+
const overlaid = currentText.split("\n");
|
|
67
|
+
let maxCachedLine = 0;
|
|
68
|
+
for (const lineNum of snapshotLines.keys()) {
|
|
69
|
+
if (lineNum > maxCachedLine) maxCachedLine = lineNum;
|
|
70
|
+
}
|
|
71
|
+
while (overlaid.length < maxCachedLine) overlaid.push("");
|
|
72
|
+
for (const [lineNum, content] of snapshotLines) {
|
|
73
|
+
overlaid[lineNum - 1] = content;
|
|
74
|
+
}
|
|
75
|
+
return overlaid.join("\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isHeadSnapshot(head: FileReadSnapshot | null, snapshot: FileReadSnapshot): boolean {
|
|
79
|
+
return head === snapshot;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function resolveRecoveryWarning(head: FileReadSnapshot | null, snapshot: FileReadSnapshot): string {
|
|
83
|
+
return isHeadSnapshot(head, snapshot) ? HASHLINE_RECOVERY_EXTERNAL_WARNING : HASHLINE_RECOVERY_SESSION_CHAIN_WARNING;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Attempt to recover from a section file-hash mismatch by replaying the edits
|
|
88
|
+
* against a cached pre-edit snapshot of the file and 3-way-merging the result
|
|
89
|
+
* onto the current on-disk content. Returns `null` when no recovery is possible.
|
|
90
|
+
*/
|
|
91
|
+
export function tryRecoverHashlineWithCache(args: HashlineRecoveryArgs): HashlineRecoveryResult | null {
|
|
92
|
+
const { cache, absolutePath, currentText, fileHash, edits, options } = args;
|
|
93
|
+
const head = cache.get(absolutePath);
|
|
94
|
+
const snapshot = cache.getByHash(absolutePath, fileHash);
|
|
95
|
+
if (!snapshot || snapshot.lines.size === 0) return null;
|
|
96
|
+
|
|
97
|
+
const recoveryWarning = resolveRecoveryWarning(head, snapshot);
|
|
98
|
+
if (snapshot.fullText !== undefined) {
|
|
99
|
+
return applyEditsToSnapshot(snapshot.fullText, currentText, edits, options, recoveryWarning);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const overlayText = buildSparseOverlayText(currentText, snapshot.lines);
|
|
103
|
+
if (computeFileHash(overlayText) !== fileHash) return null;
|
|
104
|
+
return applyEditsToSnapshot(overlayText, currentText, edits, options, recoveryWarning);
|
|
105
|
+
}
|
package/src/hashline/stream.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatNumberedLine } from "./hash";
|
|
2
2
|
import type { HashlineStreamOptions } from "./types";
|
|
3
3
|
|
|
4
4
|
interface ResolvedHashlineStreamOptions {
|
|
@@ -34,7 +34,7 @@ function createHashlineChunkEmitter(options: ResolvedHashlineStreamOptions): Has
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const pushLine = (line: string): string[] => {
|
|
37
|
-
const formatted =
|
|
37
|
+
const formatted = formatNumberedLine(lineNumber, line);
|
|
38
38
|
lineNumber++;
|
|
39
39
|
|
|
40
40
|
const chunks: string[] = [];
|