Skip to content

Commit 1ee01a3

Browse files
mangeshraut712cursoragent
authored andcommitted
Handle carriage-return lines in Edit guard
1 parent 96ec930 commit 1ee01a3

4 files changed

Lines changed: 43 additions & 3 deletions

File tree

‎packages/agent-core-v2/src/app/edit/editService.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ export type EditApplyResult =
1212
| { readonly ok: true; readonly rawContent: string; readonly count: number }
1313
| { readonly ok: false; readonly error: string };
1414

15-
/** Multi-line empty replacements without an explicit opt-in are refused (see #2427). */
1615
const LARGE_DELETE_MIN_OLD_LINES = 3;
1716

1817
export function countEditLines(text: string): number {
1918
if (text.length === 0) return 0;
2019
let lines = 1;
2120
for (let i = 0; i < text.length; i++) {
22-
if (text.charCodeAt(i) === 10) lines++;
21+
const char = text[i];
22+
if (char === '\n' || (char === '\r' && text[i + 1] !== '\n')) lines++;
2323
}
2424
return lines;
2525
}

‎packages/agent-core-v2/test/app/edit/tools/edit.test.ts‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -581,6 +581,25 @@ describe('EditTool', () => {
581581
expect(writeText).toHaveBeenCalledWith('/tmp/skill.md', '\n\nmore');
582582
});
583583

584+
it('counts lone carriage returns when guarding multi-line deletions', async () => {
585+
const writeText = vi.fn().mockResolvedValue(undefined);
586+
const { fs } = createSpiedEditFs({
587+
readText: vi.fn().mockResolvedValue('first\rsecond\rthird\rtail'),
588+
writeText,
589+
});
590+
const tool = buildTool(fs, createTestEnv(), PERMISSIVE_WORKSPACE);
591+
592+
const result = await execute(tool, {
593+
path: '/tmp/cr.txt',
594+
old_string: 'first\rsecond\rthird',
595+
new_string: '',
596+
});
597+
598+
expect(result).toMatchObject({ isError: true });
599+
expect(result.output).toContain('Refusing a multi-line deletion');
600+
expect(writeText).not.toHaveBeenCalled();
601+
});
602+
584603
it('tells the model to reread a large region when old_string is missing', async () => {
585604
const writeText = vi.fn().mockResolvedValue(undefined);
586605
const { fs } = createSpiedEditFs({

‎packages/agent-core/src/tools/builtin/file/edit.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@ export function countEditLines(text: string): number {
6262
if (text.length === 0) return 0;
6363
let lines = 1;
6464
for (let i = 0; i < text.length; i++) {
65-
if (text.charCodeAt(i) === 10) lines++;
65+
const char = text[i];
66+
if (char === '\n' || (char === '\r' && text[i + 1] !== '\n')) lines++;
6667
}
6768
return lines;
6869
}

‎packages/agent-core/test/tools/edit.test.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,26 @@ describe('EditTool', () => {
382382
expect(writeText).toHaveBeenCalledWith('/tmp/skill.md', '\n\nmore');
383383
});
384384

385+
it('counts lone carriage returns when guarding multi-line deletions', async () => {
386+
const writeText = vi.fn().mockResolvedValue(0);
387+
const tool = new EditTool(
388+
createFakeKaos({
389+
readText: vi.fn().mockResolvedValue('first\rsecond\rthird\rtail'),
390+
writeText,
391+
}),
392+
PERMISSIVE_WORKSPACE,
393+
);
394+
395+
const result = await executeTool(
396+
tool,
397+
context({ path: '/tmp/cr.txt', old_string: 'first\rsecond\rthird', new_string: '' }),
398+
);
399+
400+
expect(result).toMatchObject({ isError: true });
401+
expect(result.output).toContain('Refusing a multi-line deletion');
402+
expect(writeText).not.toHaveBeenCalled();
403+
});
404+
385405
it('tells the model to reread a large region when old_string is missing', async () => {
386406
const writeText = vi.fn().mockResolvedValue(0);
387407
const tool = new EditTool(

0 commit comments

Comments
 (0)