mirror of
https://github.com/block/buzz.git
synced 2026-11-09 02:41:57 +00:00
Prototype alternate AI client onboarding paths
Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -18,7 +18,9 @@ export type OnboardingPreviewPage =
|
||||
| "identity-key-help"
|
||||
| "backup-password"
|
||||
| "setup"
|
||||
| "harness-connection-method"
|
||||
| "harness-connection"
|
||||
| "harness-connection-help"
|
||||
| "harness-connection-detail"
|
||||
| "config"
|
||||
| "community-choice"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { openUrl } from "@tauri-apps/plugin-opener";
|
||||
import { ChevronRight, CreditCard, ExternalLink, KeyRound } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { AgentDropdownSelect } from "@/features/agents/ui/agentConfigControls";
|
||||
@@ -42,11 +43,13 @@ const CONNECTION_METHOD_OPTIONS = [
|
||||
function previewRuntime({
|
||||
available = false,
|
||||
id,
|
||||
installInstructionsUrl = "https://github.com/block/buzz",
|
||||
label,
|
||||
source = "preset",
|
||||
}: {
|
||||
available?: boolean;
|
||||
id: string;
|
||||
installInstructionsUrl?: string;
|
||||
label: string;
|
||||
source?: AcpRuntimeCatalogEntry["source"];
|
||||
}): AcpRuntimeCatalogEntry {
|
||||
@@ -63,7 +66,7 @@ function previewRuntime({
|
||||
defaultArgs: [],
|
||||
id,
|
||||
installHint: `Install ${label} to connect it to Buzz.`,
|
||||
installInstructionsUrl: "https://github.com/block/buzz",
|
||||
installInstructionsUrl,
|
||||
label,
|
||||
loginHint: null,
|
||||
maxRoundsEnvVar: null,
|
||||
@@ -82,23 +85,31 @@ function previewRuntime({
|
||||
/** Fixed workshop catalog. Availability changes only in React memory. */
|
||||
export const HARNESS_CONNECTION_OPTIONS: readonly HarnessConnectionOption[] = [
|
||||
{
|
||||
methods: ["subscription", "api"],
|
||||
methods: ["subscription"],
|
||||
runtime: previewRuntime({
|
||||
available: true,
|
||||
id: "claude",
|
||||
installInstructionsUrl: "https://code.claude.com/docs/en/getting-started",
|
||||
label: "Claude Code",
|
||||
source: "builtin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["subscription", "api"],
|
||||
runtime: previewRuntime({ id: "codex", label: "Codex", source: "builtin" }),
|
||||
methods: ["subscription"],
|
||||
runtime: previewRuntime({
|
||||
id: "codex",
|
||||
installInstructionsUrl: "https://developers.openai.com/codex/cli/",
|
||||
label: "Codex",
|
||||
source: "builtin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["api"],
|
||||
runtime: previewRuntime({
|
||||
available: true,
|
||||
id: "goose",
|
||||
installInstructionsUrl:
|
||||
"https://goose-docs.ai/docs/getting-started/installation/",
|
||||
label: "Goose",
|
||||
source: "builtin",
|
||||
}),
|
||||
@@ -114,39 +125,75 @@ export const HARNESS_CONNECTION_OPTIONS: readonly HarnessConnectionOption[] = [
|
||||
},
|
||||
{
|
||||
methods: ["subscription"],
|
||||
runtime: previewRuntime({ id: "cursor", label: "Cursor" }),
|
||||
runtime: previewRuntime({
|
||||
id: "cursor",
|
||||
installInstructionsUrl: "https://cursor.com/downloads",
|
||||
label: "Cursor",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["subscription"],
|
||||
runtime: previewRuntime({ id: "devin", label: "Devin" }),
|
||||
runtime: previewRuntime({
|
||||
id: "devin",
|
||||
installInstructionsUrl: "https://docs.devin.ai/cli",
|
||||
label: "Devin",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["api"],
|
||||
runtime: previewRuntime({ id: "omp", label: "Oh My Pi" }),
|
||||
runtime: previewRuntime({
|
||||
id: "omp",
|
||||
installInstructionsUrl: "https://omp.sh/",
|
||||
label: "Oh My Pi",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["api"],
|
||||
runtime: previewRuntime({ id: "grok", label: "Grok Build" }),
|
||||
runtime: previewRuntime({
|
||||
id: "grok",
|
||||
installInstructionsUrl: "https://build.x.ai/docs",
|
||||
label: "Grok Build",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["api"],
|
||||
runtime: previewRuntime({ id: "opencode", label: "OpenCode" }),
|
||||
runtime: previewRuntime({
|
||||
id: "opencode",
|
||||
installInstructionsUrl: "https://opencode.ai/docs",
|
||||
label: "OpenCode",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["api"],
|
||||
runtime: previewRuntime({ id: "kimi", label: "Kimi Code" }),
|
||||
runtime: previewRuntime({
|
||||
id: "kimi",
|
||||
installInstructionsUrl: "https://kimi.ai/download",
|
||||
label: "Kimi Code",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["subscription"],
|
||||
runtime: previewRuntime({ id: "amp", label: "Amp" }),
|
||||
runtime: previewRuntime({
|
||||
id: "amp",
|
||||
installInstructionsUrl: "https://github.com/tao12345666333/amp-acp",
|
||||
label: "Amp",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["api"],
|
||||
runtime: previewRuntime({ id: "hermes", label: "Hermes Agent" }),
|
||||
runtime: previewRuntime({
|
||||
id: "hermes",
|
||||
installInstructionsUrl: "https://hermes-agent.nousresearch.com",
|
||||
label: "Hermes Agent",
|
||||
}),
|
||||
},
|
||||
{
|
||||
methods: ["api"],
|
||||
runtime: previewRuntime({ id: "openclaw", label: "OpenClaw" }),
|
||||
runtime: previewRuntime({
|
||||
id: "openclaw",
|
||||
installInstructionsUrl: "https://docs.openclaw.ai/start/getting-started",
|
||||
label: "OpenClaw",
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -157,12 +204,12 @@ export const HARNESS_CONNECTION_OPTIONS: readonly HarnessConnectionOption[] = [
|
||||
*/
|
||||
export function HarnessConnectionList({
|
||||
installedIds,
|
||||
onInstall,
|
||||
method,
|
||||
onSelect,
|
||||
options,
|
||||
}: {
|
||||
installedIds: ReadonlySet<string>;
|
||||
onInstall: (option: HarnessConnectionOption) => void;
|
||||
method?: HarnessConnectionMethod;
|
||||
onSelect: (option: HarnessConnectionOption) => void;
|
||||
options: readonly HarnessConnectionOption[];
|
||||
}) {
|
||||
@@ -223,27 +270,56 @@ export function HarnessConnectionList({
|
||||
onScroll={updateScrollEdges}
|
||||
ref={scrollRef}
|
||||
>
|
||||
{orderedOptions.map((option) => {
|
||||
{orderedOptions.map((option, index) => {
|
||||
const { runtime } = option;
|
||||
const installed = installedIds.has(runtime.id);
|
||||
const previousOption = orderedOptions[index - 1];
|
||||
const startsNotInstalledSection =
|
||||
!installed &&
|
||||
(previousOption === undefined ||
|
||||
installedIds.has(previousOption.runtime.id));
|
||||
const label = getRuntimeDisplayLabel(runtime);
|
||||
const isEasiestApiOption =
|
||||
method === "api" && runtime.id === "buzz-agent";
|
||||
const rowClassName =
|
||||
"group flex min-h-14 w-full items-center gap-3 rounded-xl px-2 py-2 text-left text-sm font-medium text-foreground transition-colors duration-150 ease-out hover:bg-foreground/[0.04] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-foreground/20 motion-reduce:transition-none";
|
||||
const contents = (
|
||||
<>
|
||||
<span className="flex size-10 shrink-0 items-center justify-center">
|
||||
<span className="flex size-10 shrink-0 items-center justify-start">
|
||||
<RuntimeIcon className="size-9" runtime={runtime} />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
<span className="flex min-w-0 flex-1 items-center gap-2">
|
||||
<span className="truncate">{label}</span>
|
||||
{isEasiestApiOption ? (
|
||||
<>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="shrink-0 text-muted-foreground/50"
|
||||
>
|
||||
·
|
||||
</span>
|
||||
<span className="inline-flex shrink-0 items-center rounded-md bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground">
|
||||
Easiest
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
|
||||
if (installed) {
|
||||
return (
|
||||
return (
|
||||
<React.Fragment key={runtime.id}>
|
||||
{startsNotInstalledSection ? (
|
||||
<p
|
||||
className="px-2 pb-1 pt-4 text-xs font-medium text-muted-foreground"
|
||||
data-testid="onboarding-preview-harness-not-installed"
|
||||
>
|
||||
Not installed
|
||||
</p>
|
||||
) : null}
|
||||
<button
|
||||
className={rowClassName}
|
||||
data-testid={`onboarding-preview-harness-${runtime.id}`}
|
||||
key={runtime.id}
|
||||
onClick={() => onSelect(option)}
|
||||
type="button"
|
||||
>
|
||||
@@ -251,27 +327,11 @@ export function HarnessConnectionList({
|
||||
<span className="flex size-10 shrink-0 items-center justify-center">
|
||||
<ChevronRight
|
||||
aria-hidden="true"
|
||||
className="size-4 text-muted-foreground transition-[color,transform] duration-150 ease-out group-hover:translate-x-0.5 group-hover:text-foreground motion-reduce:transition-none"
|
||||
className="size-4 text-muted-foreground transition-colors duration-150 ease-out group-hover:text-foreground motion-reduce:transition-none"
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={rowClassName} key={runtime.id}>
|
||||
{contents}
|
||||
<Button
|
||||
aria-label={`Install ${label}`}
|
||||
className="ml-auto h-7 shrink-0 rounded-full bg-foreground px-3 text-xs text-background shadow-none hover:bg-foreground/85"
|
||||
data-testid={`onboarding-preview-harness-install-${runtime.id}`}
|
||||
onClick={() => onInstall(option)}
|
||||
size="xs"
|
||||
type="button"
|
||||
>
|
||||
Install
|
||||
</Button>
|
||||
</div>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -281,17 +341,25 @@ export function HarnessConnectionList({
|
||||
|
||||
export function HarnessConnectionPreview({
|
||||
installedIds,
|
||||
method,
|
||||
onBack,
|
||||
onInstall,
|
||||
onHelp,
|
||||
onSelect,
|
||||
total,
|
||||
}: {
|
||||
installedIds: ReadonlySet<string>;
|
||||
method?: HarnessConnectionMethod;
|
||||
onBack: () => void;
|
||||
onInstall: (option: HarnessConnectionOption) => void;
|
||||
onHelp?: () => void;
|
||||
onSelect: (option: HarnessConnectionOption) => void;
|
||||
total: number;
|
||||
}) {
|
||||
const options = method
|
||||
? HARNESS_CONNECTION_OPTIONS.filter(({ methods }) =>
|
||||
methods.includes(method),
|
||||
)
|
||||
: HARNESS_CONNECTION_OPTIONS;
|
||||
|
||||
return (
|
||||
<OnboardingPreviewStep
|
||||
current={3}
|
||||
@@ -306,19 +374,36 @@ export function HarnessConnectionPreview({
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<h1 className="text-title font-normal text-foreground">
|
||||
Connect your AI client
|
||||
{method === "subscription"
|
||||
? "Continue with an AI subscription"
|
||||
: method === "api"
|
||||
? "Continue with an AI API key"
|
||||
: "Connect your AI client"}
|
||||
</h1>
|
||||
<p className="mt-2 text-base leading-6 text-foreground/80">
|
||||
Choose which AI client your agents will use. You can change this
|
||||
anytime.
|
||||
</p>
|
||||
{!method ? (
|
||||
<p className="mt-2 text-base leading-6 text-foreground/80">
|
||||
Choose which AI client your agents will use. You can change this
|
||||
anytime.{" "}
|
||||
{onHelp ? (
|
||||
<Button
|
||||
className="inline-flex h-auto p-0 align-baseline text-base leading-6 text-foreground underline decoration-foreground/45 underline-offset-4 hover:decoration-foreground"
|
||||
data-testid="onboarding-preview-harness-help-trigger"
|
||||
onClick={onHelp}
|
||||
type="button"
|
||||
variant="link"
|
||||
>
|
||||
Need help choosing?
|
||||
</Button>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-6 flex min-h-0 flex-1 flex-col">
|
||||
<HarnessConnectionList
|
||||
installedIds={installedIds}
|
||||
onInstall={onInstall}
|
||||
method={method}
|
||||
onSelect={onSelect}
|
||||
options={HARNESS_CONNECTION_OPTIONS}
|
||||
options={options}
|
||||
/>
|
||||
</div>
|
||||
</OnboardingSlideTransition>
|
||||
@@ -326,6 +411,178 @@ export function HarnessConnectionPreview({
|
||||
);
|
||||
}
|
||||
|
||||
const CONNECTION_METHOD_CHOICES = [
|
||||
{
|
||||
icon: CreditCard,
|
||||
label: "I use a subscription",
|
||||
method: "subscription",
|
||||
},
|
||||
{
|
||||
icon: KeyRound,
|
||||
label: "I have an API key",
|
||||
method: "api",
|
||||
},
|
||||
] as const satisfies ReadonlyArray<{
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
method: HarnessConnectionMethod;
|
||||
}>;
|
||||
|
||||
export function HarnessConnectionMethodPreview({
|
||||
onBack,
|
||||
onSelect,
|
||||
total,
|
||||
}: {
|
||||
onBack: () => void;
|
||||
onSelect: (method: HarnessConnectionMethod) => void;
|
||||
total: number;
|
||||
}) {
|
||||
return (
|
||||
<OnboardingPreviewStep
|
||||
current={3}
|
||||
onBack={onBack}
|
||||
testId="onboarding-preview-harness-method"
|
||||
total={total}
|
||||
>
|
||||
<OnboardingSlideTransition
|
||||
className="flex min-h-0 flex-1 flex-col"
|
||||
containerClassName="min-h-0 flex-1"
|
||||
transitionKey="preview-harness-method"
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<h1 className="text-title font-normal text-foreground">
|
||||
How do you use AI?
|
||||
</h1>
|
||||
<p className="mt-2 text-base leading-6 text-foreground/80">
|
||||
Choose the option that matches how you access AI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="-mx-2 mt-6 flex w-[calc(100%+1rem)] flex-1 flex-col gap-2">
|
||||
{CONNECTION_METHOD_CHOICES.map(
|
||||
({ icon: MethodIcon, label, method }) => (
|
||||
<Button
|
||||
className="group h-auto min-h-14 w-full justify-start gap-3 rounded-xl px-2 py-2 text-left text-sm text-foreground shadow-none hover:bg-foreground/[0.04] hover:text-foreground focus-visible:ring-2 focus-visible:ring-foreground/20"
|
||||
data-testid={`onboarding-preview-harness-method-${method}`}
|
||||
key={method}
|
||||
onClick={() => onSelect(method)}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-start">
|
||||
<MethodIcon aria-hidden className="!size-6" />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-medium">{label}</span>
|
||||
</span>
|
||||
<span className="ml-auto flex size-10 shrink-0 items-center justify-center">
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className="size-4 text-muted-foreground transition-colors duration-150 ease-out group-hover:text-foreground motion-reduce:transition-none"
|
||||
/>
|
||||
</span>
|
||||
</Button>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</OnboardingSlideTransition>
|
||||
</OnboardingPreviewStep>
|
||||
);
|
||||
}
|
||||
|
||||
function HarnessCompatibilityList({
|
||||
method,
|
||||
}: {
|
||||
method: HarnessConnectionMethod;
|
||||
}) {
|
||||
const options = HARNESS_CONNECTION_OPTIONS.filter(({ methods }) =>
|
||||
methods.includes(method),
|
||||
);
|
||||
|
||||
return (
|
||||
<ul
|
||||
className="mt-4 grid grid-cols-2 gap-x-5 gap-y-3"
|
||||
data-testid={`onboarding-preview-harness-help-${method}-list`}
|
||||
>
|
||||
{options.map(({ runtime }) => (
|
||||
<li className="flex min-w-0 items-center gap-2" key={runtime.id}>
|
||||
<span className="flex size-7 shrink-0 items-center justify-center">
|
||||
<RuntimeIcon className="size-6" runtime={runtime} />
|
||||
</span>
|
||||
<span className="truncate text-sm font-medium text-foreground">
|
||||
{getRuntimeDisplayLabel(runtime)}
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function HarnessConnectionHelpPreview({
|
||||
onBack,
|
||||
total,
|
||||
}: {
|
||||
onBack: () => void;
|
||||
total: number;
|
||||
}) {
|
||||
return (
|
||||
<OnboardingPreviewStep
|
||||
current={3}
|
||||
onBack={onBack}
|
||||
testId="onboarding-preview-harness-help"
|
||||
total={total}
|
||||
>
|
||||
<OnboardingSlideTransition
|
||||
className="flex min-h-0 w-full max-w-[500px] flex-1 flex-col items-stretch text-left"
|
||||
containerClassName="min-h-0 flex-1"
|
||||
transitionKey="preview-harness-help"
|
||||
>
|
||||
<div className="shrink-0">
|
||||
<h1 className="text-title font-normal text-foreground">
|
||||
Choose how to connect
|
||||
</h1>
|
||||
<p className="mt-2 text-base leading-6 text-foreground/80">
|
||||
Connect an AI client using a subscription or API key.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 min-h-0 flex-1 space-y-7 overflow-y-auto pb-4 pr-1">
|
||||
<section aria-labelledby="harness-subscription-heading">
|
||||
<h2
|
||||
className="text-base font-medium text-foreground"
|
||||
id="harness-subscription-heading"
|
||||
>
|
||||
I use a subscription
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-foreground/75">
|
||||
Choose this if you sign in to a paid AI product, such as ChatGPT
|
||||
or Claude.
|
||||
</p>
|
||||
<HarnessCompatibilityList method="subscription" />
|
||||
</section>
|
||||
|
||||
<section
|
||||
aria-labelledby="harness-api-heading"
|
||||
className="border-t border-[#e2e2e2] pt-7"
|
||||
>
|
||||
<h2
|
||||
className="text-base font-medium text-foreground"
|
||||
id="harness-api-heading"
|
||||
>
|
||||
I use an API key
|
||||
</h2>
|
||||
<p className="mt-2 text-sm leading-6 text-foreground/75">
|
||||
Choose this if you have an API key from an AI provider or your
|
||||
team.
|
||||
</p>
|
||||
<HarnessCompatibilityList method="api" />
|
||||
</section>
|
||||
</div>
|
||||
</OnboardingSlideTransition>
|
||||
</OnboardingPreviewStep>
|
||||
);
|
||||
}
|
||||
|
||||
const API_PROVIDER_OPTIONS = PERSONA_LLM_PROVIDER_OPTIONS.map((provider) => ({
|
||||
label: provider.label,
|
||||
value: provider.id,
|
||||
@@ -344,6 +601,15 @@ function defaultApiProvider(runtimeId: string) {
|
||||
return "anthropic";
|
||||
}
|
||||
|
||||
export function runtimeNeedsOnboardingConnection(
|
||||
method: HarnessConnectionMethod,
|
||||
runtimeId: string,
|
||||
) {
|
||||
return (
|
||||
method === "subscription" || runtimeSupportsLlmProviderSelection(runtimeId)
|
||||
);
|
||||
}
|
||||
|
||||
function apiKeyLabel(provider: string) {
|
||||
return (
|
||||
getProviderApiKeyLabel(provider)?.replace("API Key", "API key") ?? "API key"
|
||||
@@ -433,15 +699,21 @@ function ConnectedConfiguration({
|
||||
}
|
||||
|
||||
export function HarnessConnectionDetailPreview({
|
||||
installed,
|
||||
lockMethod = false,
|
||||
method,
|
||||
onBack,
|
||||
onCheckAgain,
|
||||
onContinue,
|
||||
onMethodChange,
|
||||
option,
|
||||
total,
|
||||
}: {
|
||||
installed: boolean;
|
||||
lockMethod?: boolean;
|
||||
method: HarnessConnectionMethod;
|
||||
onBack: () => void;
|
||||
onCheckAgain: () => void;
|
||||
onContinue: () => void;
|
||||
onMethodChange: (method: HarnessConnectionMethod) => void;
|
||||
option: HarnessConnectionOption;
|
||||
@@ -449,7 +721,7 @@ export function HarnessConnectionDetailPreview({
|
||||
}) {
|
||||
const cardLayout = useOnboardingPreviewCardLayout();
|
||||
const label = getRuntimeDisplayLabel(option.runtime);
|
||||
const hasMethodChoice = option.methods.length > 1;
|
||||
const hasMethodChoice = option.methods.length > 1 && !lockMethod;
|
||||
const [provider, setProvider] = React.useState(() =>
|
||||
defaultApiProvider(option.runtime.id),
|
||||
);
|
||||
@@ -496,6 +768,70 @@ export function HarnessConnectionDetailPreview({
|
||||
if (canConnect) setConnected(true);
|
||||
}
|
||||
|
||||
if (!installed) {
|
||||
return (
|
||||
<OnboardingPreviewStep
|
||||
current={3}
|
||||
onBack={onBack}
|
||||
testId="onboarding-preview-harness-setup-guide"
|
||||
total={total}
|
||||
>
|
||||
<OnboardingSlideTransition
|
||||
className={cn(
|
||||
"flex min-h-0 w-full max-w-[500px] flex-1 flex-col",
|
||||
cardLayout ? "items-stretch" : "items-center",
|
||||
)}
|
||||
containerClassName="min-h-0 flex-1"
|
||||
transitionKey={`preview-harness-setup-${option.runtime.id}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex size-10 shrink-0 items-center justify-center">
|
||||
<RuntimeIcon className="size-9" runtime={option.runtime} />
|
||||
</span>
|
||||
<h1 className="text-title font-normal text-foreground">
|
||||
Set up {label}
|
||||
</h1>
|
||||
</div>
|
||||
<p className="mt-2 max-w-[440px] text-base leading-6 text-foreground/80">
|
||||
Follow the setup guide to install {label}. When you’re done, come
|
||||
back and check again.
|
||||
</p>
|
||||
|
||||
<div className="mt-8 flex min-h-0 flex-1 flex-col">
|
||||
<div className="rounded-xl bg-[#e2e2e2]/30 px-4 py-4">
|
||||
<p className="text-sm font-medium text-foreground">
|
||||
CLI not detected
|
||||
</p>
|
||||
<Button
|
||||
className="mt-1 h-auto justify-start p-0 text-sm text-foreground underline decoration-foreground/45 underline-offset-4 hover:decoration-foreground"
|
||||
data-testid="onboarding-preview-harness-open-setup-guide"
|
||||
onClick={() =>
|
||||
void openUrl(option.runtime.installInstructionsUrl)
|
||||
}
|
||||
type="button"
|
||||
variant="link"
|
||||
>
|
||||
Open setup guide
|
||||
<ExternalLink aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<OnboardingFooter>
|
||||
<Button
|
||||
className={ONBOARDING_PRIMARY_CTA_CLASS}
|
||||
data-testid="onboarding-preview-harness-check-again"
|
||||
onClick={onCheckAgain}
|
||||
type="button"
|
||||
>
|
||||
Check again
|
||||
</Button>
|
||||
</OnboardingFooter>
|
||||
</OnboardingSlideTransition>
|
||||
</OnboardingPreviewStep>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<OnboardingPreviewStep
|
||||
current={3}
|
||||
|
||||
@@ -19,8 +19,11 @@ import {
|
||||
import {
|
||||
HARNESS_CONNECTION_OPTIONS,
|
||||
HarnessConnectionDetailPreview,
|
||||
HarnessConnectionHelpPreview,
|
||||
HarnessConnectionMethodPreview,
|
||||
type HarnessConnectionMethod,
|
||||
HarnessConnectionPreview,
|
||||
runtimeNeedsOnboardingConnection,
|
||||
} from "./HarnessConnectionStep";
|
||||
import { JoinPolicyNotice } from "./JoinPolicyNotice";
|
||||
import { KeySignInPreview } from "./KeySignInPreview";
|
||||
@@ -383,6 +386,10 @@ export function OnboardingPreviewApp() {
|
||||
React.useState<HarnessConnectionMethod>("subscription");
|
||||
const [harnessConnectionInOnboarding, setHarnessConnectionInOnboarding] =
|
||||
React.useState(true);
|
||||
const [
|
||||
chooseHarnessConnectionMethodFirst,
|
||||
setChooseHarnessConnectionMethodFirst,
|
||||
] = React.useState(false);
|
||||
const [setupBackPage, setSetupBackPage] =
|
||||
React.useState<OnboardingPreviewPage>("landing");
|
||||
const [communityRoute, setCommunityRoute] =
|
||||
@@ -442,9 +449,20 @@ export function OnboardingPreviewApp() {
|
||||
const continueFromAccount = React.useCallback(
|
||||
(backPage: OnboardingPreviewPage) => {
|
||||
setSetupBackPage(backPage);
|
||||
setPage(journey.afterAccount);
|
||||
setPage(
|
||||
variant === "v3" &&
|
||||
harnessConnectionInOnboarding &&
|
||||
chooseHarnessConnectionMethodFirst
|
||||
? "harness-connection-method"
|
||||
: journey.afterAccount,
|
||||
);
|
||||
},
|
||||
[journey.afterAccount],
|
||||
[
|
||||
chooseHarnessConnectionMethodFirst,
|
||||
harnessConnectionInOnboarding,
|
||||
journey.afterAccount,
|
||||
variant,
|
||||
],
|
||||
);
|
||||
|
||||
let content: React.ReactNode;
|
||||
@@ -574,32 +592,81 @@ export function OnboardingPreviewApp() {
|
||||
onNext={() => setPage("config")}
|
||||
/>
|
||||
);
|
||||
} else if (page === "harness-connection-method") {
|
||||
content = (
|
||||
<HarnessConnectionMethodPreview
|
||||
onBack={() => setPage(setupBackPage)}
|
||||
onSelect={(method) => {
|
||||
setHarnessConnectionMethod(method);
|
||||
setPage("harness-connection");
|
||||
}}
|
||||
total={journey.totalSteps}
|
||||
/>
|
||||
);
|
||||
} else if (page === "harness-connection") {
|
||||
content = (
|
||||
<HarnessConnectionPreview
|
||||
installedIds={installedHarnessIds}
|
||||
onBack={() => setPage(setupBackPage)}
|
||||
onInstall={(option) => {
|
||||
setInstalledHarnessIds((current) =>
|
||||
new Set(current).add(option.runtime.id),
|
||||
);
|
||||
setSelectedHarnessId(option.runtime.id);
|
||||
setHarnessConnectionMethod(option.methods[0] ?? "api");
|
||||
setPage("harness-connection-detail");
|
||||
}}
|
||||
method={
|
||||
chooseHarnessConnectionMethodFirst
|
||||
? harnessConnectionMethod
|
||||
: undefined
|
||||
}
|
||||
onBack={() =>
|
||||
setPage(
|
||||
chooseHarnessConnectionMethodFirst
|
||||
? "harness-connection-method"
|
||||
: setupBackPage,
|
||||
)
|
||||
}
|
||||
onHelp={
|
||||
chooseHarnessConnectionMethodFirst
|
||||
? undefined
|
||||
: () => setPage("harness-connection-help")
|
||||
}
|
||||
onSelect={(option) => {
|
||||
setSelectedHarnessId(option.runtime.id);
|
||||
setHarnessConnectionMethod(option.methods[0] ?? "api");
|
||||
setPage("harness-connection-detail");
|
||||
const nextMethod = chooseHarnessConnectionMethodFirst
|
||||
? harnessConnectionMethod
|
||||
: (option.methods[0] ?? "api");
|
||||
setHarnessConnectionMethod(nextMethod);
|
||||
setPage(
|
||||
installedHarnessIds.has(option.runtime.id) &&
|
||||
!runtimeNeedsOnboardingConnection(nextMethod, option.runtime.id)
|
||||
? "community-choice"
|
||||
: "harness-connection-detail",
|
||||
);
|
||||
}}
|
||||
total={journey.totalSteps}
|
||||
/>
|
||||
);
|
||||
} else if (page === "harness-connection-help") {
|
||||
content = (
|
||||
<HarnessConnectionHelpPreview
|
||||
onBack={() => setPage("harness-connection")}
|
||||
total={journey.totalSteps}
|
||||
/>
|
||||
);
|
||||
} else if (page === "harness-connection-detail") {
|
||||
content = (
|
||||
<HarnessConnectionDetailPreview
|
||||
installed={installedHarnessIds.has(selectedHarness.runtime.id)}
|
||||
lockMethod={chooseHarnessConnectionMethodFirst}
|
||||
method={harnessConnectionMethod}
|
||||
onBack={() => setPage("harness-connection")}
|
||||
onCheckAgain={() => {
|
||||
setInstalledHarnessIds((current) =>
|
||||
new Set(current).add(selectedHarness.runtime.id),
|
||||
);
|
||||
if (
|
||||
!runtimeNeedsOnboardingConnection(
|
||||
harnessConnectionMethod,
|
||||
selectedHarness.runtime.id,
|
||||
)
|
||||
) {
|
||||
setPage("community-choice");
|
||||
}
|
||||
}}
|
||||
onContinue={() => setPage("community-choice")}
|
||||
onMethodChange={setHarnessConnectionMethod}
|
||||
option={selectedHarness}
|
||||
@@ -700,7 +767,12 @@ export function OnboardingPreviewApp() {
|
||||
return (
|
||||
<>
|
||||
<OnboardingPreviewControls
|
||||
chooseHarnessConnectionMethodFirst={chooseHarnessConnectionMethodFirst}
|
||||
harnessConnectionInOnboarding={harnessConnectionInOnboarding}
|
||||
onChooseHarnessConnectionMethodFirstChange={(enabled) => {
|
||||
setChooseHarnessConnectionMethodFirst(enabled);
|
||||
restart();
|
||||
}}
|
||||
onHarnessConnectionInOnboardingChange={(included) => {
|
||||
setHarnessConnectionInOnboarding(included);
|
||||
restart();
|
||||
|
||||
@@ -11,13 +11,17 @@ const PREVIEW_VARIANT_OPTIONS = [
|
||||
] as const;
|
||||
|
||||
export function OnboardingPreviewControls({
|
||||
chooseHarnessConnectionMethodFirst,
|
||||
harnessConnectionInOnboarding,
|
||||
onChooseHarnessConnectionMethodFirstChange,
|
||||
onHarnessConnectionInOnboardingChange,
|
||||
onRestart,
|
||||
onVariantChange,
|
||||
variant,
|
||||
}: {
|
||||
chooseHarnessConnectionMethodFirst: boolean;
|
||||
harnessConnectionInOnboarding: boolean;
|
||||
onChooseHarnessConnectionMethodFirstChange: (enabled: boolean) => void;
|
||||
onHarnessConnectionInOnboardingChange: (included: boolean) => void;
|
||||
onRestart: () => void;
|
||||
onVariantChange: (variant: OnboardingPreviewVariant) => void;
|
||||
@@ -62,20 +66,38 @@ export function OnboardingPreviewControls({
|
||||
value={variant}
|
||||
/>
|
||||
{variant === "v3" ? (
|
||||
<label
|
||||
className="flex cursor-pointer items-center gap-2 text-xs font-medium text-foreground"
|
||||
htmlFor="onboarding-preview-harness-placement"
|
||||
>
|
||||
<Checkbox
|
||||
checked={harnessConnectionInOnboarding}
|
||||
className="border-foreground data-[state=checked]:bg-foreground data-[state=checked]:text-background"
|
||||
id="onboarding-preview-harness-placement"
|
||||
onCheckedChange={(checked) =>
|
||||
onHarnessConnectionInOnboardingChange(checked === true)
|
||||
}
|
||||
/>
|
||||
<span>Harness connection in onboarding</span>
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<label
|
||||
className="flex cursor-pointer items-center gap-2 text-xs font-medium text-foreground"
|
||||
htmlFor="onboarding-preview-harness-placement"
|
||||
>
|
||||
<Checkbox
|
||||
checked={harnessConnectionInOnboarding}
|
||||
className="border-foreground data-[state=checked]:bg-foreground data-[state=checked]:text-background"
|
||||
id="onboarding-preview-harness-placement"
|
||||
onCheckedChange={(checked) =>
|
||||
onHarnessConnectionInOnboardingChange(checked === true)
|
||||
}
|
||||
/>
|
||||
<span>Harness connection in onboarding</span>
|
||||
</label>
|
||||
{harnessConnectionInOnboarding ? (
|
||||
<label
|
||||
className="flex cursor-pointer items-center gap-2 pl-6 text-xs font-medium text-foreground"
|
||||
htmlFor="onboarding-preview-harness-method-first"
|
||||
>
|
||||
<Checkbox
|
||||
checked={chooseHarnessConnectionMethodFirst}
|
||||
className="border-foreground data-[state=checked]:bg-foreground data-[state=checked]:text-background"
|
||||
id="onboarding-preview-harness-method-first"
|
||||
onCheckedChange={(checked) =>
|
||||
onChooseHarnessConnectionMethodFirstChange(checked === true)
|
||||
}
|
||||
/>
|
||||
<span>Choose subscription or API first</span>
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
</aside>
|
||||
);
|
||||
|
||||
@@ -347,18 +347,18 @@ export function CommunityChoicePreview({
|
||||
|
||||
return cardLayout ? (
|
||||
<Button
|
||||
className="group h-12 w-full justify-start gap-0 rounded-xl px-2 py-2 text-left text-sm font-medium text-foreground shadow-none hover:bg-foreground/[0.04] hover:text-foreground focus-visible:ring-2 focus-visible:ring-foreground/20"
|
||||
className="group h-auto min-h-14 w-full justify-start gap-3 rounded-xl px-2 py-2 text-left text-sm font-medium text-foreground shadow-none hover:bg-foreground/[0.04] hover:text-foreground focus-visible:ring-2 focus-visible:ring-foreground/20"
|
||||
data-testid={`onboarding-preview-community-${route}`}
|
||||
key={route}
|
||||
onClick={() => onChoose(route)}
|
||||
type="button"
|
||||
variant="ghost"
|
||||
>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center">
|
||||
<RouteIcon aria-hidden className="size-4" />
|
||||
<span className="flex size-8 shrink-0 items-center justify-start">
|
||||
<RouteIcon aria-hidden className="!size-6" />
|
||||
</span>
|
||||
<span>{label}</span>
|
||||
<span className="ml-auto flex size-8 shrink-0 items-center justify-center">
|
||||
<span className="min-w-0 flex-1 truncate">{label}</span>
|
||||
<span className="ml-auto flex size-10 shrink-0 items-center justify-center">
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className="size-4 text-muted-foreground transition-colors duration-150 ease-out group-hover:text-foreground motion-reduce:transition-none"
|
||||
|
||||
Reference in New Issue
Block a user