fix(edit): warned on hashline separator padding

Tightened the hashline prompt to forbid readability spaces after the payload separator.

Preserved parsed payload text verbatim and surfaced a warning when a payload block looks like separator padding was added across every line.

Fixes #1166
This commit is contained in:
roboomp
2026-05-18 21:42:35 +00:00
parent 12e92262e8
commit 48253d36ef
3 changed files with 29 additions and 10 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
## [Unreleased]
### Fixed
- Fixed hashline edit payloads that use a readability space after `~` by warning on separator-padding-shaped payload blocks and tightening the model prompt. ([#1166](https://github.com/can1357/oh-my-pi/issues/1166))
- Fixed ACP command and custom tool-call notifications to carry the original tool arguments in replayed and final updates, so command text is preserved and raw input is no longer wrapped
- Fixed ACP async-job draining to be scoped by session owner so `getAsyncJobSnapshot` and `drainAsyncJobDeliveriesForAcp` no longer consume or expose jobs from other sessions
- Fixed async job status reporting to include in-flight completions so queued/delivering indicators remain accurate while callbacks are still running
+23 -6
View File
@@ -74,19 +74,29 @@ export function cloneCursor(cursor: HashlineCursor): HashlineCursor {
if (cursor.kind === "after_anchor") return { kind: "after_anchor", anchor: { ...cursor.anchor } };
return cursor;
}
/** Returns true when every non-empty payload line starts with `${sep} ` (sep + one space). */
function hasUniformSeparatorPadding(payload: string[]): boolean {
let any = false;
for (const text of payload) {
if (text.length === 0) continue;
if (!text.startsWith(" ")) return false;
any = true;
}
return any;
}
function collectPayload(
lines: string[],
startIndex: number,
opLineNum: number,
requirePayload: boolean,
): { payload: string[]; nextIndex: number } {
): { payload: string[]; nextIndex: number; paddingWarning?: string } {
const payload: string[] = [];
let index = startIndex;
while (index < lines.length) {
const line = lines[index];
if (line.startsWith(HL_EDIT_SEP)) {
payload.push(line.slice(1).trimEnd());
payload.push(line.slice(HL_EDIT_SEP.length).trimEnd());
index++;
continue;
}
@@ -115,7 +125,11 @@ function collectPayload(
if (payload.length === 0 && requirePayload) {
throw new Error(`line ${opLineNum}: + and < operations require at least one ${HL_EDIT_SEP}TEXT payload line.`);
}
return { payload, nextIndex: index };
const paddingWarning = hasUniformSeparatorPadding(payload)
? `line ${opLineNum}: all payload lines start with "${HL_EDIT_SEP} " (separator + space). ` +
`The space becomes file content. Remove it unless the target file requires leading spaces.`
: undefined;
return { payload, nextIndex: index, paddingWarning };
}
export function parseHashline(diff: string): HashlineEdit[] {
@@ -158,7 +172,8 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
const insertBeforeMatch = INSERT_BEFORE_OP_RE.exec(line);
if (insertBeforeMatch) {
const cursor = parseInsertTarget(insertBeforeMatch[1], lineNum, "before");
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, true);
const { payload, nextIndex, paddingWarning } = collectPayload(lines, i + 1, lineNum, true);
if (paddingWarning) warnings.push(paddingWarning);
for (const text of payload) pushInsert(cursor, text, lineNum);
i = nextIndex;
continue;
@@ -167,7 +182,8 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
const insertAfterMatch = INSERT_AFTER_OP_RE.exec(line);
if (insertAfterMatch) {
const cursor = parseInsertTarget(insertAfterMatch[1], lineNum, "after");
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, true);
const { payload, nextIndex, paddingWarning } = collectPayload(lines, i + 1, lineNum, true);
if (paddingWarning) warnings.push(paddingWarning);
for (const text of payload) pushInsert(cursor, text, lineNum);
i = nextIndex;
continue;
@@ -185,7 +201,8 @@ export function parseHashlineWithWarnings(diff: string): { edits: HashlineEdit[]
const replaceMatch = REPLACE_OP_RE.exec(line);
if (replaceMatch) {
const range = parseRange(replaceMatch[1], lineNum);
const { payload, nextIndex } = collectPayload(lines, i + 1, lineNum, false);
const { payload, nextIndex, paddingWarning } = collectPayload(lines, i + 1, lineNum, false);
if (paddingWarning) warnings.push(paddingWarning);
// `= A..B` with no payload blanks the range to a single empty line.
const replacement = payload.length === 0 ? [""] : payload;
for (const text of replacement) {
@@ -19,8 +19,9 @@ Each op line is ONE of:
Op lines carry no content — payload goes on the next line.
WRONG: + 5pg| some code
WRONG: {{hsep}} some code
RIGHT: + 5pg
{{hsep}} some code
{{hsep}}some code
A single `+`/`<`/`=` op accepts MANY `{{hsep}}` payload lines. To insert N consecutive lines, write ONE op followed by N payload lines — NEVER N ops with one payload each.
@@ -37,8 +38,9 @@ RIGHT (one op, many payload lines):
</format-reminder>
<rules>
- Every payload line MUST start with `{{hsep}}`.
- Payload is verbatim — NEVER escape unicode.
- Every payload line MUST start with `{{hsep}}` immediately followed by payload text. Do NOT add a readability space after `{{hsep}}`.
- Every character after `{{hsep}}` is file content. If the target line intentionally starts with one space, write exactly one space after `{{hsep}}`; otherwise write none.
- Payload text is verbatim — NEVER escape unicode.
- **Payload is only what's NEW relative to your range:**
- `=` replaces inside; NEVER include lines outside.
- `+`/`<` adds at the anchor; NEVER repeat line A or neighbors.