1- import { motion } from "framer-motion" ;
2- import { Check , Pencil , Plus , Server , Trash2 } from "lucide-react" ;
3- import { useState } from "react" ;
4- import { SettingsForm } from "@/components/settings/SettingsForm" ;
1+ import { AnimatePresence , motion } from "framer-motion" ;
2+ import { Check , ChevronRight , Cloud , Pencil , Plus , Server , Sparkles , Trash2 } from "lucide-react" ;
3+ import { useEffect , useState } from "react" ;
4+ import { type ConnectionPreset , SettingsForm } from "@/components/settings/SettingsForm" ;
55import { Button } from "@/components/ui/button" ;
66import { Muted } from "@/components/ui/typography" ;
77import { useInstances } from "@/hooks/useInstances" ;
8- import type { Instance } from "@/lib/config" ;
8+ import { checkConnection , HONCHO_CLOUD_URL , type Instance , isCloudInstance } from "@/lib/config" ;
99import { COLOR } from "@/lib/constants" ;
1010
11- type Mode = { kind : "list" } | { kind : "create" } | { kind : "edit" ; id : string } ;
11+ const LOCALHOST_PROBE_URL = "http://localhost:8000" ;
12+
13+ type Mode =
14+ | { kind : "list" }
15+ | { kind : "choose-type" }
16+ | { kind : "create" ; preset : ConnectionPreset }
17+ | { kind : "edit" ; id : string } ;
1218
1319interface InstancesManagerProps {
1420 onActivated ?: ( ) => void ;
1521}
1622
1723export function InstancesManager ( { onActivated } : InstancesManagerProps ) {
1824 const { instances, activeId, activate, remove } = useInstances ( ) ;
19- const [ mode , setMode ] = useState < Mode > ( { kind : "list" } ) ;
25+ const isFirstRun = instances . length === 0 ;
26+ const [ mode , setMode ] = useState < Mode > ( isFirstRun ? { kind : "choose-type" } : { kind : "list" } ) ;
27+
28+ const backFromCreate = ( ) => setMode ( isFirstRun ? { kind : "choose-type" } : { kind : "list" } ) ;
29+
30+ if ( mode . kind === "choose-type" ) {
31+ return (
32+ < ConnectionTypeChooser
33+ onPick = { ( preset ) => setMode ( { kind : "create" , preset } ) }
34+ onCancel = { isFirstRun ? undefined : ( ) => setMode ( { kind : "list" } ) }
35+ />
36+ ) ;
37+ }
2038
2139 if ( mode . kind === "create" ) {
2240 return (
2341 < SettingsForm
2442 instance = { null }
43+ preset = { mode . preset }
2544 onSaved = { ( ) => {
2645 setMode ( { kind : "list" } ) ;
2746 onActivated ?.( ) ;
2847 } }
29- onCancel = { instances . length > 0 ? ( ) => setMode ( { kind : "list" } ) : undefined }
30- hideCancel = { instances . length === 0 }
48+ onCancel = { backFromCreate }
49+ hideCancel = { false }
50+ submitLabel = { isFirstRun ? "Save Connection" : undefined }
3151 />
3252 ) ;
3353 }
@@ -44,17 +64,6 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) {
4464 ) ;
4565 }
4666
47- if ( instances . length === 0 ) {
48- return (
49- < SettingsForm
50- instance = { null }
51- onSaved = { ( ) => onActivated ?.( ) }
52- hideCancel
53- submitLabel = "Save Connection"
54- />
55- ) ;
56- }
57-
5867 return (
5968 < div className = "space-y-3" >
6069 < div className = "space-y-2" >
@@ -76,7 +85,7 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) {
7685 < Button
7786 type = "button"
7887 variant = "ghost"
79- onClick = { ( ) => setMode ( { kind : "create " } ) }
88+ onClick = { ( ) => setMode ( { kind : "choose-type " } ) }
8089 className = "w-full py-2.5 px-4 rounded-xl flex items-center justify-center gap-2"
8190 >
8291 < Plus className = "w-4 h-4" strokeWidth = { 1.5 } />
@@ -86,6 +95,153 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) {
8695 ) ;
8796}
8897
98+ interface ConnectionTypeChooserProps {
99+ onPick : ( preset : ConnectionPreset ) => void ;
100+ onCancel ?: ( ) => void ;
101+ }
102+
103+ function ConnectionTypeChooser ( { onPick, onCancel } : ConnectionTypeChooserProps ) {
104+ const [ localhostDetected , setLocalhostDetected ] = useState ( false ) ;
105+
106+ useEffect ( ( ) => {
107+ let cancelled = false ;
108+ void checkConnection ( LOCALHOST_PROBE_URL ) . then ( ( result ) => {
109+ if ( cancelled ) return ;
110+ if ( result . status === "ok" || result . status === "auth-required" ) {
111+ setLocalhostDetected ( true ) ;
112+ }
113+ } ) ;
114+ return ( ) => {
115+ cancelled = true ;
116+ } ;
117+ } , [ ] ) ;
118+
119+ return (
120+ < div
121+ className = "rounded-2xl p-6 space-y-3"
122+ style = { {
123+ background : "var(--bg-2)" ,
124+ border : "1px solid var(--border)" ,
125+ } }
126+ >
127+ < div className = "mb-2" >
128+ < h2 className = "text-base font-medium" style = { { color : "var(--text-1)" } } >
129+ How do you want to connect?
130+ </ h2 >
131+ < Muted className = "text-xs mt-1" >
132+ You can add more connections later — Cloud, self-hosted, or both.
133+ </ Muted >
134+ </ div >
135+
136+ < AnimatePresence >
137+ { localhostDetected && (
138+ < motion . button
139+ type = "button"
140+ initial = { { opacity : 0 , height : 0 } }
141+ animate = { { opacity : 1 , height : "auto" } }
142+ exit = { { opacity : 0 , height : 0 } }
143+ onClick = { ( ) => onPick ( "self-hosted" ) }
144+ className = "w-full overflow-hidden rounded-xl p-3 flex items-center gap-2.5 text-left"
145+ style = { {
146+ background : COLOR . successDim ,
147+ border : `1px solid ${ COLOR . successBorder } ` ,
148+ } }
149+ >
150+ < Sparkles
151+ className = "w-4 h-4 shrink-0"
152+ style = { { color : COLOR . success } }
153+ strokeWidth = { 1.5 }
154+ />
155+ < div className = "min-w-0 flex-1" >
156+ < p className = "text-xs font-medium" style = { { color : COLOR . success } } >
157+ Detected Honcho at { LOCALHOST_PROBE_URL . replace ( / ^ h t t p s ? : \/ \/ / , "" ) }
158+ </ p >
159+ < Muted className = "text-xs mt-0.5" > Tap to connect to it</ Muted >
160+ </ div >
161+ </ motion . button >
162+ ) }
163+ </ AnimatePresence >
164+
165+ < ConnectionTypeButton
166+ icon = { Cloud }
167+ title = "Honcho Cloud"
168+ description = { `Hosted at ${ HONCHO_CLOUD_URL . replace ( / ^ h t t p s ? : \/ \/ / , "" ) } — sign in with your API key` }
169+ accent
170+ onClick = { ( ) => onPick ( "cloud" ) }
171+ />
172+
173+ < ConnectionTypeButton
174+ icon = { Server }
175+ title = "Self-Hosted"
176+ description = "Connect to your own Honcho deployment"
177+ onClick = { ( ) => onPick ( "self-hosted" ) }
178+ />
179+
180+ { onCancel && (
181+ < div className = "pt-1" >
182+ < Button
183+ type = "button"
184+ variant = "ghost"
185+ onClick = { onCancel }
186+ className = "w-full py-2 px-4 rounded-xl"
187+ >
188+ Cancel
189+ </ Button >
190+ </ div >
191+ ) }
192+ </ div >
193+ ) ;
194+ }
195+
196+ interface ConnectionTypeButtonProps {
197+ icon : typeof Cloud ;
198+ title : string ;
199+ description : string ;
200+ accent ?: boolean ;
201+ onClick : ( ) => void ;
202+ }
203+
204+ function ConnectionTypeButton ( {
205+ icon : Icon ,
206+ title,
207+ description,
208+ accent,
209+ onClick,
210+ } : ConnectionTypeButtonProps ) {
211+ return (
212+ < button
213+ type = "button"
214+ onClick = { onClick }
215+ className = "w-full rounded-xl p-4 flex items-center gap-3 text-left transition-colors"
216+ style = { {
217+ background : "var(--surface)" ,
218+ border : `1px solid ${ accent ? "var(--accent-border)" : "var(--border)" } ` ,
219+ } }
220+ >
221+ < div
222+ className = "w-10 h-10 rounded-lg flex items-center justify-center shrink-0"
223+ style = { {
224+ background : accent ? "var(--accent)" : "var(--bg-2)" ,
225+ color : accent ? "white" : "var(--text-2)" ,
226+ } }
227+ >
228+ < Icon className = "w-5 h-5" strokeWidth = { 1.5 } />
229+ </ div >
230+ < div className = "min-w-0 flex-1" >
231+ < p className = "text-sm font-medium" style = { { color : "var(--text-1)" } } >
232+ { title }
233+ </ p >
234+ < Muted className = "text-xs mt-0.5" > { description } </ Muted >
235+ </ div >
236+ < ChevronRight
237+ className = "w-4 h-4 shrink-0"
238+ style = { { color : "var(--text-3)" } }
239+ strokeWidth = { 1.5 }
240+ />
241+ </ button >
242+ ) ;
243+ }
244+
89245interface InstanceRowProps {
90246 instance : Instance ;
91247 active : boolean ;
@@ -96,6 +252,7 @@ interface InstanceRowProps {
96252
97253function InstanceRow ( { instance, active, onActivate, onEdit, onDelete } : InstanceRowProps ) {
98254 const [ confirmingDelete , setConfirmingDelete ] = useState ( false ) ;
255+ const cloud = isCloudInstance ( instance ) ;
99256
100257 return (
101258 < motion . div
@@ -122,6 +279,8 @@ function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: Instanc
122279 >
123280 { active ? (
124281 < Check className = "w-4 h-4" strokeWidth = { 2 } />
282+ ) : cloud ? (
283+ < Cloud className = "w-4 h-4" strokeWidth = { 1.5 } />
125284 ) : (
126285 < Server className = "w-4 h-4" strokeWidth = { 1.5 } />
127286 ) }
@@ -134,7 +293,7 @@ function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: Instanc
134293 { instance . name }
135294 </ p >
136295 < Muted className = "text-xs font-mono truncate" >
137- { instance . baseUrl . replace ( / ^ h t t p s ? : \/ \/ / , "" ) }
296+ { cloud ? "Honcho Cloud" : instance . baseUrl . replace ( / ^ h t t p s ? : \/ \/ / , "" ) }
138297 </ Muted >
139298 </ div >
140299 </ button >
0 commit comments