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.
Files changed (89) hide show
  1. package/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
  2. package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
  3. package/dist/resources/extensions/gsd/auto-prompts.ts +71 -41
  4. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
  5. package/dist/resources/extensions/gsd/auto.ts +54 -15
  6. package/dist/resources/extensions/gsd/commands.ts +20 -2
  7. package/dist/resources/extensions/gsd/complexity.ts +236 -0
  8. package/dist/resources/extensions/gsd/docs/preferences-reference.md +1 -0
  9. package/dist/resources/extensions/gsd/files.ts +6 -2
  10. package/dist/resources/extensions/gsd/git-service.ts +19 -8
  11. package/dist/resources/extensions/gsd/gitignore.ts +41 -2
  12. package/dist/resources/extensions/gsd/guided-flow.ts +10 -6
  13. package/dist/resources/extensions/gsd/metrics.ts +44 -0
  14. package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
  15. package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  16. package/dist/resources/extensions/gsd/preferences.ts +122 -1
  17. package/dist/resources/extensions/gsd/routing-history.ts +290 -0
  18. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  19. package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  20. package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  21. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  22. package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  23. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  24. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  25. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  26. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  27. package/dist/resources/extensions/gsd/types.ts +28 -0
  28. package/dist/resources/extensions/gsd/worktree.ts +2 -2
  29. package/package.json +1 -1
  30. package/packages/pi-ai/dist/models.generated.d.ts +493 -13
  31. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  32. package/packages/pi-ai/dist/models.generated.js +422 -62
  33. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  34. package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
  35. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  36. package/packages/pi-ai/dist/providers/google-shared.js +9 -22
  37. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  38. package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
  39. package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
  40. package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
  41. package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
  42. package/packages/pi-ai/src/models.generated.ts +422 -62
  43. package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
  44. package/packages/pi-ai/src/providers/google-shared.ts +10 -19
  45. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
  46. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
  48. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  49. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
  50. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
  51. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
  52. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
  53. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  55. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  56. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
  57. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
  58. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
  59. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  60. package/pkg/dist/modes/interactive/theme/theme.js +10 -0
  61. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  62. package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
  63. package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
  64. package/src/resources/extensions/gsd/auto-prompts.ts +71 -41
  65. package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
  66. package/src/resources/extensions/gsd/auto.ts +54 -15
  67. package/src/resources/extensions/gsd/commands.ts +20 -2
  68. package/src/resources/extensions/gsd/complexity.ts +236 -0
  69. package/src/resources/extensions/gsd/docs/preferences-reference.md +1 -0
  70. package/src/resources/extensions/gsd/files.ts +6 -2
  71. package/src/resources/extensions/gsd/git-service.ts +19 -8
  72. package/src/resources/extensions/gsd/gitignore.ts +41 -2
  73. package/src/resources/extensions/gsd/guided-flow.ts +10 -6
  74. package/src/resources/extensions/gsd/metrics.ts +44 -0
  75. package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
  76. package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  77. package/src/resources/extensions/gsd/preferences.ts +122 -1
  78. package/src/resources/extensions/gsd/routing-history.ts +290 -0
  79. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  80. package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  81. package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  82. package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  83. package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  84. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  85. package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  86. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  87. package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  88. package/src/resources/extensions/gsd/types.ts +28 -0
  89. 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: Part) => p.functionResponse)) {
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 in anyOf/oneOf blocks
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 (key === "properties" && typeof value === "object") {
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
- // Fallback for new/unknown FinishReason values
357
- return "error";
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
- * Hot-path functions (fuzzyFindText, normalizeForFuzzyMatch, generateDiffString)
6
- * delegate to the native Rust engine for performance on large files.
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 (native Rust implementation).
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;;;;;;GAMG;AAWH,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,CAE3D;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;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,gBAAgB,CAEhF;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,CAMxD;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;AAED;;;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"}
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
- * Hot-path functions (fuzzyFindText, normalizeForFuzzyMatch, generateDiffString)
6
- * delegate to the native Rust engine for performance on large files.
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 (native Rust implementation).
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 nativeNormalizeForFuzzyMatch(text);
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
- return nativeFuzzyFindText(content, oldText);
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 result = nativeGenerateDiff(oldContent, newContent, contextLines);
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: result.diff,
61
- firstChangedLine: result.firstChangedLine ?? undefined,
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=edit-diff.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edit-diff.test.d.ts","sourceRoot":"","sources":["../../../src/core/tools/edit-diff.test.ts"],"names":[],"mappings":""}