diff --git a/src/renderer/src/components/onboarding/AgentStep.tsx b/src/renderer/src/components/onboarding/AgentStep.tsx index 397e7c7ea8f..1c33bac615e 100644 --- a/src/renderer/src/components/onboarding/AgentStep.tsx +++ b/src/renderer/src/components/onboarding/AgentStep.tsx @@ -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() 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: }} /> - +
+ {selectedEntry.id === 'antigravity' && + antigravityInstallTarget && + onInstallAntigravity && ( + + )} + +
)}
Promise + 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 ( + + event.preventDefault()} + onEscapeKeyDown={(event) => event.preventDefault()} + onInteractOutside={(event) => event.preventDefault()} + > + + + {translate('components.onboarding.agyInstall.title', 'Install Antigravity')} + + + {translate( + 'components.onboarding.agyInstall.description', + 'Press Enter in the terminal to run the official installer. Closing this terminal stops any command still running.' + )} + + +

+ {translate('components.onboarding.agyInstall.target', 'Install location: {{location}}', { + location: + target.runtime.label || + translate('components.onboarding.agyInstall.thisDevice', 'This device') + })} +

+ + {detected && ( +

+ {translate( + 'components.onboarding.agyInstall.detected', + 'Antigravity is detected on PATH. Close the terminal when the installer has finished.' + )} +

+ )} + + + + +
+
+ ) +} diff --git a/src/renderer/src/components/onboarding/OnboardingFlow.tsx b/src/renderer/src/components/onboarding/OnboardingFlow.tsx index dcbb1accfbd..c66461e917d 100644 --- a/src/renderer/src/components/onboarding/OnboardingFlow.tsx +++ b/src/renderer/src/components/onboarding/OnboardingFlow.tsx @@ -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( + 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 ( @@ -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({ />
+ {antigravityInstall && ( + setAntigravityInstall(null)} + /> + )} { + 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() + }) +}) diff --git a/src/renderer/src/components/onboarding/antigravity-install-target.ts b/src/renderer/src/components/onboarding/antigravity-install-target.ts new file mode 100644 index 00000000000..65ac5a2d88c --- /dev/null +++ b/src/renderer/src/components/onboarding/antigravity-install-target.ts @@ -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() } + } +} diff --git a/src/renderer/src/components/onboarding/use-onboarding-flow.ts b/src/renderer/src/components/onboarding/use-onboarding-flow.ts index 10bb5ab087f..2b6d642b84b 100644 --- a/src/renderer/src/components/onboarding/use-onboarding-flow.ts +++ b/src/renderer/src/components/onboarding/use-onboarding-flow.ts @@ -319,6 +319,7 @@ export function useOnboardingFlow( busyLabel, detectedSet, isDetectingAgents, + refreshDetectedAgents, next, skipToRepo, dismissOnboarding, diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 8b2bd3af38c..62cb86cc2e1 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -19587,6 +19587,18 @@ "chatTip": "Want chat too? Turn it on any time in Settings → Chat UI.", "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",