@burdenoff/microfe-vibecontrols 2026.525.9 → 2026.526.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/ai/PluginHarnessPicker.js +64 -65
- package/dist/components/ai/PluginHarnessPicker.js.map +1 -1
- package/dist/components/shared/PluginGate.js +70 -71
- package/dist/components/shared/PluginGate.js.map +1 -1
- package/dist/components/tunnels/TunnelCard.js +45 -41
- package/dist/components/tunnels/TunnelCard.js.map +1 -1
- package/dist/components/tunnels/TunnelCreateWizard.js +9 -13
- package/dist/components/tunnels/TunnelCreateWizard.js.map +1 -1
- package/dist/generated/wspace-operations.js +309 -220
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/hooks/usePluginAvailability.js +59 -83
- package/dist/hooks/usePluginAvailability.js.map +1 -1
- package/dist/index.js +10 -10
- package/dist/pages/AgentDetailsPage.js +486 -508
- package/dist/pages/AgentDetailsPage.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js +457 -457
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/pages/ai/AIPage.js +10 -10
- package/dist/pages/ai/AISessionDetailPage.js +4 -4
- package/dist/pages/ai/AgentManagerPage.js +11 -11
- package/dist/pages/ai/AgentManagerPage.js.map +1 -1
- package/dist/pages/tunnels/AuditTab.js +127 -14
- package/dist/pages/tunnels/AuditTab.js.map +1 -1
- package/dist/pages/tunnels/DomainsTab.js +53 -47
- package/dist/pages/tunnels/DomainsTab.js.map +1 -1
- package/dist/pages/tunnels/SessionsTab.js +98 -38
- package/dist/pages/tunnels/SessionsTab.js.map +1 -1
- package/dist/pages/tunnels/TunnelDetailsPage.js +82 -67
- package/dist/pages/tunnels/TunnelDetailsPage.js.map +1 -1
- package/dist/pages/tunnels/TunnelsTab.js +106 -63
- package/dist/pages/tunnels/TunnelsTab.js.map +1 -1
- package/dist/pages/tunnels/UsageTab.js +68 -14
- package/dist/pages/tunnels/UsageTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelAuditTab.js +4 -4
- package/dist/pages/tunnels/tabs/TunnelAuditTab.js.map +1 -1
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js +107 -77
- package/dist/pages/tunnels/tabs/TunnelDomainsTab.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginGate.js","names":[],"sources":["../../../src/components/shared/PluginGate.tsx"],"sourcesContent":["/**\n * PluginGate — gates a UI surface on one or more vibecontrols-agent plugins\n * being installed. If missing, renders a card listing each required plugin\n * with an Install button. Once satisfied, renders children.\n *\n * Supports two modes:\n * - Single-group (backwards compat): `required` + `mode` + `title/description`.\n * - Multi-group: `requirements={[{packages, mode, ...}, ...]}` — one fetch\n * for all packages across all groups, unified loading state, stacked\n * install cards for any unsatisfied group. Prevents the \"flicker cascade\"\n * you get when nesting PluginGates (outer resolves, inner starts its own\n * initial-loading spinner, etc.).\n */\n\nimport { useCallback, useMemo, type ReactNode } from 'react';\nimport { Puzzle, Download, Loader2, AlertCircle, Server, RefreshCw, Clock } from 'lucide-react';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { QuotaExhaustedDialog } from './QuotaExhaustedDialog';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\n\nexport interface PluginRequirement {\n /** Package names that satisfy this requirement. */\n packages: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Optional per-group eyebrow, title, description (fall back to defaults). */\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nexport interface PluginGateProps {\n /** Active agent id. When null, gate shows an \"activate an agent\" prompt. */\n agentId: string | null;\n\n // ── Single-group API (backwards compat) ────────────────────────────────\n /** Package names required for the wrapped surface to render. */\n required?: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Card heading. */\n title?: string;\n /** Card subheading / instruction line. */\n description?: string;\n /** Optional eyebrow label above the title (e.g. \"Plugin required\"). */\n eyebrow?: string;\n\n // ── Multi-group API (preferred when gating multiple requirements) ──────\n /** Multiple requirement groups — fetched + evaluated in one pass. */\n requirements?: readonly PluginRequirement[];\n\n /** Pass to render a compact version (smaller vertical padding). */\n compact?: boolean;\n /** Optional callback fired after install succeeds and gate resolves. */\n onInstalled?: () => void;\n children: ReactNode;\n}\n\ninterface ResolvedRequirement {\n packages: readonly string[];\n mode: 'all' | 'any';\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nfunction resolveRequirements(props: PluginGateProps): ResolvedRequirement[] {\n if (props.requirements && props.requirements.length > 0) {\n return props.requirements.map((r) => ({\n packages: r.packages,\n mode: r.mode ?? 'all',\n eyebrow: r.eyebrow,\n title: r.title,\n description: r.description,\n }));\n }\n if (props.required && props.required.length > 0) {\n return [\n {\n packages: props.required,\n mode: props.mode ?? 'all',\n eyebrow: props.eyebrow,\n title: props.title,\n description: props.description,\n },\n ];\n }\n return [];\n}\n\nexport function PluginGate(props: PluginGateProps) {\n const { agentId, compact = false, onInstalled, children } = props;\n const { active: activeProfile } = useActiveAgentProfile(agentId);\n const resolved = useMemo(() => resolveRequirements(props), [props]);\n\n const allPackages = useMemo(() => {\n const set = new Set<string>();\n for (const req of resolved) {\n for (const pkg of req.packages) set.add(pkg);\n }\n return [...set];\n }, [resolved]);\n\n const {\n loading,\n initialLoading,\n installed,\n installing,\n install,\n error,\n errorIsAgentState,\n quotaExhausted,\n clearQuotaExhausted,\n refetch,\n } = usePluginAvailability(allPackages, agentId, activeProfile);\n\n const navigate = useVibeNavigate();\n\n // Per-requirement satisfaction.\n const requirementSatisfied = useMemo(\n () =>\n resolved.map((req) => {\n if (req.mode === 'any') return req.packages.some((p) => !!installed[p]);\n return req.packages.every((p) => !!installed[p]);\n }),\n [resolved, installed]\n );\n\n const allSatisfied = resolved.length > 0 && requirementSatisfied.every(Boolean);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n const ok = await install(pkg);\n if (ok) onInstalled?.();\n },\n [install, onInstalled]\n );\n\n // No agent available — universal precondition.\n if (!agentId) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Server className=\"h-5 w-5 text-text-secondary shrink-0 mt-0.5\" />\n <div className=\"flex-1 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">No active agent</p>\n <p className=\"text-sm text-text-secondary\">\n This surface needs an active vibecontrols agent with a tunnel URL and API key.\n </p>\n <button\n onClick={() => navigate('/agents')}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n Go to Agents\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (allSatisfied) {\n return <>{children}</>;\n }\n\n // First-fetch loading state — one spinner for ALL requirements. Prevents\n // the cascade you get with nested PluginGates (outer resolves -> inner\n // starts a fresh initialLoading cycle). After the first fetch settles,\n // subsequent refetches (focus, install, retry) fall through to the\n // install card so context isn't replaced by a spinner.\n if (initialLoading) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n data-testid=\"plugin-gate-initial-loading\"\n >\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n Checking plugin availability on the agent…\n </div>\n </div>\n );\n }\n\n // Agent is not ready (initializing / awaiting-config / unreachable).\n // The install button would just fail with the same message — instead,\n // render a dedicated waiting state with a retry button and an escape\n // hatch to the Agents page.\n if (errorIsAgentState) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Clock className=\"h-5 w-5 text-status-warning-text shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">Agent is not ready yet</p>\n <p className=\"text-sm text-text-secondary\">\n Your agent is still starting up or waiting for gateway credentials. Plugin install is\n only available once the agent reaches the ready state.\n </p>\n <p className=\"text-xs font-mono text-text-placeholder break-all\">{error}</p>\n <div className=\"flex flex-wrap items-center gap-2 pt-1\">\n <button\n onClick={() => void refetch()}\n disabled={loading}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n data-testid=\"plugin-gate-retry\"\n >\n <RefreshCw className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} />\n Retry\n </button>\n <button\n onClick={() => navigate('/agents')}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:text-text-primary hover:border-border-strong transition-colors\"\n >\n Manage agents\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n // Stack one install card per unsatisfied requirement. The user sees every\n // outstanding dependency at once instead of resolving them one-by-one with\n // a re-render between each.\n return (\n <div className=\"space-y-3\">\n {resolved.map((req, idx) => {\n if (requirementSatisfied[idx]) return null;\n const packagesToOffer =\n req.mode === 'any'\n ? req.packages.filter((p) => !installed[p])\n : req.packages.filter((p) => !installed[p]);\n const defaultTitle =\n req.mode === 'any'\n ? 'Install a plugin to continue'\n : req.packages.length === 1\n ? 'Plugin required'\n : 'Plugins required';\n const defaultDescription =\n req.mode === 'any'\n ? 'Pick any one of the plugins below. Once installed, this surface will render automatically.'\n : req.packages.length === 1\n ? 'This surface needs the following agent plugin installed.'\n : 'This surface needs the following agent plugins installed.';\n\n return (\n <div\n key={idx}\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Puzzle className=\"h-5 w-5 text-action-primary-bg shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0\">\n {req.eyebrow && (\n <p className=\"text-xs uppercase tracking-wide text-text-secondary mb-1\">\n {req.eyebrow}\n </p>\n )}\n <p className=\"text-sm font-semibold text-text-primary\">\n {req.title ?? defaultTitle}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {req.description ?? defaultDescription}\n </p>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mt-3 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n <AlertCircle className=\"h-3.5 w-3.5 mt-0.5 shrink-0\" />\n <span>{error}</span>\n </div>\n )}\n\n <ul className=\"mt-4 space-y-2\">\n {packagesToOffer.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalling = installing === pkg;\n const isBusy = installing !== null;\n return (\n <li\n key={pkg}\n className=\"flex items-center justify-between gap-3 rounded-md border border-border-default bg-bg-sunken px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {meta.displayName}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder truncate\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n <button\n onClick={() => void handleInstall(pkg)}\n disabled={isBusy || loading}\n className=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"h-3.5 w-3.5\" />\n Install\n </>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n\n {loading && packagesToOffer.length === 0 && (\n <div className=\"mt-4 flex items-center gap-2 text-xs text-text-secondary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n Checking plugins…\n </div>\n )}\n </div>\n </div>\n </div>\n );\n })}\n\n <QuotaExhaustedDialog\n isOpen={quotaExhausted}\n onClose={clearQuotaExhausted}\n onConfirm={() => {\n clearQuotaExhausted();\n navigate('/billing');\n }}\n title=\"Plugin quota reached\"\n message=\"You've used all plugin install quota on your current plan. Upgrade to install more plugins.\"\n />\n </div>\n );\n}\n"],"mappings":";;;;;;;;;AAoEA,SAAS,EAAoB,GAA+C;AAqB1E,QApBI,EAAM,gBAAgB,EAAM,aAAa,SAAS,IAC7C,EAAM,aAAa,KAAK,OAAO;EACpC,UAAU,EAAE;EACZ,MAAM,EAAE,QAAQ;EAChB,SAAS,EAAE;EACX,OAAO,EAAE;EACT,aAAa,EAAE;EAChB,EAAE,GAED,EAAM,YAAY,EAAM,SAAS,SAAS,IACrC,CACL;EACE,UAAU,EAAM;EAChB,MAAM,EAAM,QAAQ;EACpB,SAAS,EAAM;EACf,OAAO,EAAM;EACb,aAAa,EAAM;EACpB,CACF,GAEI,EAAE;;AAGX,SAAgB,EAAW,GAAwB;CACjD,IAAM,EAAE,YAAS,aAAU,IAAO,gBAAa,gBAAa,GACtD,EAAE,QAAQ,MAAkB,EAAsB,EAAQ,EAC1D,IAAW,QAAc,EAAoB,EAAM,EAAE,CAAC,EAAM,CAAC,EAU7D,EACJ,YACA,mBACA,cACA,eACA,YACA,UACA,sBACA,mBACA,wBACA,eACE,EAnBgB,QAAc;EAChC,IAAM,oBAAM,IAAI,KAAa;AAC7B,OAAK,IAAM,KAAO,EAChB,MAAK,IAAM,KAAO,EAAI,SAAU,GAAI,IAAI,EAAI;AAE9C,SAAO,CAAC,GAAG,EAAI;IACd,CAAC,EAAS,CAAC,EAayB,GAAS,EAAc,EAExD,IAAW,GAAiB,EAG5B,IAAuB,QAEzB,EAAS,KAAK,MACR,EAAI,SAAS,QAAc,EAAI,SAAS,MAAM,MAAM,CAAC,CAAC,EAAU,GAAG,GAChE,EAAI,SAAS,OAAO,MAAM,CAAC,CAAC,EAAU,GAAG,CAChD,EACJ,CAAC,GAAU,EAAU,CACtB,EAEK,IAAe,EAAS,SAAS,KAAK,EAAqB,MAAM,QAAQ,EAEzE,IAAgB,EACpB,OAAO,MAAgB;AAErB,EADW,MAAM,EAAQ,EAAI,IACrB,KAAe;IAEzB,CAAC,GAAS,EAAY,CACvB;AA8FD,QA3FK,IAwBD,IACK,kBAAA,GAAA,EAAG,aAAY,CAAA,GAQpB,IAEA,kBAAC,OAAD;EACE,WAAW,yDAAyD,IAAU,QAAQ;EACtF,eAAY;YAEZ,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAS,WAAU,wBAAyB,CAAA,EAAA,6CAExC;;EACF,CAAA,GAQN,IAEA,kBAAC,OAAD;EACE,WAAW,yDAAyD,IAAU,QAAQ;YAEtF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAO,WAAU,oDAAqD,CAAA,EACtE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBAA0C;MAA0B,CAAA;KACjF,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAGvC,CAAA;KACJ,kBAAC,KAAD;MAAG,WAAU;gBAAqD;MAAU,CAAA;KAC5E,kBAAC,OAAD;MAAK,WAAU;gBAAf,CACE,kBAAC,UAAD;OACE,eAAe,KAAK,GAAS;OAC7B,UAAU;OACV,WAAU;OACV,eAAY;iBAJd,CAME,kBAAC,GAAD,EAAW,WAAW,eAAe,IAAU,iBAAiB,MAAQ,CAAA,EAAA,QAEjE;UACT,kBAAC,UAAD;OACE,eAAe,EAAS,UAAU;OAClC,WAAU;iBACX;OAEQ,CAAA,CACL;;KACF;MACF;;EACF,CAAA,GAQR,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,EAAS,KAAK,GAAK,MAAQ;AAC1B,OAAI,EAAqB,GAAM,QAAO;GACtC,IAAM,KACJ,EAAI,MACA,EAAI,SAAS,QAAQ,MAAM,CAAC,EAAU,GAAG,GAEzC,IACJ,EAAI,SAAS,QACT,iCACA,EAAI,SAAS,WAAW,IACtB,oBACA,oBACF,IACJ,EAAI,SAAS,QACT,+FACA,EAAI,SAAS,WAAW,IACtB,6DACA;AAER,UACE,kBAAC,OAAD;IAEE,WAAW,yDAAyD,IAAU,QAAQ;cAEtF,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,kDAAmD,CAAA,EACrE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACG,EAAI,WACH,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI;QACH,CAAA;OAEN,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI,SAAS;QACZ,CAAA;OACJ,kBAAC,KAAD;QAAG,WAAU;kBACV,EAAI,eAAe;QAClB,CAAA;OAEH,KACC,kBAAC,OAAD;QACE,MAAK;QACL,WAAU;kBAFZ,CAIE,kBAAC,GAAD,EAAa,WAAU,+BAAgC,CAAA,EACvD,kBAAC,QAAD,EAAA,UAAO,GAAa,CAAA,CAChB;;OAGR,kBAAC,MAAD;QAAI,WAAU;kBACX,EAAgB,KAAK,MAAQ;SAC5B,IAAM,IAAO,EAAc,EAAI,EACzB,IAAe,MAAe,GAC9B,IAAS,MAAe;AAC9B,gBACE,kBAAC,MAAD;UAEE,WAAU;oBAFZ,CAIE,kBAAC,OAAD;WAAK,WAAU;qBAAf;YACE,kBAAC,KAAD;aAAG,WAAU;uBACV,EAAK;aACJ,CAAA;YACJ,kBAAC,KAAD;aAAG,WAAU;uBAAwC,EAAK;aAAgB,CAAA;YAC1E,kBAAC,KAAD;aACE,WAAU;aACV,OAAO;uBAEN;aACC,CAAA;YACA;cACN,kBAAC,UAAD;WACE,eAAe,KAAK,EAAc,EAAI;WACtC,UAAU,KAAU;WACpB,WAAU;WACV,eAAa,kBAAkB;qBAE9B,IACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA,EAAA,cAE/C,EAAA,CAAA,GAEH,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD,EAAU,WAAU,eAAgB,CAAA,EAAA,UAEnC,EAAA,CAAA;WAEE,CAAA,CACN;YAjCE,EAiCF;UAEP;QACC,CAAA;OAEJ,KAAW,EAAgB,WAAW,KACrC,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,GAAD,EAAS,WAAU,4BAA6B,CAAA,EAAA,oBAE5C;;OAEJ;QACF;;IACF,EAjFC,EAiFD;IAER,EAEF,kBAAC,GAAD;GACE,QAAQ;GACR,SAAS;GACT,iBAAiB;AAEf,IADA,GAAqB,EACrB,EAAS,WAAW;;GAEtB,OAAM;GACN,SAAQ;GACR,CAAA,CACE;MA/MJ,kBAAC,OAAD;EACE,WAAW,yDAAyD,IAAU,QAAQ;YAEtF,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,+CAAgD,CAAA,EAClE,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,KAAD;MAAG,WAAU;gBAA0C;MAAmB,CAAA;KAC1E,kBAAC,KAAD;MAAG,WAAU;gBAA8B;MAEvC,CAAA;KACJ,kBAAC,UAAD;MACE,eAAe,EAAS,UAAU;MAClC,WAAU;gBACX;MAEQ,CAAA;KACL;MACF;;EACF,CAAA"}
|
|
1
|
+
{"version":3,"file":"PluginGate.js","names":[],"sources":["../../../src/components/shared/PluginGate.tsx"],"sourcesContent":["/**\n * PluginGate — gates a UI surface on one or more vibecontrols-agent plugins\n * being installed. If missing, renders a card listing each required plugin\n * with an Install button. Once satisfied, renders children.\n *\n * Supports two modes:\n * - Single-group (backwards compat): `required` + `mode` + `title/description`.\n * - Multi-group: `requirements={[{packages, mode, ...}, ...]}` — one fetch\n * for all packages across all groups, unified loading state, stacked\n * install cards for any unsatisfied group. Prevents the \"flicker cascade\"\n * you get when nesting PluginGates (outer resolves, inner starts its own\n * initial-loading spinner, etc.).\n */\n\nimport { useCallback, useMemo, type ReactNode } from 'react';\nimport { Puzzle, Download, Loader2, AlertCircle, Server, RefreshCw, Clock } from 'lucide-react';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { QuotaExhaustedDialog } from './QuotaExhaustedDialog';\nimport { useVibeNavigate } from '@/providers/VibeControlsProvider';\n\nexport interface PluginRequirement {\n /** Package names that satisfy this requirement. */\n packages: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Optional per-group eyebrow, title, description (fall back to defaults). */\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nexport interface PluginGateProps {\n /** Active agent id. When null, gate shows an \"activate an agent\" prompt. */\n agentId: string | null;\n\n // ── Single-group API (backwards compat) ────────────────────────────────\n /** Package names required for the wrapped surface to render. */\n required?: readonly string[];\n /** 'all' (default) — every package must be installed. 'any' — at least one. */\n mode?: 'all' | 'any';\n /** Card heading. */\n title?: string;\n /** Card subheading / instruction line. */\n description?: string;\n /** Optional eyebrow label above the title (e.g. \"Plugin required\"). */\n eyebrow?: string;\n\n // ── Multi-group API (preferred when gating multiple requirements) ──────\n /** Multiple requirement groups — fetched + evaluated in one pass. */\n requirements?: readonly PluginRequirement[];\n\n /** Pass to render a compact version (smaller vertical padding). */\n compact?: boolean;\n /** Optional callback fired after install succeeds and gate resolves. */\n onInstalled?: () => void;\n children: ReactNode;\n}\n\ninterface ResolvedRequirement {\n packages: readonly string[];\n mode: 'all' | 'any';\n eyebrow?: string;\n title?: string;\n description?: string;\n}\n\nfunction resolveRequirements(props: PluginGateProps): ResolvedRequirement[] {\n if (props.requirements && props.requirements.length > 0) {\n return props.requirements.map((r) => ({\n packages: r.packages,\n mode: r.mode ?? 'all',\n eyebrow: r.eyebrow,\n title: r.title,\n description: r.description,\n }));\n }\n if (props.required && props.required.length > 0) {\n return [\n {\n packages: props.required,\n mode: props.mode ?? 'all',\n eyebrow: props.eyebrow,\n title: props.title,\n description: props.description,\n },\n ];\n }\n return [];\n}\n\nexport function PluginGate(props: PluginGateProps) {\n const { agentId, compact = false, onInstalled, children } = props;\n // Plugins are agent-wide (BOFF-2612). `activeProfile` no longer\n // participates in plugin availability — the install state is the\n // same for every profile on the agent.\n const resolved = useMemo(() => resolveRequirements(props), [props]);\n\n const allPackages = useMemo(() => {\n const set = new Set<string>();\n for (const req of resolved) {\n for (const pkg of req.packages) set.add(pkg);\n }\n return [...set];\n }, [resolved]);\n\n const {\n loading,\n initialLoading,\n installed,\n installing,\n install,\n error,\n errorIsAgentState,\n quotaExhausted,\n clearQuotaExhausted,\n refetch,\n } = usePluginAvailability(allPackages, agentId);\n\n const navigate = useVibeNavigate();\n\n // Per-requirement satisfaction.\n const requirementSatisfied = useMemo(\n () =>\n resolved.map((req) => {\n if (req.mode === 'any') return req.packages.some((p) => !!installed[p]);\n return req.packages.every((p) => !!installed[p]);\n }),\n [resolved, installed]\n );\n\n const allSatisfied = resolved.length > 0 && requirementSatisfied.every(Boolean);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n const ok = await install(pkg);\n if (ok) onInstalled?.();\n },\n [install, onInstalled]\n );\n\n // No agent available — universal precondition.\n if (!agentId) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Server className=\"h-5 w-5 text-text-secondary shrink-0 mt-0.5\" />\n <div className=\"flex-1 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">No active agent</p>\n <p className=\"text-sm text-text-secondary\">\n This surface needs an active vibecontrols agent with a tunnel URL and API key.\n </p>\n <button\n onClick={() => navigate('/agents')}\n className=\"px-3 py-1.5 text-xs font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition-colors\"\n >\n Go to Agents\n </button>\n </div>\n </div>\n </div>\n );\n }\n\n if (allSatisfied) {\n return <>{children}</>;\n }\n\n // First-fetch loading state — one spinner for ALL requirements. Prevents\n // the cascade you get with nested PluginGates (outer resolves -> inner\n // starts a fresh initialLoading cycle). After the first fetch settles,\n // subsequent refetches (focus, install, retry) fall through to the\n // install card so context isn't replaced by a spinner.\n if (initialLoading) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n data-testid=\"plugin-gate-initial-loading\"\n >\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n Checking plugin availability on the agent…\n </div>\n </div>\n );\n }\n\n // Agent is not ready (initializing / awaiting-config / unreachable).\n // The install button would just fail with the same message — instead,\n // render a dedicated waiting state with a retry button and an escape\n // hatch to the Agents page.\n if (errorIsAgentState) {\n return (\n <div\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Clock className=\"h-5 w-5 text-status-warning-text shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0 space-y-2\">\n <p className=\"text-sm font-semibold text-text-primary\">Agent is not ready yet</p>\n <p className=\"text-sm text-text-secondary\">\n Your agent is still starting up or waiting for gateway credentials. Plugin install is\n only available once the agent reaches the ready state.\n </p>\n <p className=\"text-xs font-mono text-text-placeholder break-all\">{error}</p>\n <div className=\"flex flex-wrap items-center gap-2 pt-1\">\n <button\n onClick={() => void refetch()}\n disabled={loading}\n className=\"inline-flex items-center gap-1.5 rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n data-testid=\"plugin-gate-retry\"\n >\n <RefreshCw className={`h-3.5 w-3.5 ${loading ? 'animate-spin' : ''}`} />\n Retry\n </button>\n <button\n onClick={() => navigate('/agents')}\n className=\"rounded-md border border-border-default px-3 py-1.5 text-xs font-medium text-text-secondary hover:text-text-primary hover:border-border-strong transition-colors\"\n >\n Manage agents\n </button>\n </div>\n </div>\n </div>\n </div>\n );\n }\n\n // Stack one install card per unsatisfied requirement. The user sees every\n // outstanding dependency at once instead of resolving them one-by-one with\n // a re-render between each.\n return (\n <div className=\"space-y-3\">\n {resolved.map((req, idx) => {\n if (requirementSatisfied[idx]) return null;\n const packagesToOffer =\n req.mode === 'any'\n ? req.packages.filter((p) => !installed[p])\n : req.packages.filter((p) => !installed[p]);\n const defaultTitle =\n req.mode === 'any'\n ? 'Install a plugin to continue'\n : req.packages.length === 1\n ? 'Plugin required'\n : 'Plugins required';\n const defaultDescription =\n req.mode === 'any'\n ? 'Pick any one of the plugins below. Once installed, this surface will render automatically.'\n : req.packages.length === 1\n ? 'This surface needs the following agent plugin installed.'\n : 'This surface needs the following agent plugins installed.';\n\n return (\n <div\n key={idx}\n className={`rounded-lg border border-border-default bg-bg-surface ${compact ? 'p-4' : 'p-6'}`}\n >\n <div className=\"flex items-start gap-3\">\n <Puzzle className=\"h-5 w-5 text-action-primary-bg shrink-0 mt-0.5\" />\n <div className=\"flex-1 min-w-0\">\n {req.eyebrow && (\n <p className=\"text-xs uppercase tracking-wide text-text-secondary mb-1\">\n {req.eyebrow}\n </p>\n )}\n <p className=\"text-sm font-semibold text-text-primary\">\n {req.title ?? defaultTitle}\n </p>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {req.description ?? defaultDescription}\n </p>\n\n {error && (\n <div\n role=\"alert\"\n className=\"mt-3 flex items-start gap-2 rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n <AlertCircle className=\"h-3.5 w-3.5 mt-0.5 shrink-0\" />\n <span>{error}</span>\n </div>\n )}\n\n <ul className=\"mt-4 space-y-2\">\n {packagesToOffer.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalling = installing === pkg;\n const isBusy = installing !== null;\n return (\n <li\n key={pkg}\n className=\"flex items-center justify-between gap-3 rounded-md border border-border-default bg-bg-sunken px-3 py-2\"\n >\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {meta.displayName}\n </p>\n <p className=\"text-xs text-text-secondary truncate\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder truncate\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n <button\n onClick={() => void handleInstall(pkg)}\n disabled={isBusy || loading}\n className=\"inline-flex items-center gap-1.5 whitespace-nowrap rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-text hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"h-3.5 w-3.5\" />\n Install\n </>\n )}\n </button>\n </li>\n );\n })}\n </ul>\n\n {loading && packagesToOffer.length === 0 && (\n <div className=\"mt-4 flex items-center gap-2 text-xs text-text-secondary\">\n <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />\n Checking plugins…\n </div>\n )}\n </div>\n </div>\n </div>\n );\n })}\n\n <QuotaExhaustedDialog\n isOpen={quotaExhausted}\n onClose={clearQuotaExhausted}\n onConfirm={() => {\n clearQuotaExhausted();\n navigate('/billing');\n }}\n title=\"Plugin quota reached\"\n message=\"You've used all plugin install quota on your current plan. Upgrade to install more plugins.\"\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -2,13 +2,13 @@ import { useTr as e } from "../../shared/hooks/useTr.js";
|
|
|
2
2
|
import { TunnelStatus as t } from "../../generated/wspace-types.js";
|
|
3
3
|
import { TunnelStatusBadge as n } from "./TunnelStatusBadge.js";
|
|
4
4
|
import { ProviderBadge as r } from "./ProviderBadge.js";
|
|
5
|
-
import { ExternalLink as i,
|
|
6
|
-
import { motion as
|
|
7
|
-
import { jsx as
|
|
5
|
+
import { ExternalLink as i, Loader2 as a, Play as o, Square as s, Trash2 as c } from "lucide-react";
|
|
6
|
+
import { motion as l } from "framer-motion";
|
|
7
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
8
8
|
//#region src/components/tunnels/TunnelCard.tsx
|
|
9
|
-
function
|
|
10
|
-
let
|
|
11
|
-
return /* @__PURE__ */
|
|
9
|
+
function f({ tunnel: f, isPending: p = !1, onOpen: m, onStart: h, onStop: g, onDelete: _ }) {
|
|
10
|
+
let v = e(), y = !p && f.status !== t.Active && f.status !== t.Provisioning, b = !p && f.status === t.Active, x = f.primaryDomain?.hostname, S = x ? `${f.protocol.toLowerCase() === "tcp" ? "tcp" : "https"}://${x}` : null;
|
|
11
|
+
return /* @__PURE__ */ d(l.div, {
|
|
12
12
|
initial: {
|
|
13
13
|
opacity: 0,
|
|
14
14
|
y: 4
|
|
@@ -18,81 +18,85 @@ function d({ tunnel: d, onOpen: f, onStart: p, onStop: m, onDelete: h }) {
|
|
|
18
18
|
y: 0
|
|
19
19
|
},
|
|
20
20
|
className: "rounded-lg border border-border-subtle bg-bg-surface p-4 hover:border-border-default transition-colors",
|
|
21
|
-
children: [/* @__PURE__ */
|
|
22
|
-
className: "flex items-start justify-between gap-
|
|
23
|
-
children: [/* @__PURE__ */
|
|
21
|
+
children: [/* @__PURE__ */ d("div", {
|
|
22
|
+
className: "flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3",
|
|
23
|
+
children: [/* @__PURE__ */ d("button", {
|
|
24
24
|
type: "button",
|
|
25
|
-
onClick: () => f
|
|
25
|
+
onClick: () => m(f.id),
|
|
26
26
|
className: "flex-1 text-left min-w-0",
|
|
27
27
|
children: [
|
|
28
|
-
/* @__PURE__ */
|
|
29
|
-
className: "flex items-center gap-2 mb-1",
|
|
28
|
+
/* @__PURE__ */ d("div", {
|
|
29
|
+
className: "flex flex-wrap items-center gap-2 mb-1",
|
|
30
30
|
children: [
|
|
31
|
-
/* @__PURE__ */
|
|
31
|
+
/* @__PURE__ */ u("h3", {
|
|
32
32
|
className: "text-base font-semibold text-text-primary truncate",
|
|
33
|
-
children:
|
|
33
|
+
children: f.name
|
|
34
34
|
}),
|
|
35
|
-
/* @__PURE__ */
|
|
36
|
-
/* @__PURE__ */
|
|
35
|
+
/* @__PURE__ */ u(n, { status: f.status }),
|
|
36
|
+
/* @__PURE__ */ u(r, { providerKey: f.providerKey })
|
|
37
37
|
]
|
|
38
38
|
}),
|
|
39
|
-
|
|
39
|
+
S ? /* @__PURE__ */ u("p", {
|
|
40
40
|
className: "text-sm text-text-secondary truncate",
|
|
41
|
-
children:
|
|
42
|
-
}) : /* @__PURE__ */
|
|
41
|
+
children: S
|
|
42
|
+
}) : /* @__PURE__ */ u("p", {
|
|
43
43
|
className: "text-sm text-text-muted",
|
|
44
|
-
children:
|
|
44
|
+
children: v("vibecontrols.tunnels.noDomain", "No domain attached")
|
|
45
45
|
}),
|
|
46
|
-
/* @__PURE__ */
|
|
46
|
+
/* @__PURE__ */ d("p", {
|
|
47
47
|
className: "text-xs text-text-muted mt-1",
|
|
48
48
|
children: [
|
|
49
|
-
|
|
49
|
+
f.protocol,
|
|
50
50
|
":",
|
|
51
|
-
|
|
51
|
+
f.localPort,
|
|
52
52
|
" on ",
|
|
53
|
-
|
|
53
|
+
f.localHost
|
|
54
54
|
]
|
|
55
55
|
})
|
|
56
56
|
]
|
|
57
|
-
}), /* @__PURE__ */
|
|
58
|
-
className: "flex items-center gap-2 flex-shrink-0",
|
|
57
|
+
}), /* @__PURE__ */ d("div", {
|
|
58
|
+
className: "flex items-center gap-2 flex-shrink-0 flex-wrap",
|
|
59
59
|
children: [
|
|
60
|
-
|
|
61
|
-
href:
|
|
60
|
+
S && /* @__PURE__ */ d("a", {
|
|
61
|
+
href: S,
|
|
62
62
|
target: "_blank",
|
|
63
63
|
rel: "noreferrer",
|
|
64
64
|
className: "inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated",
|
|
65
65
|
onClick: (e) => e.stopPropagation(),
|
|
66
|
-
children: [/* @__PURE__ */
|
|
66
|
+
children: [/* @__PURE__ */ u(i, { className: "w-3.5 h-3.5 mr-1" }), v("vibecontrols.tunnels.open", "Open")]
|
|
67
67
|
}),
|
|
68
|
-
|
|
68
|
+
p && /* @__PURE__ */ d("span", {
|
|
69
|
+
className: "inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-muted",
|
|
70
|
+
children: [/* @__PURE__ */ u(a, { className: "w-3.5 h-3.5 mr-1 animate-spin" }), f.status === "ACTIVE" ? v("vibecontrols.tunnels.stopping", "Stopping…") : v("vibecontrols.tunnels.starting", "Starting…")]
|
|
71
|
+
}),
|
|
72
|
+
y && /* @__PURE__ */ d("button", {
|
|
69
73
|
type: "button",
|
|
70
|
-
onClick: () =>
|
|
74
|
+
onClick: () => h(f.id),
|
|
71
75
|
className: "inline-flex items-center rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-fg hover:bg-action-primary-bg-hover",
|
|
72
|
-
children: [/* @__PURE__ */
|
|
76
|
+
children: [/* @__PURE__ */ u(o, { className: "w-3.5 h-3.5 mr-1" }), v("vibecontrols.tunnels.start", "Start")]
|
|
73
77
|
}),
|
|
74
|
-
|
|
78
|
+
b && /* @__PURE__ */ d("button", {
|
|
75
79
|
type: "button",
|
|
76
|
-
onClick: () =>
|
|
80
|
+
onClick: () => g(f.id),
|
|
77
81
|
className: "inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated",
|
|
78
|
-
children: [/* @__PURE__ */
|
|
82
|
+
children: [/* @__PURE__ */ u(s, { className: "w-3.5 h-3.5 mr-1" }), v("vibecontrols.tunnels.stop", "Stop")]
|
|
79
83
|
}),
|
|
80
|
-
/* @__PURE__ */
|
|
84
|
+
/* @__PURE__ */ u("button", {
|
|
81
85
|
type: "button",
|
|
82
|
-
onClick: () =>
|
|
86
|
+
onClick: () => _(f.id, f.name),
|
|
83
87
|
className: "inline-flex items-center rounded-md border border-border-default px-2 py-1.5 text-xs text-status-error-fg hover:bg-status-error-bg",
|
|
84
88
|
"aria-label": "Delete tunnel",
|
|
85
|
-
children: /* @__PURE__ */
|
|
89
|
+
children: /* @__PURE__ */ u(c, { className: "w-3.5 h-3.5" })
|
|
86
90
|
})
|
|
87
91
|
]
|
|
88
92
|
})]
|
|
89
|
-
}),
|
|
93
|
+
}), f.lastError && /* @__PURE__ */ u("div", {
|
|
90
94
|
className: "mt-3 rounded-md bg-status-error-bg px-3 py-2 text-xs text-status-error-fg",
|
|
91
|
-
children:
|
|
95
|
+
children: f.lastError
|
|
92
96
|
})]
|
|
93
97
|
});
|
|
94
98
|
}
|
|
95
99
|
//#endregion
|
|
96
|
-
export {
|
|
100
|
+
export { f as TunnelCard };
|
|
97
101
|
|
|
98
102
|
//# sourceMappingURL=TunnelCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TunnelCard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCard.tsx"],"sourcesContent":["import { motion } from 'framer-motion';\nimport { Play, Square, Trash2, ExternalLink } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport type { ListTunnelsQuery } from '@/generated/wspace-operations';\nimport { TunnelStatus } from '@/generated/wspace-types';\nimport { TunnelStatusBadge } from './TunnelStatusBadge';\nimport { ProviderBadge } from './ProviderBadge';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\ninterface TunnelCardProps {\n tunnel: TunnelRow;\n onOpen: (id: string) => void;\n onStart: (id: string) => void;\n onStop: (id: string) => void;\n onDelete: (id: string, name: string) => void;\n}\n\nexport function TunnelCard({
|
|
1
|
+
{"version":3,"file":"TunnelCard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCard.tsx"],"sourcesContent":["import { motion } from 'framer-motion';\nimport { Loader2, Play, Square, Trash2, ExternalLink } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport type { ListTunnelsQuery } from '@/generated/wspace-operations';\nimport { TunnelStatus } from '@/generated/wspace-types';\nimport { TunnelStatusBadge } from './TunnelStatusBadge';\nimport { ProviderBadge } from './ProviderBadge';\n\ntype TunnelRow = NonNullable<ListTunnelsQuery['tunnels']>['items'][number];\n\ninterface TunnelCardProps {\n tunnel: TunnelRow;\n isPending?: boolean;\n onOpen: (id: string) => void;\n onStart: (id: string) => void;\n onStop: (id: string) => void;\n onDelete: (id: string, name: string) => void;\n}\n\nexport function TunnelCard({\n tunnel,\n isPending = false,\n onOpen,\n onStart,\n onStop,\n onDelete,\n}: TunnelCardProps) {\n const tr = useTr();\n const canStart =\n !isPending &&\n tunnel.status !== TunnelStatus.Active &&\n tunnel.status !== TunnelStatus.Provisioning;\n const canStop = !isPending && tunnel.status === TunnelStatus.Active;\n const hostname = tunnel.primaryDomain?.hostname;\n const publicUrl = hostname\n ? `${tunnel.protocol.toLowerCase() === 'tcp' ? 'tcp' : 'https'}://${hostname}`\n : null;\n\n return (\n <motion.div\n initial={{ opacity: 0, y: 4 }}\n animate={{ opacity: 1, y: 0 }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-4 hover:border-border-default transition-colors\"\n >\n <div className=\"flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3\">\n <button\n type=\"button\"\n onClick={() => onOpen(tunnel.id)}\n className=\"flex-1 text-left min-w-0\"\n >\n <div className=\"flex flex-wrap items-center gap-2 mb-1\">\n <h3 className=\"text-base font-semibold text-text-primary truncate\">{tunnel.name}</h3>\n <TunnelStatusBadge status={tunnel.status} />\n <ProviderBadge providerKey={tunnel.providerKey} />\n </div>\n {publicUrl ? (\n <p className=\"text-sm text-text-secondary truncate\">{publicUrl}</p>\n ) : (\n <p className=\"text-sm text-text-muted\">\n {tr('vibecontrols.tunnels.noDomain', 'No domain attached')}\n </p>\n )}\n <p className=\"text-xs text-text-muted mt-1\">\n {tunnel.protocol}:{tunnel.localPort} on {tunnel.localHost}\n </p>\n </button>\n\n <div className=\"flex items-center gap-2 flex-shrink-0 flex-wrap\">\n {publicUrl && (\n <a\n href={publicUrl}\n target=\"_blank\"\n rel=\"noreferrer\"\n className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated\"\n onClick={(e) => e.stopPropagation()}\n >\n <ExternalLink className=\"w-3.5 h-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.open', 'Open')}\n </a>\n )}\n {isPending && (\n <span className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-muted\">\n <Loader2 className=\"w-3.5 h-3.5 mr-1 animate-spin\" />\n {tunnel.status === 'ACTIVE'\n ? tr('vibecontrols.tunnels.stopping', 'Stopping…')\n : tr('vibecontrols.tunnels.starting', 'Starting…')}\n </span>\n )}\n {canStart && (\n <button\n type=\"button\"\n onClick={() => onStart(tunnel.id)}\n className=\"inline-flex items-center rounded-md bg-action-primary-bg px-3 py-1.5 text-xs font-medium text-action-primary-fg hover:bg-action-primary-bg-hover\"\n >\n <Play className=\"w-3.5 h-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.start', 'Start')}\n </button>\n )}\n {canStop && (\n <button\n type=\"button\"\n onClick={() => onStop(tunnel.id)}\n className=\"inline-flex items-center rounded-md border border-border-default px-3 py-1.5 text-xs text-text-primary hover:bg-bg-elevated\"\n >\n <Square className=\"w-3.5 h-3.5 mr-1\" />\n {tr('vibecontrols.tunnels.stop', 'Stop')}\n </button>\n )}\n <button\n type=\"button\"\n onClick={() => onDelete(tunnel.id, tunnel.name)}\n className=\"inline-flex items-center rounded-md border border-border-default px-2 py-1.5 text-xs text-status-error-fg hover:bg-status-error-bg\"\n aria-label=\"Delete tunnel\"\n >\n <Trash2 className=\"w-3.5 h-3.5\" />\n </button>\n </div>\n </div>\n {tunnel.lastError && (\n <div className=\"mt-3 rounded-md bg-status-error-bg px-3 py-2 text-xs text-status-error-fg\">\n {tunnel.lastError}\n </div>\n )}\n </motion.div>\n );\n}\n"],"mappings":";;;;;;;;AAoBA,SAAgB,EAAW,EACzB,WACA,eAAY,IACZ,WACA,YACA,WACA,eACkB;CAClB,IAAM,IAAK,GAAO,EACZ,IACJ,CAAC,KACD,EAAO,WAAW,EAAa,UAC/B,EAAO,WAAW,EAAa,cAC3B,IAAU,CAAC,KAAa,EAAO,WAAW,EAAa,QACvD,IAAW,EAAO,eAAe,UACjC,IAAY,IACd,GAAG,EAAO,SAAS,aAAa,KAAK,QAAQ,QAAQ,QAAQ,KAAK,MAClE;AAEJ,QACE,kBAAC,EAAO,KAAR;EACE,SAAS;GAAE,SAAS;GAAG,GAAG;GAAG;EAC7B,SAAS;GAAE,SAAS;GAAG,GAAG;GAAG;EAC7B,WAAU;YAHZ,CAKE,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,UAAD;IACE,MAAK;IACL,eAAe,EAAO,EAAO,GAAG;IAChC,WAAU;cAHZ;KAKE,kBAAC,OAAD;MAAK,WAAU;gBAAf;OACE,kBAAC,MAAD;QAAI,WAAU;kBAAsD,EAAO;QAAU,CAAA;OACrF,kBAAC,GAAD,EAAmB,QAAQ,EAAO,QAAU,CAAA;OAC5C,kBAAC,GAAD,EAAe,aAAa,EAAO,aAAe,CAAA;OAC9C;;KACL,IACC,kBAAC,KAAD;MAAG,WAAU;gBAAwC;MAAc,CAAA,GAEnE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,iCAAiC,qBAAqB;MACxD,CAAA;KAEN,kBAAC,KAAD;MAAG,WAAU;gBAAb;OACG,EAAO;OAAS;OAAE,EAAO;OAAU;OAAK,EAAO;OAC9C;;KACG;OAET,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,KACC,kBAAC,KAAD;MACE,MAAM;MACN,QAAO;MACP,KAAI;MACJ,WAAU;MACV,UAAU,MAAM,EAAE,iBAAiB;gBALrC,CAOE,kBAAC,GAAD,EAAc,WAAU,oBAAqB,CAAA,EAC5C,EAAG,6BAA6B,OAAO,CACtC;;KAEL,KACC,kBAAC,QAAD;MAAM,WAAU;gBAAhB,CACE,kBAAC,GAAD,EAAS,WAAU,iCAAkC,CAAA,EACpD,EAAO,WAAW,WACf,EAAG,iCAAiC,YAAY,GAChD,EAAG,iCAAiC,YAAY,CAC/C;;KAER,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAQ,EAAO,GAAG;MACjC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAM,WAAU,oBAAqB,CAAA,EACpC,EAAG,8BAA8B,QAAQ,CACnC;;KAEV,KACC,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAO,EAAO,GAAG;MAChC,WAAU;gBAHZ,CAKE,kBAAC,GAAD,EAAQ,WAAU,oBAAqB,CAAA,EACtC,EAAG,6BAA6B,OAAO,CACjC;;KAEX,kBAAC,UAAD;MACE,MAAK;MACL,eAAe,EAAS,EAAO,IAAI,EAAO,KAAK;MAC/C,WAAU;MACV,cAAW;gBAEX,kBAAC,GAAD,EAAQ,WAAU,eAAgB,CAAA;MAC3B,CAAA;KACL;MACF;MACL,EAAO,aACN,kBAAC,OAAD;GAAK,WAAU;aACZ,EAAO;GACJ,CAAA,CAEG"}
|
|
@@ -270,26 +270,23 @@ function D() {
|
|
|
270
270
|
/* @__PURE__ */ C("div", {
|
|
271
271
|
className: "flex gap-2",
|
|
272
272
|
children: [
|
|
273
|
-
/* @__PURE__ */ S(k, {
|
|
273
|
+
Q?.supportsManagedSubdomains && /* @__PURE__ */ S(k, {
|
|
274
274
|
active: N.hostnameKind === "managed",
|
|
275
275
|
onClick: () => Y("hostnameKind", "managed"),
|
|
276
|
-
disabled: !Q?.supportsManagedSubdomains,
|
|
277
276
|
label: "Managed subdomain",
|
|
278
277
|
hint: "*.vibetunnels.com"
|
|
279
278
|
}),
|
|
280
|
-
/* @__PURE__ */ S(k, {
|
|
279
|
+
Q?.supportsCustomDomains && /* @__PURE__ */ S(k, {
|
|
281
280
|
active: N.hostnameKind === "custom",
|
|
282
281
|
onClick: () => Y("hostnameKind", "custom"),
|
|
283
|
-
disabled: !Q?.supportsCustomDomains,
|
|
284
282
|
label: "Custom domain",
|
|
285
283
|
hint: "TXT verification"
|
|
286
284
|
}),
|
|
287
|
-
/* @__PURE__ */ S(k, {
|
|
285
|
+
$ && /* @__PURE__ */ S(k, {
|
|
288
286
|
active: N.hostnameKind === "auto",
|
|
289
287
|
onClick: () => Y("hostnameKind", "auto"),
|
|
290
|
-
disabled: !$,
|
|
291
288
|
label: "Auto",
|
|
292
|
-
hint: "
|
|
289
|
+
hint: "assigned by Cloudflare"
|
|
293
290
|
})
|
|
294
291
|
]
|
|
295
292
|
}),
|
|
@@ -511,18 +508,17 @@ function O({ label: e, children: t }) {
|
|
|
511
508
|
})]
|
|
512
509
|
});
|
|
513
510
|
}
|
|
514
|
-
function k({ active: e,
|
|
511
|
+
function k({ active: e, onClick: t, label: n, hint: r }) {
|
|
515
512
|
return /* @__PURE__ */ C("button", {
|
|
516
513
|
type: "button",
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
className: `flex-1 rounded-md border p-3 text-left transition-colors ${e ? "border-action-primary-bg bg-bg-sunken" : "border-border-subtle hover:border-border-default"} ${t ? "opacity-50 cursor-not-allowed" : ""}`,
|
|
514
|
+
onClick: t,
|
|
515
|
+
className: `flex-1 rounded-md border p-3 text-left transition-colors ${e ? "border-action-primary-bg bg-bg-sunken" : "border-border-subtle hover:border-border-default"}`,
|
|
520
516
|
children: [/* @__PURE__ */ S("div", {
|
|
521
517
|
className: "text-sm font-medium text-text-primary",
|
|
522
|
-
children:
|
|
518
|
+
children: n
|
|
523
519
|
}), /* @__PURE__ */ S("div", {
|
|
524
520
|
className: "text-xs text-text-muted mt-0.5",
|
|
525
|
-
children:
|
|
521
|
+
children: r
|
|
526
522
|
})]
|
|
527
523
|
});
|
|
528
524
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TunnelCreateWizard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCreateWizard.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport {\n TunnelEnvironment,\n TunnelProtocol,\n TunnelProviderKey,\n type CreateTunnelInput,\n} from '@/generated/wspace-types';\nimport {\n useCreateTunnelMutation,\n useListTunnelProvidersQuery,\n useListAgentsQuery,\n} from '@/generated/wspace-operations';\nimport { getQuotaDialogContent, isQuotaExhaustedError } from '@/utils/quotaUtils';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { useDraftEntityTags } from '@/hooks/useDraftEntityTags';\n\nconst SUBDOMAIN_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;\n\ntype HostnameKind = 'managed' | 'custom' | 'auto';\n\ninterface WizardState {\n providerKey: TunnelProviderKey | '';\n agentId: string;\n name: string;\n description: string;\n protocol: TunnelProtocol;\n localHost: string;\n localPort: number;\n hostnameKind: HostnameKind;\n subdomain: string;\n environment: TunnelEnvironment;\n customDomain: string;\n basicAuthUser: string;\n basicAuthPassword: string;\n}\n\nconst INITIAL_STATE: WizardState = {\n // Default to Vibe Tunnels as the preferred provider; users can still pick\n // Cloudflare Tunnels from the provider list.\n providerKey: TunnelProviderKey.Vibetunnels,\n agentId: '',\n name: '',\n description: '',\n protocol: TunnelProtocol.Http,\n localHost: '127.0.0.1',\n localPort: 3000,\n hostnameKind: 'managed',\n subdomain: '',\n environment: TunnelEnvironment.Prod,\n customDomain: '',\n basicAuthUser: '',\n basicAuthPassword: '',\n};\n\nconst STEPS = [\n { id: 'provider', label: 'Provider' },\n { id: 'target', label: 'Protocol & target' },\n { id: 'hostname', label: 'Hostname' },\n { id: 'policies', label: 'Policies' },\n { id: 'review', label: 'Review' },\n] as const;\n\nexport function TunnelCreateWizard() {\n const tr = useTr();\n const navigate = useNavigate();\n const [step, setStep] = useState(0);\n const [state, setState] = useState<WizardState>(INITIAL_STATE);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [isFlushingTags, setIsFlushingTags] = useState(false);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n // Tags selected before the tunnel exists — persisted right after create.\n const { setDraftTagIds, flushTags } = useDraftEntityTags('VIBECONTROLS_TUNNEL');\n\n const {\n data: providersData,\n loading: providersLoading,\n error: providersError,\n } = useListTunnelProvidersQuery();\n const { data: agentsData } = useListAgentsQuery({ variables: { pagination: { limit: 100 } } });\n\n const [createTunnel, { loading: creating }] = useCreateTunnelMutation();\n\n const update = useCallback(<K extends keyof WizardState>(key: K, value: WizardState[K]) => {\n setState((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const selectProvider = useCallback((providerKey: TunnelProviderKey) => {\n setState((prev) => ({\n ...prev,\n providerKey,\n hostnameKind: providerKey === TunnelProviderKey.Cloudflare ? 'auto' : 'managed',\n }));\n }, []);\n\n // VibeTunnels first, Cloudflare second, everything else alphabetically.\n const orderedProviders = useMemo(\n () =>\n [...(providersData?.tunnelProviders ?? [])].sort((a: { key: string }, b: { key: string }) => {\n const rank = (key: string) => (key === 'VIBETUNNELS' ? 0 : key === 'CLOUDFLARE' ? 1 : 2);\n const diff = rank(a.key) - rank(b.key);\n if (diff !== 0) return diff;\n return a.key.localeCompare(b.key);\n }),\n [providersData]\n );\n\n const selectedProvider = useMemo(\n () => orderedProviders.find((p: { key: string }) => p.key === state.providerKey),\n [orderedProviders, state.providerKey]\n );\n\n const caps = selectedProvider?.capabilities;\n const supportsAutoHostname = state.providerKey === TunnelProviderKey.Cloudflare;\n const protocolOptions = useMemo(() => {\n if (!caps) return [TunnelProtocol.Http];\n const list: TunnelProtocol[] = [];\n if (caps.supportsHttp) list.push(TunnelProtocol.Http);\n if (caps.supportsHttps) list.push(TunnelProtocol.Https);\n if (caps.supportsTcp) list.push(TunnelProtocol.Tcp);\n return list;\n }, [caps]);\n\n const canAdvance = useMemo(() => {\n if (step === 0) return Boolean(state.providerKey && state.agentId && state.name.trim());\n if (step === 1) return state.localPort > 0 && state.localPort < 65536;\n if (step === 2) {\n if (state.hostnameKind === 'managed')\n return Boolean(state.subdomain && SUBDOMAIN_RE.test(state.subdomain));\n if (state.hostnameKind === 'custom') return state.customDomain.trim().includes('.');\n return supportsAutoHostname;\n }\n return true;\n }, [step, state, supportsAutoHostname]);\n\n const next = () => setStep((s) => Math.min(s + 1, STEPS.length - 1));\n const back = () => setStep((s) => Math.max(s - 1, 0));\n\n const submit = async () => {\n setSubmitError(null);\n try {\n const input: CreateTunnelInput = {\n agentId: state.agentId,\n name: state.name,\n description: state.description || undefined,\n providerKey: state.providerKey as TunnelProviderKey,\n protocol: state.protocol,\n localHost: state.localHost,\n localPort: state.localPort,\n environment: state.environment,\n basicAuthUser: state.basicAuthUser || undefined,\n basicAuthPassword: state.basicAuthPassword || undefined,\n managedSubdomain: state.hostnameKind === 'managed' ? state.subdomain : undefined,\n };\n const result = await createTunnel({ variables: { input } });\n\n if (result.error) {\n if (isQuotaExhaustedError(result.error)) {\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(result.error.message);\n return;\n }\n\n const id = result.data?.createTunnel.id;\n if (id) {\n setIsFlushingTags(true);\n try {\n await flushTags(id);\n } finally {\n setIsFlushingTags(false);\n }\n navigate(`/vibecontrols/tunnels/${id}`);\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setSubmitError(null);\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(err instanceof Error ? err.message : String(err));\n }\n };\n\n if (providersLoading) return <LoadingState message=\"Loading providers\" />;\n if (providersError) {\n return (\n <ErrorState\n title=\"Failed to load providers\"\n message={providersError.message}\n onRetry={() => window.location.reload()}\n />\n );\n }\n\n return (\n <div className=\"p-6 max-w-3xl mx-auto\">\n <PageHeader\n breadcrumb={[{ label: 'Tunnels', path: '/vibecontrols/tunnels' }, { label: 'New tunnel' }]}\n title={tr('vibecontrols.tunnels.createTitle', 'Create tunnel')}\n description={tr(\n 'vibecontrols.tunnels.createDescription',\n 'Expose a local service through a managed tunnel.'\n )}\n />\n\n <div className=\"mb-6 flex items-center gap-2\">\n {STEPS.map((s, i) => (\n <div\n key={s.id}\n className={`flex-1 h-1.5 rounded-full ${\n i <= step ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n }`}\n aria-label={`Step ${i + 1}: ${s.label}`}\n />\n ))}\n </div>\n\n <AnimatePresence mode=\"wait\">\n <motion.div\n key={step}\n initial={{ opacity: 0, x: 6 }}\n animate={{ opacity: 1, x: 0 }}\n exit={{ opacity: 0, x: -6 }}\n transition={{ duration: 0.15 }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-6\"\n >\n {step === 0 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Provider</h3>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {orderedProviders.map(\n (p: {\n id: string;\n key: TunnelProviderKey;\n displayName: string;\n description?: string | null;\n }) => (\n <button\n key={p.id}\n type=\"button\"\n onClick={() => selectProvider(p.key)}\n className={`rounded-lg border p-4 text-left transition-colors ${\n state.providerKey === p.key\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n }`}\n >\n <div className=\"font-semibold text-text-primary\">{p.displayName}</div>\n <div className=\"text-xs text-text-muted mt-1\">{p.key}</div>\n {p.description && (\n <div className=\"text-xs text-text-secondary mt-2\">{p.description}</div>\n )}\n </button>\n )\n )}\n </div>\n <LabeledField label=\"Agent\">\n <select\n value={state.agentId}\n onChange={(e) => update('agentId', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">Select agent</option>\n {(agentsData?.vibecontrolsAgents ?? []).map((a: { id: string; name: string }) => (\n <option key={a.id} value={a.id}>\n {a.name}\n </option>\n ))}\n </select>\n </LabeledField>\n <LabeledField label=\"Name\">\n <input\n type=\"text\"\n value={state.name}\n onChange={(e) => update('name', e.target.value)}\n placeholder=\"My dev tunnel\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n <LabeledField label=\"Description (optional)\">\n <input\n type=\"text\"\n value={state.description}\n onChange={(e) => update('description', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 1 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Protocol & target</h3>\n <LabeledField label=\"Protocol\">\n <div className=\"flex gap-2\">\n {protocolOptions.map((proto) => (\n <button\n key={proto}\n type=\"button\"\n onClick={() => update('protocol', proto)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.protocol === proto\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n {proto}\n </button>\n ))}\n </div>\n </LabeledField>\n <LabeledField label=\"Local host\">\n <input\n type=\"text\"\n value={state.localHost}\n onChange={(e) => update('localHost', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Local port\">\n <input\n type=\"number\"\n value={state.localPort}\n onChange={(e) => update('localPort', parseInt(e.target.value, 10) || 0)}\n min={1}\n max={65535}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 2 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Hostname</h3>\n <div className=\"flex gap-2\">\n <HostnameKindButton\n active={state.hostnameKind === 'managed'}\n onClick={() => update('hostnameKind', 'managed')}\n disabled={!caps?.supportsManagedSubdomains}\n label=\"Managed subdomain\"\n hint=\"*.vibetunnels.com\"\n />\n <HostnameKindButton\n active={state.hostnameKind === 'custom'}\n onClick={() => update('hostnameKind', 'custom')}\n disabled={!caps?.supportsCustomDomains}\n label=\"Custom domain\"\n hint=\"TXT verification\"\n />\n <HostnameKindButton\n active={state.hostnameKind === 'auto'}\n onClick={() => update('hostnameKind', 'auto')}\n disabled={!supportsAutoHostname}\n label=\"Auto\"\n hint=\"provider default\"\n />\n </div>\n\n {state.hostnameKind === 'managed' && (\n <>\n <LabeledField label=\"Environment\">\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Prod)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Prod\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Prod (vibetunnels.com)\n </button>\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Alpha)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Alpha\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Alpha\n </button>\n </div>\n </LabeledField>\n <LabeledField label=\"Subdomain label\">\n <input\n type=\"text\"\n value={state.subdomain}\n onChange={(e) => update('subdomain', e.target.value.toLowerCase())}\n placeholder=\"demo\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n {state.subdomain && (\n <p className=\"text-xs text-text-secondary\">\n Will be reserved as{' '}\n <span className=\"font-mono text-text-primary\">\n {state.subdomain}\n {state.environment === TunnelEnvironment.Alpha\n ? '.alpha.vibetunnels.com'\n : '.vibetunnels.com'}\n </span>\n </p>\n )}\n </>\n )}\n\n {state.hostnameKind === 'custom' && (\n <LabeledField label=\"Custom domain\">\n <input\n type=\"text\"\n value={state.customDomain}\n onChange={(e) => update('customDomain', e.target.value.toLowerCase())}\n placeholder=\"preview.example.com\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"text-xs text-text-muted mt-2\">\n Custom domains are registered on create but must be verified via a TXT record\n before they can be attached. You'll see the verification challenge in the\n Domains tab after creation.\n </p>\n </LabeledField>\n )}\n </div>\n )}\n\n {step === 3 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Policies (optional)</h3>\n <LabeledField label=\"Basic auth user\">\n <input\n type=\"text\"\n value={state.basicAuthUser}\n onChange={(e) => update('basicAuthUser', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Basic auth password\">\n <input\n type=\"password\"\n value={state.basicAuthPassword}\n onChange={(e) => update('basicAuthPassword', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 4 && (\n <div className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('vibecontrols.tunnels.review', 'Review')}\n </h3>\n <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm\">\n <dt className=\"text-text-muted\">Provider</dt>\n <dd className=\"text-text-primary\">{state.providerKey}</dd>\n <dt className=\"text-text-muted\">Name</dt>\n <dd className=\"text-text-primary\">{state.name}</dd>\n <dt className=\"text-text-muted\">Agent</dt>\n <dd className=\"text-text-primary font-mono\">{state.agentId}</dd>\n <dt className=\"text-text-muted\">Protocol</dt>\n <dd className=\"text-text-primary\">{state.protocol}</dd>\n <dt className=\"text-text-muted\">Target</dt>\n <dd className=\"text-text-primary font-mono\">\n {state.localHost}:{state.localPort}\n </dd>\n <dt className=\"text-text-muted\">Hostname</dt>\n <dd className=\"text-text-primary\">\n {state.hostnameKind === 'managed'\n ? `${state.subdomain}.${state.environment === 'ALPHA' ? 'alpha.' : ''}vibetunnels.com`\n : state.hostnameKind === 'custom'\n ? state.customDomain\n : 'auto-assigned'}\n </dd>\n <dt className=\"text-text-muted\">Basic auth</dt>\n <dd className=\"text-text-primary\">{state.basicAuthUser ? 'yes' : 'no'}</dd>\n </dl>\n <div className=\"border-t border-border-subtle pt-3\">\n <h4 className=\"mb-2 text-sm font-medium text-text-secondary\">\n {tr('vibecontrols.tunnels.tags', 'Tags')}\n </h4>\n <EntityTagPicker entityType=\"VIBECONTROLS_TUNNEL\" onDraftChange={setDraftTagIds} />\n </div>\n {submitError && (\n <div className=\"rounded-md bg-status-error-bg p-3 text-sm text-status-error-fg\">\n {submitError}\n </div>\n )}\n </div>\n )}\n </motion.div>\n </AnimatePresence>\n\n <div className=\"flex items-center justify-between mt-4\">\n <button\n type=\"button\"\n disabled={step === 0}\n onClick={back}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-elevated disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.back', 'Back')}\n </button>\n {step < STEPS.length - 1 ? (\n <button\n type=\"button\"\n disabled={!canAdvance}\n onClick={next}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.next', 'Next')}\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={creating || isFlushingTags}\n onClick={() => void submit()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {creating\n ? tr('vibecontrols.tunnels.creating', 'Creating...')\n : tr('vibecontrols.tunnels.createTunnel', 'Create tunnel')}\n </button>\n )}\n </div>\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n {...getQuotaDialogContent('tunnel-subdomain')}\n />\n </div>\n );\n}\n\nfunction LabeledField({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <label className=\"block\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n <div className=\"mt-1\">{children}</div>\n </label>\n );\n}\n\nfunction HostnameKindButton({\n active,\n disabled,\n onClick,\n label,\n hint,\n}: {\n active: boolean;\n disabled?: boolean;\n onClick: () => void;\n label: string;\n hint: string;\n}) {\n return (\n <button\n type=\"button\"\n disabled={disabled}\n onClick={onClick}\n className={`flex-1 rounded-md border p-3 text-left transition-colors ${\n active\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n } ${disabled ? 'opacity-50 cursor-not-allowed' : ''}`}\n >\n <div className=\"text-sm font-medium text-text-primary\">{label}</div>\n <div className=\"text-xs text-text-muted mt-0.5\">{hint}</div>\n </button>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TunnelCreateWizard.js","names":[],"sources":["../../../src/components/tunnels/TunnelCreateWizard.tsx"],"sourcesContent":["import { useState, useMemo, useCallback } from 'react';\nimport { useNavigate } from 'react-router-dom';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport { useTr } from '../../shared/hooks/useTr';\n\nimport { PageHeader } from '@/components/shared/PageHeader';\nimport { LoadingState } from '@/components/shared/LoadingState';\nimport { ErrorState } from '@/components/shared/ErrorState';\nimport { QuotaExhaustedDialog } from '@/components/shared/QuotaExhaustedDialog';\nimport {\n TunnelEnvironment,\n TunnelProtocol,\n TunnelProviderKey,\n type CreateTunnelInput,\n} from '@/generated/wspace-types';\nimport {\n useCreateTunnelMutation,\n useListTunnelProvidersQuery,\n useListAgentsQuery,\n} from '@/generated/wspace-operations';\nimport { getQuotaDialogContent, isQuotaExhaustedError } from '@/utils/quotaUtils';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { useDraftEntityTags } from '@/hooks/useDraftEntityTags';\n\nconst SUBDOMAIN_RE = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;\n\ntype HostnameKind = 'managed' | 'custom' | 'auto';\n\ninterface WizardState {\n providerKey: TunnelProviderKey | '';\n agentId: string;\n name: string;\n description: string;\n protocol: TunnelProtocol;\n localHost: string;\n localPort: number;\n hostnameKind: HostnameKind;\n subdomain: string;\n environment: TunnelEnvironment;\n customDomain: string;\n basicAuthUser: string;\n basicAuthPassword: string;\n}\n\nconst INITIAL_STATE: WizardState = {\n // Default to Vibe Tunnels as the preferred provider; users can still pick\n // Cloudflare Tunnels from the provider list.\n providerKey: TunnelProviderKey.Vibetunnels,\n agentId: '',\n name: '',\n description: '',\n protocol: TunnelProtocol.Http,\n localHost: '127.0.0.1',\n localPort: 3000,\n hostnameKind: 'managed',\n subdomain: '',\n environment: TunnelEnvironment.Prod,\n customDomain: '',\n basicAuthUser: '',\n basicAuthPassword: '',\n};\n\nconst STEPS = [\n { id: 'provider', label: 'Provider' },\n { id: 'target', label: 'Protocol & target' },\n { id: 'hostname', label: 'Hostname' },\n { id: 'policies', label: 'Policies' },\n { id: 'review', label: 'Review' },\n] as const;\n\nexport function TunnelCreateWizard() {\n const tr = useTr();\n const navigate = useNavigate();\n const [step, setStep] = useState(0);\n const [state, setState] = useState<WizardState>(INITIAL_STATE);\n const [submitError, setSubmitError] = useState<string | null>(null);\n const [isFlushingTags, setIsFlushingTags] = useState(false);\n const [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n // Tags selected before the tunnel exists — persisted right after create.\n const { setDraftTagIds, flushTags } = useDraftEntityTags('VIBECONTROLS_TUNNEL');\n\n const {\n data: providersData,\n loading: providersLoading,\n error: providersError,\n } = useListTunnelProvidersQuery();\n const { data: agentsData } = useListAgentsQuery({ variables: { pagination: { limit: 100 } } });\n\n const [createTunnel, { loading: creating }] = useCreateTunnelMutation();\n\n const update = useCallback(<K extends keyof WizardState>(key: K, value: WizardState[K]) => {\n setState((prev) => ({ ...prev, [key]: value }));\n }, []);\n\n const selectProvider = useCallback((providerKey: TunnelProviderKey) => {\n setState((prev) => ({\n ...prev,\n providerKey,\n hostnameKind: providerKey === TunnelProviderKey.Cloudflare ? 'auto' : 'managed',\n }));\n }, []);\n\n // VibeTunnels first, Cloudflare second, everything else alphabetically.\n const orderedProviders = useMemo(\n () =>\n [...(providersData?.tunnelProviders ?? [])].sort((a: { key: string }, b: { key: string }) => {\n const rank = (key: string) => (key === 'VIBETUNNELS' ? 0 : key === 'CLOUDFLARE' ? 1 : 2);\n const diff = rank(a.key) - rank(b.key);\n if (diff !== 0) return diff;\n return a.key.localeCompare(b.key);\n }),\n [providersData]\n );\n\n const selectedProvider = useMemo(\n () => orderedProviders.find((p: { key: string }) => p.key === state.providerKey),\n [orderedProviders, state.providerKey]\n );\n\n const caps = selectedProvider?.capabilities;\n const supportsAutoHostname = state.providerKey === TunnelProviderKey.Cloudflare;\n const protocolOptions = useMemo(() => {\n if (!caps) return [TunnelProtocol.Http];\n const list: TunnelProtocol[] = [];\n if (caps.supportsHttp) list.push(TunnelProtocol.Http);\n if (caps.supportsHttps) list.push(TunnelProtocol.Https);\n if (caps.supportsTcp) list.push(TunnelProtocol.Tcp);\n return list;\n }, [caps]);\n\n const canAdvance = useMemo(() => {\n if (step === 0) return Boolean(state.providerKey && state.agentId && state.name.trim());\n if (step === 1) return state.localPort > 0 && state.localPort < 65536;\n if (step === 2) {\n if (state.hostnameKind === 'managed')\n return Boolean(state.subdomain && SUBDOMAIN_RE.test(state.subdomain));\n if (state.hostnameKind === 'custom') return state.customDomain.trim().includes('.');\n return supportsAutoHostname;\n }\n return true;\n }, [step, state, supportsAutoHostname]);\n\n const next = () => setStep((s) => Math.min(s + 1, STEPS.length - 1));\n const back = () => setStep((s) => Math.max(s - 1, 0));\n\n const submit = async () => {\n setSubmitError(null);\n try {\n const input: CreateTunnelInput = {\n agentId: state.agentId,\n name: state.name,\n description: state.description || undefined,\n providerKey: state.providerKey as TunnelProviderKey,\n protocol: state.protocol,\n localHost: state.localHost,\n localPort: state.localPort,\n environment: state.environment,\n basicAuthUser: state.basicAuthUser || undefined,\n basicAuthPassword: state.basicAuthPassword || undefined,\n managedSubdomain: state.hostnameKind === 'managed' ? state.subdomain : undefined,\n };\n const result = await createTunnel({ variables: { input } });\n\n if (result.error) {\n if (isQuotaExhaustedError(result.error)) {\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(result.error.message);\n return;\n }\n\n const id = result.data?.createTunnel.id;\n if (id) {\n setIsFlushingTags(true);\n try {\n await flushTags(id);\n } finally {\n setIsFlushingTags(false);\n }\n navigate(`/vibecontrols/tunnels/${id}`);\n }\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n setSubmitError(null);\n setShowQuotaExhausted(true);\n return;\n }\n\n setSubmitError(err instanceof Error ? err.message : String(err));\n }\n };\n\n if (providersLoading) return <LoadingState message=\"Loading providers\" />;\n if (providersError) {\n return (\n <ErrorState\n title=\"Failed to load providers\"\n message={providersError.message}\n onRetry={() => window.location.reload()}\n />\n );\n }\n\n return (\n <div className=\"p-6 max-w-3xl mx-auto\">\n <PageHeader\n breadcrumb={[{ label: 'Tunnels', path: '/vibecontrols/tunnels' }, { label: 'New tunnel' }]}\n title={tr('vibecontrols.tunnels.createTitle', 'Create tunnel')}\n description={tr(\n 'vibecontrols.tunnels.createDescription',\n 'Expose a local service through a managed tunnel.'\n )}\n />\n\n <div className=\"mb-6 flex items-center gap-2\">\n {STEPS.map((s, i) => (\n <div\n key={s.id}\n className={`flex-1 h-1.5 rounded-full ${\n i <= step ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n }`}\n aria-label={`Step ${i + 1}: ${s.label}`}\n />\n ))}\n </div>\n\n <AnimatePresence mode=\"wait\">\n <motion.div\n key={step}\n initial={{ opacity: 0, x: 6 }}\n animate={{ opacity: 1, x: 0 }}\n exit={{ opacity: 0, x: -6 }}\n transition={{ duration: 0.15 }}\n className=\"rounded-lg border border-border-subtle bg-bg-surface p-6\"\n >\n {step === 0 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Provider</h3>\n <div className=\"grid gap-3 sm:grid-cols-2\">\n {orderedProviders.map(\n (p: {\n id: string;\n key: TunnelProviderKey;\n displayName: string;\n description?: string | null;\n }) => (\n <button\n key={p.id}\n type=\"button\"\n onClick={() => selectProvider(p.key)}\n className={`rounded-lg border p-4 text-left transition-colors ${\n state.providerKey === p.key\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n }`}\n >\n <div className=\"font-semibold text-text-primary\">{p.displayName}</div>\n <div className=\"text-xs text-text-muted mt-1\">{p.key}</div>\n {p.description && (\n <div className=\"text-xs text-text-secondary mt-2\">{p.description}</div>\n )}\n </button>\n )\n )}\n </div>\n <LabeledField label=\"Agent\">\n <select\n value={state.agentId}\n onChange={(e) => update('agentId', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">Select agent</option>\n {(agentsData?.vibecontrolsAgents ?? []).map((a: { id: string; name: string }) => (\n <option key={a.id} value={a.id}>\n {a.name}\n </option>\n ))}\n </select>\n </LabeledField>\n <LabeledField label=\"Name\">\n <input\n type=\"text\"\n value={state.name}\n onChange={(e) => update('name', e.target.value)}\n placeholder=\"My dev tunnel\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n <LabeledField label=\"Description (optional)\">\n <input\n type=\"text\"\n value={state.description}\n onChange={(e) => update('description', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 1 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Protocol & target</h3>\n <LabeledField label=\"Protocol\">\n <div className=\"flex gap-2\">\n {protocolOptions.map((proto) => (\n <button\n key={proto}\n type=\"button\"\n onClick={() => update('protocol', proto)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.protocol === proto\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n {proto}\n </button>\n ))}\n </div>\n </LabeledField>\n <LabeledField label=\"Local host\">\n <input\n type=\"text\"\n value={state.localHost}\n onChange={(e) => update('localHost', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Local port\">\n <input\n type=\"number\"\n value={state.localPort}\n onChange={(e) => update('localPort', parseInt(e.target.value, 10) || 0)}\n min={1}\n max={65535}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 2 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Hostname</h3>\n <div className=\"flex gap-2\">\n {caps?.supportsManagedSubdomains && (\n <HostnameKindButton\n active={state.hostnameKind === 'managed'}\n onClick={() => update('hostnameKind', 'managed')}\n label=\"Managed subdomain\"\n hint=\"*.vibetunnels.com\"\n />\n )}\n {caps?.supportsCustomDomains && (\n <HostnameKindButton\n active={state.hostnameKind === 'custom'}\n onClick={() => update('hostnameKind', 'custom')}\n label=\"Custom domain\"\n hint=\"TXT verification\"\n />\n )}\n {supportsAutoHostname && (\n <HostnameKindButton\n active={state.hostnameKind === 'auto'}\n onClick={() => update('hostnameKind', 'auto')}\n label=\"Auto\"\n hint=\"assigned by Cloudflare\"\n />\n )}\n </div>\n\n {state.hostnameKind === 'managed' && (\n <>\n <LabeledField label=\"Environment\">\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Prod)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Prod\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Prod (vibetunnels.com)\n </button>\n <button\n type=\"button\"\n onClick={() => update('environment', TunnelEnvironment.Alpha)}\n className={`rounded-md border px-4 py-2 text-sm ${\n state.environment === TunnelEnvironment.Alpha\n ? 'border-action-primary-bg bg-bg-sunken text-text-primary'\n : 'border-border-subtle text-text-secondary hover:border-border-default'\n }`}\n >\n Alpha\n </button>\n </div>\n </LabeledField>\n <LabeledField label=\"Subdomain label\">\n <input\n type=\"text\"\n value={state.subdomain}\n onChange={(e) => update('subdomain', e.target.value.toLowerCase())}\n placeholder=\"demo\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n {state.subdomain && (\n <p className=\"text-xs text-text-secondary\">\n Will be reserved as{' '}\n <span className=\"font-mono text-text-primary\">\n {state.subdomain}\n {state.environment === TunnelEnvironment.Alpha\n ? '.alpha.vibetunnels.com'\n : '.vibetunnels.com'}\n </span>\n </p>\n )}\n </>\n )}\n\n {state.hostnameKind === 'custom' && (\n <LabeledField label=\"Custom domain\">\n <input\n type=\"text\"\n value={state.customDomain}\n onChange={(e) => update('customDomain', e.target.value.toLowerCase())}\n placeholder=\"preview.example.com\"\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n <p className=\"text-xs text-text-muted mt-2\">\n Custom domains are registered on create but must be verified via a TXT record\n before they can be attached. You'll see the verification challenge in the\n Domains tab after creation.\n </p>\n </LabeledField>\n )}\n </div>\n )}\n\n {step === 3 && (\n <div className=\"space-y-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">Policies (optional)</h3>\n <LabeledField label=\"Basic auth user\">\n <input\n type=\"text\"\n value={state.basicAuthUser}\n onChange={(e) => update('basicAuthUser', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n <LabeledField label=\"Basic auth password\">\n <input\n type=\"password\"\n value={state.basicAuthPassword}\n onChange={(e) => update('basicAuthPassword', e.target.value)}\n className=\"w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n />\n </LabeledField>\n </div>\n )}\n\n {step === 4 && (\n <div className=\"space-y-3\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {tr('vibecontrols.tunnels.review', 'Review')}\n </h3>\n <dl className=\"grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 text-sm\">\n <dt className=\"text-text-muted\">Provider</dt>\n <dd className=\"text-text-primary\">{state.providerKey}</dd>\n <dt className=\"text-text-muted\">Name</dt>\n <dd className=\"text-text-primary\">{state.name}</dd>\n <dt className=\"text-text-muted\">Agent</dt>\n <dd className=\"text-text-primary font-mono\">{state.agentId}</dd>\n <dt className=\"text-text-muted\">Protocol</dt>\n <dd className=\"text-text-primary\">{state.protocol}</dd>\n <dt className=\"text-text-muted\">Target</dt>\n <dd className=\"text-text-primary font-mono\">\n {state.localHost}:{state.localPort}\n </dd>\n <dt className=\"text-text-muted\">Hostname</dt>\n <dd className=\"text-text-primary\">\n {state.hostnameKind === 'managed'\n ? `${state.subdomain}.${state.environment === 'ALPHA' ? 'alpha.' : ''}vibetunnels.com`\n : state.hostnameKind === 'custom'\n ? state.customDomain\n : 'auto-assigned'}\n </dd>\n <dt className=\"text-text-muted\">Basic auth</dt>\n <dd className=\"text-text-primary\">{state.basicAuthUser ? 'yes' : 'no'}</dd>\n </dl>\n <div className=\"border-t border-border-subtle pt-3\">\n <h4 className=\"mb-2 text-sm font-medium text-text-secondary\">\n {tr('vibecontrols.tunnels.tags', 'Tags')}\n </h4>\n <EntityTagPicker entityType=\"VIBECONTROLS_TUNNEL\" onDraftChange={setDraftTagIds} />\n </div>\n {submitError && (\n <div className=\"rounded-md bg-status-error-bg p-3 text-sm text-status-error-fg\">\n {submitError}\n </div>\n )}\n </div>\n )}\n </motion.div>\n </AnimatePresence>\n\n <div className=\"flex items-center justify-between mt-4\">\n <button\n type=\"button\"\n disabled={step === 0}\n onClick={back}\n className=\"rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-elevated disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.back', 'Back')}\n </button>\n {step < STEPS.length - 1 ? (\n <button\n type=\"button\"\n disabled={!canAdvance}\n onClick={next}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {tr('vibecontrols.tunnels.next', 'Next')}\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={creating || isFlushingTags}\n onClick={() => void submit()}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-fg hover:bg-action-primary-bg-hover disabled:opacity-50\"\n >\n {creating\n ? tr('vibecontrols.tunnels.creating', 'Creating...')\n : tr('vibecontrols.tunnels.createTunnel', 'Create tunnel')}\n </button>\n )}\n </div>\n\n <QuotaExhaustedDialog\n isOpen={showQuotaExhausted}\n onClose={() => setShowQuotaExhausted(false)}\n onConfirm={() => {\n setShowQuotaExhausted(false);\n navigate('/billing');\n }}\n {...getQuotaDialogContent('tunnel-subdomain')}\n />\n </div>\n );\n}\n\nfunction LabeledField({ label, children }: { label: string; children: React.ReactNode }) {\n return (\n <label className=\"block\">\n <span className=\"text-xs font-medium text-text-secondary\">{label}</span>\n <div className=\"mt-1\">{children}</div>\n </label>\n );\n}\n\nfunction HostnameKindButton({\n active,\n onClick,\n label,\n hint,\n}: {\n active: boolean;\n onClick: () => void;\n label: string;\n hint: string;\n}) {\n return (\n <button\n type=\"button\"\n onClick={onClick}\n className={`flex-1 rounded-md border p-3 text-left transition-colors ${\n active\n ? 'border-action-primary-bg bg-bg-sunken'\n : 'border-border-subtle hover:border-border-default'\n }`}\n >\n <div className=\"text-sm font-medium text-text-primary\">{label}</div>\n <div className=\"text-xs text-text-muted mt-0.5\">{hint}</div>\n </button>\n );\n}\n"],"mappings":";;;;;;;;;;;;;;;AAwBA,IAAM,IAAe,wCAoBf,IAA6B;CAGjC,aAAa,EAAkB;CAC/B,SAAS;CACT,MAAM;CACN,aAAa;CACb,UAAU,EAAe;CACzB,WAAW;CACX,WAAW;CACX,cAAc;CACd,WAAW;CACX,aAAa,EAAkB;CAC/B,cAAc;CACd,eAAe;CACf,mBAAmB;CACpB,EAEK,IAAQ;CACZ;EAAE,IAAI;EAAY,OAAO;EAAY;CACrC;EAAE,IAAI;EAAU,OAAO;EAAqB;CAC5C;EAAE,IAAI;EAAY,OAAO;EAAY;CACrC;EAAE,IAAI;EAAY,OAAO;EAAY;CACrC;EAAE,IAAI;EAAU,OAAO;EAAU;CAClC;AAED,SAAgB,IAAqB;CACnC,IAAM,IAAK,GAAO,EACZ,IAAW,GAAa,EACxB,CAAC,GAAM,KAAW,EAAS,EAAE,EAC7B,CAAC,GAAO,KAAY,EAAsB,EAAc,EACxD,CAAC,GAAa,KAAkB,EAAwB,KAAK,EAC7D,CAAC,GAAgB,KAAqB,EAAS,GAAM,EACrD,CAAC,GAAoB,KAAyB,EAAS,GAAM,EAE7D,EAAE,mBAAgB,iBAAc,EAAmB,sBAAsB,EAEzE,EACJ,MAAM,GACN,SAAS,GACT,OAAO,MACL,GAA6B,EAC3B,EAAE,MAAM,MAAe,EAAmB,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,KAAK,EAAE,EAAE,CAAC,EAExF,CAAC,GAAc,EAAE,SAAS,OAAc,GAAyB,EAEjE,IAAS,GAA0C,GAAQ,MAA0B;AACzF,KAAU,OAAU;GAAE,GAAG;IAAO,IAAM;GAAO,EAAE;IAC9C,EAAE,CAAC,EAEA,IAAiB,GAAa,MAAmC;AACrE,KAAU,OAAU;GAClB,GAAG;GACH;GACA,cAAc,MAAgB,EAAkB,aAAa,SAAS;GACvE,EAAE;IACF,EAAE,CAAC,EAGA,IAAmB,QAErB,CAAC,GAAI,GAAe,mBAAmB,EAAE,CAAE,CAAC,MAAM,GAAoB,MAAuB;EAC3F,IAAM,KAAQ,MAAiB,MAAQ,gBAAgB,IAAI,MAAQ,eAAe,IAAI,GAChF,IAAO,EAAK,EAAE,IAAI,GAAG,EAAK,EAAE,IAAI;AAEtC,SADI,MAAS,IACN,EAAE,IAAI,cAAc,EAAE,IAAI,GADV;GAEvB,EACJ,CAAC,EAAc,CAChB,EAOK,IALmB,QACjB,EAAiB,MAAM,MAAuB,EAAE,QAAQ,EAAM,YAAY,EAChF,CAAC,GAAkB,EAAM,YAAY,CACtC,EAE8B,cACzB,IAAuB,EAAM,gBAAgB,EAAkB,YAC/D,KAAkB,QAAc;AACpC,MAAI,CAAC,EAAM,QAAO,CAAC,EAAe,KAAK;EACvC,IAAM,IAAyB,EAAE;AAIjC,SAHI,EAAK,gBAAc,EAAK,KAAK,EAAe,KAAK,EACjD,EAAK,iBAAe,EAAK,KAAK,EAAe,MAAM,EACnD,EAAK,eAAa,EAAK,KAAK,EAAe,IAAI,EAC5C;IACN,CAAC,EAAK,CAAC,EAEJ,KAAa,QACb,MAAS,IAAU,GAAQ,EAAM,eAAe,EAAM,WAAW,EAAM,KAAK,MAAM,IAClF,MAAS,IAAU,EAAM,YAAY,KAAK,EAAM,YAAY,QAC5D,MAAS,IACP,EAAM,iBAAiB,YAClB,GAAQ,EAAM,aAAa,EAAa,KAAK,EAAM,UAAU,IAClE,EAAM,iBAAiB,WAAiB,EAAM,aAAa,MAAM,CAAC,SAAS,IAAI,GAC5E,IAEF,IACN;EAAC;EAAM;EAAO;EAAqB,CAAC,EAEjC,WAAa,GAAS,MAAM,KAAK,IAAI,IAAI,GAAG,EAAM,SAAS,EAAE,CAAC,EAC9D,WAAa,GAAS,MAAM,KAAK,IAAI,IAAI,GAAG,EAAE,CAAC,EAE/C,KAAS,YAAY;AACzB,IAAe,KAAK;AACpB,MAAI;GAcF,IAAM,IAAS,MAAM,EAAa,EAAE,WAAW,EAAE,OAbhB;IAC/B,SAAS,EAAM;IACf,MAAM,EAAM;IACZ,aAAa,EAAM,eAAe,KAAA;IAClC,aAAa,EAAM;IACnB,UAAU,EAAM;IAChB,WAAW,EAAM;IACjB,WAAW,EAAM;IACjB,aAAa,EAAM;IACnB,eAAe,EAAM,iBAAiB,KAAA;IACtC,mBAAmB,EAAM,qBAAqB,KAAA;IAC9C,kBAAkB,EAAM,iBAAiB,YAAY,EAAM,YAAY,KAAA;IACxE,EACuD,EAAE,CAAC;AAE3D,OAAI,EAAO,OAAO;AAChB,QAAI,EAAsB,EAAO,MAAM,EAAE;AACvC,OAAsB,GAAK;AAC3B;;AAGF,MAAe,EAAO,MAAM,QAAQ;AACpC;;GAGF,IAAM,IAAK,EAAO,MAAM,aAAa;AACrC,OAAI,GAAI;AACN,MAAkB,GAAK;AACvB,QAAI;AACF,WAAM,EAAU,EAAG;cACX;AACR,OAAkB,GAAM;;AAE1B,MAAS,yBAAyB,IAAK;;WAElC,GAAK;AACZ,OAAI,EAAsB,EAAI,EAAE;AAE9B,IADA,EAAe,KAAK,EACpB,EAAsB,GAAK;AAC3B;;AAGF,KAAe,aAAe,QAAQ,EAAI,UAAU,OAAO,EAAI,CAAC;;;AAepE,QAXI,IAAyB,kBAAC,GAAD,EAAc,SAAQ,qBAAsB,CAAA,GACrE,IAEA,kBAAC,GAAD;EACE,OAAM;EACN,SAAS,EAAe;EACxB,eAAe,OAAO,SAAS,QAAQ;EACvC,CAAA,GAKJ,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,YAAY,CAAC;KAAE,OAAO;KAAW,MAAM;KAAyB,EAAE,EAAE,OAAO,cAAc,CAAC;IAC1F,OAAO,EAAG,oCAAoC,gBAAgB;IAC9D,aAAa,EACX,0CACA,mDACD;IACD,CAAA;GAEF,kBAAC,OAAD;IAAK,WAAU;cACZ,EAAM,KAAK,GAAG,MACb,kBAAC,OAAD;KAEE,WAAW,6BACT,KAAK,IAAO,yBAAyB;KAEvC,cAAY,QAAQ,IAAI,EAAE,IAAI,EAAE;KAChC,EALK,EAAE,GAKP,CACF;IACE,CAAA;GAEN,kBAAC,GAAD;IAAiB,MAAK;cACpB,kBAAC,EAAO,KAAR;KAEE,SAAS;MAAE,SAAS;MAAG,GAAG;MAAG;KAC7B,SAAS;MAAE,SAAS;MAAG,GAAG;MAAG;KAC7B,MAAM;MAAE,SAAS;MAAG,GAAG;MAAI;KAC3B,YAAY,EAAE,UAAU,KAAM;KAC9B,WAAU;eANZ;MAQG,MAAS,KACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,MAAD;SAAI,WAAU;mBAA0C;SAAa,CAAA;QACrE,kBAAC,OAAD;SAAK,WAAU;mBACZ,EAAiB,KACf,MAMC,kBAAC,UAAD;UAEE,MAAK;UACL,eAAe,EAAe,EAAE,IAAI;UACpC,WAAW,qDACT,EAAM,gBAAgB,EAAE,MACpB,0CACA;oBAPR;WAUE,kBAAC,OAAD;YAAK,WAAU;sBAAmC,EAAE;YAAkB,CAAA;WACtE,kBAAC,OAAD;YAAK,WAAU;sBAAgC,EAAE;YAAU,CAAA;WAC1D,EAAE,eACD,kBAAC,OAAD;YAAK,WAAU;sBAAoC,EAAE;YAAkB,CAAA;WAElE;YAdF,EAAE,GAcA,CAEZ;SACG,CAAA;QACN,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,UAAD;UACE,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,WAAW,EAAE,OAAO,MAAM;UAClD,WAAU;oBAHZ,CAKE,kBAAC,UAAD;WAAQ,OAAM;qBAAG;WAAqB,CAAA,GACpC,GAAY,sBAAsB,EAAE,EAAE,KAAK,MAC3C,kBAAC,UAAD;WAAmB,OAAO,EAAE;qBACzB,EAAE;WACI,EAFI,EAAE,GAEN,CACT,CACK;;SACI,CAAA;QACf,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,QAAQ,EAAE,OAAO,MAAM;UAC/C,aAAY;UACZ,WAAU;UACV,CAAA;SACW,CAAA;QACf,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,eAAe,EAAE,OAAO,MAAM;UACtD,WAAU;UACV,CAAA;SACW,CAAA;QACX;;MAGP,MAAS,KACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,MAAD;SAAI,WAAU;mBAA0C;SAAsB,CAAA;QAC9E,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,OAAD;UAAK,WAAU;oBACZ,GAAgB,KAAK,MACpB,kBAAC,UAAD;WAEE,MAAK;WACL,eAAe,EAAO,YAAY,EAAM;WACxC,WAAW,uCACT,EAAM,aAAa,IACf,4DACA;qBAGL;WACM,EAVF,EAUE,CACT;UACE,CAAA;SACO,CAAA;QACf,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,aAAa,EAAE,OAAO,MAAM;UACpD,WAAU;UACV,CAAA;SACW,CAAA;QACf,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,aAAa,SAAS,EAAE,OAAO,OAAO,GAAG,IAAI,EAAE;UACvE,KAAK;UACL,KAAK;UACL,WAAU;UACV,CAAA;SACW,CAAA;QACX;;MAGP,MAAS,KACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,MAAD;SAAI,WAAU;mBAA0C;SAAa,CAAA;QACrE,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACG,GAAM,6BACL,kBAAC,GAAD;WACE,QAAQ,EAAM,iBAAiB;WAC/B,eAAe,EAAO,gBAAgB,UAAU;WAChD,OAAM;WACN,MAAK;WACL,CAAA;UAEH,GAAM,yBACL,kBAAC,GAAD;WACE,QAAQ,EAAM,iBAAiB;WAC/B,eAAe,EAAO,gBAAgB,SAAS;WAC/C,OAAM;WACN,MAAK;WACL,CAAA;UAEH,KACC,kBAAC,GAAD;WACE,QAAQ,EAAM,iBAAiB;WAC/B,eAAe,EAAO,gBAAgB,OAAO;WAC7C,OAAM;WACN,MAAK;WACL,CAAA;UAEA;;QAEL,EAAM,iBAAiB,aACtB,kBAAA,GAAA,EAAA,UAAA;SACE,kBAAC,GAAD;UAAc,OAAM;oBAClB,kBAAC,OAAD;WAAK,WAAU;qBAAf,CACE,kBAAC,UAAD;YACE,MAAK;YACL,eAAe,EAAO,eAAe,EAAkB,KAAK;YAC5D,WAAW,uCACT,EAAM,gBAAgB,EAAkB,OACpC,4DACA;sBAEP;YAEQ,CAAA,EACT,kBAAC,UAAD;YACE,MAAK;YACL,eAAe,EAAO,eAAe,EAAkB,MAAM;YAC7D,WAAW,uCACT,EAAM,gBAAgB,EAAkB,QACpC,4DACA;sBAEP;YAEQ,CAAA,CACL;;UACO,CAAA;SACf,kBAAC,GAAD;UAAc,OAAM;oBAClB,kBAAC,SAAD;WACE,MAAK;WACL,OAAO,EAAM;WACb,WAAW,MAAM,EAAO,aAAa,EAAE,OAAO,MAAM,aAAa,CAAC;WAClE,aAAY;WACZ,WAAU;WACV,CAAA;UACW,CAAA;SACd,EAAM,aACL,kBAAC,KAAD;UAAG,WAAU;oBAAb;WAA2C;WACrB;WACpB,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CACG,EAAM,WACN,EAAM,gBAAgB,EAAkB,QACrC,2BACA,mBACC;;WACL;;SAEL,EAAA,CAAA;QAGJ,EAAM,iBAAiB,YACtB,kBAAC,GAAD;SAAc,OAAM;mBAApB,CACE,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,gBAAgB,EAAE,OAAO,MAAM,aAAa,CAAC;UACrE,aAAY;UACZ,WAAU;UACV,CAAA,EACF,kBAAC,KAAD;UAAG,WAAU;oBAA+B;UAIxC,CAAA,CACS;;QAEb;;MAGP,MAAS,KACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,MAAD;SAAI,WAAU;mBAA0C;SAAwB,CAAA;QAChF,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,iBAAiB,EAAE,OAAO,MAAM;UACxD,WAAU;UACV,CAAA;SACW,CAAA;QACf,kBAAC,GAAD;SAAc,OAAM;mBAClB,kBAAC,SAAD;UACE,MAAK;UACL,OAAO,EAAM;UACb,WAAW,MAAM,EAAO,qBAAqB,EAAE,OAAO,MAAM;UAC5D,WAAU;UACV,CAAA;SACW,CAAA;QACX;;MAGP,MAAS,KACR,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,MAAD;SAAI,WAAU;mBACX,EAAG,+BAA+B,SAAS;SACzC,CAAA;QACL,kBAAC,MAAD;SAAI,WAAU;mBAAd;UACE,kBAAC,MAAD;WAAI,WAAU;qBAAkB;WAAa,CAAA;UAC7C,kBAAC,MAAD;WAAI,WAAU;qBAAqB,EAAM;WAAiB,CAAA;UAC1D,kBAAC,MAAD;WAAI,WAAU;qBAAkB;WAAS,CAAA;UACzC,kBAAC,MAAD;WAAI,WAAU;qBAAqB,EAAM;WAAU,CAAA;UACnD,kBAAC,MAAD;WAAI,WAAU;qBAAkB;WAAU,CAAA;UAC1C,kBAAC,MAAD;WAAI,WAAU;qBAA+B,EAAM;WAAa,CAAA;UAChE,kBAAC,MAAD;WAAI,WAAU;qBAAkB;WAAa,CAAA;UAC7C,kBAAC,MAAD;WAAI,WAAU;qBAAqB,EAAM;WAAc,CAAA;UACvD,kBAAC,MAAD;WAAI,WAAU;qBAAkB;WAAW,CAAA;UAC3C,kBAAC,MAAD;WAAI,WAAU;qBAAd;YACG,EAAM;YAAU;YAAE,EAAM;YACtB;;UACL,kBAAC,MAAD;WAAI,WAAU;qBAAkB;WAAa,CAAA;UAC7C,kBAAC,MAAD;WAAI,WAAU;qBACX,EAAM,iBAAiB,YACpB,GAAG,EAAM,UAAU,GAAG,EAAM,gBAAgB,UAAU,WAAW,GAAG,mBACpE,EAAM,iBAAiB,WACrB,EAAM,eACN;WACH,CAAA;UACL,kBAAC,MAAD;WAAI,WAAU;qBAAkB;WAAe,CAAA;UAC/C,kBAAC,MAAD;WAAI,WAAU;qBAAqB,EAAM,gBAAgB,QAAQ;WAAU,CAAA;UACxE;;QACL,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAG,6BAA6B,OAAO;UACrC,CAAA,EACL,kBAAC,GAAD;UAAiB,YAAW;UAAsB,eAAe;UAAkB,CAAA,CAC/E;;QACL,KACC,kBAAC,OAAD;SAAK,WAAU;mBACZ;SACG,CAAA;QAEJ;;MAEG;OArRN,EAqRM;IACG,CAAA;GAElB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,MAAS;KACnB,SAAS;KACT,WAAU;eAET,EAAG,6BAA6B,OAAO;KACjC,CAAA,EACR,IAAO,EAAM,SAAS,IACrB,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,CAAC;KACX,SAAS;KACT,WAAU;eAET,EAAG,6BAA6B,OAAO;KACjC,CAAA,GAET,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,KAAY;KACtB,eAAe,KAAK,IAAQ;KAC5B,WAAU;eAET,IACG,EAAG,iCAAiC,cAAc,GAClD,EAAG,qCAAqC,gBAAgB;KACrD,CAAA,CAEP;;GAEN,kBAAC,GAAD;IACE,QAAQ;IACR,eAAe,EAAsB,GAAM;IAC3C,iBAAiB;AAEf,KADA,EAAsB,GAAM,EAC5B,EAAS,WAAW;;IAEtB,GAAI,EAAsB,mBAAmB;IAC7C,CAAA;GACE;;;AAIV,SAAS,EAAa,EAAE,UAAO,eAA0D;AACvF,QACE,kBAAC,SAAD;EAAO,WAAU;YAAjB,CACE,kBAAC,QAAD;GAAM,WAAU;aAA2C;GAAa,CAAA,EACxE,kBAAC,OAAD;GAAK,WAAU;GAAQ;GAAe,CAAA,CAChC;;;AAIZ,SAAS,EAAmB,EAC1B,WACA,YACA,UACA,WAMC;AACD,QACE,kBAAC,UAAD;EACE,MAAK;EACI;EACT,WAAW,4DACT,IACI,0CACA;YANR,CASE,kBAAC,OAAD;GAAK,WAAU;aAAyC;GAAY,CAAA,EACpE,kBAAC,OAAD;GAAK,WAAU;aAAkC;GAAW,CAAA,CACrD"}
|