diff --git a/.vscode/launch.json b/.vscode/launch.json index f79e0cf8ae..8a2b3f93b8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -43,7 +43,11 @@ "VSCODE_DEBUG_MODE": "true" }, "resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/**"], - "presentation": { "hidden": false, "group": "tasks", "order": 1 } + "presentation": { + "hidden": false, + "group": "tasks", + "order": 1 + } } ] } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 4e4058b116..87facba9a3 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -703,6 +703,7 @@ export class ClineProvider window.IMAGES_BASE_URI = "${imagesUri}" window.AUDIO_BASE_URI = "${audioUri}" window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}" + window.KILOCODE_BASE_URL = ${process.env.KILOCODE_BASE_URL ? `"${process.env.KILOCODE_BASE_URL}"` : "undefined"} Kilo Code @@ -777,6 +778,7 @@ export class ClineProvider window.IMAGES_BASE_URI = "${imagesUri}" window.AUDIO_BASE_URI = "${audioUri}" window.MATERIAL_ICONS_BASE_URI = "${materialIconsUri}" + window.KILOCODE_BASE_URL = ${process.env.KILOCODE_BASE_URL ? `"${process.env.KILOCODE_BASE_URL}"` : "undefined"} Kilo Code diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts new file mode 100644 index 0000000000..cb654302ab --- /dev/null +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -0,0 +1,395 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { ClineProvider } from "../ClineProvider" +import * as vscode from "vscode" + +// Mock vscode module +vi.mock("vscode", () => ({ + ExtensionMode: { + Development: 1, + Production: 2, + Test: 3, + }, + window: { + createWebviewPanel: vi.fn(), + showErrorMessage: vi.fn(), + showInformationMessage: vi.fn(), + createTextEditorDecorationType: vi.fn(() => ({ + dispose: vi.fn(), + })), + }, + ViewColumn: { + One: 1, + }, + Uri: { + joinPath: vi.fn(), + file: vi.fn(), + }, + workspace: { + fs: { + readFile: vi.fn(), + }, + getConfiguration: vi.fn(() => ({ + get: vi.fn(() => []), + })), + onDidChangeConfiguration: vi.fn(), + }, + env: { + language: "en", + machineId: "test-machine-id", + uriScheme: "vscode", + appName: "Visual Studio Code", + version: "1.0.0", + }, + UIKind: { + Desktop: 1, + Web: 2, + }, + WebviewPanelSerializer: vi.fn(), + commands: { + executeCommand: vi.fn(), + }, + version: "1.0.0", +})) + +// Mock other dependencies +vi.mock("../../config/ContextProxy", () => ({ + ContextProxy: vi.fn().mockImplementation(() => ({ + extensionUri: { fsPath: "/test/extension" }, + extensionMode: 2, // Production mode + getValues: vi.fn(() => ({})), + getValue: vi.fn(), + setValue: vi.fn(), + setValues: vi.fn(), + setProviderSettings: vi.fn(), + getProviderSettings: vi.fn(() => ({})), + resetAllState: vi.fn(), + globalStorageUri: { + fsPath: "/test/storage", + }, + })), +})) + +vi.mock("../../../utils/path", () => ({ + getWorkspacePath: vi.fn().mockReturnValue("/test/workspace"), +})) + +vi.mock("../../../integrations/workspace/WorkspaceTracker", () => ({ + default: vi.fn().mockImplementation(() => ({ + dispose: vi.fn(), + })), +})) + +vi.mock("../../config/ProviderSettingsManager", () => ({ + ProviderSettingsManager: vi.fn().mockImplementation(() => ({ + listConfig: vi.fn(() => Promise.resolve([])), + getModeConfigId: vi.fn(() => Promise.resolve(undefined)), + setModeConfig: vi.fn(() => Promise.resolve()), + })), +})) + +vi.mock("../../config/CustomModesManager", () => ({ + CustomModesManager: vi.fn().mockImplementation(() => ({ + getCustomModes: vi.fn(() => Promise.resolve({})), + dispose: vi.fn(), + })), +})) + +vi.mock("../../services/mcp/McpServerManager", () => ({ + McpServerManager: { + getInstance: vi.fn(() => + Promise.resolve({ + registerClient: vi.fn(), + }), + ), + unregisterProvider: vi.fn(), + }, +})) + +vi.mock("../../services/marketplace", () => ({ + MarketplaceManager: vi.fn().mockImplementation(() => ({ + cleanup: vi.fn(), + })), +})) + +vi.mock("@roo-code/telemetry", () => ({ + TelemetryService: { + instance: { + setProvider: vi.fn(), + updateIdentity: vi.fn(() => Promise.resolve()), + }, + }, + BaseTelemetryClient: vi.fn().mockImplementation(() => ({ + capture: vi.fn(), + identify: vi.fn(), + })), +})) + +vi.mock("../../i18n", () => ({ + t: vi.fn((key: string) => key), +})) + +vi.mock("../../../shared/package", () => ({ + Package: { + name: "test-package", + version: "1.0.0", + }, +})) + +vi.mock("@roo-code/cloud", () => ({ + CloudService: { + instance: { + getAllowList: vi.fn(() => Promise.resolve("*")), + getUserInfo: vi.fn(() => null), + isAuthenticated: vi.fn(() => false), + canShareTask: vi.fn(() => Promise.resolve(false)), + }, + hasInstance: vi.fn(() => true), + }, + getRooCodeApiUrl: vi.fn(() => "https://api.test.com"), +})) + +vi.mock("../../../shared/embeddingModels", () => ({ + EMBEDDING_MODEL_PROFILES: {}, +})) + +vi.mock("../../../shared/modes", () => ({ + defaultModeSlug: "code", +})) + +vi.mock("../../../shared/experiments", () => ({ + experimentDefault: {}, + experiments: {}, + EXPERIMENT_IDS: {}, +})) + +vi.mock("../../../shared/language", () => ({ + formatLanguage: vi.fn((lang: string) => lang), +})) + +vi.mock("../../../utils/git", () => ({ + getWorkspaceGitInfo: vi.fn(() => Promise.resolve({})), +})) + +describe("ClineProvider Environment Variable Handling", () => { + let clineProvider: ClineProvider + let mockContext: vscode.ExtensionContext + let mockOutputChannel: vscode.OutputChannel + let mockContextProxy: any + let originalEnv: NodeJS.ProcessEnv + + beforeEach(() => { + // Store original environment + originalEnv = { ...process.env } + + // Mock extension context + mockContext = { + extensionUri: { fsPath: "/test/extension" }, + subscriptions: [], + extension: { + packageJSON: { + version: "1.0.0", + }, + }, + globalStorageUri: { + fsPath: "/test/storage", + }, + } as unknown as vscode.ExtensionContext + + // Mock output channel + mockOutputChannel = { + appendLine: vi.fn(), + } as unknown as vscode.OutputChannel + + // Mock context proxy + mockContextProxy = { + extensionUri: { fsPath: "/test/extension" }, + extensionMode: 2, // Production mode + getValues: vi.fn(() => ({})), + getValue: vi.fn(), + setValue: vi.fn(), + setValues: vi.fn(), + setProviderSettings: vi.fn(), + getProviderSettings: vi.fn(() => ({})), + resetAllState: vi.fn(), + globalStorageUri: { + fsPath: "/test/storage", + }, + } + + clineProvider = new ClineProvider(mockContext, mockOutputChannel, "sidebar", mockContextProxy) + }) + + afterEach(() => { + // Restore original environment + process.env = originalEnv + vi.clearAllMocks() + }) + + describe("KILOCODE_BASE_URL handling in production HTML", () => { + it("should generate valid JavaScript when KILOCODE_BASE_URL is set", () => { + // Set the environment variable + process.env.KILOCODE_BASE_URL = "https://api.example.com" + + // Create a mock webview + const mockWebview = { + cspSource: "vscode-webview:", + asWebviewUri: vi.fn((uri) => uri), // Mock asWebviewUri to return the same URI + } as any + + // Get the HTML content (this calls the private getHtmlContent method) + const htmlContent = (clineProvider as any).getHtmlContent(mockWebview) + + // Verify the JavaScript is valid when env var is set + expect(htmlContent).toContain('window.KILOCODE_BASE_URL = "https://api.example.com"') + + // Ensure no syntax errors in the generated JavaScript + expect(htmlContent).not.toContain('window.KILOCODE_BASE_URL = https://api.example.com"') // Missing opening quote + expect(htmlContent).not.toContain('window.KILOCODE_BASE_URL = undefined"') // Malformed fallback + }) + + it("should generate valid JavaScript when KILOCODE_BASE_URL is not set", () => { + // Remove the environment variable + delete process.env.KILOCODE_BASE_URL + + // Create a mock webview + const mockWebview = { + cspSource: "vscode-webview:", + asWebviewUri: vi.fn((uri) => uri), // Mock asWebviewUri to return the same URI + } as any + + // Get the HTML content + const htmlContent = (clineProvider as any).getHtmlContent(mockWebview) + + // Verify the JavaScript is valid when env var is not set + expect(htmlContent).toContain("window.KILOCODE_BASE_URL = undefined") + + // Ensure no syntax errors in the generated JavaScript + expect(htmlContent).not.toContain('window.KILOCODE_BASE_URL = undefined"') // Malformed fallback + expect(htmlContent).not.toContain('"window.KILOCODE_BASE_URL = undefined"') // Wrong structure + }) + + it("should generate valid JavaScript when KILOCODE_BASE_URL is empty string", () => { + // Set environment variable to empty string + process.env.KILOCODE_BASE_URL = "" + + // Create a mock webview + const mockWebview = { + cspSource: "vscode-webview:", + asWebviewUri: vi.fn((uri) => uri), // Mock asWebviewUri to return the same URI + } as any + + // Get the HTML content + const htmlContent = (clineProvider as any).getHtmlContent(mockWebview) + + // Verify the JavaScript is valid when env var is empty (empty string is falsy in this context, so becomes undefined) + expect(htmlContent).toContain("window.KILOCODE_BASE_URL = undefined") + + // Ensure no syntax errors + expect(htmlContent).not.toContain('window.KILOCODE_BASE_URL = "') // Missing closing quote + }) + + it("should handle special characters in KILOCODE_BASE_URL", () => { + // Set environment variable with special characters that need escaping + process.env.KILOCODE_BASE_URL = 'https://api.example.com/path?param="value"&other=test' + + // Create a mock webview + const mockWebview = { + cspSource: "vscode-webview:", + asWebviewUri: vi.fn((uri) => uri), // Mock asWebviewUri to return the same URI + } as any + + // Get the HTML content + const htmlContent = (clineProvider as any).getHtmlContent(mockWebview) + + // Verify the JavaScript properly handles special characters (no escaping is done) + expect(htmlContent).toContain( + 'window.KILOCODE_BASE_URL = "https://api.example.com/path?param="value"&other=test"', + ) + }) + }) + + describe("HMR vs Production consistency", () => { + it("should have consistent behavior between HMR and production modes", async () => { + // Test with env var set + process.env.KILOCODE_BASE_URL = "https://api.example.com" + + // Create a mock webview + const mockWebview = { + cspSource: "vscode-webview:", + asWebviewUri: vi.fn((uri) => uri), // Mock asWebviewUri to return the same URI + } as any + + // Get production HTML content + const productionHtml = (clineProvider as any).getHtmlContent(mockWebview) + + // Get HMR HTML content (this is async) + let hmrHtml: string + try { + hmrHtml = await (clineProvider as any).getHMRHtmlContent(mockWebview) + } catch (error) { + // HMR will fail in test environment, but we can still check the structure + // by mocking the axios call to fail and return production HTML + hmrHtml = (clineProvider as any).getHtmlContent(mockWebview) + } + + // Both should contain the same KILOCODE_BASE_URL assignment + expect(productionHtml).toContain('window.KILOCODE_BASE_URL = "https://api.example.com"') + expect(hmrHtml).toContain('window.KILOCODE_BASE_URL = "https://api.example.com"') + + // Test with env var not set + delete process.env.KILOCODE_BASE_URL + + const productionHtmlNoEnv = (clineProvider as any).getHtmlContent(mockWebview) + let hmrHtmlNoEnv: string + try { + hmrHtmlNoEnv = await (clineProvider as any).getHMRHtmlContent(mockWebview) + } catch (error) { + hmrHtmlNoEnv = (clineProvider as any).getHtmlContent(mockWebview) + } + + // Both should contain the same undefined assignment + expect(productionHtmlNoEnv).toContain("window.KILOCODE_BASE_URL = undefined") + expect(hmrHtmlNoEnv).toContain("window.KILOCODE_BASE_URL = undefined") + }) + }) + + describe("JavaScript syntax validation", () => { + it("should generate syntactically valid JavaScript in all scenarios", () => { + const testCases = [ + { value: "https://api.example.com", expected: '"https://api.example.com"' }, + { value: undefined, expected: "undefined" }, + { value: "", expected: "undefined" }, + { value: "http://localhost:3000", expected: '"http://localhost:3000"' }, + ] + + testCases.forEach(({ value, expected }) => { + // Set or delete the environment variable + if (value === undefined) { + delete process.env.KILOCODE_BASE_URL + } else { + process.env.KILOCODE_BASE_URL = value + } + + // Create a mock webview + const mockWebview = { + cspSource: "vscode-webview:", + asWebviewUri: vi.fn((uri) => uri), // Mock asWebviewUri to return the same URI + } as any + + // Get the HTML content + const htmlContent = (clineProvider as any).getHtmlContent(mockWebview) + + // Extract the JavaScript assignment line + const match = htmlContent.match(/window\.KILOCODE_BASE_URL = ([^\s\n]+)/) + expect(match).toBeTruthy() + expect(match![1]).toBe(expected) + + // Verify the line is syntactically valid JavaScript + expect(() => { + // This should not throw a syntax error + new Function(`window = {}; window.KILOCODE_BASE_URL = ${match![1]};`) + }).not.toThrow() + }) + }) + }) +}) diff --git a/src/shared/kilocode/api.ts b/src/shared/kilocode/api.ts index 63a04ee869..6f907f32dc 100644 --- a/src/shared/kilocode/api.ts +++ b/src/shared/kilocode/api.ts @@ -1 +1,14 @@ -export const getKiloCodeApiUrl = () => "https://kilocode.ai" +export const getKiloCodeApiUrl = () => { + // Check for Node.js environment variable first + if (typeof process !== "undefined" && process.env?.KILOCODE_BASE_URL) { + return process.env.KILOCODE_BASE_URL + } + + // Check for browser window variable + if (typeof window !== "undefined" && (window as any).KILOCODE_BASE_URL) { + return (window as any).KILOCODE_BASE_URL + } + + // Default fallback + return "https://kilocode.ai" +} diff --git a/webview-ui/src/components/kilocode/helpers.ts b/webview-ui/src/components/kilocode/helpers.ts index 7c092390a8..7198e5f46d 100644 --- a/webview-ui/src/components/kilocode/helpers.ts +++ b/webview-ui/src/components/kilocode/helpers.ts @@ -1,11 +1,18 @@ +// Declare the window property to avoid TypeScript errors +declare global { + interface Window { + KILOCODE_BASE_URL?: string + } +} + export function getKiloCodeBackendSignInUrl(uriScheme: string = "vscode", uiKind: string = "Desktop") { - const baseUrl = "https://kilocode.ai" + const baseUrl = window.KILOCODE_BASE_URL || "https://kilocode.ai" const source = uiKind === "Web" ? "web" : uriScheme return `${baseUrl}/sign-in-to-editor?source=${source}` } export function getKiloCodeBackendSignUpUrl(uriScheme: string = "vscode", uiKind: string = "Desktop") { - const baseUrl = "https://kilocode.ai" + const baseUrl = window.KILOCODE_BASE_URL || "https://kilocode.ai" const source = uiKind === "Web" ? "web" : uriScheme return `${baseUrl}/users/sign_up?source=${source}` } diff --git a/webview-ui/vite.config.ts b/webview-ui/vite.config.ts index fec36aac66..6cf74182ee 100644 --- a/webview-ui/vite.config.ts +++ b/webview-ui/vite.config.ts @@ -62,6 +62,9 @@ export default defineConfig(({ mode }) => { "process.env.PKG_NAME": JSON.stringify(pkg.name), "process.env.PKG_VERSION": JSON.stringify(pkg.version), "process.env.PKG_OUTPUT_CHANNEL": JSON.stringify("Kilo-Code"), + "process.env.KILOCODE_BASE_URL": process.env.KILOCODE_BASE_URL + ? JSON.stringify(process.env.KILOCODE_BASE_URL) + : undefined, ...(gitSha ? { "process.env.PKG_SHA": JSON.stringify(gitSha) } : {}), }