Skip to content

Commit f071762

Browse files
committed
feat: add Honcho Cloud connection preset
Adds a "choose-type" step to the settings flow so users can pick between Honcho Cloud (https://api.honcho.dev, API key required) and Self-Hosted (URL + optional token) when creating a connection. Multi-instance support already exists in the data layer, so cloud and self-hosted instances can coexist. - new HONCHO_CLOUD_URL constant and isCloudInstance helper in config.ts - SettingsForm accepts a preset prop; cloud variant locks the endpoint and enforces an API key - InstancesManager gains a ConnectionTypeChooser entry point and renders a Cloud icon for cloud instances in the list - unit tests for both preset paths and cloud edit-mode detection
1 parent 4fc54a3 commit f071762

6 files changed

Lines changed: 315 additions & 49 deletions

File tree

packages/web/src/components/settings/InstancesManager.tsx

Lines changed: 133 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,51 @@
11
import { motion } from "framer-motion";
2-
import { Check, Pencil, Plus, Server, Trash2 } from "lucide-react";
2+
import { Check, ChevronRight, Cloud, Pencil, Plus, Server, Trash2 } from "lucide-react";
33
import { useState } from "react";
4-
import { SettingsForm } from "@/components/settings/SettingsForm";
4+
import { type ConnectionPreset, SettingsForm } from "@/components/settings/SettingsForm";
55
import { Button } from "@/components/ui/button";
66
import { Muted } from "@/components/ui/typography";
77
import { useInstances } from "@/hooks/useInstances";
8-
import type { Instance } from "@/lib/config";
8+
import { HONCHO_CLOUD_URL, type Instance, isCloudInstance } from "@/lib/config";
99
import { COLOR } from "@/lib/constants";
1010

11-
type Mode = { kind: "list" } | { kind: "create" } | { kind: "edit"; id: string };
11+
type Mode =
12+
| { kind: "list" }
13+
| { kind: "choose-type" }
14+
| { kind: "create"; preset: ConnectionPreset }
15+
| { kind: "edit"; id: string };
1216

1317
interface InstancesManagerProps {
1418
onActivated?: () => void;
1519
}
1620

1721
export function InstancesManager({ onActivated }: InstancesManagerProps) {
1822
const { instances, activeId, activate, remove } = useInstances();
19-
const [mode, setMode] = useState<Mode>({ kind: "list" });
23+
const isFirstRun = instances.length === 0;
24+
const [mode, setMode] = useState<Mode>(isFirstRun ? { kind: "choose-type" } : { kind: "list" });
25+
26+
const backFromCreate = () => setMode(isFirstRun ? { kind: "choose-type" } : { kind: "list" });
27+
28+
if (mode.kind === "choose-type") {
29+
return (
30+
<ConnectionTypeChooser
31+
onPick={(preset) => setMode({ kind: "create", preset })}
32+
onCancel={isFirstRun ? undefined : () => setMode({ kind: "list" })}
33+
/>
34+
);
35+
}
2036

2137
if (mode.kind === "create") {
2238
return (
2339
<SettingsForm
2440
instance={null}
41+
preset={mode.preset}
2542
onSaved={() => {
2643
setMode({ kind: "list" });
2744
onActivated?.();
2845
}}
29-
onCancel={instances.length > 0 ? () => setMode({ kind: "list" }) : undefined}
30-
hideCancel={instances.length === 0}
46+
onCancel={backFromCreate}
47+
hideCancel={false}
48+
submitLabel={isFirstRun ? "Save Connection" : undefined}
3149
/>
3250
);
3351
}
@@ -44,17 +62,6 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) {
4462
);
4563
}
4664

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-
5865
return (
5966
<div className="space-y-3">
6067
<div className="space-y-2">
@@ -76,7 +83,7 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) {
7683
<Button
7784
type="button"
7885
variant="ghost"
79-
onClick={() => setMode({ kind: "create" })}
86+
onClick={() => setMode({ kind: "choose-type" })}
8087
className="w-full py-2.5 px-4 rounded-xl flex items-center justify-center gap-2"
8188
>
8289
<Plus className="w-4 h-4" strokeWidth={1.5} />
@@ -86,6 +93,109 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) {
8693
);
8794
}
8895

