@oh-my-pi/pi-coding-agent 15.3.2 → 15.4.1
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 +104 -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/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/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 +17 -23
- 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
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { ABORT_WARNING } from "./constants";
|
|
2
|
+
import { HL_OP_CHARS, HL_OP_DELETE, HL_OP_INSERT_AFTER, HL_OP_INSERT_BEFORE, HL_OP_REPLACE } from "./hash";
|
|
3
|
+
import {
|
|
4
|
+
cloneCursor,
|
|
5
|
+
type HashlineToken,
|
|
6
|
+
HashlineTokenizer,
|
|
7
|
+
isDeleteOpWithPayload,
|
|
8
|
+
type ParsedRange,
|
|
9
|
+
} from "./tokenizer";
|
|
10
|
+
import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
|
|
11
|
+
|
|
12
|
+
function validateRangeOrder(range: ParsedRange, lineNum: number): void {
|
|
13
|
+
if (range.end.line < range.start.line) {
|
|
14
|
+
throw new Error(`line ${lineNum}: range ${range.start.line}-${range.end.line} ends before it starts.`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function expandRange(range: ParsedRange): Anchor[] {
|
|
19
|
+
const anchors: Anchor[] = [];
|
|
20
|
+
for (let line = range.start.line; line <= range.end.line; line++) {
|
|
21
|
+
anchors.push({ line });
|
|
22
|
+
}
|
|
23
|
+
return anchors;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type PendingOp =
|
|
27
|
+
| { kind: "insert"; cursor: HashlineCursor; lineNum: number }
|
|
28
|
+
| { kind: "replace"; range: ParsedRange; lineNum: number };
|
|
29
|
+
|
|
30
|
+
interface Pending {
|
|
31
|
+
op: PendingOp;
|
|
32
|
+
payload: string[];
|
|
33
|
+
pendingBlanks: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Token-driven state machine that turns a stream of {@link HashlineToken}s
|
|
38
|
+
* into the flat list of {@link HashlineEdit}s applied downstream by the
|
|
39
|
+
* apply/diff layers.
|
|
40
|
+
*
|
|
41
|
+
* The executor owns:
|
|
42
|
+
* - the running edit index (kept monotonic across pending flushes),
|
|
43
|
+
* - the pending-payload buffer (lines accumulated for the most recently
|
|
44
|
+
* opened insert/replace op),
|
|
45
|
+
* - all parse-time diagnostics (range order, "delete with payload",
|
|
46
|
+
* orphan payload, unrecognized op),
|
|
47
|
+
* - the {@link terminated} flag set by `envelope-end`/`abort`.
|
|
48
|
+
*
|
|
49
|
+
* Tokens are dispatched in the order they arrive; the matching tokenizer
|
|
50
|
+
* supplies the line numbers carried inside each token so diagnostics line
|
|
51
|
+
* up with the source.
|
|
52
|
+
*/
|
|
53
|
+
export class HashlineExecutor {
|
|
54
|
+
#edits: HashlineEdit[] = [];
|
|
55
|
+
#warnings: string[] = [];
|
|
56
|
+
#editIndex = 0;
|
|
57
|
+
#pending: Pending | undefined;
|
|
58
|
+
#terminated = false;
|
|
59
|
+
|
|
60
|
+
/** True once an `envelope-end` or `abort` token has been observed. */
|
|
61
|
+
get terminated(): boolean {
|
|
62
|
+
return this.#terminated;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Consume one token. After `terminated` flips true subsequent feeds
|
|
67
|
+
* are silently ignored so callers can keep draining their tokenizer
|
|
68
|
+
* without explicit early-exit guards.
|
|
69
|
+
*/
|
|
70
|
+
feed(token: HashlineToken): void {
|
|
71
|
+
if (this.#terminated) return;
|
|
72
|
+
|
|
73
|
+
switch (token.kind) {
|
|
74
|
+
case "envelope-begin":
|
|
75
|
+
return;
|
|
76
|
+
case "envelope-end":
|
|
77
|
+
this.#terminated = true;
|
|
78
|
+
return;
|
|
79
|
+
case "abort":
|
|
80
|
+
this.#warnings.push(ABORT_WARNING);
|
|
81
|
+
this.#terminated = true;
|
|
82
|
+
return;
|
|
83
|
+
case "header":
|
|
84
|
+
this.#flushPending(false);
|
|
85
|
+
return;
|
|
86
|
+
case "blank":
|
|
87
|
+
if (this.#pending) this.#pending.pendingBlanks++;
|
|
88
|
+
return;
|
|
89
|
+
case "payload":
|
|
90
|
+
this.#handlePayload(token.text, token.lineNum);
|
|
91
|
+
return;
|
|
92
|
+
case "op-delete":
|
|
93
|
+
this.#flushPending(false);
|
|
94
|
+
if (token.trailingPayload) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`line ${token.lineNum}: ${HL_OP_DELETE} deletes only. Payload is forbidden after ${HL_OP_DELETE}; use ${HL_OP_REPLACE} to replace.`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
validateRangeOrder(token.range, token.lineNum);
|
|
100
|
+
for (const anchor of expandRange(token.range)) {
|
|
101
|
+
this.#edits.push({ kind: "delete", anchor, lineNum: token.lineNum, index: this.#editIndex++ });
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
case "op-insert":
|
|
105
|
+
this.#flushPending(false);
|
|
106
|
+
this.#pending = {
|
|
107
|
+
op: { kind: "insert", cursor: token.cursor, lineNum: token.lineNum },
|
|
108
|
+
payload: [token.inlineBody ?? ""],
|
|
109
|
+
pendingBlanks: 0,
|
|
110
|
+
};
|
|
111
|
+
return;
|
|
112
|
+
case "op-replace":
|
|
113
|
+
this.#flushPending(false);
|
|
114
|
+
validateRangeOrder(token.range, token.lineNum);
|
|
115
|
+
this.#pending = {
|
|
116
|
+
op: { kind: "replace", range: token.range, lineNum: token.lineNum },
|
|
117
|
+
payload: [token.inlineBody ?? ""],
|
|
118
|
+
pendingBlanks: 0,
|
|
119
|
+
};
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Flush any open pending op (including its trailing blank lines, which
|
|
126
|
+
* are payload-significant) and return the accumulated edits and
|
|
127
|
+
* warnings. The executor is single-use; reset() is required for reuse.
|
|
128
|
+
*/
|
|
129
|
+
end(): { edits: HashlineEdit[]; warnings: string[] } {
|
|
130
|
+
this.#flushPending(true);
|
|
131
|
+
return { edits: this.#edits, warnings: this.#warnings };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Reset to a fresh state so the same instance can drive another parse. */
|
|
135
|
+
reset(): void {
|
|
136
|
+
this.#edits = [];
|
|
137
|
+
this.#warnings = [];
|
|
138
|
+
this.#editIndex = 0;
|
|
139
|
+
this.#pending = undefined;
|
|
140
|
+
this.#terminated = false;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
#handlePayload(text: string, lineNum: number): void {
|
|
144
|
+
if (this.#pending) {
|
|
145
|
+
this.#flushPendingBlanks();
|
|
146
|
+
this.#pending.payload.push(text);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Whitespace-only payload outside any pending op is a visual
|
|
151
|
+
// separator (matches the legacy outer-loop isBlankLine skip);
|
|
152
|
+
// only fully-empty lines arrive as `blank` tokens.
|
|
153
|
+
if (text.trim().length === 0) return;
|
|
154
|
+
// Orphan payload outside any pending op: pick the most specific
|
|
155
|
+
// diagnostic so the model sees the actionable hint.
|
|
156
|
+
if (isDeleteOpWithPayload(text)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`line ${lineNum}: ${HL_OP_DELETE} deletes only. Payload is forbidden after ${HL_OP_DELETE}; use ${HL_OP_REPLACE} to replace.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const firstChar = text[0];
|
|
163
|
+
const startsWithOp = firstChar !== undefined && HL_OP_CHARS.includes(firstChar);
|
|
164
|
+
if (startsWithOp || firstChar === "-" || firstChar === "@" || firstChar === "«" || firstChar === "»") {
|
|
165
|
+
throw new Error(
|
|
166
|
+
`line ${lineNum}: unrecognized op. Use LINE${HL_OP_INSERT_BEFORE} (insert before), LINE${HL_OP_INSERT_AFTER} (insert after), LINE${HL_OP_REPLACE} / A-B${HL_OP_REPLACE} (replace), or LINE${HL_OP_DELETE} / A-B${HL_OP_DELETE} (delete). ` +
|
|
167
|
+
`Got ${JSON.stringify(text)}.`,
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
throw new Error(
|
|
172
|
+
`line ${lineNum}: payload line has no preceding ${HL_OP_INSERT_BEFORE}, ${HL_OP_INSERT_AFTER}, ${HL_OP_REPLACE}, or ${HL_OP_DELETE} operation. ` +
|
|
173
|
+
`Got ${JSON.stringify(text)}.`,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#flushPendingBlanks(): void {
|
|
178
|
+
if (!this.#pending) return;
|
|
179
|
+
for (let count = 0; count < this.#pending.pendingBlanks; count++) this.#pending.payload.push("");
|
|
180
|
+
this.#pending.pendingBlanks = 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
#flushPending(includeTrailingBlanks: boolean): void {
|
|
184
|
+
const pending = this.#pending;
|
|
185
|
+
if (!pending) return;
|
|
186
|
+
if (includeTrailingBlanks) this.#flushPendingBlanks();
|
|
187
|
+
|
|
188
|
+
const { op, payload } = pending;
|
|
189
|
+
const linesToInsert = payload;
|
|
190
|
+
|
|
191
|
+
if (op.kind === "insert") {
|
|
192
|
+
for (const text of linesToInsert) {
|
|
193
|
+
this.#edits.push({
|
|
194
|
+
kind: "insert",
|
|
195
|
+
cursor: cloneCursor(op.cursor),
|
|
196
|
+
text,
|
|
197
|
+
lineNum: op.lineNum,
|
|
198
|
+
index: this.#editIndex++,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
} else {
|
|
202
|
+
for (const text of linesToInsert) {
|
|
203
|
+
this.#edits.push({
|
|
204
|
+
kind: "insert",
|
|
205
|
+
cursor: { kind: "before_anchor", anchor: { ...op.range.start } },
|
|
206
|
+
text,
|
|
207
|
+
lineNum: op.lineNum,
|
|
208
|
+
index: this.#editIndex++,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
for (const anchor of expandRange(op.range)) {
|
|
212
|
+
this.#edits.push({ kind: "delete", anchor, lineNum: op.lineNum, index: this.#editIndex++ });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
this.#pending = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Drive a full hashline diff through the tokenizer + executor pipeline and
|
|
222
|
+
* return the resulting edits plus any parse-time warnings. This is the
|
|
223
|
+
* convenience entry point most callers want; reach for {@link
|
|
224
|
+
* HashlineTokenizer}/{@link HashlineExecutor} directly only when you need
|
|
225
|
+
* streaming feeds, cross-section state, or custom token handling.
|
|
226
|
+
*/
|
|
227
|
+
export function parseHashline(diff: string): { edits: HashlineEdit[]; warnings: string[] } {
|
|
228
|
+
const tokenizer = new HashlineTokenizer();
|
|
229
|
+
const executor = new HashlineExecutor();
|
|
230
|
+
const drain = (tokens: HashlineToken[]): void => {
|
|
231
|
+
for (const token of tokens) {
|
|
232
|
+
if (executor.terminated) return;
|
|
233
|
+
executor.feed(token);
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
drain(tokenizer.feed(diff));
|
|
237
|
+
drain(tokenizer.end());
|
|
238
|
+
return executor.end();
|
|
239
|
+
}
|
|
@@ -3,19 +3,21 @@ begin_patch: "*** Begin Patch" LF
|
|
|
3
3
|
end_patch: "*** End Patch" LF?
|
|
4
4
|
|
|
5
5
|
hunk: update_hunk
|
|
6
|
-
update_hunk: "$HFILE$" filename LF line_op*
|
|
6
|
+
update_hunk: "$HFILE$" filename ("#" file_hash)? LF line_op*
|
|
7
7
|
|
|
8
|
-
filename: /(
|
|
8
|
+
filename: /([^\s#]+)/
|
|
9
|
+
file_hash: /[0-9a-f]{4}/
|
|
9
10
|
|
|
10
|
-
line_op: insert_before | insert_after | replace |
|
|
11
|
-
insert_before: "$HOP_INSERT_BEFORE$"
|
|
12
|
-
insert_after: "$HOP_INSERT_AFTER$"
|
|
13
|
-
replace: "$HOP_REPLACE$"
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
line_op: insert_before | insert_after | replace | delete
|
|
12
|
+
insert_before: anchor "$HOP_INSERT_BEFORE$" inline_body? LF payload*
|
|
13
|
+
insert_after: anchor "$HOP_INSERT_AFTER$" inline_body? LF payload*
|
|
14
|
+
replace: range "$HOP_REPLACE$" inline_body? LF payload*
|
|
15
|
+
delete: range "$HOP_DELETE$" LF
|
|
16
|
+
inline_body: /[^\n]+/
|
|
17
|
+
payload: /(.*)/ LF
|
|
16
18
|
|
|
17
19
|
anchor: LID | "EOF" | "BOF"
|
|
18
|
-
range: LID ("
|
|
19
|
-
LID: /[1-9]\d
|
|
20
|
+
range: LID ("-" LID)?
|
|
21
|
+
LID: /[1-9]\d*/
|
|
20
22
|
|
|
21
23
|
%import common.LF
|
package/src/hashline/hash.ts
CHANGED
|
@@ -3,70 +3,54 @@
|
|
|
3
3
|
* and prompt helpers.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 647 single-token BPE bigrams for hashline anchors. Every entry tokenizes as
|
|
10
|
-
* exactly one token in modern BPE vocabularies (cl100k / o200k / Claude family),
|
|
11
|
-
* so a hashline anchor built from one bigram is exactly 1 token.
|
|
12
|
-
*
|
|
13
|
-
* This is the complete set of 2-letter lowercase combinations that are single
|
|
14
|
-
* tokens — the 29 missing combinations are rare-letter pairs (q/x/z heavy)
|
|
15
|
-
* that no major BPE vocabulary merges into a single token.
|
|
16
|
-
*
|
|
17
|
-
* Order is stable forever — changing it would invalidate every saved
|
|
18
|
-
* `LINE+ID` reference in transcripts and prompts.
|
|
19
|
-
*/
|
|
20
|
-
export const HL_BIGRAMS: readonly string[] = bigrams;
|
|
21
|
-
|
|
22
|
-
export const HL_BIGRAMS_COUNT = HL_BIGRAMS.length;
|
|
6
|
+
const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
23
7
|
|
|
24
8
|
/**
|
|
25
|
-
* Decoration prefix that may precede a
|
|
9
|
+
* Decoration prefix that may precede a line number in tool output:
|
|
26
10
|
* `>` (context line in grep), `+` (added line in diff), `-` (removed line),
|
|
27
11
|
* `*` (match line). Any combination, in any order, surrounded by optional
|
|
28
|
-
* whitespace. Output formatters emit at most one decoration per
|
|
29
|
-
*
|
|
30
|
-
* echoes back.
|
|
12
|
+
* whitespace. Output formatters emit at most one decoration per line; the
|
|
13
|
+
* parser stays liberal because it accepts whatever the model echoes back.
|
|
31
14
|
*/
|
|
32
15
|
export const HL_ANCHOR_DECORATION_RE_RAW = `\\s*[>+\\-*]*\\s*`;
|
|
33
16
|
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
* captures the line number (digits only); group 2 captures the hash. The
|
|
37
|
-
* source is intentionally unanchored — anchoring with `^` (or composing into a
|
|
38
|
-
* larger pattern) is the caller's responsibility.
|
|
39
|
-
*/
|
|
40
|
-
export const HL_ANCHOR_RE_RAW = `${HL_ANCHOR_DECORATION_RE_RAW}(\\d+)([a-z]{2})`;
|
|
17
|
+
/** Capture-group regex source for a decorated bare line-number anchor. */
|
|
18
|
+
export const HL_ANCHOR_RE_RAW = `${HL_ANCHOR_DECORATION_RE_RAW}(\\d+)`;
|
|
41
19
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
44
|
-
* embedding inside larger patterns where the line+hash unit appears as a
|
|
45
|
-
* literal (e.g. range bounds, alternation arms, op-line heuristics).
|
|
46
|
-
*/
|
|
47
|
-
export const HL_HASH_RE_RAW = `[1-9]\\d*[a-z]{2}`;
|
|
20
|
+
/** Bare positive line-number Lid (no decorations, no captures, no anchors). */
|
|
21
|
+
export const HL_LINE_RE_RAW = `[1-9]\\d*`;
|
|
48
22
|
|
|
49
|
-
/**
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
*/
|
|
53
|
-
export const
|
|
23
|
+
/** Capture-group form of {@link HL_LINE_RE_RAW}. */
|
|
24
|
+
export const HL_LINE_CAPTURE_RE_RAW = `([1-9]\\d*)`;
|
|
25
|
+
|
|
26
|
+
/** Four-hex-character file hash carried by a hashline section header. */
|
|
27
|
+
export const HL_FILE_HASH_RE_RAW = `[0-9a-f]{4}`;
|
|
28
|
+
|
|
29
|
+
/** Capture-group form of {@link HL_FILE_HASH_RE_RAW}. */
|
|
30
|
+
export const HL_FILE_HASH_CAPTURE_RE_RAW = `(${HL_FILE_HASH_RE_RAW})`;
|
|
31
|
+
|
|
32
|
+
/** Separator between a hashline file path and its file hash. */
|
|
33
|
+
export const HL_FILE_HASH_SEP = "#";
|
|
34
|
+
|
|
35
|
+
/** Separator between a line number and displayed line content in hashline mode. */
|
|
36
|
+
export const HL_LINE_BODY_SEP = ":";
|
|
54
37
|
|
|
55
|
-
/**
|
|
56
|
-
export const
|
|
38
|
+
/** Regex-escaped form of {@link HL_LINE_BODY_SEP}, safe for embedding inside a regex. */
|
|
39
|
+
export const HL_LINE_BODY_SEP_RE_RAW = regexEscape(HL_LINE_BODY_SEP);
|
|
57
40
|
|
|
58
41
|
/**
|
|
59
|
-
* Representative
|
|
60
|
-
*
|
|
42
|
+
* Representative file hashes for use in user-facing error messages and prompt
|
|
43
|
+
* examples.
|
|
61
44
|
*/
|
|
62
|
-
export const
|
|
45
|
+
export const HL_FILE_HASH_EXAMPLES = ["1a2b", "3c4d", "9f3e"] as const;
|
|
63
46
|
|
|
64
47
|
/**
|
|
65
48
|
* Format a comma-separated list of example anchors with an optional line-number
|
|
66
|
-
* prefix, quoted for inclusion in error messages: `"
|
|
49
|
+
* prefix, quoted for inclusion in error messages: `"160", "42", "7"`.
|
|
67
50
|
*/
|
|
68
51
|
export function describeAnchorExamples(linePrefix = ""): string {
|
|
69
|
-
|
|
52
|
+
const examples = linePrefix ? [linePrefix, `${linePrefix.slice(0, -1) || "4"}2`, "7"] : ["160", "42", "7"];
|
|
53
|
+
return examples.map(e => `"${e}"`).join(", ");
|
|
70
54
|
}
|
|
71
55
|
|
|
72
56
|
/**
|
|
@@ -76,98 +60,69 @@ export function describeAnchorExamples(linePrefix = ""): string {
|
|
|
76
60
|
*/
|
|
77
61
|
export function resolveHashlineGrammarPlaceholders(grammar: string): string {
|
|
78
62
|
return grammar
|
|
79
|
-
.replaceAll("$HFMT$", "
|
|
63
|
+
.replaceAll("$HFMT$", "")
|
|
64
|
+
.replaceAll("$HFILE_HASH$", HL_FILE_HASH_RE_RAW)
|
|
65
|
+
.replaceAll("$HFILE_HASH_SEP$", HL_FILE_HASH_SEP)
|
|
80
66
|
.replaceAll("$HOP_INSERT_BEFORE$", HL_OP_INSERT_BEFORE)
|
|
81
67
|
.replaceAll("$HOP_INSERT_AFTER$", HL_OP_INSERT_AFTER)
|
|
82
68
|
.replaceAll("$HOP_REPLACE$", HL_OP_REPLACE)
|
|
69
|
+
.replaceAll("$HOP_DELETE$", HL_OP_DELETE)
|
|
83
70
|
.replaceAll("$HOP_CHARS$", HL_OP_CHARS)
|
|
84
71
|
.replaceAll("$HFILE$", HL_FILE_PREFIX);
|
|
85
72
|
}
|
|
86
73
|
|
|
87
|
-
/** @deprecated Use {@link resolveHashlineGrammarPlaceholders}. */
|
|
88
|
-
export const resolveLarkLidPlaceholders = resolveHashlineGrammarPlaceholders;
|
|
89
|
-
|
|
90
|
-
const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
91
|
-
|
|
92
74
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
* verbatim file content
|
|
75
|
+
* op lines have an `ANCHOR<SIGIL>[INLINE_PAYLOAD]` shape, where SIGIL is one of
|
|
76
|
+
* {@link HL_OP_INSERT_BEFORE}, {@link HL_OP_INSERT_AFTER}, {@link HL_OP_REPLACE},
|
|
77
|
+
* or {@link HL_OP_DELETE}.
|
|
78
|
+
* Multi-line payloads follow on subsequent lines as verbatim file content with no
|
|
79
|
+
* per-line marker.
|
|
97
80
|
*
|
|
98
81
|
* These constants are the single source of truth for the edit parser, grammar,
|
|
99
82
|
* renderer, and prompt.
|
|
100
83
|
*/
|
|
101
|
-
export const HL_OP_INSERT_BEFORE = "
|
|
102
|
-
export const HL_OP_INSERT_AFTER = "
|
|
103
|
-
export const HL_OP_REPLACE = "
|
|
84
|
+
export const HL_OP_INSERT_BEFORE = "↑";
|
|
85
|
+
export const HL_OP_INSERT_AFTER = "↓";
|
|
86
|
+
export const HL_OP_REPLACE = ":";
|
|
87
|
+
export const HL_OP_DELETE = "!";
|
|
104
88
|
|
|
105
89
|
/** All hashline edit op sigils, concatenated for fast membership tests. */
|
|
106
|
-
export const HL_OP_CHARS = `${HL_OP_INSERT_BEFORE}${HL_OP_INSERT_AFTER}${HL_OP_REPLACE}`;
|
|
90
|
+
export const HL_OP_CHARS = `${HL_OP_INSERT_BEFORE}${HL_OP_INSERT_AFTER}${HL_OP_REPLACE}${HL_OP_DELETE}`;
|
|
107
91
|
|
|
108
92
|
/** Hashline edit file section header marker. */
|
|
109
|
-
export const HL_FILE_PREFIX = "
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
93
|
+
export const HL_FILE_PREFIX = "¶";
|
|
94
|
+
|
|
95
|
+
function normalizeFileHashText(text: string): string {
|
|
96
|
+
return text
|
|
97
|
+
.replace(/\r/g, "")
|
|
98
|
+
.split("\n")
|
|
99
|
+
.map(line => line.trimEnd())
|
|
100
|
+
.join("\n");
|
|
101
|
+
}
|
|
116
102
|
|
|
117
103
|
/**
|
|
118
|
-
* Compute
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* for call-site symmetry with line numbers but is intentionally unused so
|
|
122
|
-
* that anchors remain stable across line shifts caused by sibling edits.
|
|
123
|
-
*
|
|
124
|
-
* The line input should not include a trailing newline.
|
|
104
|
+
* Compute the 4-hex-character hash carried by a hashline section header.
|
|
105
|
+
* The hash normalizes CR characters and trailing whitespace before hashing so
|
|
106
|
+
* platform line endings and display-trimmed lines do not invalidate anchors.
|
|
125
107
|
*/
|
|
126
|
-
export function
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// in `idx` for blank/punctuation-only lines, but that meant any line shift
|
|
131
|
-
// (e.g. from a sibling edit in the same batch) invalidated anchors whose
|
|
132
|
-
// content had not changed. Identical blank lines are intentionally allowed
|
|
133
|
-
// to collide — the edit op's line number disambiguates them.
|
|
134
|
-
return HL_BIGRAMS[Bun.hash.xxHash32(line, 0) % HL_BIGRAMS_COUNT];
|
|
108
|
+
export function computeFileHash(text: string): string {
|
|
109
|
+
const normalized = normalizeFileHashText(text);
|
|
110
|
+
const low16 = Bun.hash.xxHash32(normalized, 0) & 0xffff;
|
|
111
|
+
return low16.toString(16).padStart(4, "0");
|
|
135
112
|
}
|
|
136
113
|
|
|
137
|
-
/**
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
* number and hash.
|
|
141
|
-
*/
|
|
142
|
-
export function formatLineHash(line: number, lines: string): string {
|
|
143
|
-
return `${line}${computeLineHash(line, lines)}`;
|
|
114
|
+
/** Format a hashline section header for a file path and file hash. */
|
|
115
|
+
export function formatHashlineHeader(filePath: string, fileHash: string): string {
|
|
116
|
+
return `${HL_FILE_PREFIX}${filePath}${HL_FILE_HASH_SEP}${fileHash}`;
|
|
144
117
|
}
|
|
145
118
|
|
|
146
|
-
/**
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
*/
|
|
150
|
-
export function formatHashLine(lineNumber: number, line: string): string {
|
|
151
|
-
return `${lineNumber}${computeLineHash(lineNumber, line)}${HL_BODY_SEP}${line}`;
|
|
119
|
+
/** Formats a single numbered line as `LINE:TEXT`. */
|
|
120
|
+
export function formatNumberedLine(lineNumber: number, line: string): string {
|
|
121
|
+
return `${lineNumber}${HL_LINE_BODY_SEP}${line}`;
|
|
152
122
|
}
|
|
153
123
|
|
|
154
|
-
/**
|
|
155
|
-
|
|
156
|
-
*
|
|
157
|
-
* Each line becomes `LINE+ID|TEXT` where LINENUM is 1-indexed.
|
|
158
|
-
* No padding on line numbers; pipe separator between anchor and content.
|
|
159
|
-
*
|
|
160
|
-
* @param text - Raw file text string
|
|
161
|
-
* @param startLine - First line number (1-indexed, defaults to 1)
|
|
162
|
-
* @returns Formatted string with one hashline-prefixed line per input line
|
|
163
|
-
*
|
|
164
|
-
* @example
|
|
165
|
-
* ```
|
|
166
|
-
* formatHashLines("function hi() {\n return;\n}")
|
|
167
|
-
* // "1bm|function hi() {\n2er| return;\n3ab|}"
|
|
168
|
-
* ```
|
|
169
|
-
*/
|
|
170
|
-
export function formatHashLines(text: string, startLine = 1): string {
|
|
124
|
+
/** Format file text with hashline-mode line-number prefixes for display. */
|
|
125
|
+
export function formatNumberedLines(text: string, startLine = 1): string {
|
|
171
126
|
const lines = text.split("\n");
|
|
172
|
-
return lines.map((line, i) =>
|
|
127
|
+
return lines.map((line, i) => formatNumberedLine(startLine + i, line)).join("\n");
|
|
173
128
|
}
|
package/src/hashline/index.ts
CHANGED
|
@@ -4,10 +4,11 @@ export * from "./constants";
|
|
|
4
4
|
export * from "./diff";
|
|
5
5
|
export * from "./diff-preview";
|
|
6
6
|
export * from "./execute";
|
|
7
|
+
export * from "./executor";
|
|
7
8
|
export * from "./hash";
|
|
8
9
|
export * from "./input";
|
|
9
|
-
export * from "./parser";
|
|
10
10
|
export * from "./prefixes";
|
|
11
11
|
export * from "./recovery";
|
|
12
12
|
export * from "./stream";
|
|
13
|
+
export * from "./tokenizer";
|
|
13
14
|
export * from "./types";
|