From 426f6d1d776cfb646c40c903266004758d9633f3 Mon Sep 17 00:00:00 2001 From: cynfria Date: Fri, 17 Jul 2026 11:40:22 -0700 Subject: [PATCH 1/8] feat(desktop): polish onboarding harness --- desktop/src/app/App.tsx | 8 +- .../agents/ui/GlobalAgentConfigFields.tsx | 360 ++++++++-- .../agents/ui/buzzAgentModelTuningFields.tsx | 110 ++- .../agents/ui/personaProviderModelFields.tsx | 354 ++++++++-- .../agents/ui/usePersonaModelDiscovery.ts | 46 +- .../features/communities/ui/WelcomeSetup.tsx | 10 +- .../assets/harness-logos/chatgpt.png | Bin 0 -> 5622 bytes .../assets/harness-logos/claude.png | Bin 0 -> 11536 bytes .../assets/harness-logos/gemini.png | Bin 0 -> 11810 bytes .../onboarding/assets/harness-logos/goose.png | Bin 0 -> 3799 bytes .../features/onboarding/machineOnboarding.ts | 1 + .../onboarding/ui/CommunityOnboardingFlow.tsx | 6 +- .../onboarding/ui/DefaultConfigStep.tsx | 120 ++-- .../onboarding/ui/MachineOnboardingFlow.tsx | 64 +- .../features/onboarding/ui/OnboardingFlow.tsx | 8 +- .../ui/OnboardingSlideTransition.tsx | 2 +- .../src/features/onboarding/ui/SetupStep.tsx | 642 +++++++++--------- desktop/src/features/onboarding/ui/types.ts | 1 + .../src/shared/styles/globals/components.css | 49 ++ desktop/src/testing/e2eBridge.ts | 20 +- .../e2e/onboarding-agent-defaults.spec.ts | 468 ++++++++++--- desktop/tests/helpers/bridge.ts | 3 + 22 files changed, 1602 insertions(+), 670 deletions(-) create mode 100644 desktop/src/features/onboarding/assets/harness-logos/chatgpt.png create mode 100644 desktop/src/features/onboarding/assets/harness-logos/claude.png create mode 100644 desktop/src/features/onboarding/assets/harness-logos/gemini.png create mode 100644 desktop/src/features/onboarding/assets/harness-logos/goose.png diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index 1a3a0c9f5f..713340a8a2 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -215,9 +215,11 @@ function CommunityQueryProvider({ children }: { children: ReactNode }) { function AppReady({ isSharedIdentity, isCommunitySwitch, + onBackToAgentDefaults, }: { isSharedIdentity: boolean; isCommunitySwitch: boolean; + onBackToAgentDefaults: () => void; }) { const onboarding = useAppOnboardingState(isSharedIdentity); @@ -236,7 +238,10 @@ function AppReady({ if (onboarding.stage === "onboarding") { return ( {showBootSplashOverlay ? (
void; onIsCustomProviderChange: (value: boolean) => void; onValidityChange?: (valid: boolean) => void; + autoSelectModelOnProviderChange?: boolean; + disableModelSelectDuringDiscovery?: boolean; + effortPlaceholderLabel?: string; + effortLabel?: string; + keepSelectedModelValueLabel?: boolean; + modelPlaceholderLabel?: string; + providerLabel?: string; + requireProviderForModelAndEffort?: boolean; + selectClassName?: string; + showAdvancedFields?: boolean; + showCustomModelOption?: boolean; + showCustomProviderOption?: boolean; + showDescriptions?: boolean; + showEffortField?: boolean; + showProviderPlaceholderOption?: boolean; + showRequiredIndicators?: boolean; + showUnavailableEffortOptions?: boolean; + unstyled?: boolean; + useCustomSelect?: boolean; + useChevronSelectIcon?: boolean; }; export function GlobalAgentConfigFields({ @@ -77,6 +102,26 @@ export function GlobalAgentConfigFields({ onCustomModelEditingChange, onIsCustomProviderChange, onValidityChange, + autoSelectModelOnProviderChange = false, + disableModelSelectDuringDiscovery = true, + effortPlaceholderLabel, + effortLabel = "Thinking/effort", + keepSelectedModelValueLabel = false, + modelPlaceholderLabel = "Select model", + providerLabel = "LLM provider", + requireProviderForModelAndEffort = false, + selectClassName, + showAdvancedFields = true, + showCustomModelOption = true, + showCustomProviderOption = true, + showDescriptions = true, + showEffortField = true, + showProviderPlaceholderOption = true, + showRequiredIndicators = true, + showUnavailableEffortOptions = true, + unstyled = false, + useCustomSelect = false, + useChevronSelectIcon = false, }: GlobalAgentConfigFieldsProps) { const bakedProvider = React.useMemo( () => bakedEnv.find((e) => e.key === "BUZZ_AGENT_PROVIDER")?.value ?? null, @@ -103,7 +148,12 @@ export function GlobalAgentConfigFields({ ); const providerValue = config.provider ?? ""; - const providerForDiscovery = isCustomProvider ? "" : providerValue; + const providerForDiscovery = isCustomProvider + ? "" + : providerValue || bakedProvider || ""; + const dependentFieldsDisabled = + requireProviderForModelAndEffort && + providerForDiscovery.trim().length === 0; const credentialProvider = isCustomProvider ? "" : effectiveProvider; const requiredEnvKeys = requiredCredentialEnvKeys( "buzz-agent", @@ -131,14 +181,65 @@ export function GlobalAgentConfigFields({ } = usePersonaModelDiscovery({ envVars: config.env_vars, isCustomProviderEditing: isCustomProvider, - modelFieldVisible: true, + modelFieldVisible: !dependentFieldsDisabled, open: true, provider: providerForDiscovery, selectedRuntime, }); + const autoSelectedModelProviderRef = React.useRef(null); + React.useEffect(() => { + if (!autoSelectModelOnProviderChange) return; + const trimmedProvider = providerForDiscovery.trim(); + if (trimmedProvider.length === 0 || isCustomProvider) { + autoSelectedModelProviderRef.current = null; + return; + } + if ((config.model ?? "").trim().length > 0) return; + if (modelDiscoveryLoading || discoveredModelOptions === null) return; + if (autoSelectedModelProviderRef.current === trimmedProvider) return; + + const firstModel = discoveredModelOptions.find( + (option) => option.id.trim().length > 0, + ); + if (!firstModel) return; + + autoSelectedModelProviderRef.current = trimmedProvider; + onCustomModelEditingChange(false); + onConfigChange({ ...config, model: firstModel.id }); + }, [ + autoSelectModelOnProviderChange, + config, + discoveredModelOptions, + isCustomProvider, + modelDiscoveryLoading, + onConfigChange, + onCustomModelEditingChange, + providerForDiscovery, + ]); + const currentEffortForAutoClear = config.env_vars[BUZZ_AGENT_THINKING_EFFORT] ?? ""; + React.useEffect(() => { + if (!dependentFieldsDisabled) return; + if ( + (config.model ?? "").trim().length === 0 && + currentEffortForAutoClear.length === 0 + ) { + return; + } + + const nextEnvVars = { ...config.env_vars }; + delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT]; + onCustomModelEditingChange(false); + onConfigChange({ ...config, env_vars: nextEnvVars, model: null }); + }, [ + config, + currentEffortForAutoClear, + dependentFieldsDisabled, + onConfigChange, + onCustomModelEditingChange, + ]); const { validValues: effortValidForAutoClear } = getProviderEffortConfig( config.provider ?? "", config.model ?? "", @@ -171,6 +272,7 @@ export function GlobalAgentConfigFields({ if (previousApiKey && previousApiKey !== nextApiKey) { delete nextEnvVars[previousApiKey]; } + const providerChanged = nextProvider !== (config.provider ?? null); onIsCustomProviderChange(false); onConfigChange({ @@ -178,7 +280,11 @@ export function GlobalAgentConfigFields({ env_vars: nextEnvVars, provider: nextProvider, model: - nextProvider === "relay-mesh" ? config.model || "auto" : config.model, + nextProvider === "relay-mesh" + ? config.model || "auto" + : autoSelectModelOnProviderChange && providerChanged + ? null + : config.model, }); } @@ -226,33 +332,103 @@ export function GlobalAgentConfigFields({ if (!bakedProvider) return null; return getBakedProviderInheritLabel(bakedProvider, providerOptions); }, [bakedProvider, providerOptions]); + const compactProviderZeroLabel = React.useMemo(() => { + if (bakedProvider) { + return ( + providerOptions.find((option) => option.id === bakedProvider)?.label ?? + bakedProvider + ); + } + return "Select a provider"; + }, [bakedProvider, providerOptions]); const { validValues: effortValid, defaultValue: effortDefault } = getProviderEffortConfig(config.provider ?? "", config.model ?? ""); const currentEffort = config.env_vars[BUZZ_AGENT_THINKING_EFFORT] ?? ""; - return ( - + const fieldClassName = unstyled ? "space-y-4" : "space-y-1.5 p-3"; + const blockClassName = unstyled ? "" : "p-3"; + const fieldLabelClassName = unstyled ? "pl-3" : undefined; + const providerDropdownOptions = [ + ...providerOptions + .filter( + (opt) => + showProviderPlaceholderOption || + opt.id !== "" || + providerSelectValue === AUTO_PROVIDER_DROPDOWN_VALUE, + ) + .map((opt) => ({ + label: + opt.id === "" + ? showProviderPlaceholderOption + ? (providerZeroLabel ?? opt.label) + : compactProviderZeroLabel + : opt.label, + value: opt.id || AUTO_PROVIDER_DROPDOWN_VALUE, + })), + ...(showCustomProviderOption + ? [{ label: "Custom provider…", value: CUSTOM_PROVIDER_DROPDOWN_VALUE }] + : []), + ]; + const providerSelect = useCustomSelect ? ( + + ) : ( + + ); + + const content = ( + <> {/* Provider field */} -
- - + {providerLabel} + + {!useCustomSelect && useChevronSelectIcon ? ( +
+ {providerSelect} +
+ ) : ( + providerSelect + )} {isCustomProvider ? ( - {apiKeyEnvVar ? ( -
+ {showAdvancedFields && apiKeyEnvVar ? ( +
+
{/* Thinking / Effort */} -
- { - const nextEnvVars = { ...config.env_vars }; - if (value === "") { - delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT]; - } else { - nextEnvVars[BUZZ_AGENT_THINKING_EFFORT] = value; + {showEffortField ? ( +
+ -
+ inheritedEffort={bakedEffort ?? undefined} + label={effortLabel} + labelClassName={fieldLabelClassName} + onChange={(value) => { + const nextEnvVars = { ...config.env_vars }; + if (value === "") { + delete nextEnvVars[BUZZ_AGENT_THINKING_EFFORT]; + } else { + nextEnvVars[BUZZ_AGENT_THINKING_EFFORT] = value; + } + onConfigChange({ ...config, env_vars: nextEnvVars }); + }} + selectClassName={selectClassName} + showUnavailableOptions={showUnavailableEffortOptions} + testId="global-agent-thinking-effort-select" + useCustomSelect={useCustomSelect} + /> +
+ ) : null} - {/* Env vars */} -
- k !== BUZZ_AGENT_THINKING_EFFORT, - ), - )} - /> -
- + {showAdvancedFields ? ( + <> + {/* Env vars */} +
+ k !== BUZZ_AGENT_THINKING_EFFORT, + ), + )} + /> +
+ + ) : null} + ); + + if (unstyled) { + return
{content}
; + } + + return {content}; } diff --git a/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx b/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx index 25af85ba9e..191b22e211 100644 --- a/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx +++ b/desktop/src/features/agents/ui/buzzAgentModelTuningFields.tsx @@ -7,7 +7,12 @@ */ import * as React from "react"; import { Input } from "@/shared/ui/input"; +import { cn } from "@/shared/lib/cn"; import type { EnvVarsValue } from "./EnvVarsEditor"; +import { + AgentDropdownSelect, + type AgentDropdownOption, +} from "./personaProviderModelFields"; import { BUZZ_AGENT_MAX_CONTEXT_TOKENS, BUZZ_AGENT_MAX_OUTPUT_TOKENS, @@ -32,21 +37,34 @@ import { */ export function EffortSelectField({ currentEffort, + disabled = false, + emptyOptionLabel, effortDefault, effortValid, + fieldClassName, htmlFor, inheritedEffort, inheritFallbackLabel, label, + labelClassName, onChange, + selectClassName, + showUnavailableOptions = true, testId, + useCustomSelect = false, }: { /** Current effort value from env vars ("" = inherit). */ currentEffort: string; + /** Disable the dropdown. */ + disabled?: boolean; + /** Optional replacement label for the empty/inherit option. */ + emptyOptionLabel?: string; /** Semantic default for this provider/model combination, or null for manual-budget. */ effortDefault: string | null; /** Valid effort values for this provider/model. */ effortValid: ReadonlyArray; + /** Optional class override for the field wrapper. */ + fieldClassName?: string; /** `htmlFor` attribute for the label element. */ htmlFor: string; /** Inherited effort from a higher-precedence layer (shown in the Inherit option label). */ @@ -67,40 +85,82 @@ export function EffortSelectField({ inheritFallbackLabel?: string; /** Label text for the dropdown. */ label: string; + /** Optional class override for the label. */ + labelClassName?: string; /** Called when the user selects a new value. */ onChange: (value: string) => void; + /** Optional class override for the select/trigger. */ + selectClassName?: string; + /** When false, hide effort values that are not valid for the provider/model. */ + showUnavailableOptions?: boolean; /** data-testid attribute for the select element. */ testId: string; + /** Render the polished app dropdown instead of the native select. */ + useCustomSelect?: boolean; }) { + const inheritLabel = inheritedEffort + ? `Inherit (${inheritedEffort})` + : effortDefault === null + ? "Inherit (default)" + : (inheritFallbackLabel ?? "Inherit"); + const effortOptions: AgentDropdownOption[] = [ + { label: emptyOptionLabel ?? inheritLabel, value: "" }, + ...BUZZ_AGENT_THINKING_EFFORT_VALUES.flatMap((v) => { + const isValid = (effortValid as readonly string[]).includes(v); + if (!showUnavailableOptions && !isValid) return []; + const isDefault = v === effortDefault; + return [ + { + disabled: !isValid, + label: isDefault ? `${v} (default)` : v, + value: v, + }, + ]; + }), + ]; + return ( -
-
- ); - } - return ( - + {installError ? "RETRY" : "SET UP"} + ); } -function RuntimeDetails({ runtime }: { runtime: AcpRuntimeCatalogEntry }) { - if ( - runtime.availability === "available" && - runtime.command && - runtime.binaryPath - ) { - const description = describeResolvedCommand( - runtime.command, - runtime.binaryPath, - ); - return ( - <> -

- {description.charAt(0).toUpperCase() + description.slice(1)} -

- {runtime.defaultArgs.length > 0 ? ( -

- Args:{" "} - {runtime.defaultArgs.join(", ")} -

- ) : null} - - ); - } - - if (runtime.availability === "adapter_missing") { - return ( - <> -

- CLI detected; ACP adapter missing. -

-

- {runtime.installHint} -

- - ); - } - - if (runtime.availability === "adapter_outdated") { - return ( - <> -

- ACP adapter detected but outdated — reinstall required. -

-

- This updates the machine-global{" "} - codex-acp{" "} - adapter. Older Buzz releases using the legacy adapter contract may - lose community access until{" "} - - @zed-industries/codex-acp@0.16.0 - {" "} - is restored. -

-

- {runtime.installHint} -

- - ); - } - - if (runtime.availability === "cli_missing") { - return ( - <> -

- ACP adapter detected; CLI missing. -

-

- {runtime.installHint} -

- - ); - } - - return ( - <> -

- Not installed yet. -

-

- {runtime.installHint} -

- - ); -} - -function runtimeDetailText(runtime: AcpRuntimeCatalogEntry): string { - if ( - runtime.availability === "available" && - runtime.command && - runtime.binaryPath - ) { - const description = describeResolvedCommand( - runtime.command, - runtime.binaryPath, - ); - return description.charAt(0).toUpperCase() + description.slice(1); - } - if (runtime.availability === "adapter_missing") { - return "CLI detected; ACP adapter missing."; - } - if (runtime.availability === "adapter_outdated") { - return "ACP adapter detected but outdated — reinstall required."; - } - if (runtime.availability === "cli_missing") { - return "ACP adapter detected; CLI missing."; - } - return "Not installed yet."; -} - function isSupportedOnboardingAuthMethod( runtime: AcpRuntimeCatalogEntry, method: { id: string; name: string }, @@ -322,24 +286,25 @@ function RuntimeAuthActions({ if (runtime.authStatus.status === "config_invalid") { return ( -

+

{runtime.authStatus.diagnostic}

); } + if (runtime.authStatus.status === "unknown") { return ( -
+
Couldn’t verify authentication.
); } + if (runtime.authStatus.status !== "logged_out") return null; const methods = (methodsQuery.data?.methods ?? []).filter((method) => isSupportedOnboardingAuthMethod(runtime, method), ); + return ( -
+
{methodsQuery.isLoading ? ( Loading sign-in… ) : methods.length > 0 ? ( methods.map((method) => ( - - - - - -
- - - -
-

- {runtime.label} -

- {!isAvailable && !installError ? ( -

- {runtimeDetailText(runtime)} -

+ + +
+
+ + {!isBuzzRuntime(runtime) ? ( +

+ {runtime.label} +

+ ) : null} +
+ {showStatusPill ? ( + ) : null} - {installError ? ( -

+ {selected && installError ? ( +

{installError}

) : null} - {installSuccess && runtime.availability !== "available" ? ( -

Installed

- ) : null} - {selected ? ( -

Preferred

- ) : null}
@@ -527,10 +497,8 @@ function GitBashPrerequisiteCard() { return (
@@ -577,16 +545,32 @@ function GitBashPrerequisiteCard() { ); } +function RuntimeProvidersLoadingState() { + return ( +
+
+ +

+ Finding your providers... +

+
+
+ ); +} + function RuntimeProvidersSection({ - isSelectionSaving, - onSelectedRuntimeChange, + onSelectedRuntimeIdsChange, runtimeProviders, - selectedRuntimeId, + selectedRuntimeIds, }: { - isSelectionSaving: boolean; - onSelectedRuntimeChange: (runtimeId: string) => void; + onSelectedRuntimeIdsChange: (runtimeIds: readonly string[]) => void; runtimeProviders: SetupStepState["runtimeProviders"]; - selectedRuntimeId: string | null; + selectedRuntimeIds: readonly string[]; }) { const { errorMessage, isChecking, items } = runtimeProviders; const runtimeOrder = ["claude", "codex", "goose", "buzz-agent"]; @@ -602,6 +586,25 @@ function RuntimeProvidersSection({ const [installResults, setInstallResults] = React.useState< Record >({}); + const selectedRuntimeIdSet = React.useMemo( + () => new Set(selectedRuntimeIds), + [selectedRuntimeIds], + ); + + function handleRuntimeToggle(runtimeId: string) { + if (selectedRuntimeIdSet.has(runtimeId)) { + onSelectedRuntimeIdsChange( + selectedRuntimeIds.filter((selectedId) => selectedId !== runtimeId), + ); + return; + } + onSelectedRuntimeIdsChange([...selectedRuntimeIds, runtimeId]); + } + + function handleRuntimeSelect(runtimeId: string) { + if (selectedRuntimeIdSet.has(runtimeId)) return; + onSelectedRuntimeIdsChange([...selectedRuntimeIds, runtimeId]); + } function handleInstall(runtimeId: string) { setInstallResults((current) => ({ @@ -631,61 +634,61 @@ function RuntimeProvidersSection({ } return ( -
-
+
+

Use the models that fit the task

-

- These are the local agent harnesses Buzz detected. You choose a - harness when creating each agent. +

+ Connect different model providers so each agent can use the right + model for the work. +

+

+ Choose at least one to start using Buzz.

- - - {items.length > 0 ? ( -
- {orderedItems.map((runtime) => ( - handleInstall(runtime.id)} - onSelect={() => onSelectedRuntimeChange(runtime.id)} - runtime={runtime} - selectionDisabled={isSelectionSaving} - selected={selectedRuntimeId === runtime.id} - /> - ))} -
- ) : isChecking ? ( -
- Looking for compatible runtimes... -
- ) : errorMessage ? null : ( -

- No compatible ACP runtimes detected yet. You can finish setup now and - come back later in Settings > Doctor. -

- )} +
+ + + {items.length > 0 ? ( +
+ Agent harnesses + {orderedItems.map((runtime) => ( + handleInstall(runtime.id)} + onSelect={() => handleRuntimeSelect(runtime.id)} + onToggle={() => handleRuntimeToggle(runtime.id)} + runtime={runtime} + selected={selectedRuntimeIdSet.has(runtime.id)} + /> + ))} +
+ ) : isChecking ? ( + + ) : errorMessage ? null : ( +

+ No compatible ACP runtimes detected yet. You can finish setup now + and come back later in Settings > Doctor. +

+ )} - {errorMessage ? ( -

- {errorMessage} -

- ) : null} + {errorMessage ? ( +

+ {errorMessage} +

+ ) : null} +
); } @@ -694,42 +697,37 @@ function SetupStepContent({ actions, direction, isSelectionSaving, - onSelectedRuntimeChange, + onSelectedRuntimeIdsChange, selectionError, - selectedRuntimeId, + selectedRuntimeIds, state, }: SetupStepContentProps) { const { runtimeProviders } = state; return ( {selectionError ? ( -

+

{selectionError}

) : null} +
diff --git a/desktop/src/features/onboarding/ui/OnboardingFlow.tsx b/desktop/src/features/onboarding/ui/OnboardingFlow.tsx index 269d70046f..1928ee3835 100644 --- a/desktop/src/features/onboarding/ui/OnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/OnboardingFlow.tsx @@ -149,7 +149,7 @@ export function OnboardingFlow({ identityLost = false, initialProfile, }: OnboardingFlowProps) { - const { backToAgentDefaults, complete, skipForNow } = actions; + const { complete, skipForNow } = actions; const { activeCommunity } = useCommunities(); const queryClient = useQueryClient(); const savedProfile = resolveSavedProfile(initialProfile); @@ -458,7 +458,7 @@ export function OnboardingFlow({
{membershipError && (currentPage === "profile" || currentPage === "avatar") ? ( @@ -498,10 +498,6 @@ export function OnboardingFlow({ actions={{ advanceWithoutSaving: advanceFromProfileWithoutSaving, back: () => { - if (backToAgentDefaults) { - backToAgentDefaults(); - return; - } setMembershipError(null); setIsCommunityChangeOpen(true); }, diff --git a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx new file mode 100644 index 0000000000..f738b19059 --- /dev/null +++ b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx @@ -0,0 +1,91 @@ +import * as React from "react"; +import { TerminalSquare } from "lucide-react"; + +import type { AcpRuntimeCatalogEntry } from "@/shared/api/types"; +import { cn } from "@/shared/lib/cn"; +import { useTheme } from "@/shared/theme/ThemeProvider"; +import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; +import chatgptLogoUrl from "../assets/harness-logos/chatgpt.png?inline"; +import claudeLogoUrl from "../assets/harness-logos/claude.png?inline"; +import geminiLogoUrl from "../assets/harness-logos/gemini.png?inline"; +import gooseLogoUrl from "../assets/harness-logos/goose.png?inline"; + +const RUNTIME_LOGOS: Record = { + chatgpt: chatgptLogoUrl, + claude: claudeLogoUrl, + "claude-code": claudeLogoUrl, + codex: chatgptLogoUrl, + gemini: geminiLogoUrl, + goose: gooseLogoUrl, + openai: chatgptLogoUrl, +}; + +function isBuzzRuntime(runtime: AcpRuntimeCatalogEntry): boolean { + const runtimeId = runtime.id.trim().toLowerCase(); + const runtimeLabel = runtime.label.trim().toLowerCase(); + return runtimeId === "buzz-agent" || runtimeLabel === "buzz"; +} + +export function getRuntimeDisplayLabel( + runtime: AcpRuntimeCatalogEntry, +): string { + return isBuzzRuntime(runtime) ? "Buzz" : runtime.label; +} + +function getRuntimeLogoUrl(runtime: AcpRuntimeCatalogEntry): string | null { + const runtimeId = runtime.id.trim().toLowerCase(); + const runtimeLabel = runtime.label.trim().toLowerCase(); + return ( + RUNTIME_LOGOS[runtimeId] ?? + (runtimeLabel.includes("claude") + ? claudeLogoUrl + : runtimeLabel.includes("goose") + ? gooseLogoUrl + : runtimeLabel.includes("gemini") + ? geminiLogoUrl + : runtimeLabel.includes("codex") || runtimeLabel.includes("chatgpt") + ? chatgptLogoUrl + : null) + ); +} + +export function RuntimeIcon({ + className = "h-8 w-8", + runtime, +}: { + className?: string; + runtime: AcpRuntimeCatalogEntry; +}) { + const [imageFailed, setImageFailed] = React.useState(false); + const { isDark } = useTheme(); + const runtimeLogoUrl = getRuntimeLogoUrl(runtime); + const imageUrl = runtimeLogoUrl ?? runtime.avatarUrl; + const shouldForceForegroundColor = !runtimeLogoUrl && runtime.id === "goose"; + + if (isBuzzRuntime(runtime)) { + return ; + } + + if (imageUrl && !imageFailed) { + return ( + setImageFailed(true)} + src={imageUrl} + /> + ); + } + + return ( + + ); +} diff --git a/desktop/src/features/onboarding/ui/SetupStep.tsx b/desktop/src/features/onboarding/ui/SetupStep.tsx index f3604e0830..2f3132610e 100644 --- a/desktop/src/features/onboarding/ui/SetupStep.tsx +++ b/desktop/src/features/onboarding/ui/SetupStep.tsx @@ -1,11 +1,6 @@ import * as React from "react"; import { openUrl } from "@tauri-apps/plugin-opener"; -import { - AlertTriangle, - Check, - ExternalLink, - TerminalSquare, -} from "lucide-react"; +import { AlertTriangle, Check, ExternalLink } from "lucide-react"; import { useAcpAuthMethodsQuery, @@ -14,22 +9,22 @@ import { useInstallAcpRuntimeMutation, useGitBashPrerequisiteQuery, } from "@/features/agents/hooks"; -import type { AcpRuntimeCatalogEntry } from "@/shared/api/types"; +import { describeResolvedCommand } from "@/features/agents/ui/agentUi"; +import type { AcpAuthMethod, AcpRuntimeCatalogEntry } from "@/shared/api/types"; import { getInstallErrorMessage } from "@/shared/lib/installError"; import { cn } from "@/shared/lib/cn"; -import { useTheme } from "@/shared/theme/ThemeProvider"; import { Badge } from "@/shared/ui/badge"; import { Button } from "@/shared/ui/button"; -import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; import { FlappingBee } from "@/shared/ui/buzz-logo/FlappingBee"; import { Spinner } from "@/shared/ui/spinner"; -import { runtimeCanBeSelected } from "./onboardingRuntimeSelection"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +import { + runtimeCanAdvanceOnboarding, + runtimeCanBeSelected, +} from "./onboardingRuntimeSelection"; import { ONBOARDING_PRIMARY_CTA_CLASS } from "./OnboardingChrome"; import { OnboardingFooter } from "./OnboardingFooter"; -import chatgptLogoUrl from "../assets/harness-logos/chatgpt.png"; -import claudeLogoUrl from "../assets/harness-logos/claude.png"; -import geminiLogoUrl from "../assets/harness-logos/gemini.png"; -import gooseLogoUrl from "../assets/harness-logos/goose.png"; +import { getRuntimeDisplayLabel, RuntimeIcon } from "./RuntimeIcon"; import { type OnboardingTransitionDirection, OnboardingSlideTransition, @@ -60,38 +55,7 @@ type InstallResultState = { success: boolean; }; -const RUNTIME_LOGOS: Record = { - chatgpt: chatgptLogoUrl, - claude: claudeLogoUrl, - "claude-code": claudeLogoUrl, - codex: chatgptLogoUrl, - gemini: geminiLogoUrl, - goose: gooseLogoUrl, - openai: chatgptLogoUrl, -}; - -function isBuzzRuntime(runtime: AcpRuntimeCatalogEntry): boolean { - const runtimeId = runtime.id.trim().toLowerCase(); - const runtimeLabel = runtime.label.trim().toLowerCase(); - return runtimeId === "buzz-agent" || runtimeLabel === "buzz"; -} - -function getRuntimeLogoUrl(runtime: AcpRuntimeCatalogEntry): string | null { - const runtimeId = runtime.id.trim().toLowerCase(); - const runtimeLabel = runtime.label.trim().toLowerCase(); - return ( - RUNTIME_LOGOS[runtimeId] ?? - (runtimeLabel.includes("claude") - ? claudeLogoUrl - : runtimeLabel.includes("goose") - ? gooseLogoUrl - : runtimeLabel.includes("gemini") - ? geminiLogoUrl - : runtimeLabel.includes("codex") || runtimeLabel.includes("chatgpt") - ? chatgptLogoUrl - : null) - ); -} +type InstallResultsState = Record; function useSetupStepState(): SetupStepState { const runtimesQuery = useAcpRuntimesQuery(); @@ -109,47 +73,6 @@ function useSetupStepState(): SetupStepState { }; } -function RuntimeIcon({ - className = "h-8 w-8", - runtime, -}: { - className?: string; - runtime: AcpRuntimeCatalogEntry; -}) { - const [imageFailed, setImageFailed] = React.useState(false); - const { isDark } = useTheme(); - const runtimeLogoUrl = getRuntimeLogoUrl(runtime); - const imageUrl = runtimeLogoUrl ?? runtime.avatarUrl; - const shouldForceForegroundColor = !runtimeLogoUrl && runtime.id === "goose"; - - if (isBuzzRuntime(runtime)) { - return ; - } - - if (imageUrl && !imageFailed) { - return ( - setImageFailed(true)} - src={imageUrl} - /> - ); - } - - return ( - - ); -} - function RuntimeSelectionIndicator({ runtime, selected, @@ -161,7 +84,7 @@ function RuntimeSelectionIndicator({