feat(onboarding): install Antigravity in an explicit setup terminal (#39172)

* feat(onboarding): install Antigravity in an explicit setup terminal

* fix(onboarding): address review on Antigravity installer dialog
This commit is contained in:
Neil
2026-11-06 13:30:03 -08:00
committed by GitHub
parent 77214beaae
commit 88fa7ed2a9
7 changed files with 295 additions and 17 deletions
@@ -11,6 +11,15 @@ import { i18n, translate } from '@/i18n/i18n'
import { openAppHttpLink } from '@/lib/browser/navigation/app-http-link-open'
import { useScrollEdgeFades } from '@/lib/use-scroll-edge-fades'
import { cn } from '@/lib/utils'
import { getLocalAgentPreflightContext } from '@/lib/local-preflight-context'
import { getRendererAppPlatform } from '@/lib/renderer-app-platform'
import { resolveTerminalPtyOwner } from '@/lib/terminal-worktree-route'
import { useAppStore } from '@/store'
import {
ANTIGRAVITY_INSTALL_TERMINAL_WORKTREE_ID,
resolveAntigravityInstallTarget,
type AntigravityInstallTarget
} from './antigravity-install-target'
import type { AgentSurface } from './use-onboarding-flow-types'
import { AgentPickerButton, AgentSectionHeader } from './AgentPickerButton'
import { YoloPermissionsControl } from './YoloPermissionsControl'
@@ -41,6 +50,7 @@ type AgentStepProps = {
nativeChatProviders?: GlobalSettings['nativeChatProviders']
yoloPermissions?: boolean
onYoloPermissionsChange?: (enabled: boolean) => void
onInstallAntigravity?: (target: AntigravityInstallTarget) => void
}
export function AgentStep({
@@ -55,9 +65,25 @@ export function AgentStep({
chatDetected = null,
nativeChatProviders,
yoloPermissions = true,
onYoloPermissionsChange
onYoloPermissionsChange,
onInstallAntigravity
}: AgentStepProps) {
const agentCatalog = getAgentCatalog()
// Offer the installer only where its terminal spawns on this computer, the host detection reads.
const installerSpawnsLocally = useAppStore(
(state) =>
resolveTerminalPtyOwner(state, ANTIGRAVITY_INSTALL_TERMINAL_WORKTREE_ID, 'spawn').kind ===
'non-runtime'
)
// Chat never runs in WSL, so Chat UI installs natively; Terminal follows agent detection.
const detectionContext = useAppStore((state) =>
agentSurface === 'chat' ? undefined : getLocalAgentPreflightContext(state)
)
const antigravityInstallTarget = resolveAntigravityInstallTarget(
getRendererAppPlatform(),
detectionContext,
installerSpawnsLocally
)
const gridFades = useScrollEdgeFades<HTMLElement>()
const chatMode = agentSurface === 'chat'
// Chat runs only on native installs; an agent found only in WSL opens in a terminal.
@@ -175,16 +201,28 @@ export function AgentStep({
components={{ agent: <span className="font-medium text-foreground" /> }}
/>
</span>
<Button
type="button"
variant="outline"
size="xs"
className="shrink-0"
onClick={(event) => openAppHttpLink(selectedEntry.homepageUrl, { event })}
>
{translate('auto.components.onboarding.AgentStep.9c163bb0e0', 'Install instructions')}
<ExternalLink className="size-3" />
</Button>
<div className="flex shrink-0 items-center gap-2">
{selectedEntry.id === 'antigravity' &&
antigravityInstallTarget &&
onInstallAntigravity && (
<Button
type="button"
size="xs"
onClick={() => onInstallAntigravity(antigravityInstallTarget)}
>
{translate('components.onboarding.agyInstall.open', 'Install')}
</Button>
)}
<Button
type="button"
variant="outline"
size="xs"
onClick={(event) => openAppHttpLink(selectedEntry.homepageUrl, { event })}
>
{translate('auto.components.onboarding.AgentStep.9c163bb0e0', 'Install instructions')}
<ExternalLink className="size-3" />
</Button>
</div>
</div>
)}
<section
@@ -0,0 +1,107 @@
// COMPAT(wsl-bridge): removed when WSL runs as an orcad host.
import { useCallback, useState } from 'react'
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import { translate } from '@/i18n/i18n'
import { buildSkillSetupTerminalCommand } from '@/lib/skill-runtime-command'
import { OnboardingInlineCommandTerminal } from './OnboardingInlineCommandTerminal'
import {
ANTIGRAVITY_INSTALL_TERMINAL_WORKTREE_ID,
type AntigravityInstallTarget
} from './antigravity-install-target'
/** Blocks onboarding navigation while open, so leaving the step can't close a running installer. */
export function AntigravityInstallDialog({
target,
detected,
onRecheck,
onClose
}: {
target: AntigravityInstallTarget
detected: boolean
onRecheck: () => Promise<unknown>
onClose: () => void
}): React.JSX.Element {
const [rechecking, setRechecking] = useState(false)
const prepareCommand = useCallback(
(command: string, shell: string | undefined) =>
buildSkillSetupTerminalCommand(command, shell, target.runtime, target.platform),
[target]
)
const recheck = useCallback(() => {
setRechecking(true)
void onRecheck().finally(() => setRechecking(false))
}, [onRecheck])
const terminalLabel = translate(
'components.onboarding.agyInstall.terminal',
'Antigravity installer'
)
return (
<Dialog open>
<DialogContent
className="z-[120] sm:max-w-3xl"
overlayClassName="z-[110]"
showCloseButton={false}
// Focus lands in the terminal once it's ready, so an early Enter can't hit Close.
onOpenAutoFocus={(event) => event.preventDefault()}
onEscapeKeyDown={(event) => event.preventDefault()}
onInteractOutside={(event) => event.preventDefault()}
>
<DialogHeader>
<DialogTitle>
{translate('components.onboarding.agyInstall.title', 'Install Antigravity')}
</DialogTitle>
<DialogDescription>
{translate(
'components.onboarding.agyInstall.description',
'Press Enter in the terminal to run the official installer. Closing this terminal stops any command still running.'
)}
</DialogDescription>
</DialogHeader>
<p className="text-xs text-muted-foreground">
{translate('components.onboarding.agyInstall.target', 'Install location: {{location}}', {
location:
target.runtime.label ||
translate('components.onboarding.agyInstall.thisDevice', 'This device')
})}
</p>
<OnboardingInlineCommandTerminal
command={target.command}
prepareCommandForShell={prepareCommand}
shellOverride={target.shellOverride}
forceHostRuntime
title={terminalLabel}
ariaLabel={terminalLabel}
worktreeId={ANTIGRAVITY_INSTALL_TERMINAL_WORKTREE_ID}
terminalTopMarginPx={0}
autoScrollIntoView={false}
onCommandFinished={recheck}
onTerminalExit={onClose}
/>
{detected && (
<p role="status" className="text-sm text-foreground">
{translate(
'components.onboarding.agyInstall.detected',
'Antigravity is detected on PATH. Close the terminal when the installer has finished.'
)}
</p>
)}
<DialogFooter>
<Button variant="ghost" onClick={onClose}>
{translate('components.onboarding.agyInstall.close', 'Close terminal')}
</Button>
<Button disabled={rechecking} onClick={recheck}>
{translate('components.onboarding.agyInstall.recheck', 'Re-check agents')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}
@@ -6,6 +6,8 @@ import { getScreenSubmitModifierLabel, isScreenSubmitShortcut } from '@/lib/scre
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
import type { OnboardingState } from '../../../../shared/onboarding-state-types'
import { AgentStep } from './AgentStep'
import { AntigravityInstallDialog } from './AntigravityInstallDialog'
import type { AntigravityInstallTarget } from './antigravity-install-target'
import { ThemeStep } from './ThemeStep'
import { NotificationStep } from './NotificationStep'
import { IntegrationsStep } from './IntegrationsStep'
@@ -47,18 +49,22 @@ export default function OnboardingFlow({
: translate('components.onboarding.flow.actions.continue', 'Continue'))
const [languagePickerOpen, setLanguagePickerOpen] = useState(false)
const [skipConfirmOpen, setSkipConfirmOpen] = useState(false)
const [antigravityInstall, setAntigravityInstall] = useState<AntigravityInstallTarget | null>(
null
)
const installOpen = antigravityInstall !== null
const skipConfirmAdvancedViaRef = useRef<'button' | 'keyboard'>('button')
const { next: flowNext, dismissOnboarding: flowDismissOnboarding } = flow
const requestSkipConfirmation = useCallback(
(advancedVia: 'button' | 'keyboard') => {
if (busyLabel || skipConfirmOpen || languagePickerOpen) {
if (busyLabel || skipConfirmOpen || languagePickerOpen || installOpen) {
return
}
skipConfirmAdvancedViaRef.current = advancedVia
setSkipConfirmOpen(true)
},
[busyLabel, skipConfirmOpen, languagePickerOpen]
[busyLabel, skipConfirmOpen, languagePickerOpen, installOpen]
)
const confirmSkipOnboarding = useCallback(() => {
@@ -70,7 +76,8 @@ export default function OnboardingFlow({
// Depend on stable callbacks/step ID so the changing flow object cannot churn the key listener.
useEffect(() => {
const onKeyDown = (event: KeyboardEvent): void => {
if (languagePickerOpen || skipConfirmOpen) {
// The installer terminal owns the keyboard; leaving the step would close it mid-install.
if (languagePickerOpen || skipConfirmOpen || installOpen) {
return
}
// Do not submit while typing in clone URL or another editable field.
@@ -86,11 +93,11 @@ export default function OnboardingFlow({
}
addAppKeyListener('keydown', onKeyDown)
return () => removeAppKeyListener('keydown', onKeyDown)
}, [flowNext, languagePickerOpen, skipConfirmOpen])
}, [flowNext, languagePickerOpen, skipConfirmOpen, installOpen])
useEffect(() => {
const onKeyDown = (event: KeyboardEvent): void => {
if (event.key !== 'Escape' || skipConfirmOpen || languagePickerOpen) {
if (event.key !== 'Escape' || skipConfirmOpen || languagePickerOpen || installOpen) {
return
}
event.preventDefault()
@@ -98,7 +105,7 @@ export default function OnboardingFlow({
}
addAppKeyListener('keydown', onKeyDown)
return () => removeAppKeyListener('keydown', onKeyDown)
}, [requestSkipConfirmation, skipConfirmOpen, languagePickerOpen])
}, [requestSkipConfirmation, skipConfirmOpen, languagePickerOpen, installOpen])
return (
<TooltipProvider delayDuration={0} skipDelayDuration={0}>
@@ -246,6 +253,7 @@ export default function OnboardingFlow({
isDetecting={flow.isDetectingAgents}
yoloPermissions={flow.yoloPermissions}
onYoloPermissionsChange={flow.setYoloPermissions}
onInstallAntigravity={setAntigravityInstall}
/>
)}
{currentStep.id === 'theme' && (
@@ -284,6 +292,17 @@ export default function OnboardingFlow({
/>
</div>
</section>
{antigravityInstall && (
<AntigravityInstallDialog
target={antigravityInstall}
detected={
flow.detectedSet.has('antigravity') ||
flow.chatDetected?.includes('antigravity') === true
}
onRecheck={flow.refreshDetectedAgents}
onClose={() => setAntigravityInstall(null)}
/>
)}
<OnboardingSkipConfirmationDialog
open={skipConfirmOpen}
onOpenChange={setSkipConfirmOpen}
@@ -0,0 +1,47 @@
import { describe, expect, it } from 'vitest'
import { resolveAntigravityInstallTarget } from './antigravity-install-target'
describe('resolveAntigravityInstallTarget', () => {
it('keeps the user shell on POSIX hosts even with WSL context', () => {
const target = resolveAntigravityInstallTarget('linux', { wslDistro: 'Ubuntu' }, true)
expect(target?.runtime.runtime).toBe('host')
expect(target?.shellOverride).toBeUndefined()
})
it('installs natively through PowerShell on Windows', () => {
expect(resolveAntigravityInstallTarget('win32', undefined, true)).toMatchObject({
shellOverride: 'powershell.exe',
runtime: { runtime: 'host' }
})
})
it('installs into the WSL distro agent detection reads', () => {
expect(
resolveAntigravityInstallTarget('win32', { wslDistro: 'Ubuntu-24.04' }, true)
).toMatchObject({
shellOverride: 'powershell.exe',
runtime: { runtime: 'wsl', wslDistro: 'Ubuntu-24.04' }
})
})
it('offers no install when the WSL runtime needs repair', () => {
expect(
resolveAntigravityInstallTarget(
'win32',
{
projectRuntime: {
status: 'repair-required',
repair: {
projectId: 'folder-project',
preferredRuntime: { kind: 'wsl', distro: null },
reason: 'wsl-distro-required',
source: 'global-default',
cacheKey: 'repair'
}
}
},
true
)
).toBeNull()
})
})
@@ -0,0 +1,54 @@
// COMPAT(wsl-bridge): removed when WSL runs as an orcad host.
import type { LocalPreflightContext } from '@/lib/local-preflight-context'
import { translate } from '@/i18n/i18n'
import { getHostRuntimeLabel, type LocalAgentRuntime } from '@/lib/skill-runtime-command'
import { brandEphemeralSetupTerminalWorktreeId } from '../../../../shared/ephemeral-setup-terminal-worktree-id'
export const ANTIGRAVITY_INSTALL_TERMINAL_WORKTREE_ID = brandEphemeralSetupTerminalWorktreeId(
'onboarding-antigravity-install'
)
// Official commands from https://antigravity.google/docs/getting-started?tab=cli
const POSIX_INSTALL_COMMAND = 'curl -fsSL https://antigravity.google/cli/install.sh | bash'
const POWERSHELL_INSTALL_COMMAND = 'irm https://antigravity.google/cli/install.ps1 | iex'
export type AntigravityInstallTarget = {
platform: NodeJS.Platform
command: string
/** PowerShell on Windows (native, or to reach WSL); POSIX hosts keep the user's own shell. */
shellOverride?: string
runtime: LocalAgentRuntime
}
/** Install where onboarding detects agents, or nowhere when that host isn't this computer. */
export function resolveAntigravityInstallTarget(
platform: NodeJS.Platform,
context: LocalPreflightContext,
hasLocalTerminalAuthority: boolean
): AntigravityInstallTarget | null {
if (!hasLocalTerminalAuthority || context?.projectRuntime?.status === 'repair-required') {
return null
}
const projectRuntime = context?.projectRuntime
const distro =
projectRuntime?.status === 'resolved' && projectRuntime.runtime.kind === 'wsl'
? projectRuntime.runtime.distro
: context?.wslDistro
if (platform !== 'win32') {
return { platform, command: POSIX_INSTALL_COMMAND, runtime: { runtime: 'host', label: '' } }
}
return {
platform,
command: distro ? POSIX_INSTALL_COMMAND : POWERSHELL_INSTALL_COMMAND,
shellOverride: 'powershell.exe',
runtime: distro
? {
runtime: 'wsl',
wslDistro: distro,
label: translate('components.onboarding.agyInstall.wslLocation', 'WSL {{distro}}', {
distro
})
}
: { runtime: 'host', label: getHostRuntimeLabel() }
}
}
@@ -319,6 +319,7 @@ export function useOnboardingFlow(
busyLabel,
detectedSet,
isDetectingAgents,
refreshDetectedAgents,
next,
skipToRepo,
dismissOnboarding,
+12
View File
@@ -19587,6 +19587,18 @@
"chatTip": "Want chat too? Turn it on any time in <em>Settings → Chat UI</em>.",
"turnOnChat": "Turn on now"
},
"agyInstall": {
"open": "Install",
"title": "Install Antigravity",
"description": "Press Enter in the terminal to run the official installer. Closing this terminal stops any command still running.",
"target": "Install location: {{location}}",
"thisDevice": "This device",
"wslLocation": "WSL {{distro}}",
"terminal": "Antigravity installer",
"detected": "Antigravity is detected on PATH. Close the terminal when the installer has finished.",
"close": "Close terminal",
"recheck": "Re-check agents"
},
"agentSurface": {
"chat": {
"label": "Chat UI",