diff --git a/src/api/index.ts b/src/api/index.ts index 48a0a89ec5..6c70a1485d 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -143,6 +143,8 @@ export function buildApiHandler(configuration: ProviderSettings): ApiHandler { case "io-intelligence": return new IOIntelligenceHandler(options) case "roo": + // Never throw exceptions from provider constructors + // The provider-proxy server will handle authentication and return appropriate error codes return new RooHandler(options) case "featherless": return new FeatherlessHandler(options) diff --git a/src/api/providers/__tests__/roo.spec.ts b/src/api/providers/__tests__/roo.spec.ts index 093137e1b2..2d22f992a0 100644 --- a/src/api/providers/__tests__/roo.spec.ts +++ b/src/api/providers/__tests__/roo.spec.ts @@ -131,21 +131,25 @@ describe("RooHandler", () => { expect(handler.getModel().id).toBe(mockOptions.apiModelId) }) - it("should throw error if CloudService is not available", () => { + it("should not throw error if CloudService is not available", () => { mockHasInstanceFn.mockReturnValue(false) expect(() => { new RooHandler(mockOptions) - }).toThrow("Authentication required for Roo Code Cloud") - expect(t).toHaveBeenCalledWith("common:errors.roo.authenticationRequired") + }).not.toThrow() + // Constructor should succeed even without CloudService + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) }) - it("should throw error if session token is not available", () => { + it("should not throw error if session token is not available", () => { mockHasInstanceFn.mockReturnValue(true) mockGetSessionTokenFn.mockReturnValue(null) expect(() => { new RooHandler(mockOptions) - }).toThrow("Authentication required for Roo Code Cloud") - expect(t).toHaveBeenCalledWith("common:errors.roo.authenticationRequired") + }).not.toThrow() + // Constructor should succeed even without session token + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) }) it("should initialize with default model if no model specified", () => { @@ -400,7 +404,7 @@ describe("RooHandler", () => { expect(mockGetSessionTokenFn).toHaveBeenCalled() }) - it("should handle undefined auth service", () => { + it("should handle undefined auth service gracefully", () => { mockHasInstanceFn.mockReturnValue(true) // Mock CloudService with undefined authService const originalGetter = Object.getOwnPropertyDescriptor(CloudService, "instance")?.get @@ -413,7 +417,10 @@ describe("RooHandler", () => { expect(() => { new RooHandler(mockOptions) - }).toThrow("Authentication required for Roo Code Cloud") + }).not.toThrow() + // Constructor should succeed even with undefined auth service + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) } finally { // Always restore original getter, even if test fails if (originalGetter) { @@ -425,12 +432,15 @@ describe("RooHandler", () => { } }) - it("should handle empty session token", () => { + it("should handle empty session token gracefully", () => { mockGetSessionTokenFn.mockReturnValue("") expect(() => { new RooHandler(mockOptions) - }).toThrow("Authentication required for Roo Code Cloud") + }).not.toThrow() + // Constructor should succeed even with empty session token + const handler = new RooHandler(mockOptions) + expect(handler).toBeInstanceOf(RooHandler) }) }) }) diff --git a/src/api/providers/roo.ts b/src/api/providers/roo.ts index d986d6cd10..8611acd25c 100644 --- a/src/api/providers/roo.ts +++ b/src/api/providers/roo.ts @@ -4,29 +4,27 @@ import { CloudService } from "@roo-code/cloud" import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream } from "../transform/stream" -import { t } from "../../i18n" import type { ApiHandlerCreateMessageMetadata } from "../index" import { BaseOpenAiCompatibleProvider } from "./base-openai-compatible-provider" export class RooHandler extends BaseOpenAiCompatibleProvider { constructor(options: ApiHandlerOptions) { - // Check if CloudService is available and get the session token. - if (!CloudService.hasInstance()) { - throw new Error(t("common:errors.roo.authenticationRequired")) - } - - const sessionToken = CloudService.instance.authService?.getSessionToken() + // Get the session token if available, but don't throw if not. + // The server will handle authentication errors and return appropriate status codes. + let sessionToken = "" - if (!sessionToken) { - throw new Error(t("common:errors.roo.authenticationRequired")) + if (CloudService.hasInstance()) { + sessionToken = CloudService.instance.authService?.getSessionToken() || "" } + // Always construct the handler, even without a valid token. + // The provider-proxy server will return 401 if authentication fails. super({ ...options, providerName: "Roo Code Cloud", baseURL: process.env.ROO_CODE_PROVIDER_URL ?? "https://api.roocode.com/proxy/v1", - apiKey: sessionToken, + apiKey: sessionToken || "unauthenticated", // Use a placeholder if no token defaultProviderModelId: rooDefaultModelId, providerModels: rooModels, defaultTemperature: 0.7, diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 5e4971ecaf..427303b497 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -288,7 +288,24 @@ export const webviewMessageHandler = async ( // Initializing new instance of Cline will make sure that any // agentically running promises in old instance don't affect our new // task. This essentially creates a fresh slate for the new task. - await provider.createTask(message.text, message.images) + try { + await provider.createTask(message.text, message.images) + // Task created successfully - notify the UI to reset + await provider.postMessageToWebview({ + type: "invoke", + invoke: "newChat", + }) + } catch (error) { + // For all errors, reset the UI and show error + await provider.postMessageToWebview({ + type: "invoke", + invoke: "newChat", + }) + // Show error to user + vscode.window.showErrorMessage( + `Failed to create task: ${error instanceof Error ? error.message : String(error)}`, + ) + } break case "customInstructions": await provider.updateCustomInstructions(message.text)