2026-03-03 12:26:28 -06:00
import { useEffect , useState , useRef , useCallback } from "react" ;
2026-02-17 13:24:33 -06:00
import { useNavigate } from "react-router-dom" ;
2026-02-17 20:07:41 -06:00
import { useQuery , useQueryClient } from "@tanstack/react-query" ;
2026-03-03 12:51:42 -06:00
import type { AdapterEnvironmentTestResult } from "@paperclipai/shared" ;
2026-02-17 13:24:33 -06:00
import { useDialog } from "../context/DialogContext" ;
import { useCompany } from "../context/CompanyContext" ;
import { companiesApi } from "../api/companies" ;
import { goalsApi } from "../api/goals" ;
import { agentsApi } from "../api/agents" ;
import { issuesApi } from "../api/issues" ;
import { queryKeys } from "../lib/queryKeys" ;
2026-02-26 16:33:48 -06:00
import { Dialog , DialogOverlay , DialogPortal } from "@/components/ui/dialog" ;
2026-02-17 20:07:41 -06:00
import {
Popover ,
PopoverContent ,
2026-03-03 13:05:35 -06:00
PopoverTrigger
2026-02-17 20:07:41 -06:00
} from "@/components/ui/popover" ;
2026-02-17 13:24:33 -06:00
import { Button } from "@/components/ui/button" ;
import { cn } from "../lib/utils" ;
2026-02-18 13:53:03 -06:00
import { getUIAdapter } from "../adapters" ;
2026-02-20 12:28:42 -06:00
import { defaultCreateValues } from "./agent-config-defaults" ;
2026-03-03 12:41:50 -06:00
import {
DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX ,
2026-03-03 13:05:35 -06:00
DEFAULT_CODEX_LOCAL_MODEL
2026-03-03 12:41:50 -06:00
} from "@paperclipai/adapter-codex-local" ;
2026-02-26 16:33:48 -06:00
import { AsciiArtAnimation } from "./AsciiArtAnimation" ;
2026-03-02 16:09:07 -06:00
import { ChoosePathButton } from "./PathInstructionsModal" ;
2026-03-03 11:31:48 -06:00
import { HintIcon } from "./agent-config-primitives" ;
2026-02-17 13:24:33 -06:00
import {
Building2 ,
Bot ,
2026-02-26 16:33:48 -06:00
Code ,
2026-02-17 13:24:33 -06:00
ListTodo ,
Rocket ,
ArrowLeft ,
ArrowRight ,
Terminal ,
Globe ,
Sparkles ,
2026-03-03 11:29:34 -06:00
MousePointer2 ,
2026-02-17 13:24:33 -06:00
Check ,
Loader2 ,
2026-02-17 20:07:41 -06:00
FolderOpen ,
ChevronDown ,
2026-03-03 13:05:35 -06:00
X
2026-02-17 13:24:33 -06:00
} from "lucide-react" ;
type Step = 1 | 2 | 3 | 4 ;
2026-03-03 13:05:35 -06:00
type AdapterType =
| "claude_local"
| "codex_local"
| "process"
| "http"
| "openclaw" ;
2026-02-17 13:24:33 -06:00
2026-03-03 10:08:18 -06:00
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)
2026-03-03 13:05:35 -06:00
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
And after you ' ve finished that , hire yourself a Founding Engineer agent ` ;
2026-03-03 10:08:18 -06:00
2026-02-17 13:24:33 -06:00
export function OnboardingWizard() {
2026-03-03 11:37:19 -06:00
const { onboardingOpen , onboardingOptions , closeOnboarding } = useDialog ( ) ;
const { selectedCompanyId , companies , setSelectedCompanyId } = useCompany ( ) ;
2026-02-17 13:24:33 -06:00
const queryClient = useQueryClient ( ) ;
const navigate = useNavigate ( ) ;
2026-03-03 11:37:19 -06:00
const initialStep = onboardingOptions . initialStep ? ? 1 ;
const existingCompanyId = onboardingOptions . companyId ;
const [ step , setStep ] = useState < Step > ( initialStep ) ;
2026-02-17 13:24:33 -06:00
const [ loading , setLoading ] = useState ( false ) ;
const [ error , setError ] = useState < string | null > ( null ) ;
2026-02-17 20:07:41 -06:00
const [ modelOpen , setModelOpen ] = useState ( false ) ;
2026-02-17 13:24:33 -06:00
// Step 1
const [ companyName , setCompanyName ] = useState ( "" ) ;
const [ companyGoal , setCompanyGoal ] = useState ( "" ) ;
// Step 2
const [ agentName , setAgentName ] = useState ( "CEO" ) ;
const [ adapterType , setAdapterType ] = useState < AdapterType > ( "claude_local" ) ;
const [ cwd , setCwd ] = useState ( "" ) ;
const [ model , setModel ] = useState ( "" ) ;
const [ command , setCommand ] = useState ( "" ) ;
const [ args , setArgs ] = useState ( "" ) ;
const [ url , setUrl ] = useState ( "" ) ;
2026-03-03 13:05:35 -06:00
const [ adapterEnvResult , setAdapterEnvResult ] =
useState < AdapterEnvironmentTestResult | null > ( null ) ;
2026-03-03 12:51:42 -06:00
const [ adapterEnvError , setAdapterEnvError ] = useState < string | null > ( null ) ;
const [ adapterEnvLoading , setAdapterEnvLoading ] = useState ( false ) ;
2026-02-17 13:24:33 -06:00
// Step 3
2026-02-18 11:45:43 -06:00
const [ taskTitle , setTaskTitle ] = useState ( "Create your CEO HEARTBEAT.md" ) ;
2026-03-03 13:05:35 -06:00
const [ taskDescription , setTaskDescription ] = useState (
DEFAULT_TASK_DESCRIPTION
) ;
2026-02-17 13:24:33 -06:00
2026-03-03 12:26:28 -06:00
// Auto-grow textarea for task description
const textareaRef = useRef < HTMLTextAreaElement > ( null ) ;
const autoResizeTextarea = useCallback ( ( ) = > {
const el = textareaRef . current ;
if ( ! el ) return ;
2026-03-03 13:05:35 -06:00
el . style . height = "auto" ;
el . style . height = el . scrollHeight + "px" ;
2026-03-03 12:26:28 -06:00
} , [ ] ) ;
2026-03-03 11:37:19 -06:00
// Created entity IDs — pre-populate from existing company when skipping step 1
2026-03-03 13:05:35 -06:00
const [ createdCompanyId , setCreatedCompanyId ] = useState < string | null > (
existingCompanyId ? ? null
) ;
const [ createdCompanyPrefix , setCreatedCompanyPrefix ] = useState <
string | null
> ( null ) ;
2026-02-17 13:24:33 -06:00
const [ createdAgentId , setCreatedAgentId ] = useState < string | null > ( null ) ;
2026-03-03 10:08:18 -06:00
const [ createdIssueRef , setCreatedIssueRef ] = useState < string | null > ( null ) ;
2026-02-17 13:24:33 -06:00
2026-03-03 12:16:05 -06:00
// Sync step and company when onboarding opens with options.
// Keep this independent from company-list refreshes so Step 1 completion
// doesn't get reset after creating a company.
2026-03-03 11:37:19 -06:00
useEffect ( ( ) = > {
2026-03-03 12:16:05 -06:00
if ( ! onboardingOpen ) return ;
const cId = onboardingOptions . companyId ? ? null ;
setStep ( onboardingOptions . initialStep ? ? 1 ) ;
setCreatedCompanyId ( cId ) ;
setCreatedCompanyPrefix ( null ) ;
2026-03-03 13:05:35 -06:00
} , [
onboardingOpen ,
onboardingOptions . companyId ,
onboardingOptions . initialStep
] ) ;
2026-03-03 12:16:05 -06:00
// Backfill issue prefix for an existing company once companies are loaded.
useEffect ( ( ) = > {
if ( ! onboardingOpen || ! createdCompanyId || createdCompanyPrefix ) return ;
const company = companies . find ( ( c ) = > c . id === createdCompanyId ) ;
if ( company ) setCreatedCompanyPrefix ( company . issuePrefix ) ;
} , [ onboardingOpen , createdCompanyId , createdCompanyPrefix , companies ] ) ;
2026-03-03 11:37:19 -06:00
2026-03-03 12:26:28 -06:00
// Resize textarea when step 3 is shown or description changes
useEffect ( ( ) = > {
if ( step === 3 ) autoResizeTextarea ( ) ;
} , [ step , taskDescription , autoResizeTextarea ] ) ;
2026-02-17 20:07:41 -06:00
const { data : adapterModels } = useQuery ( {
queryKey : [ "adapter-models" , adapterType ] ,
queryFn : ( ) = > agentsApi . adapterModels ( adapterType ) ,
2026-03-03 13:05:35 -06:00
enabled : onboardingOpen && step === 2
2026-02-17 20:07:41 -06:00
} ) ;
2026-03-03 13:05:35 -06:00
const isLocalAdapter =
adapterType === "claude_local" || adapterType === "codex_local" ;
const effectiveAdapterCommand =
command . trim ( ) || ( adapterType === "codex_local" ? "codex" : "claude" ) ;
2026-03-03 12:51:42 -06:00
useEffect ( ( ) = > {
if ( step !== 2 ) return ;
setAdapterEnvResult ( null ) ;
setAdapterEnvError ( null ) ;
} , [ step , adapterType , cwd , model , command , args , url ] ) ;
2026-02-17 20:07:41 -06:00
const selectedModel = ( adapterModels ? ? [ ] ) . find ( ( m ) = > m . id === model ) ;
2026-02-17 13:24:33 -06:00
function reset() {
setStep ( 1 ) ;
setLoading ( false ) ;
setError ( null ) ;
setCompanyName ( "" ) ;
setCompanyGoal ( "" ) ;
setAgentName ( "CEO" ) ;
setAdapterType ( "claude_local" ) ;
setCwd ( "" ) ;
setModel ( "" ) ;
setCommand ( "" ) ;
setArgs ( "" ) ;
setUrl ( "" ) ;
2026-03-03 12:51:42 -06:00
setAdapterEnvResult ( null ) ;
setAdapterEnvError ( null ) ;
setAdapterEnvLoading ( false ) ;
2026-02-18 11:45:43 -06:00
setTaskTitle ( "Create your CEO HEARTBEAT.md" ) ;
2026-03-03 10:08:18 -06:00
setTaskDescription ( DEFAULT_TASK_DESCRIPTION ) ;
2026-02-17 13:24:33 -06:00
setCreatedCompanyId ( null ) ;
2026-03-03 10:08:18 -06:00
setCreatedCompanyPrefix ( null ) ;
2026-02-17 13:24:33 -06:00
setCreatedAgentId ( null ) ;
2026-03-03 10:08:18 -06:00
setCreatedIssueRef ( null ) ;
2026-02-17 13:24:33 -06:00
}
2026-02-26 16:33:48 -06:00
function handleClose() {
reset ( ) ;
closeOnboarding ( ) ;
}
2026-02-17 13:24:33 -06:00
function buildAdapterConfig ( ) : Record < string , unknown > {
2026-02-18 13:53:03 -06:00
const adapter = getUIAdapter ( adapterType ) ;
return adapter . buildAdapterConfig ( {
. . . defaultCreateValues ,
adapterType ,
cwd ,
2026-03-03 13:05:35 -06:00
model :
adapterType === "codex_local"
? model || DEFAULT_CODEX_LOCAL_MODEL
: model ,
2026-02-18 13:53:03 -06:00
command ,
args ,
url ,
dangerouslySkipPermissions : adapterType === "claude_local" ,
2026-03-03 12:41:50 -06:00
dangerouslyBypassSandbox :
adapterType === "codex_local"
? DEFAULT_CODEX_LOCAL_BYPASS_APPROVALS_AND_SANDBOX
2026-03-03 13:05:35 -06:00
: defaultCreateValues . dangerouslyBypassSandbox
2026-02-18 13:53:03 -06:00
} ) ;
2026-02-17 13:24:33 -06:00
}
2026-03-03 12:51:42 -06:00
async function runAdapterEnvironmentTest ( ) : Promise < AdapterEnvironmentTestResult | null > {
if ( ! createdCompanyId ) {
2026-03-03 13:05:35 -06:00
setAdapterEnvError (
"Create or select a company before testing adapter environment."
) ;
2026-03-03 12:51:42 -06:00
return null ;
}
setAdapterEnvLoading ( true ) ;
setAdapterEnvError ( null ) ;
try {
2026-03-03 13:05:35 -06:00
const result = await agentsApi . testEnvironment (
createdCompanyId ,
adapterType ,
{
adapterConfig : buildAdapterConfig ( )
}
) ;
2026-03-03 12:51:42 -06:00
setAdapterEnvResult ( result ) ;
return result ;
} catch ( err ) {
2026-03-03 13:05:35 -06:00
setAdapterEnvError (
err instanceof Error ? err . message : "Adapter environment test failed"
) ;
2026-03-03 12:51:42 -06:00
return null ;
} finally {
setAdapterEnvLoading ( false ) ;
}
}
2026-02-17 13:24:33 -06:00
async function handleStep1Next() {
setLoading ( true ) ;
setError ( null ) ;
try {
const company = await companiesApi . create ( { name : companyName.trim ( ) } ) ;
setCreatedCompanyId ( company . id ) ;
2026-03-03 10:08:18 -06:00
setCreatedCompanyPrefix ( company . issuePrefix ) ;
2026-02-17 13:24:33 -06:00
setSelectedCompanyId ( company . id ) ;
queryClient . invalidateQueries ( { queryKey : queryKeys.companies.all } ) ;
if ( companyGoal . trim ( ) ) {
await goalsApi . create ( company . id , {
title : companyGoal.trim ( ) ,
level : "company" ,
2026-03-03 13:05:35 -06:00
status : "active"
} ) ;
queryClient . invalidateQueries ( {
queryKey : queryKeys.goals.list ( company . id )
2026-02-17 13:24:33 -06:00
} ) ;
}
setStep ( 2 ) ;
} catch ( err ) {
setError ( err instanceof Error ? err . message : "Failed to create company" ) ;
} finally {
setLoading ( false ) ;
}
}
async function handleStep2Next() {
if ( ! createdCompanyId ) return ;
setLoading ( true ) ;
setError ( null ) ;
try {
2026-03-03 12:51:42 -06:00
if ( isLocalAdapter ) {
const result = adapterEnvResult ? ? ( await runAdapterEnvironmentTest ( ) ) ;
if ( ! result ) return ;
if ( result . status === "fail" ) {
2026-03-03 13:05:35 -06:00
setError (
"Adapter environment test failed. Fix the errors and test again before continuing."
) ;
2026-03-03 12:51:42 -06:00
return ;
}
}
2026-02-17 13:24:33 -06:00
const agent = await agentsApi . create ( createdCompanyId , {
name : agentName.trim ( ) ,
role : "ceo" ,
adapterType ,
adapterConfig : buildAdapterConfig ( ) ,
runtimeConfig : {
heartbeat : {
enabled : true ,
2026-03-03 13:10:06 -06:00
intervalSec : 3600 ,
2026-02-18 16:46:29 -06:00
wakeOnDemand : true ,
2026-02-17 13:24:33 -06:00
cooldownSec : 10 ,
2026-03-03 13:05:35 -06:00
maxConcurrentRuns : 1
}
}
2026-02-17 13:24:33 -06:00
} ) ;
setCreatedAgentId ( agent . id ) ;
queryClient . invalidateQueries ( {
2026-03-03 13:05:35 -06:00
queryKey : queryKeys.agents.list ( createdCompanyId )
2026-02-17 13:24:33 -06:00
} ) ;
setStep ( 3 ) ;
} catch ( err ) {
setError ( err instanceof Error ? err . message : "Failed to create agent" ) ;
} finally {
setLoading ( false ) ;
}
}
async function handleStep3Next() {
if ( ! createdCompanyId || ! createdAgentId ) return ;
setLoading ( true ) ;
setError ( null ) ;
try {
2026-03-03 10:08:18 -06:00
const issue = await issuesApi . create ( createdCompanyId , {
2026-02-17 13:24:33 -06:00
title : taskTitle.trim ( ) ,
2026-03-03 13:05:35 -06:00
. . . ( taskDescription . trim ( )
? { description : taskDescription.trim ( ) }
: { } ) ,
2026-02-17 13:24:33 -06:00
assigneeAgentId : createdAgentId ,
2026-03-03 13:05:35 -06:00
status : "todo"
2026-02-17 13:24:33 -06:00
} ) ;
2026-03-03 10:08:18 -06:00
setCreatedIssueRef ( issue . identifier ? ? issue . id ) ;
2026-02-17 13:24:33 -06:00
queryClient . invalidateQueries ( {
2026-03-03 13:05:35 -06:00
queryKey : queryKeys.issues.list ( createdCompanyId )
2026-02-17 13:24:33 -06:00
} ) ;
setStep ( 4 ) ;
} catch ( err ) {
setError ( err instanceof Error ? err . message : "Failed to create task" ) ;
} finally {
setLoading ( false ) ;
}
}
async function handleLaunch() {
if ( ! createdAgentId ) return ;
setLoading ( true ) ;
setError ( null ) ;
setLoading ( false ) ;
reset ( ) ;
closeOnboarding ( ) ;
2026-03-03 10:08:18 -06:00
if ( createdCompanyPrefix && createdIssueRef ) {
navigate ( ` / ${ createdCompanyPrefix } /issues/ ${ createdIssueRef } ` ) ;
return ;
}
if ( createdCompanyPrefix ) {
navigate ( ` / ${ createdCompanyPrefix } /dashboard ` ) ;
return ;
}
navigate ( "/dashboard" ) ;
2026-02-17 13:24:33 -06:00
}
function handleKeyDown ( e : React.KeyboardEvent ) {
if ( e . key === "Enter" && ( e . metaKey || e . ctrlKey ) ) {
e . preventDefault ( ) ;
if ( step === 1 && companyName . trim ( ) ) handleStep1Next ( ) ;
else if ( step === 2 && agentName . trim ( ) ) handleStep2Next ( ) ;
else if ( step === 3 && taskTitle . trim ( ) ) handleStep3Next ( ) ;
else if ( step === 4 ) handleLaunch ( ) ;
}
}
2026-02-26 16:33:48 -06:00
if ( ! onboardingOpen ) return null ;
2026-02-17 13:24:33 -06:00
return (
< Dialog
open = { onboardingOpen }
onOpenChange = { ( open ) = > {
2026-02-26 16:33:48 -06:00
if ( ! open ) handleClose ( ) ;
2026-02-17 13:24:33 -06:00
} }
>
2026-02-26 16:33:48 -06:00
< DialogPortal >
< DialogOverlay className = "bg-background" / >
2026-03-03 13:05:35 -06:00
< div className = "fixed inset-0 z-50 flex" onKeyDown = { handleKeyDown } >
2026-02-26 16:33:48 -06:00
{ /* Close button */ }
< button
onClick = { handleClose }
className = "absolute top-4 left-4 z-10 rounded-sm p-1.5 text-muted-foreground/60 hover:text-foreground transition-colors"
>
< X className = "h-5 w-5" / >
< span className = "sr-only" > Close < / span >
< / button >
2026-02-17 13:24:33 -06:00
2026-02-26 16:33:48 -06:00
{ /* Left half — form */ }
< div className = "w-full md:w-1/2 flex flex-col overflow-y-auto" >
2026-03-03 13:05:35 -06:00
< div className = "w-full max-w-md mx-auto my-auto px-8 py-12 shrink-0" >
2026-02-26 16:33:48 -06:00
{ /* 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 }
2026-02-17 13:24:33 -06:00
className = { cn (
2026-02-26 16:33:48 -06:00
"h-1.5 w-6 rounded-full transition-colors" ,
s < step
? "bg-green-500"
: s === step
? "bg-foreground"
: "bg-muted"
2026-02-17 13:24:33 -06:00
) }
2026-02-26 16:33:48 -06:00
/ >
2026-02-17 13:24:33 -06:00
) ) }
< / div >
< / div >
2026-02-26 16:33:48 -06:00
{ /* Step content */ }
{ step === 1 && (
< div className = "space-y-5" >
< div className = "flex items-center gap-3 mb-1" >
< div className = "bg-muted/50 p-2" >
< Building2 className = "h-5 w-5 text-muted-foreground" / >
< / div >
< div >
< h3 className = "font-medium" > Name your company < / h3 >
< p className = "text-xs text-muted-foreground" >
This is the organization your agents will work for .
< / p >
< / div >
< / div >
2026-02-17 13:24:33 -06:00
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
2026-02-26 16:33:48 -06:00
Company name
2026-02-17 13:24:33 -06:00
< / label >
2026-02-26 16:33:48 -06:00
< input
className = "w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
placeholder = "Acme Corp"
value = { companyName }
onChange = { ( e ) = > setCompanyName ( e . target . value ) }
autoFocus
/ >
< / div >
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
Mission / goal ( optional )
< / label >
< textarea
className = "w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[60px]"
placeholder = "What is this company trying to achieve?"
value = { companyGoal }
onChange = { ( e ) = > setCompanyGoal ( e . target . value ) }
/ >
< / div >
< / div >
) }
{ step === 2 && (
< div className = "space-y-5" >
< div className = "flex items-center gap-3 mb-1" >
< div className = "bg-muted/50 p-2" >
< Bot className = "h-5 w-5 text-muted-foreground" / >
< / div >
< div >
< h3 className = "font-medium" > Create your first agent < / h3 >
< p className = "text-xs text-muted-foreground" >
Choose how this agent will run tasks .
< / p >
2026-02-17 20:07:41 -06:00
< / div >
2026-02-17 13:24:33 -06:00
< / div >
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
2026-02-26 16:33:48 -06:00
Agent name
2026-02-17 13:24:33 -06:00
< / label >
2026-02-26 16:33:48 -06:00
< input
className = "w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
placeholder = "CEO"
value = { agentName }
onChange = { ( e ) = > setAgentName ( e . target . value ) }
autoFocus
/ >
< / div >
{ /* Adapter type radio cards */ }
< div >
< label className = "text-xs text-muted-foreground mb-2 block" >
Adapter type
< / label >
< div className = "grid grid-cols-2 gap-2" >
2026-03-03 13:05:35 -06:00
{ [
2026-02-26 16:33:48 -06:00
{
value : "claude_local" as const ,
label : "Claude Code" ,
icon : Sparkles ,
2026-03-03 13:05:35 -06:00
desc : "Local Claude agent"
2026-02-26 16:33:48 -06:00
} ,
{
value : "codex_local" as const ,
label : "Codex" ,
icon : Code ,
2026-03-03 13:05:35 -06:00
desc : "Local Codex agent"
2026-02-26 16:33:48 -06:00
} ,
{
value : "openclaw" as const ,
label : "OpenClaw" ,
icon : Bot ,
desc : "Notify OpenClaw webhook" ,
2026-03-03 13:05:35 -06:00
comingSoon : true
2026-03-03 11:29:34 -06:00
} ,
{
value : "cursor" as const ,
label : "Cursor" ,
icon : MousePointer2 ,
desc : "Cursor AI agent" ,
2026-03-03 13:05:35 -06:00
comingSoon : true
2026-02-26 16:33:48 -06:00
} ,
{
value : "process" as const ,
label : "Shell Command" ,
icon : Terminal ,
desc : "Run a process" ,
2026-03-03 13:05:35 -06:00
comingSoon : true
2026-02-26 16:33:48 -06:00
} ,
{
value : "http" as const ,
label : "HTTP Webhook" ,
icon : Globe ,
desc : "Call an endpoint" ,
2026-03-03 13:05:35 -06:00
comingSoon : true
}
] . map ( ( opt ) = > (
2026-02-17 20:07:41 -06:00
< button
2026-02-26 16:33:48 -06:00
key = { opt . value }
2026-03-03 11:29:34 -06:00
disabled = { ! ! opt . comingSoon }
2026-02-17 20:07:41 -06:00
className = { cn (
2026-03-03 11:29:34 -06:00
"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"
2026-02-17 20:07:41 -06:00
) }
2026-03-03 11:29:34 -06:00
onClick = { ( ) = > {
2026-03-03 12:41:50 -06:00
if ( opt . comingSoon ) return ;
const nextType = opt . value as AdapterType ;
setAdapterType ( nextType ) ;
if ( nextType === "codex_local" && ! model ) {
setModel ( DEFAULT_CODEX_LOCAL_MODEL ) ;
}
2026-03-03 11:29:34 -06:00
} }
2026-02-17 20:07:41 -06:00
>
2026-02-26 16:33:48 -06:00
< opt.icon className = "h-4 w-4" / >
< span className = "font-medium" > { opt . label } < / span >
< span className = "text-muted-foreground text-[10px]" >
2026-03-03 11:29:34 -06:00
{ opt . comingSoon ? "Coming soon" : opt . desc }
2026-02-26 16:33:48 -06:00
< / span >
2026-02-17 20:07:41 -06:00
< / button >
2026-02-26 16:33:48 -06:00
) ) }
< / div >
< / div >
{ /* Conditional adapter fields */ }
2026-03-03 13:05:35 -06:00
{ ( adapterType === "claude_local" ||
adapterType === "codex_local" ) && (
2026-02-26 16:33:48 -06:00
< div className = "space-y-3" >
< div >
2026-03-03 11:31:48 -06:00
< div className = "flex items-center gap-1.5 mb-1" >
< label className = "text-xs text-muted-foreground" >
Working directory
< / label >
< HintIcon text = "Paperclip works best if you create a new folder for your agents to keep their memories and stay organized. Create a new folder and put the path here." / >
< / div >
2026-02-26 16:33:48 -06:00
< div className = "flex items-center gap-2 rounded-md border border-border px-2.5 py-1.5" >
< FolderOpen className = "h-3.5 w-3.5 text-muted-foreground shrink-0" / >
< input
className = "w-full bg-transparent outline-none text-sm font-mono placeholder:text-muted-foreground/50"
placeholder = "/path/to/project"
value = { cwd }
onChange = { ( e ) = > setCwd ( e . target . value ) }
/ >
2026-03-02 16:09:07 -06:00
< ChoosePathButton / >
2026-02-26 16:33:48 -06:00
< / div >
< / div >
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
Model
< / label >
< Popover open = { modelOpen } onOpenChange = { setModelOpen } >
< PopoverTrigger asChild >
< button className = "inline-flex items-center gap-1.5 rounded-md border border-border px-2.5 py-1.5 text-sm hover:bg-accent/50 transition-colors w-full justify-between" >
2026-03-03 13:05:35 -06:00
< span
className = { cn (
! model && "text-muted-foreground"
) }
>
{ selectedModel
? selectedModel . label
: model || "Default" }
2026-02-26 16:33:48 -06:00
< / span >
< ChevronDown className = "h-3 w-3 text-muted-foreground" / >
< / button >
< / PopoverTrigger >
2026-03-03 13:05:35 -06:00
< PopoverContent
className = "w-[var(--radix-popover-trigger-width)] p-1"
align = "start"
>
2026-02-26 16:33:48 -06:00
< button
className = { cn (
"flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50" ,
! model && "bg-accent"
) }
2026-03-03 13:05:35 -06:00
onClick = { ( ) = > {
setModel ( "" ) ;
setModelOpen ( false ) ;
} }
2026-02-26 16:33:48 -06:00
>
Default
< / button >
{ ( adapterModels ? ? [ ] ) . map ( ( m ) = > (
< button
key = { m . id }
className = { cn (
"flex items-center justify-between w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50" ,
m . id === model && "bg-accent"
) }
2026-03-03 13:05:35 -06:00
onClick = { ( ) = > {
setModel ( m . id ) ;
setModelOpen ( false ) ;
} }
2026-02-26 16:33:48 -06:00
>
< span > { m . label } < / span >
2026-03-03 13:05:35 -06:00
< span className = "text-xs text-muted-foreground font-mono" >
{ m . id }
< / span >
2026-02-26 16:33:48 -06:00
< / button >
) ) }
< / PopoverContent >
< / Popover >
< / div >
< / div >
) }
2026-03-03 12:51:42 -06:00
{ isLocalAdapter && (
< div className = "space-y-2 rounded-md border border-border p-3" >
< div className = "flex items-center justify-between gap-2" >
< div >
2026-03-03 13:05:35 -06:00
< p className = "text-xs font-medium" >
Adapter environment check
< / p >
2026-03-03 12:51:42 -06:00
< p className = "text-[11px] text-muted-foreground" >
2026-03-03 13:05:35 -06:00
Runs a live probe that asks the adapter CLI to
respond with hello .
2026-03-03 12:51:42 -06:00
< / p >
< / div >
< Button
size = "sm"
variant = "outline"
className = "h-7 px-2.5 text-xs"
disabled = { adapterEnvLoading }
onClick = { ( ) = > void runAdapterEnvironmentTest ( ) }
>
{ adapterEnvLoading ? "Testing..." : "Test now" }
< / Button >
< / div >
{ adapterEnvError && (
< div className = "rounded-md border border-destructive/30 bg-destructive/10 px-2.5 py-2 text-[11px] text-destructive" >
{ adapterEnvError }
< / div >
) }
{ adapterEnvResult && (
< AdapterEnvironmentResult result = { adapterEnvResult } / >
) }
< 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 === "codex_local"
? ` ${ effectiveAdapterCommand } exec --json - `
: ` ${ effectiveAdapterCommand } --print - --output-format stream-json --verbose ` }
< / p >
< p className = "text-muted-foreground" >
2026-03-03 13:05:35 -06:00
Prompt : { " " }
< span className = "font-mono" > Respond with hello . < / span >
2026-03-03 12:51:42 -06:00
< / p >
{ adapterType === "codex_local" ? (
< p className = "text-muted-foreground" >
2026-03-03 13:05:35 -06:00
If auth fails , set { " " }
< span className = "font-mono" > OPENAI_API_KEY < / span > in
env or run { " " }
2026-03-03 12:51:42 -06:00
< span className = "font-mono" > codex login < / span > .
< / p >
) : (
< p className = "text-muted-foreground" >
2026-03-03 13:05:35 -06:00
If login is required , run { " " }
< span className = "font-mono" > claude login < / span > and
retry .
2026-03-03 12:51:42 -06:00
< / p >
) }
< / div >
< / div >
) }
2026-02-26 16:33:48 -06:00
{ adapterType === "process" && (
< div className = "space-y-3" >
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
Command
< / 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 = "e.g. node, python"
value = { command }
onChange = { ( e ) = > setCommand ( e . target . value ) }
/ >
< / div >
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
Args ( comma - separated )
< / 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 = "e.g. script.js, --flag"
value = { args }
onChange = { ( e ) = > setArgs ( e . target . value ) }
/ >
< / div >
< / div >
) }
{ ( adapterType === "http" || adapterType === "openclaw" ) && (
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
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 = "https://..."
value = { url }
onChange = { ( e ) = > setUrl ( e . target . value ) }
/ >
< / div >
) }
2026-02-17 13:24:33 -06:00
< / div >
) }
2026-02-26 16:33:48 -06:00
{ step === 3 && (
< div className = "space-y-5" >
< div className = "flex items-center gap-3 mb-1" >
< div className = "bg-muted/50 p-2" >
< ListTodo className = "h-5 w-5 text-muted-foreground" / >
< / div >
< div >
< h3 className = "font-medium" > Give it something to do < / h3 >
< p className = "text-xs text-muted-foreground" >
2026-03-03 13:05:35 -06:00
Give your agent a small task to start with — a bug fix ,
a research question , writing a script .
2026-02-26 16:33:48 -06:00
< / p >
< / div >
< / div >
2026-02-17 13:24:33 -06:00
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
2026-02-26 16:33:48 -06:00
Task title
2026-02-17 13:24:33 -06:00
< / label >
< input
2026-02-26 16:33:48 -06:00
className = "w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50"
placeholder = "e.g. Research competitor pricing"
value = { taskTitle }
onChange = { ( e ) = > setTaskTitle ( e . target . value ) }
autoFocus
2026-02-17 13:24:33 -06:00
/ >
< / div >
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
2026-02-26 16:33:48 -06:00
Description ( optional )
2026-02-17 13:24:33 -06:00
< / label >
2026-02-26 16:33:48 -06:00
< textarea
2026-03-03 12:26:28 -06:00
ref = { textareaRef }
className = "w-full rounded-md border border-border bg-transparent px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-ring placeholder:text-muted-foreground/50 resize-none min-h-[120px] max-h-[300px] overflow-y-auto"
2026-02-26 16:33:48 -06:00
placeholder = "Add more detail about what the agent should do..."
value = { taskDescription }
onChange = { ( e ) = > setTaskDescription ( e . target . value ) }
2026-02-17 13:24:33 -06:00
/ >
< / div >
< / div >
) }
2026-02-26 16:33:48 -06:00
{ step === 4 && (
< div className = "space-y-5" >
< div className = "flex items-center gap-3 mb-1" >
< div className = "bg-muted/50 p-2" >
< Rocket className = "h-5 w-5 text-muted-foreground" / >
< / div >
< div >
< h3 className = "font-medium" > Ready to launch < / h3 >
< p className = "text-xs text-muted-foreground" >
2026-03-03 13:10:06 -06:00
Everything is set up . Your assigned task already woke
the agent , so you can jump straight to the issue .
2026-02-26 16:33:48 -06:00
< / p >
< / div >
< / div >
< div className = "border border-border divide-y divide-border" >
< div className = "flex items-center gap-3 px-3 py-2.5" >
< Building2 className = "h-4 w-4 text-muted-foreground shrink-0" / >
< div className = "flex-1 min-w-0" >
2026-03-03 13:05:35 -06:00
< p className = "text-sm font-medium truncate" >
{ companyName }
< / p >
2026-02-26 16:33:48 -06:00
< p className = "text-xs text-muted-foreground" > Company < / p >
< / div >
< Check className = "h-4 w-4 text-green-500 shrink-0" / >
< / div >
< div className = "flex items-center gap-3 px-3 py-2.5" >
< Bot className = "h-4 w-4 text-muted-foreground shrink-0" / >
< div className = "flex-1 min-w-0" >
2026-03-03 13:05:35 -06:00
< p className = "text-sm font-medium truncate" >
{ agentName }
< / p >
2026-02-26 16:33:48 -06:00
< p className = "text-xs text-muted-foreground" >
{ getUIAdapter ( adapterType ) . label }
< / p >
< / div >
< Check className = "h-4 w-4 text-green-500 shrink-0" / >
< / div >
< div className = "flex items-center gap-3 px-3 py-2.5" >
< ListTodo className = "h-4 w-4 text-muted-foreground shrink-0" / >
< div className = "flex-1 min-w-0" >
2026-03-03 13:05:35 -06:00
< p className = "text-sm font-medium truncate" >
{ taskTitle }
< / p >
2026-02-26 16:33:48 -06:00
< p className = "text-xs text-muted-foreground" > Task < / p >
< / div >
< Check className = "h-4 w-4 text-green-500 shrink-0" / >
< / div >
< / div >
2026-02-17 13:24:33 -06:00
< / div >
) }
2026-02-26 16:33:48 -06:00
{ /* Error */ }
{ error && (
< div className = "mt-3" >
< p className = "text-xs text-destructive" > { error } < / p >
2026-02-17 13:24:33 -06:00
< / div >
2026-02-26 16:33:48 -06:00
) }
2026-02-17 13:24:33 -06:00
2026-02-26 16:33:48 -06:00
{ /* Footer navigation */ }
< div className = "flex items-center justify-between mt-8" >
2026-02-17 13:24:33 -06:00
< div >
2026-03-03 11:37:19 -06:00
{ step > 1 && step > ( onboardingOptions . initialStep ? ? 1 ) && (
2026-02-26 16:33:48 -06:00
< Button
variant = "ghost"
size = "sm"
onClick = { ( ) = > setStep ( ( step - 1 ) as Step ) }
disabled = { loading }
>
< ArrowLeft className = "h-3.5 w-3.5 mr-1" / >
Back
< / Button >
) }
2026-02-17 13:24:33 -06:00
< / div >
2026-02-26 16:33:48 -06:00
< div className = "flex items-center gap-2" >
{ step === 1 && (
< Button
size = "sm"
disabled = { ! companyName . trim ( ) || loading }
onClick = { handleStep1Next }
>
{ loading ? (
< Loader2 className = "h-3.5 w-3.5 mr-1 animate-spin" / >
) : (
< ArrowRight className = "h-3.5 w-3.5 mr-1" / >
) }
{ loading ? "Creating..." : "Next" }
< / Button >
) }
{ step === 2 && (
< Button
size = "sm"
2026-03-03 13:05:35 -06:00
disabled = {
! agentName . trim ( ) || loading || adapterEnvLoading
}
2026-02-26 16:33:48 -06:00
onClick = { handleStep2Next }
>
{ loading ? (
< Loader2 className = "h-3.5 w-3.5 mr-1 animate-spin" / >
) : (
< ArrowRight className = "h-3.5 w-3.5 mr-1" / >
) }
{ loading ? "Creating..." : "Next" }
< / Button >
) }
{ step === 3 && (
< Button
size = "sm"
disabled = { ! taskTitle . trim ( ) || loading }
onClick = { handleStep3Next }
>
{ loading ? (
< Loader2 className = "h-3.5 w-3.5 mr-1 animate-spin" / >
) : (
< ArrowRight className = "h-3.5 w-3.5 mr-1" / >
) }
{ loading ? "Creating..." : "Next" }
< / Button >
) }
{ step === 4 && (
< Button size = "sm" disabled = { loading } onClick = { handleLaunch } >
{ loading ? (
< Loader2 className = "h-3.5 w-3.5 mr-1 animate-spin" / >
) : (
2026-03-03 13:10:06 -06:00
< ArrowRight className = "h-3.5 w-3.5 mr-1" / >
2026-02-26 16:33:48 -06:00
) }
2026-03-03 13:10:06 -06:00
{ loading ? "Opening..." : "Open Issue" }
2026-02-26 16:33:48 -06:00
< / Button >
) }
2026-02-17 13:24:33 -06:00
< / div >
< / div >
< / div >
< / div >
2026-02-26 16:33:48 -06:00
{ /* Right half — ASCII art (hidden on mobile) */ }
< div className = "hidden md:block w-1/2 overflow-hidden" >
< AsciiArtAnimation / >
2026-02-17 13:24:33 -06:00
< / div >
< / div >
2026-02-26 16:33:48 -06:00
< / DialogPortal >
2026-02-17 13:24:33 -06:00
< / Dialog >
) ;
}
2026-03-03 12:51:42 -06:00
2026-03-03 13:05:35 -06:00
function AdapterEnvironmentResult ( {
result
} : {
result : AdapterEnvironmentTestResult ;
} ) {
2026-03-03 12:51:42 -06:00
const statusLabel =
2026-03-03 13:05:35 -06:00
result . status === "pass"
? "Passed"
: result . status === "warn"
? "Warnings"
: "Failed" ;
2026-03-03 12:51:42 -06:00
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" ;
return (
< div className = { ` rounded-md border px-2.5 py-2 text-[11px] ${ statusClass } ` } >
< div className = "flex items-center justify-between gap-2" >
< span className = "font-medium" > { statusLabel } < / span >
< span className = "opacity-80" >
{ new Date ( result . testedAt ) . toLocaleTimeString ( ) }
< / span >
< / div >
< div className = "mt-1.5 space-y-1" >
{ result . checks . map ( ( check , idx ) = > (
2026-03-03 13:05:35 -06:00
< div
key = { ` ${ check . code } - ${ idx } ` }
className = "leading-relaxed break-words"
>
2026-03-03 12:51:42 -06:00
< span className = "font-medium uppercase tracking-wide opacity-80" >
{ check . level }
< / span >
< span className = "mx-1 opacity-60" > · < / span >
< span > { check . message } < / span >
2026-03-03 13:05:35 -06:00
{ check . detail && (
< span className = "block opacity-75 break-all" >
( { check . detail } )
< / span >
) }
{ check . hint && (
< span className = "block opacity-90 break-words" >
Hint : { check . hint }
< / span >
) }
2026-03-03 12:51:42 -06:00
< / div >
) ) }
< / div >
< / div >
) ;
}