gsd-pi 2.16.0 → 2.18.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 (225) hide show
  1. package/README.md +39 -0
  2. package/dist/onboarding.js +2 -2
  3. package/dist/remote-questions-config.d.ts +10 -0
  4. package/dist/remote-questions-config.js +36 -0
  5. package/dist/resources/extensions/gsd/activity-log.ts +37 -7
  6. package/dist/resources/extensions/gsd/auto-dashboard.ts +4 -0
  7. package/dist/resources/extensions/gsd/auto-dispatch.ts +9 -3
  8. package/dist/resources/extensions/gsd/auto-prompts.ts +91 -42
  9. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
  10. package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
  11. package/dist/resources/extensions/gsd/auto.ts +177 -25
  12. package/dist/resources/extensions/gsd/commands.ts +264 -23
  13. package/dist/resources/extensions/gsd/complexity.ts +236 -0
  14. package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
  15. package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  16. package/dist/resources/extensions/gsd/files.ts +129 -3
  17. package/dist/resources/extensions/gsd/git-service.ts +19 -8
  18. package/dist/resources/extensions/gsd/gitignore.ts +41 -2
  19. package/dist/resources/extensions/gsd/guided-flow.ts +247 -10
  20. package/dist/resources/extensions/gsd/index.ts +47 -3
  21. package/dist/resources/extensions/gsd/metrics.ts +44 -0
  22. package/dist/resources/extensions/gsd/native-git-bridge.ts +5 -0
  23. package/dist/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  24. package/dist/resources/extensions/gsd/paths.ts +9 -0
  25. package/dist/resources/extensions/gsd/preferences.ts +181 -2
  26. package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
  27. package/dist/resources/extensions/gsd/prompts/system.md +2 -0
  28. package/dist/resources/extensions/gsd/queue-order.ts +231 -0
  29. package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  30. package/dist/resources/extensions/gsd/routing-history.ts +290 -0
  31. package/dist/resources/extensions/gsd/state.ts +15 -3
  32. package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
  33. package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
  34. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  35. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  36. package/dist/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  37. package/dist/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  38. package/dist/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  39. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  40. package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  41. package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  42. package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  43. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  44. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  45. package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  46. package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  47. package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  48. package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  49. package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  50. package/dist/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  51. package/dist/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  52. package/dist/resources/extensions/gsd/types.ts +28 -0
  53. package/dist/resources/extensions/gsd/worktree-manager.ts +8 -5
  54. package/dist/resources/extensions/gsd/worktree.ts +24 -2
  55. package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
  56. package/package.json +1 -1
  57. package/packages/pi-ai/dist/models.generated.d.ts +493 -13
  58. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  59. package/packages/pi-ai/dist/models.generated.js +422 -62
  60. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  61. package/packages/pi-ai/dist/providers/google-shared.d.ts +12 -0
  62. package/packages/pi-ai/dist/providers/google-shared.d.ts.map +1 -1
  63. package/packages/pi-ai/dist/providers/google-shared.js +9 -22
  64. package/packages/pi-ai/dist/providers/google-shared.js.map +1 -1
  65. package/packages/pi-ai/dist/providers/google-shared.test.d.ts +2 -0
  66. package/packages/pi-ai/dist/providers/google-shared.test.d.ts.map +1 -0
  67. package/packages/pi-ai/dist/providers/google-shared.test.js +125 -0
  68. package/packages/pi-ai/dist/providers/google-shared.test.js.map +1 -0
  69. package/packages/pi-ai/src/models.generated.ts +422 -62
  70. package/packages/pi-ai/src/providers/google-shared.test.ts +137 -0
  71. package/packages/pi-ai/src/providers/google-shared.ts +10 -19
  72. package/packages/pi-coding-agent/dist/cli/args.d.ts +5 -0
  73. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  74. package/packages/pi-coding-agent/dist/cli/args.js +21 -0
  75. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  76. package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
  77. package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
  78. package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
  79. package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
  80. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
  81. package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
  82. package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
  83. package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
  84. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
  85. package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
  86. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
  87. package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
  88. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
  89. package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
  90. package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
  91. package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
  92. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
  93. package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
  94. package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
  95. package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
  96. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
  97. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
  98. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
  99. package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
  100. package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
  101. package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
  103. package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
  105. package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
  106. package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
  107. package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
  108. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
  109. package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
  110. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
  111. package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
  112. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
  113. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
  115. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  117. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  118. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  119. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts +7 -7
  120. package/packages/pi-coding-agent/dist/core/tools/edit-diff.d.ts.map +1 -1
  121. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js +209 -13
  122. package/packages/pi-coding-agent/dist/core/tools/edit-diff.js.map +1 -1
  123. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts +2 -0
  124. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.d.ts.map +1 -0
  125. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js +67 -0
  126. package/packages/pi-coding-agent/dist/core/tools/edit-diff.test.js.map +1 -0
  127. package/packages/pi-coding-agent/dist/index.d.ts +5 -1
  128. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  129. package/packages/pi-coding-agent/dist/index.js +4 -1
  130. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  131. package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
  132. package/packages/pi-coding-agent/dist/main.js +17 -2
  133. package/packages/pi-coding-agent/dist/main.js.map +1 -1
  134. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
  135. package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
  136. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
  137. package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
  138. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
  139. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  140. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
  141. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
  142. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
  143. package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
  144. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
  145. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  146. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
  147. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  148. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  149. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -0
  150. package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
  151. package/packages/pi-coding-agent/src/cli/args.ts +21 -0
  152. package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
  153. package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
  154. package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
  155. package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
  156. package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
  157. package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
  158. package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
  159. package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
  160. package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
  161. package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
  162. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  163. package/packages/pi-coding-agent/src/core/tools/edit-diff.test.ts +85 -0
  164. package/packages/pi-coding-agent/src/core/tools/edit-diff.ts +245 -17
  165. package/packages/pi-coding-agent/src/index.ts +5 -0
  166. package/packages/pi-coding-agent/src/main.ts +19 -2
  167. package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
  168. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
  169. package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
  170. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
  171. package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +13 -0
  172. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  173. package/pkg/dist/modes/interactive/theme/theme.js +10 -0
  174. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
  175. package/src/resources/extensions/gsd/activity-log.ts +37 -7
  176. package/src/resources/extensions/gsd/auto-dashboard.ts +4 -0
  177. package/src/resources/extensions/gsd/auto-dispatch.ts +9 -3
  178. package/src/resources/extensions/gsd/auto-prompts.ts +91 -42
  179. package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
  180. package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
  181. package/src/resources/extensions/gsd/auto.ts +177 -25
  182. package/src/resources/extensions/gsd/commands.ts +264 -23
  183. package/src/resources/extensions/gsd/complexity.ts +236 -0
  184. package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
  185. package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
  186. package/src/resources/extensions/gsd/files.ts +129 -3
  187. package/src/resources/extensions/gsd/git-service.ts +19 -8
  188. package/src/resources/extensions/gsd/gitignore.ts +41 -2
  189. package/src/resources/extensions/gsd/guided-flow.ts +247 -10
  190. package/src/resources/extensions/gsd/index.ts +47 -3
  191. package/src/resources/extensions/gsd/metrics.ts +44 -0
  192. package/src/resources/extensions/gsd/native-git-bridge.ts +5 -0
  193. package/src/resources/extensions/gsd/native-parser-bridge.ts +5 -0
  194. package/src/resources/extensions/gsd/paths.ts +9 -0
  195. package/src/resources/extensions/gsd/preferences.ts +181 -2
  196. package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
  197. package/src/resources/extensions/gsd/prompts/system.md +2 -0
  198. package/src/resources/extensions/gsd/queue-order.ts +231 -0
  199. package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
  200. package/src/resources/extensions/gsd/routing-history.ts +290 -0
  201. package/src/resources/extensions/gsd/state.ts +15 -3
  202. package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
  203. package/src/resources/extensions/gsd/templates/preferences.md +14 -0
  204. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
  205. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -0
  206. package/src/resources/extensions/gsd/tests/budget-prediction.test.ts +220 -0
  207. package/src/resources/extensions/gsd/tests/complexity-routing.test.ts +294 -0
  208. package/src/resources/extensions/gsd/tests/context-compression.test.ts +180 -0
  209. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +99 -0
  210. package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
  211. package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
  212. package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
  213. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
  214. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
  215. package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
  216. package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
  217. package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
  218. package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
  219. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -0
  220. package/src/resources/extensions/gsd/tests/stop-auto-remote.test.ts +130 -0
  221. package/src/resources/extensions/gsd/tests/token-profile.test.ts +263 -0
  222. package/src/resources/extensions/gsd/types.ts +28 -0
  223. package/src/resources/extensions/gsd/worktree-manager.ts +8 -5
  224. package/src/resources/extensions/gsd/worktree.ts +24 -2
  225. package/src/resources/extensions/shared/next-action-ui.ts +16 -1
