"use client"; import { Radio as RadioPrimitive } from "@base-ui/react/radio";import { CheckIcon, CopyIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react";import { useCallback, useEffect, useMemo, useRef, useState } from "react";import { useAtomValue } from "@effect/atom-react";import { type HermesGatewayConnectionState, type HermesGatewayEnrollmentResult, ProviderInstanceId, ProviderDriverKind, type ProviderInstanceConfig,} from "@t3tools/contracts";import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings";import { cn, randomUUID } from "../../lib/utils";import { normalizeProviderAccentColor } from "../../providerInstances";import { Button } from "../ui/button";import { ACPRegistryIcon, Gemini, GithubCopilotIcon, PiAgentIcon, type Icon } from "../Icons";import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPopup, DialogTitle,} from "../ui/dialog";import { Badge } from "../ui/badge";import { Input } from "../ui/input";import { RadioGroup } from "../ui/radio-group";import { toastManager } from "../ui/toast";import { DRIVER_OPTION_BY_VALUE, DRIVER_OPTIONS } from "./providerDriverMeta";import { ProviderSettingsForm, deriveProviderSettingsFields } from "./ProviderSettingsForm";import { AnimatedHeight } from "../AnimatedHeight";import { ADD_PROVIDER_WIZARD_STEPS, createHermesProviderInstanceId, HERMES_GATEWAY_RESTART_COMMAND, HERMES_PLUGIN_INSTALL_COMMAND, isHermesInstanceRemovedError, isOwnedHermesEnrollmentRetry, resolveHermesWaitingState, resolveWizardNavigation, rollbackProviderInstances, type WizardNavigation, validateProviderInstanceIdForWizard,} from "./AddProviderInstanceDialog.logic";import { AddProviderInstanceWizardSteps } from "./AddProviderInstanceWizardSteps";import { usePrimaryEnvironment } from "../../state/environments";import { primaryServerProvidersAtom, serverEnvironment } from "../../state/server";import { useAtomCommand } from "../../state/use-atom-command";import { defaultHermesConnectorUrl, formatHermesLastConnected, hermesGatewayProviderSignal, hermesGatewayStatusGuidance, hermesGatewayStatusLabel, isHermesEnrollmentExpired, messageFromUnknownError,} from "./HermesGatewayInstanceSection.logic"; const PROVIDER_ACCENT_SWATCHES = [ "#2563eb", "#16a34a", "#ea580c", "#dc2626", "#7c3aed", "#0891b2",] as const; /** * Normalize a user-provided label into a slug suffix for the instance id. * The full id is formed by prefixing the driver slug — e.g. label "Work" on * driver "codex" becomes `codex_work`. Hermes uses a random durable identity * instead because its historical thread bindings must never target a newly * created gateway with the same display name. */function slugifyLabel(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, "") .slice(0, 48);} function deriveInstanceId(driver: ProviderDriverKind, label: string): string { const slug = slugifyLabel(label); return slug ? `${driver}_${slug}` : "";} const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex");const DEFAULT_DRIVER_OPTION = DRIVER_OPTIONS[0]!;const EMPTY_CONFIG_DRAFT: Record<string, unknown> = {};interface ComingSoonDriverOption { readonly value: ProviderDriverKind; readonly label: string; readonly icon: Icon;} const COMING_SOON_DRIVER_OPTIONS: readonly ComingSoonDriverOption[] = [ { value: ProviderDriverKind.make("githubCopilot"), label: "Github Copilot", icon: GithubCopilotIcon, }, { value: ProviderDriverKind.make("gemini"), label: "Gemini", icon: Gemini, }, { value: ProviderDriverKind.make("acpRegistry"), label: "ACP Registry", icon: ACPRegistryIcon, }, { value: ProviderDriverKind.make("piAgent"), label: "Pi Agent", icon: PiAgentIcon, },]; const HERMES_BADGE_VARIANT_BY_STATUS: Record< HermesGatewayConnectionState, "success" | "warning" | "error" | "secondary"> = { connected: "success", connecting: "warning", offline: "secondary", "upgrade-required": "warning", revoked: "error",}; interface AddProviderInstanceDialogProps { open: boolean; onOpenChange: (open: boolean) => void;} export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const environmentId = usePrimaryEnvironment()?.environmentId ?? null; const updateServerSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false, }); const createHermesEnrollment = useAtomCommand(serverEnvironment.hermesGatewayCreateEnrollment, { reportFailure: false, }); const getHermesStatus = useAtomCommand(serverEnvironment.hermesGatewayGetInstanceStatus, { reportFailure: false, }); const serverProviders = useAtomValue(primaryServerProvidersAtom); const [wizardStep, setWizardStep] = useState(0); const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND); const [label, setLabel] = useState(""); const [accentColor, setAccentColor] = useState<string>(""); const [instanceIdOverride, setInstanceIdOverride] = useState<string | null>(null); const [hermesIdentityNonce, setHermesIdentityNonce] = useState(randomUUID); // Driver-specific config drafts keyed by driver so toggling between drivers // during the same dialog session does not lose in-progress input. const [configByDriver, setConfigByDriver] = useState<Record<string, Record<string, unknown>>>({}); // Errors are suppressed until the user has tried to submit once. After that // they update live so fixing the problem clears the message in place. const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false); const [hermesConnectorUrl, setHermesConnectorUrl] = useState(() => typeof window === "undefined" ? "http://localhost/api/hermes-gateway/ws" : defaultHermesConnectorUrl(window.location.origin), ); const [hermesEnrollment, setHermesEnrollment] = useState<HermesGatewayEnrollmentResult | null>( null, ); const [saveError, setSaveError] = useState<string | null>(null); const [isSaving, setIsSaving] = useState(false); const [createdHermesIdentity, setCreatedHermesIdentity] = useState<{ readonly instanceId: string; readonly nickname: string; /** * False once a failed enrollment rolled the settings write back. The id * stays claimed by this dialog session (so validation keeps exempting it * and no duplicate is ever created), but the next attempt has to write it * again before the server will accept an enrollment for it. */ readonly persisted: boolean; } | null>(null); const [hermesConnectionState, setHermesConnectionState] = useState<HermesGatewayConnectionState | null>(null); const [nowMillis, setNowMillis] = useState(() => Date.now()); const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "Hermes enrollment command", onCopy: () => toastManager.add({ type: "success", title: "Hermes command copied", description: "Run it in the terminal where Hermes is installed.", }), }); const { copyToClipboard: copyInstallCommand, isCopied: isInstallCommandCopied } = useCopyToClipboard({ target: "Hermes plugin install command", onCopy: () => toastManager.add({ type: "success", title: "Install command copied", description: "Run it from the T3 Code checkout on the Hermes host.", }), }); const { copyToClipboard: copyRestartCommand, isCopied: isRestartCommandCopied } = useCopyToClipboard({ target: "Hermes gateway restart command", onCopy: () => toastManager.add({ type: "success", title: "Restart command copied", description: "Run it on the Hermes host to activate the pairing.", }), }); // Rollback of a failed Hermes enrollment must survive the dialog closing // mid-failure, so the pending undo lives in a ref that an unmount-safe // effect drains rather than in the `handleSave` closure alone. const pendingRollbackRef = useRef<{ readonly instanceId: string; readonly written: ProviderInstanceConfig; } | null>(null); const latestProviderInstancesRef = useRef(settings.providerInstances); latestProviderInstancesRef.current = settings.providerInstances; const environmentIdRef = useRef(environmentId); environmentIdRef.current = environmentId; const updateServerSettingsRef = useRef(updateServerSettings); updateServerSettingsRef.current = updateServerSettings; const rollbackPendingHermesInstance = useCallback(async () => { const pending = pendingRollbackRef.current; pendingRollbackRef.current = null; const rollbackEnvironmentId = environmentIdRef.current; if (pending === null || rollbackEnvironmentId === null) return; const providerInstances = rollbackProviderInstances({ latest: latestProviderInstancesRef.current ?? {}, instanceId: pending.instanceId, written: pending.written, }); if (providerInstances === null) return; await updateServerSettingsRef.current({ environmentId: rollbackEnvironmentId, input: { patch: { providerInstances } }, }); }, []); // Fires both when the dialog is closed and when it is unmounted outright // (its parent renders it conditionally), so an orphan is never left behind. useEffect(() => { if (open) return; void rollbackPendingHermesInstance(); setWizardStep(0); setDriver(DEFAULT_DRIVER_KIND); setLabel(""); setAccentColor(""); setInstanceIdOverride(null); setHermesIdentityNonce(randomUUID()); setConfigByDriver({}); setHasAttemptedSubmit(false); setHermesConnectorUrl( typeof window === "undefined" ? "http://localhost/api/hermes-gateway/ws" : defaultHermesConnectorUrl(window.location.origin), ); setHermesEnrollment(null); setSaveError(null); setIsSaving(false); setCreatedHermesIdentity(null); setHermesConnectionState(null); }, [open, rollbackPendingHermesInstance]); useEffect( () => () => { void rollbackPendingHermesInstance(); }, [rollbackPendingHermesInstance], ); const enrolledInstanceId = hermesEnrollment?.instanceId ?? null; // The server pushes a fresh `ServerProvider` snapshot over the already-open // `subscribeServerConfig` stream every time the broker publishes a gateway // status change, so this flips the moment Hermes dials in — no polling. const hermesProviderSignal = useMemo( () => enrolledInstanceId === null ? null : hermesGatewayProviderSignal(serverProviders, enrolledInstanceId), [serverProviders, enrolledInstanceId], ); const hermesProviderSignalKey = hermesProviderSignal ? `${hermesProviderSignal.present}:${hermesProviderSignal.connected}:${hermesProviderSignal.revision ?? ""}` : null; // The pushed snapshot only tells us connected/not. Resolve the authoritative // `HermesGatewayConnectionState` once per change so `upgrade-required` and // `revoked` are distinguishable from a plain wait. useEffect(() => { if (enrolledInstanceId === null || environmentId === null) return; if (hermesProviderSignalKey === null) return; let cancelled = false; void (async () => { const result = await getHermesStatus({ environmentId, input: { instanceId: ProviderInstanceId.make(enrolledInstanceId) }, }); if (cancelled) return; if (result._tag === "Success") setHermesConnectionState(result.value.status); })(); return () => { cancelled = true; }; }, [enrolledInstanceId, environmentId, getHermesStatus, hermesProviderSignalKey]); // Re-evaluate the enrollment expiry on a coarse tick so the panel switches // itself to the "mint a new command" affordance without a manual refresh. useEffect(() => { if (hermesEnrollment === null) return; setNowMillis(Date.now()); const interval = window.setInterval(() => setNowMillis(Date.now()), 15_000); return () => window.clearInterval(interval); }, [hermesEnrollment]); const hermesWaitingState = useMemo( () => hermesEnrollment === null ? null : resolveHermesWaitingState({ connectionState: hermesConnectionState, isExpired: isHermesEnrollmentExpired(hermesEnrollment.expiresAt, nowMillis), }), [hermesEnrollment, hermesConnectionState, nowMillis], ); const existingIds = useMemo( () => new Set(Object.keys(settings.providerInstances ?? {})), [settings.providerInstances], ); const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION; const instanceId = driver === "hermes" ? createHermesProviderInstanceId(label, () => hermesIdentityNonce) : (instanceIdOverride ?? deriveInstanceId(driver, label)); const driverSettingsFields = useMemo( () => deriveProviderSettingsFields(driverOption), [driverOption], ); const instanceIdError = driver === "hermes" && label.trim().length === 0 ? "Hermes nickname is required." : validateProviderInstanceIdForWizard({ driver, instanceId, existingIds, createdHermesInstanceId: createdHermesIdentity?.instanceId ?? null, }); const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null; const previewLabel = label.trim() || `${driverOption.label} Workspace`; const wizardStepSummaries = [driverOption.label, previewLabel, null] as const; const configDraft = configByDriver[driver] ?? EMPTY_CONFIG_DRAFT; const setConfigDraft = (config: Record<string, unknown> | undefined) => { setConfigByDriver((existing) => { const next = { ...existing }; if (config === undefined || Object.keys(config).length === 0) { delete next[driver]; } else { next[driver] = config; } return next; }); }; const applyWizardNavigation = (navigation: WizardNavigation) => { if (navigation.kind === "blocked") { setHasAttemptedSubmit(true); } setWizardStep(navigation.step); }; const navigateToStep = (requestedStep: number) => { applyWizardNavigation( resolveWizardNavigation(wizardStep, requestedStep, ADD_PROVIDER_WIZARD_STEPS.length, { instanceIdError, }), ); }; const handleSave = async () => { setHasAttemptedSubmit(true); if (instanceIdError !== null) return; const config = configByDriver[driver] ?? {}; const hasConfig = Object.keys(config).length > 0; const normalizedAccentColor = normalizeProviderAccentColor(accentColor); const nextInstance: ProviderInstanceConfig = { driver, enabled: true, ...(label.trim().length > 0 ? { displayName: label.trim() } : {}), ...(normalizedAccentColor ? { accentColor: normalizedAccentColor } : {}), ...(hasConfig ? { config } : {}), }; // `ProviderInstanceId.make` revalidates the slug; we've already checked // it via `validateProviderInstanceIdForWizard`, but going through the brand constructor // keeps the type boundary honest and guards against any future drift in // the slug rules. const brandedId = ProviderInstanceId.make(instanceId); // Only skip the settings write when the instance this dialog created is // still persisted. After a rolled-back failure the id is still ours, but // it has to be written again before the server will enroll it. const isOwnedHermesRetry = isOwnedHermesEnrollmentRetry({ driver, instanceId, createdHermesInstanceId: createdHermesIdentity?.instanceId ?? null, }) && createdHermesIdentity?.persisted === true; const nextMap = { ...settings.providerInstances, [brandedId]: nextInstance, }; try { setIsSaving(true); setSaveError(null); if (driver === "hermes") { if (environmentId === null) { throw new Error("Connect this browser to a T3 server before pairing Hermes."); } // The server rejects `create-enrollment` unless the instance already // exists in `providerInstances` with the Hermes driver, so the write // genuinely has to come first. Instead of enroll-first, the write is // registered as a pending rollback that the failure path — and the // close/unmount effects — undo, so a failed enrollment never leaves a // visible `instance-not-found` orphan in the model picker. if (!isOwnedHermesRetry) { const settingsResult = await updateServerSettings({ environmentId, input: { patch: { providerInstances: nextMap } }, }); if (settingsResult._tag === "Failure") { throw squashAtomCommandFailure(settingsResult); } pendingRollbackRef.current = { instanceId, written: nextInstance }; setCreatedHermesIdentity({ instanceId, nickname: label.trim(), persisted: true, }); } const enrollmentResult = await createHermesEnrollment({ environmentId, input: { instanceId: brandedId, nickname: createdHermesIdentity?.nickname ?? label.trim(), connectorUrl: hermesConnectorUrl, }, }); if (enrollmentResult._tag === "Failure") { const enrollmentError = squashAtomCommandFailure(enrollmentResult); // The server already deleted the tombstoned instance itself, so // there is nothing left for us to roll back — just rotate the nonce // so the next attempt uses a fresh, untainted id. if (isHermesInstanceRemovedError(enrollmentError)) { pendingRollbackRef.current = null; setCreatedHermesIdentity(null); setHermesIdentityNonce(randomUUID()); } else { await rollbackPendingHermesInstance(); setCreatedHermesIdentity((current) => current ? { ...current, persisted: false } : current, ); } throw enrollmentError; } // Enrollment succeeded: the instance is legitimately persisted now. pendingRollbackRef.current = null; setHermesConnectionState(null); setHermesEnrollment(enrollmentResult.value); toastManager.add({ type: "success", title: "Hermes instance added", description: "Run the one-time command to connect it.", }); setIsSaving(false); return; } updateSettings({ providerInstances: nextMap }); toastManager.add({ type: "success", title: "Provider instance added", description: `${driverOption.label} instance '${instanceId}' was added.`, });Lines 1–500 of 988. Load more
"use client"; import { Radio as RadioPrimitive } from "@base-ui/react/radio"; import { CheckIcon, CopyIcon, LoaderIcon, TriangleAlertIcon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useAtomValue } from "@effect/atom-react"; import { type HermesGatewayConnectionState, type HermesGatewayEnrollmentResult, ProviderInstanceId, ProviderDriverKind, type ProviderInstanceConfig, } from "@t3tools/contracts"; import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { useCopyToClipboard } from "../../hooks/useCopyToClipboard"; import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; import { cn, randomUUID } from "../../lib/utils"; import { normalizeProviderAccentColor } from "../../providerInstances"; import { Button } from "../ui/button"; import { ACPRegistryIcon, Gemini, GithubCopilotIcon, PiAgentIcon, type Icon } from "../Icons"; import { Dialog, DialogDescription, DialogFooter, DialogHeader, DialogPopup, DialogTitle, } from "../ui/dialog"; import { Badge } from "../ui/badge"; import { Input } from "../ui/input"; import { RadioGroup } from "../ui/radio-group"; import { toastManager } from "../ui/toast"; import { DRIVER_OPTION_BY_VALUE, DRIVER_OPTIONS } from "./providerDriverMeta"; import { ProviderSettingsForm, deriveProviderSettingsFields } from "./ProviderSettingsForm"; import { AnimatedHeight } from "../AnimatedHeight"; import { ADD_PROVIDER_WIZARD_STEPS, createHermesProviderInstanceId, HERMES_GATEWAY_RESTART_COMMAND, HERMES_PLUGIN_INSTALL_COMMAND, isHermesInstanceRemovedError, isOwnedHermesEnrollmentRetry, resolveHermesWaitingState, resolveWizardNavigation, rollbackProviderInstances, type WizardNavigation, validateProviderInstanceIdForWizard, } from "./AddProviderInstanceDialog.logic"; import { AddProviderInstanceWizardSteps } from "./AddProviderInstanceWizardSteps"; import { usePrimaryEnvironment } from "../../state/environments"; import { primaryServerProvidersAtom, serverEnvironment } from "../../state/server"; import { useAtomCommand } from "../../state/use-atom-command"; import { defaultHermesConnectorUrl, formatHermesLastConnected, hermesGatewayProviderSignal, hermesGatewayStatusGuidance, hermesGatewayStatusLabel, isHermesEnrollmentExpired, messageFromUnknownError, } from "./HermesGatewayInstanceSection.logic"; const PROVIDER_ACCENT_SWATCHES = [ "#2563eb", "#16a34a", "#ea580c", "#dc2626", "#7c3aed", "#0891b2", ] as const; /** * Normalize a user-provided label into a slug suffix for the instance id. * The full id is formed by prefixing the driver slug — e.g. label "Work" on * driver "codex" becomes `codex_work`. Hermes uses a random durable identity * instead because its historical thread bindings must never target a newly * created gateway with the same display name. */ function slugifyLabel(value: string): string { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, "") .slice(0, 48); } function deriveInstanceId(driver: ProviderDriverKind, label: string): string { const slug = slugifyLabel(label); return slug ? `${driver}_${slug}` : ""; } const DEFAULT_DRIVER_KIND = ProviderDriverKind.make("codex"); const DEFAULT_DRIVER_OPTION = DRIVER_OPTIONS[0]!; const EMPTY_CONFIG_DRAFT: Record<string, unknown> = {}; interface ComingSoonDriverOption { readonly value: ProviderDriverKind; readonly label: string; readonly icon: Icon; } const COMING_SOON_DRIVER_OPTIONS: readonly ComingSoonDriverOption[] = [ { value: ProviderDriverKind.make("githubCopilot"), label: "Github Copilot", icon: GithubCopilotIcon, }, { value: ProviderDriverKind.make("gemini"), label: "Gemini", icon: Gemini, }, { value: ProviderDriverKind.make("acpRegistry"), label: "ACP Registry", icon: ACPRegistryIcon, }, { value: ProviderDriverKind.make("piAgent"), label: "Pi Agent", icon: PiAgentIcon, }, ]; const HERMES_BADGE_VARIANT_BY_STATUS: Record< HermesGatewayConnectionState, "success" | "warning" | "error" | "secondary" > = { connected: "success", connecting: "warning", offline: "secondary", "upgrade-required": "warning", revoked: "error", }; interface AddProviderInstanceDialogProps { open: boolean; onOpenChange: (open: boolean) => void; } export function AddProviderInstanceDialog({ open, onOpenChange }: AddProviderInstanceDialogProps) { const settings = usePrimarySettings(); const updateSettings = useUpdatePrimarySettings(); const environmentId = usePrimaryEnvironment()?.environmentId ?? null; const updateServerSettings = useAtomCommand(serverEnvironment.updateSettings, { reportFailure: false, }); const createHermesEnrollment = useAtomCommand(serverEnvironment.hermesGatewayCreateEnrollment, { reportFailure: false, }); const getHermesStatus = useAtomCommand(serverEnvironment.hermesGatewayGetInstanceStatus, { reportFailure: false, }); const serverProviders = useAtomValue(primaryServerProvidersAtom); const [wizardStep, setWizardStep] = useState(0); const [driver, setDriver] = useState<ProviderDriverKind>(DEFAULT_DRIVER_KIND); const [label, setLabel] = useState(""); const [accentColor, setAccentColor] = useState<string>(""); const [instanceIdOverride, setInstanceIdOverride] = useState<string | null>(null); const [hermesIdentityNonce, setHermesIdentityNonce] = useState(randomUUID); // Driver-specific config drafts keyed by driver so toggling between drivers // during the same dialog session does not lose in-progress input. const [configByDriver, setConfigByDriver] = useState<Record<string, Record<string, unknown>>>({}); // Errors are suppressed until the user has tried to submit once. After that // they update live so fixing the problem clears the message in place. const [hasAttemptedSubmit, setHasAttemptedSubmit] = useState(false); const [hermesConnectorUrl, setHermesConnectorUrl] = useState(() => typeof window === "undefined" ? "http://localhost/api/hermes-gateway/ws" : defaultHermesConnectorUrl(window.location.origin), ); const [hermesEnrollment, setHermesEnrollment] = useState<HermesGatewayEnrollmentResult | null>( null, ); const [saveError, setSaveError] = useState<string | null>(null); const [isSaving, setIsSaving] = useState(false); const [createdHermesIdentity, setCreatedHermesIdentity] = useState<{ readonly instanceId: string; readonly nickname: string; /** * False once a failed enrollment rolled the settings write back. The id * stays claimed by this dialog session (so validation keeps exempting it * and no duplicate is ever created), but the next attempt has to write it * again before the server will accept an enrollment for it. */ readonly persisted: boolean; } | null>(null); const [hermesConnectionState, setHermesConnectionState] = useState<HermesGatewayConnectionState | null>(null); const [nowMillis, setNowMillis] = useState(() => Date.now()); const { copyToClipboard, isCopied } = useCopyToClipboard({ target: "Hermes enrollment command", onCopy: () => toastManager.add({ type: "success", title: "Hermes command copied", description: "Run it in the terminal where Hermes is installed.", }), }); const { copyToClipboard: copyInstallCommand, isCopied: isInstallCommandCopied } = useCopyToClipboard({ target: "Hermes plugin install command", onCopy: () => toastManager.add({ type: "success", title: "Install command copied", description: "Run it from the T3 Code checkout on the Hermes host.", }), }); const { copyToClipboard: copyRestartCommand, isCopied: isRestartCommandCopied } = useCopyToClipboard({ target: "Hermes gateway restart command", onCopy: () => toastManager.add({ type: "success", title: "Restart command copied", description: "Run it on the Hermes host to activate the pairing.", }), }); // Rollback of a failed Hermes enrollment must survive the dialog closing // mid-failure, so the pending undo lives in a ref that an unmount-safe // effect drains rather than in the `handleSave` closure alone. const pendingRollbackRef = useRef<{ readonly instanceId: string; readonly written: ProviderInstanceConfig; } | null>(null); const latestProviderInstancesRef = useRef(settings.providerInstances); latestProviderInstancesRef.current = settings.providerInstances; const environmentIdRef = useRef(environmentId); environmentIdRef.current = environmentId; const updateServerSettingsRef = useRef(updateServerSettings); updateServerSettingsRef.current = updateServerSettings; const rollbackPendingHermesInstance = useCallback(async () => { const pending = pendingRollbackRef.current; pendingRollbackRef.current = null; const rollbackEnvironmentId = environmentIdRef.current; if (pending === null || rollbackEnvironmentId === null) return; const providerInstances = rollbackProviderInstances({ latest: latestProviderInstancesRef.current ?? {}, instanceId: pending.instanceId, written: pending.written, }); if (providerInstances === null) return; await updateServerSettingsRef.current({ environmentId: rollbackEnvironmentId, input: { patch: { providerInstances } }, }); }, []); // Fires both when the dialog is closed and when it is unmounted outright // (its parent renders it conditionally), so an orphan is never left behind. useEffect(() => { if (open) return; void rollbackPendingHermesInstance(); setWizardStep(0); setDriver(DEFAULT_DRIVER_KIND); setLabel(""); setAccentColor(""); setInstanceIdOverride(null); setHermesIdentityNonce(randomUUID()); setConfigByDriver({}); setHasAttemptedSubmit(false); setHermesConnectorUrl( typeof window === "undefined" ? "http://localhost/api/hermes-gateway/ws" : defaultHermesConnectorUrl(window.location.origin), ); setHermesEnrollment(null); setSaveError(null); setIsSaving(false); setCreatedHermesIdentity(null); setHermesConnectionState(null); }, [open, rollbackPendingHermesInstance]); useEffect( () => () => { void rollbackPendingHermesInstance(); }, [rollbackPendingHermesInstance], ); const enrolledInstanceId = hermesEnrollment?.instanceId ?? null; // The server pushes a fresh `ServerProvider` snapshot over the already-open // `subscribeServerConfig` stream every time the broker publishes a gateway // status change, so this flips the moment Hermes dials in — no polling. const hermesProviderSignal = useMemo( () => enrolledInstanceId === null ? null : hermesGatewayProviderSignal(serverProviders, enrolledInstanceId), [serverProviders, enrolledInstanceId], ); const hermesProviderSignalKey = hermesProviderSignal ? `${hermesProviderSignal.present}:${hermesProviderSignal.connected}:${hermesProviderSignal.revision ?? ""}` : null; // The pushed snapshot only tells us connected/not. Resolve the authoritative // `HermesGatewayConnectionState` once per change so `upgrade-required` and // `revoked` are distinguishable from a plain wait. useEffect(() => { if (enrolledInstanceId === null || environmentId === null) return; if (hermesProviderSignalKey === null) return; let cancelled = false; void (async () => { const result = await getHermesStatus({ environmentId, input: { instanceId: ProviderInstanceId.make(enrolledInstanceId) }, }); if (cancelled) return; if (result._tag === "Success") setHermesConnectionState(result.value.status); })(); return () => { cancelled = true; }; }, [enrolledInstanceId, environmentId, getHermesStatus, hermesProviderSignalKey]); // Re-evaluate the enrollment expiry on a coarse tick so the panel switches // itself to the "mint a new command" affordance without a manual refresh. useEffect(() => { if (hermesEnrollment === null) return; setNowMillis(Date.now()); const interval = window.setInterval(() => setNowMillis(Date.now()), 15_000); return () => window.clearInterval(interval); }, [hermesEnrollment]); const hermesWaitingState = useMemo( () => hermesEnrollment === null ? null : resolveHermesWaitingState({ connectionState: hermesConnectionState, isExpired: isHermesEnrollmentExpired(hermesEnrollment.expiresAt, nowMillis), }), [hermesEnrollment, hermesConnectionState, nowMillis], ); const existingIds = useMemo( () => new Set(Object.keys(settings.providerInstances ?? {})), [settings.providerInstances], ); const driverOption = DRIVER_OPTION_BY_VALUE[driver] ?? DEFAULT_DRIVER_OPTION; const instanceId = driver === "hermes" ? createHermesProviderInstanceId(label, () => hermesIdentityNonce) : (instanceIdOverride ?? deriveInstanceId(driver, label)); const driverSettingsFields = useMemo( () => deriveProviderSettingsFields(driverOption), [driverOption], ); const instanceIdError = driver === "hermes" && label.trim().length === 0 ? "Hermes nickname is required." : validateProviderInstanceIdForWizard({ driver, instanceId, existingIds, createdHermesInstanceId: createdHermesIdentity?.instanceId ?? null, }); const showInstanceIdError = hasAttemptedSubmit && instanceIdError !== null; const previewLabel = label.trim() || `${driverOption.label} Workspace`; const wizardStepSummaries = [driverOption.label, previewLabel, null] as const; const configDraft = configByDriver[driver] ?? EMPTY_CONFIG_DRAFT; const setConfigDraft = (config: Record<string, unknown> | undefined) => { setConfigByDriver((existing) => { const next = { ...existing }; if (config === undefined || Object.keys(config).length === 0) { delete next[driver]; } else { next[driver] = config; } return next; }); }; const applyWizardNavigation = (navigation: WizardNavigation) => { if (navigation.kind === "blocked") { setHasAttemptedSubmit(true); } setWizardStep(navigation.step); }; const navigateToStep = (requestedStep: number) => { applyWizardNavigation( resolveWizardNavigation(wizardStep, requestedStep, ADD_PROVIDER_WIZARD_STEPS.length, { instanceIdError, }), ); }; const handleSave = async () => { setHasAttemptedSubmit(true); if (instanceIdError !== null) return; const config = configByDriver[driver] ?? {}; const hasConfig = Object.keys(config).length > 0; const normalizedAccentColor = normalizeProviderAccentColor(accentColor); const nextInstance: ProviderInstanceConfig = { driver, enabled: true, ...(label.trim().length > 0 ? { displayName: label.trim() } : {}), ...(normalizedAccentColor ? { accentColor: normalizedAccentColor } : {}), ...(hasConfig ? { config } : {}), }; // `ProviderInstanceId.make` revalidates the slug; we've already checked // it via `validateProviderInstanceIdForWizard`, but going through the brand constructor // keeps the type boundary honest and guards against any future drift in // the slug rules. const brandedId = ProviderInstanceId.make(instanceId); // Only skip the settings write when the instance this dialog created is // still persisted. After a rolled-back failure the id is still ours, but // it has to be written again before the server will enroll it. const isOwnedHermesRetry = isOwnedHermesEnrollmentRetry({ driver, instanceId, createdHermesInstanceId: createdHermesIdentity?.instanceId ?? null, }) && createdHermesIdentity?.persisted === true; const nextMap = { ...settings.providerInstances, [brandedId]: nextInstance, }; try { setIsSaving(true); setSaveError(null); if (driver === "hermes") { if (environmentId === null) { throw new Error("Connect this browser to a T3 server before pairing Hermes."); } // The server rejects `create-enrollment` unless the instance already // exists in `providerInstances` with the Hermes driver, so the write // genuinely has to come first. Instead of enroll-first, the write is // registered as a pending rollback that the failure path — and the // close/unmount effects — undo, so a failed enrollment never leaves a // visible `instance-not-found` orphan in the model picker. if (!isOwnedHermesRetry) { const settingsResult = await updateServerSettings({ environmentId, input: { patch: { providerInstances: nextMap } }, }); if (settingsResult._tag === "Failure") { throw squashAtomCommandFailure(settingsResult); } pendingRollbackRef.current = { instanceId, written: nextInstance }; setCreatedHermesIdentity({ instanceId, nickname: label.trim(), persisted: true, }); } const enrollmentResult = await createHermesEnrollment({ environmentId, input: { instanceId: brandedId, nickname: createdHermesIdentity?.nickname ?? label.trim(), connectorUrl: hermesConnectorUrl, }, }); if (enrollmentResult._tag === "Failure") { const enrollmentError = squashAtomCommandFailure(enrollmentResult); // The server already deleted the tombstoned instance itself, so // there is nothing left for us to roll back — just rotate the nonce // so the next attempt uses a fresh, untainted id. if (isHermesInstanceRemovedError(enrollmentError)) { pendingRollbackRef.current = null; setCreatedHermesIdentity(null); setHermesIdentityNonce(randomUUID()); } else { await rollbackPendingHermesInstance(); setCreatedHermesIdentity((current) => current ? { ...current, persisted: false } : current, ); } throw enrollmentError; } // Enrollment succeeded: the instance is legitimately persisted now. pendingRollbackRef.current = null; setHermesConnectionState(null); setHermesEnrollment(enrollmentResult.value); toastManager.add({ type: "success", title: "Hermes instance added", description: "Run the one-time command to connect it.", }); setIsSaving(false); return; } updateSettings({ providerInstances: nextMap }); toastManager.add({ type: "success", title: "Provider instance added", description: `${driverOption.label} instance '${instanceId}' was added.`, });