Compare commits

...

9 Commits

Author SHA1 Message Date
abeatrix b49b460284 Add E2E testing environment configuration and staging URL support
- Configure E2E tests to use staging environment automatically
- Add clineAppBaseUrl to extension state for dynamic URL handling
- Display staging URL in welcome view button tooltip for testing
- Move environment logging to config getter function
- Update E2E auth test to verify staging URL is displayed correctly
2025-07-21 12:57:07 -07:00
0xtoshii 516c210678 merge fixes 2025-07-21 10:35:41 -07:00
Toshii 77e9f0040d Merge branch 'main' into bee/fix-launch-config 2025-07-21 10:16:58 -07:00
0xtoshii d53d65468f change to app url 2025-07-21 09:56:33 -07:00
0xtoshii a966632867 fallback changed to app 2025-07-21 09:52:04 -07:00
0xtoshii a7d8959218 local option 2025-07-21 08:00:05 -07:00
abeatrix 761551cda6 replace apiBaseUrl with appBaseUrl 2025-07-18 17:19:41 -07:00
abeatrix 4fce90b1de fix credit uri 2025-07-18 15:56:20 -07:00
abeatrix 5c21ba321f Fix CLINE_ENVIRONMENT configuration not being passed to webview
## Problem

The CLINE_ENVIRONMENT configuration set in launch.json was not being properly passed to the webview, causing the webview to break when trying to access environment-specific configurations.

This resulted in:

Webview using incorrect API URLs (always defaulting to production)
Broken authentication flows in development/staging environments
Inconsistent behavior between the main extension and webview components

## Root Cause

The issue occurred because:

