feat(agy): trim the form hint and require a key on custom cards

The long paragraph above the Antigravity form explained a case that should
not exist: a non-official card without a key is just the official login
card again. Drop the paragraph and warn on save when a custom card has no
API key, as Gemini CLI already does. A key without an endpoint stays valid
and uses Google's default API.

The endpoint and model rules move into "?" tips next to their fields. The
card's terminal button now reads "Open Terminal with This Provider"; the
shorter "Open Terminal" label stays for the Claude Code card menu.
This commit is contained in:
Jason committed 2026-10-21 11:31:01 +08:00
1 parent 9a10e4f7a9
commit c776fd0ebf
9 files changed
+50 -23

No files matched your search

@@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next";
import { FormLabel } from "@/components/ui/form";
import { Download, Info, Loader2 } from "lucide-react";
import { Button } from "@/components/ui/button";
import { HelpTip } from "@/components/ui/help-tip";
import { toast } from "@/lib/toast";
import EndpointSpeedTest from "./EndpointSpeedTest";
import { ApiKeySection, EndpointField, ModelInputWithFetch } from "./shared";
@@ -118,11 +119,6 @@ export function GeminiFormFields({
return (
<>
{appId === "agy" && (
<p className="rounded-lg border border-border-strong bg-subtle p-4 text-sm text-fg-2">
{t("provider.form.agy.connectionHint")}
</p>
)}
{/* Google OAuth 提示 */}
{isGoogleOfficial && (
<div className="rounded-lg border border-border-strong bg-subtle p-4">
@@ -151,7 +147,7 @@ export function GeminiFormFields({
value={apiKey}
onChange={onApiKeyChange}
category={category}
required={appId !== "agy" || Boolean(baseUrl.trim())}
required
shouldShowLink={shouldShowApiKeyLink}
websiteUrl={websiteUrl}
isPartner={isPartner}
@@ -170,6 +166,9 @@ export function GeminiFormFields({
placeholder={t("providerForm.apiEndpointPlaceholder", {
defaultValue: "https://your-api-endpoint.com/",
})}
help={
appId === "agy" ? t("provider.form.agy.endpointHelp") : undefined
}
onManageClick={() => onEndpointModalToggle(true)}
/>
)}
@@ -178,9 +177,20 @@ export function GeminiFormFields({
{shouldShowModelField && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<FormLabel htmlFor="gemini-model">
{t("provider.form.gemini.model", { defaultValue: "模型" })}
</FormLabel>
<div className="flex items-center gap-0.5">
<FormLabel htmlFor="gemini-model">
{t("provider.form.gemini.model", { defaultValue: "模型" })}
</FormLabel>
{appId === "agy" && (
<HelpTip
title={t("provider.form.gemini.model", {
defaultValue: "模型",
})}
>
{t("provider.form.agy.modelHelp")}
</HelpTip>
)}
</div>
<Button
type="button"
variant="outline"
@@ -1628,8 +1628,8 @@ function ProviderFormFull({
}
}
if (usesGeminiStyleForm(appId)) {
// agy uses its own login when both fields are empty; an API key may
// use Google's default endpoint without a custom base URL.
// agy 只填 Key 不填地址是合法用法(走 Google 默认 API);不填 Key 的登录模式就是官方卡,
// 不该再存一张非官方的。
if (appId !== "agy" && !geminiBaseUrl.trim()) {
issues.push(
t("providerForm.endpointRequired", {
@@ -1637,10 +1637,7 @@ function ProviderFormFull({
}),
);
}
if (
!geminiApiKey.trim() &&
(appId !== "agy" || Boolean(geminiBaseUrl.trim()))
) {
if (!geminiApiKey.trim()) {
issues.push(
t("providerForm.apiKeyRequired", {
defaultValue: "非官方供应商请填写 API Key",
+1 -1
View File
@@ -668,7 +668,7 @@ export function buildLaunchSections(input: LaunchInput): ProviderSection[] {
buttons: [
{
key: "openTerminal",
label: t("provider.openTerminal"),
label: t("provider.openTerminalWithProvider"),
onClick: () => openTerminal(p),
},
],
+3 -1
View File
@@ -180,6 +180,7 @@
"managedByHermes": "Hermes Managed",
"managedByHermesHint": "Defined in Hermes' providers: dict. Edit or remove it via Hermes Web UI.",
"openTerminal": "Open Terminal",
"openTerminalWithProvider": "Open Terminal with This Provider",
"terminalOpened": "Terminal opened",
"terminalOpenFailed": "Failed to open terminal",
"openTerminalDialog": {
@@ -210,7 +211,8 @@
"geminiConfigHint": "Use .env format to configure Gemini",
"form": {
"agy": {
"connectionHint": "Leave the API key and endpoint empty to use agy's Google sign-in; with a key, the provider runs in third-party mode. Both modes only apply to agy started from the card's \"Open Terminal\": the key and endpoint go to that one process, and the model applies to that session only. Running agy directly in a terminal still uses your Google sign-in, with the model chosen by agy's own /model. A key without an endpoint uses Google's default API; third-party endpoints must support the Gemini API and agy's built-in Gemini models. In both modes, use the display name from the model list.",
"endpointHelp": "Leave empty for Google's default API. Third-party endpoints must support the Gemini API and agy's built-in Gemini models.",
"modelHelp": "Use the display name from the model list (Fetch Models). Applies to that session only.",
"keyFieldsHint": "The model, key and endpoint are only used by \"Open Terminal\" and are never written to Antigravity's own settings.",
"envFile": "Launcher environment variables",
"envFileHint": "When agy is started from the card's \"Open Terminal\", CC Switch passes the API key and endpoint to that one agy process only and never writes them to a file. Other environment variables are not applied.",
+3 -1
View File
@@ -180,6 +180,7 @@
"managedByHermes": "Hermes 管理",
"managedByHermesHint": "Hermes の providers: dict で定義されています。Hermes Web UI で編集または削除してください。",
"openTerminal": "ターミナルを開く",
"openTerminalWithProvider": "このプロバイダーでターミナルを開く",
"terminalOpened": "ターミナルを開きました",
"terminalOpenFailed": "ターミナルを開けませんでした",
"openTerminalDialog": {
@@ -210,7 +211,8 @@
"geminiConfigHint": ".env 形式で Gemini を設定してください",
"form": {
"agy": {
"connectionHint": "API Key と接続先を空欄にすると agy の Google ログインを使い、Key を入力するとサードパーティモードになります。どちらのモードもカードの「ターミナルを開く」から起動した agy にのみ適用され、Key と接続先はその 1 つのプロセスにだけ渡し、モデルはそのセッションにだけ適用されます。ターミナルで直接 agy を実行した場合は Google ログインのままで、モデルは agy 自身の /model で決まります。Key のみを指定すると Google の標準 API を使用します。外部接続先は Gemini API と agy 内蔵の Gemini モデルに対応している必要があります。どちらのモードでもモデル一覧の表示名を入力してください。",
"endpointHelp": "空欄なら Google の標準 API を使います。外部接続先は Gemini API と agy 内蔵の Gemini モデルに対応している必要があります。",
"modelHelp": "モデル一覧の表示名を入力してください(「モデル一覧を取得」から選べます)。そのセッションにのみ適用されます。",
"keyFieldsHint": "モデル、キー、エンドポイントは「ターミナルを開く」でのみ使われ、Antigravity 自身の設定には書き込みません。",
"envFile": "起動時の環境変数",
"envFileHint": "カードの「ターミナルを開く」から agy を起動すると、CC Switch は API キーとエンドポイントをその agy プロセスだけに渡し、ファイルには書き込みません。他の環境変数は適用されません。",
+3 -1
View File
@@ -180,6 +180,7 @@
"managedByHermes": "Hermes 託管",
"managedByHermesHint": "該專案定義於 Hermes 的 providers: dict,請在 Hermes Web UI 中編輯或刪除。",
"openTerminal": "開啟終端機",
"openTerminalWithProvider": "使用此供應商開啟終端機",
"terminalOpened": "終端機已開啟",
"terminalOpenFailed": "開啟終端機失敗",
"openTerminalDialog": {
@@ -210,7 +211,8 @@
"geminiConfigHint": "使用 .env 格式設定 Gemini",
"form": {
"agy": {
"connectionHint": "不填 API Key 和請求位址時使用 agy 本身的 Google 登入;填寫 Key 後是第三方模式。兩種模式都只作用於從卡片「開啟終端機」啟動的 agy:Key 和位址只傳給那一個行程,模型只對那次工作階段生效。直接在終端機裡執行 agy 仍是 Google 登入,模型由 agy 自己的 /model 決定。不填位址時使用 Google 預設 API;第三方位址須相容 Gemini API,並支援 agy 內建的 Gemini 模型。兩種模式都請填寫模型清單中的顯示名稱。",
"endpointHelp": "留空用 Google 預設 API。第三方位址須相容 Gemini API,並支援 agy 內建的 Gemini 模型。",
"modelHelp": "填模型清單裡的顯示名稱,可點「取得模型清單」選擇。只對那次工作階段生效。",
"keyFieldsHint": "模型、Key 和位址都只在「開啟終端機」時使用,不寫進 Antigravity 自己的設定。",
"envFile": "啟動環境變數",
"envFileHint": "從卡片「開啟終端機」啟動 agy 時,CC Switch 只把 API Key 和請求位址傳給那一個 agy 行程,不寫進任何檔案。其他環境變數不會套用。",
+3 -1
View File
@@ -180,6 +180,7 @@
"managedByHermes": "Hermes 托管",
"managedByHermesHint": "该条目定义在 Hermes 的 providers: dict,请在 Hermes Web UI 中编辑或删除。",
"openTerminal": "打开终端",
"openTerminalWithProvider": "使用此供应商打开终端",
"terminalOpened": "终端已打开",
"terminalOpenFailed": "打开终端失败",
"openTerminalDialog": {
@@ -210,7 +211,8 @@
"geminiConfigHint": "使用 .env 格式配置 Gemini",
"form": {
"agy": {
"connectionHint": "不填 API Key 和请求地址时使用 agy 自身的 Google 登录;填写 Key 后是第三方模式。两种模式都只作用于从卡片「打开终端」启动的 agy:Key 和地址只传给那一个进程,模型只对那次会话生效。直接在终端里运行 agy 仍是 Google 登录,模型由 agy 自己的 /model 决定。不填地址时使用 Google 默认 API;第三方地址须兼容 Gemini API,并支持 agy 内置的 Gemini 模型。两种模式都请填写模型列表中的显示名称。",
"endpointHelp": "留空用 Google 默认 API。第三方地址须兼容 Gemini API,并支持 agy 内置的 Gemini 模型。",
"modelHelp": "填模型列表里的显示名称,可点「获取模型列表」选择。只对那次会话生效。",
"keyFieldsHint": "模型、Key 和地址都只在「打开终端」时使用,不写进 Antigravity 自己的设置。",
"envFile": "启动环境变量",
"envFileHint": "从卡片「打开终端」启动 agy 时,CC Switch 只把 API Key 和请求地址传给那一个 agy 进程,不写进任何文件。其他环境变量不会应用。",
@@ -29,6 +29,7 @@ vi.mock("@/components/providers/forms/hooks", async (importOriginal) => {
function renderLogin(
env: Record<string, string> = {},
appId: "agy" | "gemini" = "agy",
category: "official" | "custom" = "custom",
) {
const onSubmit = vi.fn();
render(
@@ -41,7 +42,7 @@ function renderLogin(
onCancel={vi.fn()}
initialData={{
name: "Imported login",
category: "custom",
category,
settingsConfig: {
env,
config: { model: { name: "Gemini 3.8 Flash (High)" } },
@@ -55,7 +56,8 @@ function renderLogin(
describe("Antigravity login and API authentication", () => {
it("saves an imported login after changing its model without a missing-key warning", async () => {
const onSubmit = renderLogin();
// 导入的登录卡一律是 official(agy_config 导入时设定)
const onSubmit = renderLogin({}, "agy", "official");
fireEvent.change(screen.getByDisplayValue("Gemini 3.8 Flash (High)"), {
target: { value: "Gemini 3.8 Flash (Low)" },
});
@@ -79,6 +81,16 @@ describe("Antigravity login and API authentication", () => {
).not.toBeInTheDocument();
});
it("warns when a non-official card has no API key", async () => {
// 不填 Key 的登录模式就是官方卡,非官方卡必须带 Key
const onSubmit = renderLogin();
fireEvent.click(screen.getByRole("button", { name: "save-provider" }));
expect(
await screen.findByRole("button", { name: "仍要保存" }),
).toBeInTheDocument();
expect(onSubmit).not.toHaveBeenCalled();
});
it("still warns when a custom endpoint has no API key", async () => {
const onSubmit = renderLogin({
GOOGLE_GEMINI_BASE_URL: "https://relay.example",
+1 -1
View File
@@ -51,7 +51,7 @@ describe("Antigravity locale parity", () => {
expect(reference).toEqual(
expect.arrayContaining([
"apps.agy",
"provider.form.agy.connectionHint",
"provider.form.agy.modelHelp",
"sessionManager.agyDeleteHint",
]),
);