@@ -0,0 +1,85 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { describe, it } from "node:test";
6
+
7
+ import {
8
+ computeEditDiff,
9
+ fuzzyFindText,
10
+ generateDiffString,
11
+ normalizeForFuzzyMatch,
12
+ } from "./edit-diff.js";
13
+
14
+ describe("edit-diff", () => {
15
+ it("normalizes quotes, dashes, spaces, and trailing whitespace", () => {
16
+ const input = "“hello”\u00A0world — test \nnext\t\t\n";
17
+ assert.equal(normalizeForFuzzyMatch(input), "\"hello\" world - test\nnext\n");
18
+ });
19
+
20
+ it("falls back to fuzzy matching when unicode punctuation differs", () => {
21
+ const result = fuzzyFindText("const title = “Hello”;\n", "const title = \"Hello\";\n");
22
+ assert.equal(result.found, true);
23
+ assert.equal(result.usedFuzzyMatch, true);
24
+ assert.equal(result.contentForReplacement, "const title = \"Hello\";\n");
25
+ });
26
+
27
+ it("renders numbered diffs with the first changed line", () => {
28
+ const result = generateDiffString("line 1\nline 2\nline 3\n", "line 1\nline two\nline 3\n");
29
+ assert.equal(result.firstChangedLine, 2);
30
+ assert.match(result.diff, /-2 line 2/);
31
+ assert.match(result.diff, /\+2 line two/);
32
+ });
33
+
34
+ it("respects contextLines and inserts separators for distant changes", () => {
35
+ const lines = Array.from({ length: 20 }, (_, i) => `line ${i + 1}`);
36
+ const oldContent = lines.join("\n") + "\n";
37
+ const modified = [...lines];
38
+ modified[1] = "changed 2"; // line 2
39
+ modified[17] = "changed 18"; // line 18
40
+ const newContent = modified.join("\n") + "\n";
41
+
42
+ const result = generateDiffString(oldContent, newContent, 2);
43
+ // Should contain separator between the two distant change regions
44
+ assert.match(result.diff, /\.\.\./);
45
+ // Should NOT contain lines far from changes (e.g. line 10)
46
+ assert.doesNotMatch(result.diff, /line 10/);
47
+ // Should contain the changed lines
48
+ assert.match(result.diff, /changed 2/);
49
+ assert.match(result.diff, /changed 18/);
50
+ });
51
+
52
+ it("handles large files without OOM by falling back to linear diff", () => {
53
+ // Create files large enough to exceed the DP threshold
54
+ const lineCount = 3000;
55
+ const oldLines = Array.from({ length: lineCount }, (_, i) => `line ${i}`);
56
+ const newLines = [...oldLines];
57
+ newLines[1500] = "CHANGED";
58
+ const result = generateDiffString(oldLines.join("\n") + "\n", newLines.join("\n") + "\n");
59
+ assert.ok(result.firstChangedLine !== undefined);
60
+ assert.match(result.diff, /CHANGED/);
61
+ });
62
+
63
+ it("computes diffs for preview without native helpers", async () => {
64
+ const dir = mkdtempSync(join(tmpdir(), "edit-diff-test-"));
65
+ try {
66
+ const file = join(dir, "sample.ts");
67
+ writeFileSync(file, "const title = “Hello”;\n", "utf-8");
68
+
69
+ const result = await computeEditDiff(
70
+ file,
71
+ "const title = \"Hello\";\n",
72
+ "const title = \"Hi\";\n",
73
+ dir,
74
+ );
75
+
76
+ assert.ok(!("error" in result), "expected a diff result");
77
+ if (!("error" in result)) {
78
+ assert.equal(result.firstChangedLine, 1);
79
+ assert.match(result.diff, /\+1 const title = "Hi";/);
80
+ }
81
+ } finally {
82
+ rmSync(dir, { recursive: true, force: true });
83
+ }
84
+ });
85
+ });
@@ -2,15 +2,11 @@
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
 
