Skip to content

Commit 403ee48

Browse files
Merge pull request #11 from offendingcommit/claude/add-honcho-cloud-option-vk93o
2 parents 4fc54a3 + 38e76d3 commit 403ee48

10 files changed

Lines changed: 482 additions & 53 deletions

File tree

packages/web/src/components/layout/Sidebar.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import {
1313
Sun,
1414
} from "lucide-react";
1515
import { useEffect, useRef, useState } from "react";
16+
import { HealthDot } from "@/components/shared/HealthDot";
1617
import { useDemo } from "@/hooks/useDemo";
18+
import { useHealthStatus } from "@/hooks/useHealthStatus";
1719
import { useInstances } from "@/hooks/useInstances";
1820
import { useTheme } from "@/hooks/useTheme";
1921
import { COLOR } from "@/lib/constants";
@@ -29,6 +31,7 @@ export function Sidebar() {
2931
const { instances, active, activate } = useInstances();
3032
const { theme, toggle } = useTheme();
3133
const { demo, toggle: toggleDemo, mask } = useDemo();
34+
const { data: health } = useHealthStatus();
3235
const [switcherOpen, setSwitcherOpen] = useState(false);
3336
const switcherRef = useRef<HTMLDivElement | null>(null);
3437

@@ -87,8 +90,12 @@ export function Sidebar() {
8790
title={mask(active.baseUrl)}
8891
>
8992
<div className="min-w-0 flex-1">
90-
<p className="text-xs font-medium truncate" style={{ color: "var(--text-2)" }}>
91-
{active.name}
93+
<p
94+
className="text-xs font-medium truncate flex items-center gap-1.5"
95+
style={{ color: "var(--text-2)" }}
96+
>
97+
<HealthDot status={health?.status} message={health?.message} />
98+
<span className="truncate">{active.name}</span>
9299
</p>
93100
<p className="text-xs font-mono truncate" style={{ color: "var(--text-4)" }}>
94101
{mask(active.baseUrl.replace(/^https?:\/\//, ""))}

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

Lines changed: 181 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,53 @@
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";
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 { checkConnection, 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+
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

1319
interface InstancesManagerProps {
1420
onActivated?: () => void;
1521
}
1622

1723
export 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(/^https?:\/\//, "")}
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(/^https?:\/\//, "")} — 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+
89245
interface InstanceRowProps {
90246
instance: Instance;
91247
active: boolean;
@@ -96,6 +252,7 @@ interface InstanceRowProps {
96252

97253
function 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(/^https?:\/\//, "")}
296+
{cloud ? "Honcho Cloud" : instance.baseUrl.replace(/^https?:\/\//, "")}
138297
</Muted>
139298
</div>
140299
</button>

0 commit comments

Comments
 (0)