Improve onboarding defaults and issue goal fallback

This commit is contained in:
Dotta
2026-03-12 08:50:31 -05:00
parent 5f3f354b3a
commit 448e9c192b
9 changed files with 378 additions and 77 deletions

View File

@@ -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, SOULD.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();
@@ -159,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
});
@@ -181,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;
@@ -219,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 }>>();
@@ -234,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]);
@@ -281,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,
@@ -347,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"
});
@@ -373,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 ?? [];
@@ -393,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;
}
@@ -554,19 +567,23 @@ export function OnboardingWizard() {
</button>
{/* Left half — form */}
<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={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 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 }) => (
{(
[
{ 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"
@@ -599,7 +616,14 @@ export function OnboardingWizard() {
</div>
</div>
<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")}>
<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
@@ -611,7 +635,14 @@ export function OnboardingWizard() {
/>
</div>
<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")}>
<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
@@ -707,7 +738,12 @@ export function OnboardingWizard() {
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")} />
<ChevronDown
className={cn(
"h-3 w-3 transition-transform",
showMoreAdapters ? "rotate-0" : "-rotate-90"
)}
/>
More Agent Adapter Types
</button>
@@ -755,8 +791,8 @@ export function OnboardingWizard() {
opt.comingSoon
? "border-border opacity-40 cursor-not-allowed"
: adapterType === opt.value
? "border-foreground bg-accent"
: "border-border hover:bg-accent/50"
? "border-foreground bg-accent"
: "border-border hover:bg-accent/50"
)}
onClick={() => {
if (opt.comingSoon) return;
@@ -783,8 +819,8 @@ export function OnboardingWizard() {
<span className="font-medium">{opt.label}</span>
<span className="text-muted-foreground text-[10px]">
{opt.comingSoon
? (opt as { disabledLabel?: string }).disabledLabel ??
"Coming soon"
? (opt as { disabledLabel?: string })
.disabledLabel ?? "Coming soon"
: opt.desc}
</span>
</button>
@@ -869,12 +905,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})
@@ -892,8 +931,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>
))}
@@ -940,7 +984,8 @@ export function OnboardingWizard() {
</div>
)}
{adapterEnvResult && adapterEnvResult.status === "pass" ? (
{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>
@@ -952,17 +997,23 @@ export function OnboardingWizard() {
{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>
)}
@@ -1001,24 +1052,43 @@ export function OnboardingWizard() {
in
env or run{" "}
<span className="font-mono">
{adapterType === "cursor"
? "agent login"
: adapterType === "codex_local"
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_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>
)}
</span>
.
</p>
) : (
<p className="text-muted-foreground">
If login is required, run{" "}
<span className="font-mono">claude login</span>{" "}
and retry.
</p>
)}
</div>
)}
</div>
)}
@@ -1049,14 +1119,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)}
/>
@@ -1240,10 +1317,12 @@ export function OnboardingWizard() {
</div>
{/* Right half — ASCII art (hidden on mobile) */}
<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"
)}>
<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>
@@ -1261,14 +1340,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}`}>

View 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",
});
});
});

View 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,
};
}