gsd-pi 2.16.0 → 2.17.0
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/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/dist/resources/extensions/gsd/auto-prompts.ts +71 -41
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/dist/resources/extensions/gsd/auto.ts +54 -15
- package/dist/resources/extensions/gsd/commands.ts +20 -2
- package/dist/resources/extensions/gsd/complexity.ts +236 -0
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/dist/resources/extensions/gsd/files.ts +6 -2
- package/dist/resources/extensions/gsd/git-service.ts +19 -8
- package/dist/resources/extensions/gsd/gitignore.ts +41 -2
- package/dist/resources/extensions/gsd/guided-flow.ts +10 -6
- package/dist/resources/extensions/gsd/metrics.ts +44 -0
- package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/dist/resources/extensions/gsd/preferences.ts +122 -1
- package/dist/resources/extensions/gsd/routing-history.ts +290 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/dist/resources/extensions/gsd/types.ts +28 -0
- package/dist/resources/extensions/gsd/worktree.ts +2 -2
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +493 -13
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +422 -62
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
- package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.js +9 -22
- package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
- package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
- package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
- package/packages/pi-ai/src/models.generated.ts +422 -62
- package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
- package/packages/pi-ai/src/providers/google-shared.ts +10 -19
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
- package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
- package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +10 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +71 -41
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/src/resources/extensions/gsd/auto.ts +54 -15
- package/src/resources/extensions/gsd/commands.ts +20 -2
- package/src/resources/extensions/gsd/complexity.ts +236 -0
- package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
- package/src/resources/extensions/gsd/files.ts +6 -2
- package/src/resources/extensions/gsd/git-service.ts +19 -8
- package/src/resources/extensions/gsd/gitignore.ts +41 -2
- package/src/resources/extensions/gsd/guided-flow.ts +10 -6
- package/src/resources/extensions/gsd/metrics.ts +44 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
- package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
- package/src/resources/extensions/gsd/preferences.ts +122 -1
- package/src/resources/extensions/gsd/routing-history.ts +290 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
- package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
- package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
- package/src/resources/extensions/gsd/types.ts +28 -0
- package/src/resources/extensions/gsd/worktree.ts +2 -2
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { describe, it } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { sanitizeSchemaForGoogle } from "./google-shared.js";
|
|
4
|
+
|
|
5
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
// sanitizeSchemaForGoogle
|
|
7
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
8
|
+
|
|
9
|
+
describe("sanitizeSchemaForGoogle", () => {
|
|
10
|
+
it("passes through primitives unchanged", () => {
|
|
11
|
+
assert.equal(sanitizeSchemaForGoogle(null), null);
|
|
12
|
+
assert.equal(sanitizeSchemaForGoogle(42), 42);
|
|
13
|
+
assert.equal(sanitizeSchemaForGoogle("hello"), "hello");
|
|
14
|
+
assert.equal(sanitizeSchemaForGoogle(true), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("passes through a valid schema with no banned fields", () => {
|
|
18
|
+
const schema = {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
name: { type: "string" },
|
|
22
|
+
age: { type: "number" },
|
|
23
|
+
},
|
|
24
|
+
required: ["name"],
|
|
25
|
+
};
|
|
26
|
+
assert.deepEqual(sanitizeSchemaForGoogle(schema), schema);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("removes top-level patternProperties", () => {
|
|
30
|
+
const schema = {
|
|
31
|
+
type: "object",
|
|
32
|
+
patternProperties: { "^S_": { type: "string" } },
|
|
33
|
+
properties: { foo: { type: "string" } },
|
|
34
|
+
};
|
|
35
|
+
const result = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
|
|
36
|
+
assert.ok(!("patternProperties" in result));
|
|
37
|
+
assert.deepEqual(result.properties, { foo: { type: "string" } });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("removes nested patternProperties", () => {
|
|
41
|
+
const schema = {
|
|
42
|
+
type: "object",
|
|
43
|
+
properties: {
|
|
44
|
+
nested: {
|
|
45
|
+
type: "object",
|
|
46
|
+
patternProperties: { ".*": { type: "string" } },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
const result = sanitizeSchemaForGoogle(schema) as any;
|
|
51
|
+
assert.ok(!("patternProperties" in result.properties.nested));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("converts top-level const to enum", () => {
|
|
55
|
+
const schema = { const: "fixed-value" };
|
|
56
|
+
const result = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
|
|
57
|
+
assert.deepEqual(result.enum, ["fixed-value"]);
|
|
58
|
+
assert.ok(!("const" in result));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("converts const to enum inside anyOf", () => {
|
|
62
|
+
const schema = {
|
|
63
|
+
anyOf: [{ const: "a" }, { const: "b" }, { type: "string" }],
|
|
64
|
+
};
|
|
65
|
+
const result = sanitizeSchemaForGoogle(schema) as any;
|
|
66
|
+
assert.deepEqual(result.anyOf[0], { enum: ["a"] });
|
|
67
|
+
assert.deepEqual(result.anyOf[1], { enum: ["b"] });
|
|
68
|
+
assert.deepEqual(result.anyOf[2], { type: "string" });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("converts const to enum inside oneOf", () => {
|
|
72
|
+
const schema = {
|
|
73
|
+
oneOf: [{ const: "x" }, { const: "y" }],
|
|
74
|
+
};
|
|
75
|
+
const result = sanitizeSchemaForGoogle(schema) as any;
|
|
76
|
+
assert.deepEqual(result.oneOf[0], { enum: ["x"] });
|
|
77
|
+
assert.deepEqual(result.oneOf[1], { enum: ["y"] });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("recursively sanitizes deeply nested schemas", () => {
|
|
81
|
+
const schema = {
|
|
82
|
+
type: "object",
|
|
83
|
+
properties: {
|
|
84
|
+
level1: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {
|
|
87
|
+
level2: {
|
|
88
|
+
anyOf: [{ const: "deep" }, { type: "null" }],
|
|
89
|
+
patternProperties: { ".*": { type: "string" } },
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
const result = sanitizeSchemaForGoogle(schema) as any;
|
|
96
|
+
const level2 = result.properties.level1.properties.level2;
|
|
97
|
+
assert.deepEqual(level2.anyOf[0], { enum: ["deep"] });
|
|
98
|
+
assert.ok(!("patternProperties" in level2));
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("sanitizes items in array schemas", () => {
|
|
102
|
+
const schema = {
|
|
103
|
+
type: "array",
|
|
104
|
+
items: {
|
|
105
|
+
anyOf: [{ const: "foo" }, { type: "string" }],
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const result = sanitizeSchemaForGoogle(schema) as any;
|
|
109
|
+
assert.deepEqual(result.items.anyOf[0], { enum: ["foo"] });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("sanitizes arrays of schemas", () => {
|
|
113
|
+
const input = [{ const: "a" }, { const: "b" }];
|
|
114
|
+
const result = sanitizeSchemaForGoogle(input) as any[];
|
|
115
|
+
assert.deepEqual(result[0], { enum: ["a"] });
|
|
116
|
+
assert.deepEqual(result[1], { enum: ["b"] });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("preserves non-string const values unchanged", () => {
|
|
120
|
+
// Only string const values are converted; number const is passed through
|
|
121
|
+
const schema = { const: 42 };
|
|
122
|
+
const result = sanitizeSchemaForGoogle(schema) as Record<string, unknown>;
|
|
123
|
+
assert.equal(result.const, 42);
|
|
124
|
+
assert.ok(!("enum" in result));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("sanitizes additionalProperties", () => {
|
|
128
|
+
const schema = {
|
|
129
|
+
type: "object",
|
|
130
|
+
additionalProperties: {
|
|
131
|
+
patternProperties: { "^x-": { type: "string" } },
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const result = sanitizeSchemaForGoogle(schema) as any;
|
|
135
|
+
assert.ok(!("patternProperties" in result.additionalProperties));
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -204,7 +204,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
204
204
|
// Cloud Code Assist API requires all function responses to be in a single user turn.
|
|
205
205
|
// Check if the last content is already a user turn with function responses and merge.
|
|
206
206
|
const lastContent = contents[contents.length - 1];
|
|
207
|
-
if (lastContent?.role === "user" && lastContent.parts?.some((p
|
|
207
|
+
if (lastContent?.role === "user" && lastContent.parts?.some((p) => p.functionResponse)) {
|
|
208
208
|
lastContent.parts.push(functionResponsePart);
|
|
209
209
|
} else {
|
|
210
210
|
contents.push({
|
|
@@ -237,7 +237,7 @@ export function convertMessages<T extends GoogleApiType>(model: Model<T>, contex
|
|
|
237
237
|
* This is needed for providers like `google-antigravity` when proxying Claude models,
|
|
238
238
|
* since Google Cloud Code Assist uses a restricted subset of JSON Schema.
|
|
239
239
|
*/
|
|
240
|
-
function sanitizeSchemaForGoogle(schema: unknown): unknown {
|
|
240
|
+
export function sanitizeSchemaForGoogle(schema: unknown): unknown {
|
|
241
241
|
if (!schema || typeof schema !== "object") {
|
|
242
242
|
return schema;
|
|
243
243
|
}
|
|
@@ -250,29 +250,19 @@ function sanitizeSchemaForGoogle(schema: unknown): unknown {
|
|
|
250
250
|
const sanitized: Record<string, unknown> = {};
|
|
251
251
|
|
|
252
252
|
for (const [key, value] of Object.entries(obj)) {
|
|
253
|
-
// Skip patternProperties entirely
|
|
253
|
+
// Skip patternProperties entirely — not supported by Google's API
|
|
254
254
|
if (key === "patternProperties") {
|
|
255
255
|
continue;
|
|
256
256
|
}
|
|
257
257
|
|
|
258
|
-
// Convert const to enum
|
|
258
|
+
// Convert const to enum — Google's API rejects the const keyword
|
|
259
259
|
if (key === "const" && typeof value === "string") {
|
|
260
|
-
// Only convert if we're inside anyOf/oneOf; otherwise leave as-is
|
|
261
|
-
// This will be handled by the anyOf/oneOf case below
|
|
262
260
|
sanitized.enum = [value];
|
|
263
261
|
continue;
|
|
264
262
|
}
|
|
265
263
|
|
|
266
|
-
// Recursively sanitize nested objects and arrays
|
|
267
|
-
if (
|
|
268
|
-
sanitized[key] = sanitizeSchemaForGoogle(value);
|
|
269
|
-
} else if (key === "items" && typeof value === "object") {
|
|
270
|
-
sanitized[key] = sanitizeSchemaForGoogle(value);
|
|
271
|
-
} else if (key === "anyOf" || key === "oneOf" || key === "allOf") {
|
|
272
|
-
sanitized[key] = sanitizeSchemaForGoogle(value);
|
|
273
|
-
} else if (key === "additionalProperties" && typeof value === "object") {
|
|
274
|
-
sanitized[key] = sanitizeSchemaForGoogle(value);
|
|
275
|
-
} else if (typeof value === "object" && !Array.isArray(value)) {
|
|
264
|
+
// Recursively sanitize all nested objects and arrays
|
|
265
|
+
if (typeof value === "object") {
|
|
276
266
|
sanitized[key] = sanitizeSchemaForGoogle(value);
|
|
277
267
|
} else {
|
|
278
268
|
sanitized[key] = value;
|
|
@@ -352,9 +342,10 @@ export function mapStopReason(reason: FinishReason): StopReason {
|
|
|
352
342
|
case FinishReason.UNEXPECTED_TOOL_CALL:
|
|
353
343
|
case FinishReason.NO_IMAGE:
|
|
354
344
|
return "error";
|
|
355
|
-
default:
|
|
356
|
-
|
|
357
|
-
|
|
345
|
+
default: {
|
|
346
|
+
const _exhaustive: never = reason;
|
|
347
|
+
throw new Error(`Unhandled stop reason: ${_exhaustive}`);
|
|
348
|
+
}
|
|
358
349
|
}
|
|
359
350
|
}
|
|
360
351
|
|
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
* Shared diff computation utilities for the edit tool.
|
|
3
3
|
* Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* These helpers intentionally stay in JavaScript. Issue #453 showed that
|
|
6
|
+
* post-tool preview paths must not depend on the native addon because a native
|
|
7
|
+
* hang there can wedge the entire interactive session after a successful tool run.
|
|
7
8
|
*/
|
|
8
9
|
export declare function detectLineEnding(content: string): "\r\n" | "\n";
|
|
9
10
|
export declare function normalizeToLF(text: string): string;
|
|
10
11
|
export declare function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string;
|
|
11
12
|
/**
|
|
12
|
-
* Normalize text for fuzzy matching
|
|
13
|
+
* Normalize text for fuzzy matching.
|
|
13
14
|
* - Strip trailing whitespace from each line
|
|
14
15
|
* - Normalize smart quotes to ASCII equivalents
|
|
15
16
|
* - Normalize Unicode dashes/hyphens to ASCII hyphen
|
|
@@ -32,8 +33,7 @@ export interface FuzzyMatchResult {
|
|
|
32
33
|
contentForReplacement: string;
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
|
-
* Find oldText in content, trying exact match first, then fuzzy match
|
|
36
|
-
* (native Rust implementation).
|
|
36
|
+
* Find oldText in content, trying exact match first, then fuzzy match.
|
|
37
37
|
*
|
|
38
38
|
* When fuzzy matching is used, the returned contentForReplacement is the
|
|
39
39
|
* fuzzy-normalized version of the content.
|
|
@@ -45,10 +45,10 @@ export declare function stripBom(content: string): {
|
|
|
45
45
|
text: string;
|
|
46
46
|
};
|
|
47
47
|
/**
|
|
48
|
-
* Generate a unified diff string with line numbers and context
|
|
49
|
-
* (native Rust implementation using Myers' algorithm via the `similar` crate).
|
|
48
|
+
* Generate a unified diff string with line numbers and context.
|
|
50
49
|
*
|
|
51
50
|
* Returns both the diff string and the first changed line number (in the new file).
|
|
51
|
+
* Only lines within `contextLines` of a change are included (like unified diff).
|
|
52
52
|
*/
|
|
53
53
|
export declare function generateDiffString(oldContent: string, newContent: string, contextLines?: number): {
|
|
54
54
|
diff: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"edit-diff.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAMH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAM/D;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAElD;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAE9E;AAED;;;;;;GAMG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAW3D;AAED,MAAM,WAAW,gBAAgB;IAChC,gCAAgC;IAChC,KAAK,EAAE,OAAO,CAAC;IACf,4FAA4F;IAC5F,KAAK,EAAE,MAAM,CAAC;IACd,iCAAiC;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,4DAA4D;IAC5D,cAAc,EAAE,OAAO,CAAC;IACxB;;;OAGG;IACH,qBAAqB,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAiChF;AAED,uFAAuF;AACvF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAEvE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CACjC,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,YAAY,SAAI,GACd;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,CAmExD;AAED,MAAM,WAAW,cAAc;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,gBAAgB,EAAE,MAAM,GAAG,SAAS,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC7B,KAAK,EAAE,MAAM,CAAC;CACd;AAsID;;;GAGG;AACH,wBAAsB,eAAe,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,GACT,OAAO,CAAC,cAAc,GAAG,aAAa,CAAC,CA6DzC"}
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Shared diff computation utilities for the edit tool.
|
|
3
3
|
* Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* These helpers intentionally stay in JavaScript. Issue #453 showed that
|
|
6
|
+
* post-tool preview paths must not depend on the native addon because a native
|
|
7
|
+
* hang there can wedge the entire interactive session after a successful tool run.
|
|
7
8
|
*/
|
|
8
|
-
import { fuzzyFindText as nativeFuzzyFindText, generateDiff as nativeGenerateDiff, normalizeForFuzzyMatch as nativeNormalizeForFuzzyMatch, } from "@gsd/native";
|
|
9
9
|
import { constants } from "fs";
|
|
10
10
|
import { access, readFile } from "fs/promises";
|
|
11
11
|
import { resolveToCwd } from "./path-utils.js";
|
|
@@ -25,42 +25,238 @@ export function restoreLineEndings(text, ending) {
|
|
|
25
25
|
return ending === "\r\n" ? text.replace(/\n/g, "\r\n") : text;
|
|
26
26
|
}
|
|
27
27
|
/**
|
|
28
|
-
* Normalize text for fuzzy matching
|
|
28
|
+
* Normalize text for fuzzy matching.
|
|
29
29
|
* - Strip trailing whitespace from each line
|
|
30
30
|
* - Normalize smart quotes to ASCII equivalents
|
|
31
31
|
* - Normalize Unicode dashes/hyphens to ASCII hyphen
|
|
32
32
|
* - Normalize special Unicode spaces to regular space
|
|
33
33
|
*/
|
|
34
34
|
export function normalizeForFuzzyMatch(text) {
|
|
35
|
-
return
|
|
35
|
+
return text
|
|
36
|
+
.replace(/\r\n/g, "\n")
|
|
37
|
+
.replace(/\r/g, "\n")
|
|
38
|
+
.replace(/[“”]/g, '"')
|
|
39
|
+
.replace(/[‘’]/g, "'")
|
|
40
|
+
.replace(/[‐‑‒–—−]/g, "-")
|
|
41
|
+
.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, " ")
|
|
42
|
+
.split("\n")
|
|
43
|
+
.map((line) => line.replace(/[ \t]+$/g, ""))
|
|
44
|
+
.join("\n");
|
|
36
45
|
}
|
|
37
46
|
/**
|
|
38
|
-
* Find oldText in content, trying exact match first, then fuzzy match
|
|
39
|
-
* (native Rust implementation).
|
|
47
|
+
* Find oldText in content, trying exact match first, then fuzzy match.
|
|
40
48
|
*
|
|
41
49
|
* When fuzzy matching is used, the returned contentForReplacement is the
|
|
42
50
|
* fuzzy-normalized version of the content.
|
|
43
51
|
*/
|
|
44
52
|
export function fuzzyFindText(content, oldText) {
|
|
45
|
-
|
|
53
|
+
const exactIndex = content.indexOf(oldText);
|
|
54
|
+
if (exactIndex !== -1) {
|
|
55
|
+
return {
|
|
56
|
+
found: true,
|
|
57
|
+
index: exactIndex,
|
|
58
|
+
matchLength: oldText.length,
|
|
59
|
+
usedFuzzyMatch: false,
|
|
60
|
+
contentForReplacement: content,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const normalizedContent = normalizeForFuzzyMatch(content);
|
|
64
|
+
const normalizedOldText = normalizeForFuzzyMatch(oldText);
|
|
65
|
+
const fuzzyIndex = normalizedContent.indexOf(normalizedOldText);
|
|
66
|
+
if (fuzzyIndex === -1) {
|
|
67
|
+
return {
|
|
68
|
+
found: false,
|
|
69
|
+
index: -1,
|
|
70
|
+
matchLength: 0,
|
|
71
|
+
usedFuzzyMatch: false,
|
|
72
|
+
contentForReplacement: content,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
found: true,
|
|
77
|
+
index: fuzzyIndex,
|
|
78
|
+
matchLength: normalizedOldText.length,
|
|
79
|
+
usedFuzzyMatch: true,
|
|
80
|
+
contentForReplacement: normalizedContent,
|
|
81
|
+
};
|
|
46
82
|
}
|
|
47
83
|
/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
|
|
48
84
|
export function stripBom(content) {
|
|
49
85
|
return content.startsWith("\uFEFF") ? { bom: "\uFEFF", text: content.slice(1) } : { bom: "", text: content };
|
|
50
86
|
}
|
|
51
87
|
/**
|
|
52
|
-
* Generate a unified diff string with line numbers and context
|
|
53
|
-
* (native Rust implementation using Myers' algorithm via the `similar` crate).
|
|
88
|
+
* Generate a unified diff string with line numbers and context.
|
|
54
89
|
*
|
|
55
90
|
* Returns both the diff string and the first changed line number (in the new file).
|
|
91
|
+
* Only lines within `contextLines` of a change are included (like unified diff).
|
|
56
92
|
*/
|
|
57
93
|
export function generateDiffString(oldContent, newContent, contextLines = 4) {
|
|
58
|
-
const
|
|
94
|
+
const ops = buildLineDiff(oldContent, newContent);
|
|
95
|
+
let firstChangedLine;
|
|
96
|
+
// First pass: assign line numbers and find changed indices
|
|
97
|
+
const annotated = [];
|
|
98
|
+
let oldLine = 1;
|
|
99
|
+
let newLine = 1;
|
|
100
|
+
const changedIndices = [];
|
|
101
|
+
for (let idx = 0; idx < ops.length; idx++) {
|
|
102
|
+
const op = ops[idx];
|
|
103
|
+
annotated.push({ op, oldLine, newLine });
|
|
104
|
+
if (op.type !== "context") {
|
|
105
|
+
changedIndices.push(idx);
|
|
106
|
+
if (firstChangedLine === undefined) {
|
|
107
|
+
firstChangedLine = newLine;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (op.type === "remove") {
|
|
111
|
+
oldLine += 1;
|
|
112
|
+
}
|
|
113
|
+
else if (op.type === "add") {
|
|
114
|
+
newLine += 1;
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
oldLine += 1;
|
|
118
|
+
newLine += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Build set of indices to include (changes + surrounding context)
|
|
122
|
+
const includeSet = new Set();
|
|
123
|
+
for (const ci of changedIndices) {
|
|
124
|
+
for (let k = Math.max(0, ci - contextLines); k <= Math.min(ops.length - 1, ci + contextLines); k++) {
|
|
125
|
+
includeSet.add(k);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const maxLine = Math.max(oldLine - 1, newLine - 1, 1);
|
|
129
|
+
const lineNumberWidth = String(maxLine).length;
|
|
130
|
+
const rendered = [];
|
|
131
|
+
let lastIncluded = -1;
|
|
132
|
+
for (let idx = 0; idx < annotated.length; idx++) {
|
|
133
|
+
if (!includeSet.has(idx))
|
|
134
|
+
continue;
|
|
135
|
+
// Insert separator when there's a gap between included regions
|
|
136
|
+
if (lastIncluded !== -1 && idx > lastIncluded + 1) {
|
|
137
|
+
rendered.push("...");
|
|
138
|
+
}
|
|
139
|
+
lastIncluded = idx;
|
|
140
|
+
const { op, oldLine: ol, newLine: nl } = annotated[idx];
|
|
141
|
+
if (op.type === "context") {
|
|
142
|
+
rendered.push(` ${String(nl).padStart(lineNumberWidth, " ")} ${op.line}`);
|
|
143
|
+
}
|
|
144
|
+
else if (op.type === "remove") {
|
|
145
|
+
rendered.push(`-${String(ol).padStart(lineNumberWidth, " ")} ${op.line}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
rendered.push(`+${String(nl).padStart(lineNumberWidth, " ")} ${op.line}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
59
151
|
return {
|
|
60
|
-
diff:
|
|
61
|
-
firstChangedLine
|
|
152
|
+
diff: rendered.join("\n"),
|
|
153
|
+
firstChangedLine,
|
|
62
154
|
};
|
|
63
155
|
}
|
|
156
|
+
function splitLines(text) {
|
|
157
|
+
const lines = text.split("\n");
|
|
158
|
+
if (lines.length > 0 && lines.at(-1) === "") {
|
|
159
|
+
lines.pop();
|
|
160
|
+
}
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Maximum number of cells (oldLines * newLines) before we switch from the
|
|
165
|
+
* full LCS DP algorithm to a simpler linear-scan diff. This prevents OOM
|
|
166
|
+
* on large files (e.g. 10k lines would need a 100M-cell matrix).
|
|
167
|
+
*/
|
|
168
|
+
const MAX_DP_CELLS = 4_000_000; // ~32 MB for 64-bit numbers
|
|
169
|
+
function buildLineDiff(oldContent, newContent) {
|
|
170
|
+
const oldLines = splitLines(oldContent);
|
|
171
|
+
const newLines = splitLines(newContent);
|
|
172
|
+
const cells = (oldLines.length + 1) * (newLines.length + 1);
|
|
173
|
+
if (cells > MAX_DP_CELLS) {
|
|
174
|
+
return buildLineDiffLinear(oldLines, newLines);
|
|
175
|
+
}
|
|
176
|
+
return buildLineDiffLCS(oldLines, newLines);
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Full LCS-based diff using O(n*m) DP table. Produces optimal diffs but
|
|
180
|
+
* is only safe for files where n*m <= MAX_DP_CELLS.
|
|
181
|
+
*/
|
|
182
|
+
function buildLineDiffLCS(oldLines, newLines) {
|
|
183
|
+
const dp = Array.from({ length: oldLines.length + 1 }, () => Array(newLines.length + 1).fill(0));
|
|
184
|
+
for (let i = oldLines.length - 1; i >= 0; i--) {
|
|
185
|
+
for (let j = newLines.length - 1; j >= 0; j--) {
|
|
186
|
+
if (oldLines[i] === newLines[j]) {
|
|
187
|
+
dp[i][j] = dp[i + 1][j + 1] + 1;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const ops = [];
|
|
195
|
+
let i = 0;
|
|
196
|
+
let j = 0;
|
|
197
|
+
while (i < oldLines.length && j < newLines.length) {
|
|
198
|
+
if (oldLines[i] === newLines[j]) {
|
|
199
|
+
ops.push({ type: "context", line: oldLines[i] });
|
|
200
|
+
i += 1;
|
|
201
|
+
j += 1;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
205
|
+
ops.push({ type: "remove", line: oldLines[i] });
|
|
206
|
+
i += 1;
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
ops.push({ type: "add", line: newLines[j] });
|
|
210
|
+
j += 1;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
while (i < oldLines.length) {
|
|
214
|
+
ops.push({ type: "remove", line: oldLines[i] });
|
|
215
|
+
i += 1;
|
|
216
|
+
}
|
|
217
|
+
while (j < newLines.length) {
|
|
218
|
+
ops.push({ type: "add", line: newLines[j] });
|
|
219
|
+
j += 1;
|
|
220
|
+
}
|
|
221
|
+
return ops;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Linear-time fallback diff for large files. Matches common prefix/suffix,
|
|
225
|
+
* then treats the remaining middle as a bulk remove+add. Not optimal but
|
|
226
|
+
* O(n+m) in both time and space.
|
|
227
|
+
*/
|
|
228
|
+
function buildLineDiffLinear(oldLines, newLines) {
|
|
229
|
+
const ops = [];
|
|
230
|
+
// Match common prefix
|
|
231
|
+
let prefixLen = 0;
|
|
232
|
+
const minLen = Math.min(oldLines.length, newLines.length);
|
|
233
|
+
while (prefixLen < minLen && oldLines[prefixLen] === newLines[prefixLen]) {
|
|
234
|
+
prefixLen++;
|
|
235
|
+
}
|
|
236
|
+
// Match common suffix (not overlapping with prefix)
|
|
237
|
+
let suffixLen = 0;
|
|
238
|
+
while (suffixLen < minLen - prefixLen &&
|
|
239
|
+
oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
|
|
240
|
+
suffixLen++;
|
|
241
|
+
}
|
|
242
|
+
// Emit prefix context
|
|
243
|
+
for (let i = 0; i < prefixLen; i++) {
|
|
244
|
+
ops.push({ type: "context", line: oldLines[i] });
|
|
245
|
+
}
|
|
246
|
+
// Emit removed lines from the middle
|
|
247
|
+
for (let i = prefixLen; i < oldLines.length - suffixLen; i++) {
|
|
248
|
+
ops.push({ type: "remove", line: oldLines[i] });
|
|
249
|
+
}
|
|
250
|
+
// Emit added lines from the middle
|
|
251
|
+
for (let j = prefixLen; j < newLines.length - suffixLen; j++) {
|
|
252
|
+
ops.push({ type: "add", line: newLines[j] });
|
|
253
|
+
}
|
|
254
|
+
// Emit suffix context
|
|
255
|
+
for (let i = oldLines.length - suffixLen; i < oldLines.length; i++) {
|
|
256
|
+
ops.push({ type: "context", line: oldLines[i] });
|
|
257
|
+
}
|
|
258
|
+
return ops;
|
|
259
|
+
}
|
|
64
260
|
/**
|
|
65
261
|
* Compute the diff for an edit operation without applying it.
|
|
66
262
|
* Used for preview rendering in the TUI before the tool executes.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"edit-diff.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n *\n * Hot-path functions (fuzzyFindText, normalizeForFuzzyMatch, generateDiffString)\n * delegate to the native Rust engine for performance on large files.\n */\n\nimport {\n\tfuzzyFindText as nativeFuzzyFindText,\n\tgenerateDiff as nativeGenerateDiff,\n\tnormalizeForFuzzyMatch as nativeNormalizeForFuzzyMatch,\n} from \"@gsd/native\";\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching (native Rust implementation).\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn nativeNormalizeForFuzzyMatch(text);\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match\n * (native Rust implementation).\n *\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content.\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\treturn nativeFuzzyFindText(content, oldText);\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Generate a unified diff string with line numbers and context\n * (native Rust implementation using Myers' algorithm via the `similar` crate).\n *\n * Returns both the diff string and the first changed line number (in the new file).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst result = nativeGenerateDiff(oldContent, newContent, contextLines);\n\treturn {\n\t\tdiff: result.diff,\n\t\tfirstChangedLine: result.firstChangedLine ?? undefined,\n\t};\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\n/**\n * Compute the diff for an edit operation without applying it.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch {\n\t\t\treturn { error: `File not found: ${path}` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst normalizedOldText = normalizeToLF(oldText);\n\t\tconst normalizedNewText = normalizeToLF(newText);\n\n\t\t// Find the old text using fuzzy matching (tries exact match first, then fuzzy)\n\t\tconst matchResult = fuzzyFindText(normalizedContent, normalizedOldText);\n\n\t\tif (!matchResult.found) {\n\t\t\treturn {\n\t\t\t\terror: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t\t};\n\t\t}\n\n\t\t// Count occurrences using fuzzy-normalized content for consistency\n\t\tconst fuzzyContent = normalizeForFuzzyMatch(normalizedContent);\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);\n\t\tconst occurrences = fuzzyContent.split(fuzzyOldText).length - 1;\n\n\t\tif (occurrences > 1) {\n\t\t\treturn {\n\t\t\t\terror: `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t\t};\n\t\t}\n\n\t\t// Compute the new content using the matched position\n\t\t// When fuzzy matching was used, contentForReplacement is the normalized version\n\t\tconst baseContent = matchResult.contentForReplacement;\n\t\tconst newContent =\n\t\t\tbaseContent.substring(0, matchResult.index) +\n\t\t\tnormalizedNewText +\n\t\t\tbaseContent.substring(matchResult.index + matchResult.matchLength);\n\n\t\t// Check if it would actually change anything\n\t\tif (baseContent === newContent) {\n\t\t\treturn {\n\t\t\t\terror: `No changes would be made to ${path}. The replacement produces identical content.`,\n\t\t\t};\n\t\t}\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"edit-diff.js","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared diff computation utilities for the edit tool.\n * Used by both edit.ts (for execution) and tool-execution.ts (for preview rendering).\n *\n * These helpers intentionally stay in JavaScript. Issue #453 showed that\n * post-tool preview paths must not depend on the native addon because a native\n * hang there can wedge the entire interactive session after a successful tool run.\n */\n\nimport { constants } from \"fs\";\nimport { access, readFile } from \"fs/promises\";\nimport { resolveToCwd } from \"./path-utils.js\";\n\nexport function detectLineEnding(content: string): \"\\r\\n\" | \"\\n\" {\n\tconst crlfIdx = content.indexOf(\"\\r\\n\");\n\tconst lfIdx = content.indexOf(\"\\n\");\n\tif (lfIdx === -1) return \"\\n\";\n\tif (crlfIdx === -1) return \"\\n\";\n\treturn crlfIdx < lfIdx ? \"\\r\\n\" : \"\\n\";\n}\n\nexport function normalizeToLF(text: string): string {\n\treturn text.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\");\n}\n\nexport function restoreLineEndings(text: string, ending: \"\\r\\n\" | \"\\n\"): string {\n\treturn ending === \"\\r\\n\" ? text.replace(/\\n/g, \"\\r\\n\") : text;\n}\n\n/**\n * Normalize text for fuzzy matching.\n * - Strip trailing whitespace from each line\n * - Normalize smart quotes to ASCII equivalents\n * - Normalize Unicode dashes/hyphens to ASCII hyphen\n * - Normalize special Unicode spaces to regular space\n */\nexport function normalizeForFuzzyMatch(text: string): string {\n\treturn text\n\t\t.replace(/\\r\\n/g, \"\\n\")\n\t\t.replace(/\\r/g, \"\\n\")\n\t\t.replace(/[“”]/g, '\"')\n\t\t.replace(/[‘’]/g, \"'\")\n\t\t.replace(/[‐‑‒–—−]/g, \"-\")\n\t\t.replace(/[\\u00A0\\u1680\\u2000-\\u200A\\u202F\\u205F\\u3000]/g, \" \")\n\t\t.split(\"\\n\")\n\t\t.map((line) => line.replace(/[ \\t]+$/g, \"\"))\n\t\t.join(\"\\n\");\n}\n\nexport interface FuzzyMatchResult {\n\t/** Whether a match was found */\n\tfound: boolean;\n\t/** The index where the match starts (in the content that should be used for replacement) */\n\tindex: number;\n\t/** Length of the matched text */\n\tmatchLength: number;\n\t/** Whether fuzzy matching was used (false = exact match) */\n\tusedFuzzyMatch: boolean;\n\t/**\n\t * The content to use for replacement operations.\n\t * When exact match: original content. When fuzzy match: normalized content.\n\t */\n\tcontentForReplacement: string;\n}\n\n/**\n * Find oldText in content, trying exact match first, then fuzzy match.\n *\n * When fuzzy matching is used, the returned contentForReplacement is the\n * fuzzy-normalized version of the content.\n */\nexport function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {\n\tconst exactIndex = content.indexOf(oldText);\n\tif (exactIndex !== -1) {\n\t\treturn {\n\t\t\tfound: true,\n\t\t\tindex: exactIndex,\n\t\t\tmatchLength: oldText.length,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\tconst normalizedContent = normalizeForFuzzyMatch(content);\n\tconst normalizedOldText = normalizeForFuzzyMatch(oldText);\n\tconst fuzzyIndex = normalizedContent.indexOf(normalizedOldText);\n\n\tif (fuzzyIndex === -1) {\n\t\treturn {\n\t\t\tfound: false,\n\t\t\tindex: -1,\n\t\t\tmatchLength: 0,\n\t\t\tusedFuzzyMatch: false,\n\t\t\tcontentForReplacement: content,\n\t\t};\n\t}\n\n\treturn {\n\t\tfound: true,\n\t\tindex: fuzzyIndex,\n\t\tmatchLength: normalizedOldText.length,\n\t\tusedFuzzyMatch: true,\n\t\tcontentForReplacement: normalizedContent,\n\t};\n}\n\n/** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */\nexport function stripBom(content: string): { bom: string; text: string } {\n\treturn content.startsWith(\"\\uFEFF\") ? { bom: \"\\uFEFF\", text: content.slice(1) } : { bom: \"\", text: content };\n}\n\n/**\n * Generate a unified diff string with line numbers and context.\n *\n * Returns both the diff string and the first changed line number (in the new file).\n * Only lines within `contextLines` of a change are included (like unified diff).\n */\nexport function generateDiffString(\n\toldContent: string,\n\tnewContent: string,\n\tcontextLines = 4,\n): { diff: string; firstChangedLine: number | undefined } {\n\tconst ops = buildLineDiff(oldContent, newContent);\n\tlet firstChangedLine: number | undefined;\n\n\t// First pass: assign line numbers and find changed indices\n\tconst annotated: { op: LineDiffOp; oldLine: number; newLine: number }[] = [];\n\tlet oldLine = 1;\n\tlet newLine = 1;\n\tconst changedIndices: number[] = [];\n\n\tfor (let idx = 0; idx < ops.length; idx++) {\n\t\tconst op = ops[idx];\n\t\tannotated.push({ op, oldLine, newLine });\n\n\t\tif (op.type !== \"context\") {\n\t\t\tchangedIndices.push(idx);\n\t\t\tif (firstChangedLine === undefined) {\n\t\t\t\tfirstChangedLine = newLine;\n\t\t\t}\n\t\t}\n\n\t\tif (op.type === \"remove\") {\n\t\t\toldLine += 1;\n\t\t} else if (op.type === \"add\") {\n\t\t\tnewLine += 1;\n\t\t} else {\n\t\t\toldLine += 1;\n\t\t\tnewLine += 1;\n\t\t}\n\t}\n\n\t// Build set of indices to include (changes + surrounding context)\n\tconst includeSet = new Set<number>();\n\tfor (const ci of changedIndices) {\n\t\tfor (let k = Math.max(0, ci - contextLines); k <= Math.min(ops.length - 1, ci + contextLines); k++) {\n\t\t\tincludeSet.add(k);\n\t\t}\n\t}\n\n\tconst maxLine = Math.max(oldLine - 1, newLine - 1, 1);\n\tconst lineNumberWidth = String(maxLine).length;\n\tconst rendered: string[] = [];\n\tlet lastIncluded = -1;\n\n\tfor (let idx = 0; idx < annotated.length; idx++) {\n\t\tif (!includeSet.has(idx)) continue;\n\n\t\t// Insert separator when there's a gap between included regions\n\t\tif (lastIncluded !== -1 && idx > lastIncluded + 1) {\n\t\t\trendered.push(\"...\");\n\t\t}\n\t\tlastIncluded = idx;\n\n\t\tconst { op, oldLine: ol, newLine: nl } = annotated[idx];\n\t\tif (op.type === \"context\") {\n\t\t\trendered.push(` ${String(nl).padStart(lineNumberWidth, \" \")} ${op.line}`);\n\t\t} else if (op.type === \"remove\") {\n\t\t\trendered.push(`-${String(ol).padStart(lineNumberWidth, \" \")} ${op.line}`);\n\t\t} else {\n\t\t\trendered.push(`+${String(nl).padStart(lineNumberWidth, \" \")} ${op.line}`);\n\t\t}\n\t}\n\n\treturn {\n\t\tdiff: rendered.join(\"\\n\"),\n\t\tfirstChangedLine,\n\t};\n}\n\nexport interface EditDiffResult {\n\tdiff: string;\n\tfirstChangedLine: number | undefined;\n}\n\nexport interface EditDiffError {\n\terror: string;\n}\n\ntype LineDiffOp =\n\t| { type: \"context\"; line: string }\n\t| { type: \"remove\"; line: string }\n\t| { type: \"add\"; line: string };\n\nfunction splitLines(text: string): string[] {\n\tconst lines = text.split(\"\\n\");\n\tif (lines.length > 0 && lines.at(-1) === \"\") {\n\t\tlines.pop();\n\t}\n\treturn lines;\n}\n\n/**\n * Maximum number of cells (oldLines * newLines) before we switch from the\n * full LCS DP algorithm to a simpler linear-scan diff. This prevents OOM\n * on large files (e.g. 10k lines would need a 100M-cell matrix).\n */\nconst MAX_DP_CELLS = 4_000_000; // ~32 MB for 64-bit numbers\n\nfunction buildLineDiff(oldContent: string, newContent: string): LineDiffOp[] {\n\tconst oldLines = splitLines(oldContent);\n\tconst newLines = splitLines(newContent);\n\n\tconst cells = (oldLines.length + 1) * (newLines.length + 1);\n\tif (cells > MAX_DP_CELLS) {\n\t\treturn buildLineDiffLinear(oldLines, newLines);\n\t}\n\n\treturn buildLineDiffLCS(oldLines, newLines);\n}\n\n/**\n * Full LCS-based diff using O(n*m) DP table. Produces optimal diffs but\n * is only safe for files where n*m <= MAX_DP_CELLS.\n */\nfunction buildLineDiffLCS(oldLines: string[], newLines: string[]): LineDiffOp[] {\n\tconst dp: number[][] = Array.from({ length: oldLines.length + 1 }, () =>\n\t\tArray<number>(newLines.length + 1).fill(0),\n\t);\n\n\tfor (let i = oldLines.length - 1; i >= 0; i--) {\n\t\tfor (let j = newLines.length - 1; j >= 0; j--) {\n\t\t\tif (oldLines[i] === newLines[j]) {\n\t\t\t\tdp[i][j] = dp[i + 1][j + 1] + 1;\n\t\t\t} else {\n\t\t\t\tdp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);\n\t\t\t}\n\t\t}\n\t}\n\n\tconst ops: LineDiffOp[] = [];\n\tlet i = 0;\n\tlet j = 0;\n\n\twhile (i < oldLines.length && j < newLines.length) {\n\t\tif (oldLines[i] === newLines[j]) {\n\t\t\tops.push({ type: \"context\", line: oldLines[i] });\n\t\t\ti += 1;\n\t\t\tj += 1;\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (dp[i + 1][j] >= dp[i][j + 1]) {\n\t\t\tops.push({ type: \"remove\", line: oldLines[i] });\n\t\t\ti += 1;\n\t\t} else {\n\t\t\tops.push({ type: \"add\", line: newLines[j] });\n\t\t\tj += 1;\n\t\t}\n\t}\n\n\twhile (i < oldLines.length) {\n\t\tops.push({ type: \"remove\", line: oldLines[i] });\n\t\ti += 1;\n\t}\n\n\twhile (j < newLines.length) {\n\t\tops.push({ type: \"add\", line: newLines[j] });\n\t\tj += 1;\n\t}\n\n\treturn ops;\n}\n\n/**\n * Linear-time fallback diff for large files. Matches common prefix/suffix,\n * then treats the remaining middle as a bulk remove+add. Not optimal but\n * O(n+m) in both time and space.\n */\nfunction buildLineDiffLinear(oldLines: string[], newLines: string[]): LineDiffOp[] {\n\tconst ops: LineDiffOp[] = [];\n\n\t// Match common prefix\n\tlet prefixLen = 0;\n\tconst minLen = Math.min(oldLines.length, newLines.length);\n\twhile (prefixLen < minLen && oldLines[prefixLen] === newLines[prefixLen]) {\n\t\tprefixLen++;\n\t}\n\n\t// Match common suffix (not overlapping with prefix)\n\tlet suffixLen = 0;\n\twhile (\n\t\tsuffixLen < minLen - prefixLen &&\n\t\toldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]\n\t) {\n\t\tsuffixLen++;\n\t}\n\n\t// Emit prefix context\n\tfor (let i = 0; i < prefixLen; i++) {\n\t\tops.push({ type: \"context\", line: oldLines[i] });\n\t}\n\n\t// Emit removed lines from the middle\n\tfor (let i = prefixLen; i < oldLines.length - suffixLen; i++) {\n\t\tops.push({ type: \"remove\", line: oldLines[i] });\n\t}\n\n\t// Emit added lines from the middle\n\tfor (let j = prefixLen; j < newLines.length - suffixLen; j++) {\n\t\tops.push({ type: \"add\", line: newLines[j] });\n\t}\n\n\t// Emit suffix context\n\tfor (let i = oldLines.length - suffixLen; i < oldLines.length; i++) {\n\t\tops.push({ type: \"context\", line: oldLines[i] });\n\t}\n\n\treturn ops;\n}\n\n/**\n * Compute the diff for an edit operation without applying it.\n * Used for preview rendering in the TUI before the tool executes.\n */\nexport async function computeEditDiff(\n\tpath: string,\n\toldText: string,\n\tnewText: string,\n\tcwd: string,\n): Promise<EditDiffResult | EditDiffError> {\n\tconst absolutePath = resolveToCwd(path, cwd);\n\n\ttry {\n\t\t// Check if file exists and is readable\n\t\ttry {\n\t\t\tawait access(absolutePath, constants.R_OK);\n\t\t} catch {\n\t\t\treturn { error: `File not found: ${path}` };\n\t\t}\n\n\t\t// Read the file\n\t\tconst rawContent = await readFile(absolutePath, \"utf-8\");\n\n\t\t// Strip BOM before matching (LLM won't include invisible BOM in oldText)\n\t\tconst { text: content } = stripBom(rawContent);\n\n\t\tconst normalizedContent = normalizeToLF(content);\n\t\tconst normalizedOldText = normalizeToLF(oldText);\n\t\tconst normalizedNewText = normalizeToLF(newText);\n\n\t\t// Find the old text using fuzzy matching (tries exact match first, then fuzzy)\n\t\tconst matchResult = fuzzyFindText(normalizedContent, normalizedOldText);\n\n\t\tif (!matchResult.found) {\n\t\t\treturn {\n\t\t\t\terror: `Could not find the exact text in ${path}. The old text must match exactly including all whitespace and newlines.`,\n\t\t\t};\n\t\t}\n\n\t\t// Count occurrences using fuzzy-normalized content for consistency\n\t\tconst fuzzyContent = normalizeForFuzzyMatch(normalizedContent);\n\t\tconst fuzzyOldText = normalizeForFuzzyMatch(normalizedOldText);\n\t\tconst occurrences = fuzzyContent.split(fuzzyOldText).length - 1;\n\n\t\tif (occurrences > 1) {\n\t\t\treturn {\n\t\t\t\terror: `Found ${occurrences} occurrences of the text in ${path}. The text must be unique. Please provide more context to make it unique.`,\n\t\t\t};\n\t\t}\n\n\t\t// Compute the new content using the matched position\n\t\t// When fuzzy matching was used, contentForReplacement is the normalized version\n\t\tconst baseContent = matchResult.contentForReplacement;\n\t\tconst newContent =\n\t\t\tbaseContent.substring(0, matchResult.index) +\n\t\t\tnormalizedNewText +\n\t\t\tbaseContent.substring(matchResult.index + matchResult.matchLength);\n\n\t\t// Check if it would actually change anything\n\t\tif (baseContent === newContent) {\n\t\t\treturn {\n\t\t\t\terror: `No changes would be made to ${path}. The replacement produces identical content.`,\n\t\t\t};\n\t\t}\n\n\t\t// Generate the diff\n\t\treturn generateDiffString(baseContent, newContent);\n\t} catch (err) {\n\t\treturn { error: err instanceof Error ? err.message : String(err) };\n\t}\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"edit-diff.test.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.test.ts"],"names":[],"mappings":""}
|