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.
- package/README.md +39 -0
- package/dist/onboarding.js +2 -2
- package/dist/remote-questions-config.d.ts +10 -0
- package/dist/remote-questions-config.js +36 -0
- package/dist/resources/extensions/gsd/activity-log.ts +37 -7
- 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 +91 -42
- package/dist/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/dist/resources/extensions/gsd/auto.ts +177 -25
- package/dist/resources/extensions/gsd/commands.ts +264 -23
- package/dist/resources/extensions/gsd/complexity.ts +236 -0
- package/dist/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/dist/resources/extensions/gsd/files.ts +129 -3
- 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 +247 -10
- package/dist/resources/extensions/gsd/index.ts +47 -3
- 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/paths.ts +9 -0
- package/dist/resources/extensions/gsd/preferences.ts +181 -2
- package/dist/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/dist/resources/extensions/gsd/prompts/system.md +2 -0
- package/dist/resources/extensions/gsd/queue-order.ts +231 -0
- package/dist/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/dist/resources/extensions/gsd/routing-history.ts +290 -0
- package/dist/resources/extensions/gsd/state.ts +15 -3
- package/dist/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +14 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -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/derive-state-deps.test.ts +99 -0
- package/dist/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/dist/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/dist/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/dist/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/dist/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/dist/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/dist/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -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-manager.ts +8 -5
- package/dist/resources/extensions/gsd/worktree.ts +24 -2
- package/dist/resources/extensions/shared/next-action-ui.ts +16 -1
- 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/cli/args.d.ts +5 -0
- package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/args.js +21 -0
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts +14 -3
- package/packages/pi-coding-agent/dist/cli/list-models.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/cli/list-models.js +52 -17
- package/packages/pi-coding-agent/dist/cli/list-models.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts +27 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js +79 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js +140 -0
- package/packages/pi-coding-agent/dist/core/discovery-cache.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts +35 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js +162 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js +100 -0
- package/packages/pi-coding-agent/dist/core/model-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js +113 -0
- package/packages/pi-coding-agent/dist/core/model-registry-discovery.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts +26 -0
- package/packages/pi-coding-agent/dist/core/model-registry.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/model-registry.js +98 -0
- package/packages/pi-coding-agent/dist/core/model-registry.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts +62 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js +145 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js +118 -0
- package/packages/pi-coding-agent/dist/core/models-json-writer.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +9 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +11 -0
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
- package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
- 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/index.d.ts +5 -1
- package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/index.js +4 -1
- package/packages/pi-coding-agent/dist/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/main.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/main.js +17 -2
- package/packages/pi-coding-agent/dist/main.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts +25 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js +121 -0
- package/packages/pi-coding-agent/dist/modes/interactive/components/provider-manager.js.map +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +32 -0
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- 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/cli/args.ts +21 -0
- package/packages/pi-coding-agent/src/cli/list-models.ts +70 -17
- package/packages/pi-coding-agent/src/core/discovery-cache.test.ts +170 -0
- package/packages/pi-coding-agent/src/core/discovery-cache.ts +97 -0
- package/packages/pi-coding-agent/src/core/model-discovery.test.ts +125 -0
- package/packages/pi-coding-agent/src/core/model-discovery.ts +231 -0
- package/packages/pi-coding-agent/src/core/model-registry-discovery.test.ts +135 -0
- package/packages/pi-coding-agent/src/core/model-registry.ts +107 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.test.ts +145 -0
- package/packages/pi-coding-agent/src/core/models-json-writer.ts +188 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +21 -0
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
- 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/index.ts +5 -0
- package/packages/pi-coding-agent/src/main.ts +19 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/index.ts +1 -0
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/provider-manager.ts +163 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +37 -0
- 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/activity-log.ts +37 -7
- 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 +91 -42
- package/src/resources/extensions/gsd/auto-recovery.ts +7 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +33 -4
- package/src/resources/extensions/gsd/auto.ts +177 -25
- package/src/resources/extensions/gsd/commands.ts +264 -23
- package/src/resources/extensions/gsd/complexity.ts +236 -0
- package/src/resources/extensions/gsd/dispatch-guard.ts +7 -19
- package/src/resources/extensions/gsd/docs/preferences-reference.md +202 -2
- package/src/resources/extensions/gsd/files.ts +129 -3
- 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 +247 -10
- package/src/resources/extensions/gsd/index.ts +47 -3
- 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/paths.ts +9 -0
- package/src/resources/extensions/gsd/preferences.ts +181 -2
- package/src/resources/extensions/gsd/prompts/execute-task.md +6 -5
- package/src/resources/extensions/gsd/prompts/system.md +2 -0
- package/src/resources/extensions/gsd/queue-order.ts +231 -0
- package/src/resources/extensions/gsd/queue-reorder-ui.ts +263 -0
- package/src/resources/extensions/gsd/routing-history.ts +290 -0
- package/src/resources/extensions/gsd/state.ts +15 -3
- package/src/resources/extensions/gsd/templates/knowledge.md +19 -0
- package/src/resources/extensions/gsd/templates/preferences.md +14 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +50 -0
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +20 -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/derive-state-deps.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/git-service.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/in-flight-tool-tracking.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +161 -0
- package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/preferences-git.test.ts +28 -0
- package/src/resources/extensions/gsd/tests/preferences-wizard-fields.test.ts +168 -0
- package/src/resources/extensions/gsd/tests/queue-order.test.ts +204 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +281 -0
- package/src/resources/extensions/gsd/tests/routing-history.test.ts +87 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +139 -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-manager.ts +8 -5
- package/src/resources/extensions/gsd/worktree.ts +24 -2
- 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
|
-
*
|
|
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
|
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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:
|
|
91
|
-
firstChangedLine
|
|
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 =
|
|
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
|
+
}
|