From f0717624eb129be82a24c686588085b4c2303bc0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 14 May 2026 23:21:28 +0000 Subject: [PATCH 1/2] 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 --- .../components/settings/InstancesManager.tsx | 153 +++++++++++++++--- .../src/components/settings/SettingsForm.tsx | 125 ++++++++++---- packages/web/src/lib/config.ts | 10 ++ packages/web/src/routes/settings.tsx | 2 +- packages/web/src/test/app.test.tsx | 2 +- packages/web/src/test/settings-form.test.tsx | 72 +++++++++ 6 files changed, 315 insertions(+), 49 deletions(-) create mode 100644 packages/web/src/test/settings-form.test.tsx diff --git a/packages/web/src/components/settings/InstancesManager.tsx b/packages/web/src/components/settings/InstancesManager.tsx index 4ff3af8..e85075c 100644 --- a/packages/web/src/components/settings/InstancesManager.tsx +++ b/packages/web/src/components/settings/InstancesManager.tsx @@ -1,14 +1,18 @@ import { motion } from "framer-motion"; -import { Check, Pencil, Plus, Server, Trash2 } from "lucide-react"; +import { Check, ChevronRight, Cloud, Pencil, Plus, Server, Trash2 } from "lucide-react"; import { useState } from "react"; -import { SettingsForm } from "@/components/settings/SettingsForm"; +import { type ConnectionPreset, SettingsForm } from "@/components/settings/SettingsForm"; import { Button } from "@/components/ui/button"; import { Muted } from "@/components/ui/typography"; import { useInstances } from "@/hooks/useInstances"; -import type { Instance } from "@/lib/config"; +import { HONCHO_CLOUD_URL, type Instance, isCloudInstance } from "@/lib/config"; import { COLOR } from "@/lib/constants"; -type Mode = { kind: "list" } | { kind: "create" } | { kind: "edit"; id: string }; +type Mode = + | { kind: "list" } + | { kind: "choose-type" } + | { kind: "create"; preset: ConnectionPreset } + | { kind: "edit"; id: string }; interface InstancesManagerProps { onActivated?: () => void; @@ -16,18 +20,32 @@ interface InstancesManagerProps { export function InstancesManager({ onActivated }: InstancesManagerProps) { const { instances, activeId, activate, remove } = useInstances(); - const [mode, setMode] = useState({ kind: "list" }); + const isFirstRun = instances.length === 0; + const [mode, setMode] = useState(isFirstRun ? { kind: "choose-type" } : { kind: "list" }); + + const backFromCreate = () => setMode(isFirstRun ? { kind: "choose-type" } : { kind: "list" }); + + if (mode.kind === "choose-type") { + return ( + setMode({ kind: "create", preset })} + onCancel={isFirstRun ? undefined : () => setMode({ kind: "list" })} + /> + ); + } if (mode.kind === "create") { return ( { setMode({ kind: "list" }); onActivated?.(); }} - onCancel={instances.length > 0 ? () => setMode({ kind: "list" }) : undefined} - hideCancel={instances.length === 0} + onCancel={backFromCreate} + hideCancel={false} + submitLabel={isFirstRun ? "Save Connection" : undefined} /> ); } @@ -44,17 +62,6 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) { ); } - if (instances.length === 0) { - return ( - onActivated?.()} - hideCancel - submitLabel="Save Connection" - /> - ); - } - return (
@@ -76,7 +83,7 @@ export function InstancesManager({ onActivated }: InstancesManagerProps) { +
+ )} +
+ ); +} + +interface ConnectionTypeButtonProps { + icon: typeof Cloud; + title: string; + description: string; + accent?: boolean; + onClick: () => void; +} + +function ConnectionTypeButton({ + icon: Icon, + title, + description, + accent, + onClick, +}: ConnectionTypeButtonProps) { + return ( + + ); +} + interface InstanceRowProps { instance: Instance; active: boolean; @@ -96,6 +206,7 @@ interface InstanceRowProps { function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: InstanceRowProps) { const [confirmingDelete, setConfirmingDelete] = useState(false); + const cloud = isCloudInstance(instance); return ( {active ? ( + ) : cloud ? ( + ) : ( )} @@ -134,7 +247,7 @@ function InstanceRow({ instance, active, onActivate, onEdit, onDelete }: Instanc {instance.name}

- {instance.baseUrl.replace(/^https?:\/\//, "")} + {cloud ? "Honcho Cloud" : instance.baseUrl.replace(/^https?:\/\//, "")} diff --git a/packages/web/src/components/settings/SettingsForm.tsx b/packages/web/src/components/settings/SettingsForm.tsx index 236bdf0..1a767a5 100644 --- a/packages/web/src/components/settings/SettingsForm.tsx +++ b/packages/web/src/components/settings/SettingsForm.tsx @@ -1,17 +1,37 @@ import { AnimatePresence, motion } from "framer-motion"; -import { AlertCircle, CheckCircle, Loader, Lock, LockOpen, Wifi, WifiOff } from "lucide-react"; +import { + AlertCircle, + CheckCircle, + Cloud, + Loader, + Lock, + LockOpen, + Wifi, + WifiOff, +} from "lucide-react"; import { useState } from "react"; import { Button } from "@/components/ui/button"; import { Input, Textarea } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Muted } from "@/components/ui/typography"; import { useInstances } from "@/hooks/useInstances"; -import { checkConnection, type HealthStatus, type Instance, instanceSchema } from "@/lib/config"; +import { + checkConnection, + type HealthStatus, + HONCHO_CLOUD_URL, + type Instance, + instanceSchema, + isCloudInstance, +} from "@/lib/config"; import { COLOR } from "@/lib/constants"; +export type ConnectionPreset = "cloud" | "self-hosted"; + interface SettingsFormProps { /** Instance to edit; pass `null` to create a new one. */ instance: Instance | null; + /** Whether this form is for a Cloud or Self-Hosted connection. Inferred from `instance` in edit mode. */ + preset?: ConnectionPreset; /** Called after a successful save. Receives the saved instance id. */ onSaved?: (id: string) => void; /** Called when the user cancels (only meaningful when there's something to cancel back to). */ @@ -31,6 +51,7 @@ const statusConfig = { export function SettingsForm({ instance, + preset, onSaved, onCancel, hideCancel, @@ -38,8 +59,17 @@ export function SettingsForm({ }: SettingsFormProps) { const { add, update, activate } = useInstances(); - const [name, setName] = useState(instance?.name ?? ""); - const [baseUrl, setBaseUrl] = useState(instance?.baseUrl ?? "http://localhost:8000"); + const resolvedPreset: ConnectionPreset = + preset ?? (instance && isCloudInstance(instance) ? "cloud" : "self-hosted"); + const isCloud = resolvedPreset === "cloud"; + + const initialName = instance?.name ?? (isCloud ? "Honcho Cloud" : ""); + const initialBaseUrl = isCloud + ? HONCHO_CLOUD_URL + : (instance?.baseUrl ?? "http://localhost:8000"); + + const [name, setName] = useState(initialName); + const [baseUrl, setBaseUrl] = useState(initialBaseUrl); const [token, setToken] = useState(instance?.token ?? ""); const [errors, setErrors] = useState>>({}); const [saved, setSaved] = useState(false); @@ -64,8 +94,8 @@ export function SettingsForm({ e.preventDefault(); const candidate = { id: instance?.id ?? "placeholder", - name: name.trim() || "Default", - baseUrl, + name: name.trim() || (isCloud ? "Honcho Cloud" : "Default"), + baseUrl: isCloud ? HONCHO_CLOUD_URL : baseUrl, token, }; const result = instanceSchema.safeParse(candidate); @@ -78,6 +108,10 @@ export function SettingsForm({ setErrors(fieldErrors); return; } + if (isCloud && !token.trim()) { + setErrors({ token: "API key is required for Honcho Cloud" }); + return; + } setErrors({}); let id: string; @@ -136,18 +170,36 @@ export function SettingsForm({ {/* Base URL */}
- +
- { - setBaseUrl(e.target.value); - setHealth(null); - }} - placeholder="http://localhost:8000" - className="flex-1 font-mono rounded-xl" - /> + {isCloud ? ( +
+ + {HONCHO_CLOUD_URL} +
+ ) : ( + { + setBaseUrl(e.target.value); + setHealth(null); + }} + placeholder="http://localhost:8000" + className="flex-1 font-mono rounded-xl" + /> + )}
- {errors.baseUrl && ( + {errors.baseUrl && !isCloud && (

{errors.baseUrl}

)} - URL of your self-hosted Honcho instance + + {isCloud + ? "Hosted Honcho service — endpoint is fixed" + : "URL of your self-hosted Honcho instance"} +
{/* Health status */} @@ -225,27 +281,39 @@ export function SettingsForm({ strokeWidth={1.5} /> )} - API Token + {isCloud ? "API key" : "API Token"} - optional + {isCloud ? "required" : "optional"}