From 281db607f0dde696b499f232f4ff9ead364488b8 Mon Sep 17 00:00:00 2001 From: harrisonofcline Date: Wed, 21 Oct 2026 20:35:17 -0700 Subject: [PATCH] fix: route the remaining account links through getClineDashboardUrl [DAT-111] Follows the review on #15043 to the links still hardcoding production: - desktop: onboarding (Sign up, Find your key, Get ClinePass), the connectors view's Create account, and the welcome view's GitHub connect fallback build on the sidecar's Cline app and hide when none resolves. The welcome view's cloud selection storage key is left as it was. - VS Code: CreditLimitError's fallback buy-credits link and RemoteConfigSection's dashboard link follow the user's app base URL. - CLI: the credits link in the out-of-credits message follows the configured environment and carries client attribution. - sdk/DOC.md: new URL(path, base) drops the base query for a non-empty path (Greptile 4344084543). --- apps/cli/src/tui/cline-account.test.ts | 15 ++++ apps/cli/src/tui/cline-account.ts | 9 ++- apps/cli/src/tui/components/chat-entry.tsx | 19 ++--- .../views/chat/cloud-onboarding.tsx | 6 +- .../views/chat/welcome-chat.test.tsx | 32 ++++++++ .../components/views/chat/welcome-chat.tsx | 37 ++++++--- .../views/onboarding/onboarding-view.test.tsx | 10 ++- .../views/onboarding/onboarding-view.tsx | 77 +++++++++++-------- .../composio-connectors-view.test.tsx | 18 +++-- .../settings/composio-connectors-view.tsx | 30 +++++--- .../webview/lib/provider-key-urls.ts | 3 - .../src/components/account/helpers.ts | 2 +- .../src/components/chat/CreditLimitError.tsx | 28 +++---- .../settings/sections/RemoteConfigSection.tsx | 18 ++++- sdk/DOC.md | 6 +- 15 files changed, 209 insertions(+), 101 deletions(-) diff --git a/apps/cli/src/tui/cline-account.test.ts b/apps/cli/src/tui/cline-account.test.ts index 0eed5a08e0..2dbaba3006 100644 --- a/apps/cli/src/tui/cline-account.test.ts +++ b/apps/cli/src/tui/cline-account.test.ts @@ -445,3 +445,18 @@ describe("isClineAccountCreditsErrorMessage", () => { ).toBe(false); }); }); + +describe("getClineCreditsDashboardUrl", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("opens the credits tab on the configured Cline environment's app", async () => { + vi.stubEnv("CLINE_ENVIRONMENT", "staging"); + const { getClineCreditsDashboardUrl } = await import("./cline-account"); + + expect(getClineCreditsDashboardUrl()).toBe( + "https://staging-app.cline.bot/dashboard/account?tab=credits&client=cline-cli", + ); + }); +}); diff --git a/apps/cli/src/tui/cline-account.ts b/apps/cli/src/tui/cline-account.ts index 04dc7f8450..a9412694a6 100644 --- a/apps/cli/src/tui/cline-account.ts +++ b/apps/cli/src/tui/cline-account.ts @@ -25,8 +25,13 @@ import { formatCreditBalance, normalizeCreditBalance } from "../utils/output"; import { identifyTelemetryAccount } from "../utils/telemetry"; import type { Config } from "../utils/types"; -export const CLINE_CREDITS_DASHBOARD_URL = - "https://app.cline.bot/dashboard/account?tab=credits"; +export function getClineCreditsDashboardUrl(): string | undefined { + return getClineDashboardUrl( + "dashboard/account?tab=credits", + getClineEnvironmentConfig().appBaseUrl, + "cline-cli", + ); +} type ClineAccountConfig = Pick; diff --git a/apps/cli/src/tui/components/chat-entry.tsx b/apps/cli/src/tui/components/chat-entry.tsx index 68b9b3266e..192a34063f 100644 --- a/apps/cli/src/tui/components/chat-entry.tsx +++ b/apps/cli/src/tui/components/chat-entry.tsx @@ -18,7 +18,7 @@ import { isClinePassSubscriptionError, } from "../../utils/cline-pass-errors"; import { - CLINE_CREDITS_DASHBOARD_URL, + getClineCreditsDashboardUrl, isClineAccountCreditsErrorMessage, } from "../cline-account"; import { getUserMessageBackground } from "../palette"; @@ -278,6 +278,7 @@ function ClineCreditsClinePassErrorView(props: { }) { const linkColor = props.theme.accents.act; const subscriptionUrl = getCliSubscriptionUrl(); + const creditsUrl = getClineCreditsDashboardUrl(); return ( @@ -296,14 +297,14 @@ function ClineCreditsClinePassErrorView(props: { "You have run out of Cline credits. Add credits in the dashboard or purchase and switch to ClinePass to continue." } /> - - Purchase Credits: - - - {CLINE_CREDITS_DASHBOARD_URL} - - - + {creditsUrl ? ( + + Purchase Credits: + + {creditsUrl} + + + ) : null} Purchase ClinePass: diff --git a/apps/examples/desktop-app/webview/components/views/chat/cloud-onboarding.tsx b/apps/examples/desktop-app/webview/components/views/chat/cloud-onboarding.tsx index 8e9a556c91..4eb645c85f 100644 --- a/apps/examples/desktop-app/webview/components/views/chat/cloud-onboarding.tsx +++ b/apps/examples/desktop-app/webview/components/views/chat/cloud-onboarding.tsx @@ -34,7 +34,7 @@ export function CloudOnboardingCard({ checking = false, }: { variant: CloudOnboardingVariant; - onConnect: () => void; + onConnect?: () => void; onRefresh: () => void; onSignIn?: () => void; onCancelSignIn?: () => void; @@ -152,13 +152,13 @@ export function CloudOnboardingCard({ )} - ) : ( + ) : onConnect ? ( - )} + ) : null} {isSignedOut ? null : ( - ) : ( + ) : clineAppUrl ? ( - )} + ) : null} )} {!user && signingIn && deviceUserCode ? ( @@ -679,18 +680,20 @@ function ConnectStep({ {clineKeySaving ? "Connecting..." : "Connect"} - + {clineAppUrl ? ( + + ) : null} {clineKeyError ? (