Duplicate Configuration Logic: The webview had its own separate config.ts file that was trying to read process.env.CLINE_ENVIRONMENT directly
Environment Variable Propagation: While Vite was configured to pass CLINE_ENVIRONMENT to the build process, the webview's runtime code couldn't access this environment variable properly
Configuration Mismatch: The main extension and webview were using different configuration sources, leading to inconsistent environment settings
2025-07-18 15:14:11 -07:00
15 changed files with 114 additions and 90 deletions
+3
View File
@@ -15,6 +15,9 @@
"env": {
"IS_DEV": "true",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
// Use the environment variable to determine the backend URL
// Options: "production", "staging", "local"
// Require the extension to reload to apply changes
"CLINE_ENVIRONMENT": "production"
}
},
+1
View File
@@ -53,6 +53,7 @@ message UserInfo {
optional string display_name = 2;
optional string email = 3;
optional string photo_url = 4;
optional string app_base_url = 5; // Cline app base URL
}
message UserOrganization {
+63 -41
View File
@@ -1,6 +1,8 @@
export type Environment = "production" | "staging" | "local"
const CLINE_ENVIRONMENT: Environment = (process.env.CLINE_ENVIRONMENT as Environment) || "production"
export enum Environment {
production = "production",
staging = "staging",
local = "local",
}
interface EnvironmentConfig {
appBaseUrl: string
@@ -16,43 +18,63 @@ interface EnvironmentConfig {
}
}
const configs: Record<Environment, EnvironmentConfig> = {
production: {
appBaseUrl: "https://app.cline.bot",
apiBaseUrl: "https://api.cline.bot",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
authDomain: "cline-prod.firebaseapp.com",
projectId: "cline-prod",
storageBucket: "cline-prod.firebasestorage.app",
messagingSenderId: "941048379330",
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
},
},
staging: {
appBaseUrl: "https://staging-app.cline.bot",
apiBaseUrl: "https://core-api.staging.int.cline.bot",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
authDomain: "cline-staging.firebaseapp.com",
projectId: "cline-staging",
storageBucket: "cline-staging.firebasestorage.app",
messagingSenderId: "853479478430",
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
},
},
local: {
appBaseUrl: "http://localhost:3000",
apiBaseUrl: "http://localhost:7777",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
authDomain: "cline-preview.firebaseapp.com",
projectId: "cline-preview",
},
},
function getClineEnv(): Environment {
const isE2ETesting = process?.env?.E2E_TEST === "true"
const _env = isE2ETesting ? Environment.staging : process?.env?.CLINE_ENVIRONMENT
if (_env && Object.values(Environment).includes(_env as Environment)) {
return _env as Environment
}
return Environment.production
}
export const clineEnvConfig = configs[CLINE_ENVIRONMENT]
// Config getter function to avoid storing all configs in memory
function getEnvironmentConfig(env: Environment): EnvironmentConfig {
console.info("Cline environment:", CLINE_ENVIRONMENT)
switch (env) {
case Environment.staging:
return {
appBaseUrl: "https://staging-app.cline.bot",
apiBaseUrl: "https://core-api.staging.int.cline.bot",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyASSwkwX1kSO8vddjZkE5N19QU9cVQ0CIk",
authDomain: "cline-staging.firebaseapp.com",
projectId: "cline-staging",
storageBucket: "cline-staging.firebasestorage.app",
messagingSenderId: "853479478430",
appId: "1:853479478430:web:2de0dba1c63c3262d4578f",
},
}
case Environment.local:
return {
appBaseUrl: "http://localhost:3000",
apiBaseUrl: "http://localhost:7777",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyD8wtkd1I-EICuAg6xgAQpRdwYTvwxZG2w",
authDomain: "cline-preview.firebaseapp.com",
projectId: "cline-preview",
},
}
default:
return {
appBaseUrl: "https://app.cline.bot",
apiBaseUrl: "https://api.cline.bot",
mcpBaseUrl: "https://api.cline.bot/v1/mcp",
firebase: {
apiKey: "AIzaSyC5rx59Xt8UgwdU3PCfzUF7vCwmp9-K2vk",
authDomain: "cline-prod.firebaseapp.com",
projectId: "cline-prod",
storageBucket: "cline-prod.firebasestorage.app",
messagingSenderId: "941048379330",
appId: "1:941048379330:web:45058eedeefc5cdfcc485b",
},
}
}
}
// Get environment once at module load
const CLINE_ENVIRONMENT = getClineEnv()
const _configCache = getEnvironmentConfig(CLINE_ENVIRONMENT)
export const clineEnvConfig = _configCache
+2 -1
View File
@@ -34,7 +34,7 @@ import { sendStateUpdate } from "./state/subscribeToState"
import { sendAddToInputEvent } from "./ui/subscribeToAddToInput"
import { sendMcpMarketplaceCatalogEvent } from "./mcp/subscribeToMcpMarketplaceCatalog"
import { AuthService } from "@/services/auth/AuthService"
import { ShowMessageRequest, ShowMessageType } from "@/shared/proto/host/window"
import { ShowMessageType } from "@/shared/proto/host/window"
import { getHostBridgeProvider } from "@/hosts/host-providers"
import { clineEnvConfig } from "@/config"
@@ -744,6 +744,7 @@ export class Controller {
return {
version: this.context.extension?.packageJSON?.version ?? "",
clineAppBaseUrl: clineEnvConfig.appBaseUrl,
apiConfiguration,
uriScheme: vscode.env.uriScheme,
currentTaskItem: this.task?.taskId ? (taskHistory || []).find((item) => item.id === this.task?.taskId) : undefined,
+14 -7
View File
@@ -1,11 +1,11 @@
import vscode from "vscode"
import { EmptyRequest, String } from "../../shared/proto/common"
import { AuthState, UserInfo } from "../../shared/proto/account"
import { StreamingResponseHandler, getRequestRegistry } from "@/core/controller/grpc-handler"
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
import { Controller } from "@/core/controller"
import { storeSecret } from "@/core/storage/state"
import { clineEnvConfig } from "@/config"
import { Controller } from "@/core/controller"
import { getRequestRegistry, type StreamingResponseHandler } from "@/core/controller/grpc-handler"
import { storeSecret } from "@/core/storage/state"
import { AuthState, UserInfo } from "../../shared/proto/account"
import { type EmptyRequest, String } from "../../shared/proto/common"
import { FirebaseAuthProvider } from "./providers/FirebaseAuthProvider"
import { openExternal } from "@/utils/env"
const DefaultClineAccountURI = `${clineEnvConfig.appBaseUrl}/auth`
@@ -32,6 +32,10 @@ export interface ClineAccountUserInfo {
email: string
id: string
organizations: ClineAccountOrganization[]
/**
* Cline app base URL, used for webview UI and other client-side operations
*/
appBaseUrl?: string
}
export interface ClineAccountOrganization {
@@ -156,17 +160,20 @@ export class AuthService {
let user: any = null
if (this._clineAuthInfo && this._authenticated) {
const userInfo = this._clineAuthInfo.userInfo
this._clineAuthInfo.userInfo.appBaseUrl = clineEnvConfig?.appBaseUrl
user = UserInfo.create({
// TODO: create proto for new user info type
uid: userInfo?.id,
displayName: userInfo?.displayName,
email: userInfo?.email,
photoUrl: undefined,
appBaseUrl: userInfo?.appBaseUrl,
})
}
return AuthState.create({
user: user,
user,
})
}
+1
View File
@@ -29,6 +29,7 @@ export const DEFAULT_PLATFORM = "unknown"
export interface ExtensionState {
isNewUser: boolean
welcomeViewCompleted: boolean
clineAppBaseUrl: string
apiConfiguration?: ApiConfiguration
autoApprovalSettings: AutoApprovalSettings
browserSettings: BrowserSettings
+1
View File
@@ -2,4 +2,5 @@ export interface UserInfo {
displayName?: string
email?: string
photoUrl?: string
apiBaseUrl?: string // Base URL for API requests
}
+4
View File
@@ -6,6 +6,10 @@ e2e("Auth - can set up API keys", async ({ page, sidebar }) => {
await expect(sidebar.getByRole("button", { name: "Get Started for Free" })).toBeVisible()
await expect(sidebar.getByRole("button", { name: "Use your own API key" })).toBeVisible()
// Check if Sign In button has title attribute set to staging URL
await sidebar.getByRole("button", { name: "Get Started for Free" }).hover()
await expect(sidebar.getByTitle("https://staging-app.cline.bot")).toBeVisible()
// Navigate to API key setup
await sidebar.getByRole("button", { name: "Use your own API key" }).click()
@@ -1,4 +1,3 @@
import { clineEnvConfig } from "@/config"
import { useClineAuth } from "@/context/ClineAuthContext"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { AccountServiceClient } from "@/services/grpc-client"
@@ -14,10 +13,10 @@ import {
VSCodeOption,
VSCodeTag,
} from "@vscode/webview-ui-toolkit/react"
import { memo, useCallback, useEffect, useRef, useState } from "react"
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
import ClineLogoWhite from "../../assets/ClineLogoWhite"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
import CreditsHistoryTable from "./CreditsHistoryTable"
import VSCodeButtonLink from "../common/VSCodeButtonLink"
// Custom hook for animated credit display with styled decimals
const useAnimatedCredits = (targetValue: number, duration: number = 660) => {
@@ -35,7 +34,7 @@ const useAnimatedCredits = (targetValue: number, duration: number = 660) => {
const progress = Math.min(elapsed / duration, 1)
// Easing function (ease-out)
const easedProgress = 1 - Math.pow(1 - progress, 3)
const easedProgress = 1 - (1 - progress) ** 3
const newValue = easedProgress * targetValue
setCurrentValue(newValue)
@@ -112,11 +111,13 @@ const getMainRole = (roles?: string[]) => {
return "Member"
}
const CLINE_APP_URL = "https://app.cline.bot"
export const ClineAccountView = () => {
const { clineUser, handleSignIn, handleSignOut } = useClineAuth()
const { userInfo, apiConfiguration } = useExtensionState()
let user = apiConfiguration?.clineAccountId ? clineUser || userInfo : undefined
const user = apiConfiguration?.clineAccountId ? clineUser || userInfo : undefined
const [balance, setBalance] = useState<number | null>(null)
const [userOrganizations, setUserOrganizations] = useState<UserOrganization[]>([])
@@ -127,9 +128,18 @@ export const ClineAccountView = () => {
const [paymentsData, setPaymentsData] = useState<PaymentTransaction[]>([])
const intervalRef = useRef<NodeJS.Timeout | null>(null)
const dashboardAddCreditsURL = activeOrganization
? `${clineEnvConfig.appBaseUrl}/dashboard/organization?tab=credits&redirect=true`
: `${clineEnvConfig.appBaseUrl}/dashboard/account?tab=credits&redirect=true`
const clineUris = useMemo(() => {
const base = new URL(clineUser?.appBaseUrl || CLINE_APP_URL)
const dashboard = new URL("dashboard", base)
const credits = new URL(activeOrganization ? "/organization" : "/account", dashboard)
credits.searchParams.set("tab", "credits")
credits.searchParams.set("redirect", "true")
return {
dashboard,
credits,
}
}, [clineUser?.appBaseUrl, activeOrganization])
async function getUserCredits() {
setIsLoading(true)
@@ -291,10 +301,7 @@ export const ClineAccountView = () => {
<div className="w-full flex gap-2 flex-col min-[225px]:flex-row">
<div className="w-full min-[225px]:w-1/2">
<VSCodeButtonLink
href={`${clineEnvConfig.appBaseUrl}/dashboard`}
appearance="primary"
className="w-full">
<VSCodeButtonLink href={clineUris.dashboard.href} appearance="primary" className="w-full">
Dashboard
</VSCodeButtonLink>
</div>
@@ -332,7 +339,7 @@ export const ClineAccountView = () => {
</div>
<div className="w-full">
<VSCodeButtonLink href={dashboardAddCreditsURL} className="w-full">
<VSCodeButtonLink href={clineUris.credits.href} className="w-full">
Add Credits
</VSCodeButtonLink>
</div>
@@ -3,7 +3,6 @@ import { TaskServiceClient } from "@/services/grpc-client"
import { AskResponseRequest } from "@shared/proto/task"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import React from "react"
import { clineEnvConfig } from "@/config"
interface CreditLimitErrorProps {
currentBalance: number
@@ -18,7 +17,7 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
totalSpent = 0,
totalPromotions = 0,
message = "You have run out of credit.",
buyCreditsUrl = `${clineEnvConfig.appBaseUrl}/dashboard`,
buyCreditsUrl = "https://app.cline.bot/dashboard/account?tab=credits&redirect=true",
}) => {
// We have to divide because the balance is stored in microcredits
return (
@@ -8,7 +8,7 @@ import { AccountServiceClient, StateServiceClient } from "@/services/grpc-client
import { EmptyRequest, BooleanRequest } from "@shared/proto/common"
const WelcomeView = memo(() => {
const { apiConfiguration, chatSettings } = useExtensionState()
const { apiConfiguration, chatSettings, clineAppBaseUrl } = useExtensionState()
const [apiErrorMessage, setApiErrorMessage] = useState<string | undefined>(undefined)
const [showApiOptions, setShowApiOptions] = useState(false)
@@ -54,7 +54,7 @@ const WelcomeView = memo(() => {
3.7 Sonnet.
</p>
<VSCodeButton appearance="primary" onClick={handleLogin} className="w-full mt-1">
<VSCodeButton appearance="primary" title={clineAppBaseUrl} onClick={handleLogin} className="w-full mt-1">
Get Started for Free
</VSCodeButton>
-21
View File
@@ -1,21 +0,0 @@
export type Environment = "production" | "staging" | "local"
const CLINE_ENVIRONMENT: Environment = (process.env.CLINE_ENVIRONMENT as Environment) || "production"
interface EnvironmentConfig {
appBaseUrl: string
}
const configs: Record<Environment, EnvironmentConfig> = {
production: {
appBaseUrl: "https://app.cline.bot",
},
staging: {
appBaseUrl: "https://staging-app.cline.bot",
},
local: {
appBaseUrl: "http://localhost:3000",
},
}
export const clineEnvConfig = configs[CLINE_ENVIRONMENT]
@@ -8,6 +8,7 @@ export interface ClineUser {
email?: string
displayName?: string
photoUrl?: string
appBaseUrl?: string
}
export interface ClineAuthContextType {
@@ -26,8 +26,6 @@ import {
requestyDefaultModelInfo,
groqDefaultModelId,
groqModels,
huggingFaceDefaultModelId,
huggingFaceModels,
} from "../../../src/shared/api"
import { McpMarketplaceCatalog, McpServer, McpViewTab } from "../../../src/shared/mcp"
import { convertTextMateToHljs } from "../utils/textMateToHljs"
@@ -173,6 +171,7 @@ export const ExtensionStateContextProvider: React.FC<{
const [state, setState] = useState<ExtensionState>({
version: "",
clineAppBaseUrl: "https://app.cline.bot",
clineMessages: [],
taskHistory: [],
shouldShowAnnouncement: false,
-1
View File
@@ -67,7 +67,6 @@ export default defineConfig({
NODE_ENV: JSON.stringify(process.env.IS_DEV ? "development" : "production"),
IS_DEV: JSON.stringify(process.env.IS_DEV),
IS_TEST: JSON.stringify(process.env.IS_TEST),
CLINE_ENVIRONMENT: JSON.stringify(process.env.CLINE_ENVIRONMENT ?? "production"),
},
},
resolve: {