9
- import {
10
- fuzzyFindText as nativeFuzzyFindText,
11
- generateDiff as nativeGenerateDiff,
12
- normalizeForFuzzyMatch as nativeNormalizeForFuzzyMatch,
13
- } from "@gsd/native";
14
10
  import { constants } from "fs";
15
11
  import { access, readFile } from "fs/promises";
16
12
  import { resolveToCwd } from "./path-utils.js";
@@ -32,14 +28,23 @@ export function restoreLineEndings(text: string, ending: "\r\n" | "\n"): string
32
28
  }
33
29
 
34
30
  /**
35
- * Normalize text for fuzzy matching (native Rust implementation).
31
+ * Normalize text for fuzzy matching.
36
32
  * - Strip trailing whitespace from each line
37
33
  * - Normalize smart quotes to ASCII equivalents
38
34
  * - Normalize Unicode dashes/hyphens to ASCII hyphen
39
35
  * - Normalize special Unicode spaces to regular space
40
36
  */
41
37
  export function normalizeForFuzzyMatch(text: string): string {
42
- return nativeNormalizeForFuzzyMatch(text);
38
+ return text
39
+ .replace(/\r\n/g, "\n")
40
+ .replace(/\r/g, "\n")
41
+ .replace(/[“”]/g, '"')
42
+ .replace(/[‘’]/g, "'")
43
+ .replace(/[‐‑‒–—−]/g, "-")
44
+ .replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, " ")
45
+ .split("\n")
46
+ .map((line) => line.replace(/[ \t]+$/g, ""))
47
+ .join("\n");
43
48
  }
