-
+
+
+
+
+
{
+ const next = !on
+ return enqueueProviderUpdate({
+ getSettings: () => useAppStore.getState().settings,
+ fallbackSettings: settings,
+ updateSettings,
+ buildUpdate: (latest) =>
+ buildNativeChatProviderUpdate(latest, agent, next)
+ })
+ }}
+ />
-
-
{
- const next = !on
- return enqueueProviderUpdate({
- getSettings: () => useAppStore.getState().settings,
- fallbackSettings: settings,
- updateSettings,
- buildUpdate: (latest) => buildNativeChatProviderUpdate(latest, agent, next)
- })
- }}
- />
+ {/* Chats sign in on the host they run on, under the program that serves them. */}
+ {agent === 'antigravity' && enabledInAgents ? (
+
+ ) : null}
)
})}
diff --git a/src/renderer/src/i18n/en-runtime-required.json b/src/renderer/src/i18n/en-runtime-required.json
index 909bb04264e..a9739ece499 100644
--- a/src/renderer/src/i18n/en-runtime-required.json
+++ b/src/renderer/src/i18n/en-runtime-required.json
@@ -2902,6 +2902,7 @@
"agentCommandNotSignedIn": "Sign in to {{agent}} with `{{loginCommand}}` on the computer running this chat.",
"agentNotSignedIn": "Sign in to {{agent}}.",
"answerUnconfirmed": "Your answer was recorded but the agent did not confirm it.",
+ "antigravityNotSignedIn": "Antigravity isn't signed in. Sign in under Settings → Accounts → Antigravity.",
"argumentsMissingValue": "Saved Arguments need a value for {{option}}.",
"argumentsMultipleValues": "Saved Arguments give {{option}} more than one value.",
"argumentsPositionalPrompt": "Saved Arguments include a prompt.",
diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json
index 91bdd78b50c..bb460b5ba0d 100644
--- a/src/renderer/src/i18n/locales/en.json
+++ b/src/renderer/src/i18n/locales/en.json
@@ -216,7 +216,8 @@
"notInstalled": "Not installed",
"install": "Install",
"toggleLabel": "Open {{value0}} as a chat",
- "antigravityDescription": "Antigravity chats run a server Orca downloads to this host, separate from the agy CLI."
+ "antigravityDescription": "Antigravity chats run a server Orca downloads to this host, separate from the agy CLI.",
+ "antigravityNotSignedIn": "Not signed in. Chats need an Antigravity sign-in, separate from your agy terminal account."
}
},
"terminal": {
@@ -18320,6 +18321,7 @@
"claudeManagedNotSignedIn": "This Claude account isn't signed in. Sign in again in Claude Accounts settings.",
"codexSystemNotSignedIn": "Codex isn't signed in. Run `{{loginCommand}}`.",
"codexManagedNotSignedIn": "This Codex account isn't signed in. Sign in again in Codex Accounts settings.",
+ "antigravityNotSignedIn": "Antigravity isn't signed in. Sign in under Settings → Accounts → Antigravity.",
"cliMissing": "{{agent}} wasn't found on the computer running this chat. Install it, or check its Command in Settings → Agents.",
"signInFirst": "Sign in first.",
"signInThenRunCommand": "Sign in, then run /{{command}} again.",
@@ -19467,12 +19469,14 @@
"cancelFailed": "Couldn't cancel the installation. Try again.",
"install": "Install {{agent}}",
"installManaged": "Install Orca's copy",
- "installed": "Installed.",
+ "installed": "Installed",
"notInstalled": "Not installed.",
"progress": "{{agent}} download",
"readFailed": "Could not check {{agent}} on this host. Retrying.",
- "reinstall": "Reinstall",
+ "reinstall": "Reinstall {{agent}}",
+ "reinstallShort": "Reinstall",
"remove": "Remove downloaded runtime",
+ "moreActions": "More actions for {{agent}}",
"removeConfirm": "Remove",
"removeDescription": "{{agent}} chats that are running stop. Their history is kept.",
"removeKeep": "Cancel",
@@ -20010,6 +20014,27 @@
"description": "Agents there use the distro's default Claude and Codex accounts until Orca in WSL is updated."
},
"antigravity": {
+ "chatTitle": "Antigravity chats",
+ "chatDescription": "Chats use their own Antigravity sign-in, separate from your agy account above. Sign in once on this device.",
+ "chatSignIn": "Sign in",
+ "chatChangeAccount": "Change account",
+ "chatRetry": "Retry sign-in",
+ "chatCancel": "Cancel",
+ "chatCopyLink": "Copy sign-in link",
+ "chatCopyFailed": "Could not copy the link.",
+ "chatStarting": "Starting sign-in…",
+ "chatWaiting": "Finish signing in in your browser.",
+ "chatOpenFailed": "Copy the link and open it in your browser.",
+ "chatVerifying": "Finishing sign-in…",
+ "chatPasteHelp": "If the last page after signing in doesn't load, copy its full address from your browser and paste it here.",
+ "chatContinue": "Continue",
+ "chatSignedInOn": "Signed in on {{host}}.",
+ "chatCancelled": "Sign-in cancelled.",
+ "chatExpired": "Sign-in timed out. Try again.",
+ "chatFailed": "Sign-in failed. Try again.",
+ "chatNotSignedIn": "Not signed in on {{host}}.",
+ "chatOtherClient": "Sign-in is in progress in another client.",
+ "chatReadFailed": "Could not check the sign-in on {{host}}.",
"scope": "Manage the native agy account on {{host}}.",
"signIn": "Start agy on this host and complete its browser sign-in, then save the current account. To add a different account, use /logout in agy and sign in again.",
"docs": "Sign-in instructions",
diff --git a/src/renderer/src/i18n/locales/es.json b/src/renderer/src/i18n/locales/es.json
index 30127b10cfd..089da9f8916 100644
--- a/src/renderer/src/i18n/locales/es.json
+++ b/src/renderer/src/i18n/locales/es.json
@@ -39,7 +39,8 @@
},
"providers": {
"title": "Proveedores",
- "antigravityDescription": "Los chats de Antigravity usan un servidor que Orca descarga en este host, independiente de la CLI de agy."
+ "antigravityDescription": "Los chats de Antigravity usan un servidor que Orca descarga en este host, independiente de la CLI de agy.",
+ "antigravityNotSignedIn": "No has iniciado sesión. Los chats necesitan un inicio de sesión de Antigravity, independiente de tu cuenta de agy en la terminal."
}
},
"terminal": {
@@ -18060,6 +18061,7 @@
"claudeManagedNotSignedIn": "Esta cuenta de Claude no ha iniciado sesión. Vuelve a iniciar sesión en la configuración de Cuentas de Claude.",
"codexSystemNotSignedIn": "Codex no ha iniciado sesión. Ejecuta `{{loginCommand}}`.",
"codexManagedNotSignedIn": "Esta cuenta de Codex no ha iniciado sesión. Vuelve a iniciar sesión en la configuración de Cuentas de Codex.",
+ "antigravityNotSignedIn": "Antigravity no ha iniciado sesión. Inicia sesión en Ajustes → Cuentas → Antigravity.",
"cliMissing": "No se encontró {{agent}} en el equipo que ejecuta este chat. Instálelo o revise su Comando en Ajustes → Agentes.",
"signInFirst": "Inicie sesión primero.",
"signInThenRunCommand": "Inicie sesión y vuelva a ejecutar /{{command}}.",
@@ -19232,12 +19234,14 @@
"cancelFailed": "No se pudo cancelar la instalación. Inténtalo de nuevo.",
"install": "Instalar {{agent}}",
"installManaged": "Instalar la copia de Orca",
- "installed": "Instalado.",
+ "installed": "Instalado",
"notInstalled": "No instalado.",
"progress": "Descarga de {{agent}}",
"readFailed": "No se pudo comprobar {{agent}} en este host. Reintentando.",
- "reinstall": "Reinstalar",
+ "reinstall": "Reinstalar {{agent}}",
+ "reinstallShort": "Reinstalar",
"remove": "Quitar el runtime descargado",
+ "moreActions": "Más acciones para {{agent}}",
"removeConfirm": "Quitar",
"removeDescription": "Los chats de {{agent}} en curso se detienen. Su historial se conserva.",
"removeKeep": "Cancelar",
@@ -19675,6 +19679,27 @@
"cancel": "Cancelar"
},
"antigravity": {
+ "chatTitle": "Chats de Antigravity",
+ "chatDescription": "Los chats usan su propio inicio de sesión de Antigravity, independiente de tu cuenta de agy de arriba. Inicia sesión una vez en este dispositivo.",
+ "chatSignIn": "Iniciar sesión",
+ "chatChangeAccount": "Cambiar de cuenta",
+ "chatRetry": "Reintentar inicio de sesión",
+ "chatCancel": "Cancelar",
+ "chatCopyLink": "Copiar enlace de inicio de sesión",
+ "chatCopyFailed": "No se pudo copiar el enlace.",
+ "chatStarting": "Iniciando sesión…",
+ "chatWaiting": "Termina de iniciar sesión en tu navegador.",
+ "chatOpenFailed": "Copia el enlace y ábrelo en tu navegador.",
+ "chatVerifying": "Finalizando el inicio de sesión…",
+ "chatPasteHelp": "Si la última página después de iniciar sesión no carga, copia su dirección completa desde el navegador y pégala aquí.",
+ "chatContinue": "Continuar",
+ "chatSignedInOn": "Sesión iniciada en {{host}}.",
+ "chatCancelled": "Inicio de sesión cancelado.",
+ "chatExpired": "El inicio de sesión caducó. Inténtalo de nuevo.",
+ "chatFailed": "No se pudo iniciar sesión. Inténtalo de nuevo.",
+ "chatNotSignedIn": "No has iniciado sesión en {{host}}.",
+ "chatOtherClient": "Hay un inicio de sesión en curso en otro cliente.",
+ "chatReadFailed": "No se pudo comprobar el inicio de sesión en {{host}}.",
"retry": "Reintentar",
"remove": "Eliminar",
"usage": "Actualizar uso",
diff --git a/src/renderer/src/i18n/locales/fr.json b/src/renderer/src/i18n/locales/fr.json
index 727e00d12f5..5e7994b4737 100644
--- a/src/renderer/src/i18n/locales/fr.json
+++ b/src/renderer/src/i18n/locales/fr.json
@@ -143,7 +143,8 @@
},
"providers": {
"title": "Fournisseurs",
- "antigravityDescription": "Les chats Antigravity utilisent un serveur qu’Orca télécharge sur cet hôte, distinct de la CLI agy."
+ "antigravityDescription": "Les chats Antigravity utilisent un serveur qu’Orca télécharge sur cet hôte, distinct de la CLI agy.",
+ "antigravityNotSignedIn": "Non connecté. Les discussions nécessitent une connexion Antigravity, distincte de votre compte agy du terminal."
}
},
"terminal": {
@@ -18169,6 +18170,7 @@
"claudeManagedNotSignedIn": "Ce compte Claude n’est pas connecté. Reconnectez-vous dans les paramètres des Comptes Claude.",
"codexSystemNotSignedIn": "Codex n’est pas connecté. Exécutez `{{loginCommand}}`.",
"codexManagedNotSignedIn": "Ce compte Codex n’est pas connecté. Reconnectez-vous dans les paramètres des Comptes Codex.",
+ "antigravityNotSignedIn": "Antigravity n’est pas connecté. Connectez-vous dans Paramètres → Comptes → Antigravity.",
"cliMissing": "{{agent}} est introuvable sur l’ordinateur qui exécute cette conversation. Installez-le ou vérifiez sa Commande dans Paramètres → Agents.",
"signInFirst": "Connectez-vous d'abord.",
"signInThenRunCommand": "Connectez-vous, puis relancez /{{command}}.",
@@ -19348,12 +19350,14 @@
"cancelFailed": "Impossible d'annuler l'installation. Réessayez.",
"install": "Installer {{agent}}",
"installManaged": "Installer la copie d'Orca",
- "installed": "Installé.",
+ "installed": "Installé",
"notInstalled": "Non installé.",
"progress": "Téléchargement de {{agent}}",
"readFailed": "Impossible de vérifier {{agent}} sur cet hôte. Nouvel essai en cours.",
- "reinstall": "Réinstaller",
+ "reinstall": "Réinstaller {{agent}}",
+ "reinstallShort": "Réinstaller",
"remove": "Supprimer le runtime téléchargé",
+ "moreActions": "Plus d'actions pour {{agent}}",
"removeConfirm": "Supprimer",
"removeDescription": "Les chats {{agent}} en cours s'arrêtent. Leur historique est conservé.",
"removeKeep": "Annuler",
@@ -19828,6 +19832,27 @@
"cancel": "Annuler"
},
"antigravity": {
+ "chatTitle": "Discussions Antigravity",
+ "chatDescription": "Les discussions utilisent leur propre connexion Antigravity, distincte de votre compte agy ci-dessus. Connectez-vous une seule fois sur cet appareil.",
+ "chatSignIn": "Se connecter",
+ "chatChangeAccount": "Changer de compte",
+ "chatRetry": "Réessayer la connexion",
+ "chatCancel": "Annuler",
+ "chatCopyLink": "Copier le lien de connexion",
+ "chatCopyFailed": "Impossible de copier le lien.",
+ "chatStarting": "Démarrage de la connexion…",
+ "chatWaiting": "Terminez la connexion dans votre navigateur.",
+ "chatOpenFailed": "Copiez le lien et ouvrez-le dans votre navigateur.",
+ "chatVerifying": "Finalisation de la connexion…",
+ "chatPasteHelp": "Si la dernière page après la connexion ne se charge pas, copiez son adresse complète depuis votre navigateur et collez-la ici.",
+ "chatContinue": "Continuer",
+ "chatSignedInOn": "Connecté sur {{host}}.",
+ "chatCancelled": "Connexion annulée.",
+ "chatExpired": "La connexion a expiré. Réessayez.",
+ "chatFailed": "La connexion a échoué. Réessayez.",
+ "chatNotSignedIn": "Non connecté sur {{host}}.",
+ "chatOtherClient": "Une connexion est en cours dans un autre client.",
+ "chatReadFailed": "Impossible de vérifier la connexion sur {{host}}.",
"scope": "Gérez le compte agy natif sur {{host}}.",
"signIn": "Démarrez agy sur cet hôte et complétez la connexion au navigateur, puis enregistrez le compte actuel. Pour ajouter un autre compte, utilisez /logout dans agy et reconnectez-vous.",
"docs": "Instructions de connexion",
diff --git a/src/renderer/src/i18n/locales/ja.json b/src/renderer/src/i18n/locales/ja.json
index 6e241475cf3..3c0675ee201 100644
--- a/src/renderer/src/i18n/locales/ja.json
+++ b/src/renderer/src/i18n/locales/ja.json
@@ -39,7 +39,8 @@
},
"providers": {
"title": "プロバイダー",
- "antigravityDescription": "Antigravity チャットは、Orca がこのホストにダウンロードするサーバーで動作します。agy CLI とは別です。"
+ "antigravityDescription": "Antigravity チャットは、Orca がこのホストにダウンロードするサーバーで動作します。agy CLI とは別です。",
+ "antigravityNotSignedIn": "サインインしていません。チャットには、ターミナルの agy アカウントとは別の Antigravity サインインが必要です。"
}
},
"terminal": {
@@ -18100,6 +18101,7 @@
"claudeManagedNotSignedIn": "この Claude アカウントにサインインしていません。設定の Claude アカウントで再度サインインしてください。",
"codexSystemNotSignedIn": "Codex にサインインしていません。`{{loginCommand}}` を実行してください。",
"codexManagedNotSignedIn": "この Codex アカウントにサインインしていません。設定の Codex アカウントで再度サインインしてください。",
+ "antigravityNotSignedIn": "Antigravity にサインインしていません。[設定] → [アカウント] → [Antigravity] でサインインしてください。",
"cliMissing": "このチャットを実行しているコンピューターで {{agent}} が見つかりませんでした。インストールするか、[設定] → [Agent] の [コマンド] を確認してください。",
"signInFirst": "先にサインインしてください。",
"signInThenRunCommand": "サインインしてから、/{{command}} をもう一度実行してください。",
@@ -19244,12 +19246,14 @@
"cancelFailed": "インストールをキャンセルできませんでした。もう一度お試しください。",
"install": "{{agent}} をインストール",
"installManaged": "Orca のコピーをインストール",
- "installed": "インストール済みです。",
+ "installed": "インストール済み",
"notInstalled": "インストールされていません。",
"progress": "{{agent}} のダウンロード",
"readFailed": "このホストの {{agent}} を確認できませんでした。再試行しています。",
- "reinstall": "再インストール",
+ "reinstall": "{{agent}} を再インストール",
+ "reinstallShort": "再インストール",
"remove": "ダウンロードしたランタイムを削除",
+ "moreActions": "{{agent}}の追加操作",
"removeConfirm": "削除",
"removeDescription": "実行中の {{agent}} チャットは停止します。履歴は保持されます。",
"removeKeep": "キャンセル",
@@ -19828,6 +19832,27 @@
"cancel": "キャンセル"
},
"antigravity": {
+ "chatTitle": "Antigravity チャット",
+ "chatDescription": "チャットは上の agy アカウントとは別の、独自の Antigravity サインインを使用します。このデバイスで一度サインインしてください。",
+ "chatSignIn": "サインイン",
+ "chatChangeAccount": "アカウントを変更",
+ "chatRetry": "サインインを再試行",
+ "chatCancel": "キャンセル",
+ "chatCopyLink": "サインインリンクをコピー",
+ "chatCopyFailed": "リンクをコピーできませんでした。",
+ "chatStarting": "サインインを開始しています…",
+ "chatWaiting": "ブラウザーでサインインを完了してください。",
+ "chatOpenFailed": "リンクをコピーしてブラウザーで開いてください。",
+ "chatVerifying": "サインインを完了しています…",
+ "chatPasteHelp": "サインイン後の最後のページが読み込まれない場合は、ブラウザーからそのページの完全なアドレスをコピーしてここに貼り付けてください。",
+ "chatContinue": "続行",
+ "chatSignedInOn": "{{host}} でサインイン済みです。",
+ "chatCancelled": "サインインをキャンセルしました。",
+ "chatExpired": "サインインがタイムアウトしました。もう一度お試しください。",
+ "chatFailed": "サインインに失敗しました。もう一度お試しください。",
+ "chatNotSignedIn": "{{host}} でサインインしていません。",
+ "chatOtherClient": "別のクライアントでサインインが進行中です。",
+ "chatReadFailed": "{{host}} のサインインを確認できませんでした。",
"scope": "{{host}} でネイティブ agy アカウントを管理します。",
"signIn": "このホストで agy を起動し、ブラウザのサインインを完了して、現在のアカウントを保存します。別のアカウントを追加するには、agy で /logout を使用し、再度サインインします。",
"docs": "サインイン手順",
diff --git a/src/renderer/src/i18n/locales/ko.json b/src/renderer/src/i18n/locales/ko.json
index 3e3ac2c90ed..a710e5c2751 100644
--- a/src/renderer/src/i18n/locales/ko.json
+++ b/src/renderer/src/i18n/locales/ko.json
@@ -39,7 +39,8 @@
},
"providers": {
"title": "공급자",
- "antigravityDescription": "Antigravity 채팅은 Orca가 이 호스트에 다운로드하는 서버에서 실행되며, agy CLI와는 별개입니다."
+ "antigravityDescription": "Antigravity 채팅은 Orca가 이 호스트에 다운로드하는 서버에서 실행되며, agy CLI와는 별개입니다.",
+ "antigravityNotSignedIn": "로그인되어 있지 않습니다. 채팅에는 터미널 agy 계정과 별개인 Antigravity 로그인이 필요합니다."
}
},
"terminal": {
@@ -18100,6 +18101,7 @@
"claudeManagedNotSignedIn": "이 Claude 계정에 로그인하지 않았습니다. Claude 계정 설정에서 다시 로그인하세요.",
"codexSystemNotSignedIn": "Codex에 로그인하지 않았습니다. `{{loginCommand}}`을 실행하세요.",
"codexManagedNotSignedIn": "이 Codex 계정에 로그인하지 않았습니다. Codex 계정 설정에서 다시 로그인하세요.",
+ "antigravityNotSignedIn": "Antigravity에 로그인되어 있지 않습니다. 설정 → 계정 → Antigravity에서 로그인하세요.",
"cliMissing": "이 채팅을 실행하는 컴퓨터에서 {{agent}}를 찾을 수 없습니다. 설치하거나 설정 → 에이전트에서 명령을 확인하세요.",
"signInFirst": "먼저 로그인하세요.",
"signInThenRunCommand": "로그인한 다음 /{{command}}을(를) 다시 실행하세요.",
@@ -19244,12 +19246,14 @@
"cancelFailed": "설치를 취소하지 못했습니다. 다시 시도하세요.",
"install": "{{agent}} 설치",
"installManaged": "Orca 사본 설치",
- "installed": "설치되었습니다.",
+ "installed": "설치됨",
"notInstalled": "설치되지 않았습니다.",
"progress": "{{agent}} 다운로드",
"readFailed": "이 호스트에서 {{agent}}을(를) 확인할 수 없습니다. 다시 시도하는 중입니다.",
- "reinstall": "재설치",
+ "reinstall": "{{agent}} 재설치",
+ "reinstallShort": "재설치",
"remove": "다운로드한 런타임 제거",
+ "moreActions": "{{agent}}의 더 많은 작업",
"removeConfirm": "제거",
"removeDescription": "실행 중인 {{agent}} 채팅이 중지됩니다. 기록은 유지됩니다.",
"removeKeep": "취소",
@@ -19828,6 +19832,27 @@
"cancel": "취소"
},
"antigravity": {
+ "chatTitle": "Antigravity 채팅",
+ "chatDescription": "채팅은 위의 agy 계정과 별개인 자체 Antigravity 로그인을 사용합니다. 이 기기에서 한 번 로그인하세요.",
+ "chatSignIn": "로그인",
+ "chatChangeAccount": "계정 변경",
+ "chatRetry": "로그인 다시 시도",
+ "chatCancel": "취소",
+ "chatCopyLink": "로그인 링크 복사",
+ "chatCopyFailed": "링크를 복사할 수 없습니다.",
+ "chatStarting": "로그인을 시작하는 중…",
+ "chatWaiting": "브라우저에서 로그인을 완료하세요.",
+ "chatOpenFailed": "링크를 복사해 브라우저에서 여세요.",
+ "chatVerifying": "로그인을 마무리하는 중…",
+ "chatPasteHelp": "로그인 후 마지막 페이지가 열리지 않으면 브라우저에서 그 페이지의 전체 주소를 복사해 여기에 붙여 넣으세요.",
+ "chatContinue": "계속",
+ "chatSignedInOn": "{{host}}에 로그인되어 있습니다.",
+ "chatCancelled": "로그인이 취소되었습니다.",
+ "chatExpired": "로그인 시간이 초과되었습니다. 다시 시도하세요.",
+ "chatFailed": "로그인하지 못했습니다. 다시 시도하세요.",
+ "chatNotSignedIn": "{{host}}에 로그인되어 있지 않습니다.",
+ "chatOtherClient": "다른 클라이언트에서 로그인이 진행 중입니다.",
+ "chatReadFailed": "{{host}}의 로그인을 확인할 수 없습니다.",
"scope": "{{host}}의 agy 자체 계정을 관리합니다.",
"signIn": "이 호스트에서 agy를 실행하고 브라우저 로그인을 완료한 후 현재 계정을 저장하세요. 다른 계정을 추가하려면 agy에서 /logout 명령을 실행하고 다시 로그인하세요.",
"docs": "로그인 안내",
diff --git a/src/renderer/src/i18n/locales/zh.json b/src/renderer/src/i18n/locales/zh.json
index 64eab50aaee..6f9b51f6e05 100644
--- a/src/renderer/src/i18n/locales/zh.json
+++ b/src/renderer/src/i18n/locales/zh.json
@@ -39,7 +39,8 @@
},
"providers": {
"title": "提供方",
- "antigravityDescription": "Antigravity 聊天运行在 Orca 下载到此主机的服务器上,与 agy CLI 相互独立。"
+ "antigravityDescription": "Antigravity 聊天运行在 Orca 下载到此主机的服务器上,与 agy CLI 相互独立。",
+ "antigravityNotSignedIn": "未登录。聊天需要单独的 Antigravity 登录,与终端中的 agy 帐户分开。"
}
},
"terminal": {
@@ -18065,6 +18066,7 @@
"claudeManagedNotSignedIn": "此 Claude 账户尚未登录。请在 Claude 账户设置中重新登录。",
"codexSystemNotSignedIn": "Codex 尚未登录。请运行 `{{loginCommand}}`。",
"codexManagedNotSignedIn": "此 Codex 账户尚未登录。请在 Codex 账户设置中重新登录。",
+ "antigravityNotSignedIn": "Antigravity 尚未登录。请在设置 → 账户 → Antigravity 中登录。",
"cliMissing": "在运行此聊天的计算机上找不到 {{agent}}。请安装它,或检查设置 → 智能体中的命令。",
"signInFirst": "请先登录。",
"signInThenRunCommand": "请登录,然后重新运行 /{{command}}。",
@@ -19244,12 +19246,14 @@
"cancelFailed": "无法取消安装。请重试。",
"install": "安装 {{agent}}",
"installManaged": "安装 Orca 的副本",
- "installed": "已安装。",
+ "installed": "已安装",
"notInstalled": "未安装。",
"progress": "{{agent}} 下载",
"readFailed": "无法检查此主机上的 {{agent}}。正在重试。",
- "reinstall": "重新安装",
+ "reinstall": "重新安装 {{agent}}",
+ "reinstallShort": "重新安装",
"remove": "移除已下载的运行时",
+ "moreActions": "{{agent}} 的更多操作",
"removeConfirm": "移除",
"removeDescription": "正在运行的 {{agent}} 聊天会停止。其历史记录会保留。",
"removeKeep": "取消",
@@ -19828,6 +19832,27 @@
"cancel": "取消"
},
"antigravity": {
+ "chatTitle": "Antigravity 聊天",
+ "chatDescription": "聊天使用自己的 Antigravity 登录,与上方的 agy 帐户分开。在此设备上登录一次即可。",
+ "chatSignIn": "登录",
+ "chatChangeAccount": "更换帐户",
+ "chatRetry": "重试登录",
+ "chatCancel": "取消",
+ "chatCopyLink": "复制登录链接",
+ "chatCopyFailed": "无法复制链接。",
+ "chatStarting": "正在开始登录…",
+ "chatWaiting": "请在浏览器中完成登录。",
+ "chatOpenFailed": "复制链接并在浏览器中打开。",
+ "chatVerifying": "正在完成登录…",
+ "chatPasteHelp": "如果登录后的最后一个页面无法加载,请从浏览器复制它的完整地址并粘贴到这里。",
+ "chatContinue": "继续",
+ "chatSignedInOn": "已在 {{host}} 上登录。",
+ "chatCancelled": "已取消登录。",
+ "chatExpired": "登录超时。请重试。",
+ "chatFailed": "登录失败。请重试。",
+ "chatNotSignedIn": "未在 {{host}} 上登录。",
+ "chatOtherClient": "另一个客户端正在登录。",
+ "chatReadFailed": "无法检查 {{host}} 上的登录。",
"scope": "管理 {{host}} 上的本机 agy 帐户。",
"signIn": "在此主机上启动 agy 并完成其浏览器登录,然后保存当前帐户。要添加其他帐户,请在 agy 中使用 /logout 并再次登录。",
"docs": "登录说明",
diff --git a/src/renderer/src/runtime/antigravity-chat-sign-in-status.ts b/src/renderer/src/runtime/antigravity-chat-sign-in-status.ts
new file mode 100644
index 00000000000..8be8b2efe0b
--- /dev/null
+++ b/src/renderer/src/runtime/antigravity-chat-sign-in-status.ts
@@ -0,0 +1,189 @@
+import type {
+ AntigravityChatAuth,
+ AntigravityChatSignInState,
+ AntigravityChatSignInStatus
+} from '../../../shared/antigravity-account-types'
+import type { RuntimeClientTarget } from '../../../shared/runtime-client-target'
+import {
+ createRevisionedHostStateStore,
+ type RevisionedHostSnapshot
+} from './revisioned-host-state-store'
+import {
+ callAntigravityChatSignIn,
+ subscribeAntigravityChatSignIn
+} from './runtime-antigravity-accounts-client'
+
+/**
+ * Each host's Antigravity chat sign-in, followed like an install's status: every Settings row for
+ * a host shows the same flow and the server's last sign-in answer, from another client's flow too.
+ */
+
+export type AntigravityChatSignInView = {
+ /** Whether the host has answered; `readFailed` says nothing about the sign-in. */
+ status: RevisionedHostSnapshot
['kind']
+ flow: AntigravityChatSignInState | null
+ auth: AntigravityChatAuth
+ /** Why this client's last request failed; empty when it gave no reason. */
+ error: string | null
+ /** The browser could not open the link this client asked for. */
+ openFailed: boolean
+}
+
+type Notes = Pick
+type Local = {
+ owner: RuntimeClientTarget
+ observers: number
+ /** The flow this client started while shown, whose link it opens once. */
+ opens: string | null
+ starts: number
+ notes: Notes
+}
+
+const NO_NOTES: Notes = { error: null, openFailed: false }
+const hostKey = (owner: RuntimeClientTarget): string =>
+ owner.kind === 'local' ? '' : owner.environmentId
+
+const signIns = createRevisionedHostStateStore({
+ key: hostKey,
+ hostKey,
+ subscribe: subscribeAntigravityChatSignIn
+})
+const locals = new Map()
+type Shown = {
+ snapshot: RevisionedHostSnapshot
+ notes: Notes
+ view: AntigravityChatSignInView
+}
+const views = new Map()
+const listeners = new Set<() => void>()
+
+export const antigravityChatSignInRunning = (
+ flow: AntigravityChatSignInState | null
+): flow is AntigravityChatSignInState =>
+ flow?.phase === 'starting' || flow?.phase === 'waiting' || flow?.phase === 'verifying'
+
+const flowOf = (owner: RuntimeClientTarget): AntigravityChatSignInState | null => {
+ const snapshot = signIns.read(owner)
+ return snapshot.kind === 'known' ? snapshot.state.flow : null
+}
+
+function localFor(owner: RuntimeClientTarget): Local {
+ let local = locals.get(hostKey(owner))
+ if (!local) {
+ local = { owner, observers: 0, opens: null, starts: 0, notes: NO_NOTES }
+ locals.set(hostKey(owner), local)
+ }
+ return local
+}
+
+function note(local: Local, patch: Partial): void {
+ local.notes = { ...local.notes, ...patch }
+ listeners.forEach((listener) => listener())
+}
+
+const message = (cause: unknown): string => (cause instanceof Error ? cause.message : '')
+
+/** Opens the link of a flow this client started, once, when the host first shows it. */
+function openStartedLinks(): void {
+ for (const local of locals.values()) {
+ const flow = flowOf(local.owner)
+ if (local.opens && flow?.flowId === local.opens && flow.phase === 'waiting' && flow.url) {
+ const { flowId } = flow
+ local.opens = null
+ void window.api.shell.openUrl(flow.url).catch(() => {
+ if (flowOf(local.owner)?.flowId === flowId) {
+ note(local, { openFailed: true })
+ }
+ })
+ }
+ }
+}
+signIns.subscribe(openStartedLinks)
+
+/** Starts a sign-in on the host, or rejoins this client's running one; the host's status shows it. */
+export async function startAntigravityChatSignIn(owner: RuntimeClientTarget): Promise {
+ const local = localFor(owner)
+ const ticket = ++local.starts
+ note(local, NO_NOTES)
+ try {
+ const { flowId } = await callAntigravityChatSignIn<{ flowId: string }>(owner, 'Start')
+ // A row that left before the host answered opens nothing; one shown later shows the flow.
+ if (ticket === local.starts) {
+ local.opens = local.observers > 0 ? flowId : null
+ openStartedLinks()
+ }
+ } catch (cause) {
+ if (ticket === local.starts) {
+ note(local, { error: message(cause) })
+ }
+ }
+}
+
+/** Only the client that started a flow is told its id, so only it can cancel or complete it. */
+const ownFlowId = (owner: RuntimeClientTarget): string | null => flowOf(owner)?.flowId ?? null
+
+export async function cancelAntigravityChatSignIn(owner: RuntimeClientTarget): Promise {
+ const flowId = ownFlowId(owner)
+ if (flowId) {
+ await callAntigravityChatSignIn(owner, 'Cancel', { flowId }).catch(
+ (cause: unknown) => note(localFor(owner), { error: message(cause) })
+ )
+ }
+}
+
+/** Sends the address the browser ended on to the host, for a browser on another computer.
+ * Resolves to the host's refusal, empty when it gave no reason, or null once it took it. */
+export async function completeAntigravityChatSignIn(
+ owner: RuntimeClientTarget,
+ callbackUrl: string
+): Promise {
+ const flowId = ownFlowId(owner)
+ if (!flowId) {
+ return ''
+ }
+ return callAntigravityChatSignIn(owner, 'Complete', { flowId, callbackUrl }).then(
+ () => null,
+ message
+ )
+}
+
+/** Follows the host's sign-in while a row shows it. Returns the release. */
+export function observeAntigravityChatSignIn(owner: RuntimeClientTarget): () => void {
+ const local = localFor(owner)
+ local.observers += 1
+ const release = signIns.observe(owner)
+ return () => {
+ release()
+ local.observers -= 1
+ if (local.observers === 0) {
+ local.opens = null
+ }
+ }
+}
+
+export function readAntigravityChatSignIn(owner: RuntimeClientTarget): AntigravityChatSignInView {
+ const snapshot = signIns.read(owner)
+ const notes = locals.get(hostKey(owner))?.notes ?? NO_NOTES
+ const cached = views.get(hostKey(owner))
+ if (cached?.snapshot === snapshot && cached.notes === notes) {
+ return cached.view
+ }
+ const known = snapshot.kind === 'known' ? snapshot.state : null
+ const view = {
+ status: snapshot.kind,
+ flow: known?.flow ?? null,
+ auth: known?.auth ?? 'unknown',
+ ...notes
+ }
+ views.set(hostKey(owner), { snapshot, notes, view })
+ return view
+}
+
+export function subscribeAntigravityChatSignIns(listener: () => void): () => void {
+ listeners.add(listener)
+ const unsubscribe = signIns.subscribe(listener)
+ return () => {
+ listeners.delete(listener)
+ unsubscribe()
+ }
+}
diff --git a/src/renderer/src/runtime/managed-runtime-install-status.ts b/src/renderer/src/runtime/managed-runtime-install-status.ts
index 2285e4520e5..03b4458526b 100644
--- a/src/renderer/src/runtime/managed-runtime-install-status.ts
+++ b/src/renderer/src/runtime/managed-runtime-install-status.ts
@@ -1,13 +1,15 @@
import type { ManagedRuntimeInstallState } from '../../../shared/managed-runtime-install'
import type { RuntimeClientTarget } from '../../../shared/runtime-client-target'
+import {
+ createRevisionedHostStateStore,
+ type RevisionedHostSnapshot
+} from './revisioned-host-state-store'
import { callRuntimeRpc } from './runtime-rpc-client'
import { subscribeRuntimeRpc } from './runtime-rpc-subscribe'
/**
* What each host says about the runtime it installs for an agent, one entry per host runtime
- * instance and agent, so Settings and a chat read the same answer. While any surface observes
- * an entry, one subscription carries the host's pushed state, idle included, so another client's
- * change shows up too; the last surface leaving ends it.
+ * instance and agent, so Settings and a chat read the same answer.
*/
export type ManagedRuntimeInstallSubject = {
@@ -17,163 +19,27 @@ export type ManagedRuntimeInstallSubject = {
agent: string
}
-export type ManagedRuntimeInstallSnapshot =
- | { kind: 'unknown' }
- /** No answer yet and the last read failed: says nothing about whether it is installed. */
- | { kind: 'readFailed' }
- | { kind: 'known'; state: ManagedRuntimeInstallState }
-
-type Entry = {
- subject: ManagedRuntimeInstallSubject
- snapshot: ManagedRuntimeInstallSnapshot
- observers: number
- /** Bumped by every subscribe and every loss; a frame from another generation is stale. */
- generation: number
- /** The generation the shown state came from: revisions only order one subscription's frames. */
- shownGeneration: number
- stream: { unsubscribe: () => void } | null
- retry: ReturnType | null
- failures: number
-}
-
-const UNKNOWN: ManagedRuntimeInstallSnapshot = { kind: 'unknown' }
-
-const entries = new Map()
-const listeners = new Set<() => void>()
+export type ManagedRuntimeInstallSnapshot = RevisionedHostSnapshot
const hostAgentKey = ({ target, agent }: ManagedRuntimeInstallSubject): string =>
`${target.kind === 'local' ? '' : target.environmentId}\u0000${agent}`
-const subjectKey = (subject: ManagedRuntimeInstallSubject): string =>
- `${hostAgentKey(subject)}\u0000${subject.runtimeId ?? ''}`
-function setSnapshot(entry: Entry, snapshot: ManagedRuntimeInstallSnapshot): void {
- entry.snapshot = snapshot
- listeners.forEach((listener) => listener())
-}
-
-function entryFor(subject: ManagedRuntimeInstallSubject): Entry {
- const key = subjectKey(subject)
- let entry = entries.get(key)
- if (!entry) {
- // A restarted host runtime answers under a new key; the old one's answer is stale.
- for (const [otherKey, other] of entries) {
- if (other.observers === 0 && hostAgentKey(other.subject) === hostAgentKey(subject)) {
- entries.delete(otherKey)
- }
- }
- entry = {
- subject,
- snapshot: UNKNOWN,
- observers: 0,
- generation: 0,
- shownGeneration: 0,
- stream: null,
- retry: null,
- failures: 0
- }
- entries.set(key, entry)
- }
- return entry
-}
-
-function accept(entry: Entry, state: ManagedRuntimeInstallState, generation: number): void {
- // A new subscription may reach a restarted host whose revisions start again at 0.
- if (
- entry.snapshot.kind !== 'known' ||
- generation !== entry.shownGeneration ||
- entry.snapshot.state.revision < state.revision
- ) {
- entry.shownGeneration = generation
- setSnapshot(entry, { kind: 'known', state })
- }
-}
-
-const isInstallState = (value: unknown): value is ManagedRuntimeInstallState =>
- typeof value === 'object' &&
- value !== null &&
- 'revision' in value &&
- typeof value.revision === 'number'
-
-function close(entry: Entry): void {
- entry.generation += 1
- entry.stream?.unsubscribe()
- entry.stream = null
- if (entry.retry) {
- clearTimeout(entry.retry)
- entry.retry = null
- }
-}
-
-function open(entry: Entry): void {
- close(entry)
- const { generation, subject } = entry
- const current = (): boolean => entry.observers > 0 && entry.generation === generation
- const lost = (): void => {
- if (!current()) {
- return
- }
- close(entry)
- // A known answer stays through a blip; only a first read failing changes what is shown.
- if (entry.snapshot.kind !== 'known') {
- setSnapshot(entry, { kind: 'readFailed' })
- }
- entry.failures += 1
- const delay = Math.min(30_000, 1000 * 2 ** (entry.failures - 1))
- entry.retry = setTimeout(() => open(entry), delay)
- }
- void subscribeRuntimeRpc(
- subject.target,
- 'managedRuntime.subscribe',
- { agent: subject.agent },
- {
- onEvent: (result) => {
- if (current() && isInstallState(result)) {
- entry.failures = 0
- accept(entry, result, generation)
- }
- },
- onError: lost,
- onClose: lost
- }
- ).then((opened) => {
- if (current()) {
- entry.stream = opened
- } else {
- opened.unsubscribe()
- }
- }, lost)
-}
-
-export function readManagedRuntimeInstall(
- subject: ManagedRuntimeInstallSubject
-): ManagedRuntimeInstallSnapshot {
- return entries.get(subjectKey(subject))?.snapshot ?? UNKNOWN
-}
+const installs = createRevisionedHostStateStore<
+ ManagedRuntimeInstallSubject,
+ ManagedRuntimeInstallState
+>({
+ key: (subject) => `${hostAgentKey(subject)}\u0000${subject.runtimeId ?? ''}`,
+ hostKey: hostAgentKey,
+ subscribe: ({ target, agent }, handlers) =>
+ subscribeRuntimeRpc(target, 'managedRuntime.subscribe', { agent }, handlers)
+})
+export const readManagedRuntimeInstall = installs.read
/** Subscribes while at least one shown surface observes this subject. Returns the release. */
-export function observeManagedRuntimeInstall(subject: ManagedRuntimeInstallSubject): () => void {
- const entry = entryFor(subject)
- entry.observers += 1
- if (entry.observers === 1) {
- entry.failures = 0
- open(entry)
- }
- return () => {
- entry.observers -= 1
- if (entry.observers === 0) {
- close(entry)
- }
- }
-}
-
+export const observeManagedRuntimeInstall = installs.observe
/** Resubscribes a lost stream at once, e.g. when the host answers again after an outage. */
-export function wakeManagedRuntimeInstall(subject: ManagedRuntimeInstallSubject): void {
- const entry = entries.get(subjectKey(subject))
- if (entry?.retry) {
- entry.failures = 0
- open(entry)
- }
-}
+export const wakeManagedRuntimeInstall = installs.wake
+export const subscribeManagedRuntimeInstalls = installs.subscribe
function callHost(
subject: ManagedRuntimeInstallSubject,
@@ -188,22 +54,11 @@ function callHost(
)
}
-async function startOperation(
- subject: ManagedRuntimeInstallSubject,
- method: 'managedRuntime.install' | 'managedRuntime.remove'
-): Promise {
- const entry = entryFor(subject)
- const { generation } = entry
- const state = await callHost(subject, method, {})
- // A reply that crossed a resubscribe is stale; the new subscription's snapshot governs.
- if (entry.generation === generation) {
- accept(entry, state, generation)
- }
-}
-
/** Starts, or joins, the host's install; rejects with the host's refusal. */
export function installManagedRuntime(subject: ManagedRuntimeInstallSubject): Promise {
- return startOperation(subject, 'managedRuntime.install')
+ return installs.applyReply(subject, () =>
+ callHost(subject, 'managedRuntime.install', {})
+ )
}
/** Resolves once the run has unwound; the subscription shows its outcome. */
@@ -216,12 +71,7 @@ export async function cancelManagedRuntimeInstall(
/** Starts, or joins, the host's removal; it fails, keeping the files, while anything may run them. */
export function removeManagedRuntime(subject: ManagedRuntimeInstallSubject): Promise {
- return startOperation(subject, 'managedRuntime.remove')
-}
-
-export function subscribeManagedRuntimeInstalls(listener: () => void): () => void {
- listeners.add(listener)
- return () => {
- listeners.delete(listener)
- }
+ return installs.applyReply(subject, () =>
+ callHost(subject, 'managedRuntime.remove', {})
+ )
}
diff --git a/src/renderer/src/runtime/revisioned-host-state-store.ts b/src/renderer/src/runtime/revisioned-host-state-store.ts
new file mode 100644
index 00000000000..8df9618d90c
--- /dev/null
+++ b/src/renderer/src/runtime/revisioned-host-state-store.ts
@@ -0,0 +1,181 @@
+import type { RuntimeRpcSubscriptionHandlers } from './runtime-rpc-subscribe'
+
+/**
+ * A host-owned state per subject, pushed by revision. While any surface observes a subject, one
+ * subscription carries the host's state, idle included, so another client's change shows up too;
+ * the last surface leaving ends it.
+ */
+
+export type RevisionedHostSnapshot =
+ | { kind: 'unknown' }
+ /** No answer yet and the last read failed: says nothing about the host's state. */
+ | { kind: 'readFailed' }
+ | { kind: 'known'; state: S }
+
+type Entry = {
+ subject: Subject
+ snapshot: RevisionedHostSnapshot
+ observers: number
+ /** Bumped by every subscribe and every loss; a frame from another generation is stale. */
+ generation: number
+ /** The generation the shown state came from: revisions only order one subscription's frames. */
+ shownGeneration: number
+ stream: { unsubscribe: () => void } | null
+ retry: ReturnType | null
+ failures: number
+}
+
+const UNKNOWN = { kind: 'unknown' } as const
+
+export function createRevisionedHostStateStore(options: {
+ key: (subject: Subject) => string
+ /** Subjects with the same host key and a different key answer for a restarted host. */
+ hostKey: (subject: Subject) => string
+ /** A full state on subscribe, then one per newer revision, until unsubscribed or lost. */
+ subscribe: (
+ subject: Subject,
+ handlers: RuntimeRpcSubscriptionHandlers
+ ) => Promise<{ unsubscribe: () => void }>
+}) {
+ const entries = new Map>()
+ const listeners = new Set<() => void>()
+
+ const setSnapshot = (entry: Entry, snapshot: RevisionedHostSnapshot): void => {
+ entry.snapshot = snapshot
+ listeners.forEach((listener) => listener())
+ }
+
+ const entryFor = (subject: Subject): Entry => {
+ const key = options.key(subject)
+ let entry = entries.get(key)
+ if (!entry) {
+ // A restarted host runtime answers under a new key; the old one's answer is stale.
+ for (const [otherKey, other] of entries) {
+ if (other.observers === 0 && options.hostKey(other.subject) === options.hostKey(subject)) {
+ entries.delete(otherKey)
+ }
+ }
+ entry = {
+ subject,
+ snapshot: UNKNOWN,
+ observers: 0,
+ generation: 0,
+ shownGeneration: 0,
+ stream: null,
+ retry: null,
+ failures: 0
+ }
+ entries.set(key, entry)
+ }
+ return entry
+ }
+
+ const accept = (entry: Entry, state: S, generation: number): void => {
+ // A new subscription may reach a restarted host whose revisions start again at 0.
+ if (
+ entry.snapshot.kind !== 'known' ||
+ generation !== entry.shownGeneration ||
+ entry.snapshot.state.revision < state.revision
+ ) {
+ entry.shownGeneration = generation
+ setSnapshot(entry, { kind: 'known', state })
+ }
+ }
+
+ const isState = (value: unknown): value is S =>
+ typeof value === 'object' &&
+ value !== null &&
+ 'revision' in value &&
+ typeof value.revision === 'number'
+
+ const close = (entry: Entry): void => {
+ entry.generation += 1
+ entry.stream?.unsubscribe()
+ entry.stream = null
+ if (entry.retry) {
+ clearTimeout(entry.retry)
+ entry.retry = null
+ }
+ }
+
+ const open = (entry: Entry): void => {
+ close(entry)
+ const { generation } = entry
+ const current = (): boolean => entry.observers > 0 && entry.generation === generation
+ const lost = (): void => {
+ if (!current()) {
+ return
+ }
+ close(entry)
+ // A known answer stays through a blip; only a first read failing changes what is shown.
+ if (entry.snapshot.kind !== 'known') {
+ setSnapshot(entry, { kind: 'readFailed' })
+ }
+ entry.failures += 1
+ const delay = Math.min(30_000, 1000 * 2 ** (entry.failures - 1))
+ entry.retry = setTimeout(() => open(entry), delay)
+ }
+ void options
+ .subscribe(entry.subject, {
+ onEvent: (result) => {
+ if (current() && isState(result)) {
+ entry.failures = 0
+ accept(entry, result, generation)
+ }
+ },
+ onError: lost,
+ onClose: lost
+ })
+ .then((opened) => {
+ if (current()) {
+ entry.stream = opened
+ } else {
+ opened.unsubscribe()
+ }
+ }, lost)
+ }
+
+ return {
+ read: (subject: Subject): RevisionedHostSnapshot =>
+ entries.get(options.key(subject))?.snapshot ?? UNKNOWN,
+ /** Subscribes while at least one shown surface observes `subject`. Returns the release. */
+ observe: (subject: Subject): (() => void) => {
+ const entry = entryFor(subject)
+ entry.observers += 1
+ if (entry.observers === 1) {
+ entry.failures = 0
+ open(entry)
+ }
+ return () => {
+ entry.observers -= 1
+ if (entry.observers === 0) {
+ close(entry)
+ }
+ }
+ },
+ /** Resubscribes a lost stream at once, e.g. when the host answers again after an outage. */
+ wake: (subject: Subject): void => {
+ const entry = entries.get(options.key(subject))
+ if (entry?.retry) {
+ entry.failures = 0
+ open(entry)
+ }
+ },
+ /** Shows an action's reply, ordered by revision with the subscription's frames. */
+ applyReply: async (subject: Subject, request: () => Promise): Promise => {
+ const entry = entryFor(subject)
+ const { generation } = entry
+ const state = await request()
+ // A reply that crossed a resubscribe is stale; the new subscription's snapshot governs.
+ if (entry.generation === generation) {
+ accept(entry, state, generation)
+ }
+ },
+ subscribe: (listener: () => void): (() => void) => {
+ listeners.add(listener)
+ return () => {
+ listeners.delete(listener)
+ }
+ }
+ }
+}
diff --git a/src/renderer/src/runtime/runtime-antigravity-accounts-client.ts b/src/renderer/src/runtime/runtime-antigravity-accounts-client.ts
index c115770c494..215a3a639d2 100644
--- a/src/renderer/src/runtime/runtime-antigravity-accounts-client.ts
+++ b/src/renderer/src/runtime/runtime-antigravity-accounts-client.ts
@@ -2,9 +2,13 @@ import type {
AntigravityAccountState,
AntigravityAccountTarget
} from '../../../shared/antigravity-account-types'
-import { ANTIGRAVITY_ACCOUNTS_RUNTIME_CAPABILITY } from '../../../shared/account-runtime-capabilities'
+import {
+ ANTIGRAVITY_ACCOUNTS_RUNTIME_CAPABILITY,
+ ANTIGRAVITY_CHAT_SIGN_IN_RUNTIME_CAPABILITY
+} from '../../../shared/account-runtime-capabilities'
import type { RuntimeClientTarget } from '../../../shared/runtime-client-target'
import { assertRuntimeEnvironmentCapability, callRuntimeRpc } from './runtime-rpc-client'
+import { subscribeRuntimeRpc, type RuntimeRpcSubscriptionHandlers } from './runtime-rpc-subscribe'
export async function callAntigravityAccounts(
owner: RuntimeClientTarget,
@@ -26,3 +30,33 @@ export async function callAntigravityAccounts(
{ timeoutMs: 20_000 }
)
}
+
+async function assertChatSignInHost(owner: RuntimeClientTarget): Promise {
+ if (owner.kind === 'environment') {
+ await assertRuntimeEnvironmentCapability(
+ owner.environmentId,
+ ANTIGRAVITY_CHAT_SIGN_IN_RUNTIME_CAPABILITY,
+ 'This execution host does not support Antigravity chat sign-in yet. Update Orca on that host.'
+ )
+ }
+}
+
+export async function callAntigravityChatSignIn(
+ owner: RuntimeClientTarget,
+ action: 'Start' | 'Cancel' | 'Complete',
+ params?: { flowId: string; callbackUrl?: string }
+): Promise {
+ await assertChatSignInHost(owner)
+ return callRuntimeRpc(owner, `accounts.antigravityChatSignIn${action}`, params, {
+ timeoutMs: 20_000
+ })
+}
+
+/** The host's sign-in status: a snapshot, then one frame per newer revision. */
+export async function subscribeAntigravityChatSignIn(
+ owner: RuntimeClientTarget,
+ handlers: RuntimeRpcSubscriptionHandlers
+): Promise<{ unsubscribe: () => void }> {
+ await assertChatSignInHost(owner)
+ return subscribeRuntimeRpc(owner, 'accounts.antigravityChatSignInSubscribe', null, handlers)
+}
diff --git a/src/shared/agent-session-availability.test.ts b/src/shared/agent-session-availability.test.ts
index 2c81374072d..446aeb393c6 100644
--- a/src/shared/agent-session-availability.test.ts
+++ b/src/shared/agent-session-availability.test.ts
@@ -42,4 +42,9 @@ describe("the host's sign-in and CLI verdict", () => {
)
)
})
+ it('sends Antigravity to its sign-in in Settings', () => {
+ expect(
+ agentSessionFailureSentence({ kind: 'notSignedIn' }, 'row', { agentName: 'antigravity' })
+ ).toBe(AGENT_SESSION_FAILURE_COPY.antigravityNotSignedIn)
+ })
})
diff --git a/src/shared/agent-session-availability.ts b/src/shared/agent-session-availability.ts
index 78a072d0c25..e2e7496b54a 100644
--- a/src/shared/agent-session-availability.ts
+++ b/src/shared/agent-session-availability.ts
@@ -36,9 +36,11 @@ export function agentSessionSignInCopyId(provider: string, account?: AgentSessio
? account === 'managed'
? 'codexManagedNotSignedIn'
: 'codexSystemNotSignedIn'
- : signIn?.agent === 'pi'
- ? 'interactiveAgentNotSignedIn'
- : signIn?.loginCommand.length
- ? 'agentCommandNotSignedIn'
- : 'agentNotSignedIn'
+ : signIn?.agent === 'antigravity'
+ ? 'antigravityNotSignedIn'
+ : signIn?.agent === 'pi'
+ ? 'interactiveAgentNotSignedIn'
+ : signIn?.loginCommand.length
+ ? 'agentCommandNotSignedIn'
+ : 'agentNotSignedIn'
}
diff --git a/src/shared/agent-session-failure-copy.ts b/src/shared/agent-session-failure-copy.ts
index 83f80a1f390..bd7523db85c 100644
--- a/src/shared/agent-session-failure-copy.ts
+++ b/src/shared/agent-session-failure-copy.ts
@@ -46,6 +46,8 @@ export const AGENT_SESSION_FAILURE_COPY = {
codexSystemNotSignedIn: "Codex isn't signed in. Run `{{loginCommand}}`.",
codexManagedNotSignedIn:
"This Codex account isn't signed in. Sign in again in Codex Accounts settings.",
+ antigravityNotSignedIn:
+ "Antigravity isn't signed in. Sign in under Settings → Accounts → Antigravity.",
agentCommandNotSignedIn:
'Sign in to {{agent}} with `{{loginCommand}}` on the computer running this chat.',
interactiveAgentNotSignedIn: