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) } : {}),
}