44
49
 
45
50
  export interface FuzzyMatchResult {
@@ -59,14 +64,44 @@ export interface FuzzyMatchResult {
59
64
  }
60
65
 
61
66
  /**
62
- * Find oldText in content, trying exact match first, then fuzzy match
63
- * (native Rust implementation).
67
+ * Find oldText in content, trying exact match first, then fuzzy match.
64
68
  *
65
69
  * When fuzzy matching is used, the returned contentForReplacement is the
66
70
  * fuzzy-normalized version of the content.
67
71
  */
68
72
  export function fuzzyFindText(content: string, oldText: string): FuzzyMatchResult {
69
- return nativeFuzzyFindText(content, oldText);
73
+ const exactIndex = content.indexOf(oldText);
74
+ if (exactIndex !== -1) {
75
+ return {
76
+ found: true,
77
+ index: exactIndex,
78
+ matchLength: oldText.length,
79
+ usedFuzzyMatch: false,
80
+ contentForReplacement: content,
81
+ };
82
+ }
83
+
84
+ const normalizedContent = normalizeForFuzzyMatch(content);
85
+ const normalizedOldText = normalizeForFuzzyMatch(oldText);
86
+ const fuzzyIndex = normalizedContent.indexOf(normalizedOldText);
87
+
88
+ if (fuzzyIndex === -1) {
89
+ return {
90
+ found: false,
91
+ index: -1,
92
+ matchLength: 0,
93
+ usedFuzzyMatch: false,
94
+ contentForReplacement: content,
95
+ };
96
+ }
97
+
98
+ return {
99
+ found: true,
100
+ index: fuzzyIndex,
101
+ matchLength: normalizedOldText.length,
102
+ usedFuzzyMatch: true,
103
+ contentForReplacement: normalizedContent,
104
+ };
70
105
  }
71
106
 
72
107
  /** Strip UTF-8 BOM if present, return both the BOM (if any) and the text without it */
@@ -75,20 +110,81 @@ export function stripBom(content: string): { bom: string; text: string } {
75
110
  }
76
111
 
77
112
  /**
78
- * Generate a unified diff string with line numbers and context
79
- * (native Rust implementation using Myers' algorithm via the `similar` crate).
113
+ * Generate a unified diff string with line numbers and context.
80
114
  *
81
115
  * Returns both the diff string and the first changed line number (in the new file).
116
+ * Only lines within `contextLines` of a change are included (like unified diff).
82
117
  */
83
118
  export function generateDiffString(
84
119
  oldContent: string,
85
120
  newContent: string,
86
121
  contextLines = 4,
87
122
  ): { diff: string; firstChangedLine: number | undefined } {
88
- const result = nativeGenerateDiff(oldContent, newContent, contextLines);
123
+ const ops = buildLineDiff(oldContent, newContent);
124
+ let firstChangedLine: number | undefined;
125
+
126
+ // First pass: assign line numbers and find changed indices
127
+ const annotated: { op: LineDiffOp; oldLine: number; newLine: number }[] = [];
128
+ let oldLine = 1;
129
+ let newLine = 1;
130
+ const changedIndices: number[] = [];
131
+
132
+ for (let idx = 0; idx < ops.length; idx++) {
133
+ const op = ops[idx];
134
+ annotated.push({ op, oldLine, newLine });
135
+
136
+ if (op.type !== "context") {
137
+ changedIndices.push(idx);
138
+ if (firstChangedLine === undefined) {
139
+ firstChangedLine = newLine;
140
+ }
141
+ }
142
+
143
+ if (op.type === "remove") {
144
+ oldLine += 1;
145
+ } else if (op.type === "add") {
146
+ newLine += 1;
147
+ } else {
148
+ oldLine += 1;
149
+ newLine += 1;
150
+ }
151
+ }
152
+
153
+ // Build set of indices to include (changes + surrounding context)
154
+ const includeSet = new Set<number>();
155
+ for (const ci of changedIndices) {
156
+ for (let k = Math.max(0, ci - contextLines); k <= Math.min(ops.length - 1, ci + contextLines); k++) {
157
+ includeSet.add(k);
158
+ }
159
+ }
160
+
161
+ const maxLine = Math.max(oldLine - 1, newLine - 1, 1);
162
+ const lineNumberWidth = String(maxLine).length;
163
+ const rendered: string[] = [];
164
+ let lastIncluded = -1;
165
+
166
+ for (let idx = 0; idx < annotated.length; idx++) {
167
+ if (!includeSet.has(idx)) continue;
168
+
169
+ // Insert separator when there's a gap between included regions
170
+ if (lastIncluded !== -1 && idx > lastIncluded + 1) {
171
+ rendered.push("...");
172
+ }
173
+ lastIncluded = idx;
174
+
175
+ const { op, oldLine: ol, newLine: nl } = annotated[idx];
176
+ if (op.type === "context") {
177
+ rendered.push(` ${String(nl).padStart(lineNumberWidth, " ")} ${op.line}`);
178
+ } else if (op.type === "remove") {
179
+ rendered.push(`-${String(ol).padStart(lineNumberWidth, " ")} ${op.line}`);
180
+ } else {
181
+ rendered.push(`+${String(nl).padStart(lineNumberWidth, " ")} ${op.line}`);
182
+ }
183
+ }
184
+
89
185
  return {
90
- diff: result.diff,
91
- firstChangedLine: result.firstChangedLine ?? undefined,
186
+ diff: rendered.join("\n"),
187
+ firstChangedLine,
92
188
  };
93
189
  }
94
190
 
@@ -101,6 +197,138 @@ export interface EditDiffError {
101
197
  error: string;
102
198
  }
103
199
 
200
+ type LineDiffOp =
201
+ | { type: "context"; line: string }
202
+ | { type: "remove"; line: string }
203
+ | { type: "add"; line: string };
204
+
205
+ function splitLines(text: string): string[] {
206
+ const lines = text.split("\n");
207
+ if (lines.length > 0 && lines.at(-1) === "") {
208
+ lines.pop();
209
+ }
210
+ return lines;
211
+ }
212
+
213
+ /**
214
+ * Maximum number of cells (oldLines * newLines) before we switch from the
215
+ * full LCS DP algorithm to a simpler linear-scan diff. This prevents OOM
216
+ * on large files (e.g. 10k lines would need a 100M-cell matrix).
217
+ */
218
+ const MAX_DP_CELLS = 4_000_000; // ~32 MB for 64-bit numbers
219
+
220
+ function buildLineDiff(oldContent: string, newContent: string): LineDiffOp[] {
221
+ const oldLines = splitLines(oldContent);
222
+ const newLines = splitLines(newContent);
223
+
224
+ const cells = (oldLines.length + 1) * (newLines.length + 1);
225
+ if (cells > MAX_DP_CELLS) {
226
+ return buildLineDiffLinear(oldLines, newLines);
227
+ }
228
+
229
+ return buildLineDiffLCS(oldLines, newLines);
230
+ }
231
+
232
+ /**
233
+ * Full LCS-based diff using O(n*m) DP table. Produces optimal diffs but
234
+ * is only safe for files where n*m <= MAX_DP_CELLS.
235
+ */
236
+ function buildLineDiffLCS(oldLines: string[], newLines: string[]): LineDiffOp[] {
237
+ const dp: number[][] = Array.from({ length: oldLines.length + 1 }, () =>
238
+ Array<number>(newLines.length + 1).fill(0),
239
+ );
240
+
241
+ for (let i = oldLines.length - 1; i >= 0; i--) {
242
+ for (let j = newLines.length - 1; j >= 0; j--) {
243
+ if (oldLines[i] === newLines[j]) {
244
+ dp[i][j] = dp[i + 1][j + 1] + 1;
245
+ } else {
246
+ dp[i][j] = Math.max(dp[i + 1][j], dp[i][j + 1]);
247
+ }
248
+ }
249
+ }
250
+
251
+ const ops: LineDiffOp[] = [];
252
+ let i = 0;
253
+ let j = 0;
254
+
255
+ while (i < oldLines.length && j < newLines.length) {
256
+ if (oldLines[i] === newLines[j]) {
257
+ ops.push({ type: "context", line: oldLines[i] });
258
+ i += 1;
259
+ j += 1;
260
+ continue;
261
+ }
262
+
263
+ if (dp[i + 1][j] >= dp[i][j + 1]) {
264
+ ops.push({ type: "remove", line: oldLines[i] });
265
+ i += 1;
266
+ } else {
267
+ ops.push({ type: "add", line: newLines[j] });
268
+ j += 1;
269
+ }
270
+ }
271
+
272
+ while (i < oldLines.length) {
273
+ ops.push({ type: "remove", line: oldLines[i] });
274
+ i += 1;
275
+ }
276
+
277
+ while (j < newLines.length) {
278
+ ops.push({ type: "add", line: newLines[j] });
279
+ j += 1;
280
+ }
281
+
282
+ return ops;
283
+ }
284
+
285
+ /**
286
+ * Linear-time fallback diff for large files. Matches common prefix/suffix,
287
+ * then treats the remaining middle as a bulk remove+add. Not optimal but
288
+ * O(n+m) in both time and space.
289
+ */
290
+ function buildLineDiffLinear(oldLines: string[], newLines: string[]): LineDiffOp[] {
291
+ const ops: LineDiffOp[] = [];
292
+
293
+ // Match common prefix
294
+ let prefixLen = 0;
295
+ const minLen = Math.min(oldLines.length, newLines.length);
296
+ while (prefixLen < minLen && oldLines[prefixLen] === newLines[prefixLen]) {
297
+ prefixLen++;
298
+ }
299
+
300
+ // Match common suffix (not overlapping with prefix)
301
+ let suffixLen = 0;
302
+ while (
303
+ suffixLen < minLen - prefixLen &&
304
+ oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]
305
+ ) {
306
+ suffixLen++;
307
+ }
308
+
309
+ // Emit prefix context
310
+ for (let i = 0; i < prefixLen; i++) {
311
+ ops.push({ type: "context", line: oldLines[i] });
312
+ }
313
+
314
+ // Emit removed lines from the middle
315
+ for (let i = prefixLen; i < oldLines.length - suffixLen; i++) {
316
+ ops.push({ type: "remove", line: oldLines[i] });
317
+ }
318
+
319
+ // Emit added lines from the middle
320
+ for (let j = prefixLen; j < newLines.length - suffixLen; j++) {
321
+ ops.push({ type: "add", line: newLines[j] });
322
+ }
323
+
324
+ // Emit suffix context
325
+ for (let i = oldLines.length - suffixLen; i < oldLines.length; i++) {
326
+ ops.push({ type: "context", line: oldLines[i] });
327
+ }
328
+
329
+ return ops;
330
+ }
331
+
104
332
  /**
105
333
  * Compute the diff for an edit operation without applying it.
106
334
  * Used for preview rendering in the TUI before the tool executes.
@@ -143,7 +143,11 @@ export {
143
143
  // Footer data provider (git branch + extension statuses - data not otherwise available to extensions)
144
144
  export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.js";
145
145
  export { convertToLlm } from "./core/messages.js";
146
+ export { ModelDiscoveryCache } from "./core/discovery-cache.js";
147
+ export type { DiscoveredModel, DiscoveryResult, ProviderDiscoveryAdapter } from "./core/model-discovery.js";
148
+ export { getDiscoverableProviders, getDiscoveryAdapter } from "./core/model-discovery.js";
146
149
  export { ModelRegistry } from "./core/model-registry.js";
150
+ export { ModelsJsonWriter } from "./core/models-json-writer.js";
147
151
  export type {
148
152
  PackageManager,
149
153
  PathMetadata,
@@ -307,6 +311,7 @@ export {
307
311
  LoginDialogComponent,
308
312
  ModelSelectorComponent,
309
313
  OAuthSelectorComponent,
314
+ ProviderManagerComponent,
310
315
  type RenderDiffOptions,
311
316
  rawKeyHint,
312
317
  renderDiff,
@@ -11,7 +11,7 @@ import { createInterface } from "readline";
11
11
  import { type Args, parseArgs, printHelp } from "./cli/args.js";
12
12
  import { selectConfig } from "./cli/config-selector.js";
13
13
  import { processFileArguments } from "./cli/file-processor.js";
14
- import { listModels } from "./cli/list-models.js";
14
+ import { discoverAndPrintModels, listModels } from "./cli/list-models.js";
15
15
  import { selectSession } from "./cli/session-picker.js";
16
16
  import { APP_NAME, getAgentDir, getModelsPath, VERSION } from "./config.js";
17
17
  import { AuthStorage } from "./core/auth-storage.js";
@@ -660,9 +660,26 @@ export async function main(args: string[]) {
660
660
  process.exit(0);
661
661
  }
662
662
 
663
+ if (parsed.addProvider) {
664
+ const { ModelsJsonWriter } = await import("./core/models-json-writer.js");
665
+ const writer = new ModelsJsonWriter();
666
+ writer.setProvider(parsed.addProvider, {
667
+ baseUrl: parsed.addProviderBaseUrl,
668
+ apiKey: parsed.apiKey,
669
+ });
670
+ console.log(`Provider "${parsed.addProvider}" added to models.json`);
671
+ process.exit(0);
672
+ }
673
+
674
+ if (parsed.discoverModels !== undefined) {
675
+ const provider = typeof parsed.discoverModels === "string" ? parsed.discoverModels : undefined;
676
+ await discoverAndPrintModels(modelRegistry, provider);
677
+ process.exit(0);
678
+ }
679
+
663
680
  if (parsed.listModels !== undefined) {
664
681
  const searchPattern = typeof parsed.listModels === "string" ? parsed.listModels : undefined;
665
- await listModels(modelRegistry, searchPattern);
682
+ await listModels(modelRegistry, { searchPattern, discover: parsed.discover });
666
683
  process.exit(0);
667
684
  }
668
685
 
@@ -18,6 +18,7 @@ export { appKey, appKeyHint, editorKey, keyHint, rawKeyHint } from "./keybinding
18
18
  export { LoginDialogComponent } from "./login-dialog.js";
19
19
  export { ModelSelectorComponent } from "./model-selector.js";
20
20
  export { OAuthSelectorComponent } from "./oauth-selector.js";
21
+ export { ProviderManagerComponent } from "./provider-manager.js";
21
22
  export { type ModelsCallbacks, type ModelsConfig, ScopedModelsSelectorComponent } from "./scoped-models-selector.js";
22
23
  export { SessionSelectorComponent } from "./session-selector.js";
23
24
  export { type SettingsCallbacks, type SettingsConfig, SettingsSelectorComponent } from "./settings-selector.js";
@@ -160,7 +160,7 @@ export class ModelSelectorComponent extends Container implements Focusable {
160
160
 
161
161
  // Load available models (built-in models still work even if models.json failed)
162
162
  try {
163
- const availableModels = await this.modelRegistry.getAvailable();
163
+ const availableModels = this.modelRegistry.getAvailable();
164
164
  models = availableModels.map((model: Model<any>) => ({
165
165
  provider: model.provider,
166
166
  id: model.id,
@@ -0,0 +1,163 @@
1
+ /**
2
+ * TUI component for managing provider configurations.
3
+ * Shows providers with auth status, discovery support, and model counts.
4
+ */
5
+
6
+ import {
7
+ Container,
8
+ type Focusable,
9
+ getEditorKeybindings,
10
+ Spacer,
11
+ Text,
12
+ type TUI,
13
+ } from "@gsd/pi-tui";
14
+ import type { AuthStorage } from "../../../core/auth-storage.js";
15
+ import { getDiscoverableProviders } from "../../../core/model-discovery.js";
16
+ import type { ModelRegistry } from "../../../core/model-registry.js";
17
+ import { theme } from "../theme/theme.js";
18
+ import { rawKeyHint } from "./keybinding-hints.js";
19
+
20
+ interface ProviderInfo {
21
+ name: string;
22
+ hasAuth: boolean;
23
+ supportsDiscovery: boolean;
24
+ modelCount: number;
25
+ }
26
+
27
+ export class ProviderManagerComponent extends Container implements Focusable {
28
+ private _focused = false;
29
+ get focused(): boolean {
30
+ return this._focused;
31
+ }
32
+ set focused(value: boolean) {
33
+ this._focused = value;
34
+ }
35
+
36
+ private providers: ProviderInfo[] = [];
37
+ private selectedIndex = 0;
38
+ private listContainer: Container;
39
+ private tui: TUI;
40
+ private authStorage: AuthStorage;
41
+ private modelRegistry: ModelRegistry;
42
+ private onDone: () => void;
43
+ private onDiscover: (provider: string) => void;
44
+
45
+ constructor(
46
+ tui: TUI,
47
+ authStorage: AuthStorage,
48
+ modelRegistry: ModelRegistry,
49
+ onDone: () => void,
50
+ onDiscover: (provider: string) => void,
51
+ ) {
52
+ super();
53
+
54
+ this.tui = tui;
55
+ this.authStorage = authStorage;
56
+ this.modelRegistry = modelRegistry;
57
+ this.onDone = onDone;
58
+ this.onDiscover = onDiscover;
59
+
60
+ // Header
61
+ this.addChild(new Text(theme.fg("accent", "Provider Manager"), 0, 0));
62
+ this.addChild(new Spacer(1));
63
+
64
+ // Hints
65
+ const hints = [
66
+ rawKeyHint("d", "discover"),
67
+ rawKeyHint("r", "remove auth"),
68
+ rawKeyHint("esc", "close"),
69
+ ].join(" ");
70
+ this.addChild(new Text(hints, 0, 0));
71
+ this.addChild(new Spacer(1));
72
+
73
+ // List
74
+ this.listContainer = new Container();
75
+ this.addChild(this.listContainer);
76
+
77
+ this.loadProviders();
78
+ this.updateList();
79
+ }
80
+
81
+ private loadProviders(): void {
82
+ const discoverableSet = new Set(getDiscoverableProviders());
83
+ const allModels = this.modelRegistry.getAll();
84
+
85
+ // Group models by provider
86
+ const providerModelCounts = new Map<string, number>();
87
+ for (const model of allModels) {
88
+ providerModelCounts.set(model.provider, (providerModelCounts.get(model.provider) ?? 0) + 1);
89
+ }
90
+
91
+ // Build provider list from all known providers
92
+ const providerNames = new Set([
93
+ ...providerModelCounts.keys(),
94
+ ...discoverableSet,
95
+ ]);
96
+
97
+ this.providers = Array.from(providerNames)
98
+ .sort()
99
+ .map((name) => ({
100
+ name,
101
+ hasAuth: this.authStorage.hasAuth(name),
102
+ supportsDiscovery: discoverableSet.has(name),
103
+ modelCount: providerModelCounts.get(name) ?? 0,
104
+ }));
105
+ }
106
+
107
+ private updateList(): void {
108
+ this.listContainer.clear();
109
+
110
+ for (let i = 0; i < this.providers.length; i++) {
111
+ const p = this.providers[i];
112
+ const isSelected = i === this.selectedIndex;
113
+
114
+ const authBadge = p.hasAuth ? theme.fg("success", "[auth]") : theme.fg("muted", "[no auth]");
115
+ const discoveryBadge = p.supportsDiscovery ? theme.fg("accent", "[discovery]") : "";
116
+ const countBadge = theme.fg("muted", `(${p.modelCount} models)`);
117
+
118
+ const prefix = isSelected ? theme.fg("accent", "> ") : " ";
119
+ const nameText = isSelected ? theme.fg("accent", p.name) : p.name;
120
+
121
+ const parts = [prefix, nameText, " ", authBadge];
122
+ if (discoveryBadge) parts.push(" ", discoveryBadge);
123
+ parts.push(" ", countBadge);
124
+
125
+ this.listContainer.addChild(new Text(parts.join(""), 0, 0));
126
+ }
127
+
128
+ if (this.providers.length === 0) {
129
+ this.listContainer.addChild(new Text(theme.fg("muted", " No providers configured"), 0, 0));
130
+ }
131
+ }
132
+
133
+ handleInput(keyData: string): void {
134
+ const kb = getEditorKeybindings();
135
+
136
+ if (kb.matches(keyData, "selectUp")) {
137
+ if (this.providers.length === 0) return;
138
+ this.selectedIndex = this.selectedIndex === 0 ? this.providers.length - 1 : this.selectedIndex - 1;
139
+ this.updateList();
140
+ this.tui.requestRender();
141
+ } else if (kb.matches(keyData, "selectDown")) {
142
+ if (this.providers.length === 0) return;
143
+ this.selectedIndex = this.selectedIndex === this.providers.length - 1 ? 0 : this.selectedIndex + 1;
144
+ this.updateList();
145
+ this.tui.requestRender();
146
+ } else if (kb.matches(keyData, "selectCancel")) {
147
+ this.onDone();
148
+ } else if (keyData === "d" || keyData === "D") {
149
+ const provider = this.providers[this.selectedIndex];
150
+ if (provider?.supportsDiscovery) {
151
+ this.onDiscover(provider.name);
152
+ }
153
+ } else if (keyData === "r" || keyData === "R") {
154
+ const provider = this.providers[this.selectedIndex];
155
+ if (provider?.hasAuth) {
156
+ this.authStorage.remove(provider.name);
157
+ this.loadProviders();
158
+ this.updateList();
159
+ this.tui.requestRender();
160
+ }
161
+ }
162
+ }
163
+ }