Failed to save API key: {clineKeyError} @@ -980,6 +983,12 @@ function ImportHistoryStep({ * the composer. */ function ClineModelsSummary() { + const { clineAppBaseUrl } = useAccount(); + const clinePassSubscribeUrl = getClineDashboardUrl( + "onboarding/individual-plan", + clineAppBaseUrl, + CLINE_CLIENT_NAME, + ); const [freeModels, setFreeModels] = useState< NonNullable >([]); @@ -1032,16 +1041,18 @@ function ClineModelsSummary() { Kimi, and GLM.

- + {clinePassSubscribeUrl ? ( + + ) : null} ); diff --git a/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.test.tsx b/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.test.tsx index fbbdfb66bf..1e906fab10 100644 --- a/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.test.tsx +++ b/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.test.tsx @@ -24,7 +24,10 @@ vi.mock("@/lib/desktop-client", () => ({ desktopClient: { invoke: mocks.invoke, subscribe: () => () => {}, - subscribeTransportState: () => () => {}, + subscribeTransportState: (handler: (state: string) => void) => { + handler("connected"); + return () => {}; + }, }, openExternalUrl: mocks.openExternalUrl, })); @@ -425,10 +428,11 @@ describe("installed connectors", () => { it("offers Cline sign-in when connectors are unavailable and the user is signed out", async () => { mocks.configured = false; - mocks.invoke.mockResolvedValue({ - signedIn: false, - code: "ACCOUNT_NOT_AUTHENTICATED", - }); + mocks.invoke.mockImplementation(async (command: string) => + command === "get_cline_app_base_url" + ? "https://staging-app.cline.bot" + : { signedIn: false, code: "ACCOUNT_NOT_AUTHENTICATED" }, + ); await act(async () => root.render( @@ -453,7 +457,9 @@ describe("installed connectors", () => { ); expect(mocks.refresh).toHaveBeenCalled(); await act(async () => button("Create account")?.click()); - expect(mocks.openExternalUrl).toHaveBeenCalledWith("https://app.cline.bot"); + expect(mocks.openExternalUrl).toHaveBeenCalledWith( + "https://staging-app.cline.bot/?client=cline-desktop", + ); }); it("explains the beta rollout when signed in without connector access", async () => { diff --git a/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.tsx b/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.tsx index a26b05fced..b85261f2e6 100644 --- a/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.tsx +++ b/apps/examples/desktop-app/webview/components/views/settings/composio-connectors-view.tsx @@ -1,5 +1,6 @@ "use client"; +import { getClineDashboardUrl } from "@cline/shared/browser"; import { GitHubIcon } from "@cline/ui"; import { CalendarDays, @@ -47,7 +48,7 @@ import { cn } from "@/lib/utils"; /** Shared connector browser for Customize and Marketplace. */ -const CREATE_ACCOUNT_URL = "https://app.cline.bot"; +const CLINE_CLIENT_NAME = "cline-desktop"; /** How many catalog entries to show before asking the user to search. */ const CATALOG_PREVIEW_COUNT = 24; @@ -755,7 +756,12 @@ function ConnectorsUnavailable({ onRefresh: () => Promise; refreshing: boolean; }) { - const { user, accountReady, refreshAccount } = useAccount(); + const { user, accountReady, clineAppBaseUrl, refreshAccount } = useAccount(); + const createAccountUrl = getClineDashboardUrl( + "/", + clineAppBaseUrl, + CLINE_CLIENT_NAME, + ); const [signingIn, setSigningIn] = useState(false); const [error, setError] = useState(null); const deviceUserCode = useOAuthUserCode(signingIn); @@ -838,15 +844,17 @@ function ConnectorsUnavailable({ )} {signingIn ? "Waiting for browser…" : "Sign in"} - + {createAccountUrl ? ( + + ) : null} )} {signingIn && deviceUserCode ? ( diff --git a/apps/examples/desktop-app/webview/lib/provider-key-urls.ts b/apps/examples/desktop-app/webview/lib/provider-key-urls.ts index 4f2856a947..e260616d28 100644 --- a/apps/examples/desktop-app/webview/lib/provider-key-urls.ts +++ b/apps/examples/desktop-app/webview/lib/provider-key-urls.ts @@ -21,9 +21,6 @@ const PROVIDER_API_KEY_URLS: Record = { wandb: "https://wandb.ai/settings", }; -/** Cline account dashboard, where Cline API keys are created and managed. */ -export const CLINE_DASHBOARD_URL = "https://app.cline.bot"; - /** * Resolve the "get an API key" URL for a provider, preferring the catalog's * own `docUrl` and falling back to the curated map of popular providers. diff --git a/apps/vscode/webview-ui/src/components/account/helpers.ts b/apps/vscode/webview-ui/src/components/account/helpers.ts index 55e3c3e036..550d40a13f 100644 --- a/apps/vscode/webview-ui/src/components/account/helpers.ts +++ b/apps/vscode/webview-ui/src/components/account/helpers.ts @@ -5,7 +5,7 @@ import type { UsageTransaction as ProtoUsageTransaction, UserOrganization } from import { PLATFORM_CONFIG, PlatformType } from "@/config/platform.config" import type { ClineUser } from "@/context/ClineAuthContext" -const getClineClientName = (): ClientName => +export const getClineClientName = (): ClientName => PLATFORM_CONFIG.type === PlatformType.STANDALONE ? "cline-jetbrains" : "cline-vscode" export const getMainRole = (roles?: string[]) => { diff --git a/apps/vscode/webview-ui/src/components/chat/CreditLimitError.tsx b/apps/vscode/webview-ui/src/components/chat/CreditLimitError.tsx index d6856941d9..2ee4c8e6aa 100644 --- a/apps/vscode/webview-ui/src/components/chat/CreditLimitError.tsx +++ b/apps/vscode/webview-ui/src/components/chat/CreditLimitError.tsx @@ -1,5 +1,6 @@ import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import React, { useEffect, useMemo, useState } from "react" +import { getClineUris } from "@/components/account/helpers" import VSCodeButtonLink from "@/components/common/VSCodeButtonLink" import { useClineAuth } from "@/context/ClineAuthContext" import { AccountServiceClient } from "@/services/grpc-client" @@ -13,11 +14,6 @@ interface CreditLimitErrorProps { retryFailedRequest?: () => Promise } -const DEFAULT_BUY_CREDITS_URL = { - USER: "https://app.cline.bot/dashboard/account?tab=credits&redirect=true", - ORG: "https://app.cline.bot/dashboard/organization?tab=credits&redirect=true", -} - const CreditLimitError: React.FC = ({ message = "You have run out of credits.", buyCreditsUrl, @@ -26,14 +22,18 @@ const CreditLimitError: React.FC = ({ totalSpent, retryFailedRequest, }) => { - const { activeOrganization } = useClineAuth() + const { clineUser, activeOrganization } = useClineAuth() const [fullBuyCreditsUrl, setFullBuyCreditsUrl] = useState("") - const dashboardUrl = useMemo(() => { - return buyCreditsUrl ?? (activeOrganization?.organizationId ? DEFAULT_BUY_CREDITS_URL.ORG : DEFAULT_BUY_CREDITS_URL.USER) - }, [buyCreditsUrl, activeOrganization?.organizationId]) + const dashboardUrl = useMemo( + () => + buyCreditsUrl ?? + getClineUris(clineUser?.appBaseUrl, "credits", activeOrganization?.organizationId ? "organization" : "account"), + [buyCreditsUrl, clineUser?.appBaseUrl, activeOrganization?.organizationId], + ) useEffect(() => { + if (!dashboardUrl) return const fetchCallbackUrl = async () => { try { const callbackUrl = (await AccountServiceClient.getRedirectUrl({})).value @@ -67,10 +67,12 @@ const CreditLimitError: React.FC = ({ - - - Buy Credits - + {dashboardUrl && ( + + + Buy Credits + + )}

- You haven't configured remote config yet. Do so through our{" "} - dashboard. + You haven't configured remote config yet. + {organizationSettingsUrl && ( + <> + {" "} + Do so through our dashboard. + + )}

diff --git a/sdk/DOC.md b/sdk/DOC.md index 886ac7660a..bc431593af 100644 --- a/sdk/DOC.md +++ b/sdk/DOC.md @@ -484,9 +484,9 @@ the client explicitly: `getClineClientIdentity()` is process-local and holds display names for request headers (`"VSCode Extension"`), so webviews cannot read it. -`client` is appended after the path is resolved. `new URL(path, base)` keeps the -base's own query string for a relative path and replaces it for a path that has -its own, so appending afterwards is the only ordering that holds for both. +`new URL(path, base)` drops the base's own query string as soon as a non-empty +path is supplied, so `client` is appended after the path is resolved; added to +the base first, it would be lost. It returns `undefined` when the path or base URL is missing, the base URL is unusable, or the result would leave the app's origin, such as an absolute,