96+
interface ConnectionTypeChooserProps {
97+
onPick: (preset: ConnectionPreset) => void;
98+
onCancel?: () => void;
99+
}
100+
101+
function ConnectionTypeChooser({ onPick, onCancel }: ConnectionTypeChooserProps) {
102+
return (
103+
<div
104+
className="rounded-2xl p-6 space-y-3"
105+
style={{
106+
background: "var(--bg-2)",
107+
border: "1px solid var(--border)",
108+
}}
109+
>
110+
<div className="mb-2">
111+
<h2 className="text-base font-medium" style={{ color: "var(--text-1)" }}>
112+
How do you want to connect?
113+
</h2>
114+
<Muted className="text-xs mt-1">
115+
You can add more connections later — Cloud, self-hosted, or both.
116+
</Muted>
117+
</div>
118+
119+
<ConnectionTypeButton
120+
icon={Cloud}
121+
title="Honcho Cloud"
122+
description={`Hosted at ${HONCHO_CLOUD_URL.replace(/^https?:\/\//, "")} — sign in with your API key`}
123+
accent
124+
onClick={() => onPick("cloud")}
125+
/>
126+
127+
<ConnectionTypeButton
128+
icon={Server}
129+
title="Self-Hosted"
130+
description="Connect to your own Honcho deployment"
131+
onClick={() => onPick("self-hosted")}
132+
/>
133+
134+
{onCancel && (
135+
<div className="pt-1">
136+
<Button
137+
type="button"
138+
variant="ghost"
139+
onClick={onCancel}
140+
className="w-full py-2 px-4 rounded-xl"
141+
>
142+
Cancel
143+
</Button>
144+
</div>
145+
)}
146+
</div>
147+
);
148+
}
149+
150+
interface ConnectionTypeButtonProps {
151+
icon: typeof Cloud;
152+
title: string;
153+
description: string;
154+
accent?: boolean;
155+
onClick: () => void;
156+
}
157+
158+
function ConnectionTypeButton({
159+
icon: Icon,
160+
title,
161+
description,
162+
accent,
163+
onClick,
164+
}: ConnectionTypeButtonProps) {
165+
return (
166+
<button
167+
type="button"
168+
onClick={onClick}
169+
className="w-full rounded-xl p-4 flex items-center gap-3 text-left transition-colors"
170+
style={{
171+
background: "var(--surface)",
172+
border: `1px solid ${accent ? "var(--accent-border)" : "var(--border)"}`,
173+
}}
174+
>
175+
<div
176+
className="w-10 h-10 rounded-lg flex items-center justify-center shrink-0"
177+
style={{
178+
background: accent ? "var(--accent)" : "var(--bg-2)",
179+
color: accent ? "white" : "var(--text-2)",
180+
}}
181+
>
182+
<Icon className="w-5 h-5" strokeWidth={1.5} />
183+
</div>
184+
<div className="min-w-0 flex-1">
185+
<p className="text-sm font-medium" style={{ color: "var(--text-1)" }}>
186+
{title}
187+
</p>
188+
<Muted className="text-xs mt-0.5">{description}</Muted>
189+
</div>
190+
<ChevronRight
191+
className="w-4 h-4 shrink-0"
192+
style={{ color: "var(--text-3)" }}
193+
strokeWidth={1.5}
194+
/>
195+
</button>
196+
);
197+
}
198+
89199
interface InstanceRowProps {
90200
instance: Instance;
91201
active: boolean;
@@ -96,6 +206,7 @@ interface InstanceRowProps {
96206

97207
function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: InstanceRowProps) {
98208
const [confirmingDelete, setConfirmingDelete] = useState(false);
209+
const cloud = isCloudInstance(instance);
99210

100211
return (
101212
<motion.div
@@ -122,6 +233,8 @@ function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: Instanc
122233
>
123234
{active ? (
124235
<Check className="w-4 h-4" strokeWidth={2} />
236+
) : cloud ? (
237+
<Cloud className="w-4 h-4" strokeWidth={1.5} />
125238
) : (
126239
<Server className="w-4 h-4" strokeWidth={1.5} />
127240
)}
@@ -134,7 +247,7 @@ function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: Instanc
134247
{instance.name}
135248
</p>
136249
<Muted className="text-xs font-mono truncate">
137-
{instance.baseUrl.replace(/^https?:\/\//, "")}
250+
{cloud ? "Honcho Cloud" : instance.baseUrl.replace(/^https?:\/\//, "")}
138251
</Muted>
139252
</div>
140253
</button>

0 commit comments

Comments
 (0)