mirror of
https://github.com/cline/cline.git
synced 2026-10-24 02:02:27 +08:00
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).
This commit is contained in:
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Config, "apiKey" | "logger" | "providerId">;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<box flexDirection="row">
|
||||
<text fg="red" content="* " />
|
||||
@@ -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."
|
||||
}
|
||||
/>
|
||||
<box flexDirection="row">
|
||||
<text fg="gray">Purchase Credits: </text>
|
||||
<text fg={linkColor} selectable>
|
||||
<a href={CLINE_CREDITS_DASHBOARD_URL}>
|
||||
{CLINE_CREDITS_DASHBOARD_URL}
|
||||
</a>
|
||||
</text>
|
||||
</box>
|
||||
{creditsUrl ? (
|
||||
<box flexDirection="row">
|
||||
<text fg="gray">Purchase Credits: </text>
|
||||
<text fg={linkColor} selectable>
|
||||
<a href={creditsUrl}>{creditsUrl}</a>
|
||||
</text>
|
||||
</box>
|
||||
) : null}
|
||||
<box flexDirection="row">
|
||||
<text fg="gray">Purchase ClinePass: </text>
|
||||
<text fg={linkColor} selectable>
|
||||
|
||||
@@ -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({
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
) : onConnect ? (
|
||||
<Button onClick={onConnect} size="sm">
|
||||
<Github aria-hidden="true" className="size-3.5" />
|
||||
{isNoRepositories ? "Manage repository access" : "Connect GitHub"}
|
||||
<ExternalLink aria-hidden="true" className="size-3" />
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
{isSignedOut ? null : (
|
||||
<Button
|
||||
disabled={checking}
|
||||
|
||||
@@ -26,6 +26,7 @@ const { invokeMock, subscribeMock, accountRef, openExternalUrlMock } =
|
||||
accountRef: {
|
||||
user: null as { id: string } | null,
|
||||
activeOrganization: null as ClineAccountOrganization | null,
|
||||
clineAppBaseUrl: undefined as string | undefined,
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -54,6 +55,7 @@ vi.mock("@/contexts/account-context", () => ({
|
||||
useAccount: () => ({
|
||||
user: accountRef.user,
|
||||
activeOrganization: accountRef.activeOrganization,
|
||||
clineAppBaseUrl: accountRef.clineAppBaseUrl,
|
||||
refreshAccount: vi.fn(async () => undefined),
|
||||
}),
|
||||
}));
|
||||
@@ -64,6 +66,7 @@ beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
accountRef.user = null;
|
||||
accountRef.activeOrganization = null;
|
||||
accountRef.clineAppBaseUrl = undefined;
|
||||
Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });
|
||||
window.matchMedia = vi.fn().mockReturnValue({
|
||||
matches: true,
|
||||
@@ -451,6 +454,35 @@ describe("WelcomeScreen", () => {
|
||||
accountRef.user = null;
|
||||
});
|
||||
|
||||
it("falls back to the sidecar's Cline app when the API sends no connect URL", async () => {
|
||||
accountRef.user = { id: "user-1" };
|
||||
accountRef.activeOrganization = {
|
||||
organizationId: "org-1",
|
||||
memberId: "member-1",
|
||||
name: "Acme",
|
||||
roles: ["member"],
|
||||
active: true,
|
||||
};
|
||||
accountRef.clineAppBaseUrl = "https://staging-app.cline.bot";
|
||||
invokeMock.mockImplementation(async (command: string) =>
|
||||
command === "list_cloud_repositories"
|
||||
? { connected: false, connectUrl: " ", repositories: [] }
|
||||
: {},
|
||||
);
|
||||
|
||||
await renderWelcomeScreen({
|
||||
cloudAgentsEnabled: true,
|
||||
executionTarget: "cloud",
|
||||
workspaceRoot: "/projects/project-1",
|
||||
workspaces: ["/projects/project-1"],
|
||||
});
|
||||
await clickButton("Connect GitHub");
|
||||
|
||||
expect(invokeMock).toHaveBeenCalledWith("open_external_url", {
|
||||
url: "https://staging-app.cline.bot/dashboard/integrations?client=cline-desktop",
|
||||
});
|
||||
});
|
||||
|
||||
it("places the environment selector before the workspace selector", async () => {
|
||||
await renderWelcomeScreen({
|
||||
environmentSelector: (
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import type { AgendaTaskRecord } from "@cline/shared";
|
||||
import { getClineEnvironmentConfig } from "@cline/shared/browser";
|
||||
import {
|
||||
getClineDashboardUrl,
|
||||
getClineEnvironmentConfig,
|
||||
} from "@cline/shared/browser";
|
||||
import {
|
||||
AgentConversationLayout,
|
||||
type AgentQuickAction,
|
||||
@@ -37,9 +40,6 @@ import {
|
||||
} from "./cloud-onboarding";
|
||||
import { WelcomeWorkspaceControls } from "./welcome-workspace-controls";
|
||||
|
||||
// Used only until the API's connectUrl arrives (or when it is blank), so a
|
||||
// staging/local build still points at its own dashboard.
|
||||
const FALLBACK_CONNECT_URL = `${getClineEnvironmentConfig().appBaseUrl}/dashboard/integrations`;
|
||||
const CLOUD_SETUP_POLL_INTERVAL_MS = 6_000;
|
||||
|
||||
type CloudSetupState = {
|
||||
@@ -51,7 +51,7 @@ type CloudSetupState = {
|
||||
| "no_repositories"
|
||||
| "restore_error"
|
||||
| "error";
|
||||
connectUrl: string;
|
||||
connectUrl: string | undefined;
|
||||
scope: string | null;
|
||||
requestId: number;
|
||||
repositories: CloudRepositoryOption[];
|
||||
@@ -100,7 +100,9 @@ export function WelcomeScreen({
|
||||
onWorkInChange?: (next: WorkIn) => void;
|
||||
onOpenSession?: (sessionId: string) => void | Promise<void>;
|
||||
}) {
|
||||
const { user, activeOrganization, refreshAccount } = useAccount();
|
||||
const { user, activeOrganization, clineAppBaseUrl, refreshAccount } =
|
||||
useAccount();
|
||||
// A storage key, not a link: changing it would orphan saved selections.
|
||||
const cloudScope = user
|
||||
? JSON.stringify([
|
||||
getClineEnvironmentConfig().appBaseUrl,
|
||||
@@ -122,7 +124,7 @@ export function WelcomeScreen({
|
||||
});
|
||||
const [cloudSetup, setCloudSetup] = useState<CloudSetupState>({
|
||||
status: "unknown",
|
||||
connectUrl: FALLBACK_CONNECT_URL,
|
||||
connectUrl: undefined,
|
||||
scope: null,
|
||||
requestId: 0,
|
||||
repositories: [],
|
||||
@@ -150,7 +152,7 @@ export function WelcomeScreen({
|
||||
: result.repositories.length === 0
|
||||
? "no_repositories"
|
||||
: "ready",
|
||||
connectUrl: result.connectUrl?.trim() || FALLBACK_CONNECT_URL,
|
||||
connectUrl: result.connectUrl?.trim() || undefined,
|
||||
repositoryUrls: result.repositories.map((repository) =>
|
||||
normalizeCloudRepositoryUrl(repository.url),
|
||||
),
|
||||
@@ -435,6 +437,14 @@ export function WelcomeScreen({
|
||||
repoUrl,
|
||||
]);
|
||||
|
||||
// The fallback covers the window before the API's connectUrl arrives, or a blank one.
|
||||
const connectUrl =
|
||||
cloudSetup.connectUrl ??
|
||||
getClineDashboardUrl(
|
||||
"dashboard/integrations",
|
||||
clineAppBaseUrl,
|
||||
"cline-desktop",
|
||||
);
|
||||
const cloudOnboardingVariant: CloudOnboardingVariant | null = !cloudModeActive
|
||||
? null
|
||||
: !signedIn
|
||||
@@ -507,10 +517,13 @@ export function WelcomeScreen({
|
||||
cancelling={cancelling}
|
||||
onCancelSignIn={() => void cancelSignIn()}
|
||||
checking={cloudSetupChecking}
|
||||
onConnect={() =>
|
||||
void (cloudOnboardingVariant === "not_connected"
|
||||
? connectGitHub(cloudSetup.connectUrl)
|
||||
: openExternalUrl(cloudSetup.connectUrl))
|
||||
onConnect={
|
||||
connectUrl
|
||||
? () =>
|
||||
void (cloudOnboardingVariant === "not_connected"
|
||||
? connectGitHub(connectUrl)
|
||||
: openExternalUrl(connectUrl))
|
||||
: undefined
|
||||
}
|
||||
onRefresh={() => void checkCloudSetup()}
|
||||
onSignIn={() => void signIn()}
|
||||
|
||||
+8
-2
@@ -23,7 +23,10 @@ vi.mock("@/lib/desktop-client", () => ({
|
||||
desktopClient: {
|
||||
invoke,
|
||||
subscribe: vi.fn(() => () => {}),
|
||||
subscribeTransportState: vi.fn(() => () => {}),
|
||||
subscribeTransportState: vi.fn((handler: (state: string) => void) => {
|
||||
handler("connected");
|
||||
return () => {};
|
||||
}),
|
||||
},
|
||||
openExternalUrl,
|
||||
}));
|
||||
@@ -405,6 +408,9 @@ describe("OnboardingView", () => {
|
||||
if (command === "get_feature_flags") {
|
||||
return GITHUB_STEP_ENABLED_FLAGS;
|
||||
}
|
||||
if (command === "get_cline_app_base_url") {
|
||||
return "https://staging-app.cline.bot";
|
||||
}
|
||||
if (command === "list_cline_recommended_models") {
|
||||
return {
|
||||
recommended: [],
|
||||
@@ -445,7 +451,7 @@ describe("OnboardingView", () => {
|
||||
buttonByText("Get ClinePass").click();
|
||||
});
|
||||
expect(openExternalUrl).toHaveBeenCalledWith(
|
||||
"https://app.cline.bot/onboarding/individual-plan",
|
||||
"https://staging-app.cline.bot/onboarding/individual-plan?client=cline-desktop",
|
||||
);
|
||||
const doneGrid = container.querySelector<HTMLElement>(
|
||||
'[data-welcome-hero-variant="grid-only"]',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { getClineDashboardUrl } from "@cline/shared/browser";
|
||||
import { AgentWelcomeHero, Button, IconButton } from "@cline/ui";
|
||||
import {
|
||||
ArrowLeft,
|
||||
@@ -35,10 +36,7 @@ import {
|
||||
readModelSelectionStorageFromWindow,
|
||||
writeModelSelectionStorageToWindow,
|
||||
} from "@/lib/model-selection";
|
||||
import {
|
||||
CLINE_DASHBOARD_URL,
|
||||
getProviderApiKeyUrl,
|
||||
} from "@/lib/provider-key-urls";
|
||||
import { getProviderApiKeyUrl } from "@/lib/provider-key-urls";
|
||||
import {
|
||||
fetchProviderCatalog,
|
||||
invalidateProviderCatalogCache,
|
||||
@@ -52,9 +50,7 @@ import {
|
||||
} from "@/lib/session-import";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const CREATE_ACCOUNT_URL = "https://app.cline.bot";
|
||||
const CLINE_PASS_SUBSCRIBE_URL =
|
||||
"https://app.cline.bot/onboarding/individual-plan";
|
||||
const CLINE_CLIENT_NAME = "cline-desktop";
|
||||
|
||||
const CLINE_SIGN_IN_BENEFITS = [
|
||||
"Regular free model promotions",
|
||||
@@ -335,7 +331,12 @@ function ConnectStep({
|
||||
onConnected: (connection: OnboardingConnection) => void;
|
||||
onSkip: () => void;
|
||||
}) {
|
||||
const { user, refreshAccount } = useAccount();
|
||||
const { user, clineAppBaseUrl, refreshAccount } = useAccount();
|
||||
const clineAppUrl = getClineDashboardUrl(
|
||||
"/",
|
||||
clineAppBaseUrl,
|
||||
CLINE_CLIENT_NAME,
|
||||
);
|
||||
const {
|
||||
signIn: signInWithCline,
|
||||
cancelSignIn: cancelSignInWithCline,
|
||||
@@ -589,9 +590,9 @@ function ConnectStep({
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
) : (
|
||||
) : clineAppUrl ? (
|
||||
<Button
|
||||
onClick={() => void openExternalUrl(CREATE_ACCOUNT_URL)}
|
||||
onClick={() => void openExternalUrl(clineAppUrl)}
|
||||
size="md"
|
||||
tone="neutral"
|
||||
type="button"
|
||||
@@ -599,7 +600,7 @@ function ConnectStep({
|
||||
>
|
||||
Sign up
|
||||
</Button>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{!user && signingIn && deviceUserCode ? (
|
||||
@@ -679,18 +680,20 @@ function ConnectStep({
|
||||
{clineKeySaving ? "Connecting..." : "Connect"}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
className="self-start -ml-1 mt-1"
|
||||
disabled={clineKeySaving}
|
||||
onClick={() => void openExternalUrl(CLINE_DASHBOARD_URL)}
|
||||
size="xs"
|
||||
tone="neutral"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Find your key
|
||||
<ExternalLink className="size-3" />
|
||||
</Button>
|
||||
{clineAppUrl ? (
|
||||
<Button
|
||||
className="self-start -ml-1 mt-1"
|
||||
disabled={clineKeySaving}
|
||||
onClick={() => void openExternalUrl(clineAppUrl)}
|
||||
size="xs"
|
||||
tone="neutral"
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
Find your key
|
||||
<ExternalLink className="size-3" />
|
||||
</Button>
|
||||
) : null}
|
||||
{clineKeyError ? (
|
||||
<p className="text-xs text-destructive" role="alert">
|
||||
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<ClineRecommendedModelsResponse["free"]>
|
||||
>([]);
|
||||
@@ -1032,16 +1041,18 @@ function ClineModelsSummary() {
|
||||
Kimi, and GLM.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => void openExternalUrl(CLINE_PASS_SUBSCRIBE_URL)}
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
type="button"
|
||||
variant="surface"
|
||||
>
|
||||
Get ClinePass
|
||||
<ExternalLink className="size-3.5" />
|
||||
</Button>
|
||||
{clinePassSubscribeUrl ? (
|
||||
<Button
|
||||
onClick={() => void openExternalUrl(clinePassSubscribeUrl)}
|
||||
size="sm"
|
||||
tone="neutral"
|
||||
type="button"
|
||||
variant="surface"
|
||||
>
|
||||
Get ClinePass
|
||||
<ExternalLink className="size-3.5" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+12
-6
@@ -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(
|
||||
<AccountProvider>
|
||||
@@ -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 () => {
|
||||
|
||||
+19
-11
@@ -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<void>;
|
||||
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<string | null>(null);
|
||||
const deviceUserCode = useOAuthUserCode(signingIn);
|
||||
@@ -838,15 +844,17 @@ function ConnectorsUnavailable({
|
||||
)}
|
||||
{signingIn ? "Waiting for browser…" : "Sign in"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => void openExternalUrl(CREATE_ACCOUNT_URL)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Create account
|
||||
<ExternalLink className="size-4" />
|
||||
</Button>
|
||||
{createAccountUrl ? (
|
||||
<Button
|
||||
onClick={() => void openExternalUrl(createAccountUrl)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Create account
|
||||
<ExternalLink className="size-4" />
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
{signingIn && deviceUserCode ? (
|
||||
|
||||
@@ -21,9 +21,6 @@ const PROVIDER_API_KEY_URLS: Record<string, string> = {
|
||||
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.
|
||||
|
||||
@@ -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[]) => {
|
||||
|
||||
@@ -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<boolean>
|
||||
}
|
||||
|
||||
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<CreditLimitErrorProps> = ({
|
||||
message = "You have run out of credits.",
|
||||
buyCreditsUrl,
|
||||
@@ -26,14 +22,18 @@ const CreditLimitError: React.FC<CreditLimitErrorProps> = ({
|
||||
totalSpent,
|
||||
retryFailedRequest,
|
||||
}) => {
|
||||
const { activeOrganization } = useClineAuth()
|
||||
const { clineUser, activeOrganization } = useClineAuth()
|
||||
const [fullBuyCreditsUrl, setFullBuyCreditsUrl] = useState<string>("")
|
||||
|
||||
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<CreditLimitErrorProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VSCodeButtonLink className="w-full mb-2" href={fullBuyCreditsUrl}>
|
||||
<span className="codicon codicon-credit-card mr-[6px] text-[14px]" />
|
||||
Buy Credits
|
||||
</VSCodeButtonLink>
|
||||
{dashboardUrl && (
|
||||
<VSCodeButtonLink className="w-full mb-2" href={fullBuyCreditsUrl}>
|
||||
<span className="codicon codicon-credit-card mr-[6px] text-[14px]" />
|
||||
Buy Credits
|
||||
</VSCodeButtonLink>
|
||||
)}
|
||||
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { getClineDashboardUrl } from "@cline/shared/browser"
|
||||
import { EmptyRequest } from "@shared/proto/index.cline"
|
||||
import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import { getClineClientName } from "@/components/account/helpers"
|
||||
import { RemoteConfigToggle } from "@/components/account/RemoteConfigToggle"
|
||||
import { useClineAuth } from "@/context/ClineAuthContext"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
@@ -283,7 +285,12 @@ function PromptUploadingSection() {
|
||||
|
||||
export function RemoteConfigSection({ renderSectionHeader }: RemoteConfigSectionProps) {
|
||||
const { remoteConfigSettings, optOutOfRemoteConfig, remoteConfigAvailable } = useExtensionState()
|
||||
const { activeOrganization } = useClineAuth()
|
||||
const { clineUser, activeOrganization } = useClineAuth()
|
||||
const organizationSettingsUrl = getClineDashboardUrl(
|
||||
"dashboard/organization?tab=settings",
|
||||
clineUser?.appBaseUrl,
|
||||
getClineClientName(),
|
||||
)
|
||||
|
||||
if (optOutOfRemoteConfig && remoteConfigAvailable) {
|
||||
return (
|
||||
@@ -302,8 +309,13 @@ export function RemoteConfigSection({ renderSectionHeader }: RemoteConfigSection
|
||||
<BaseRemoteConfigSection renderSectionHeader={renderSectionHeader}>
|
||||
<div className="flex flex-col justify-center gap-4">
|
||||
<h3>
|
||||
You haven't configured remote config yet. Do so through our{" "}
|
||||
<VSCodeLink href="https://app.cline.bot/dashboard/organization?tab=settings">dashboard</VSCodeLink>.
|
||||
You haven't configured remote config yet.
|
||||
{organizationSettingsUrl && (
|
||||
<>
|
||||
{" "}
|
||||
Do so through our <VSCodeLink href={organizationSettingsUrl}>dashboard</VSCodeLink>.
|
||||
</>
|
||||
)}
|
||||
</h3>
|
||||
|
||||
<RefreshButton />
|
||||
|
||||
+3
-3
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user