Merge remote-tracking branch 'public-gh/master'
* public-gh/master: Drop pnpm lockfile from PR Update ui/src/components/OnboardingWizard.tsx Update ui/src/components/OnboardingWizard.tsx Fix onboarding manual debug JSX Improve onboarding defaults and issue goal fallback Simplify adapter environment check: animate pass, show debug only on fail Show Claude Code and Codex as recommended, collapse other adapter types Animate onboarding layout when switching between Company and Agent steps Make onboarding wizard steps clickable tabs for easier dev navigation Add agent chat architecture plan Style tweaks for onboarding wizard step 1 Add direct onboarding routes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Navigate, Outlet, Route, Routes, useLocation } from "@/lib/router";
|
||||
import { Navigate, Outlet, Route, Routes, useLocation, useParams } from "@/lib/router";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Layout } from "./components/Layout";
|
||||
@@ -108,6 +108,7 @@ function boardRoutes() {
|
||||
<>
|
||||
<Route index element={<Navigate to="dashboard" replace />} />
|
||||
<Route path="dashboard" element={<Dashboard />} />
|
||||
<Route path="onboarding" element={<OnboardingRoutePage />} />
|
||||
<Route path="companies" element={<Companies />} />
|
||||
<Route path="company/settings" element={<CompanySettings />} />
|
||||
<Route path="settings" element={<LegacySettingsRedirect />} />
|
||||
@@ -164,6 +165,57 @@ function LegacySettingsRedirect() {
|
||||
return <Navigate to={`/instance/settings${location.search}${location.hash}`} replace />;
|
||||
}
|
||||
|
||||
function OnboardingRoutePage() {
|
||||
const { companies, loading } = useCompany();
|
||||
const { onboardingOpen, openOnboarding } = useDialog();
|
||||
const { companyPrefix } = useParams<{ companyPrefix?: string }>();
|
||||
const opened = useRef(false);
|
||||
const matchedCompany = companyPrefix
|
||||
? companies.find((company) => company.issuePrefix.toUpperCase() === companyPrefix.toUpperCase()) ?? null
|
||||
: null;
|
||||
|
||||
useEffect(() => {
|
||||
if (loading || opened.current || onboardingOpen) return;
|
||||
opened.current = true;
|
||||
if (matchedCompany) {
|
||||
openOnboarding({ initialStep: 2, companyId: matchedCompany.id });
|
||||
return;
|
||||
}
|
||||
openOnboarding();
|
||||
}, [companyPrefix, loading, matchedCompany, onboardingOpen, openOnboarding]);
|
||||
|
||||
const title = matchedCompany
|
||||
? `Add another agent to ${matchedCompany.name}`
|
||||
: companies.length > 0
|
||||
? "Create another company"
|
||||
: "Create your first company";
|
||||
const description = matchedCompany
|
||||
? "Run onboarding again to add an agent and a starter task for this company."
|
||||
: companies.length > 0
|
||||
? "Run onboarding again to create another company and seed its first agent."
|
||||
: "Get started by creating a company and your first agent.";
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-xl py-10">
|
||||
<div className="rounded-lg border border-border bg-card p-6">
|
||||
<h1 className="text-xl font-semibold">{title}</h1>
|
||||
<p className="mt-2 text-sm text-muted-foreground">{description}</p>
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
onClick={() =>
|
||||
matchedCompany
|
||||
? openOnboarding({ initialStep: 2, companyId: matchedCompany.id })
|
||||
: openOnboarding()
|
||||
}
|
||||
>
|
||||
{matchedCompany ? "Add Agent" : "Start Onboarding"}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CompanyRootRedirect() {
|
||||
const { companies, selectedCompany, loading } = useCompany();
|
||||
const { onboardingOpen } = useDialog();
|
||||
@@ -242,6 +294,7 @@ export function App() {
|
||||
|
||||
<Route element={<CloudAccessGate />}>
|
||||
<Route index element={<CompanyRootRedirect />} />
|
||||
<Route path="onboarding" element={<OnboardingRoutePage />} />
|
||||
<Route path="instance" element={<Navigate to="/instance/settings" replace />} />
|
||||
<Route path="instance/settings" element={<Layout />}>
|
||||
<Route index element={<InstanceSettings />} />
|
||||
|
||||
@@ -17,9 +17,13 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "../lib/utils";
|
||||
import { extractModelName, extractProviderIdWithFallback } from "../lib/model-utils";
|
||||
import {
|
||||
extractModelName,
|
||||
extractProviderIdWithFallback
|
||||
} from "../lib/model-utils";
|
||||
import { getUIAdapter } from "../adapters";
|
||||
import { defaultCreateValues } from "./agent-config-defaults";
|
||||
import { parseOnboardingGoalInput } from "../lib/onboarding-goal";
|
||||
import {
|
||||
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX,
|
||||
DEFAULT_CODEX_LOCAL_MODEL
|
||||
@@ -61,11 +65,13 @@ type AdapterType =
|
||||
| "http"
|
||||
| "openclaw_gateway";
|
||||
|
||||
const DEFAULT_TASK_DESCRIPTION = `Setup yourself as the CEO. Use the ceo persona found here: [https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md](https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md)
|
||||
const DEFAULT_TASK_DESCRIPTION = `Setup yourself as the CEO. Use the ceo persona found here:
|
||||
|
||||
Ensure you have a folder agents/ceo and then download this AGENTS.md as well as the sibling HEARTBEAT.md, SOUL.md, and TOOLS.md. and set that AGENTS.md as the path to your agents instruction file
|
||||
https://github.com/paperclipai/companies/blob/main/default/ceo/AGENTS.md
|
||||
|
||||
And after you've finished that, hire yourself a Founding Engineer agent`;
|
||||
Ensure you have a folder agents/ceo and then download this AGENTS.md, and sibling HEARTBEAT.md, SOUL.md, and TOOLS.md. and set that AGENTS.md as the path to your agents instruction file
|
||||
|
||||
After that, hire yourself a Founding Engineer agent and then plan the roadmap and tasks for your new company.`;
|
||||
|
||||
export function OnboardingWizard() {
|
||||
const { onboardingOpen, onboardingOptions, closeOnboarding } = useDialog();
|
||||
@@ -101,6 +107,7 @@ export function OnboardingWizard() {
|
||||
const [forceUnsetAnthropicApiKey, setForceUnsetAnthropicApiKey] =
|
||||
useState(false);
|
||||
const [unsetAnthropicLoading, setUnsetAnthropicLoading] = useState(false);
|
||||
const [showMoreAdapters, setShowMoreAdapters] = useState(false);
|
||||
|
||||
// Step 3
|
||||
const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md");
|
||||
@@ -158,12 +165,11 @@ export function OnboardingWizard() {
|
||||
data: adapterModels,
|
||||
error: adapterModelsError,
|
||||
isLoading: adapterModelsLoading,
|
||||
isFetching: adapterModelsFetching,
|
||||
isFetching: adapterModelsFetching
|
||||
} = useQuery({
|
||||
queryKey:
|
||||
createdCompanyId
|
||||
? queryKeys.agents.adapterModels(createdCompanyId, adapterType)
|
||||
: ["agents", "none", "adapter-models", adapterType],
|
||||
queryKey: createdCompanyId
|
||||
? queryKeys.agents.adapterModels(createdCompanyId, adapterType)
|
||||
: ["agents", "none", "adapter-models", adapterType],
|
||||
queryFn: () => agentsApi.adapterModels(createdCompanyId!, adapterType),
|
||||
enabled: Boolean(createdCompanyId) && onboardingOpen && step === 2
|
||||
});
|
||||
@@ -180,10 +186,10 @@ export function OnboardingWizard() {
|
||||
: adapterType === "gemini_local"
|
||||
? "gemini"
|
||||
: adapterType === "cursor"
|
||||
? "agent"
|
||||
: adapterType === "opencode_local"
|
||||
? "opencode"
|
||||
: "claude");
|
||||
? "agent"
|
||||
: adapterType === "opencode_local"
|
||||
? "opencode"
|
||||
: "claude");
|
||||
|
||||
useEffect(() => {
|
||||
if (step !== 2) return;
|
||||
@@ -218,8 +224,8 @@ export function OnboardingWizard() {
|
||||
return [
|
||||
{
|
||||
provider: "models",
|
||||
entries: [...filteredModels].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
},
|
||||
entries: [...filteredModels].sort((a, b) => a.id.localeCompare(b.id))
|
||||
}
|
||||
];
|
||||
}
|
||||
const groups = new Map<string, Array<{ id: string; label: string }>>();
|
||||
@@ -233,7 +239,7 @@ export function OnboardingWizard() {
|
||||
.sort(([a], [b]) => a.localeCompare(b))
|
||||
.map(([provider, entries]) => ({
|
||||
provider,
|
||||
entries: [...entries].sort((a, b) => a.id.localeCompare(b.id)),
|
||||
entries: [...entries].sort((a, b) => a.id.localeCompare(b.id))
|
||||
}));
|
||||
}, [filteredModels, adapterType]);
|
||||
|
||||
@@ -280,7 +286,7 @@ export function OnboardingWizard() {
|
||||
: adapterType === "gemini_local"
|
||||
? model || DEFAULT_GEMINI_LOCAL_MODEL
|
||||
: adapterType === "cursor"
|
||||
? model || DEFAULT_CURSOR_LOCAL_MODEL
|
||||
? model || DEFAULT_CURSOR_LOCAL_MODEL
|
||||
: model,
|
||||
command,
|
||||
args,
|
||||
@@ -346,8 +352,12 @@ export function OnboardingWizard() {
|
||||
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
||||
|
||||
if (companyGoal.trim()) {
|
||||
const parsedGoal = parseOnboardingGoalInput(companyGoal);
|
||||
await goalsApi.create(company.id, {
|
||||
title: companyGoal.trim(),
|
||||
title: parsedGoal.title,
|
||||
...(parsedGoal.description
|
||||
? { description: parsedGoal.description }
|
||||
: {}),
|
||||
level: "company",
|
||||
status: "active"
|
||||
});
|
||||
@@ -372,19 +382,23 @@ export function OnboardingWizard() {
|
||||
if (adapterType === "opencode_local") {
|
||||
const selectedModelId = model.trim();
|
||||
if (!selectedModelId) {
|
||||
setError("OpenCode requires an explicit model in provider/model format.");
|
||||
setError(
|
||||
"OpenCode requires an explicit model in provider/model format."
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (adapterModelsError) {
|
||||
setError(
|
||||
adapterModelsError instanceof Error
|
||||
? adapterModelsError.message
|
||||
: "Failed to load OpenCode models.",
|
||||
: "Failed to load OpenCode models."
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (adapterModelsLoading || adapterModelsFetching) {
|
||||
setError("OpenCode models are still loading. Please wait and try again.");
|
||||
setError(
|
||||
"OpenCode models are still loading. Please wait and try again."
|
||||
);
|
||||
return;
|
||||
}
|
||||
const discoveredModels = adapterModels ?? [];
|
||||
@@ -392,7 +406,7 @@ export function OnboardingWizard() {
|
||||
setError(
|
||||
discoveredModels.length === 0
|
||||
? "No OpenCode models discovered. Run `opencode models` and authenticate providers."
|
||||
: `Configured OpenCode model is unavailable: ${selectedModelId}`,
|
||||
: `Configured OpenCode model is unavailable: ${selectedModelId}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -553,30 +567,38 @@ export function OnboardingWizard() {
|
||||
</button>
|
||||
|
||||
{/* Left half — form */}
|
||||
<div className="w-full md:w-1/2 flex flex-col overflow-y-auto">
|
||||
<div
|
||||
className={cn(
|
||||
"w-full flex flex-col overflow-y-auto transition-[width] duration-500 ease-in-out",
|
||||
step === 1 ? "md:w-1/2" : "md:w-full"
|
||||
)}
|
||||
>
|
||||
<div className="w-full max-w-md mx-auto my-auto px-8 py-12 shrink-0">
|
||||
{/* Progress indicators */}
|
||||
<div className="flex items-center gap-2 mb-8">
|
||||
<Sparkles className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">Get Started</span>
|
||||
<span className="text-sm text-muted-foreground/60">
|
||||
Step {step} of 4
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5 ml-auto">
|
||||
{[1, 2, 3, 4].map((s) => (
|
||||
<div
|
||||
key={s}
|
||||
className={cn(
|
||||
"h-1.5 w-6 rounded-full transition-colors",
|
||||
s < step
|
||||
? "bg-green-500"
|
||||
: s === step
|
||||
? "bg-foreground"
|
||||
: "bg-muted"
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{/* Progress tabs */}
|
||||
<div className="flex items-center gap-0 mb-8 border-b border-border">
|
||||
{(
|
||||
[
|
||||
{ step: 1 as Step, label: "Company", icon: Building2 },
|
||||
{ step: 2 as Step, label: "Agent", icon: Bot },
|
||||
{ step: 3 as Step, label: "Task", icon: ListTodo },
|
||||
{ step: 4 as Step, label: "Launch", icon: Rocket }
|
||||
] as const
|
||||
).map(({ step: s, label, icon: Icon }) => (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => setStep(s)}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-2 text-xs font-medium border-b-2 -mb-px transition-colors cursor-pointer",
|
||||
s === step
|
||||
? "border-foreground text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground/70 hover:border-border"
|
||||
)}
|
||||
>
|
||||
<Icon className="h-3.5 w-3.5" />
|
||||
{label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Step content */}
|
||||
@@ -593,8 +615,15 @@ export function OnboardingWizard() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
<div className="mt-3 group">
|
||||
<label
|
||||
className={cn(
|
||||
"text-xs mb-1 block transition-colors",
|
||||
companyName.trim()
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground group-focus-within:text-foreground"
|
||||
)}
|
||||
>
|
||||
Company name
|
||||
</label>
|
||||
<input
|
||||
@@ -605,8 +634,15 @@ export function OnboardingWizard() {
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
<div className="group">
|
||||
<label
|
||||
className={cn(
|
||||
"text-xs mb-1 block transition-colors",
|
||||
companyGoal.trim()
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground group-focus-within:text-foreground"
|
||||
)}
|
||||
>
|
||||
Mission / goal (optional)
|
||||
</label>
|
||||
<textarea
|
||||
@@ -665,69 +701,25 @@ export function OnboardingWizard() {
|
||||
icon: Code,
|
||||
desc: "Local Codex agent",
|
||||
recommended: true
|
||||
},
|
||||
{
|
||||
value: "gemini_local" as const,
|
||||
label: "Gemini CLI",
|
||||
icon: Gem,
|
||||
desc: "Local Gemini agent"
|
||||
},
|
||||
{
|
||||
value: "opencode_local" as const,
|
||||
label: "OpenCode",
|
||||
icon: OpenCodeLogoIcon,
|
||||
desc: "Local multi-provider agent"
|
||||
},
|
||||
{
|
||||
value: "pi_local" as const,
|
||||
label: "Pi",
|
||||
icon: Terminal,
|
||||
desc: "Local Pi agent"
|
||||
},
|
||||
{
|
||||
value: "openclaw_gateway" as const,
|
||||
label: "OpenClaw Gateway",
|
||||
icon: Bot,
|
||||
desc: "Invoke OpenClaw via gateway protocol",
|
||||
comingSoon: true,
|
||||
disabledLabel: "Configure OpenClaw within the App"
|
||||
},
|
||||
{
|
||||
value: "cursor" as const,
|
||||
label: "Cursor",
|
||||
icon: MousePointer2,
|
||||
desc: "Local Cursor agent"
|
||||
}
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
disabled={!!opt.comingSoon}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1.5 rounded-md border p-3 text-xs transition-colors relative",
|
||||
opt.comingSoon
|
||||
? "border-border opacity-40 cursor-not-allowed"
|
||||
: adapterType === opt.value
|
||||
? "border-foreground bg-accent"
|
||||
: "border-border hover:bg-accent/50"
|
||||
adapterType === opt.value
|
||||
? "border-foreground bg-accent"
|
||||
: "border-border hover:bg-accent/50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (opt.comingSoon) return;
|
||||
const nextType = opt.value as AdapterType;
|
||||
setAdapterType(nextType);
|
||||
if (nextType === "codex_local" && !model) {
|
||||
setModel(DEFAULT_CODEX_LOCAL_MODEL);
|
||||
} else if (nextType === "gemini_local" && !model) {
|
||||
setModel(DEFAULT_GEMINI_LOCAL_MODEL);
|
||||
} else if (nextType === "cursor" && !model) {
|
||||
setModel(DEFAULT_CURSOR_LOCAL_MODEL);
|
||||
}
|
||||
if (nextType === "opencode_local") {
|
||||
if (!model.includes("/")) {
|
||||
setModel("");
|
||||
}
|
||||
return;
|
||||
if (nextType !== "codex_local") {
|
||||
setModel("");
|
||||
}
|
||||
setModel("");
|
||||
}}
|
||||
>
|
||||
{opt.recommended && (
|
||||
@@ -738,14 +730,105 @@ export function OnboardingWizard() {
|
||||
<opt.icon className="h-4 w-4" />
|
||||
<span className="font-medium">{opt.label}</span>
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{opt.comingSoon
|
||||
? (opt as { disabledLabel?: string }).disabledLabel ??
|
||||
"Coming soon"
|
||||
: opt.desc}
|
||||
{opt.desc}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="flex items-center gap-1.5 mt-3 text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||
onClick={() => setShowMoreAdapters((v) => !v)}
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
"h-3 w-3 transition-transform",
|
||||
showMoreAdapters ? "rotate-0" : "-rotate-90"
|
||||
)}
|
||||
/>
|
||||
More Agent Adapter Types
|
||||
</button>
|
||||
|
||||
{showMoreAdapters && (
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
{[
|
||||
{
|
||||
value: "gemini_local" as const,
|
||||
label: "Gemini CLI",
|
||||
icon: Gem,
|
||||
desc: "Local Gemini agent"
|
||||
},
|
||||
{
|
||||
value: "opencode_local" as const,
|
||||
label: "OpenCode",
|
||||
icon: OpenCodeLogoIcon,
|
||||
desc: "Local multi-provider agent"
|
||||
},
|
||||
{
|
||||
value: "pi_local" as const,
|
||||
label: "Pi",
|
||||
icon: Terminal,
|
||||
desc: "Local Pi agent"
|
||||
},
|
||||
{
|
||||
value: "cursor" as const,
|
||||
label: "Cursor",
|
||||
icon: MousePointer2,
|
||||
desc: "Local Cursor agent"
|
||||
},
|
||||
{
|
||||
value: "openclaw_gateway" as const,
|
||||
label: "OpenClaw Gateway",
|
||||
icon: Bot,
|
||||
desc: "Invoke OpenClaw via gateway protocol",
|
||||
comingSoon: true,
|
||||
disabledLabel: "Configure OpenClaw within the App"
|
||||
}
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
disabled={!!opt.comingSoon}
|
||||
className={cn(
|
||||
"flex flex-col items-center gap-1.5 rounded-md border p-3 text-xs transition-colors relative",
|
||||
opt.comingSoon
|
||||
? "border-border opacity-40 cursor-not-allowed"
|
||||
: adapterType === opt.value
|
||||
? "border-foreground bg-accent"
|
||||
: "border-border hover:bg-accent/50"
|
||||
)}
|
||||
onClick={() => {
|
||||
if (opt.comingSoon) return;
|
||||
const nextType = opt.value as AdapterType;
|
||||
setAdapterType(nextType);
|
||||
if (nextType === "gemini_local" && !model) {
|
||||
setModel(DEFAULT_GEMINI_LOCAL_MODEL);
|
||||
return;
|
||||
}
|
||||
if (nextType === "cursor" && !model) {
|
||||
setModel(DEFAULT_CURSOR_LOCAL_MODEL);
|
||||
return;
|
||||
}
|
||||
if (nextType === "opencode_local") {
|
||||
if (!model.includes("/")) {
|
||||
setModel("");
|
||||
}
|
||||
return;
|
||||
}
|
||||
setModel("");
|
||||
}}
|
||||
>
|
||||
<opt.icon className="h-4 w-4" />
|
||||
<span className="font-medium">{opt.label}</span>
|
||||
<span className="text-muted-foreground text-[10px]">
|
||||
{opt.comingSoon
|
||||
? (opt as { disabledLabel?: string })
|
||||
.disabledLabel ?? "Coming soon"
|
||||
: opt.desc}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Conditional adapter fields */}
|
||||
@@ -824,12 +907,15 @@ export function OnboardingWizard() {
|
||||
setModelOpen(false);
|
||||
}}
|
||||
>
|
||||
Default
|
||||
</button>
|
||||
Default
|
||||
</button>
|
||||
)}
|
||||
<div className="max-h-[240px] overflow-y-auto">
|
||||
{groupedModels.map((group) => (
|
||||
<div key={group.provider} className="mb-1 last:mb-0">
|
||||
<div
|
||||
key={group.provider}
|
||||
className="mb-1 last:mb-0"
|
||||
>
|
||||
{adapterType === "opencode_local" && (
|
||||
<div className="px-2 py-1 text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||
{group.provider} ({group.entries.length})
|
||||
@@ -847,8 +933,13 @@ export function OnboardingWizard() {
|
||||
setModelOpen(false);
|
||||
}}
|
||||
>
|
||||
<span className="block w-full text-left truncate" title={m.id}>
|
||||
{adapterType === "opencode_local" ? extractModelName(m.id) : m.label}
|
||||
<span
|
||||
className="block w-full text-left truncate"
|
||||
title={m.id}
|
||||
>
|
||||
{adapterType === "opencode_local"
|
||||
? extractModelName(m.id)
|
||||
: m.label}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
@@ -895,75 +986,92 @@ export function OnboardingWizard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{adapterEnvResult && (
|
||||
{adapterEnvResult &&
|
||||
adapterEnvResult.status === "pass" ? (
|
||||
<div className="flex items-center gap-2 rounded-md border border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10 px-3 py-2 text-xs text-green-700 dark:text-green-300 animate-in fade-in slide-in-from-bottom-1 duration-300">
|
||||
<Check className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="font-medium">Passed</span>
|
||||
</div>
|
||||
) : adapterEnvResult ? (
|
||||
<AdapterEnvironmentResult result={adapterEnvResult} />
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{shouldSuggestUnsetAnthropicApiKey && (
|
||||
<div className="rounded-md border border-amber-300/60 bg-amber-50/40 px-2.5 py-2 space-y-2">
|
||||
<p className="text-[11px] text-amber-900/90 leading-relaxed">
|
||||
Claude failed while <span className="font-mono">ANTHROPIC_API_KEY</span> is set.
|
||||
You can clear it in this CEO adapter config and retry the probe.
|
||||
Claude failed while{" "}
|
||||
<span className="font-mono">ANTHROPIC_API_KEY</span>{" "}
|
||||
is set. You can clear it in this CEO adapter config
|
||||
and retry the probe.
|
||||
</p>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="h-7 px-2.5 text-xs"
|
||||
disabled={adapterEnvLoading || unsetAnthropicLoading}
|
||||
disabled={
|
||||
adapterEnvLoading || unsetAnthropicLoading
|
||||
}
|
||||
onClick={() => void handleUnsetAnthropicApiKey()}
|
||||
>
|
||||
{unsetAnthropicLoading ? "Retrying..." : "Unset ANTHROPIC_API_KEY"}
|
||||
{unsetAnthropicLoading
|
||||
? "Retrying..."
|
||||
: "Unset ANTHROPIC_API_KEY"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="rounded-md border border-border/70 bg-muted/20 px-2.5 py-2 text-[11px] space-y-1.5">
|
||||
<p className="font-medium">Manual debug</p>
|
||||
<p className="text-muted-foreground font-mono break-all">
|
||||
{adapterType === "cursor"
|
||||
? `${effectiveAdapterCommand} -p --mode ask --output-format json \"Respond with hello.\"`
|
||||
: adapterType === "codex_local"
|
||||
? `${effectiveAdapterCommand} exec --json -`
|
||||
: adapterType === "gemini_local"
|
||||
? `${effectiveAdapterCommand} --output-format json \"Respond with hello.\"`
|
||||
: adapterType === "opencode_local"
|
||||
? `${effectiveAdapterCommand} run --format json "Respond with hello."`
|
||||
: `${effectiveAdapterCommand} --print - --output-format stream-json --verbose`}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
Prompt:{" "}
|
||||
<span className="font-mono">Respond with hello.</span>
|
||||
</p>
|
||||
{adapterType === "cursor" || adapterType === "codex_local" || adapterType === "gemini_local" || adapterType === "opencode_local" ? (
|
||||
{adapterEnvResult && adapterEnvResult.status === "fail" && (
|
||||
<div className="rounded-md border border-border/70 bg-muted/20 px-2.5 py-2 text-[11px] space-y-1.5">
|
||||
<p className="font-medium">Manual debug</p>
|
||||
<p className="text-muted-foreground font-mono break-all">
|
||||
{adapterType === "cursor"
|
||||
? `${effectiveAdapterCommand} -p --mode ask --output-format json \"Respond with hello.\"`
|
||||
: adapterType === "codex_local"
|
||||
? `${effectiveAdapterCommand} exec --json -`
|
||||
: adapterType === "gemini_local"
|
||||
? `${effectiveAdapterCommand} --output-format json "Respond with hello."`
|
||||
: adapterType === "opencode_local"
|
||||
? `${effectiveAdapterCommand} run --format json "Respond with hello."`
|
||||
: `${effectiveAdapterCommand} --print - --output-format stream-json --verbose`}
|
||||
</p>
|
||||
<p className="text-muted-foreground">
|
||||
If auth fails, set{" "}
|
||||
<span className="font-mono">
|
||||
{adapterType === "cursor"
|
||||
? "CURSOR_API_KEY"
|
||||
: adapterType === "gemini_local"
|
||||
? "GEMINI_API_KEY"
|
||||
: "OPENAI_API_KEY"}
|
||||
</span>{" "}
|
||||
in
|
||||
env or run{" "}
|
||||
<span className="font-mono">
|
||||
{adapterType === "cursor"
|
||||
? "agent login"
|
||||
: adapterType === "codex_local"
|
||||
? "codex login"
|
||||
Prompt:{" "}
|
||||
<span className="font-mono">Respond with hello.</span>
|
||||
</p>
|
||||
{adapterType === "cursor" ||
|
||||
adapterType === "codex_local" ||
|
||||
adapterType === "gemini_local" ||
|
||||
adapterType === "opencode_local" ? (
|
||||
<p className="text-muted-foreground">
|
||||
If auth fails, set{" "}
|
||||
<span className="font-mono">
|
||||
{adapterType === "cursor"
|
||||
? "CURSOR_API_KEY"
|
||||
: adapterType === "gemini_local"
|
||||
? "gemini auth"
|
||||
: "opencode auth login"}
|
||||
</span>.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground">
|
||||
If login is required, run{" "}
|
||||
<span className="font-mono">claude login</span> and
|
||||
retry.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
? "GEMINI_API_KEY"
|
||||
: "OPENAI_API_KEY"}
|
||||
</span>{" "}
|
||||
in env or run{" "}
|
||||
<span className="font-mono">
|
||||
{adapterType === "cursor"
|
||||
? "agent login"
|
||||
: adapterType === "codex_local"
|
||||
? "codex login"
|
||||
: adapterType === "gemini_local"
|
||||
? "gemini auth"
|
||||
: "opencode auth login"}
|
||||
</span>
|
||||
.
|
||||
</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground">
|
||||
If login is required, run{" "}
|
||||
<span className="font-mono">claude login</span>{" "}
|
||||
and retry.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -994,14 +1102,21 @@ export function OnboardingWizard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(adapterType === "http" || adapterType === "openclaw_gateway") && (
|
||||
{(adapterType === "http" ||
|
||||
adapterType === "openclaw_gateway") && (
|
||||
<div>
|
||||
<label className="text-xs text-muted-foreground mb-1 block">
|
||||
{adapterType === "openclaw_gateway" ? "Gateway URL" : "Webhook URL"}
|
||||
{adapterType === "openclaw_gateway"
|
||||
? "Gateway URL"
|
||||
: "Webhook URL"}
|
||||
</label>
|
||||
<input
|
||||
className="w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm font-mono outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
|
||||
placeholder={adapterType === "openclaw_gateway" ? "ws://127.0.0.1:18789" : "https://..."}
|
||||
placeholder={
|
||||
adapterType === "openclaw_gateway"
|
||||
? "ws://127.0.0.1:18789"
|
||||
: "https://..."
|
||||
}
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
@@ -1185,7 +1300,12 @@ export function OnboardingWizard() {
|
||||
</div>
|
||||
|
||||
{/* Right half — ASCII art (hidden on mobile) */}
|
||||
<div className="hidden md:block w-1/2 overflow-hidden">
|
||||
<div
|
||||
className={cn(
|
||||
"hidden md:block overflow-hidden bg-[#1d1d1d] transition-[width,opacity] duration-500 ease-in-out",
|
||||
step === 1 ? "w-1/2 opacity-100" : "w-0 opacity-0"
|
||||
)}
|
||||
>
|
||||
<AsciiArtAnimation />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1203,14 +1323,14 @@ function AdapterEnvironmentResult({
|
||||
result.status === "pass"
|
||||
? "Passed"
|
||||
: result.status === "warn"
|
||||
? "Warnings"
|
||||
: "Failed";
|
||||
? "Warnings"
|
||||
: "Failed";
|
||||
const statusClass =
|
||||
result.status === "pass"
|
||||
? "text-green-700 dark:text-green-300 border-green-300 dark:border-green-500/40 bg-green-50 dark:bg-green-500/10"
|
||||
: result.status === "warn"
|
||||
? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10"
|
||||
: "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10";
|
||||
? "text-amber-700 dark:text-amber-300 border-amber-300 dark:border-amber-500/40 bg-amber-50 dark:bg-amber-500/10"
|
||||
: "text-red-700 dark:text-red-300 border-red-300 dark:border-red-500/40 bg-red-50 dark:bg-red-500/10";
|
||||
|
||||
return (
|
||||
<div className={`rounded-md border px-2.5 py-2 text-[11px] ${statusClass}`}>
|
||||
|
||||
22
ui/src/lib/onboarding-goal.test.ts
Normal file
22
ui/src/lib/onboarding-goal.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseOnboardingGoalInput } from "./onboarding-goal";
|
||||
|
||||
describe("parseOnboardingGoalInput", () => {
|
||||
it("uses a single-line goal as the title only", () => {
|
||||
expect(parseOnboardingGoalInput("Ship the MVP")).toEqual({
|
||||
title: "Ship the MVP",
|
||||
description: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("splits a multiline goal into title and description", () => {
|
||||
expect(
|
||||
parseOnboardingGoalInput(
|
||||
"Ship the MVP\nLaunch to 10 design partners\nMeasure retention",
|
||||
),
|
||||
).toEqual({
|
||||
title: "Ship the MVP",
|
||||
description: "Launch to 10 design partners\nMeasure retention",
|
||||
});
|
||||
});
|
||||
});
|
||||
18
ui/src/lib/onboarding-goal.ts
Normal file
18
ui/src/lib/onboarding-goal.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
export function parseOnboardingGoalInput(raw: string): {
|
||||
title: string;
|
||||
description: string | null;
|
||||
} {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return { title: "", description: null };
|
||||
}
|
||||
|
||||
const [firstLine, ...restLines] = trimmed.split(/\r?\n/);
|
||||
const title = firstLine.trim();
|
||||
const description = restLines.join("\n").trim();
|
||||
|
||||
return {
|
||||
title,
|
||||
description: description.length > 0 ? description : null,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user