@oh-my-pi/pi-coding-agent 15.2.3 → 15.2.4
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 +21 -0
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/hash.d.ts +13 -39
- package/dist/types/hashline/parser.d.ts +2 -6
- package/dist/types/modes/shared.d.ts +9 -0
- package/dist/types/modes/theme/shimmer.d.ts +6 -3
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +1 -8
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +5 -7
- package/src/edit/streaming.ts +24 -12
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff.ts +1 -1
- package/src/hashline/execute.ts +2 -2
- package/src/hashline/grammar.lark +7 -8
- package/src/hashline/hash.ts +21 -43
- package/src/hashline/input.ts +15 -13
- package/src/hashline/parser.ts +62 -161
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/controllers/event-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +51 -10
- package/src/modes/shared.ts +16 -0
- package/src/modes/theme/shimmer.ts +15 -6
- package/src/prompts/tools/hashline.md +62 -81
- package/src/utils/commit-message-generator.ts +6 -1
- package/src/utils/title-generator.ts +45 -13
package/src/hashline/parser.ts
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { ABORT_MARKER, ABORT_WARNING, BEGIN_PATCH_MARKER, END_PATCH_MARKER, RANGE_INTERIOR_HASH } from "./constants";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
describeAnchorExamples,
|
|
4
|
+
HL_FILE_PREFIX,
|
|
5
|
+
HL_HASH_CAPTURE_RE_RAW,
|
|
6
|
+
HL_OP_CHARS,
|
|
7
|
+
HL_OP_INSERT_AFTER,
|
|
8
|
+
HL_OP_INSERT_BEFORE,
|
|
9
|
+
HL_OP_REPLACE,
|
|
10
|
+
} from "./hash";
|
|
3
11
|
import type { Anchor, HashlineCursor, HashlineEdit } from "./types";
|
|
4
12
|
|
|
5
13
|
const LID_CAPTURE_RE = new RegExp(`^${HL_HASH_CAPTURE_RE_RAW}$`);
|
|
14
|
+
const regexEscape = (str: string): string => str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6
15
|
|
|
7
16
|
function parseLid(raw: string, lineNum: number): Anchor {
|
|
8
17
|
const match = LID_CAPTURE_RE.exec(raw);
|
|
@@ -22,12 +31,8 @@ interface ParsedRange {
|
|
|
22
31
|
|
|
23
32
|
function parseRange(raw: string, lineNum: number): ParsedRange {
|
|
24
33
|
if (!raw.includes("..")) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
`Repeat the same anchor on both sides for a one-line edit (for example, ` +
|
|
28
|
-
`${describeAnchorExamples("119")}..${describeAnchorExamples("119")}); ` +
|
|
29
|
-
`got ${JSON.stringify(raw)}.`,
|
|
30
|
-
);
|
|
34
|
+
const start = parseLid(raw, lineNum);
|
|
35
|
+
return { start, end: { ...start } };
|
|
31
36
|
}
|
|
32
37
|
const [startRaw, endRaw, extra] = raw.split("..");
|
|
33
38
|
if (extra !== undefined || !startRaw || !endRaw) {
|
|
@@ -64,157 +69,61 @@ function parseInsertTarget(raw: string, lineNum: number, kind: "before" | "after
|
|
|
64
69
|
return { kind: cursorKind, anchor: parseLid(raw, lineNum) };
|
|
65
70
|
}
|
|
66
71
|
|
|
67
|
-
const INSERT_BEFORE_OP_RE =
|
|
68
|
-
const INSERT_AFTER_OP_RE =
|
|
69
|
-
const
|
|
70
|
-
|
|
72
|
+
const INSERT_BEFORE_OP_RE = new RegExp(`^${regexEscape(HL_OP_INSERT_BEFORE)}\\s*(\\S+)\\s*$`);
|
|
73
|
+
const INSERT_AFTER_OP_RE = new RegExp(`^${regexEscape(HL_OP_INSERT_AFTER)}\\s*(\\S+)\\s*$`);
|
|
74
|
+
const REPLACE_OP_RE = new RegExp(`^${regexEscape(HL_OP_REPLACE)}\\s*([^\\s+<\\-=]\\S*)\\s*$`);
|
|
75
|
+
|
|
76
|
+
function isEnvelopeOrAbortMarkerLine(line: string): boolean {
|
|
77
|
+
const trimmed = line.trimEnd();
|
|
78
|
+
return trimmed === BEGIN_PATCH_MARKER || trimmed === END_PATCH_MARKER || trimmed === ABORT_MARKER;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isPayloadTerminatorLine(line: string): boolean {
|
|
82
|
+
const first = line[0];
|
|
83
|
+
return (
|
|
84
|
+
first === HL_FILE_PREFIX ||
|
|
85
|
+
(first !== undefined && HL_OP_CHARS.includes(first)) ||
|
|
86
|
+
isEnvelopeOrAbortMarkerLine(line)
|
|
87
|
+
);
|
|
88
|
+
}
|
|
71
89
|
|
|
72
90
|
export function cloneCursor(cursor: HashlineCursor): HashlineCursor {
|
|
73
91
|
if (cursor.kind === "before_anchor") return { kind: "before_anchor", anchor: { ...cursor.anchor } };
|
|
74
92
|
if (cursor.kind === "after_anchor") return { kind: "after_anchor", anchor: { ...cursor.anchor } };
|
|
75
93
|
return cursor;
|
|
76
94
|
}
|
|
77
|
-
/**
|
|
78
|
-
* Returns true when every non-empty payload line looks like the `~ TEXT` readability-padding
|
|
79
|
-
* typo: exactly one leading space followed by a non-space character (or a bare single space).
|
|
80
|
-
*
|
|
81
|
-
* Indented file content (Python 4-space, YAML/JSON/Markdown 2-space, etc.) starts with two or
|
|
82
|
-
* more leading spaces, so this heuristic ignores legitimate indentation while still flagging
|
|
83
|
-
* the common `~ beta` mistake that silently corrupts file content with a stray space.
|
|
84
|
-
*/
|
|
85
|
-
function hasUniformSeparatorPadding(payload: string[]): boolean {
|
|
86
|
-
let any = false;
|
|
87
|
-
for (const text of payload) {
|
|
88
|
-
if (text.length === 0) continue;
|
|
89
|
-
if (text.charCodeAt(0) !== 0x20) return false;
|
|
90
|
-
// Two or more leading spaces is real indentation, not separator padding.
|
|
91
|
-
if (text.length > 1 && text.charCodeAt(1) === 0x20) return false;
|
|
92
|
-
any = true;
|
|
93
|
-
}
|
|
94
|
-
return any;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* File extensions where leading single-space indentation is plausible legitimate file content
|
|
99
|
-
* (off-side-rule languages, structured-indent data formats, prose with continuation indent).
|
|
100
|
-
* For these we suppress the separator-padding warning entirely — the heuristic's false-positive
|
|
101
|
-
* cost on a real edit outweighs the rare chance it catches a `~ TEXT` typo.
|
|
102
|
-
*/
|
|
103
|
-
const INDENT_SENSITIVE_EXTS: Record<string, true> = {
|
|
104
|
-
".py": true,
|
|
105
|
-
".pyi": true,
|
|
106
|
-
".pyx": true,
|
|
107
|
-
".pyw": true,
|
|
108
|
-
".yml": true,
|
|
109
|
-
".yaml": true,
|
|
110
|
-
".md": true,
|
|
111
|
-
".mdx": true,
|
|
112
|
-
".markdown": true,
|
|
113
|
-
".rst": true,
|
|
114
|
-
".adoc": true,
|
|
115
|
-
".asciidoc": true,
|
|
116
|
-
".toml": true,
|
|
117
|
-
".json": true,
|
|
118
|
-
".jsonc": true,
|
|
119
|
-
".json5": true,
|
|
120
|
-
".ndjson": true,
|
|
121
|
-
".jsonl": true,
|
|
122
|
-
".tf": true,
|
|
123
|
-
".tfvars": true,
|
|
124
|
-
".hcl": true,
|
|
125
|
-
".nix": true,
|
|
126
|
-
".coffee": true,
|
|
127
|
-
".litcoffee": true,
|
|
128
|
-
".haml": true,
|
|
129
|
-
".slim": true,
|
|
130
|
-
".pug": true,
|
|
131
|
-
".jade": true,
|
|
132
|
-
".sass": true,
|
|
133
|
-
".styl": true,
|
|
134
|
-
".nim": true,
|
|
135
|
-
".cr": true,
|
|
136
|
-
".elm": true,
|
|
137
|
-
".fs": true,
|
|
138
|
-
".fsi": true,
|
|
139
|
-
".fsx": true,
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
function isIndentationSensitivePath(path: string | undefined): boolean {
|
|
143
|
-
if (!path) return false;
|
|
144
|
-
const slash = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
|
|
145
|
-
const dot = path.lastIndexOf(".");
|
|
146
|
-
if (dot <= slash) return false;
|
|
147
|
-
const ext = path.slice(dot).toLowerCase();
|
|
148
|
-
return INDENT_SENSITIVE_EXTS[ext] === true;
|
|
149
|
-
}
|
|
150
95
|
|
|
151
96
|
function collectPayload(
|
|
152
97
|
lines: string[],
|
|
153
98
|
startIndex: number,
|
|
154
99
|
opLineNum: number,
|
|
155
100
|
requirePayload: boolean,
|
|
156
|
-
|
|
157
|
-
): { payload: string[]; nextIndex: number; paddingWarning?: string } {
|
|
101
|
+
): { payload: string[]; nextIndex: number } {
|
|
158
102
|
const payload: string[] = [];
|
|
159
103
|
let index = startIndex;
|
|
160
104
|
while (index < lines.length) {
|
|
161
105
|
const line = lines[index];
|
|
162
|
-
if (line
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
continue;
|
|
166
|
-
}
|
|
167
|
-
// Silently recover from a missing payload prefix on an otherwise blank
|
|
168
|
-
// line: if more payload follows (possibly past further blanks), treat
|
|
169
|
-
// each intervening blank as an empty `${HL_EDIT_SEP}` payload line.
|
|
170
|
-
// Additionally, when the op explicitly requires payload (`+`/`<`) and
|
|
171
|
-
// we have not collected any yet, accept the blank(s) themselves as the
|
|
172
|
-
// empty payload — common typo of forgetting the `${HL_EDIT_SEP}` prefix
|
|
173
|
-
// when inserting a blank line.
|
|
174
|
-
if (line.length === 0) {
|
|
175
|
-
let lookahead = index + 1;
|
|
176
|
-
while (lookahead < lines.length && lines[lookahead].length === 0) {
|
|
177
|
-
lookahead++;
|
|
178
|
-
}
|
|
179
|
-
const followedByPayload = lookahead < lines.length && lines[lookahead].startsWith(HL_EDIT_SEP);
|
|
180
|
-
const acceptBareBlank = requirePayload && payload.length === 0;
|
|
181
|
-
if (followedByPayload || acceptBareBlank) {
|
|
182
|
-
for (let j = index; j < lookahead; j++) payload.push("");
|
|
183
|
-
index = lookahead;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
106
|
+
if (isPayloadTerminatorLine(line)) break;
|
|
107
|
+
payload.push(line);
|
|
108
|
+
index++;
|
|
188
109
|
}
|
|
189
110
|
if (payload.length === 0 && requirePayload) {
|
|
190
|
-
throw new Error(
|
|
111
|
+
throw new Error(
|
|
112
|
+
`line ${opLineNum}: ${HL_OP_INSERT_BEFORE} and ${HL_OP_INSERT_AFTER} operations require at least one verbatim payload line.`,
|
|
113
|
+
);
|
|
191
114
|
}
|
|
192
|
-
|
|
193
|
-
checkPadding && hasUniformSeparatorPadding(payload)
|
|
194
|
-
? `line ${opLineNum}: every payload line begins with exactly one space before non-space content, ` +
|
|
195
|
-
`which looks like a readability gap after "${HL_EDIT_SEP}". The space becomes file content. ` +
|
|
196
|
-
`Drop it unless the file genuinely uses a one-space indent.`
|
|
197
|
-
: undefined;
|
|
198
|
-
return { payload, nextIndex: index, paddingWarning };
|
|
115
|
+
return { payload, nextIndex: index };
|
|
199
116
|
}
|
|
200
117
|
|
|
201
|
-
export function parseHashline(diff: string
|
|
202
|
-
return parseHashlineWithWarnings(diff
|
|
118
|
+
export function parseHashline(diff: string): HashlineEdit[] {
|
|
119
|
+
return parseHashlineWithWarnings(diff).edits;
|
|
203
120
|
}
|
|
204
121
|
|
|
205
|
-
export
|
|
206
|
-
/** File path the diff targets. Used to suppress indent-sensitive false-positive warnings. */
|
|
207
|
-
path?: string;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function parseHashlineWithWarnings(
|
|
211
|
-
diff: string,
|
|
212
|
-
opts: ParseHashlineOptions = {},
|
|
213
|
-
): { edits: HashlineEdit[]; warnings: string[] } {
|
|
122
|
+
export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]; warnings: string[] } {
|
|
214
123
|
const edits: HashlineEdit[] = [];
|
|
215
124
|
const warnings: string[] = [];
|
|
216
125
|
const lines = diff.split(/\r?\n/);
|
|
217
|
-
|
|
126
|
+
if (diff.endsWith("\n") && lines.at(-1) === "") lines.pop();
|
|
218
127
|
let editIndex = 0;
|
|
219
128
|
|
|
220
129
|
const pushInsert = (cursor: HashlineCursor, text: string, lineNum: number) => {
|
|
@@ -240,15 +149,11 @@ export function parseHashlineWithWarnings(
|
|
|
240
149
|
i++;
|
|
241
150
|
continue;
|
|
242
151
|
}
|
|
243
|
-
if (line.startsWith(HL_EDIT_SEP)) {
|
|
244
|
-
throw new Error(`line ${lineNum}: payload line has no preceding +, <, or = operation.`);
|
|
245
|
-
}
|
|
246
152
|
|
|
247
153
|
const insertBeforeMatch = INSERT_BEFORE_OP_RE.exec(line);
|
|
248
154
|
if (insertBeforeMatch) {
|
|
249
155
|
const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before");
|
|
250
|
-
const { payload, nextIndex
|
|
251
|
-
if (paddingWarning) warnings.push(paddingWarning);
|
|
156
|
+
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, true);
|
|
252
157
|
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
253
158
|
i = nextIndex;
|
|
254
159
|
continue;
|
|
@@ -257,37 +162,26 @@ export function parseHashlineWithWarnings(
|
|
|
257
162
|
const insertAfterMatch = INSERT_AFTER_OP_RE.exec(line);
|
|
258
163
|
if (insertAfterMatch) {
|
|
259
164
|
const cursor = parseInsertTarget(insertAfterMatch[1], lineNum, "after");
|
|
260
|
-
const { payload, nextIndex
|
|
261
|
-
if (paddingWarning) warnings.push(paddingWarning);
|
|
165
|
+
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, true);
|
|
262
166
|
for (const text of payload) pushInsert(cursor, text, lineNum);
|
|
263
167
|
i = nextIndex;
|
|
264
168
|
continue;
|
|
265
169
|
}
|
|
266
170
|
|
|
267
|
-
const deleteMatch = DELETE_OP_RE.exec(line);
|
|
268
|
-
if (deleteMatch) {
|
|
269
|
-
for (const anchor of expandRange(parseRange(deleteMatch[1], lineNum))) {
|
|
270
|
-
edits.push({ kind: "delete", anchor, lineNum, index: editIndex++ });
|
|
271
|
-
}
|
|
272
|
-
i++;
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
171
|
const replaceMatch = REPLACE_OP_RE.exec(line);
|
|
277
172
|
if (replaceMatch) {
|
|
278
173
|
const range = parseRange(replaceMatch[1], lineNum);
|
|
279
|
-
const { payload, nextIndex
|
|
280
|
-
if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
});
|
|
174
|
+
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false);
|
|
175
|
+
if (payload.length > 0) {
|
|
176
|
+
for (const text of payload) {
|
|
177
|
+
edits.push({
|
|
178
|
+
kind: "insert",
|
|
179
|
+
cursor: { kind: "before_anchor", anchor: { ...range.start } },
|
|
180
|
+
text,
|
|
181
|
+
lineNum,
|
|
182
|
+
index: editIndex++,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
291
185
|
}
|
|
292
186
|
for (const anchor of expandRange(range)) {
|
|
293
187
|
edits.push({ kind: "delete", anchor, lineNum, index: editIndex++ });
|
|
@@ -296,8 +190,15 @@ export function parseHashlineWithWarnings(
|
|
|
296
190
|
continue;
|
|
297
191
|
}
|
|
298
192
|
|
|
193
|
+
if (isPayloadTerminatorLine(line) || /^[-@\u00B6]/u.test(line)) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`line ${lineNum}: unrecognized op. Use ${HL_OP_INSERT_BEFORE}ANCHOR (insert before), ${HL_OP_INSERT_AFTER}ANCHOR (insert after), or ${HL_OP_REPLACE}A..B (replace/delete). ` +
|
|
196
|
+
`Got ${JSON.stringify(line)}.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
299
200
|
throw new Error(
|
|
300
|
-
`line ${lineNum}:
|
|
201
|
+
`line ${lineNum}: payload line has no preceding ${HL_OP_INSERT_BEFORE}, ${HL_OP_INSERT_AFTER}, or ${HL_OP_REPLACE} operation. ` +
|
|
301
202
|
`Got ${JSON.stringify(line)}.`,
|
|
302
203
|
);
|
|
303
204
|
}
|