mirror of
https://github.com/block/buzz.git
synced 2026-10-27 12:40:30 +00:00
Signed-off-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Pinky <44b8e82baa6e0e254e0208d68f335c283c94e7b78dd1fa10d5a49d3f13dd0435@sprout-oss.stage.blox.sqprod.co>
674 lines
20 KiB
TypeScript
674 lines
20 KiB
TypeScript
import * as React from "react";
|
|
import { openUrl } from "@tauri-apps/plugin-opener";
|
|
import {
|
|
AlertTriangle,
|
|
Check,
|
|
ExternalLink,
|
|
Plus,
|
|
RefreshCw,
|
|
TerminalSquare,
|
|
} from "lucide-react";
|
|
|
|
import {
|
|
useAcpRuntimesQuery,
|
|
useInstallAcpRuntimeMutation,
|
|
useGitBashPrerequisiteQuery,
|
|
} from "@/features/agents/hooks";
|
|
import { describeResolvedCommand } from "@/features/agents/ui/agentUi";
|
|
import {
|
|
GlobalAgentConfigFields,
|
|
EMPTY_GLOBAL_CONFIG,
|
|
} from "@/features/agents/ui/GlobalAgentConfigFields";
|
|
import { createSaveCoalescer } from "./saveCoalescer";
|
|
import { getBakedBuildEnv, type BakedEnvEntry } from "@/shared/api/tauri";
|
|
import {
|
|
getGlobalAgentConfig,
|
|
setGlobalAgentConfig,
|
|
} from "@/shared/api/tauriGlobalAgentConfig";
|
|
import type {
|
|
AcpRuntimeCatalogEntry,
|
|
GlobalAgentConfig,
|
|
} 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 { Spinner } from "@/shared/ui/spinner";
|
|
import {
|
|
type OnboardingTransitionDirection,
|
|
OnboardingSlideTransition,
|
|
} from "./OnboardingSlideTransition";
|
|
import type { SetupStepActions, SetupStepState } from "./types";
|
|
import { resolveAgentReadiness } from "./agentReadiness";
|
|
|
|
type SetupStepProps = {
|
|
actions: SetupStepActions;
|
|
direction: OnboardingTransitionDirection;
|
|
};
|
|
|
|
type SetupStepContentProps = {
|
|
actions: SetupStepActions;
|
|
direction: OnboardingTransitionDirection;
|
|
state: SetupStepState;
|
|
};
|
|
|
|
type InstallResultState = {
|
|
error: string | null;
|
|
success: boolean;
|
|
};
|
|
|
|
function AgentDefaultsSection() {
|
|
const runtimesQuery = useAcpRuntimesQuery();
|
|
const [config, setConfig] =
|
|
React.useState<GlobalAgentConfig>(EMPTY_GLOBAL_CONFIG);
|
|
const [isLoading, setIsLoading] = React.useState(true);
|
|
const [isCustomProvider, setIsCustomProvider] = React.useState(false);
|
|
const [isCustomModelEditing, setIsCustomModelEditing] = React.useState(false);
|
|
const [bakedEnv, setBakedEnv] = React.useState<BakedEnvEntry[]>([]);
|
|
const coalescerRef = React.useRef<{
|
|
enqueue: (value: GlobalAgentConfig) => void;
|
|
cancel: () => void;
|
|
} | null>(null);
|
|
|
|
React.useEffect(() => {
|
|
let unmounted = false;
|
|
|
|
getGlobalAgentConfig()
|
|
.then((loaded) => {
|
|
if (!unmounted) {
|
|
setConfig(loaded);
|
|
setIsLoading(false);
|
|
}
|
|
})
|
|
.catch(() => {
|
|
if (!unmounted) setIsLoading(false);
|
|
});
|
|
getBakedBuildEnv()
|
|
.then((env) => {
|
|
if (!unmounted) setBakedEnv(env);
|
|
})
|
|
.catch(() => undefined);
|
|
|
|
// The coalescer serializes autosaves and drains any edit that arrived
|
|
// while a previous save was in flight. Cancel on unmount so a slow
|
|
// in-flight request never calls setState on an unmounted component.
|
|
const coalescer = createSaveCoalescer<GlobalAgentConfig>(
|
|
// set_global_agent_config returns a save result (config + restart
|
|
// counts); the coalescer round-trips the persisted config only.
|
|
async (next) => (await setGlobalAgentConfig(next)).config,
|
|
() => undefined, // saving state not surfaced in this autosave UX
|
|
(saved) => {
|
|
if (!unmounted) setConfig(saved);
|
|
},
|
|
);
|
|
coalescerRef.current = coalescer;
|
|
|
|
return () => {
|
|
unmounted = true;
|
|
coalescer.cancel();
|
|
};
|
|
}, []);
|
|
|
|
const buzzAgentRuntime = React.useMemo(
|
|
() => (runtimesQuery.data ?? []).find((r) => r.id === "buzz-agent"),
|
|
[runtimesQuery.data],
|
|
);
|
|
|
|
const readiness = resolveAgentReadiness(runtimesQuery.data ?? [], config);
|
|
|
|
return (
|
|
<section className="space-y-4">
|
|
<div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
|
|
<div className="space-y-1">
|
|
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
|
Agent defaults
|
|
</h2>
|
|
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
|
|
Configure the LLM provider and credentials that buzz-agent uses, or
|
|
connect a CLI harness like Claude or Goose above.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex shrink-0 items-center gap-2">
|
|
{readiness.ready ? (
|
|
<Badge
|
|
className="border border-primary/20 bg-primary/10 text-primary"
|
|
data-testid="agent-readiness-badge"
|
|
variant="outline"
|
|
>
|
|
{readiness.reason === "cli"
|
|
? `${readiness.runtimeLabel} ready`
|
|
: "buzz-agent configured"}
|
|
</Badge>
|
|
) : (
|
|
<Badge
|
|
className="border border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-400"
|
|
data-testid="agent-readiness-badge"
|
|
variant="outline"
|
|
>
|
|
Not configured
|
|
</Badge>
|
|
)}
|
|
<Button
|
|
className="h-7 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
|
|
data-testid="agent-readiness-recheck"
|
|
disabled={runtimesQuery.isFetching}
|
|
onClick={() => void runtimesQuery.refetch()}
|
|
size="sm"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
{runtimesQuery.isFetching ? (
|
|
<Spinner className="h-3 w-3 border-[1.5px]" />
|
|
) : (
|
|
<RefreshCw className="h-3 w-3" />
|
|
)}
|
|
Re-check
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
{isLoading ? (
|
|
<div className="flex items-center gap-2 py-4 text-sm text-muted-foreground">
|
|
<Spinner className="h-4 w-4 border-2" />
|
|
Loading…
|
|
</div>
|
|
) : (
|
|
<GlobalAgentConfigFields
|
|
bakedEnv={bakedEnv}
|
|
buzzAgentRuntime={buzzAgentRuntime}
|
|
config={config}
|
|
isCustomModelEditing={isCustomModelEditing}
|
|
isCustomProvider={isCustomProvider}
|
|
onConfigChange={(next) => {
|
|
// Always apply optimistically so the UI never reverts mid-save,
|
|
// then enqueue the persist — the coalescer serialises multiple
|
|
// rapid edits into a single trailing request.
|
|
setConfig(next);
|
|
coalescerRef.current?.enqueue(next);
|
|
}}
|
|
onCustomModelEditingChange={setIsCustomModelEditing}
|
|
onIsCustomProviderChange={setIsCustomProvider}
|
|
/>
|
|
)}
|
|
|
|
{!readiness.ready ? (
|
|
<p className="text-sm text-muted-foreground">
|
|
You can finish now and configure agents later in Settings.
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function useSetupStepState(): SetupStepState {
|
|
const runtimesQuery = useAcpRuntimesQuery();
|
|
const items = runtimesQuery.data ?? [];
|
|
const isChecking = runtimesQuery.isLoading;
|
|
const errorMessage =
|
|
runtimesQuery.error instanceof Error ? runtimesQuery.error.message : null;
|
|
|
|
return {
|
|
runtimeProviders: {
|
|
errorMessage,
|
|
isChecking,
|
|
items,
|
|
},
|
|
};
|
|
}
|
|
|
|
function RuntimeIcon({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
|
|
const [imageFailed, setImageFailed] = React.useState(false);
|
|
const { isDark } = useTheme();
|
|
const shouldForceForegroundColor = runtime.id === "goose";
|
|
|
|
if (runtime.avatarUrl && !imageFailed) {
|
|
return (
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-md border border-border/45 bg-background/80">
|
|
<img
|
|
alt=""
|
|
className={cn(
|
|
"h-7 w-7 rounded-sm object-contain",
|
|
shouldForceForegroundColor &&
|
|
(isDark ? "brightness-0 invert" : "brightness-0"),
|
|
)}
|
|
onError={() => setImageFailed(true)}
|
|
src={runtime.avatarUrl}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="flex h-9 w-9 items-center justify-center rounded-md border border-border/45 bg-background/80 text-muted-foreground">
|
|
<TerminalSquare className="h-4 w-4" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RuntimeStatus({
|
|
installError,
|
|
installSuccess,
|
|
isInstalling,
|
|
onInstall,
|
|
runtime,
|
|
}: {
|
|
installError: string | null;
|
|
installSuccess: boolean;
|
|
isInstalling: boolean;
|
|
onInstall: () => void;
|
|
runtime: AcpRuntimeCatalogEntry;
|
|
}) {
|
|
if (isInstalling) {
|
|
return (
|
|
<div
|
|
aria-label={`Installing ${runtime.label}`}
|
|
className="flex h-8 shrink-0 items-center justify-center"
|
|
role="status"
|
|
>
|
|
<Spinner className="h-4 w-4 border-2 text-foreground" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (installError) {
|
|
return (
|
|
<div className="flex h-8 shrink-0 items-center justify-center">
|
|
<AlertTriangle className="h-4 w-4 text-destructive" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (runtime.availability === "available" || installSuccess) {
|
|
return (
|
|
<div className="flex h-8 shrink-0 items-center justify-center">
|
|
<Check className="h-4 w-4 text-primary" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (runtime.canAutoInstall) {
|
|
return (
|
|
<Button
|
|
aria-label={`Install ${runtime.label}`}
|
|
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
|
|
data-testid={`onboarding-runtime-install-${runtime.id}`}
|
|
onClick={onInstall}
|
|
size="icon"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
aria-label={`View ${runtime.label} setup instructions`}
|
|
className="h-8 w-8 shrink-0 text-muted-foreground hover:text-foreground"
|
|
data-testid={`onboarding-runtime-instructions-${runtime.id}`}
|
|
onClick={() => void openUrl(runtime.installInstructionsUrl)}
|
|
size="icon"
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
<ExternalLink className="h-4 w-4" />
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
function RuntimeDetails({ runtime }: { runtime: AcpRuntimeCatalogEntry }) {
|
|
if (
|
|
runtime.availability === "available" &&
|
|
runtime.command &&
|
|
runtime.binaryPath
|
|
) {
|
|
return (
|
|
<>
|
|
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
|
{describeResolvedCommand(runtime.command, runtime.binaryPath)}
|
|
</p>
|
|
{runtime.defaultArgs.length > 0 ? (
|
|
<p className="mt-1 text-xs text-muted-foreground/80">
|
|
Args:{" "}
|
|
<code className="font-mono">{runtime.defaultArgs.join(", ")}</code>
|
|
</p>
|
|
) : null}
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (runtime.availability === "adapter_missing") {
|
|
return (
|
|
<>
|
|
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
|
CLI detected; ACP adapter missing.
|
|
</p>
|
|
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
|
{runtime.installHint}
|
|
</p>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (runtime.availability === "adapter_outdated") {
|
|
return (
|
|
<>
|
|
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
|
ACP adapter detected but outdated — reinstall required.
|
|
</p>
|
|
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
|
This updates the machine-global{" "}
|
|
<code className="rounded bg-muted px-0.5 text-2xs">codex-acp</code>{" "}
|
|
adapter. Older Buzz releases using the legacy adapter contract may
|
|
lose community access until{" "}
|
|
<code className="rounded bg-muted px-0.5 text-2xs">
|
|
@zed-industries/codex-acp@0.16.0
|
|
</code>{" "}
|
|
is restored.
|
|
</p>
|
|
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
|
{runtime.installHint}
|
|
</p>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (runtime.availability === "cli_missing") {
|
|
return (
|
|
<>
|
|
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
|
ACP adapter detected; CLI missing.
|
|
</p>
|
|
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
|
{runtime.installHint}
|
|
</p>
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<p className="mt-2 text-sm leading-5 text-muted-foreground">
|
|
Not installed yet.
|
|
</p>
|
|
<p className="mt-1 text-xs leading-5 text-muted-foreground/80">
|
|
{runtime.installHint}
|
|
</p>
|
|
</>
|
|
);
|
|
}
|
|
|
|
function RuntimeCard({
|
|
installError,
|
|
installSuccess,
|
|
isInstalling,
|
|
onInstall,
|
|
runtime,
|
|
}: {
|
|
installError: string | null;
|
|
installSuccess: boolean;
|
|
isInstalling: boolean;
|
|
onInstall: () => void;
|
|
runtime: AcpRuntimeCatalogEntry;
|
|
}) {
|
|
const isAvailable = runtime.availability === "available" || installSuccess;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"grid min-h-28 grid-cols-[auto_1fr_auto] items-start gap-3 rounded-lg border bg-background p-3 text-left transition-colors sm:p-4",
|
|
isAvailable
|
|
? "border-primary/25 bg-primary/[0.055] shadow-[0_12px_30px_hsl(var(--primary)/0.08)] dark:bg-primary/[0.08]"
|
|
: installError
|
|
? "border-destructive/45 bg-destructive/5 shadow-xs"
|
|
: "border-2 border-dashed border-muted-foreground/35 bg-muted/20 shadow-none",
|
|
)}
|
|
data-testid={`onboarding-runtime-${runtime.id}`}
|
|
>
|
|
<RuntimeIcon runtime={runtime} />
|
|
|
|
<div className="min-w-0">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<h2 className="text-base font-medium leading-6 text-foreground">
|
|
{runtime.label}
|
|
</h2>
|
|
{isAvailable ? (
|
|
<Badge
|
|
className="border border-primary/20 bg-primary/10 text-primary"
|
|
variant="outline"
|
|
>
|
|
Installed
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
|
|
<RuntimeDetails runtime={runtime} />
|
|
|
|
{installError ? (
|
|
<p className="mt-3 whitespace-pre-line rounded-md border border-destructive/25 bg-destructive/10 px-3 py-2 text-xs leading-5 text-destructive">
|
|
{installError}
|
|
</p>
|
|
) : null}
|
|
|
|
{installSuccess && runtime.availability !== "available" ? (
|
|
<p className="mt-3 rounded-md border border-primary/25 bg-primary/10 px-3 py-2 text-xs leading-5 text-primary">
|
|
Installed successfully. You can finish onboarding now.
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
|
|
<RuntimeStatus
|
|
installError={installError}
|
|
installSuccess={installSuccess}
|
|
isInstalling={isInstalling}
|
|
onInstall={onInstall}
|
|
runtime={runtime}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GitBashPrerequisiteCard() {
|
|
const query = useGitBashPrerequisiteQuery();
|
|
const prerequisite = query.data;
|
|
if (!prerequisite) return null;
|
|
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"rounded-lg border p-3 text-left sm:p-4",
|
|
prerequisite.available
|
|
? "border-primary/25 bg-primary/[0.055]"
|
|
: "border-amber-500/30 bg-amber-500/5",
|
|
)}
|
|
data-testid="onboarding-git-bash"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{prerequisite.available ? (
|
|
<Check className="h-4 w-4 text-primary" />
|
|
) : (
|
|
<AlertTriangle className="h-4 w-4 text-warning" />
|
|
)}
|
|
<h2 className="text-base font-medium">Git Bash</h2>
|
|
{prerequisite.available ? (
|
|
<Badge
|
|
className="border border-primary/20 bg-primary/10 text-primary"
|
|
variant="outline"
|
|
>
|
|
Installed
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
{prerequisite.available ? (
|
|
<p className="mt-2 break-all font-mono text-xs text-muted-foreground">
|
|
{prerequisite.path}
|
|
</p>
|
|
) : (
|
|
<>
|
|
<p className="mt-2 text-sm text-muted-foreground">
|
|
Required for buzz-agent shell tools on Windows.
|
|
</p>
|
|
<p className="mt-1 text-xs text-muted-foreground/80">
|
|
{prerequisite.installHint}
|
|
</p>
|
|
<Button
|
|
className="mt-3"
|
|
onClick={() => void openUrl(prerequisite.installInstructionsUrl)}
|
|
size="sm"
|
|
type="button"
|
|
variant="outline"
|
|
>
|
|
<ExternalLink className="h-4 w-4" /> Install Git for Windows
|
|
</Button>
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function RuntimeProvidersSection({
|
|
runtimeProviders,
|
|
}: {
|
|
runtimeProviders: SetupStepState["runtimeProviders"];
|
|
}) {
|
|
const { errorMessage, isChecking, items } = runtimeProviders;
|
|
const installMutation = useInstallAcpRuntimeMutation();
|
|
const [installResults, setInstallResults] = React.useState<
|
|
Record<string, InstallResultState>
|
|
>({});
|
|
|
|
function handleInstall(runtimeId: string) {
|
|
setInstallResults((current) => ({
|
|
...current,
|
|
[runtimeId]: { error: null, success: false },
|
|
}));
|
|
|
|
installMutation.mutate(runtimeId, {
|
|
onSuccess: (result) => {
|
|
setInstallResults((current) => ({
|
|
...current,
|
|
[runtimeId]: result.success
|
|
? { error: null, success: true }
|
|
: { error: getInstallErrorMessage(result.steps), success: false },
|
|
}));
|
|
},
|
|
onError: (error) => {
|
|
setInstallResults((current) => ({
|
|
...current,
|
|
[runtimeId]: {
|
|
error: error instanceof Error ? error.message : "Install failed.",
|
|
success: false,
|
|
},
|
|
}));
|
|
},
|
|
});
|
|
}
|
|
|
|
return (
|
|
<section className="space-y-4">
|
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
<div className="space-y-1">
|
|
<h1 className="text-3xl font-semibold tracking-tight text-foreground">
|
|
Agent harnesses
|
|
</h1>
|
|
<p className="max-w-2xl text-sm leading-6 text-muted-foreground">
|
|
Buzz can launch local ACP-compatible agent harnesses. Install or
|
|
verify the runtimes this desktop app can see.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<GitBashPrerequisiteCard />
|
|
|
|
{items.length > 0 ? (
|
|
<div className="grid gap-3 lg:grid-cols-2">
|
|
{items.map((runtime) => (
|
|
<RuntimeCard
|
|
installError={installResults[runtime.id]?.error ?? null}
|
|
installSuccess={installResults[runtime.id]?.success ?? false}
|
|
isInstalling={
|
|
installMutation.isPending &&
|
|
installMutation.variables === runtime.id
|
|
}
|
|
key={runtime.id}
|
|
onInstall={() => handleInstall(runtime.id)}
|
|
runtime={runtime}
|
|
/>
|
|
))}
|
|
</div>
|
|
) : isChecking ? (
|
|
<div className="rounded-lg border border-border/70 bg-background px-4 py-6 text-sm text-muted-foreground">
|
|
Looking for compatible runtimes...
|
|
</div>
|
|
) : errorMessage ? null : (
|
|
<p
|
|
className="rounded-lg border border-border/70 bg-background px-4 py-6 text-sm text-muted-foreground"
|
|
data-testid="onboarding-acp-empty"
|
|
>
|
|
No compatible ACP runtimes detected yet. You can finish setup now and
|
|
come back later in Settings > Doctor.
|
|
</p>
|
|
)}
|
|
|
|
{errorMessage ? (
|
|
<p className="rounded-lg border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
{errorMessage}
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function SetupStepContent({
|
|
actions,
|
|
direction,
|
|
state,
|
|
}: SetupStepContentProps) {
|
|
const { runtimeProviders } = state;
|
|
|
|
return (
|
|
<OnboardingSlideTransition
|
|
className="space-y-7 text-left"
|
|
data-testid="onboarding-page-2"
|
|
direction={direction}
|
|
transitionKey={`setup-${direction}`}
|
|
>
|
|
<RuntimeProvidersSection runtimeProviders={runtimeProviders} />
|
|
|
|
<AgentDefaultsSection />
|
|
|
|
<div className="mx-auto flex w-full max-w-md flex-col gap-3">
|
|
<Button
|
|
className="h-10 w-full"
|
|
data-testid="onboarding-finish"
|
|
onClick={actions.complete}
|
|
type="button"
|
|
>
|
|
Finish
|
|
</Button>
|
|
|
|
<Button
|
|
className="h-10 w-full text-muted-foreground hover:text-accent-foreground"
|
|
data-testid="onboarding-back"
|
|
onClick={actions.back}
|
|
type="button"
|
|
variant="ghost"
|
|
>
|
|
Back
|
|
</Button>
|
|
</div>
|
|
</OnboardingSlideTransition>
|
|
);
|
|
}
|
|
|
|
export function SetupStep({ actions, direction }: SetupStepProps) {
|
|
const state = useSetupStepState();
|
|
|
|
return (
|
|
<SetupStepContent actions={actions} direction={direction} state={state} />
|
|
);
|
|
}
|