| author | |
| committer | |
| log | 9eb4d71681dc7d002082db2f8b4bacf7614412f4 |
| tree | 0390c8fe63f8601100b427b5c469a5aade969fa0 |
| parent | 120fab18d84f1eb993ecc7fa858a8dd212747079 |
| signature | Signed by PGP key B5690EEEBB952194 |
19 files changed, 32 insertions(+), 1095 deletions(-)
apps/mobile/src/Stack.tsx-8| ... | ... | @@ -56,7 +56,6 @@ import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteSc |
| 56 | 56 | import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; |
| 57 | 57 | import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen"; |
| 58 | 58 | import { SettingsProjectGroupingRouteScreen } from "./features/settings/SettingsProjectGroupingRouteScreen"; |
| 59 | import { SettingsProviderSetupRouteScreen } from "./features/settings/SettingsProviderSetupRouteScreen"; | |
| 60 | 59 | import { UsageRouteScreen } from "./features/usage/UsageRouteScreen"; |
| 61 | 60 | import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen"; |
| 62 | 61 | import { ShowcaseCaptureCoordinator } from "./features/showcase/ShowcaseCaptureCoordinator"; |
| ... | ... | @@ -165,13 +164,6 @@ const SettingsContentStack = createNativeStackNavigator({ |
| 165 | 164 | title: "Add Environment", |
| 166 | 165 | }, |
| 167 | 166 | }), |
| 168 | SettingsProviderSetup: createNativeStackScreen({ | |
| 169 | screen: SettingsProviderSetupRouteScreen, | |
| 170 | linking: "providers/:environmentId/:instanceId", | |
| 171 | options: { | |
| 172 | title: "Antigravity", | |
| 173 | }, | |
| 174 | }), | |
| 175 | 167 | SettingsArchive: createNativeStackScreen({ |
| 176 | 168 | screen: ArchivedThreadsRouteScreen, |
| 177 | 169 | linking: "archive", |
apps/mobile/src/features/cloud/ConnectOnboardingRouteScreen.tsx-6| ... | ... | @@ -111,12 +111,6 @@ function ConfiguredConnectOnboardingRouteScreen() { |
| 111 | 111 | <CloudEnvironmentRows |
| 112 | 112 | connectedCloudEnvironments={connectedCloudEnvironments} |
| 113 | 113 | onReconnectEnvironment={onReconnectEnvironment} |
| 114 | onSetupProvider={(params) => | |
| 115 | navigation.navigate("SettingsSheet", { | |
| 116 | screen: "SettingsContent", | |
| 117 | params: { screen: "SettingsProviderSetup", params }, | |
| 118 | }) | |
| 119 | } | |
| 120 | 114 | showHeader={false} |
| 121 | 115 | /> |
| 122 | 116 | ) : ( |
apps/mobile/src/features/connection/CloudEnvironmentRows.tsx-22| ... | ... | @@ -26,8 +26,6 @@ import { cn } from "../../lib/cn"; |
| 26 | 26 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; |
| 27 | 27 | import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types"; |
| 28 | 28 | import { serverEnvironment } from "../../state/server"; |
| 29 | import { ProviderSetupLink } from "../settings/ProviderSetupLink"; | |
| 30 | import type { ProviderSetupRouteParams } from "../settings/SettingsProviderSetupRouteScreen"; | |
| 31 | 29 | import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation"; |
| 32 | 30 | import { hasCloudPublicConfig } from "../cloud/publicConfig"; |
| 33 | 31 | import { ConnectionStatusDot } from "./ConnectionStatusDot"; |
| ... | ... | @@ -36,7 +34,6 @@ import { type RelayEnvironmentView, useConnectionController } from "./useConnect |
| 36 | 34 | interface CloudEnvironmentRowsProps { |
| 37 | 35 | readonly connectedCloudEnvironments: ReadonlyArray<ConnectedEnvironmentSummary>; |
| 38 | 36 | readonly onReconnectEnvironment: (environmentId: EnvironmentId) => void; |
| 39 | readonly onSetupProvider?: (target: ProviderSetupRouteParams) => void; | |
| 40 | 37 | readonly showcaseAvailableEnvironments?: ReadonlyArray<RelayEnvironmentView>; |
| 41 | 38 | readonly showcaseSignedIn?: boolean; |
| 42 | 39 | /** |
| ... | ... | @@ -151,7 +148,6 @@ function CloudEnvironmentRowsContent( |
| 151 | 148 | onDisconnect={() => handleDisconnectCloudEnvironment(environment.environmentId)} |
| 152 | 149 | errorExpanded={expandedErrorId === environment.environmentId} |
| 153 | 150 | onToggleError={() => handleToggleCloudError(environment.environmentId)} |
| 154 | onSetupProvider={props.onSetupProvider} | |
| 155 | 151 | /> |
| 156 | 152 | ))} |
| 157 | 153 | {availableCloudEnvironments.map((environment, index) => ( |
| ... | ... | @@ -215,7 +211,6 @@ function ConnectedCloudEnvironmentRow(props: { |
| 215 | 211 | readonly onConnect: () => void; |
| 216 | 212 | readonly onDisconnect: () => void; |
| 217 | 213 | readonly onToggleError: () => void; |
| 218 | readonly onSetupProvider?: (target: ProviderSetupRouteParams) => void; | |
| 219 | 214 | }) { |
| 220 | 215 | const serverConfig = useAtomValue( |
| 221 | 216 | serverEnvironment.configValueAtom(props.environment.environmentId), |
| ... | ... | @@ -240,23 +235,6 @@ function ConnectedCloudEnvironmentRow(props: { |
| 240 | 235 | onToggleError={props.onToggleError} |
| 241 | 236 | value={props.environment.connectionState !== "available"} |
| 242 | 237 | /> |
| 243 | {props.onSetupProvider | |
| 244 | ? serverConfig?.providers | |
| 245 | .filter((provider) => provider.setup?.canAuthenticate || provider.setup?.canInstall) | |
| 246 | .map((provider) => ( | |
| 247 | <ProviderSetupLink | |
| 248 | key={provider.instanceId} | |
| 249 | provider={provider} | |
| 250 | disabled={props.environment.connectionState !== "connected"} | |
| 251 | onPress={() => | |
| 252 | props.onSetupProvider?.({ | |
| 253 | environmentId: props.environment.environmentId, | |
| 254 | instanceId: provider.instanceId, | |
| 255 | }) | |
| 256 | } | |
| 257 | /> | |
| 258 | )) | |
| 259 | : null} | |
| 260 | 238 | </View> |
| 261 | 239 | ); |
| 262 | 240 | } |
apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx-19| ... | ... | @@ -15,8 +15,6 @@ import { cn } from "../../lib/cn"; |
| 15 | 15 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; |
| 16 | 16 | import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types"; |
| 17 | 17 | import { serverEnvironment } from "../../state/server"; |
| 18 | import { ProviderSetupLink } from "../settings/ProviderSetupLink"; | |
| 19 | import type { ProviderSetupRouteParams } from "../settings/SettingsProviderSetupRouteScreen"; | |
| 20 | 18 | import { ConnectionStatusDot } from "./ConnectionStatusDot"; |
| 21 | 19 | |
| 22 | 20 | function connectionStatusLabel(environment: ConnectedEnvironmentSummary): string | null { |
| ... | ... | @@ -33,7 +31,6 @@ export function ConnectionEnvironmentRow(props: { |
| 33 | 31 | readonly onToggle: () => void; |
| 34 | 32 | readonly onReconnect: (environmentId: EnvironmentId) => void; |
| 35 | 33 | readonly onRemove: (environmentId: EnvironmentId) => void; |
| 36 | readonly onSetupProvider: (target: ProviderSetupRouteParams) => void; | |
| 37 | 34 | readonly onUpdate: ( |
| 38 | 35 | environmentId: EnvironmentId, |
| 39 | 36 | updates: { readonly label: string; readonly displayUrl: string }, |
| ... | ... | @@ -182,22 +179,6 @@ export function ConnectionEnvironmentRow(props: { |
| 182 | 179 | </> |
| 183 | 180 | )} |
| 184 | 181 | |
| 185 | {serverConfig?.providers | |
| 186 | .filter((provider) => provider.setup?.canAuthenticate || provider.setup?.canInstall) | |
| 187 | .map((provider) => ( | |
| 188 | <ProviderSetupLink | |
| 189 | key={provider.instanceId} | |
| 190 | provider={provider} | |
| 191 | disabled={props.environment.connectionState !== "connected"} | |
| 192 | onPress={() => | |
| 193 | props.onSetupProvider({ | |
| 194 | environmentId: props.environment.environmentId, | |
| 195 | instanceId: provider.instanceId, | |
| 196 | }) | |
| 197 | } | |
| 198 | /> | |
| 199 | ))} | |
| 200 | ||
| 201 | 182 | <View className="flex-row justify-end gap-2"> |
| 202 | 183 | {props.environment.isRelayManaged ? null : ( |
| 203 | 184 | <Pressable |
apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx-6| ... | ... | @@ -75,12 +75,6 @@ export function ConnectionsRouteScreen() { |
| 75 | 75 | onReconnect={onReconnectEnvironment} |
| 76 | 76 | onRemove={onRemoveEnvironmentPress} |
| 77 | 77 | onUpdate={onUpdateEnvironment} |
| 78 | onSetupProvider={(params) => | |
| 79 | navigation.navigate("SettingsSheet", { | |
| 80 | screen: "SettingsContent", | |
| 81 | params: { screen: "SettingsProviderSetup", params }, | |
| 82 | }) | |
| 83 | } | |
| 84 | 78 | /> |
| 85 | 79 | </View> |
| 86 | 80 | ))} |
apps/mobile/src/features/settings/ProviderSetupLink.tsx deleted-29| ... | ... | @@ -1,29 +0,0 @@ |
| 1 | import type { ServerProvider } from "@t3tools/contracts"; | |
| 2 | import { Pressable } from "react-native"; | |
| 3 | ||
| 4 | import { AppText as Text } from "../../components/AppText"; | |
| 5 | import { SymbolView } from "../../components/AppSymbol"; | |
| 6 | import { ProviderIcon } from "../../components/ProviderIcon"; | |
| 7 | import { providerNeedsSetup } from "./provider-setup-state"; | |
| 8 | ||
| 9 | export function ProviderSetupLink(props: { | |
| 10 | readonly provider: ServerProvider; | |
| 11 | readonly disabled?: boolean; | |
| 12 | readonly onPress: () => void; | |
| 13 | }) { | |
| 14 | const label = props.provider.displayName ?? props.provider.driver; | |
| 15 | return ( | |
| 16 | <Pressable | |
| 17 | accessibilityRole="button" | |
| 18 | disabled={props.disabled} | |
| 19 | onPress={props.onPress} | |
| 20 | className="min-h-12 flex-row items-center gap-3 px-4 py-3 active:opacity-70 disabled:opacity-40" | |
| 21 | > | |
| 22 | <ProviderIcon provider={props.provider.driver} size={18} /> | |
| 23 | <Text className="min-w-0 flex-1 text-base text-foreground"> | |
| 24 | {providerNeedsSetup(props.provider) ? `Set up ${label}` : `Manage ${label}`} | |
| 25 | </Text> | |
| 26 | <SymbolView name="chevron.right" size={12} tintColorClassName="accent-foreground" /> | |
| 27 | </Pressable> | |
| 28 | ); | |
| 29 | } |
apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx-12| ... | ... | @@ -137,12 +137,6 @@ export function SettingsEnvironmentsRouteScreen() { |
| 137 | 137 | onReconnect={onReconnectEnvironment} |
| 138 | 138 | onRemove={onRemoveEnvironmentPress} |
| 139 | 139 | onUpdate={handleUpdateEnvironment} |
| 140 | onSetupProvider={(params) => | |
| 141 | navigation.navigate("SettingsSheet", { | |
| 142 | screen: "SettingsContent", | |
| 143 | params: { screen: "SettingsProviderSetup", params }, | |
| 144 | }) | |
| 145 | } | |
| 146 | 140 | /> |
| 147 | 141 | </View> |
| 148 | 142 | ))} |
| ... | ... | @@ -170,12 +164,6 @@ export function SettingsEnvironmentsRouteScreen() { |
| 170 | 164 | <CloudEnvironmentRows |
| 171 | 165 | connectedCloudEnvironments={connectedCloudEnvironments} |
| 172 | 166 | onReconnectEnvironment={onReconnectEnvironment} |
| 173 | onSetupProvider={(params) => | |
| 174 | navigation.navigate("SettingsSheet", { | |
| 175 | screen: "SettingsContent", | |
| 176 | params: { screen: "SettingsProviderSetup", params }, | |
| 177 | }) | |
| 178 | } | |
| 179 | 167 | {...(SHOWCASE_ENABLED |
| 180 | 168 | ? { |
| 181 | 169 | showcaseAvailableEnvironments: SHOWCASE_AVAILABLE_CLOUD_ENVIRONMENTS, |
apps/mobile/src/features/settings/SettingsProviderSetupRouteScreen.tsx deleted-574| ... | ... | @@ -1,574 +0,0 @@ |
| 1 | import { useAtomValue } from "@effect/atom-react"; | |
| 2 | import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; | |
| 3 | import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime"; | |
| 4 | import { | |
| 5 | ANTIGRAVITY_AUTH_METHODS, | |
| 6 | AuthOrchestrationOperateScope, | |
| 7 | EnvironmentId, | |
| 8 | ProviderInstanceId, | |
| 9 | } from "@t3tools/contracts"; | |
| 10 | import * as Schema from "effect/Schema"; | |
| 11 | import { AsyncResult } from "effect/unstable/reactivity"; | |
| 12 | import { useCallback, useEffect, useRef, useState } from "react"; | |
| 13 | import { Alert, Platform, Pressable, ScrollView, View } from "react-native"; | |
| 14 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | |
| 15 | ||
| 16 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | |
| 17 | import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; | |
| 18 | import { ProviderIcon } from "../../components/ProviderIcon"; | |
| 19 | import { cn } from "../../lib/cn"; | |
| 20 | import { tryCopyTextWithHaptic } from "../../lib/copyTextWithHaptic"; | |
| 21 | import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; | |
| 22 | import { NativeStackScreenOptions } from "../../native/StackHeader"; | |
| 23 | import { appAtomRegistry } from "../../state/atom-registry"; | |
| 24 | import { useEnvironments } from "../../state/environments"; | |
| 25 | import { useEnvironmentQuery } from "../../state/query"; | |
| 26 | import { serverEnvironment } from "../../state/server"; | |
| 27 | import { environmentSession } from "../../state/session"; | |
| 28 | import { useAtomCommand } from "../../state/use-atom-command"; | |
| 29 | import { | |
| 30 | antigravityEnabledPatch, | |
| 31 | readAntigravityAuthMethod, | |
| 32 | resolveProviderSignInPresentation, | |
| 33 | } from "./provider-setup-state"; | |
| 34 | ||
| 35 | export type ProviderSetupRouteParams = { | |
| 36 | readonly environmentId: EnvironmentId; | |
| 37 | readonly instanceId: ProviderInstanceId; | |
| 38 | }; | |
| 39 | ||
| 40 | const PRIVATE_COMMAND_OPTIONS = { reportFailure: false, reportDefect: false } as const; | |
| 41 | const isEnvironmentId = Schema.is(EnvironmentId); | |
| 42 | const isProviderInstanceId = Schema.is(ProviderInstanceId); | |
| 43 | ||
| 44 | function SetupButton(props: { | |
| 45 | readonly label: string; | |
| 46 | readonly disabled?: boolean; | |
| 47 | readonly primary?: boolean; | |
| 48 | readonly destructive?: boolean; | |
| 49 | readonly onPress: () => void; | |
| 50 | }) { | |
| 51 | return ( | |
| 52 | <Pressable | |
| 53 | accessibilityRole="button" | |
| 54 | disabled={props.disabled} | |
| 55 | onPress={props.onPress} | |
| 56 | className={cn( | |
| 57 | "min-h-11 items-center justify-center border border-white/25 px-4 py-3 active:opacity-70 disabled:opacity-40", | |
| 58 | props.primary && "bg-white", | |
| 59 | )} | |
| 60 | > | |
| 61 | <Text | |
| 62 | className={cn( | |
| 63 | "text-base font-t3-medium text-white", | |
| 64 | props.primary && "text-black", | |
| 65 | props.destructive && "text-red-400", | |
| 66 | )} | |
| 67 | > | |
| 68 | {props.label} | |
| 69 | </Text> | |
| 70 | </Pressable> | |
| 71 | ); | |
| 72 | } | |
| 73 | ||
| 74 | /** The same screen stays inside Settings or the current model picker. */ | |
| 75 | export function SettingsProviderSetupRouteScreen({ | |
| 76 | route, | |
| 77 | }: StaticScreenProps<ProviderSetupRouteParams>) { | |
| 78 | if ( | |
| 79 | !isEnvironmentId(route.params?.environmentId) || | |
| 80 | !isProviderInstanceId(route.params?.instanceId) | |
| 81 | ) { | |
| 82 | return ( | |
| 83 | <View className="flex-1 bg-black p-5"> | |
| 84 | <Text className="text-base text-white">This provider link is not valid.</Text> | |
| 85 | </View> | |
| 86 | ); | |
| 87 | } | |
| 88 | return ( | |
| 89 | <ProviderSetupScreen | |
| 90 | key={`${route.params.environmentId}:${route.params.instanceId}`} | |
| 91 | {...route.params} | |
| 92 | /> | |
| 93 | ); | |
| 94 | } | |
| 95 | ||
| 96 | function ProviderSetupScreen({ environmentId, instanceId }: ProviderSetupRouteParams) { | |
| 97 | const navigation = useNavigation(); | |
| 98 | const insets = useSafeAreaInsets(); | |
| 99 | const { presentationById } = useEnvironments(); | |
| 100 | const environment = presentationById.get(environmentId); | |
| 101 | const environmentLabel = environment?.entry.target.label ?? "this environment"; | |
| 102 | const isConnected = environment?.connection.phase === "connected"; | |
| 103 | const config = useAtomValue(serverEnvironment.configValueAtom(environmentId)); | |
| 104 | const provider = config?.providers.find((item) => item.instanceId === instanceId); | |
| 105 | const access = useEnvironmentQuery(environmentSession.sessionStateAtom(environmentId)); | |
| 106 | const canOperate = | |
| 107 | access.data?.authenticated === true && | |
| 108 | access.data.scopes?.includes(AuthOrchestrationOperateScope) === true; | |
| 109 | const target = { environmentId, input: { instanceId } }; | |
| 110 | const authQuery = useEnvironmentQuery( | |
| 111 | canOperate && provider?.setup?.canAuthenticate | |
| 112 | ? serverEnvironment.providerAuthState(target) | |
| 113 | : null, | |
| 114 | ); | |
| 115 | const installQuery = useEnvironmentQuery( | |
| 116 | canOperate && provider?.setup?.canInstall | |
| 117 | ? serverEnvironment.providerInstallState(target) | |
| 118 | : null, | |
| 119 | ); | |
| 120 | const auth = authQuery.data; | |
| 121 | const installation = installQuery.data; | |
| 122 | const startAuth = useAtomCommand(serverEnvironment.startProviderAuth, PRIVATE_COMMAND_OPTIONS); | |
| 123 | const completeAuth = useAtomCommand( | |
| 124 | serverEnvironment.completeProviderAuth, | |
| 125 | PRIVATE_COMMAND_OPTIONS, | |
| 126 | ); | |
| 127 | const cancelAuth = useAtomCommand(serverEnvironment.cancelProviderAuth, PRIVATE_COMMAND_OPTIONS); | |
| 128 | const logout = useAtomCommand(serverEnvironment.logoutProviderAuth, PRIVATE_COMMAND_OPTIONS); | |
| 129 | const startInstall = useAtomCommand( | |
| 130 | serverEnvironment.startProviderInstall, | |
| 131 | PRIVATE_COMMAND_OPTIONS, | |
| 132 | ); | |
| 133 | const cancelInstall = useAtomCommand( | |
| 134 | serverEnvironment.cancelProviderInstall, | |
| 135 | PRIVATE_COMMAND_OPTIONS, | |
| 136 | ); | |
| 137 | const removeInstall = useAtomCommand( | |
| 138 | serverEnvironment.removeProviderInstallation, | |
| 139 | PRIVATE_COMMAND_OPTIONS, | |
| 140 | ); | |
| 141 | const updateSettings = useAtomCommand(serverEnvironment.updateSettings, PRIVATE_COMMAND_OPTIONS); | |
| 142 | const [callbackUrl, setCallbackUrl] = useState(""); | |
| 143 | const [error, setError] = useState<string | null>(null); | |
| 144 | const [busy, setBusy] = useState(false); | |
| 145 | const busyRef = useRef(false); | |
| 146 | const authActive = | |
| 147 | auth?.phase === "starting" || auth?.phase === "waiting" || auth?.phase === "verifying"; | |
| 148 | const installActive = | |
| 149 | installation?.phase === "downloading" || | |
| 150 | installation?.phase === "extracting" || | |
| 151 | installation?.phase === "verifying"; | |
| 152 | const controlsDisabled = busy || !isConnected || !canOperate; | |
| 153 | const { | |
| 154 | signedIn, | |
| 155 | showSignOut, | |
| 156 | message: authMessage, | |
| 157 | } = resolveProviderSignInPresentation(provider, auth); | |
| 158 | const authMethod = readAntigravityAuthMethod( | |
| 159 | config?.settings.providerInstances[instanceId]?.config ?? | |
| 160 | (instanceId === "antigravity" ? config?.settings.providers.antigravity : undefined), | |
| 161 | ); | |
| 162 | const usesBrowser = authMethod === "oauth-personal" || authMethod === "oauth-business"; | |
| 163 | const methodLabel = | |
| 164 | ANTIGRAVITY_AUTH_METHODS.find((method) => method.value === authMethod)?.label ?? | |
| 165 | "Google account"; | |
| 166 | ||
| 167 | // Return URLs never enter saved drafts, navigation params, or diagnostics. | |
| 168 | useEffect(() => { | |
| 169 | setCallbackUrl(""); | |
| 170 | }, [auth?.flowId, auth?.phase]); | |
| 171 | ||
| 172 | const perform = useCallback( | |
| 173 | async (command: () => Promise<AtomCommandResult<unknown, unknown>>, failureMessage: string) => { | |
| 174 | if (busyRef.current || !isConnected || !canOperate) return false; | |
| 175 | busyRef.current = true; | |
| 176 | setBusy(true); | |
| 177 | setError(null); | |
| 178 | try { | |
| 179 | const result = await command(); | |
| 180 | if (!AsyncResult.isSuccess(result)) { | |
| 181 | setError(failureMessage); | |
| 182 | return false; | |
| 183 | } | |
| 184 | return true; | |
| 185 | } catch { | |
| 186 | setError(failureMessage); | |
| 187 | return false; | |
| 188 | } finally { | |
| 189 | busyRef.current = false; | |
| 190 | setBusy(false); | |
| 191 | } | |
| 192 | }, | |
| 193 | [canOperate, isConnected], | |
| 194 | ); | |
| 195 | ||
| 196 | const setEnabled = (enabled: boolean) => { | |
| 197 | const currentConfig = appAtomRegistry.get(serverEnvironment.configValueAtom(environmentId)); | |
| 198 | const currentProvider = currentConfig?.providers.find((item) => item.instanceId === instanceId); | |
| 199 | if (!currentConfig || !currentProvider) return; | |
| 200 | const patch = antigravityEnabledPatch(currentConfig.settings, currentProvider, enabled); | |
| 201 | if (!patch) return; | |
| 202 | void perform( | |
| 203 | () => updateSettings({ environmentId, input: { patch } }), | |
| 204 | "Could not change this provider. Reconnect and try again.", | |
| 205 | ); | |
| 206 | }; | |
| 207 | ||
| 208 | const title = provider?.displayName ?? "Antigravity"; | |
| 209 | return ( | |
| 210 | <View className="flex-1 bg-black"> | |
| 211 | <NativeStackScreenOptions | |
| 212 | options={{ | |
| 213 | title, | |
| 214 | headerShown: Platform.OS !== "android", | |
| 215 | headerTransparent: false, | |
| 216 | headerStyle: { backgroundColor: "#000" }, | |
| 217 | headerTintColor: "#fff", | |
| 218 | contentStyle: { backgroundColor: "#000" }, | |
| 219 | }} | |
| 220 | /> | |
| 221 | {Platform.OS === "android" ? ( | |
| 222 | <AndroidScreenHeader title={title} onBack={() => navigation.goBack()} /> | |
| 223 | ) : null} | |
| 224 | <ScrollView | |
| 225 | className="flex-1" | |
| 226 | contentInsetAdjustmentBehavior="automatic" | |
| 227 | contentContainerClassName="gap-5 px-5 pt-5" | |
| 228 | contentContainerStyle={{ paddingBottom: insets.bottom + 24 }} | |
| 229 | keyboardShouldPersistTaps="handled" | |
| 230 | showsVerticalScrollIndicator={false} | |
| 231 | > | |
| 232 | <View className="flex-row items-center gap-3"> | |
| 233 | <ProviderIcon provider="antigravity" size={28} /> | |
| 234 | <Text className="min-w-0 flex-1 text-base text-white">{environmentLabel}</Text> | |
| 235 | </View> | |
| 236 | {!isConnected ? ( | |
| 237 | <Text className="text-base text-white"> | |
| 238 | Reconnect to this environment to continue setup. | |
| 239 | </Text> | |
| 240 | ) : access.error ? ( | |
| 241 | <View className="gap-3"> | |
| 242 | <Text className="text-base text-white">Could not check your environment access.</Text> | |
| 243 | <SetupButton label="Retry" onPress={access.refresh} /> | |
| 244 | </View> | |
| 245 | ) : !access.data ? ( | |
| 246 | <Text className="text-base text-white">Checking environment access.</Text> | |
| 247 | ) : !canOperate ? ( | |
| 248 | <Text className="text-base text-white"> | |
| 249 | This connection cannot manage providers. Pair again with permission to operate this | |
| 250 | environment. | |
| 251 | </Text> | |
| 252 | ) : null} | |
| 253 | {!config ? ( | |
| 254 | <Text className="text-base text-white">Loading provider settings.</Text> | |
| 255 | ) : !provider || provider.driver !== "antigravity" ? ( | |
| 256 | <Text className="text-base text-white"> | |
| 257 | This provider is not available on this environment. | |
| 258 | </Text> | |
| 259 | ) : ( | |
| 260 | <> | |
| 261 | <View className="gap-3"> | |
| 262 | <Text className="text-base text-white"> | |
| 263 | {provider.enabled ? "Enabled" : "Disabled"} | |
| 264 | {provider.installed | |
| 265 | ? `. Installed${provider.version ? ` ${provider.version}` : ""}` | |
| 266 | : ". Not installed"} | |
| 267 | </Text> | |
| 268 | <SetupButton | |
| 269 | label={provider.enabled ? "Disable Antigravity" : "Enable Antigravity"} | |
| 270 | disabled={controlsDisabled} | |
| 271 | onPress={() => { | |
| 272 | if (!provider.enabled) { | |
| 273 | setEnabled(true); | |
| 274 | return; | |
| 275 | } | |
| 276 | Alert.alert( | |
| 277 | "Disable Antigravity?", | |
| 278 | `This stops Antigravity sessions on ${environmentLabel}. Google credentials stay saved.`, | |
| 279 | [ | |
| 280 | { text: "Cancel", style: "cancel" }, | |
| 281 | { text: "Disable", style: "destructive", onPress: () => setEnabled(false) }, | |
| 282 | ], | |
| 283 | ); | |
| 284 | }} | |
| 285 | /> | |
| 286 | </View> | |
| 287 | {provider.setup?.canInstall ? ( | |
| 288 | <View className="gap-3 border-t border-white/20 pt-4"> | |
| 289 | {installActive ? ( | |
| 290 | <> | |
| 291 | <Text accessibilityLiveRegion="polite" className="text-base text-white"> | |
| 292 | {installation.phase === "downloading" | |
| 293 | ? `Downloading ${Math.round(installation.downloadedBytes / 1_048_576)}${installation.totalBytes === null ? "" : ` of ${Math.round(installation.totalBytes / 1_048_576)}`} MB` | |
| 294 | : installation.phase === "extracting" | |
| 295 | ? "Extracting Antigravity files." | |
| 296 | : "Checking the Antigravity installation."} | |
| 297 | </Text> | |
| 298 | <SetupButton | |
| 299 | label="Cancel install" | |
| 300 | disabled={controlsDisabled || installation.operationId === null} | |
| 301 | onPress={() => { | |
| 302 | const operationId = installation.operationId; | |
| 303 | if (!operationId) return; | |
| 304 | void perform( | |
| 305 | () => | |
| 306 | cancelInstall({ environmentId, input: { instanceId, operationId } }), | |
| 307 | "Could not cancel the install. Try again.", | |
| 308 | ); | |
| 309 | }} | |
| 310 | /> | |
| 311 | </> | |
| 312 | ) : !provider.installed ? ( | |
| 313 | <SetupButton | |
| 314 | label={ | |
| 315 | installation?.phase === "failed" || installation?.phase === "cancelled" | |
| 316 | ? "Retry install" | |
| 317 | : "Install Antigravity" | |
| 318 | } | |
| 319 | primary | |
| 320 | disabled={controlsDisabled || authActive} | |
| 321 | onPress={() => | |
| 322 | void perform( | |
| 323 | () => startInstall(target), | |
| 324 | "Could not start the install. Try again.", | |
| 325 | ) | |
| 326 | } | |
| 327 | /> | |
| 328 | ) : null} | |
| 329 | {installation?.message ? ( | |
| 330 | <Text className="text-sm text-white">{installation.message}</Text> | |
| 331 | ) : null} | |
| 332 | {installation?.canRemove && !installActive ? ( | |
| 333 | <SetupButton | |
| 334 | label="Remove managed install" | |
| 335 | destructive | |
| 336 | disabled={controlsDisabled || authActive} | |
| 337 | onPress={() => { | |
| 338 | Alert.alert( | |
| 339 | "Remove the Antigravity install?", | |
| 340 | `This removes T3's managed install from ${environmentLabel}. Providers that use it will need it installed again. Google credentials and threads stay.`, | |
| 341 | [ | |
| 342 | { text: "Cancel", style: "cancel" }, | |
| 343 | { | |
| 344 | text: "Remove", | |
| 345 | style: "destructive", | |
| 346 | onPress: () => | |
| 347 | void perform( | |
| 348 | () => removeInstall(target), | |
| 349 | "Could not remove the managed install. Try again.", | |
| 350 | ), | |
| 351 | }, | |
| 352 | ], | |
| 353 | ); | |
| 354 | }} | |
| 355 | /> | |
| 356 | ) : null} | |
| 357 | {installQuery.error ? ( | |
| 358 | <Text className="text-sm text-white"> | |
| 359 | Install status is unavailable. Reconnect and try again. | |
| 360 | </Text> | |
| 361 | ) : null} | |
| 362 | </View> | |
| 363 | ) : !provider.installed ? ( | |
| 364 | <Text className="text-base text-white"> | |
| 365 | Antigravity cannot be installed on this environment. Use a supported server host or | |
| 366 | set an executable path in provider settings. | |
| 367 | </Text> | |
| 368 | ) : null} | |
| 369 | {provider.setup?.canAuthenticate ? ( | |
| 370 | <View className="gap-3 border-t border-white/20 pt-4"> | |
| 371 | <Text className="text-lg font-t3-medium text-white">{methodLabel}</Text> | |
| 372 | <Text className="text-base text-white"> | |
| 373 | {signedIn | |
| 374 | ? "Signed in. Credentials stay on this environment." | |
| 375 | : provider.auth.status === "unknown" | |
| 376 | ? "Sign-in has not been checked." | |
| 377 | : usesBrowser | |
| 378 | ? "Sign in with the Google account you use for Antigravity." | |
| 379 | : "Connect with the credentials set in provider settings on web or desktop."} | |
| 380 | </Text> | |
| 381 | {authActive ? ( | |
| 382 | <> | |
| 383 | <Text accessibilityLiveRegion="polite" className="text-base text-white"> | |
| 384 | {auth.phase === "starting" | |
| 385 | ? usesBrowser | |
| 386 | ? "Starting Google sign-in." | |
| 387 | : "Checking credentials." | |
| 388 | : auth.phase === "verifying" | |
| 389 | ? usesBrowser | |
| 390 | ? "Checking Google sign-in." | |
| 391 | : "Checking credentials." | |
| 392 | : auth.authorizationUrl | |
| 393 | ? "Complete sign-in in your browser." | |
| 394 | : "Sign-in is open on another client. Complete it there or wait for it to expire."} | |
| 395 | </Text> | |
| 396 | {auth.authorizationUrl && auth.phase === "waiting" ? ( | |
| 397 | <> | |
| 398 | <SetupButton | |
| 399 | label="Open Google sign-in" | |
| 400 | primary | |
| 401 | disabled={controlsDisabled} | |
| 402 | onPress={() => { | |
| 403 | if (!auth.authorizationUrl) return; | |
| 404 | void tryOpenExternalUrl(auth.authorizationUrl, "provider-auth").then( | |
| 405 | (opened) => { | |
| 406 | if (!opened) | |
| 407 | setError( | |
| 408 | "Could not open Google sign-in. Copy the link and open it in your browser.", | |
| 409 | ); | |
| 410 | }, | |
| 411 | ); | |
| 412 | }} | |
| 413 | /> | |
| 414 | <SetupButton | |
| 415 | label="Copy sign-in link" | |
| 416 | disabled={controlsDisabled} | |
| 417 | onPress={() => { | |
| 418 | if (!auth.authorizationUrl) return; | |
| 419 | void tryCopyTextWithHaptic(auth.authorizationUrl, { | |
| 420 | target: "provider-sign-in-link", | |
| 421 | }).then((copied) => { | |
| 422 | if (!copied) setError("Could not copy the sign-in link."); | |
| 423 | }); | |
| 424 | }} | |
| 425 | /> | |
| 426 | <Text className="text-sm leading-5 text-white"> | |
| 427 | After sign-in, the browser will open a 127.0.0.1 address that cannot load | |
| 428 | on your phone. Copy that full address and paste it here. | |
| 429 | </Text> | |
| 430 | <TextInput | |
| 431 | accessibilityLabel="Google sign-in return URL" | |
| 432 | autoCapitalize="none" | |
| 433 | autoComplete="off" | |
| 434 | autoCorrect={false} | |
| 435 | className="min-h-12 rounded-none border border-white/25 bg-black px-3 py-3 text-base text-white" | |
| 436 | editable={!controlsDisabled} | |
| 437 | keyboardType="url" | |
| 438 | onChangeText={setCallbackUrl} | |
| 439 | placeholder="http://127.0.0.1:.../" | |
| 440 | textContentType="none" | |
| 441 | value={callbackUrl} | |
| 442 | /> | |
| 443 | <SetupButton | |
| 444 | label="Complete sign-in" | |
| 445 | primary | |
| 446 | disabled={ | |
| 447 | controlsDisabled || callbackUrl.trim().length === 0 || !auth.flowId | |
| 448 | } | |
| 449 | onPress={() => { | |
| 450 | const flowId = auth.flowId; | |
| 451 | if (!flowId) return; | |
| 452 | const submittedUrl = callbackUrl.trim(); | |
| 453 | setCallbackUrl(""); | |
| 454 | void perform( | |
| 455 | () => | |
| 456 | completeAuth({ | |
| 457 | environmentId, | |
| 458 | input: { instanceId, flowId, callbackUrl: submittedUrl }, | |
| 459 | }), | |
| 460 | "Could not complete Google sign-in. Check the return URL and try again.", | |
| 461 | ); | |
| 462 | }} | |
| 463 | /> | |
| 464 | </> | |
| 465 | ) : null} | |
| 466 | {auth.expiresAt ? ( | |
| 467 | <Text className="text-sm text-white"> | |
| 468 | Sign-in expires at{" "} | |
| 469 | {new Date(auth.expiresAt).toLocaleTimeString([], { | |
| 470 | hour: "numeric", | |
| 471 | minute: "2-digit", | |
| 472 | })} | |
| 473 | . | |
| 474 | </Text> | |
| 475 | ) : null} | |
| 476 | {auth.flowId ? ( | |
| 477 | <SetupButton | |
| 478 | label="Cancel sign-in" | |
| 479 | disabled={controlsDisabled} | |
| 480 | onPress={() => { | |
| 481 | const flowId = auth.flowId; | |
| 482 | if (!flowId) return; | |
| 483 | setCallbackUrl(""); | |
| 484 | void perform( | |
| 485 | () => cancelAuth({ environmentId, input: { instanceId, flowId } }), | |
| 486 | "Could not cancel sign-in. Try again.", | |
| 487 | ); | |
| 488 | }} | |
| 489 | /> | |
| 490 | ) : null} | |
| 491 | </> | |
| 492 | ) : showSignOut ? ( | |
| 493 | <SetupButton | |
| 494 | label={usesBrowser ? "Sign out of Google" : "Disconnect"} | |
| 495 | destructive | |
| 496 | disabled={controlsDisabled} | |
| 497 | onPress={() => { | |
| 498 | Alert.alert( | |
| 499 | usesBrowser ? "Sign out of Google?" : "Disconnect Antigravity?", | |
| 500 | `This stops Antigravity sessions for ${title} on ${environmentLabel}. Threads and files stay.`, | |
| 501 | [ | |
| 502 | { text: "Cancel", style: "cancel" }, | |
| 503 | { | |
| 504 | text: "Sign out", | |
| 505 | style: "destructive", | |
| 506 | onPress: () => | |
| 507 | void perform( | |
| 508 | () => logout(target), | |
| 509 | "Could not sign out of Google. Try again.", | |
| 510 | ), | |
| 511 | }, | |
| 512 | ], | |
| 513 | ); | |
| 514 | }} | |
| 515 | /> | |
| 516 | ) : ( | |
| 517 | <SetupButton | |
| 518 | label={ | |
| 519 | usesBrowser | |
| 520 | ? auth?.phase === "failed" || auth?.phase === "cancelled" | |
| 521 | ? "Retry Google sign-in" | |
| 522 | : "Sign in with Google" | |
| 523 | : auth?.phase === "failed" || auth?.phase === "cancelled" | |
| 524 | ? "Retry connection" | |
| 525 | : "Connect" | |
| 526 | } | |
| 527 | primary | |
| 528 | disabled={ | |
| 529 | controlsDisabled || | |
| 530 | !provider.enabled || | |
| 531 | !provider.installed || | |
| 532 | installActive || | |
| 533 | authQuery.isPending | |
| 534 | } | |
| 535 | onPress={() => | |
| 536 | void perform( | |
| 537 | () => startAuth(target), | |
| 538 | "Could not start Google sign-in. Try again.", | |
| 539 | ) | |
| 540 | } | |
| 541 | /> | |
| 542 | )} | |
| 543 | {!provider.enabled && !signedIn ? ( | |
| 544 | <Text className="text-sm text-white">Enable Antigravity to sign in.</Text> | |
| 545 | ) : !provider.installed && !signedIn ? ( | |
| 546 | <Text className="text-sm text-white">Install Antigravity to sign in.</Text> | |
| 547 | ) : null} | |
| 548 | {authMessage ? <Text className="text-sm text-white">{authMessage}</Text> : null} | |
| 549 | {authQuery.error ? ( | |
| 550 | <Text className="text-sm text-white"> | |
| 551 | Sign-in status is unavailable. Reconnect and try again. | |
| 552 | </Text> | |
| 553 | ) : null} | |
| 554 | </View> | |
| 555 | ) : null} | |
| 556 | {provider.message ? ( | |
| 557 | <Text className="text-sm text-white">{provider.message}</Text> | |
| 558 | ) : null} | |
| 559 | </> | |
| 560 | )} | |
| 561 | {error ? ( | |
| 562 | <Text accessibilityRole="alert" className="text-base text-red-400"> | |
| 563 | {error} | |
| 564 | </Text> | |
| 565 | ) : null} | |
| 566 | {busy ? ( | |
| 567 | <Text accessibilityLiveRegion="polite" className="text-sm text-white"> | |
| 568 | Waiting for the environment. | |
| 569 | </Text> | |
| 570 | ) : null} | |
| 571 | </ScrollView> | |
| 572 | </View> | |
| 573 | ); | |
| 574 | } |
apps/mobile/src/features/settings/provider-setup-state.test.ts deleted-159| ... | ... | @@ -1,159 +0,0 @@ |
| 1 | import { | |
| 2 | DEFAULT_SERVER_SETTINGS, | |
| 3 | ProviderDriverKind, | |
| 4 | ProviderInstanceId, | |
| 5 | ServerProvider, | |
| 6 | resolveProviderInstanceEnabled, | |
| 7 | } from "@t3tools/contracts"; | |
| 8 | import * as Schema from "effect/Schema"; | |
| 9 | import { describe, expect, it } from "vite-plus/test"; | |
| 10 | ||
| 11 | import { antigravityEnabledPatch, resolveProviderSignInPresentation } from "./provider-setup-state"; | |
| 12 | ||
| 13 | const provider = Schema.decodeSync(ServerProvider)({ | |
| 14 | instanceId: "antigravity", | |
| 15 | driver: "antigravity", | |
| 16 | enabled: false, | |
| 17 | installed: false, | |
| 18 | version: null, | |
| 19 | status: "disabled", | |
| 20 | auth: { status: "unauthenticated" }, | |
| 21 | checkedAt: "2026-09-02T00:00:00.000Z", | |
| 22 | models: [], | |
| 23 | }); | |
| 24 | ||
| 25 | describe("resolveProviderSignInPresentation", () => { | |
| 26 | const completedFlow = { phase: "succeeded", message: "Google sign-in is complete." } as const; | |
| 27 | ||
| 28 | it("offers sign-in after saved credentials expire, even if the old flow succeeded", () => { | |
| 29 | expect( | |
| 30 | resolveProviderSignInPresentation( | |
| 31 | { | |
| 32 | enabled: true, | |
| 33 | auth: { status: "authenticated" }, | |
| 34 | }, | |
| 35 | completedFlow, | |
| 36 | ), | |
| 37 | ).toEqual({ | |
| 38 | signedIn: true, | |
| 39 | showSignOut: true, | |
| 40 | message: completedFlow.message, | |
| 41 | }); | |
| 42 | ||
| 43 | expect( | |
| 44 | resolveProviderSignInPresentation( | |
| 45 | { | |
| 46 | enabled: true, | |
| 47 | auth: { status: "unauthenticated" }, | |
| 48 | }, | |
| 49 | completedFlow, | |
| 50 | ), | |
| 51 | ).toEqual({ | |
| 52 | signedIn: false, | |
| 53 | showSignOut: false, | |
| 54 | message: null, | |
| 55 | }); | |
| 56 | }); | |
| 57 | ||
| 58 | it("allows credential cleanup without claiming a disabled unknown account is signed in", () => { | |
| 59 | expect( | |
| 60 | resolveProviderSignInPresentation( | |
| 61 | { | |
| 62 | enabled: false, | |
| 63 | auth: { status: "unknown" }, | |
| 64 | }, | |
| 65 | completedFlow, | |
| 66 | ), | |
| 67 | ).toEqual({ | |
| 68 | signedIn: false, | |
| 69 | showSignOut: true, | |
| 70 | message: null, | |
| 71 | }); | |
| 72 | }); | |
| 73 | ||
| 74 | it("keeps progress while Google sign-in is still pending", () => { | |
| 75 | const flow = { phase: "verifying", message: "Checking Google sign-in." } as const; | |
| 76 | expect( | |
| 77 | resolveProviderSignInPresentation( | |
| 78 | { | |
| 79 | enabled: true, | |
| 80 | auth: { status: "unauthenticated" }, | |
| 81 | }, | |
| 82 | flow, | |
| 83 | ), | |
| 84 | ).toEqual({ | |
| 85 | signedIn: false, | |
| 86 | showSignOut: false, | |
| 87 | message: flow.message, | |
| 88 | }); | |
| 89 | }); | |
| 90 | }); | |
| 91 | ||
| 92 | describe("antigravityEnabledPatch", () => { | |
| 93 | it("enables a legacy instance without losing its executable path or models", () => { | |
| 94 | const settings = { | |
| 95 | ...DEFAULT_SERVER_SETTINGS, | |
| 96 | providers: { | |
| 97 | ...DEFAULT_SERVER_SETTINGS.providers, | |
| 98 | antigravity: { | |
| 99 | ...DEFAULT_SERVER_SETTINGS.providers.antigravity, | |
| 100 | enabled: false, | |
| 101 | binaryPath: "/opt/google/agy-acp", | |
| 102 | customModels: ["gemini-native"], | |
| 103 | }, | |
| 104 | }, | |
| 105 | }; | |
| 106 | const patch = antigravityEnabledPatch(settings, provider, true); | |
| 107 | const instance = patch?.providerInstances?.[provider.instanceId]; | |
| 108 | ||
| 109 | expect(instance).toMatchObject({ | |
| 110 | enabled: true, | |
| 111 | config: { binaryPath: "/opt/google/agy-acp", customModels: ["gemini-native"] }, | |
| 112 | }); | |
| 113 | expect(instance?.config).not.toHaveProperty("enabled"); | |
| 114 | expect(instance && resolveProviderInstanceEnabled(instance)).toBe(true); | |
| 115 | expect(settings.providers.antigravity.enabled).toBe(false); | |
| 116 | }); | |
| 117 | ||
| 118 | it("keeps separate accounts and environment overrides when enabling a custom instance", () => { | |
| 119 | const workId = ProviderInstanceId.make("google_work"); | |
| 120 | const personalId = ProviderInstanceId.make("google_personal"); | |
| 121 | const personal = { driver: ProviderDriverKind.make("antigravity"), enabled: true }; | |
| 122 | const settings = { | |
| 123 | ...DEFAULT_SERVER_SETTINGS, | |
| 124 | providerInstances: { | |
| 125 | [personalId]: personal, | |
| 126 | [workId]: { | |
| 127 | driver: ProviderDriverKind.make("antigravity"), | |
| 128 | displayName: "Work Google", | |
| 129 | enabled: false, | |
| 130 | config: { enabled: false, binaryPath: "/work/agy-acp", futureSetting: "keep" }, | |
| 131 | environment: [{ name: "WORK_PROXY", value: "http://proxy", sensitive: false }], | |
| 132 | }, | |
| 133 | }, | |
| 134 | }; | |
| 135 | const patch = antigravityEnabledPatch(settings, { ...provider, instanceId: workId }, true); | |
| 136 | ||
| 137 | expect(patch?.providerInstances?.[personalId]).toBe(personal); | |
| 138 | expect(patch?.providerInstances?.[workId]).toMatchObject({ | |
| 139 | displayName: "Work Google", | |
| 140 | enabled: true, | |
| 141 | config: { binaryPath: "/work/agy-acp", futureSetting: "keep" }, | |
| 142 | environment: [{ name: "WORK_PROXY", value: "http://proxy", sensitive: false }], | |
| 143 | }); | |
| 144 | expect(patch?.providers).toBeUndefined(); | |
| 145 | }); | |
| 146 | ||
| 147 | it("does not change a different driver", () => { | |
| 148 | expect( | |
| 149 | antigravityEnabledPatch( | |
| 150 | DEFAULT_SERVER_SETTINGS, | |
| 151 | { | |
| 152 | ...provider, | |
| 153 | driver: ProviderDriverKind.make("codex"), | |
| 154 | }, | |
| 155 | true, | |
| 156 | ), | |
| 157 | ).toBeNull(); | |
| 158 | }); | |
| 159 | }); |
apps/mobile/src/features/settings/provider-setup-state.ts deleted-76| ... | ... | @@ -1,76 +0,0 @@ |
| 1 | import { | |
| 2 | ANTIGRAVITY_AUTH_METHODS, | |
| 3 | type AntigravityAuthMethod, | |
| 4 | DEFAULT_SERVER_SETTINGS, | |
| 5 | type ProviderAuthState, | |
| 6 | type ServerProvider, | |
| 7 | type ServerSettings, | |
| 8 | type ServerSettingsPatch, | |
| 9 | } from "@t3tools/contracts"; | |
| 10 | ||
| 11 | /** Read the configured method from an instance config. Unknown values fall back to personal. */ | |
| 12 | export function readAntigravityAuthMethod(config: unknown): AntigravityAuthMethod { | |
| 13 | const value = | |
| 14 | config !== null && typeof config === "object" && "authMethod" in config | |
| 15 | ? config.authMethod | |
| 16 | : undefined; | |
| 17 | return ( | |
| 18 | ANTIGRAVITY_AUTH_METHODS.find((method) => method.value === value)?.value ?? "oauth-personal" | |
| 19 | ); | |
| 20 | } | |
| 21 | ||
| 22 | /** A completed sign-in flow does not prove that saved credentials are still valid. */ | |
| 23 | export function resolveProviderSignInPresentation( | |
| 24 | provider: Pick<ServerProvider, "enabled" | "auth"> | undefined, | |
| 25 | flow: Pick<ProviderAuthState, "phase" | "message"> | null, | |
| 26 | ) { | |
| 27 | const signedIn = provider?.auth.status === "authenticated"; | |
| 28 | return { | |
| 29 | signedIn, | |
| 30 | showSignOut: signedIn || (provider?.enabled === false && provider.auth.status === "unknown"), | |
| 31 | message: flow?.phase === "succeeded" && !signedIn ? null : (flow?.message ?? null), | |
| 32 | }; | |
| 33 | } | |
| 34 | ||
| 35 | /** Keep one enabled flag when a legacy provider becomes an explicit instance. */ | |
| 36 | export function antigravityEnabledPatch( | |
| 37 | settings: ServerSettings, | |
| 38 | provider: ServerProvider, | |
| 39 | enabled: boolean, | |
| 40 | ): ServerSettingsPatch | null { | |
| 41 | if (provider.driver !== "antigravity") return null; | |
| 42 | ||
| 43 | const { enabled: _legacyEnabled, ...legacyConfig } = settings.providers.antigravity; | |
| 44 | const instance = settings.providerInstances[provider.instanceId] ?? { | |
| 45 | driver: provider.driver, | |
| 46 | config: legacyConfig, | |
| 47 | }; | |
| 48 | const config = | |
| 49 | instance.config !== null && | |
| 50 | typeof instance.config === "object" && | |
| 51 | !Array.isArray(instance.config) | |
| 52 | ? Object.fromEntries(Object.entries(instance.config).filter(([key]) => key !== "enabled")) | |
| 53 | : instance.config; | |
| 54 | ||
| 55 | return { | |
| 56 | ...(provider.instanceId === "antigravity" | |
| 57 | ? { providers: { antigravity: DEFAULT_SERVER_SETTINGS.providers.antigravity } } | |
| 58 | : {}), | |
| 59 | providerInstances: { | |
| 60 | ...settings.providerInstances, | |
| 61 | [provider.instanceId]: { ...instance, enabled, config }, | |
| 62 | }, | |
| 63 | }; | |
| 64 | } | |
| 65 | ||
| 66 | /** Setup remains available when the provider has no selectable models. */ | |
| 67 | export function providerNeedsSetup(provider: ServerProvider): boolean { | |
| 68 | return ( | |
| 69 | (provider.setup?.canAuthenticate === true || provider.setup?.canInstall === true) && | |
| 70 | (!provider.enabled || | |
| 71 | !provider.installed || | |
| 72 | provider.auth.status !== "authenticated" || | |
| 73 | provider.availability === "unavailable" || | |
| 74 | provider.models.length === 0) | |
| 75 | ); | |
| 76 | } |
apps/mobile/src/features/threads/NewTaskDraftScreen.tsx+1-1| ... | ... | @@ -904,7 +904,7 @@ export function NewTaskDraftScreen(props: { |
| 904 | 904 | ) { |
| 905 | 905 | Alert.alert( |
| 906 | 906 | "Antigravity model unavailable", |
| 907 | "Open model settings to finish setup or choose another model.", | |
| 907 | "Set up Antigravity on web or desktop, or choose another model.", | |
| 908 | 908 | ); |
| 909 | 909 | return; |
| 910 | 910 | } |
apps/mobile/src/features/threads/ThreadSettingsSheet.tsx+6-53| ... | ... | @@ -5,9 +5,7 @@ import type { |
| 5 | 5 | ProviderOptionDescriptor, |
| 6 | 6 | ProviderOptionSelection, |
| 7 | 7 | RuntimeMode, |
| 8 | ServerProvider, | |
| 9 | 8 | } from "@t3tools/contracts"; |
| 10 | import { useAtomValue } from "@effect/atom-react"; | |
| 11 | 9 | import type { LegendListRenderItemProps } from "@legendapp/list/react-native"; |
| 12 | 10 | import { AnimatedLegendList } from "@legendapp/list/reanimated"; |
| 13 | 11 | import { HeaderHeightContext } from "@react-navigation/elements"; |
| ... | ... | @@ -52,11 +50,6 @@ import { |
| 52 | 50 | } from "../../native/StackHeader"; |
| 53 | 51 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "../../native/native-glass"; |
| 54 | 52 | import { serverEnvironment } from "../../state/server"; |
| 55 | import { ProviderSetupLink } from "../settings/ProviderSetupLink"; | |
| 56 | import { | |
| 57 | SettingsProviderSetupRouteScreen, | |
| 58 | type ProviderSetupRouteParams, | |
| 59 | } from "../settings/SettingsProviderSetupRouteScreen"; | |
| 60 | 53 | import { useAtomCommand } from "../../state/use-atom-command"; |
| 61 | 54 | import { useNewTaskFlow } from "./new-task-flow-provider"; |
| 62 | 55 | import { |
| ... | ... | @@ -73,7 +66,6 @@ import { |
| 73 | 66 | canCommitPendingModel, |
| 74 | 67 | modelMatchesCatalogQuery, |
| 75 | 68 | pendingModelAfterPress, |
| 76 | providerSetupCandidates, | |
| 77 | 69 | providerSectionIsCollapsed, |
| 78 | 70 | } from "./thread-settings-sheet-state"; |
| 79 | 71 | |
| ... | ... | @@ -446,7 +438,10 @@ function ThreadSettingsSessionProvider( |
| 446 | 438 | const commitPendingModel = useCallback(() => { |
| 447 | 439 | if (pendingModel) { |
| 448 | 440 | if (!canCommitPendingModel(pendingModel, props.providerGroups)) { |
| 449 | Alert.alert("Model unavailable", "Complete provider setup or select another model."); | |
| 441 | Alert.alert( | |
| 442 | "Model unavailable", | |
| 443 | "Set up this provider on web or desktop, or select another model.", | |
| 444 | ); | |
| 450 | 445 | return false; |
| 451 | 446 | } |
| 452 | 447 | void Haptics.selectionAsync(); |
| ... | ... | @@ -581,11 +576,6 @@ type ThreadSettingsCatalogItem = |
| 581 | 576 | readonly isFirst: boolean; |
| 582 | 577 | readonly isLast: boolean; |
| 583 | 578 | } |
| 584 | | { | |
| 585 | readonly kind: "setup"; | |
| 586 | readonly key: string; | |
| 587 | readonly provider: ServerProvider; | |
| 588 | } | |
| 589 | 579 | | { |
| 590 | 580 | readonly kind: "empty"; |
| 591 | 581 | readonly key: "empty"; |
| ... | ... | @@ -795,37 +785,20 @@ function ThreadSettingsOptionsItem(props: { |
| 795 | 785 | /** One native scroll owner for the model catalog and its related settings. */ |
| 796 | 786 | function ThreadSettingsMainContent(props: { |
| 797 | 787 | readonly onOpenSubmenu: (submenu: ThreadSettingsSubmenuPage) => void; |
| 798 | readonly onOpenProviderSetup: (instanceId: ProviderInstanceId) => void; | |
| 799 | 788 | }) { |
| 800 | 789 | const session = useThreadSettingsSession(); |
| 801 | const config = useAtomValue(serverEnvironment.configValueAtom(session.environmentId)); | |
| 802 | 790 | const catalogItems = useThreadSettingsCatalogItems(session); |
| 803 | 791 | const [animationsReady, setAnimationsReady] = useState(false); |
| 804 | 792 | const nativeHeaderHeight = use(HeaderHeightContext) ?? 0; |
| 805 | 793 | const hasActiveCatalogFilter = |
| 806 | 794 | session.providerFilter !== null || session.searchQuery.trim().length > 0; |
| 807 | 795 | const usesTransparentNativeHeader = Platform.OS === "ios" && NATIVE_LIQUID_GLASS_SUPPORTED; |
| 808 | const setupProviders = useMemo( | |
| 809 | () => | |
| 810 | providerSetupCandidates({ | |
| 811 | providers: config?.providers ?? [], | |
| 812 | instanceId: session.providerInstanceId, | |
| 813 | providerFilter: session.providerFilter, | |
| 814 | query: session.searchQuery, | |
| 815 | }), | |
| 816 | [config?.providers, session.providerInstanceId, session.providerFilter, session.searchQuery], | |
| 817 | ); | |
| 818 | 796 | const listItems = useMemo<ReadonlyArray<ThreadSettingsCatalogItem>>( |
| 819 | 797 | () => [ |
| 820 | 798 | ...(catalogItems.length === 0 ? ([{ kind: "empty", key: "empty" }] as const) : catalogItems), |
| 821 | ...setupProviders.map((provider) => ({ | |
| 822 | kind: "setup" as const, | |
| 823 | key: `setup:${provider.instanceId}`, | |
| 824 | provider, | |
| 825 | })), | |
| 826 | 799 | { kind: "options", key: "options" }, |
| 827 | 800 | ], |
| 828 | [catalogItems, setupProviders], | |
| 801 | [catalogItems], | |
| 829 | 802 | ); |
| 830 | 803 | const renderCatalogItem = useCallback( |
| 831 | 804 | (itemProps: LegendListRenderItemProps<ThreadSettingsCatalogItem>) => { |
| ... | ... | @@ -842,13 +815,6 @@ function ThreadSettingsMainContent(props: { |
| 842 | 815 | option={item.option} |
| 843 | 816 | /> |
| 844 | 817 | ); |
| 845 | } else if (item.kind === "setup") { | |
| 846 | content = ( | |
| 847 | <ProviderSetupLink | |
| 848 | provider={item.provider} | |
| 849 | onPress={() => props.onOpenProviderSetup(item.provider.instanceId)} | |
| 850 | /> | |
| 851 | ); | |
| 852 | 818 | } else if (item.kind === "empty") { |
| 853 | 819 | content = ( |
| 854 | 820 | <View className="items-center px-8 py-14"> |
| ... | ... | @@ -876,7 +842,7 @@ function ThreadSettingsMainContent(props: { |
| 876 | 842 | </Animated.View> |
| 877 | 843 | ); |
| 878 | 844 | }, |
| 879 | [animationsReady, hasActiveCatalogFilter, props.onOpenProviderSetup, props.onOpenSubmenu], | |
| 845 | [animationsReady, hasActiveCatalogFilter, props.onOpenSubmenu], | |
| 880 | 846 | ); |
| 881 | 847 | |
| 882 | 848 | return ( |
| ... | ... | @@ -1002,7 +968,6 @@ function ThreadSettingsChoiceContent(props: { |
| 1002 | 968 | type ThreadSettingsPickerStackParams = { |
| 1003 | 969 | ThreadSettingsModels: undefined; |
| 1004 | 970 | ThreadSettingsChoice: ThreadSettingsSubmenuPage & { readonly title: string }; |
| 1005 | ThreadSettingsProviderSetup: ProviderSetupRouteParams; | |
| 1006 | 971 | }; |
| 1007 | 972 | |
| 1008 | 973 | type ThreadSettingsPickerPresentation = { |
| ... | ... | @@ -1146,13 +1111,6 @@ function ThreadSettingsModelsScreen() { |
| 1146 | 1111 | }} |
| 1147 | 1112 | /> |
| 1148 | 1113 | <ThreadSettingsMainContent |
| 1149 | onOpenProviderSetup={(instanceId) => { | |
| 1150 | if (!session.environmentId) return; | |
| 1151 | navigation.navigate("ThreadSettingsProviderSetup", { | |
| 1152 | environmentId: session.environmentId, | |
| 1153 | instanceId, | |
| 1154 | }); | |
| 1155 | }} | |
| 1156 | 1114 | onOpenSubmenu={(submenu) => { |
| 1157 | 1115 | const title = |
| 1158 | 1116 | submenu.kind === "runtime" |
| ... | ... | @@ -1287,11 +1245,6 @@ function ThreadSettingsPickerNavigator(props: ThreadSettingsPickerPresentation) |
| 1287 | 1245 | component={ThreadSettingsChoiceScreen} |
| 1288 | 1246 | options={({ route }) => ({ title: route.params.title })} |
| 1289 | 1247 | /> |
| 1290 | <ThreadSettingsPickerStack.Screen | |
| 1291 | name="ThreadSettingsProviderSetup" | |
| 1292 | component={SettingsProviderSetupRouteScreen} | |
| 1293 | options={{ title: "Antigravity" }} | |
| 1294 | /> | |
| 1295 | 1248 | </ThreadSettingsPickerStack.Navigator> |
| 1296 | 1249 | </ThreadSettingsPickerPresentationContext.Provider> |
| 1297 | 1250 | ); |
apps/mobile/src/features/threads/thread-settings-sheet-state.test.ts+1-81| ... | ... | @@ -1,18 +1,12 @@ |
| 1 | 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | 2 | |
| 3 | import { | |
| 4 | ProviderInstanceId, | |
| 5 | ServerProvider, | |
| 6 | type ProviderOptionSelection, | |
| 7 | } from "@t3tools/contracts"; | |
| 8 | import * as Schema from "effect/Schema"; | |
| 3 | import { ProviderInstanceId, type ProviderOptionSelection } from "@t3tools/contracts"; | |
| 9 | 4 | |
| 10 | 5 | import type { ModelOption } from "../../lib/modelOptions"; |
| 11 | 6 | import { |
| 12 | 7 | canCommitPendingModel, |
| 13 | 8 | modelMatchesCatalogQuery, |
| 14 | 9 | pendingModelAfterPress, |
| 15 | providerSetupCandidates, | |
| 16 | 10 | } from "./thread-settings-sheet-state"; |
| 17 | 11 | |
| 18 | 12 | function modelOption( |
| ... | ... | @@ -123,77 +117,3 @@ describe("thread settings sheet state", () => { |
| 123 | 117 | ).toBe(false); |
| 124 | 118 | }); |
| 125 | 119 | }); |
| 126 | ||
| 127 | const decodeServerProvider = Schema.decodeSync(ServerProvider); | |
| 128 | ||
| 129 | function setupProvider(overrides: Partial<ServerProvider> = {}): ServerProvider { | |
| 130 | return decodeServerProvider({ | |
| 131 | instanceId: "antigravity", | |
| 132 | driver: "antigravity", | |
| 133 | displayName: "Antigravity", | |
| 134 | enabled: false, | |
| 135 | installed: false, | |
| 136 | version: null, | |
| 137 | status: "disabled", | |
| 138 | auth: { status: "unauthenticated" }, | |
| 139 | checkedAt: "2026-09-02T00:00:00.000Z", | |
| 140 | setup: { canAuthenticate: true, canInstall: true }, | |
| 141 | models: [], | |
| 142 | ...overrides, | |
| 143 | }); | |
| 144 | } | |
| 145 | ||
| 146 | describe("providerSetupCandidates", () => { | |
| 147 | const unfiltered = { providerFilter: null, query: "" }; | |
| 148 | ||
| 149 | it("offers setup without a selectable model and after sign-out", () => { | |
| 150 | const disabled = setupProvider(); | |
| 151 | const signedOut = setupProvider({ enabled: true, installed: true }); | |
| 152 | ||
| 153 | expect(providerSetupCandidates({ providers: [disabled], ...unfiltered })).toEqual([disabled]); | |
| 154 | expect(providerSetupCandidates({ providers: [signedOut], ...unfiltered })).toEqual([signedOut]); | |
| 155 | }); | |
| 156 | ||
| 157 | it("uses the selected environment's status for identical instance IDs", () => { | |
| 158 | const offlineAccount = setupProvider(); | |
| 159 | const readyAccount = setupProvider({ | |
| 160 | enabled: true, | |
| 161 | installed: true, | |
| 162 | auth: { status: "authenticated" }, | |
| 163 | models: [{ slug: "gemini-native", name: "Gemini", isCustom: false, capabilities: null }], | |
| 164 | }); | |
| 165 | ||
| 166 | expect(providerSetupCandidates({ providers: [offlineAccount], ...unfiltered })).toHaveLength(1); | |
| 167 | expect(providerSetupCandidates({ providers: [readyAccount], ...unfiltered })).toEqual([]); | |
| 168 | }); | |
| 169 | ||
| 170 | it("limits existing threads to their provider and respects search", () => { | |
| 171 | const personal = setupProvider(); | |
| 172 | const work = setupProvider({ | |
| 173 | instanceId: ProviderInstanceId.make("google_work"), | |
| 174 | displayName: "Work Google", | |
| 175 | }); | |
| 176 | ||
| 177 | expect( | |
| 178 | providerSetupCandidates({ | |
| 179 | providers: [personal, work], | |
| 180 | ...unfiltered, | |
| 181 | instanceId: work.instanceId, | |
| 182 | }), | |
| 183 | ).toEqual([work]); | |
| 184 | expect( | |
| 185 | providerSetupCandidates({ | |
| 186 | providers: [personal, work], | |
| 187 | providerFilter: work.instanceId, | |
| 188 | query: "work", | |
| 189 | }), | |
| 190 | ).toEqual([work]); | |
| 191 | expect( | |
| 192 | providerSetupCandidates({ | |
| 193 | providers: [personal, work], | |
| 194 | providerFilter: null, | |
| 195 | query: "no-match", | |
| 196 | }), | |
| 197 | ).toEqual([]); | |
| 198 | }); | |
| 199 | }); |
apps/mobile/src/features/threads/thread-settings-sheet-state.ts+1-23| ... | ... | @@ -1,26 +1,4 @@ |
| 1 | import type { ProviderInstanceId, ServerProvider } from "@t3tools/contracts"; | |
| 2 | 1 | import type { ModelOption, ProviderGroup } from "../../lib/modelOptions"; |
| 3 | import { providerNeedsSetup } from "../settings/provider-setup-state"; | |
| 4 | ||
| 5 | /** Read setup choices from this environment, not the selectable model list. */ | |
| 6 | export function providerSetupCandidates(input: { | |
| 7 | readonly providers: ReadonlyArray<ServerProvider>; | |
| 8 | readonly instanceId?: ProviderInstanceId; | |
| 9 | readonly providerFilter: string | null; | |
| 10 | readonly query: string; | |
| 11 | }): ReadonlyArray<ServerProvider> { | |
| 12 | const query = input.query.trim().toLocaleLowerCase(); | |
| 13 | return input.providers.filter( | |
| 14 | (provider) => | |
| 15 | providerNeedsSetup(provider) && | |
| 16 | (input.instanceId === undefined || provider.instanceId === input.instanceId) && | |
| 17 | (input.providerFilter === null || provider.instanceId === input.providerFilter) && | |
| 18 | (query.length === 0 || | |
| 19 | [provider.displayName ?? "", provider.driver, provider.instanceId].some((label) => | |
| 20 | label.toLocaleLowerCase().includes(query), | |
| 21 | )), | |
| 22 | ); | |
| 23 | } | |
| 24 | 2 | |
| 25 | 3 | /** Match the terms a user can actually see or recognize in the model picker. */ |
| 26 | 4 | export function modelMatchesCatalogQuery(input: { |
| ... | ... | @@ -53,7 +31,7 @@ export function pendingModelAfterPress(input: { |
| 53 | 31 | return input.current?.key === input.pressed.key ? input.current : input.pressed; |
| 54 | 32 | } |
| 55 | 33 | |
| 56 | /** A model can disappear while its setup page is open inside the picker. */ | |
| 34 | /** A model can disappear while the picker is open. */ | |
| 57 | 35 | export function canCommitPendingModel( |
| 58 | 36 | pending: ModelOption, |
| 59 | 37 | groups: ReadonlyArray<ProviderGroup>, |
apps/mobile/src/features/threads/use-project-actions.ts+1-1| ... | ... | @@ -118,7 +118,7 @@ export function useCreateProjectThread() { |
| 118 | 118 | const providerError = !serverConfig |
| 119 | 119 | ? "Provider settings are still loading. Try again." |
| 120 | 120 | : isModelSelectionUnavailable(serverConfig, input.modelSelection) |
| 121 | ? "Antigravity model unavailable. Open model settings to finish setup or choose another model." | |
| 121 | ? "Antigravity model unavailable. Set it up on web or desktop, or choose another model." | |
| 122 | 122 | : null; |
| 123 | 123 | if (providerError !== null) { |
| 124 | 124 | setPendingConnectionError(providerError); |
apps/mobile/src/state/use-thread-composer-state.ts+1-1| ... | ... | @@ -276,7 +276,7 @@ export function useThreadComposerState() { |
| 276 | 276 | ) { |
| 277 | 277 | Alert.alert( |
| 278 | 278 | "Antigravity model unavailable", |
| 279 | "Open model settings to finish setup or choose another model.", | |
| 279 | "Set up Antigravity on web or desktop, or choose another model.", | |
| 280 | 280 | ); |
| 281 | 281 | return null; |
| 282 | 282 | } |
apps/mobile/src/state/use-thread-outbox-drain.ts+4-4| ... | ... | @@ -636,7 +636,7 @@ export function useThreadOutboxDrain(): void { |
| 636 | 636 | if (isModelSelectionUnavailable(serverConfig, settings.modelSelection)) { |
| 637 | 637 | return restoreQueuedMessage( |
| 638 | 638 | queuedMessage, |
| 639 | "Antigravity model unavailable. Open model settings to finish setup or choose another model.", | |
| 639 | "Antigravity model unavailable. Set it up on web or desktop, or choose another model.", | |
| 640 | 640 | ); |
| 641 | 641 | } |
| 642 | 642 | const { reportFailure } = makeDeliveryHelpers(queuedMessage); |
| ... | ... | @@ -729,7 +729,7 @@ export function useThreadOutboxDrain(): void { |
| 729 | 729 | if (isModelSelectionUnavailable(currentConfig, settings.modelSelection)) { |
| 730 | 730 | return restoreQueuedMessage( |
| 731 | 731 | persistedMessage, |
| 732 | "Antigravity model unavailable. Open model settings to finish setup or choose another model.", | |
| 732 | "Antigravity model unavailable. Set it up on web or desktop, or choose another model.", | |
| 733 | 733 | ); |
| 734 | 734 | } |
| 735 | 735 | const sendSettings = resolveQueuedThreadSettings( |
| ... | ... | @@ -811,7 +811,7 @@ export function useThreadOutboxDrain(): void { |
| 811 | 811 | if (isModelSelectionUnavailable(serverConfig, settings.modelSelection)) { |
| 812 | 812 | return restoreQueuedMessage( |
| 813 | 813 | queuedMessage, |
| 814 | "Antigravity model unavailable. Open model settings to finish setup or choose another model.", | |
| 814 | "Antigravity model unavailable. Set it up on web or desktop, or choose another model.", | |
| 815 | 815 | ); |
| 816 | 816 | } |
| 817 | 817 | let prepared: PreparedTurnAttachments; |
| ... | ... | @@ -855,7 +855,7 @@ export function useThreadOutboxDrain(): void { |
| 855 | 855 | if (isModelSelectionUnavailable(currentConfig, settings.modelSelection)) { |
| 856 | 856 | return restoreQueuedMessage( |
| 857 | 857 | persistedMessage, |
| 858 | "Antigravity model unavailable. Open model settings to finish setup or choose another model.", | |
| 858 | "Antigravity model unavailable. Set it up on web or desktop, or choose another model.", | |
| 859 | 859 | ); |
| 860 | 860 | } |
| 861 | 861 | const sendSettings = resolveQueuedThreadSettings( |
docs/user/providers-antigravity.md+12-14| ... | ... | @@ -12,16 +12,13 @@ not sign in this T3 Code provider. |
| 12 | 12 | ## Set up Antigravity |
| 13 | 13 | |
| 14 | 14 | On web or desktop, open **Settings** > **Providers**, select the device that runs your project, |
| 15 | then select **Antigravity**. On mobile, open **Settings** > **Environments**, expand the | |
| 16 | environment, then choose **Set up Antigravity**. An existing setup shows **Manage Antigravity**. | |
| 17 | The model picker can open setup for the selected environment too. | |
| 15 | then select **Antigravity**. Provider setup is not available in the mobile app. | |
| 18 | 16 | |
| 19 | 17 | 1. Choose **Enable Antigravity**. Antigravity is off by default. |
| 20 | 18 | 2. Choose **Install Antigravity**. T3 Code downloads the official runtime from Google to that |
| 21 | 19 | environment. Installation continues if you leave the page or reconnect. |
| 22 | 20 | 3. Choose **Sign in with Google**. |
| 23 | 4. Choose **Open sign-in page** on web or desktop, or **Open Google sign-in** on mobile. You can | |
| 24 | use **Copy sign-in link** to open it in another browser. | |
| 21 | 4. Choose **Open sign-in page**. You can use **Copy sign-in link** to open it in another browser. | |
| 25 | 22 | 5. Complete Google sign-in. Use the account you use for Antigravity. |
| 26 | 23 | 6. Wait for T3 Code to confirm sign-in and load the model choices. Select an Antigravity model |
| 27 | 24 | in the thread's model picker. |
| ... | ... | @@ -36,8 +33,8 @@ machine as the T3 Code environment, sign-in can finish directly. |
| 36 | 33 | |
| 37 | 34 | On a phone or another computer, the final page will usually fail to load. This is expected. |
| 38 | 35 | Copy the full address from the browser, including everything after `?`, and paste it into the |
| 39 | return URL field in T3 Code. Choose **Continue** on web or desktop, or **Complete sign-in** on | |
| 40 | mobile. Do not change the address to your server's hostname. | |
| 36 | return URL field in the web or desktop client where you started setup. Choose **Continue**. Do not | |
| 37 | change the address to your server's hostname. | |
| 41 | 38 | |
| 42 | 39 | Return to the same T3 Code client and environment where you started sign-in. Another client |
| 43 | 40 | can see that sign-in is in progress, but cannot complete that attempt. The setup screen shows |
| ... | ... | @@ -50,7 +47,7 @@ callback page alone does not prove account access. |
| 50 | 47 | ### Other sign-in methods |
| 51 | 48 | |
| 52 | 49 | **Sign-in method** in the Antigravity provider settings on web or desktop selects how the |
| 53 | agent authenticates. Mobile shows the selected method and its connect controls. | |
| 50 | agent authenticates. | |
| 54 | 51 | |
| 55 | 52 | | Method | What you enter | How it signs in | |
| 56 | 53 | | -------------------------- | ---------------------------------------- | ------------------------------- | |
| ... | ... | @@ -159,11 +156,11 @@ Each Antigravity provider instance has its own Google sign-in on its environment |
| 159 | 156 | **Add provider** in web or desktop provider settings to create a separate instance for another |
| 160 | 157 | account. To replace an instance's account, sign out first, then sign in again. |
| 161 | 158 | |
| 162 | | Action | Result | | |
| 163 | | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | |
| 164 | | Disable Antigravity | Stops the instance's sessions. Keeps Google sign-in, thread history, and files. | | |
| 165 | | Sign out of Google | Stops the instance's sessions and removes its saved Google credentials. Keeps thread history and files. | | |
| 166 | | Remove downloaded runtime on web or desktop, or Remove managed install on mobile | Removes the runtime shared by Antigravity instances on that environment. Keeps Google credentials, thread history, and files. | | |
| 159 | | Action | Result | | |
| 160 | | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | | |
| 161 | | Disable Antigravity | Stops the instance's sessions. Keeps Google sign-in, thread history, and files. | | |
| 162 | | Sign out of Google | Stops the instance's sessions and removes its saved Google credentials. Keeps thread history and files. | | |
| 163 | | Remove downloaded runtime | Removes the runtime shared by Antigravity instances on that environment. Keeps Google credentials, thread history, and files. | | |
| 167 | 164 | |
| 168 | 165 | Send `/logout` by itself in an Antigravity thread to sign out that provider instance. This has |
| 169 | 166 | the same effect as **Sign out of Google**, including stopping its other sessions. |
| ... | ... | @@ -186,7 +183,8 @@ To check account access and reload models on web or desktop, open **Settings** > |
| 186 | 183 | and select the circular arrow beside **Checked** at the top of the page. Its tooltip says |
| 187 | 184 | **Refresh provider status**. On mobile, use **Refresh models** in the model picker. |
| 188 | 185 | Refresh uses saved Google sign-in and does not open a login page. If sign-in is required, |
| 189 | use the provider's setup controls. Automatic status checks verify the installation only. | |
| 186 | use the provider's setup controls on web or desktop. Automatic status checks verify the | |
| 187 | installation only. | |
| 190 | 188 | |
| 191 | 189 | The packaged runtime can be slow to start, especially on Windows. Health checks, model refresh, |
| 192 | 190 | and sign-out each allow up to 90 seconds before reporting a timeout. |
docs/user/remote-access.md+5-6| ... | ... | @@ -206,15 +206,14 @@ If reconnecting after an app update fails, retry the SSH launch once. The launch |
| 206 | 206 | ## Antigravity Google sign-in |
| 207 | 207 | |
| 208 | 208 | Antigravity runs and saves its Google credentials on the selected environment. You can install |
| 209 | it and sign in from a remote web, desktop, or mobile client without an SSH login. | |
| 209 | it and sign in from a remote web or desktop client without an SSH login. | |
| 210 | 210 | |
| 211 | Start in **Settings** > **Providers** on web or desktop. On mobile, open **Settings** > | |
| 212 | **Environments**, expand the environment, then choose **Set up Antigravity**. | |
| 211 | Start in **Settings** > **Providers**, select the environment, then choose **Antigravity**. | |
| 212 | Provider setup is not available in the mobile app. | |
| 213 | 213 | |
| 214 | 214 | After Google sign-in, a remote browser usually reaches a `127.0.0.1` page that cannot load. |
| 215 | Copy that full address into the return URL field in the same T3 Code client. Choose | |
| 216 | **Continue** on web or desktop, or **Complete sign-in** on mobile. Keep the address unchanged. | |
| 217 | Do not paste the return URL into a thread or bug report. | |
| 215 | Copy that full address into the return URL field in the same T3 Code client and choose | |
| 216 | **Continue**. Keep the address unchanged. Do not paste the return URL into a thread or bug report. | |
| 218 | 217 | |
| 219 | 218 | See [Antigravity setup](./providers-antigravity.md) for installation, expiry, and account changes. |
| 220 | 219 |