mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 01:47:24 +00:00
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:
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user