From f31cadb7a468182733bff094916cdd03f7800c9d Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 6 Aug 2026 03:51:56 -0400 Subject: [PATCH 1/4] feat: streamline OpenCode provider connections --- apps/ade-cli/package-lock.json | 229 +++--- apps/ade-cli/package.json | 4 +- .../tools/toolsManifest.generated.json | 30 +- apps/desktop/package-lock.json | 121 +-- apps/desktop/package.json | 4 +- .../opencode/openCodeBinaryManager.test.ts | 13 +- .../opencode/openCodeBinaryManager.ts | 7 +- .../settings/OAuthConnectModal.test.tsx | 45 +- .../components/settings/OAuthConnectModal.tsx | 185 ++++- .../settings/OpenCodeProviderDetailModal.tsx | 423 ++++++++++ .../settings/ProvidersSection.test.tsx | 116 ++- .../components/settings/ProvidersSection.tsx | 739 +++++++----------- .../components/shared/ProviderLogos.tsx | 17 +- .../src/renderer/lib/lobeProviderIconSrc.ts | 4 + .../agent-tools-cache.md | 13 +- 15 files changed, 1252 insertions(+), 698 deletions(-) create mode 100644 apps/desktop/src/renderer/components/settings/OpenCodeProviderDetailModal.tsx diff --git a/apps/ade-cli/package-lock.json b/apps/ade-cli/package-lock.json index 3ba9ccdb27..511afd7b8d 100644 --- a/apps/ade-cli/package-lock.json +++ b/apps/ade-cli/package-lock.json @@ -14,7 +14,7 @@ "@linear/sdk": "^84.0.0", "@modelcontextprotocol/sdk": "^1.29.0", "@openai/codex": "0.144.5", - "@opencode-ai/sdk": "1.15.5", + "@opencode-ai/sdk": "1.18.14", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-serialize": "^0.14.0", "@xterm/headless": "^6.0.0", @@ -28,7 +28,7 @@ "marked": "^18.0.3", "node-cron": "^3.0.3", "node-pty": "^1.1.0", - "opencode-ai": "1.15.5", + "opencode-ai": "1.18.14", "react": "^19.2.7", "string-width": "^4.2.3", "ws": "^8.20.0", @@ -1129,9 +1129,9 @@ } }, "node_modules/@opencode-ai/sdk": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.5.tgz", - "integrity": "sha512-ozJuEmXzrOvia5n0L1KAuvpyf9ESGmTk1FiPhn0RK5X1whbzjlTXL0NAxqNCEkqETxL35jS1KHArEiTpvtJ6FQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.14.tgz", + "integrity": "sha512-Yd8vPDT5DW++Hs2904Q/Vvk/m1U79aKFSRusgAfBQ+AGhfoT40LdeL5eiJtVy0xBUIZF/8DEZjkWaeof9Y59PA==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" @@ -3278,9 +3278,9 @@ } }, "node_modules/opencode-ai": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.15.5.tgz", - "integrity": "sha512-ud/0sYo9h2BJALwLudRrzs551YJoi+rHo66jEsSLdOBv5RJxmN64aqqGaafhWxvgtaHyEOqfKnZPyx9GVKl/UA==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.18.14.tgz", + "integrity": "sha512-E5son8EQkh+cQ3bYkeGCOSNTcx8wmgsHBZ3bJjY0rOA84JaRb3VvqkfT41A8ChgEzwbSNe7uzEHDKqJOkNMynQ==", "cpu": [ "arm64", "x64" @@ -3296,24 +3296,24 @@ "opencode": "bin/opencode.exe" }, "optionalDependencies": { - "opencode-darwin-arm64": "1.15.5", - "opencode-darwin-x64": "1.15.5", - "opencode-darwin-x64-baseline": "1.15.5", - "opencode-linux-arm64": "1.15.5", - "opencode-linux-arm64-musl": "1.15.5", - "opencode-linux-x64": "1.15.5", - "opencode-linux-x64-baseline": "1.15.5", - "opencode-linux-x64-baseline-musl": "1.15.5", - "opencode-linux-x64-musl": "1.15.5", - "opencode-windows-arm64": "1.15.5", - "opencode-windows-x64": "1.15.5", - "opencode-windows-x64-baseline": "1.15.5" + "opencode-darwin-arm64": "1.18.14", + "opencode-darwin-x64": "1.18.14", + "opencode-darwin-x64-baseline": "1.18.14", + "opencode-linux-arm64": "1.18.14", + "opencode-linux-arm64-musl": "1.18.14", + "opencode-linux-x64": "1.18.14", + "opencode-linux-x64-baseline": "1.18.14", + "opencode-linux-x64-baseline-musl": "1.18.14", + "opencode-linux-x64-musl": "1.18.14", + "opencode-windows-arm64": "1.18.14", + "opencode-windows-x64": "1.18.14", + "opencode-windows-x64-baseline": "1.18.14" } }, "node_modules/opencode-darwin-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.15.5.tgz", - "integrity": "sha512-gv8o3WbbW33SbmJvx/enDuZewlIoWfduSFo8M/WqlRwwO//33Rs3bgSnY21a6DYtXFL7NFzXO7QOmbPY5y02Jg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.18.14.tgz", + "integrity": "sha512-xlzzkkvIE5mnB/s9xYbrY+N7d4K4iFclNp6hT9YIWEOgfJSESkPhCrmfy3qMrAGV+Ufof+QaZPSCMyngt/741Q==", "cpu": [ "arm64" ], @@ -3323,9 +3323,9 @@ ] }, "node_modules/opencode-darwin-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.15.5.tgz", - "integrity": "sha512-yuglSeJbTF4jRHmFgqteQUMCRLjVOQ1aqMaeevDNyJXOUZPWCdFG1h8Fq8dZBgBfPfBEpQNFEWv8D9DU3ycytg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.18.14.tgz", + "integrity": "sha512-HHPgOBWT3/1axMLU45z9p4g6STErwzSKJmH8w4h2RiFCii8F8cGCljZbFyLeWWQ1y3YHWl62r3ZFq1T69DasVg==", "cpu": [ "x64" ], @@ -3335,9 +3335,9 @@ ] }, "node_modules/opencode-darwin-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.15.5.tgz", - "integrity": "sha512-ROtf2AO2jXcH95280hJoVF+84kB7IGYrUb7WPG4jKYescf5a6CTq28Pr+l5csM0iBXHFR21qGS8ezZegI5K70A==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.18.14.tgz", + "integrity": "sha512-XE9hjQPztLrfcQsML0mi87rBrBii+BrK6HBz2cBCpCLRbDb0UtPX1YQqAPA4qCSZl6v6VEyolj6KvyjhGSpZUg==", "cpu": [ "x64" ], @@ -3347,9 +3347,9 @@ ] }, "node_modules/opencode-linux-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.15.5.tgz", - "integrity": "sha512-y2K5gT0IPWXx5DSy9hw+h0vSTahbdEV8spK/oZ4iwCY/Px/wdVpk5IphgkHcEes/9fIR1oLmu0/HF/SHhWRMxw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.18.14.tgz", + "integrity": "sha512-LSILNOuBO5Lotcky2XcvpAVxj/abRcWynKdTcX1QX0+bVOCEsY/fb0tGUuJFafVzWsmjFS7QF3vyD9mnrMC7Qg==", "cpu": [ "arm64" ], @@ -3359,21 +3359,24 @@ ] }, "node_modules/opencode-linux-arm64-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.15.5.tgz", - "integrity": "sha512-oBu8lSERTw6yX0umyBGzCiS9FLVpWnaHAwk7WFvN7FY9LsTEh4NfgiMdKBVtPmn14RwaWK8AHtjueJ2UvIMzlw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.18.14.tgz", + "integrity": "sha512-kc4c+jw97MLFkoLEnSSQjol9QbrHwYagk9IWSuH8GhF5dhJ5PtIT0ghQ3QT6PcWAxaUSh3aP3pt9hg6YRNVoow==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "optional": true, "os": [ "linux" ] }, "node_modules/opencode-linux-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.15.5.tgz", - "integrity": "sha512-Y3arLZezt8a6Ed2fdwrD21+JSQeNCZmfqHqYCv5iMzcpkE8/a8TLkDQVre74zjA7Gxelyk2sjk4b6ZKMuWHIkw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.14.tgz", + "integrity": "sha512-SMZj5jyOoOsZ6Cp2jD8hZbxRpw71OkyMDq02ZIfEFj/MxzX5xulIeC3XedFaATvAjKMlnfq5A9MFssExcrwegg==", "cpu": [ "x64" ], @@ -3383,9 +3386,9 @@ ] }, "node_modules/opencode-linux-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.15.5.tgz", - "integrity": "sha512-WgyIU/uAYxE4kTOWWmiaJ07hT8wLmlNMG0l9EjpeW70G8USn8EfCVZStGsKnOIMdb2KAX+b6phnC1ihROcDCvw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.18.14.tgz", + "integrity": "sha512-80cqcKMNWS1PQb+g0fud6H/j+0+MNv8OzyQwDDm+ZphFZW5v/LnfzXtCiWyqxp+vJeqlWjIn+I2p/Qr8YhbugA==", "cpu": [ "x64" ], @@ -3395,33 +3398,39 @@ ] }, "node_modules/opencode-linux-x64-baseline-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.15.5.tgz", - "integrity": "sha512-EZ3sela0UkWV+jwE2gi7I89hhHFRUigUN36md1k3G45zM5epsrLnJYFbSk8bnMFfy5t32AsLufOw5dnlkfsYTw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.18.14.tgz", + "integrity": "sha512-8cSbF24sqAtlHmW0ndtuGPlg2/hquyTpSOWlOtdgdFHPBrzRIwE2gq4U9Ce1fbPNNEy09r/ZsEDjYaVQuDeMkA==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "optional": true, "os": [ "linux" ] }, "node_modules/opencode-linux-x64-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.15.5.tgz", - "integrity": "sha512-+RJ4AKfozICakQAOGRjaYob+uWiXI8owbsMq3KoIyHKpZsDTtQ3lvs22nVcq8cBPMJMdFbgWwHt9k9kVYBJ+jA==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.18.14.tgz", + "integrity": "sha512-PIJZzMMvfkweEl0S6ZE2SND9E8vZYz8jFIlTUmdioGl56tV3Z/dAsMhShC6/aBef61wxoGghQHo4/I4tqKQz4g==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "optional": true, "os": [ "linux" ] }, "node_modules/opencode-windows-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.15.5.tgz", - "integrity": "sha512-uFtZyOYzV8nChWDy2IC0PnLFxj2MVfDRfJBr3CMqDnrlUS+B3DYwHkiCsbCsS/pHXpl/QbfpkAq+LSZp7mzgIQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.18.14.tgz", + "integrity": "sha512-lLf1fMhrBg0tdQOBvWGpwbMCXS+EEqAYdikhjGZz16SVMGAQNNemdPEvqi2M2BMEEUpvVneBS546TvXB+N52DA==", "cpu": [ "arm64" ], @@ -3431,9 +3440,9 @@ ] }, "node_modules/opencode-windows-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.15.5.tgz", - "integrity": "sha512-gOtx2ap7UOJFohM1lkbkuRcXJ6MVyPX9XE5umkItAET6wcxkncqr3lcV4SqN2dU7xfqISJS6mNKEQbO7H3xyjQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.18.14.tgz", + "integrity": "sha512-hhsDHc4dZcuxSUi4yhGxzrFPdV6fCKNrQVp6nRzxcVstcIyJ3Xl+wKcxw5JagKOgpNFJ4mVoNh9lJ+7hrsKPZA==", "cpu": [ "x64" ], @@ -3443,9 +3452,9 @@ ] }, "node_modules/opencode-windows-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.15.5.tgz", - "integrity": "sha512-6oIoyY+ZSwsTsG2tekTZ67DKLsdRrywLECZWrJW/5Lj5aaibAc0uaQxIvNXKHbpznUU/Iaz+tgQ9D6o0OISIXg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.18.14.tgz", + "integrity": "sha512-39Y0oLPpzVBdyILqWnWHl15ruQpsMH7muI6IA/LIbJRwMaSimsMKt2v/ItefFXz3tRHqdxVTPf2RsYClyWBLng==", "cpu": [ "x64" ], @@ -5871,9 +5880,9 @@ "optional": true }, "@opencode-ai/sdk": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.5.tgz", - "integrity": "sha512-ozJuEmXzrOvia5n0L1KAuvpyf9ESGmTk1FiPhn0RK5X1whbzjlTXL0NAxqNCEkqETxL35jS1KHArEiTpvtJ6FQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.14.tgz", + "integrity": "sha512-Yd8vPDT5DW++Hs2904Q/Vvk/m1U79aKFSRusgAfBQ+AGhfoT40LdeL5eiJtVy0xBUIZF/8DEZjkWaeof9Y59PA==", "requires": { "cross-spawn": "7.0.6" } @@ -7221,94 +7230,94 @@ } }, "opencode-ai": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.15.5.tgz", - "integrity": "sha512-ud/0sYo9h2BJALwLudRrzs551YJoi+rHo66jEsSLdOBv5RJxmN64aqqGaafhWxvgtaHyEOqfKnZPyx9GVKl/UA==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.18.14.tgz", + "integrity": "sha512-E5son8EQkh+cQ3bYkeGCOSNTcx8wmgsHBZ3bJjY0rOA84JaRb3VvqkfT41A8ChgEzwbSNe7uzEHDKqJOkNMynQ==", "requires": { - "opencode-darwin-arm64": "1.15.5", - "opencode-darwin-x64": "1.15.5", - "opencode-darwin-x64-baseline": "1.15.5", - "opencode-linux-arm64": "1.15.5", - "opencode-linux-arm64-musl": "1.15.5", - "opencode-linux-x64": "1.15.5", - "opencode-linux-x64-baseline": "1.15.5", - "opencode-linux-x64-baseline-musl": "1.15.5", - "opencode-linux-x64-musl": "1.15.5", - "opencode-windows-arm64": "1.15.5", - "opencode-windows-x64": "1.15.5", - "opencode-windows-x64-baseline": "1.15.5" + "opencode-darwin-arm64": "1.18.14", + "opencode-darwin-x64": "1.18.14", + "opencode-darwin-x64-baseline": "1.18.14", + "opencode-linux-arm64": "1.18.14", + "opencode-linux-arm64-musl": "1.18.14", + "opencode-linux-x64": "1.18.14", + "opencode-linux-x64-baseline": "1.18.14", + "opencode-linux-x64-baseline-musl": "1.18.14", + "opencode-linux-x64-musl": "1.18.14", + "opencode-windows-arm64": "1.18.14", + "opencode-windows-x64": "1.18.14", + "opencode-windows-x64-baseline": "1.18.14" } }, "opencode-darwin-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.15.5.tgz", - "integrity": "sha512-gv8o3WbbW33SbmJvx/enDuZewlIoWfduSFo8M/WqlRwwO//33Rs3bgSnY21a6DYtXFL7NFzXO7QOmbPY5y02Jg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.18.14.tgz", + "integrity": "sha512-xlzzkkvIE5mnB/s9xYbrY+N7d4K4iFclNp6hT9YIWEOgfJSESkPhCrmfy3qMrAGV+Ufof+QaZPSCMyngt/741Q==", "optional": true }, "opencode-darwin-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.15.5.tgz", - "integrity": "sha512-yuglSeJbTF4jRHmFgqteQUMCRLjVOQ1aqMaeevDNyJXOUZPWCdFG1h8Fq8dZBgBfPfBEpQNFEWv8D9DU3ycytg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.18.14.tgz", + "integrity": "sha512-HHPgOBWT3/1axMLU45z9p4g6STErwzSKJmH8w4h2RiFCii8F8cGCljZbFyLeWWQ1y3YHWl62r3ZFq1T69DasVg==", "optional": true }, "opencode-darwin-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.15.5.tgz", - "integrity": "sha512-ROtf2AO2jXcH95280hJoVF+84kB7IGYrUb7WPG4jKYescf5a6CTq28Pr+l5csM0iBXHFR21qGS8ezZegI5K70A==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.18.14.tgz", + "integrity": "sha512-XE9hjQPztLrfcQsML0mi87rBrBii+BrK6HBz2cBCpCLRbDb0UtPX1YQqAPA4qCSZl6v6VEyolj6KvyjhGSpZUg==", "optional": true }, "opencode-linux-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.15.5.tgz", - "integrity": "sha512-y2K5gT0IPWXx5DSy9hw+h0vSTahbdEV8spK/oZ4iwCY/Px/wdVpk5IphgkHcEes/9fIR1oLmu0/HF/SHhWRMxw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.18.14.tgz", + "integrity": "sha512-LSILNOuBO5Lotcky2XcvpAVxj/abRcWynKdTcX1QX0+bVOCEsY/fb0tGUuJFafVzWsmjFS7QF3vyD9mnrMC7Qg==", "optional": true }, "opencode-linux-arm64-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.15.5.tgz", - "integrity": "sha512-oBu8lSERTw6yX0umyBGzCiS9FLVpWnaHAwk7WFvN7FY9LsTEh4NfgiMdKBVtPmn14RwaWK8AHtjueJ2UvIMzlw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.18.14.tgz", + "integrity": "sha512-kc4c+jw97MLFkoLEnSSQjol9QbrHwYagk9IWSuH8GhF5dhJ5PtIT0ghQ3QT6PcWAxaUSh3aP3pt9hg6YRNVoow==", "optional": true }, "opencode-linux-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.15.5.tgz", - "integrity": "sha512-Y3arLZezt8a6Ed2fdwrD21+JSQeNCZmfqHqYCv5iMzcpkE8/a8TLkDQVre74zjA7Gxelyk2sjk4b6ZKMuWHIkw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.14.tgz", + "integrity": "sha512-SMZj5jyOoOsZ6Cp2jD8hZbxRpw71OkyMDq02ZIfEFj/MxzX5xulIeC3XedFaATvAjKMlnfq5A9MFssExcrwegg==", "optional": true }, "opencode-linux-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.15.5.tgz", - "integrity": "sha512-WgyIU/uAYxE4kTOWWmiaJ07hT8wLmlNMG0l9EjpeW70G8USn8EfCVZStGsKnOIMdb2KAX+b6phnC1ihROcDCvw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.18.14.tgz", + "integrity": "sha512-80cqcKMNWS1PQb+g0fud6H/j+0+MNv8OzyQwDDm+ZphFZW5v/LnfzXtCiWyqxp+vJeqlWjIn+I2p/Qr8YhbugA==", "optional": true }, "opencode-linux-x64-baseline-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.15.5.tgz", - "integrity": "sha512-EZ3sela0UkWV+jwE2gi7I89hhHFRUigUN36md1k3G45zM5epsrLnJYFbSk8bnMFfy5t32AsLufOw5dnlkfsYTw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.18.14.tgz", + "integrity": "sha512-8cSbF24sqAtlHmW0ndtuGPlg2/hquyTpSOWlOtdgdFHPBrzRIwE2gq4U9Ce1fbPNNEy09r/ZsEDjYaVQuDeMkA==", "optional": true }, "opencode-linux-x64-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.15.5.tgz", - "integrity": "sha512-+RJ4AKfozICakQAOGRjaYob+uWiXI8owbsMq3KoIyHKpZsDTtQ3lvs22nVcq8cBPMJMdFbgWwHt9k9kVYBJ+jA==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.18.14.tgz", + "integrity": "sha512-PIJZzMMvfkweEl0S6ZE2SND9E8vZYz8jFIlTUmdioGl56tV3Z/dAsMhShC6/aBef61wxoGghQHo4/I4tqKQz4g==", "optional": true }, "opencode-windows-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.15.5.tgz", - "integrity": "sha512-uFtZyOYzV8nChWDy2IC0PnLFxj2MVfDRfJBr3CMqDnrlUS+B3DYwHkiCsbCsS/pHXpl/QbfpkAq+LSZp7mzgIQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.18.14.tgz", + "integrity": "sha512-lLf1fMhrBg0tdQOBvWGpwbMCXS+EEqAYdikhjGZz16SVMGAQNNemdPEvqi2M2BMEEUpvVneBS546TvXB+N52DA==", "optional": true }, "opencode-windows-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.15.5.tgz", - "integrity": "sha512-gOtx2ap7UOJFohM1lkbkuRcXJ6MVyPX9XE5umkItAET6wcxkncqr3lcV4SqN2dU7xfqISJS6mNKEQbO7H3xyjQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.18.14.tgz", + "integrity": "sha512-hhsDHc4dZcuxSUi4yhGxzrFPdV6fCKNrQVp6nRzxcVstcIyJ3Xl+wKcxw5JagKOgpNFJ4mVoNh9lJ+7hrsKPZA==", "optional": true }, "opencode-windows-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.15.5.tgz", - "integrity": "sha512-6oIoyY+ZSwsTsG2tekTZ67DKLsdRrywLECZWrJW/5Lj5aaibAc0uaQxIvNXKHbpznUU/Iaz+tgQ9D6o0OISIXg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.18.14.tgz", + "integrity": "sha512-39Y0oLPpzVBdyILqWnWHl15ruQpsMH7muI6IA/LIbJRwMaSimsMKt2v/ItefFXz3tRHqdxVTPf2RsYClyWBLng==", "optional": true }, "p-limit": { diff --git a/apps/ade-cli/package.json b/apps/ade-cli/package.json index 3c625c1a5b..ee0e7c4b5a 100644 --- a/apps/ade-cli/package.json +++ b/apps/ade-cli/package.json @@ -32,7 +32,7 @@ "@linear/sdk": "^84.0.0", "@modelcontextprotocol/sdk": "^1.29.0", "@openai/codex": "0.144.5", - "@opencode-ai/sdk": "1.15.5", + "@opencode-ai/sdk": "1.18.14", "@xterm/addon-fit": "^0.11.0", "@xterm/addon-serialize": "^0.14.0", "@xterm/headless": "^6.0.0", @@ -46,7 +46,7 @@ "marked": "^18.0.3", "node-cron": "^3.0.3", "node-pty": "^1.1.0", - "opencode-ai": "1.15.5", + "opencode-ai": "1.18.14", "react": "^19.2.7", "string-width": "^4.2.3", "ws": "^8.20.0", diff --git a/apps/ade-cli/src/services/tools/toolsManifest.generated.json b/apps/ade-cli/src/services/tools/toolsManifest.generated.json index 768fda8b8c..cb5c364f47 100644 --- a/apps/ade-cli/src/services/tools/toolsManifest.generated.json +++ b/apps/ade-cli/src/services/tools/toolsManifest.generated.json @@ -95,33 +95,33 @@ "targets": { "darwin-arm64": { "package": "opencode-darwin-arm64", - "version": "1.15.5", - "tarball": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.15.5.tgz", - "integrity": "sha512-gv8o3WbbW33SbmJvx/enDuZewlIoWfduSFo8M/WqlRwwO//33Rs3bgSnY21a6DYtXFL7NFzXO7QOmbPY5y02Jg==" + "version": "1.18.14", + "tarball": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.18.14.tgz", + "integrity": "sha512-xlzzkkvIE5mnB/s9xYbrY+N7d4K4iFclNp6hT9YIWEOgfJSESkPhCrmfy3qMrAGV+Ufof+QaZPSCMyngt/741Q==" }, "darwin-x64": { "package": "opencode-darwin-x64", - "version": "1.15.5", - "tarball": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.15.5.tgz", - "integrity": "sha512-yuglSeJbTF4jRHmFgqteQUMCRLjVOQ1aqMaeevDNyJXOUZPWCdFG1h8Fq8dZBgBfPfBEpQNFEWv8D9DU3ycytg==" + "version": "1.18.14", + "tarball": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.18.14.tgz", + "integrity": "sha512-HHPgOBWT3/1axMLU45z9p4g6STErwzSKJmH8w4h2RiFCii8F8cGCljZbFyLeWWQ1y3YHWl62r3ZFq1T69DasVg==" }, "linux-arm64": { "package": "opencode-linux-arm64", - "version": "1.15.5", - "tarball": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.15.5.tgz", - "integrity": "sha512-y2K5gT0IPWXx5DSy9hw+h0vSTahbdEV8spK/oZ4iwCY/Px/wdVpk5IphgkHcEes/9fIR1oLmu0/HF/SHhWRMxw==" + "version": "1.18.14", + "tarball": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.18.14.tgz", + "integrity": "sha512-LSILNOuBO5Lotcky2XcvpAVxj/abRcWynKdTcX1QX0+bVOCEsY/fb0tGUuJFafVzWsmjFS7QF3vyD9mnrMC7Qg==" }, "linux-x64": { "package": "opencode-linux-x64", - "version": "1.15.5", - "tarball": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.15.5.tgz", - "integrity": "sha512-Y3arLZezt8a6Ed2fdwrD21+JSQeNCZmfqHqYCv5iMzcpkE8/a8TLkDQVre74zjA7Gxelyk2sjk4b6ZKMuWHIkw==" + "version": "1.18.14", + "tarball": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.14.tgz", + "integrity": "sha512-SMZj5jyOoOsZ6Cp2jD8hZbxRpw71OkyMDq02ZIfEFj/MxzX5xulIeC3XedFaATvAjKMlnfq5A9MFssExcrwegg==" }, "win32-x64": { "package": "opencode-windows-x64-baseline", - "version": "1.15.5", - "tarball": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.15.5.tgz", - "integrity": "sha512-6oIoyY+ZSwsTsG2tekTZ67DKLsdRrywLECZWrJW/5Lj5aaibAc0uaQxIvNXKHbpznUU/Iaz+tgQ9D6o0OISIXg==" + "version": "1.18.14", + "tarball": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.18.14.tgz", + "integrity": "sha512-39Y0oLPpzVBdyILqWnWHl15ruQpsMH7muI6IA/LIbJRwMaSimsMKt2v/ItefFXz3tRHqdxVTPf2RsYClyWBLng==" } } } diff --git a/apps/desktop/package-lock.json b/apps/desktop/package-lock.json index 1267186948..8782e51a4a 100644 --- a/apps/desktop/package-lock.json +++ b/apps/desktop/package-lock.json @@ -15,7 +15,7 @@ "@linear/sdk": "^84.0.0", "@modelcontextprotocol/sdk": "^1.29.0", "@openai/codex": "0.144.5", - "@opencode-ai/sdk": "1.15.5", + "@opencode-ai/sdk": "1.18.14", "@xterm/addon-serialize": "^0.14.0", "@xterm/headless": "^6.0.0", "bonjour-service": "^1.3.0", @@ -24,7 +24,7 @@ "graceful-fs": "^4.2.11", "node-cron": "^3.0.3", "node-pty": "^1.1.0", - "opencode-ai": "1.15.5", + "opencode-ai": "1.18.14", "ssh2": "^1.17.0", "ws": "^8.19.0", "yaml": "^2.8.2", @@ -3512,9 +3512,9 @@ } }, "node_modules/@opencode-ai/sdk": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.5.tgz", - "integrity": "sha512-ozJuEmXzrOvia5n0L1KAuvpyf9ESGmTk1FiPhn0RK5X1whbzjlTXL0NAxqNCEkqETxL35jS1KHArEiTpvtJ6FQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.14.tgz", + "integrity": "sha512-Yd8vPDT5DW++Hs2904Q/Vvk/m1U79aKFSRusgAfBQ+AGhfoT40LdeL5eiJtVy0xBUIZF/8DEZjkWaeof9Y59PA==", "license": "MIT", "dependencies": { "cross-spawn": "7.0.6" @@ -16770,9 +16770,9 @@ } }, "node_modules/opencode-ai": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.15.5.tgz", - "integrity": "sha512-ud/0sYo9h2BJALwLudRrzs551YJoi+rHo66jEsSLdOBv5RJxmN64aqqGaafhWxvgtaHyEOqfKnZPyx9GVKl/UA==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-ai/-/opencode-ai-1.18.14.tgz", + "integrity": "sha512-E5son8EQkh+cQ3bYkeGCOSNTcx8wmgsHBZ3bJjY0rOA84JaRb3VvqkfT41A8ChgEzwbSNe7uzEHDKqJOkNMynQ==", "cpu": [ "arm64", "x64" @@ -16788,24 +16788,24 @@ "opencode": "bin/opencode.exe" }, "optionalDependencies": { - "opencode-darwin-arm64": "1.15.5", - "opencode-darwin-x64": "1.15.5", - "opencode-darwin-x64-baseline": "1.15.5", - "opencode-linux-arm64": "1.15.5", - "opencode-linux-arm64-musl": "1.15.5", - "opencode-linux-x64": "1.15.5", - "opencode-linux-x64-baseline": "1.15.5", - "opencode-linux-x64-baseline-musl": "1.15.5", - "opencode-linux-x64-musl": "1.15.5", - "opencode-windows-arm64": "1.15.5", - "opencode-windows-x64": "1.15.5", - "opencode-windows-x64-baseline": "1.15.5" + "opencode-darwin-arm64": "1.18.14", + "opencode-darwin-x64": "1.18.14", + "opencode-darwin-x64-baseline": "1.18.14", + "opencode-linux-arm64": "1.18.14", + "opencode-linux-arm64-musl": "1.18.14", + "opencode-linux-x64": "1.18.14", + "opencode-linux-x64-baseline": "1.18.14", + "opencode-linux-x64-baseline-musl": "1.18.14", + "opencode-linux-x64-musl": "1.18.14", + "opencode-windows-arm64": "1.18.14", + "opencode-windows-x64": "1.18.14", + "opencode-windows-x64-baseline": "1.18.14" } }, "node_modules/opencode-darwin-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.15.5.tgz", - "integrity": "sha512-gv8o3WbbW33SbmJvx/enDuZewlIoWfduSFo8M/WqlRwwO//33Rs3bgSnY21a6DYtXFL7NFzXO7QOmbPY5y02Jg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-arm64/-/opencode-darwin-arm64-1.18.14.tgz", + "integrity": "sha512-xlzzkkvIE5mnB/s9xYbrY+N7d4K4iFclNp6hT9YIWEOgfJSESkPhCrmfy3qMrAGV+Ufof+QaZPSCMyngt/741Q==", "cpu": [ "arm64" ], @@ -16815,9 +16815,9 @@ ] }, "node_modules/opencode-darwin-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.15.5.tgz", - "integrity": "sha512-yuglSeJbTF4jRHmFgqteQUMCRLjVOQ1aqMaeevDNyJXOUZPWCdFG1h8Fq8dZBgBfPfBEpQNFEWv8D9DU3ycytg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-x64/-/opencode-darwin-x64-1.18.14.tgz", + "integrity": "sha512-HHPgOBWT3/1axMLU45z9p4g6STErwzSKJmH8w4h2RiFCii8F8cGCljZbFyLeWWQ1y3YHWl62r3ZFq1T69DasVg==", "cpu": [ "x64" ], @@ -16827,9 +16827,9 @@ ] }, "node_modules/opencode-darwin-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.15.5.tgz", - "integrity": "sha512-ROtf2AO2jXcH95280hJoVF+84kB7IGYrUb7WPG4jKYescf5a6CTq28Pr+l5csM0iBXHFR21qGS8ezZegI5K70A==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-darwin-x64-baseline/-/opencode-darwin-x64-baseline-1.18.14.tgz", + "integrity": "sha512-XE9hjQPztLrfcQsML0mi87rBrBii+BrK6HBz2cBCpCLRbDb0UtPX1YQqAPA4qCSZl6v6VEyolj6KvyjhGSpZUg==", "cpu": [ "x64" ], @@ -16839,9 +16839,9 @@ ] }, "node_modules/opencode-linux-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.15.5.tgz", - "integrity": "sha512-y2K5gT0IPWXx5DSy9hw+h0vSTahbdEV8spK/oZ4iwCY/Px/wdVpk5IphgkHcEes/9fIR1oLmu0/HF/SHhWRMxw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-arm64/-/opencode-linux-arm64-1.18.14.tgz", + "integrity": "sha512-LSILNOuBO5Lotcky2XcvpAVxj/abRcWynKdTcX1QX0+bVOCEsY/fb0tGUuJFafVzWsmjFS7QF3vyD9mnrMC7Qg==", "cpu": [ "arm64" ], @@ -16851,21 +16851,24 @@ ] }, "node_modules/opencode-linux-arm64-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.15.5.tgz", - "integrity": "sha512-oBu8lSERTw6yX0umyBGzCiS9FLVpWnaHAwk7WFvN7FY9LsTEh4NfgiMdKBVtPmn14RwaWK8AHtjueJ2UvIMzlw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-arm64-musl/-/opencode-linux-arm64-musl-1.18.14.tgz", + "integrity": "sha512-kc4c+jw97MLFkoLEnSSQjol9QbrHwYagk9IWSuH8GhF5dhJ5PtIT0ghQ3QT6PcWAxaUSh3aP3pt9hg6YRNVoow==", "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "optional": true, "os": [ "linux" ] }, "node_modules/opencode-linux-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.15.5.tgz", - "integrity": "sha512-Y3arLZezt8a6Ed2fdwrD21+JSQeNCZmfqHqYCv5iMzcpkE8/a8TLkDQVre74zjA7Gxelyk2sjk4b6ZKMuWHIkw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64/-/opencode-linux-x64-1.18.14.tgz", + "integrity": "sha512-SMZj5jyOoOsZ6Cp2jD8hZbxRpw71OkyMDq02ZIfEFj/MxzX5xulIeC3XedFaATvAjKMlnfq5A9MFssExcrwegg==", "cpu": [ "x64" ], @@ -16875,9 +16878,9 @@ ] }, "node_modules/opencode-linux-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.15.5.tgz", - "integrity": "sha512-WgyIU/uAYxE4kTOWWmiaJ07hT8wLmlNMG0l9EjpeW70G8USn8EfCVZStGsKnOIMdb2KAX+b6phnC1ihROcDCvw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline/-/opencode-linux-x64-baseline-1.18.14.tgz", + "integrity": "sha512-80cqcKMNWS1PQb+g0fud6H/j+0+MNv8OzyQwDDm+ZphFZW5v/LnfzXtCiWyqxp+vJeqlWjIn+I2p/Qr8YhbugA==", "cpu": [ "x64" ], @@ -16887,33 +16890,39 @@ ] }, "node_modules/opencode-linux-x64-baseline-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.15.5.tgz", - "integrity": "sha512-EZ3sela0UkWV+jwE2gi7I89hhHFRUigUN36md1k3G45zM5epsrLnJYFbSk8bnMFfy5t32AsLufOw5dnlkfsYTw==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-baseline-musl/-/opencode-linux-x64-baseline-musl-1.18.14.tgz", + "integrity": "sha512-8cSbF24sqAtlHmW0ndtuGPlg2/hquyTpSOWlOtdgdFHPBrzRIwE2gq4U9Ce1fbPNNEy09r/ZsEDjYaVQuDeMkA==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "optional": true, "os": [ "linux" ] }, "node_modules/opencode-linux-x64-musl": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.15.5.tgz", - "integrity": "sha512-+RJ4AKfozICakQAOGRjaYob+uWiXI8owbsMq3KoIyHKpZsDTtQ3lvs22nVcq8cBPMJMdFbgWwHt9k9kVYBJ+jA==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-linux-x64-musl/-/opencode-linux-x64-musl-1.18.14.tgz", + "integrity": "sha512-PIJZzMMvfkweEl0S6ZE2SND9E8vZYz8jFIlTUmdioGl56tV3Z/dAsMhShC6/aBef61wxoGghQHo4/I4tqKQz4g==", "cpu": [ "x64" ], + "libc": [ + "musl" + ], "optional": true, "os": [ "linux" ] }, "node_modules/opencode-windows-arm64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.15.5.tgz", - "integrity": "sha512-uFtZyOYzV8nChWDy2IC0PnLFxj2MVfDRfJBr3CMqDnrlUS+B3DYwHkiCsbCsS/pHXpl/QbfpkAq+LSZp7mzgIQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-arm64/-/opencode-windows-arm64-1.18.14.tgz", + "integrity": "sha512-lLf1fMhrBg0tdQOBvWGpwbMCXS+EEqAYdikhjGZz16SVMGAQNNemdPEvqi2M2BMEEUpvVneBS546TvXB+N52DA==", "cpu": [ "arm64" ], @@ -16923,9 +16932,9 @@ ] }, "node_modules/opencode-windows-x64": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.15.5.tgz", - "integrity": "sha512-gOtx2ap7UOJFohM1lkbkuRcXJ6MVyPX9XE5umkItAET6wcxkncqr3lcV4SqN2dU7xfqISJS6mNKEQbO7H3xyjQ==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-x64/-/opencode-windows-x64-1.18.14.tgz", + "integrity": "sha512-hhsDHc4dZcuxSUi4yhGxzrFPdV6fCKNrQVp6nRzxcVstcIyJ3Xl+wKcxw5JagKOgpNFJ4mVoNh9lJ+7hrsKPZA==", "cpu": [ "x64" ], @@ -16935,9 +16944,9 @@ ] }, "node_modules/opencode-windows-x64-baseline": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.15.5.tgz", - "integrity": "sha512-6oIoyY+ZSwsTsG2tekTZ67DKLsdRrywLECZWrJW/5Lj5aaibAc0uaQxIvNXKHbpznUU/Iaz+tgQ9D6o0OISIXg==", + "version": "1.18.14", + "resolved": "https://registry.npmjs.org/opencode-windows-x64-baseline/-/opencode-windows-x64-baseline-1.18.14.tgz", + "integrity": "sha512-39Y0oLPpzVBdyILqWnWHl15ruQpsMH7muI6IA/LIbJRwMaSimsMKt2v/ItefFXz3tRHqdxVTPf2RsYClyWBLng==", "cpu": [ "x64" ], diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ddbbd0d2d7..68ade8337f 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -78,7 +78,7 @@ "@linear/sdk": "^84.0.0", "@modelcontextprotocol/sdk": "^1.29.0", "@openai/codex": "0.144.5", - "@opencode-ai/sdk": "1.15.5", + "@opencode-ai/sdk": "1.18.14", "@xterm/addon-serialize": "^0.14.0", "@xterm/headless": "^6.0.0", "bonjour-service": "^1.3.0", @@ -87,7 +87,7 @@ "graceful-fs": "^4.2.11", "node-cron": "^3.0.3", "node-pty": "^1.1.0", - "opencode-ai": "1.15.5", + "opencode-ai": "1.18.14", "ssh2": "^1.17.0", "ws": "^8.19.0", "yaml": "^2.8.2", diff --git a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts index 7a150cebf3..2884718990 100644 --- a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts +++ b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import type * as ChildProcess from "node:child_process"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const childProcessMockState = vi.hoisted(() => ({ @@ -8,7 +9,7 @@ const childProcessMockState = vi.hoisted(() => ({ })); vi.mock("node:child_process", async () => { - const actual = await vi.importActual("node:child_process"); + const actual = await vi.importActual("node:child_process"); return { ...actual, execFileSync: (...args: unknown[]) => childProcessMockState.execFileSync(...args), @@ -30,6 +31,7 @@ const originalEnv = { SHELL: process.env.SHELL, }; const originalProcessPlatform = process.platform; +const originalProcessArch = process.arch; function setProcessPlatform(platform: NodeJS.Platform): void { Object.defineProperty(process, "platform", { @@ -38,6 +40,13 @@ function setProcessPlatform(platform: NodeJS.Platform): void { }); } +function setProcessArch(arch: NodeJS.Architecture): void { + Object.defineProperty(process, "arch", { + value: arch, + configurable: true, + }); +} + function makeExecutable(filePath: string): void { fs.mkdirSync(path.dirname(filePath), { recursive: true }); fs.writeFileSync(filePath, "#!/bin/sh\nexit 0\n", "utf8"); @@ -89,6 +98,7 @@ describe("openCodeBinaryManager", () => { afterEach(() => { setProcessPlatform(originalProcessPlatform); + setProcessArch(originalProcessArch); clearOpenCodeBinaryCache(); vi.restoreAllMocks(); restoreEnv("ADE_DISABLE_BUNDLED_OPENCODE"); @@ -146,6 +156,7 @@ describe("openCodeBinaryManager", () => { // package plus an `opencode-ai` shell whose bin/ has been pruned. The AVX2 // `opencode-windows-x64` package is deliberately absent from the installer. setProcessPlatform("win32"); + setProcessArch("x64"); process.env.ADE_OPENCODE_BUNDLE_ROOT = tempRoot; const baselinePath = path.join( tempRoot, "node_modules", "opencode-windows-x64-baseline", "bin", "opencode.exe", diff --git a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts index 5b27aea9c2..af9a4e67f3 100644 --- a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts +++ b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts @@ -61,7 +61,10 @@ function collectBundledNodeModulesRoots(env: NodeJS.ProcessEnv): string[] { const roots: string[] = []; const explicitRoot = env.ADE_OPENCODE_BUNDLE_ROOT?.trim(); if (explicitRoot) { - roots.push(explicitRoot.endsWith("node_modules") ? explicitRoot : join(explicitRoot, "node_modules")); + // Treat the explicit root as an override. This keeps development/test + // runtimes hermetic and prevents a stale checkout-local package from + // winning over the requested bundle. + return [explicitRoot.endsWith("node_modules") ? explicitRoot : join(explicitRoot, "node_modules")]; } const processWithResources = process as NodeJS.Process & { resourcesPath?: string }; @@ -110,7 +113,7 @@ function bundledBinaryCandidatePaths(args?: { // Legacy packaged fallback for builds that copied the binary directly into // Resources/. New builds resolve the pinned npm runtime package below. const resourcesPath = (process as any).resourcesPath; - if (resourcesPath) { + if (resourcesPath && !env.ADE_OPENCODE_BUNDLE_ROOT?.trim()) { candidates.push(...fileNames.map((fileName) => join(resourcesPath, fileName))); } diff --git a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx index 7764553501..a84d76b43b 100644 --- a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx +++ b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ import React from "react"; -import { act, cleanup, render, screen } from "@testing-library/react"; +import { act, cleanup, fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { OAuthConnectModal } from "./OAuthConnectModal"; @@ -9,10 +9,11 @@ describe("OAuthConnectModal", () => { const originalAde = globalThis.window.ade; beforeEach(() => { + localStorage.clear(); globalThis.window.ade = { ai: { opencodeOAuthStart: vi.fn().mockResolvedValue({ - url: "http://example.com/oauth", + url: "https://example.com/oauth", method: "auto", instructions: "Sign in to continue.", }), @@ -22,6 +23,9 @@ describe("OAuthConnectModal", () => { builtInBrowser: { navigate: vi.fn().mockResolvedValue(undefined), }, + app: { + openExternal: vi.fn().mockResolvedValue(undefined), + }, } as any; }); @@ -30,13 +34,26 @@ describe("OAuthConnectModal", () => { globalThis.window.ade = originalAde; }); - it("opens a safe OAuth URL once in the ADE browser", async () => { - window.ade.ai.opencodeOAuthStart = vi.fn().mockResolvedValue({ - url: "https://example.com/oauth", - method: "auto", - instructions: "Sign in to continue.", + it("opens a safe OAuth URL in the system browser by default", async () => { + render( + , + ); + + await act(async () => { + screen.getByRole("button", { name: "Connect" }).click(); }); + expect(window.ade.app.openExternal).toHaveBeenCalledWith("https://example.com/oauth"); + expect(window.ade.builtInBrowser.navigate).not.toHaveBeenCalled(); + }); + + it("opens the ADE browser when that open target is selected", async () => { render( { />, ); + fireEvent.change(screen.getByLabelText("Open sign-in link in"), { + target: { value: "ade-browser" }, + }); + await act(async () => { screen.getByRole("button", { name: "Connect" }).click(); }); - expect(window.ade.builtInBrowser.navigate).toHaveBeenCalledTimes(1); expect(window.ade.builtInBrowser.navigate).toHaveBeenCalledWith({ url: "https://example.com/oauth", newTab: true, }); }); - it("rejects an unsafe OAuth URL before opening it in the ADE browser", async () => { + it("rejects an unsafe OAuth URL before opening it", async () => { + window.ade.ai.opencodeOAuthStart = vi.fn().mockResolvedValue({ + url: "http://example.com/oauth", + method: "auto", + instructions: "Sign in to continue.", + }); + render( { "OpenCode returned an unsafe OAuth URL.", ); expect(window.ade.builtInBrowser.navigate).not.toHaveBeenCalled(); + expect(window.ade.app.openExternal).not.toHaveBeenCalled(); expect(window.ade.ai.opencodeOAuthCancel).toHaveBeenCalledWith({ providerId: "openai" }); }); }); diff --git a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx index 4669c912f4..a49a7db864 100644 --- a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx +++ b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx @@ -8,12 +8,41 @@ import type { OpenCodeOAuthStatusEvent, } from "../../../shared/types/config"; import { isAllowedOpenCodeOAuthUrl } from "../../../shared/opencodeOAuth"; -import { openUrlInAdeBrowser } from "../../lib/openExternal"; +import { openExternalUrl, openUrlInAdeBrowser } from "../../lib/openExternal"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; import { ProviderLogo } from "../shared/ProviderLogos"; import { COLORS, MONO_FONT, SANS_FONT, outlineButton, primaryButton } from "../lanes/laneDesignTokens"; const CODE_PATTERN = /[A-Z0-9]{4,}-[A-Z0-9]{4,}/; +const OPEN_TARGET_STORAGE_KEY = "ade.opencode.oauthOpenTarget"; + +/** How the OAuth URL should be delivered to the user. */ +export type OAuthOpenTarget = "ade-browser" | "system" | "copy" | "view"; + +const OPEN_TARGET_OPTIONS: Array<{ id: OAuthOpenTarget; label: string }> = [ + { id: "system", label: "System browser" }, + { id: "ade-browser", label: "ADE browser" }, + { id: "copy", label: "Copy link" }, + { id: "view", label: "View full link" }, +]; + +function readStoredOpenTarget(): OAuthOpenTarget { + try { + const raw = localStorage.getItem(OPEN_TARGET_STORAGE_KEY); + if (raw === "ade-browser" || raw === "system" || raw === "copy" || raw === "view") return raw; + } catch { + // ignore + } + return "system"; +} + +function writeStoredOpenTarget(target: OAuthOpenTarget): void { + try { + localStorage.setItem(OPEN_TARGET_STORAGE_KEY, target); + } catch { + // ignore + } +} /** Pull a human-typeable device code out of the CLI instructions blob. */ function extractDeviceCode(instructions: string | undefined): string | null { @@ -50,6 +79,27 @@ function buildPromptDefaults(prompts: OpenCodeProviderAuthPrompt[]): Record void, +): void { + if (!url) return; + switch (target) { + case "ade-browser": + openUrlInAdeBrowser(url); + return; + case "system": + openExternalUrl(url); + return; + case "copy": + onCopy(url); + return; + case "view": + return; + } +} + type Phase = "form" | "starting" | "waiting" | "error"; export function OAuthConnectModal({ @@ -75,10 +125,18 @@ export function OAuthConnectModal({ const prompts = method?.prompts ?? []; const [inputs, setInputs] = useState>(() => buildPromptDefaults(prompts)); + const [openTarget, setOpenTarget] = useState(() => readStoredOpenTarget()); const [phase, setPhase] = useState("form"); const [startResult, setStartResult] = useState(null); const [errorMessage, setErrorMessage] = useState(null); - const { copy, copied } = useCopyToClipboard(); + const [showFullUrl, setShowFullUrl] = useState(false); + const { copy, isCopied } = useCopyToClipboard(); + const dialogRef = useRef(null); + + const copyText = async (text: string, kind: "url" | "code") => { + if (!text) return; + await copy(text, kind); + }; const visiblePrompts = prompts.filter((prompt) => promptVisible(prompt, inputs)); const deviceCode = extractDeviceCode(startResult?.instructions); @@ -135,11 +193,24 @@ export function OAuthConnectModal({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + // Focus the dialog on open so keyboard focus is not stuck under the overlay. + useEffect(() => { + const node = dialogRef.current; + if (!node) return; + const previous = document.activeElement as HTMLElement | null; + node.focus(); + return () => { + previous?.focus?.(); + }; + }, []); + const startFlow = async () => { cancelRequestedRef.current = false; startPendingRef.current = true; + writeStoredOpenTarget(openTarget); setPhase("starting"); setErrorMessage(null); + setShowFullUrl(openTarget === "view"); try { const filteredInputs: Record = {}; for (const prompt of visiblePrompts) { @@ -159,7 +230,11 @@ export function OAuthConnectModal({ await window.ade.ai.opencodeOAuthCancel({ providerId }).catch(() => undefined); throw new Error("OpenCode returned an unsafe OAuth URL."); } - openUrlInAdeBrowser(result.url); + if (result.url) { + applyOpenTarget(result.url, openTarget, (url) => { + void copyText(url, "url"); + }); + } setStartResult(result); flowActiveRef.current = true; setPhase("waiting"); @@ -185,7 +260,12 @@ export function OAuthConnectModal({ const copyCode = async () => { const text = deviceCode ?? startResult?.instructions ?? ""; if (!text) return; - await copy(text); + await copyText(text, "code"); + }; + + const copyUrl = async () => { + if (!startResult?.url) return; + await copyText(startResult.url, "url"); }; const labelStyle: React.CSSProperties = { @@ -206,16 +286,32 @@ export function OAuthConnectModal({ outline: "none", }; + const waitingHint = + openTarget === "ade-browser" + ? host + ? `We opened ${host} in ADE’s browser.` + : "We opened ADE’s browser." + : openTarget === "system" + ? host + ? `We opened ${host} in your system browser.` + : "We opened your system browser." + : openTarget === "copy" + ? "Sign-in link copied — paste it where you can complete login." + : "Use the full link below to finish signing in."; + return createPortal(
void handleCancel()} >
+ Open sign-in link in + + +
- We'll open your browser to finish signing in with {providerName}. + OpenCode completes sign-in on the machine running the runtime. This dialog closes automatically after approval.
@@ -347,7 +459,7 @@ export function OAuthConnectModal({ disabled={phase === "starting"} onClick={() => void startFlow()} > - {phase === "starting" ? "Opening…" : "Connect"} + {phase === "starting" ? "Starting…" : "Connect"}
@@ -356,7 +468,7 @@ export function OAuthConnectModal({ {phase === "waiting" ? ( <>
- {host ? `We opened ${host} in your browser.` : "We opened your browser."} + {waitingHint}
{deviceCode ? ( @@ -396,9 +508,41 @@ export function OAuthConnectModal({ ) : null} + {startResult?.url ? ( +
+
+ Sign-in link + +
+ {showFullUrl || openTarget === "view" ? ( + + {startResult.url} + + ) : null} +
+ ) : null} +
  1. - Approve the request in the page we opened. + Approve the request in the sign-in page.
  2. {deviceCode ? (
  3. @@ -424,11 +568,26 @@ export function OAuthConnectModal({ Waiting for approval…
-
+
+ {startResult?.url ? ( + + ) : null} {deviceCode || startResult?.instructions ? ( + ) : null} + {startResult?.url ? ( + ) : null} +
+ +
+ {error ? ( +
+ {error} +
+ ) : null} + + {authMethodsError && oauthMethods.length === 0 && !provider.hasKey && (provider.connected || !supportsApi) ? ( +
+ + Could not load OpenCode sign-in methods + {authMethodsError ? ` (${authMethodsError})` : ""}. SuperGrok, ChatGPT, and Copilot OAuth may be unavailable until this succeeds. + + {onRetryAuthMethods ? ( + + ) : null} +
+ ) : null} + + {showOauthSection ? ( +
+
Subscription / OAuth
+ {oauthMethods.length > 0 ? ( +
+ {oauthMethods.map((m) => m.label).join(" · ")} +
+ ) : provider.connected ? ( +
+ Connected in OpenCode. Sign-in methods could not be listed — you can still disconnect. +
+ ) : null} + {provider.connected ? ( +
+
+ Connected via OpenCode +
+
+ {oauthMethods.length > 0 ? ( + + ) : null} + {canDisconnect ? ( + + ) : null} +
+
+ ) : oauthMethods.length > 0 ? ( + + ) : null} +
+ ) : null} + + {supportsApi ? ( +
+
API key
+ {provider.envVar ? ( +
{provider.envVar}
+ ) : null} + + {editing ? ( +
+ setKeyValue(event.target.value)} + onKeyDown={(event) => { + if (event.key === "Enter" && keyValue.trim() && !busy) { + event.preventDefault(); + void save(); + } + }} + style={{ + width: "100%", + background: COLORS.cardBg, + border: `1px solid ${COLORS.border}`, + padding: "8px 10px", + fontSize: 11, + fontFamily: MONO_FONT, + color: COLORS.textPrimary, + outline: "none", + }} + /> +
+ + +
+
+ ) : ( +
+ {provider.hasKey ? ( + <> + {verifying ? ( + Checking… + ) : verification?.ok ? ( + + Verified + + ) : verification && !verification.ok ? ( + + Failed + + ) : keySource ? ( + + From {keySource} + + ) : null} + + {keySource === "store" || (!keySource && provider.hasKey) ? ( + <> + + + + ) : keySource === "env" || keySource === "config" ? ( + + Managed outside ADE — clear the env/config value to remove. + + ) : null} + + ) : ( + + )} +
+ )} +
+ ) : null} + + {!showOauthSection && !supportsApi ? ( +
+ No sign-in methods or API key fields are available for this provider yet. Refresh the catalog or retry sign-in methods. +
+ ) : null} +
+
+
, + document.body, + )} + + {oauthOpen ? ( + setOauthOpen(false)} + onConnected={() => { + setOauthOpen(false); + onConnected(); + }} + /> + ) : null} + + ); +} diff --git a/apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx b/apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx index 9eabfb9615..e8f90ea691 100644 --- a/apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx +++ b/apps/desktop/src/renderer/components/settings/ProvidersSection.test.tsx @@ -262,6 +262,9 @@ describe("ProvidersSection", () => { builtInBrowser: { navigate: vi.fn().mockResolvedValue(undefined), }, + app: { + openExternal: vi.fn().mockResolvedValue(undefined), + }, } as any; }); @@ -488,7 +491,7 @@ describe("ProvidersSection", () => { }); }); - it("renders the Coding Agents section and the OpenCode group with the Moonshot key row", async () => { + it("renders the Coding Agents section and OpenCode popular provider cards", async () => { const getStatusMock = window.ade.ai.getStatus as ReturnType; getStatusMock.mockReset(); getStatusMock.mockResolvedValue(buildStatus(true, [])); @@ -497,12 +500,14 @@ describe("ProvidersSection", () => { expect(await screen.findByText("Coding Agents")).toBeTruthy(); expect(screen.getByText("OpenCode — Universal Model Access")).toBeTruthy(); - expect(screen.getByText("API Provider Keys")).toBeTruthy(); - // Moonshot AI was added to the API key grid. + expect(screen.getByText("All providers")).toBeTruthy(); + expect(screen.getByLabelText("Search all OpenCode providers")).toBeTruthy(); + // Popular cards include Moonshot and Kimi. expect(screen.getByText("Moonshot AI")).toBeTruthy(); - expect(screen.getByText("MOONSHOT_API_KEY")).toBeTruthy(); - // Kimi for Coding is always surfaced as a subscription/membership row. expect(screen.getByLabelText("Connect Kimi for Coding")).toBeTruthy(); + // Hated status chrome is gone. + expect(screen.queryByText(/managed by ADE/i)).toBeNull(); + expect(screen.queryByText(/subscriptions ·/i)).toBeNull(); }); it("collapses the OpenCode group to an install card when the binary is missing", async () => { @@ -515,11 +520,11 @@ describe("ProvidersSection", () => { expect(await screen.findByText("npm i -g opencode-ai")).toBeTruthy(); expect(screen.getByText("brew install anomalyco/tap/opencode")).toBeTruthy(); expect(screen.getByRole("button", { name: /Re-check/ })).toBeTruthy(); - // The group body (API keys, subscriptions) must be hidden while uninstalled. - expect(screen.queryByText("API Provider Keys")).toBeNull(); + // The group body is hidden while uninstalled. + expect(screen.queryByText("All providers")).toBeNull(); }); - it("renders subscription connect rows and keeps chips visible while the provider catalog is stale", async () => { + it("renders provider cards and catalog-updating state while inventory is stale", async () => { const getStatusMock = window.ade.ai.getStatus as ReturnType; getStatusMock.mockReset(); getStatusMock.mockResolvedValue(buildStatus(true, [], { @@ -539,12 +544,48 @@ describe("ProvidersSection", () => { renderProvidersSection(); - // OAuth subscription row built dynamically from auth methods. expect(await screen.findByLabelText("Connect OpenAI")).toBeTruthy(); - // Stale label surfaces without blocking the catalog chips. - expect(screen.getByText("updating…")).toBeTruthy(); - // Fireworks is not an API_KEY_PROVIDER, so it appears as a "More providers" chip. - expect(screen.getByText(/Fireworks/)).toBeTruthy(); + expect(screen.getByText(/Updating provider catalog/i)).toBeTruthy(); + // Search finds catalog-only providers. + fireEvent.change(screen.getByLabelText("Search all OpenCode providers"), { + target: { value: "Fireworks" }, + }); + expect(await screen.findByLabelText("Connect Fireworks")).toBeTruthy(); + }); + + it("offers a retry when the initial OpenCode status probe fails", async () => { + const getStatusMock = window.ade.ai.getStatus as ReturnType; + getStatusMock.mockReset(); + getStatusMock.mockRejectedValue(new Error("status probe unavailable")); + + renderProvidersSection(); + + expect(await screen.findByText("Could not load OpenCode status.")).toBeTruthy(); + expect(screen.getByRole("button", { name: "Re-check OpenCode" })).toBeTruthy(); + expect(screen.getAllByText("status probe unavailable").length).toBeGreaterThan(0); + expect(screen.queryByText("Binary Missing")).toBeNull(); + }); + + it("keeps an auth-method outage out of an API-key-only provider", async () => { + const getStatusMock = window.ade.ai.getStatus as ReturnType; + getStatusMock.mockReset(); + getStatusMock.mockResolvedValue(buildStatus(true, [], { + opencodeProviders: [{ id: "openai", name: "OpenAI", connected: false, modelCount: 12 }], + })); + const authMethodsMock = window.ade.ai.opencodeAuthMethods as ReturnType; + authMethodsMock.mockReset(); + authMethodsMock.mockRejectedValue(new Error("catalog unavailable")); + + renderProvidersSection(); + + await waitFor(() => { + expect(authMethodsMock).toHaveBeenCalledTimes(1); + }); + fireEvent.click(await screen.findByLabelText("Connect OpenAI")); + + expect(screen.getByRole("dialog", { name: "OpenAI provider" })).toBeTruthy(); + expect(screen.getAllByText("API key").length).toBeGreaterThan(0); + expect(screen.queryByText(/Could not load OpenCode sign-in methods/)).toBeNull(); }); it("keeps an OpenCode key editor open when provider registration fails", async () => { @@ -556,9 +597,14 @@ describe("ProvidersSection", () => { renderProvidersSection(); - const addButton = await screen.findByLabelText("Add OpenAI key"); + const openAiCard = await screen.findByLabelText("Connect OpenAI"); await act(async () => { - addButton.click(); + openAiCard.click(); + }); + expect(screen.getByRole("dialog", { name: "OpenAI provider" })).toBeTruthy(); + + await act(async () => { + screen.getByLabelText("Add OpenAI key").click(); }); fireEvent.change(screen.getByLabelText("OpenAI API key"), { target: { value: "sk-test" }, @@ -577,17 +623,31 @@ describe("ProvidersSection", () => { it("clears an OpenCode provider credential before deleting its stored key", async () => { const getStatusMock = window.ade.ai.getStatus as ReturnType; getStatusMock.mockReset(); - getStatusMock.mockResolvedValue(buildStatus(true, [])); + getStatusMock.mockResolvedValue(buildStatus(true, [], { + opencodeProviders: [{ id: "openai", name: "OpenAI", connected: true, modelCount: 12 }], + })); const listApiKeysMock = window.ade.ai.listApiKeys as ReturnType; listApiKeysMock.mockReset(); listApiKeysMock.mockResolvedValue(["openai"]); + const authMethodsMock = window.ade.ai.opencodeAuthMethods as ReturnType; + authMethodsMock.mockReset(); + authMethodsMock.mockRejectedValue(new Error("catalog unavailable")); renderProvidersSection(); - const menuButton = await screen.findByLabelText("More actions for OpenAI"); + // Wait until stored keys hydrate so the card reflects key ownership. + await waitFor(() => { + expect(window.ade.ai.listApiKeys).toHaveBeenCalled(); + }); + const openCard = + screen.queryByLabelText("Open OpenAI") + ?? screen.queryByLabelText("Connect OpenAI"); + expect(openCard).toBeTruthy(); await act(async () => { - menuButton.click(); + openCard!.click(); }); + expect(screen.queryByText("Subscription / OAuth")).toBeNull(); + expect(screen.queryByText(/Could not load OpenCode sign-in methods/)).toBeNull(); await act(async () => { screen.getByRole("button", { name: "Delete" }).click(); }); @@ -599,10 +659,10 @@ describe("ProvidersSection", () => { const clearCall = (window.ade.ai.clearOpencodeProviderKey as ReturnType).mock.invocationCallOrder[0]; const deleteCall = (window.ade.ai.deleteApiKey as ReturnType).mock.invocationCallOrder[0]; expect(clearCall).toBeLessThan(deleteCall); - expect(await screen.findByText("openai key removed.")).toBeTruthy(); + expect(await screen.findByText("OpenAI disconnected.")).toBeTruthy(); }); - it("drives the OAuth connect modal happy path", async () => { + it("drives the OAuth connect modal happy path via provider detail", async () => { const getStatusMock = window.ade.ai.getStatus as ReturnType; getStatusMock.mockReset(); getStatusMock.mockResolvedValue(buildStatus(true, [], { @@ -618,12 +678,15 @@ describe("ProvidersSection", () => { renderProvidersSection(); - const connectButton = await screen.findByLabelText("Connect OpenAI"); + const openAiCard = await screen.findByLabelText("Connect OpenAI"); await act(async () => { - connectButton.click(); + openAiCard.click(); }); + expect(screen.getByRole("dialog", { name: "OpenAI provider" })).toBeTruthy(); - // Modal opened. + await act(async () => { + screen.getByRole("button", { name: "Sign in to OpenAI" }).click(); + }); expect(screen.getByRole("dialog", { name: "Connect OpenAI" })).toBeTruthy(); await act(async () => { @@ -636,16 +699,13 @@ describe("ProvidersSection", () => { methodIndex: 0, inputs: undefined, }); - expect(window.ade.builtInBrowser.navigate).toHaveBeenCalledWith({ - url: "https://auth.openai.com/device", - newTab: true, - }); + expect(window.ade.app.openExternal).toHaveBeenCalledWith("https://auth.openai.com/device"); }); // Waiting state renders the extracted device code. expect(await screen.findByText("ABCD-1234")).toBeTruthy(); - // Backend reports success → modal closes. + // Backend reports success → oauth modal closes. await act(async () => { emitOAuthStatus?.({ providerId: "openai", state: "connected" }); }); diff --git a/apps/desktop/src/renderer/components/settings/ProvidersSection.tsx b/apps/desktop/src/renderer/components/settings/ProvidersSection.tsx index 9bc3f34e1c..c22f62a28d 100644 --- a/apps/desktop/src/renderer/components/settings/ProvidersSection.tsx +++ b/apps/desktop/src/renderer/components/settings/ProvidersSection.tsx @@ -5,13 +5,11 @@ import type { AiApiKeyVerificationResult, AiClaudeAvailability, AiProviderConnectionStatus, - AiRuntimeConnectionStatus, AiSettingsStatus, ProjectConfigSnapshot, } from "../../../shared/types"; import type { AiCustomProviderConfig, - OpenCodeProviderAuthMethod, OpenCodeProviderAuthMethods, } from "../../../shared/types/config"; import { @@ -27,7 +25,6 @@ import { CheckCircle, Copy, Cpu, - DotsThree, Info, MagnifyingGlass, WarningCircle, @@ -45,27 +42,18 @@ import { primaryButton, } from "../lanes/laneDesignTokens"; import { cursorProviderAvailable, rendererPlatformAttribute } from "../../lib/platform"; -import { deriveConfiguredModelIds } from "../../lib/modelOptions"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; import { invalidateAiDiscoveryCache } from "../../lib/aiDiscoveryCache"; import { shouldRefreshAiStatusForChatEvent } from "../../lib/aiProviderStatus"; import { showToast } from "../app/toast/toastStore"; import { ClaudeLoginPromptButton, revealTerminalSessionInWork } from "../work/ClaudeLoginPromptButton"; -import { OAuthConnectModal } from "./OAuthConnectModal"; +import { + OpenCodeProviderDetailModal, + type ApiKeySource, + type OpenCodeProviderDetail, +} from "./OpenCodeProviderDetailModal"; type CliName = "claude" | "codex" | "cursor" | "droid"; -type ApiKeySource = "config" | "env" | "store"; - -/** - * Status payload plus the OpenCode inventory freshness fields the AI service - * adds alongside the provider catalog. Typed locally as optional so this file - * stays correct before those additive members land in shared types. - */ -type ProvidersStatus = AiSettingsStatus & { - runtimeConnections?: Record; - opencodeProvidersStale?: boolean; - modelsDevLastFetchedAt?: number | null; -}; const KIMI_PROVIDER_ID = "kimi-for-coding"; @@ -165,18 +153,17 @@ const API_KEY_PROVIDERS: Array<{ label: string; envVar: string; placeholder: string; - accent: string; }> = [ - { provider: "anthropic", label: "Anthropic", envVar: "ANTHROPIC_API_KEY", placeholder: "sk-ant-...", accent: "#D97757" }, - { provider: "openai", label: "OpenAI", envVar: "OPENAI_API_KEY", placeholder: "sk-...", accent: "#10A37F" }, - { provider: "google", label: "Google AI", envVar: "GOOGLE_API_KEY", placeholder: "AIza...", accent: "#60A5FA" }, - { provider: "mistral", label: "Mistral", envVar: "MISTRAL_API_KEY", placeholder: "mistral-...", accent: "#F59E0B" }, - { provider: "deepseek", label: "DeepSeek", envVar: "DEEPSEEK_API_KEY", placeholder: "sk-...", accent: "#38BDF8" }, - { provider: "xai", label: "xAI", envVar: "XAI_API_KEY", placeholder: "xai-...", accent: "#A3A3A3" }, - { provider: "groq", label: "Groq", envVar: "GROQ_API_KEY", placeholder: "gsk_...", accent: "#F43F5E" }, - { provider: "together", label: "Together AI", envVar: "TOGETHER_API_KEY", placeholder: "tg_...", accent: "#22C55E" }, - { provider: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY", placeholder: "sk-or-...", accent: "#A78BFA" }, - { provider: "moonshotai", label: "Moonshot AI", envVar: "MOONSHOT_API_KEY", placeholder: "sk-...", accent: "#7C5CFF" }, + { provider: "anthropic", label: "Anthropic", envVar: "ANTHROPIC_API_KEY", placeholder: "sk-ant-..." }, + { provider: "openai", label: "OpenAI", envVar: "OPENAI_API_KEY", placeholder: "sk-..." }, + { provider: "google", label: "Google AI", envVar: "GOOGLE_API_KEY", placeholder: "AIza..." }, + { provider: "mistral", label: "Mistral", envVar: "MISTRAL_API_KEY", placeholder: "mistral-..." }, + { provider: "deepseek", label: "DeepSeek", envVar: "DEEPSEEK_API_KEY", placeholder: "sk-..." }, + { provider: "xai", label: "xAI", envVar: "XAI_API_KEY", placeholder: "xai-..." }, + { provider: "groq", label: "Groq", envVar: "GROQ_API_KEY", placeholder: "gsk_..." }, + { provider: "together", label: "Together AI", envVar: "TOGETHER_API_KEY", placeholder: "tg_..." }, + { provider: "openrouter", label: "OpenRouter", envVar: "OPENROUTER_API_KEY", placeholder: "sk-or-..." }, + { provider: "moonshotai", label: "Moonshot AI", envVar: "MOONSHOT_API_KEY", placeholder: "sk-..." }, ]; type LocalProviderDraft = { @@ -238,18 +225,6 @@ function prettifyProviderId(id: string): string { .join(" "); } -function formatSyncedAgo(ts: number | null | undefined): string { - if (ts == null) return "—"; - const diffMs = Date.now() - ts; - if (diffMs < 60_000) return "just now"; - const mins = Math.floor(diffMs / 60_000); - if (mins < 60) return `${mins}m ago`; - const hrs = Math.floor(mins / 60); - if (hrs < 24) return `${hrs}h ago`; - const days = Math.floor(hrs / 24); - return `${days}d ago`; -} - function AlertBanner({ tone, message, @@ -350,6 +325,61 @@ function ConnectedTag() { ); } +function OpenCodeProviderCard({ + provider, + onOpen, +}: { + provider: OpenCodeProviderDetail; + onOpen: () => void; +}) { + const badge = provider.connected + ? "Connected" + : provider.hasKey + ? "Key" + : provider.methods.some((m) => m.type === "oauth") + ? "OAuth" + : "Add"; + return ( + + ); +} + function CopyableCommand({ command }: { command: string }) { const { copy, copied } = useCopyToClipboard(); return ( @@ -452,7 +482,7 @@ function formatLocalModelLabel(modelId: string): string { function buildLocalProviderDrafts( snapshot: ProjectConfigSnapshot | null | undefined, - status: ProvidersStatus | null | undefined, + status: AiSettingsStatus | null | undefined, ): Record { const configured = snapshot?.effective.ai?.localProviders ?? {}; return Object.fromEntries( @@ -475,7 +505,7 @@ function buildLocalProviderDrafts( export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefreshOnMount?: boolean }) { const navigate = useNavigate(); - const [status, setStatus] = useState(null); + const [status, setStatus] = useState(null); const [projectConfigSnapshot, setProjectConfigSnapshot] = useState(null); const [storedProviders, setStoredProviders] = useState([]); const [loading, setLoading] = useState(true); @@ -492,15 +522,15 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh const [verifyingProvider, setVerifyingProvider] = useState(null); const [verificationByProvider, setVerificationByProvider] = useState>({}); const [authMethods, setAuthMethods] = useState(null); - const [oauthTarget, setOauthTarget] = useState<{ providerId: string; providerName: string; methods: OpenCodeProviderAuthMethod[] } | null>(null); - const [kimiDialogOpen, setKimiDialogOpen] = useState(false); + const [authMethodsError, setAuthMethodsError] = useState(null); + const [detailProviderId, setDetailProviderId] = useState(null); const [providerSearch, setProviderSearch] = useState(""); - const [verifyingAll, setVerifyingAll] = useState(false); - const [openRowMenu, setOpenRowMenu] = useState(null); const [refreshingCatalog, setRefreshingCatalog] = useState(false); const [customProviderDraft, setCustomProviderDraft] = useState(EMPTY_CUSTOM_PROVIDER); const [customModelSlugs, setCustomModelSlugs] = useState(""); const [savingAdvanced, setSavingAdvanced] = useState(false); + const [statusLoadError, setStatusLoadError] = useState(null); + const statusKnownRef = useRef(false); const pendingRefreshTimerRef = useRef(null); // Seed the slugs field from config exactly once — saves send the full list // (replace semantics), so the field must start from what's persisted or a @@ -510,9 +540,10 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh revealTerminalSessionInWork(navigate, terminal); }, [navigate]); - const refreshStatus = useCallback(async (options?: { force?: boolean; silent?: boolean; refreshOpenCodeInventory?: boolean }): Promise => { + const refreshStatus = useCallback(async (options?: { force?: boolean; silent?: boolean; refreshOpenCodeInventory?: boolean }): Promise => { if (!options?.silent) { setLoading(true); + if (!statusKnownRef.current) setStatusLoadError(null); } setError(null); try { @@ -524,15 +555,19 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh window.ade.ai.listApiKeys(), window.ade.projectConfig.get(), ]); - setStatus(nextStatus as ProvidersStatus); + statusKnownRef.current = true; + setStatusLoadError(null); + setStatus(nextStatus as AiSettingsStatus); setProjectConfigSnapshot(nextProjectConfig); if (editingLocalProvider == null && savingLocalProvider == null) { - setLocalProviderDrafts(buildLocalProviderDrafts(nextProjectConfig, nextStatus as ProvidersStatus)); + setLocalProviderDrafts(buildLocalProviderDrafts(nextProjectConfig, nextStatus as AiSettingsStatus)); } setStoredProviders(nextStoredProviders.map((entry) => entry.trim().toLowerCase()).filter(Boolean)); - return nextStatus as ProvidersStatus; + return nextStatus as AiSettingsStatus; } catch (err) { - setError(err instanceof Error ? err.message : String(err)); + const message = err instanceof Error ? err.message : String(err); + if (!statusKnownRef.current) setStatusLoadError(message); + setError(message); return null; } finally { if (!options?.silent) { @@ -545,9 +580,11 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh try { const result = await window.ade.ai.opencodeAuthMethods(); setAuthMethods(result.methods ?? {}); - } catch { - // Best-effort — subscription connect rows simply stay hidden if this - // capability is not available yet. + setAuthMethodsError(null); + } catch (err) { + // Keep any previously loaded methods so an intermittent failure does not + // wipe SuperGrok / ChatGPT OAuth rows mid-session. + setAuthMethodsError(err instanceof Error ? err.message : String(err)); } }, []); @@ -589,8 +626,11 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh const detectedAuth = useMemo(() => status?.detectedAuth ?? [], [status?.detectedAuth]); const providerConnections = status?.providerConnections; - const isInitialCheckInFlight = loading && status == null; - const catalogModelIds = useMemo(() => deriveConfiguredModelIds(status), [status]); + // Keep provider cards neutral while the status payload is unavailable. A + // failed first probe must not be presented as a real "Binary Missing" state. + const isInitialCheckInFlight = status == null; + const opencodeStatusKnown = status !== null; + const opencodeStatusLoadFailed = !opencodeStatusKnown && !loading && statusLoadError !== null; const opencodeInstalled = status?.opencodeBinaryInstalled !== false; const opencodeProviders = useMemo(() => status?.opencodeProviders ?? [], [status?.opencodeProviders]); const providersStale = status?.opencodeProvidersStale === true; @@ -660,61 +700,108 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh ? apiKeyStoreWarning : null; - // ── Subscription (OAuth + Kimi membership) rows ── - const subscriptionRows = useMemo(() => { - const nameById = new Map(opencodeProviders.map((p) => [p.id, p.name] as const)); - const oauthIds = authMethods - ? Object.keys(authMethods).filter((id) => authMethods[id]?.some((m) => m.type === "oauth")) - : []; - const rows: Array<{ - id: string; - name: string; - methods: OpenCodeProviderAuthMethod[]; - kind: "oauth" | "kimi"; - connected: boolean; - }> = oauthIds - .sort((a, b) => a.localeCompare(b)) - .map((id) => ({ + // Unified OpenCode provider catalog: inventory + auth methods + known API key rows. + const openCodeCatalog = useMemo((): OpenCodeProviderDetail[] => { + const byId = new Map(); + const inventoryById = new Map( + opencodeProviders.map((p) => [p.id, p] as const), + ); + const apiById = new Map(API_KEY_PROVIDERS.map((p) => [p.provider, p] as const)); + + const upsert = (id: string, patch: Partial = {}) => { + const apiSpec = apiById.get(id); + const inventory = inventoryById.get(id); + const prev = byId.get(id); + const methods = patch.methods ?? prev?.methods ?? authMethods?.[id] ?? []; + byId.set(id, { id, - name: nameById.get(id) ?? prettifyProviderId(id), - methods: authMethods?.[id] ?? [], - kind: "oauth" as const, - connected: opencodeProviders.find((p) => p.id === id)?.connected === true, - })); - // Kimi for Coding is an API-membership key, not OAuth — always surfaced here. - rows.push({ - id: KIMI_PROVIDER_ID, + name: patch.name ?? prev?.name ?? inventory?.name ?? apiSpec?.label ?? prettifyProviderId(id), + methods, + connected: patch.connected ?? prev?.connected ?? inventory?.connected === true, + hasKey: hasKeyFor(id), + modelCount: patch.modelCount ?? prev?.modelCount ?? inventory?.modelCount, + envVar: patch.envVar ?? prev?.envVar ?? apiSpec?.envVar, + placeholder: patch.placeholder ?? prev?.placeholder ?? apiSpec?.placeholder, + }); + }; + + for (const p of opencodeProviders) { + if (p.id === "cursor" || p.id === "ollama" || p.id === "lmstudio") continue; + upsert(p.id, { name: p.name, modelCount: p.modelCount, connected: p.connected }); + } + for (const [id, methods] of Object.entries(authMethods ?? {})) { + upsert(id, { methods }); + } + for (const api of API_KEY_PROVIDERS) { + upsert(api.provider, { + name: api.label, + envVar: api.envVar, + placeholder: api.placeholder, + }); + } + // Kimi for Coding is ADE's known API-key path; keep OpenCode-advertised methods if present. + const kimiInventory = inventoryById.get(KIMI_PROVIDER_ID); + upsert(KIMI_PROVIDER_ID, { name: "Kimi for Coding", - methods: [], - kind: "kimi" as const, - connected: - opencodeProviders.find((p) => p.id === KIMI_PROVIDER_ID)?.connected === true - || hasKeyFor(KIMI_PROVIDER_ID), + envVar: "KIMI_API_KEY", + placeholder: "sk-…", + connected: kimiInventory?.connected === true || hasKeyFor(KIMI_PROVIDER_ID), }); - return rows; + + return [...byId.values()].sort((a, b) => a.name.localeCompare(b.name)); }, [authMethods, opencodeProviders, hasKeyFor]); - const keyCount = useMemo(() => { - const ids = new Set(); - for (const p of API_KEY_PROVIDERS) { - if (hasKeyFor(p.provider)) ids.add(p.provider); + const openCodeCatalogById = useMemo( + () => new Map(openCodeCatalog.map((row) => [row.id, row] as const)), + [openCodeCatalog], + ); + + const connectedOpenCodeProviders = useMemo( + () => openCodeCatalog.filter((p) => p.connected || p.hasKey), + [openCodeCatalog], + ); + + const popularOpenCodeProviders = useMemo(() => { + const popularIds = [ + ...API_KEY_PROVIDERS.map((p) => p.provider), + ...Object.keys(authMethods ?? {}).filter((id) => authMethods?.[id]?.some((m) => m.type === "oauth")), + KIMI_PROVIDER_ID, + ]; + const connectedIds = new Set( + openCodeCatalog.filter((p) => p.connected || p.hasKey).map((p) => p.id), + ); + const seen = new Set(); + const list: OpenCodeProviderDetail[] = []; + for (const id of popularIds) { + if (seen.has(id) || connectedIds.has(id)) continue; + seen.add(id); + const row = openCodeCatalogById.get(id); + if (row) list.push(row); } - for (const id of storedProviders) ids.add(id); - for (const id of apiKeySources.keys()) ids.add(id); - return ids.size; - }, [hasKeyFor, storedProviders, apiKeySources]); - - const connectedSubscriptionCount = useMemo( - () => subscriptionRows.filter((row) => row.connected).length, - [subscriptionRows], + return list; + }, [openCodeCatalog, openCodeCatalogById, authMethods]); + + const searchableOpenCodeProviders = useMemo(() => { + const query = providerSearch.trim().toLowerCase(); + return openCodeCatalog + .filter((p) => !query || p.id.toLowerCase().includes(query) || p.name.toLowerCase().includes(query)) + .sort((a, b) => (b.modelCount ?? 0) - (a.modelCount ?? 0)); + }, [openCodeCatalog, providerSearch]); + + const detailProvider = useMemo( + () => (detailProviderId ? openCodeCatalog.find((p) => p.id === detailProviderId) ?? null : null), + [detailProviderId, openCodeCatalog], ); + const openProviderDetail = useCallback((id: string) => { + // Always use the unified provider modal (OAuth + API key), including Kimi. + setDetailProviderId(id); + }, []); const beginEditing = (provider: string) => { setEditingProvider(provider); setEditValue(""); setError(null); setNotice(null); - setOpenRowMenu(null); }; const cancelEditing = () => { @@ -722,39 +809,9 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh setEditValue(""); }; - const saveApiKey = async (provider: string, options?: { alsoOpenCode?: boolean }) => { - const trimmed = editValue.trim(); - if (!trimmed) return; - - setError(null); - setNotice(null); - try { - if (options?.alsoOpenCode) { - const result = await window.ade.ai.setOpencodeProviderKey({ providerId: provider, key: trimmed }); - if (!result.ok) { - throw new Error(result.error || "OpenCode rejected the provider key."); - } - } else { - await window.ade.ai.storeApiKey(provider, trimmed); - } - invalidateAiDiscoveryCache(); - setVerificationByProvider((prev) => { - const next = { ...prev }; - delete next[provider]; - return next; - }); - setNotice(`${provider} key saved.`); - cancelEditing(); - await refreshStatus({ force: true, refreshOpenCodeInventory: true }); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } - }; - const deleteApiKey = async (provider: string, options?: { alsoOpenCode?: boolean }) => { setError(null); setNotice(null); - setOpenRowMenu(null); try { if (options?.alsoOpenCode) { const result = await window.ade.ai.clearOpencodeProviderKey({ providerId: provider }); @@ -764,7 +821,10 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh } await window.ade.ai.deleteApiKey(provider); invalidateAiDiscoveryCache(); - setNotice(`${provider} key removed.`); + const label = + API_KEY_PROVIDERS.find((row) => row.provider === provider)?.label + ?? (provider === KIMI_PROVIDER_ID ? "Kimi for Coding" : prettifyProviderId(provider)); + setNotice(`${label} disconnected.`); if (editingProvider === provider) cancelEditing(); setVerificationByProvider((prev) => { const next = { ...prev }; @@ -774,13 +834,14 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh await refreshStatus({ force: true, refreshOpenCodeInventory: true }); } catch (err) { setError(err instanceof Error ? err.message : String(err)); + // Re-throw so nested modals (detail overlay) can show the failure in-dialog. + throw err instanceof Error ? err : new Error(String(err)); } }; const verifyApiKey = async (provider: string) => { setError(null); setNotice(null); - setOpenRowMenu(null); setVerifyingProvider(provider); setVerificationByProvider((prev) => { const next = { ...prev }; @@ -805,30 +866,6 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh } }; - const verifyAllKeys = async () => { - const targets = API_KEY_PROVIDERS.filter((p) => hasKeyFor(p.provider)).map((p) => p.provider); - if (!targets.length) return; - setVerifyingAll(true); - setError(null); - setNotice(null); - try { - for (const provider of targets) { - setVerifyingProvider(provider); - try { - const result = await window.ade.ai.verifyApiKey(provider); - setVerificationByProvider((prev) => ({ ...prev, [provider]: result })); - } catch { - // Continue verifying the remaining providers. - } - } - invalidateAiDiscoveryCache(); - await refreshStatus({ force: true, refreshOpenCodeInventory: true }); - } finally { - setVerifyingProvider(null); - setVerifyingAll(false); - } - }; - const saveCursorApiKey = async () => { const trimmed = editValue.trim(); if (!trimmed) return; @@ -889,26 +926,6 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh }); }, [status?.availableModelIds, refreshStatus, loadAuthMethods]); - const saveKimiKey = async (key: string) => { - const trimmed = key.trim(); - if (!trimmed) return; - setError(null); - setNotice(null); - try { - const result = await window.ade.ai.setOpencodeProviderKey({ providerId: KIMI_PROVIDER_ID, key: trimmed }); - if (result && result.ok === false) { - setError(result.error || "Failed to save Kimi for Coding key."); - return; - } - invalidateAiDiscoveryCache(); - setKimiDialogOpen(false); - setNotice("Kimi for Coding connected."); - await refreshStatus({ force: true, refreshOpenCodeInventory: true }); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } - }; - const saveAdvancedProvider = async () => { const draft = customProviderDraft; const id = draft.id.trim(); @@ -1022,19 +1039,6 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh } }, [localProviderDrafts, refreshStatus]); - // ── More Providers chip cloud ── - const moreProviders = useMemo(() => { - const query = providerSearch.trim().toLowerCase(); - const filtered = opencodeProviders - .filter((p) => !p.connected - && p.id !== "cursor" - && !API_KEY_PROVIDERS.some((a) => a.provider === p.id) - && !["ollama", "lmstudio"].includes(p.id)) - .filter((p) => !query || p.id.toLowerCase().includes(query) || p.name.toLowerCase().includes(query)) - .sort((a, b) => b.modelCount - a.modelCount); - return { list: filtered, query }; - }, [opencodeProviders, providerSearch]); - return (
{notice && ( @@ -1229,9 +1233,7 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh ) : keySource ? ( <> {isKeyConnected ? ( - - Connected - + ) : ( - + ) : null} @@ -1294,19 +1296,38 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh OpenCode — Universal Model Access
- {status?.opencodeBinarySource ? `${status.opencodeBinarySource} · ` : ""}managed by ADE + SuperGrok OAuth, ChatGPT, Copilot, or API keys — same /connect providers as OpenCode
-
- {!opencodeInstalled ? : status?.opencodeInventoryError ? : } +
+ {!opencodeStatusKnown ? (opencodeStatusLoadFailed ? : ) : !opencodeInstalled ? : status?.opencodeInventoryError ? : } - {!opencodeInstalled ? "Not found" : status?.opencodeInventoryError ? "Error" : "Installed"} + {!opencodeStatusKnown ? (opencodeStatusLoadFailed ? "Error" : "Checking") : !opencodeInstalled ? "Not found" : status?.opencodeInventoryError ? "Error" : "Installed"}
- {!opencodeInstalled ? ( + {!opencodeStatusKnown && opencodeStatusLoadFailed ? ( +
+
+ Could not load OpenCode status. +
+ +
+ ) : !opencodeStatusKnown ? ( +
+ Checking OpenCode and its provider catalog… +
+ ) : !opencodeInstalled ? ( /* Collapsed install card */
@@ -1330,12 +1351,12 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh
) : (
- {/* Summary strip + catalog freshness */} -
-
- {connectedSubscriptionCount} subscription{connectedSubscriptionCount === 1 ? "" : "s"} · {keyCount} key{keyCount === 1 ? "" : "s"} · {catalogModelIds.length} model{catalogModelIds.length === 1 ? "" : "s"} unlocked - {providersStale ? updating… : null} -
+
+ {providersStale ? ( + + Updating provider catalog… + + ) : null}
- {/* ── a. Subscriptions ── */} + {/* ── Connected ── */}
-
Subscriptions
-
- {subscriptionRows.map((row) => ( -
-
-
- - {row.name} -
- {row.connected ? ( - - ) : ( - - )} -
- {row.id === "openai" ? ( -
- Also powers OpenAI models inside OpenCode. For the Codex agent, connect the Codex CLI above. -
- ) : null} -
- ))} -
- {subscriptionRows.length <= 1 && !authMethods ? ( -
- {providersStale ? "Loading available subscriptions…" : "No OAuth subscriptions are available from OpenCode yet."} +
Connected
+ {connectedOpenCodeProviders.length === 0 ? ( +
+ No providers connected yet. Pick one below to sign in or add a key.
- ) : null} -
- - {/* ── b. API Provider Keys ── */} -
-
-
API Provider Keys
- -
-
- {API_KEY_PROVIDERS.map((provider) => { - const keySource = apiKeySources.get(provider.provider) ?? (storedProviders.includes(provider.provider) ? "store" : undefined); - const verification = verificationByProvider[provider.provider]; - const isEditing = editingProvider === provider.provider; - const isVerifying = verifyingProvider === provider.provider; - const menuOpen = openRowMenu === provider.provider; - return ( -
-
- -
-
{provider.label}
-
{provider.envVar}
-
- {!isEditing ? ( -
- {isVerifying ? ( - Checking… - ) : verification?.ok ? ( - - Verified - - ) : verification && !verification.ok ? ( - - Failed - - ) : keySource ? ( - - ) : ( - - )} - {keySource ? ( - - ) : null} -
- ) : null} -
- - {isEditing ? ( -
- setEditValue(event.target.value)} - placeholder={provider.placeholder} - type="password" - style={{ flex: 1, minWidth: 0, background: COLORS.cardBg, border: `1px solid ${COLORS.border}`, padding: "6px 8px", fontSize: 11, fontFamily: MONO_FONT, color: COLORS.textPrimary, outline: "none" }} - /> - - -
- ) : null} - - {menuOpen && keySource ? ( - <> -
setOpenRowMenu(null)} /> -
- - {keySource === "store" ? ( - <> - - - - ) : null} -
- - ) : null} -
- ); - })} -
+ ) : ( +
+ {connectedOpenCodeProviders.map((row) => ( + openProviderDetail(row.id)} /> + ))} +
+ )}
- {/* ── c. More Providers ── */} + {/* ── All providers ── */}
-
More Providers
-
+
All providers
+
setProviderSearch(event.target.value)} - placeholder="Search providers" + placeholder="Search all OpenCode providers" style={{ flex: 1, minWidth: 0, background: "transparent", border: "none", outline: "none", fontSize: 11, fontFamily: MONO_FONT, color: COLORS.textPrimary }} />
- {moreProviders.list.length === 0 ? ( + + {!providerSearch.trim() ? ( + <> +
Popular
+
+ {popularOpenCodeProviders.map((row) => ( + openProviderDetail(row.id)} /> + ))} +
+ + ) : searchableOpenCodeProviders.length === 0 ? (
- {moreProviders.query ? "No providers match your search." : "No additional providers available."} + No providers match your search.
) : ( -
- {(moreProviders.query ? moreProviders.list : moreProviders.list.slice(0, 30)).map((p) => { - const hasKey = hasKeyFor(p.id); - const isEditing = editingProvider === `__custom:${p.id}`; - return isEditing ? ( -
- {p.name} - setEditValue(event.target.value)} - placeholder="API key" - type="password" - style={{ flex: 1, background: COLORS.cardBg, border: `1px solid ${COLORS.border}`, padding: "6px 8px", fontSize: 11, fontFamily: MONO_FONT, color: COLORS.textPrimary, outline: "none" }} - /> - - -
- ) : ( - - ); - })} - {!moreProviders.query && moreProviders.list.length > 30 ? ( - - +{moreProviders.list.length - 30} more — search to filter - - ) : null} +
+ {searchableOpenCodeProviders.map((row) => ( + openProviderDetail(row.id)} /> + ))}
)}
- {/* ── d. Local Model Servers ── */} + {/* ── Local Model Servers ── */}
Local Model Servers
@@ -1726,69 +1592,36 @@ export function ProvidersSection({ forceRefreshOnMount = false }: { forceRefresh )} - {oauthTarget ? ( - setOauthTarget(null)} - onConnected={() => void handleSubscriptionConnected(oauthTarget.providerId, oauthTarget.providerName)} + {detailProvider ? ( + setDetailProviderId(null)} + onConnected={() => void handleSubscriptionConnected(detailProvider.id, detailProvider.name)} + onRetryAuthMethods={() => void loadAuthMethods()} + onSaveKey={async (key) => { + const result = await window.ade.ai.setOpencodeProviderKey({ providerId: detailProvider.id, key }); + if (!result.ok) throw new Error(result.error || "OpenCode rejected the provider key."); + invalidateAiDiscoveryCache(); + setVerificationByProvider((prev) => { + const next = { ...prev }; + delete next[detailProvider.id]; + return next; + }); + setNotice(`${detailProvider.name} key saved.`); + await refreshStatus({ force: true, refreshOpenCodeInventory: true }); + }} + onDeleteKey={async () => { + await deleteApiKey(detailProvider.id, { alsoOpenCode: true }); + }} + onVerifyKey={async () => { + await verifyApiKey(detailProvider.id); + }} /> ) : null} - - {kimiDialogOpen ? ( - setKimiDialogOpen(false)} - onSave={(key) => void saveKimiKey(key)} - /> - ) : null} -
- ); -} - -function KimiKeyDialog({ onClose, onSave }: { onClose: () => void; onSave: (key: string) => void }) { - const [value, setValue] = useState(""); - return ( -
-
event.stopPropagation()} - > -
-
- -
Connect Kimi for Coding
-
- -
-
-
- Paste your Kimi for Coding membership key. It is stored via OpenCode. -
- setValue(event.target.value)} - placeholder="sk-..." - type="password" - style={{ width: "100%", background: COLORS.cardBg, border: `1px solid ${COLORS.border}`, padding: "8px 10px", fontSize: 12, fontFamily: MONO_FONT, color: COLORS.textPrimary, outline: "none" }} - /> -
- - -
-
-
); } diff --git a/apps/desktop/src/renderer/components/shared/ProviderLogos.tsx b/apps/desktop/src/renderer/components/shared/ProviderLogos.tsx index 02da178287..a0937912be 100644 --- a/apps/desktop/src/renderer/components/shared/ProviderLogos.tsx +++ b/apps/desktop/src/renderer/components/shared/ProviderLogos.tsx @@ -167,12 +167,18 @@ export function ProviderLogo({ return ; case "xai": return ; + case "grok": + return ; case "groq": return ; case "openrouter": return ; case "google": + case "google-vertex": + case "google-vertex-anthropic": return ; + case "gemini": + return ; case "ollama": return ; case "lmstudio": @@ -182,8 +188,17 @@ export function ProviderLogo({ case "kimi": case "kimi-for-coding": return ; + case "github-copilot": + case "github": + case "github-models": + // No dedicated GitHub mark in the local set; initials avoid mislabeling as OpenAI. + return ; + case "gitlab": + case "gitlab-duo": + return ; default: { - const lobeSrc = lobeProviderIconSrc(raw); + const lobeKey = raw === "togetherai" ? "together" : raw; + const lobeSrc = lobeProviderIconSrc(lobeKey); if (lobeSrc) { return ; } diff --git a/apps/desktop/src/renderer/lib/lobeProviderIconSrc.ts b/apps/desktop/src/renderer/lib/lobeProviderIconSrc.ts index 75fb08c135..53af3ecd6f 100644 --- a/apps/desktop/src/renderer/lib/lobeProviderIconSrc.ts +++ b/apps/desktop/src/renderer/lib/lobeProviderIconSrc.ts @@ -21,14 +21,18 @@ export const LOBE_PROVIDER_ICON_SRC: Record = { anthropic, openai, google: googleColor, + "google-vertex": googleColor, + "google-vertex-anthropic": googleColor, mistral: mistralColor, deepseek: deepseekColor, xai, + grok: xai, openrouter, ollama, lmstudio, groq, together: togetherColor, + togetherai: togetherColor, meta: metaColor, }; diff --git a/docs/features/onboarding-and-settings/agent-tools-cache.md b/docs/features/onboarding-and-settings/agent-tools-cache.md index 6f8f92db82..1fe90b48f5 100644 --- a/docs/features/onboarding-and-settings/agent-tools-cache.md +++ b/docs/features/onboarding-and-settings/agent-tools-cache.md @@ -129,7 +129,7 @@ profile — 650 MB must never roam. vendor/aarch64-apple-darwin/bin/codex .install-complete @anthropic-ai/claude-agent-sdk-darwin-arm64/0.3.220/ - opencode-darwin-arm64/1.15.5/ + opencode-darwin-arm64/1.18.14/ .staging/ .locks/ ``` @@ -242,10 +242,11 @@ manifest both answer false, because nothing is pending when nothing will ever be fetched. The probe deep-imports it rather than going through the barrel, which would pull the whole fetch/install stack in with it. -OpenCode's bundled probe walks, in order, `ADE_OPENCODE_BUNDLE_ROOT`, the -packaged `resourcesPath` roots (`app.asar.unpacked`, `app-.asar.unpacked`, -`app`), every `NODE_PATH` entry, `process.cwd()`, and then every `node_modules` -directory up the module's ancestor chain. +OpenCode's bundled probe uses `ADE_OPENCODE_BUNDLE_ROOT` as an explicit bundle +override when it is set. Otherwise it walks the packaged `resourcesPath` roots +(`app.asar.unpacked`, `app-.asar.unpacked`, `app`), every `NODE_PATH` +entry, `process.cwd()`, and then every `node_modules` directory up the module's +ancestor chain. ### Opt-outs @@ -446,7 +447,7 @@ Verified spawns from the cache: |---|---|---| | `codex` | `codex-cli 0.144.5` | accepted — Notarized Developer ID, OpenAI OpCo LLC | | `claude` | `2.1.220 (Claude Code)` | accepted — Notarized Developer ID, Anthropic PBC | -| `opencode` | `1.15.5` | **`invalid signature`** — ad-hoc / linker-signed, **not notarized** | +| `opencode` | `1.18.14` | **`invalid signature`** — ad-hoc / linker-signed, **not notarized** | OpenCode runs anyway. Its execution therefore depends entirely on the no-quarantine property above, not on notarization — if anything ever caused the From e4a9e3d6a5085bb45474e7de7976b7b6628bb303 Mon Sep 17 00:00:00 2001 From: Arul Sharma <31745423+arul28@users.noreply.github.com> Date: Thu, 6 Aug 2026 04:10:05 -0400 Subject: [PATCH 2/4] ship: iteration 1 - fix OpenCode and OAuth review findings --- .../opencode/openCodeBinaryManager.test.ts | 29 +++++++ .../opencode/openCodeBinaryManager.ts | 23 ++++-- .../settings/OAuthConnectModal.test.tsx | 78 +++++++++++++++++++ .../components/settings/OAuthConnectModal.tsx | 42 +++++++++- .../settings/OpenCodeProviderDetailModal.tsx | 2 +- .../settings/ProvidersSection.test.tsx | 37 +++++++-- .../components/settings/ProvidersSection.tsx | 3 +- .../components/shared/ProviderLogos.tsx | 3 +- 8 files changed, 198 insertions(+), 19 deletions(-) diff --git a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts index 2884718990..2b4466d15b 100644 --- a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts +++ b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.test.ts @@ -7,6 +7,9 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const childProcessMockState = vi.hoisted(() => ({ execFileSync: vi.fn(), })); +const cacheLookupMockState = vi.hoisted(() => ({ + cachedToolEntryPath: vi.fn(), +})); vi.mock("node:child_process", async () => { const actual = await vi.importActual("node:child_process"); @@ -16,6 +19,10 @@ vi.mock("node:child_process", async () => { }; }); +vi.mock("../../../../../ade-cli/src/services/tools/cacheLookup", () => ({ + cachedToolEntryPath: (..._args: unknown[]) => cacheLookupMockState.cachedToolEntryPath(), +})); + import { clearOpenCodeBinaryCache, probeOpenCodeBinaryQuarantine, @@ -74,6 +81,8 @@ describe("openCodeBinaryManager", () => { beforeEach(() => { childProcessMockState.execFileSync.mockReset(); + cacheLookupMockState.cachedToolEntryPath.mockReset(); + cacheLookupMockState.cachedToolEntryPath.mockReturnValue(null); tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-opencode-bin-")); homeDir = path.join(tempRoot, "home"); fs.mkdirSync(homeDir, { recursive: true }); @@ -151,6 +160,26 @@ describe("openCodeBinaryManager", () => { }); }); + it("keeps an explicit bundle root ahead of the tools cache and user install", () => { + process.env.ADE_OPENCODE_BUNDLE_ROOT = tempRoot; + const bundledPath = path.join(tempRoot, "node_modules", "opencode-ai", "bin", "opencode"); + const cachedPath = path.join(tempRoot, "cache", "opencode"); + const userPath = path.join(homeDir, ".npm-global", "bin", "opencode"); + makeExecutable(bundledPath); + makeExecutable(cachedPath); + makeExecutable(userPath); + cacheLookupMockState.cachedToolEntryPath.mockReturnValue(cachedPath); + + expect(resolveOpenCodeBinary()).toEqual({ + path: bundledPath, + source: "bundled", + }); + + fs.rmSync(bundledPath); + clearOpenCodeBinaryCache(); + expect(resolveOpenCodeBinary()).toEqual({ path: null, source: "missing" }); + }); + it("resolves the packaged Windows x64 layout to the baseline package", () => { // Mirrors what afterPack materializes for win32: the `-baseline` native // package plus an `opencode-ai` shell whose bin/ has been pruned. The AVX2 diff --git a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts index af9a4e67f3..80ed02f85b 100644 --- a/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts +++ b/apps/desktop/src/main/services/opencode/openCodeBinaryManager.ts @@ -146,6 +146,7 @@ export function resolveOpenCodeBinary(): OpenCodeBinaryInfo { return cachedInfo; } cachedInfo = null; + const explicitBundleRoot = process.env.ADE_OPENCODE_BUNDLE_ROOT?.trim(); // Ensure PATH includes shell paths and known CLI dirs before searching. // On Windows, `process.env.PATH = …` can create a duplicate `PATH` key @@ -160,10 +161,12 @@ export function resolveOpenCodeBinary(): OpenCodeBinaryInfo { // is fetched there rather than bundled. The manifest already encodes the // win32-x64 -> opencode-windows-x64-baseline substitution, so the AVX2 // avoidance documented on OPENCODE_PLATFORM_PACKAGES holds here too. - const cachedPath = cachedToolEntryPath("opencode"); - if (cachedPath && canRunBinaryCandidate(cachedPath)) { - cachedInfo = { path: cachedPath, source: "tools-cache" }; - return cachedInfo; + if (!explicitBundleRoot) { + const cachedPath = cachedToolEntryPath("opencode"); + if (cachedPath && canRunBinaryCandidate(cachedPath)) { + cachedInfo = { path: cachedPath, source: "tools-cache" }; + return cachedInfo; + } } const bundled = bundledBinaryCandidatePaths().find((candidate) => canRunBinaryCandidate(candidate)); @@ -174,10 +177,14 @@ export function resolveOpenCodeBinary(): OpenCodeBinaryInfo { } // 2. Fall back to user-installed binary (PATH, ~/.opencode/bin, etc.) - const userInstalled = resolveExecutableFromKnownLocations("opencode"); - if (userInstalled?.path) { - cachedInfo = { path: userInstalled.path, source: "user-installed" }; - return cachedInfo; + // An explicit bundle root is a hermetic override: never silently select a + // cache or user installation from a different runtime when it is incomplete. + if (!explicitBundleRoot) { + const userInstalled = resolveExecutableFromKnownLocations("opencode"); + if (userInstalled?.path) { + cachedInfo = { path: userInstalled.path, source: "user-installed" }; + return cachedInfo; + } } // Do not cache misses. Users can install OpenCode or fix PATH while ADE is diff --git a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx index a84d76b43b..98708b97b6 100644 --- a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx +++ b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.test.tsx @@ -7,6 +7,7 @@ import { OAuthConnectModal } from "./OAuthConnectModal"; describe("OAuthConnectModal", () => { const originalAde = globalThis.window.ade; + const originalClipboard = navigator.clipboard; beforeEach(() => { localStorage.clear(); @@ -31,6 +32,10 @@ describe("OAuthConnectModal", () => { afterEach(() => { cleanup(); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: originalClipboard, + }); globalThis.window.ade = originalAde; }); @@ -78,6 +83,79 @@ describe("OAuthConnectModal", () => { }); }); + it("persists the selected open target for the next sign-in", async () => { + const props = { + providerId: "openai", + providerName: "OpenAI", + methods: [{ type: "oauth" as const, label: "Sign in with ChatGPT" }], + onClose: vi.fn(), + onConnected: vi.fn(), + }; + const { unmount } = render(); + + fireEvent.change(screen.getByLabelText("Open sign-in link in"), { + target: { value: "ade-browser" }, + }); + await act(async () => { + screen.getByRole("button", { name: "Connect" }).click(); + }); + expect(localStorage.getItem("ade.opencode.oauthOpenTarget")).toBe("ade-browser"); + + unmount(); + render(); + expect((screen.getByLabelText("Open sign-in link in") as HTMLSelectElement).value).toBe("ade-browser"); + }); + + it("copies the OAuth URL when copy is selected", async () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, "clipboard", { + configurable: true, + value: { writeText }, + }); + render( + , + ); + + fireEvent.change(screen.getByLabelText("Open sign-in link in"), { + target: { value: "copy" }, + }); + await act(async () => { + screen.getByRole("button", { name: "Connect" }).click(); + }); + + expect(writeText).toHaveBeenCalledWith("https://example.com/oauth"); + expect(window.ade.app.openExternal).not.toHaveBeenCalled(); + }); + + it("lets a full OAuth link be hidden after starting in view mode", async () => { + render( + , + ); + + fireEvent.change(screen.getByLabelText("Open sign-in link in"), { + target: { value: "view" }, + }); + await act(async () => { + screen.getByRole("button", { name: "Connect" }).click(); + }); + + expect(screen.getByText("https://example.com/oauth")).toBeTruthy(); + fireEvent.click(screen.getByRole("button", { name: "Hide" })); + expect(screen.queryByText("https://example.com/oauth")).toBeNull(); + }); + it("rejects an unsafe OAuth URL before opening it", async () => { window.ade.ai.opencodeOAuthStart = vi.fn().mockResolvedValue({ url: "http://example.com/oauth", diff --git a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx index a49a7db864..7f0ff94120 100644 --- a/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx +++ b/apps/desktop/src/renderer/components/settings/OAuthConnectModal.tsx @@ -26,10 +26,14 @@ const OPEN_TARGET_OPTIONS: Array<{ id: OAuthOpenTarget; label: string }> = [ { id: "view", label: "View full link" }, ]; +function isOAuthOpenTarget(value: string | null): value is OAuthOpenTarget { + return OPEN_TARGET_OPTIONS.some((option) => option.id === value); +} + function readStoredOpenTarget(): OAuthOpenTarget { try { const raw = localStorage.getItem(OPEN_TARGET_STORAGE_KEY); - if (raw === "ade-browser" || raw === "system" || raw === "copy" || raw === "view") return raw; + if (isOAuthOpenTarget(raw)) return raw; } catch { // ignore } @@ -204,6 +208,40 @@ export function OAuthConnectModal({ }; }, []); + // Keep keyboard focus inside the top-most dialog while OAuth is open. The + // provider detail dialog remains mounted behind this portal. + useEffect(() => { + const node = dialogRef.current; + if (!node) return; + const onKey = (event: KeyboardEvent) => { + if (event.key !== "Tab") return; + const focusable = Array.from( + node.querySelectorAll( + 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])', + ), + ); + if (focusable.length === 0) { + event.preventDefault(); + node.focus(); + return; + } + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + const active = document.activeElement; + if (event.shiftKey) { + if (active === node || active === first) { + event.preventDefault(); + last.focus(); + } + } else if (active === node || active === last) { + event.preventDefault(); + first.focus(); + } + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, []); + const startFlow = async () => { cancelRequestedRef.current = false; startPendingRef.current = true; @@ -520,7 +558,7 @@ export function OAuthConnectModal({ {showFullUrl ? "Hide" : "View full link"}
- {showFullUrl || openTarget === "view" ? ( + {showFullUrl ? ( Could not load OpenCode sign-in methods - {authMethodsError ? ` (${authMethodsError})` : ""}. SuperGrok, ChatGPT, and Copilot OAuth may be unavailable until this succeeds. + ({authMethodsError}). SuperGrok, ChatGPT, and Copilot OAuth may be unavailable until this succeeds. {onRetryAuthMethods ? (