diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 775437f92..d6c0a5e49 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/hashline/parser.ts b/packages/coding-agent/src/hashline/parser.ts index e0ed8fc33..0bb9ca1fc 100644 --- a/packages/coding-agent/src/hashline/parser.ts +++ b/packages/coding-agent/src/hashline/parser.ts @@ -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) { diff --git a/packages/coding-agent/src/prompts/tools/hashline.md b/packages/coding-agent/src/prompts/tools/hashline.md index db77c509d..7ec445d63 100644 --- a/packages/coding-agent/src/prompts/tools/hashline.md +++ b/packages/coding-agent/src/prompts/tools/hashline.md @@ -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): -- 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.