2026-03-05 15:24:20 +01:00
import { useEffect , useState , useRef , useCallback , useMemo } 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-03-03 13:21:37 -06:00
import { Dialog , 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-03-05 15:52:59 +01:00
import { extractModelName , extractProviderIdWithFallback } from "../lib/model-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-03-05 06:31:22 -06:00
import { DEFAULT_CURSOR_LOCAL_MODEL } from "@paperclipai/adapter-cursor-local" ;
2026-03-08 16:43:34 +05:30
import { DEFAULT_GEMINI_LOCAL_MODEL } from "@paperclipai/adapter-gemini-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-03-05 15:24:20 +01:00
import { OpenCodeLogoIcon } from "./OpenCodeLogoIcon" ;
2026-02-17 13:24:33 -06:00
import {
Building2 ,
Bot ,
2026-02-26 16:33:48 -06:00
Code ,
2026-03-08 16:43:34 +05:30
Gem ,
2026-02-17 13:24:33 -06:00
ListTodo ,
Rocket ,
ArrowLeft ,
ArrowRight ,
Terminal ,
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"
2026-03-08 16:43:34 +05:30
| "gemini_local"
2026-03-04 16:48:54 -06:00
| "opencode_local"
2026-03-06 18:47:44 -08:00
| "pi_local"
2026-03-05 06:31:22 -06:00
| "cursor"
2026-03-03 13:05:35 -06:00
| "process"
| "http"
2026-03-07 08:59:29 -06:00
| "openclaw_gateway" ;
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-03-05 15:24:20 +01:00
const [ modelSearch , setModelSearch ] = useState ( "" ) ;
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-03-04 14:40:12 -06:00
const [ forceUnsetAnthropicApiKey , setForceUnsetAnthropicApiKey ] =
useState ( false ) ;
const [ unsetAnthropicLoading , setUnsetAnthropicLoading ] = 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-03-05 15:24:20 +01:00
const {
data : adapterModels ,
error : adapterModelsError ,
2026-03-05 16:07:12 +01:00
isLoading : adapterModelsLoading ,
isFetching : adapterModelsFetching ,
2026-03-05 15:24:20 +01:00
} = useQuery ( {
queryKey :
createdCompanyId
? queryKeys . agents . adapterModels ( createdCompanyId , adapterType )
: [ "agents" , "none" , "adapter-models" , adapterType ] ,
queryFn : ( ) = > agentsApi . adapterModels ( createdCompanyId ! , adapterType ) ,
enabled : Boolean ( createdCompanyId ) && onboardingOpen && step === 2
2026-02-17 20:07:41 -06:00
} ) ;
2026-03-03 13:05:35 -06:00
const isLocalAdapter =
2026-03-08 16:43:34 +05:30
adapterType === "claude_local" ||
adapterType === "codex_local" ||
adapterType === "gemini_local" ||
adapterType === "opencode_local" ||
adapterType === "cursor" ;
2026-03-03 13:05:35 -06:00
const effectiveAdapterCommand =
2026-03-04 16:48:54 -06:00
command . trim ( ) ||
( adapterType === "codex_local"
? "codex"
2026-03-08 16:43:34 +05:30
: adapterType === "gemini_local"
? "gemini"
2026-03-05 06:31:22 -06:00
: adapterType === "cursor"
? "agent"
2026-03-06 15:23:55 +00:00
: adapterType === "opencode_local"
? "opencode"
: "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-03-04 14:40:12 -06:00
const hasAnthropicApiKeyOverrideCheck =
adapterEnvResult ? . checks . some (
( check ) = >
check . code === "claude_anthropic_api_key_overrides_subscription"
) ? ? false ;
const shouldSuggestUnsetAnthropicApiKey =
adapterType === "claude_local" &&
adapterEnvResult ? . status === "fail" &&
hasAnthropicApiKeyOverrideCheck ;
2026-03-05 15:24:20 +01:00
const filteredModels = useMemo ( ( ) = > {
const query = modelSearch . trim ( ) . toLowerCase ( ) ;
return ( adapterModels ? ? [ ] ) . filter ( ( entry ) = > {
if ( ! query ) return true ;
2026-03-05 15:52:59 +01:00
const provider = extractProviderIdWithFallback ( entry . id , "" ) ;
2026-03-05 15:24:20 +01:00
return (
entry . id . toLowerCase ( ) . includes ( query ) ||
entry . label . toLowerCase ( ) . includes ( query ) ||
provider . toLowerCase ( ) . includes ( query )
) ;
} ) ;
} , [ adapterModels , modelSearch ] ) ;
const groupedModels = useMemo ( ( ) = > {
if ( adapterType !== "opencode_local" ) {
return [
{
provider : "models" ,
entries : [ . . . filteredModels ] . sort ( ( a , b ) = > a . id . localeCompare ( b . id ) ) ,
} ,
] ;
}
const groups = new Map < string , Array < { id : string ; label : string } > > ( ) ;
for ( const entry of filteredModels ) {
2026-03-05 15:52:59 +01:00
const provider = extractProviderIdWithFallback ( entry . id ) ;
2026-03-05 15:24:20 +01:00
const bucket = groups . get ( provider ) ? ? [ ] ;
bucket . push ( entry ) ;
groups . set ( provider , bucket ) ;
}
return Array . from ( groups . entries ( ) )
. sort ( ( [ a ] , [ b ] ) = > a . localeCompare ( b ) )
. map ( ( [ provider , entries ] ) = > ( {
provider ,
entries : [ . . . entries ] . sort ( ( a , b ) = > a . id . localeCompare ( b . id ) ) ,
} ) ) ;
} , [ filteredModels , adapterType ] ) ;
2026-02-17 20:07:41 -06:00
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-03-04 14:40:12 -06:00
setForceUnsetAnthropicApiKey ( false ) ;
setUnsetAnthropicLoading ( 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 ) ;
2026-03-04 14:40:12 -06:00
const config = adapter . buildAdapterConfig ( {
2026-02-18 13:53:03 -06:00
. . . defaultCreateValues ,
adapterType ,
cwd ,
2026-03-03 13:05:35 -06:00
model :
adapterType === "codex_local"
? model || DEFAULT_CODEX_LOCAL_MODEL
2026-03-08 16:43:34 +05:30
: adapterType === "gemini_local"
? model || DEFAULT_GEMINI_LOCAL_MODEL
2026-03-05 06:31:22 -06:00
: adapterType === "cursor"
? model || DEFAULT_CURSOR_LOCAL_MODEL
2026-03-03 13:05:35 -06:00
: 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-03-04 14:40:12 -06:00
if ( adapterType === "claude_local" && forceUnsetAnthropicApiKey ) {
const env =
typeof config . env === "object" &&
config . env !== null &&
! Array . isArray ( config . env )
? { . . . ( config . env as Record < string , unknown > ) }
: { } ;
env . ANTHROPIC_API_KEY = { type : "plain" , value : "" } ;
config . env = env ;
}
return config ;
2026-02-17 13:24:33 -06:00
}
2026-03-04 14:40:12 -06:00
async function runAdapterEnvironmentTest (
adapterConfigOverride? : Record < string , unknown >
) : Promise < AdapterEnvironmentTestResult | null > {
2026-03-03 12:51:42 -06:00
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 ,
{
2026-03-04 14:40:12 -06:00
adapterConfig : adapterConfigOverride ? ? buildAdapterConfig ( )
2026-03-03 13:05:35 -06:00
}
) ;
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-05 15:24:20 +01:00
if ( adapterType === "opencode_local" ) {
const selectedModelId = model . trim ( ) ;
if ( ! selectedModelId ) {
setError ( "OpenCode requires an explicit model in provider/model format." ) ;
return ;
}
if ( adapterModelsError ) {
setError (
adapterModelsError instanceof Error
? adapterModelsError . message
: "Failed to load OpenCode models." ,
) ;
return ;
}
2026-03-05 16:07:12 +01:00
if ( adapterModelsLoading || adapterModelsFetching ) {
setError ( "OpenCode models are still loading. Please wait and try again." ) ;
return ;
}
2026-03-05 15:24:20 +01:00
const discoveredModels = adapterModels ? ? [ ] ;
if ( ! discoveredModels . some ( ( entry ) = > entry . id === selectedModelId ) ) {
setError (
discoveredModels . length === 0
? "No OpenCode models discovered. Run `opencode models` and authenticate providers."
: ` Configured OpenCode model is unavailable: ${ selectedModelId } ` ,
) ;
return ;
}
}
2026-03-03 12:51:42 -06:00
if ( isLocalAdapter ) {
const result = adapterEnvResult ? ? ( await runAdapterEnvironmentTest ( ) ) ;
if ( ! result ) 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 ) ;
}
}
2026-03-04 14:40:12 -06:00
async function handleUnsetAnthropicApiKey() {
if ( ! createdCompanyId || unsetAnthropicLoading ) return ;
setUnsetAnthropicLoading ( true ) ;
setError ( null ) ;
setAdapterEnvError ( null ) ;
setForceUnsetAnthropicApiKey ( true ) ;
const configWithUnset = ( ( ) = > {
const config = buildAdapterConfig ( ) ;
const env =
typeof config . env === "object" &&
config . env !== null &&
! Array . isArray ( config . env )
? { . . . ( config . env as Record < string , unknown > ) }
: { } ;
env . ANTHROPIC_API_KEY = { type : "plain" , value : "" } ;
config . env = env ;
return config ;
} ) ( ) ;
try {
if ( createdAgentId ) {
await agentsApi . update (
createdAgentId ,
{ adapterConfig : configWithUnset } ,
createdCompanyId
) ;
queryClient . invalidateQueries ( {
queryKey : queryKeys.agents.list ( createdCompanyId )
} ) ;
}
const result = await runAdapterEnvironmentTest ( configWithUnset ) ;
if ( result ? . status === "fail" ) {
setError (
"Retried with ANTHROPIC_API_KEY unset in adapter config, but the environment test is still failing."
) ;
}
} catch ( err ) {
setError (
err instanceof Error
? err . message
: "Failed to unset ANTHROPIC_API_KEY and retry."
) ;
} finally {
setUnsetAnthropicLoading ( false ) ;
}
}
2026-02-17 13:24:33 -06:00
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 ) {
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 >
2026-03-03 13:21:37 -06:00
{ / * P l a i n d i v i n s t e a d o f D i a l o g O v e r l a y — R a d i x ' s o v e r l a y w r a p s i n
RemoveScroll which blocks wheel events on our custom ( non - DialogContent )
scroll container . A plain div preserves the background without scroll - locking . * / }
< div className = "fixed inset-0 z-50 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 */ }
2026-03-11 13:55:49 -05:00
< 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"
) } >
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-03-11 13:36:51 -05:00
{ /* 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 >
) ) }
2026-02-17 13:24:33 -06:00
< / 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-03-11 12:02:27 -05:00
< 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" ) } >
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 >
2026-03-11 12:02:27 -05:00
< 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" ) } >
2026-02-26 16:33:48 -06:00
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-05 06:04:42 -06:00
desc : "Local Claude agent" ,
recommended : true
2026-02-26 16:33:48 -06:00
} ,
{
value : "codex_local" as const ,
label : "Codex" ,
icon : Code ,
2026-03-05 06:04:42 -06:00
desc : "Local Codex agent" ,
recommended : true
2026-02-26 16:33:48 -06:00
} ,
2026-03-08 16:43:34 +05:30
{
value : "gemini_local" as const ,
label : "Gemini CLI" ,
icon : Gem ,
desc : "Local Gemini agent"
} ,
2026-03-05 15:24:20 +01:00
{
value : "opencode_local" as const ,
label : "OpenCode" ,
icon : OpenCodeLogoIcon ,
desc : "Local multi-provider agent"
} ,
2026-03-06 18:47:44 -08:00
{
value : "pi_local" as const ,
label : "Pi" ,
icon : Terminal ,
desc : "Local Pi agent"
} ,
2026-03-07 08:59:29 -06:00
{
value : "openclaw_gateway" as const ,
label : "OpenClaw Gateway" ,
icon : Bot ,
2026-03-07 18:19:06 -06:00
desc : "Invoke OpenClaw via gateway protocol" ,
comingSoon : true ,
disabledLabel : "Configure OpenClaw within the App"
2026-03-07 08:59:29 -06:00
} ,
2026-03-03 11:29:34 -06:00
{
value : "cursor" as const ,
label : "Cursor" ,
icon : MousePointer2 ,
2026-03-05 06:31:22 -06:00
desc : "Local Cursor agent"
2026-03-03 13:05:35 -06:00
}
] . 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-08 16:43:34 +05:30
} else if ( nextType === "gemini_local" && ! model ) {
setModel ( DEFAULT_GEMINI_LOCAL_MODEL ) ;
2026-03-05 06:31:22 -06:00
} else if ( nextType === "cursor" && ! model ) {
setModel ( DEFAULT_CURSOR_LOCAL_MODEL ) ;
2026-03-03 12:41:50 -06:00
}
2026-03-05 15:24:20 +01:00
if ( nextType === "opencode_local" ) {
if ( ! model . includes ( "/" ) ) {
setModel ( "" ) ;
}
return ;
2026-03-03 12:41:50 -06:00
}
2026-03-05 15:24:20 +01:00
setModel ( "" ) ;
2026-03-03 11:29:34 -06:00
} }
2026-02-17 20:07:41 -06:00
>
2026-03-05 06:04:42 -06:00
{ opt . recommended && (
2026-03-07 07:44:46 -08:00
< span className = "absolute -top-1.5 right-1.5 bg-green-500 text-white text-[9px] font-semibold px-1.5 py-0.5 rounded-full leading-none" >
2026-03-05 06:04:42 -06:00
Recommended
< / span >
) }
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-07 18:19:06 -06:00
{ opt . comingSoon
? ( opt as { disabledLabel? : string } ) . disabledLabel ? ?
"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" ||
2026-03-04 16:48:54 -06:00
adapterType === "codex_local" ||
2026-03-08 16:43:34 +05:30
adapterType === "gemini_local" ||
2026-03-05 06:31:22 -06:00
adapterType === "opencode_local" ||
2026-03-06 18:47:44 -08:00
adapterType === "pi_local" ||
2026-03-05 06:31:22 -06:00
adapterType === "cursor" ) && (
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 >
2026-03-05 15:24:20 +01:00
< Popover
open = { modelOpen }
onOpenChange = { ( next ) = > {
setModelOpen ( next ) ;
if ( ! next ) setModelSearch ( "" ) ;
} }
>
2026-02-26 16:33:48 -06:00
< 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
2026-03-05 15:24:20 +01:00
: model ||
( adapterType === "opencode_local"
? "Select model (required)"
: "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-03-05 15:24:20 +01:00
< input
className = "w-full px-2 py-1.5 text-xs bg-transparent outline-none border-b border-border mb-1 placeholder:text-muted-foreground/50"
placeholder = "Search models..."
value = { modelSearch }
onChange = { ( e ) = > setModelSearch ( e . target . value ) }
autoFocus
/ >
{ adapterType !== "opencode_local" && (
2026-02-26 16:33:48 -06:00
< button
className = { cn (
2026-03-05 15:24:20 +01:00
"flex items-center gap-2 w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50" ,
! model && "bg-accent"
2026-02-26 16:33:48 -06:00
) }
2026-03-03 13:05:35 -06:00
onClick = { ( ) = > {
2026-03-05 15:24:20 +01:00
setModel ( "" ) ;
2026-03-03 13:05:35 -06:00
setModelOpen ( false ) ;
} }
2026-02-26 16:33:48 -06:00
>
2026-03-05 15:24:20 +01:00
Default
< / button >
) }
< div className = "max-h-[240px] overflow-y-auto" >
{ groupedModels . map ( ( group ) = > (
< 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 } )
< / div >
) }
{ group . entries . map ( ( m ) = > (
< button
key = { m . id }
className = { cn (
"flex items-center w-full px-2 py-1.5 text-sm rounded hover:bg-accent/50" ,
m . id === model && "bg-accent"
) }
onClick = { ( ) = > {
setModel ( m . id ) ;
setModelOpen ( false ) ;
} }
>
< span className = "block w-full text-left truncate" title = { m . id } >
{ adapterType === "opencode_local" ? extractModelName ( m . id ) : m . label }
< / span >
< / button >
) ) }
< / div >
) ) }
< / div >
{ filteredModels . length === 0 && (
< p className = "px-2 py-1.5 text-xs text-muted-foreground" >
No models discovered .
< / p >
) }
2026-02-26 16:33:48 -06:00
< / 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 } / >
) }
2026-03-04 14:40:12 -06:00
{ 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 .
< / p >
< Button
size = "sm"
variant = "outline"
className = "h-7 px-2.5 text-xs"
disabled = { adapterEnvLoading || unsetAnthropicLoading }
onClick = { ( ) = > void handleUnsetAnthropicApiKey ( ) }
>
{ unsetAnthropicLoading ? "Retrying..." : "Unset ANTHROPIC_API_KEY" }
< / Button >
< / div >
) }
2026-03-03 12:51:42 -06:00
< 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" >
2026-03-05 06:31:22 -06:00
{ adapterType === "cursor"
? ` ${ effectiveAdapterCommand } -p --mode ask --output-format json \ "Respond with hello. \ " `
: adapterType === "codex_local"
2026-03-03 12:51:42 -06:00
? ` ${ effectiveAdapterCommand } exec --json - `
2026-03-08 16:43:34 +05:30
: adapterType === "gemini_local"
? ` ${ effectiveAdapterCommand } --output-format json \ "Respond with hello. \ " `
2026-03-04 16:48:54 -06:00
: adapterType === "opencode_local"
2026-03-06 15:23:55 +00:00
? ` ${ effectiveAdapterCommand } run --format json "Respond with hello." `
2026-03-03 12:51:42 -06:00
: ` ${ 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 >
2026-03-08 16:43:34 +05:30
{ adapterType === "cursor" || adapterType === "codex_local" || adapterType === "gemini_local" || adapterType === "opencode_local" ? (
2026-03-03 12:51:42 -06:00
< p className = "text-muted-foreground" >
2026-03-03 13:05:35 -06:00
If auth fails , set { " " }
2026-03-05 06:31:22 -06:00
< span className = "font-mono" >
2026-03-08 16:43:34 +05:30
{ adapterType === "cursor"
? "CURSOR_API_KEY"
: adapterType === "gemini_local"
? "GEMINI_API_KEY"
: "OPENAI_API_KEY" }
2026-03-05 06:31:22 -06:00
< / span > { " " }
in
2026-03-03 13:05:35 -06:00
env or run { " " }
2026-03-04 16:48:54 -06:00
< span className = "font-mono" >
2026-03-05 06:31:22 -06:00
{ adapterType === "cursor"
? "agent login"
: adapterType === "codex_local"
? "codex login"
2026-03-08 16:43:34 +05:30
: adapterType === "gemini_local"
? "gemini auth"
2026-03-05 06:31:22 -06:00
: "opencode auth login" }
2026-03-04 16:48:54 -06:00
< / span > .
2026-03-03 12:51:42 -06:00
< / 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 >
) }
2026-03-07 18:50:25 -06:00
{ ( adapterType === "http" || adapterType === "openclaw_gateway" ) && (
2026-02-26 16:33:48 -06:00
< div >
< label className = "text-xs text-muted-foreground mb-1 block" >
2026-03-07 08:59:29 -06:00
{ adapterType === "openclaw_gateway" ? "Gateway URL" : "Webhook URL" }
2026-02-26 16:33:48 -06:00
< / 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"
2026-03-07 08:59:29 -06:00
placeholder = { adapterType === "openclaw_gateway" ? "ws://127.0.0.1:18789" : "https://..." }
2026-02-26 16:33:48 -06:00
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) */ }
2026-03-11 13:55:49 -05:00
< 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"
) } >
2026-02-26 16:33:48 -06:00
< 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 >
) ;
}