@burdenoff/microfe-vibecontrols 2026.520.1 → 2026.520.3

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.
@@ -6,8 +6,8 @@ import { ErrorState as o } from "../shared/ErrorState.js";
6
6
  import { QuotaExhaustedDialog as s } from "../shared/QuotaExhaustedDialog.js";
7
7
  import { TunnelEnvironment as c, TunnelProtocol as l, TunnelProviderKey as u } from "../../generated/wspace-types.js";
8
8
  import { getQuotaDialogContent as d, isQuotaExhaustedError as f } from "../../utils/quotaUtils.js";
9
- import { useTagEntity as p } from "../../hooks/useEntityTags.js";
10
- import { EntityTagPicker as m } from "../shared/EntityTagPicker.js";
9
+ import { EntityTagPicker as p } from "../shared/EntityTagPicker.js";
10
+ import { useDraftEntityTags as m } from "../../hooks/useDraftEntityTags.js";
11
11
  import { useCallback as h, useMemo as g, useState as _ } from "react";
12
12
  import { useNavigate as v } from "react-router-dom";
13
13
  import { AnimatePresence as y, motion as b } from "framer-motion";
@@ -50,32 +50,32 @@ var w = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/, T = {
50
50
  }
51
51
  ];
52
52
  function D() {
53
- let D = e(), A = v(), [j, M] = _(0), [N, P] = _(T), [F, I] = _(null), [L, R] = _(!1), [z, B] = _([]), [V] = p(), { data: H, loading: U, error: W } = r(), { data: G } = n({ variables: { pagination: { limit: 100 } } }), [K, { loading: q }] = t(), J = h((e, t) => {
53
+ let D = e(), A = v(), [j, M] = _(0), [N, P] = _(T), [F, I] = _(null), [L, R] = _(!1), [z, B] = _(!1), { setDraftTagIds: V, flushTags: H } = m("VIBECONTROLS_TUNNEL"), { data: U, loading: W, error: G } = r(), { data: K } = n({ variables: { pagination: { limit: 100 } } }), [q, { loading: J }] = t(), Y = h((e, t) => {
54
54
  P((n) => ({
55
55
  ...n,
56
56
  [e]: t
57
57
  }));
58
- }, []), Y = h((e) => {
58
+ }, []), X = h((e) => {
59
59
  P((t) => ({
60
60
  ...t,
61
61
  providerKey: e,
62
62
  hostnameKind: e === u.Cloudflare ? "auto" : "managed"
63
63
  }));
64
- }, []), X = g(() => [...H?.tunnelProviders ?? []].sort((e, t) => {
64
+ }, []), Z = g(() => [...U?.tunnelProviders ?? []].sort((e, t) => {
65
65
  let n = (e) => e === "VIBETUNNELS" ? 0 : e === "CLOUDFLARE" ? 1 : 2, r = n(e.key) - n(t.key);
66
66
  return r === 0 ? e.key.localeCompare(t.key) : r;
67
- }), [H]), Z = g(() => X.find((e) => e.key === N.providerKey), [X, N.providerKey])?.capabilities, Q = N.providerKey === u.Cloudflare, $ = g(() => {
68
- if (!Z) return [l.Http];
67
+ }), [U]), Q = g(() => Z.find((e) => e.key === N.providerKey), [Z, N.providerKey])?.capabilities, $ = N.providerKey === u.Cloudflare, ee = g(() => {
68
+ if (!Q) return [l.Http];
69
69
  let e = [];
70
- return Z.supportsHttp && e.push(l.Http), Z.supportsHttps && e.push(l.Https), Z.supportsTcp && e.push(l.Tcp), e;
71
- }, [Z]), ee = g(() => j === 0 ? !!(N.providerKey && N.agentId && N.name.trim()) : j === 1 ? N.localPort > 0 && N.localPort < 65536 : j === 2 ? N.hostnameKind === "managed" ? !!(N.subdomain && w.test(N.subdomain)) : N.hostnameKind === "custom" ? N.customDomain.trim().includes(".") : Q : !0, [
70
+ return Q.supportsHttp && e.push(l.Http), Q.supportsHttps && e.push(l.Https), Q.supportsTcp && e.push(l.Tcp), e;
71
+ }, [Q]), te = g(() => j === 0 ? !!(N.providerKey && N.agentId && N.name.trim()) : j === 1 ? N.localPort > 0 && N.localPort < 65536 : j === 2 ? N.hostnameKind === "managed" ? !!(N.subdomain && w.test(N.subdomain)) : N.hostnameKind === "custom" ? N.customDomain.trim().includes(".") : $ : !0, [
72
72
  j,
73
73
  N,
74
- Q
75
- ]), te = () => M((e) => Math.min(e + 1, E.length - 1)), ne = () => M((e) => Math.max(e - 1, 0)), re = async () => {
74
+ $
75
+ ]), ne = () => M((e) => Math.min(e + 1, E.length - 1)), re = () => M((e) => Math.max(e - 1, 0)), ie = async () => {
76
76
  I(null);
77
77
  try {
78
- let e = await K({ variables: { input: {
78
+ let e = await q({ variables: { input: {
79
79
  agentId: N.agentId,
80
80
  name: N.name,
81
81
  description: N.description || void 0,
@@ -90,7 +90,7 @@ function D() {
90
90
  } } });
91
91
  if (e.error) {
92
92
  if (f(e.error)) {
93
- R(!0);
93
+ B(!0);
94
94
  return;
95
95
  }
96
96
  I(e.error.message);
@@ -98,26 +98,25 @@ function D() {
98
98
  }
99
99
  let t = e.data?.createTunnel.id;
100
100
  if (t) {
101
- if (z.length > 0) try {
102
- await Promise.all(z.map((e) => V({ variables: { input: {
103
- entityType: "VIBECONTROLS_TUNNEL",
104
- entityId: t,
105
- tagId: e
106
- } } })));
107
- } catch {}
101
+ R(!0);
102
+ try {
103
+ await H(t);
104
+ } finally {
105
+ R(!1);
106
+ }
108
107
  A(`/vibecontrols/tunnels/${t}`);
109
108
  }
110
109
  } catch (e) {
111
110
  if (f(e)) {
112
- I(null), R(!0);
111
+ I(null), B(!0);
113
112
  return;
114
113
  }
115
114
  I(e instanceof Error ? e.message : String(e));
116
115
  }
117
116
  };
118
- return U ? /* @__PURE__ */ S(a, { message: "Loading providers" }) : W ? /* @__PURE__ */ S(o, {
117
+ return W ? /* @__PURE__ */ S(a, { message: "Loading providers" }) : G ? /* @__PURE__ */ S(o, {
119
118
  title: "Failed to load providers",
120
- message: W.message,
119
+ message: G.message,
121
120
  onRetry: () => window.location.reload()
122
121
  }) : /* @__PURE__ */ C("div", {
123
122
  className: "p-6 max-w-3xl mx-auto",
@@ -164,9 +163,9 @@ function D() {
164
163
  }),
165
164
  /* @__PURE__ */ S("div", {
166
165
  className: "grid gap-3 sm:grid-cols-2",
167
- children: X.map((e) => /* @__PURE__ */ C("button", {
166
+ children: Z.map((e) => /* @__PURE__ */ C("button", {
168
167
  type: "button",
169
- onClick: () => Y(e.key),
168
+ onClick: () => X(e.key),
170
169
  className: `rounded-lg border p-4 text-left transition-colors ${N.providerKey === e.key ? "border-action-primary-bg bg-bg-sunken" : "border-border-subtle hover:border-border-default"}`,
171
170
  children: [
172
171
  /* @__PURE__ */ S("div", {
@@ -188,12 +187,12 @@ function D() {
188
187
  label: "Agent",
189
188
  children: /* @__PURE__ */ C("select", {
190
189
  value: N.agentId,
191
- onChange: (e) => J("agentId", e.target.value),
190
+ onChange: (e) => Y("agentId", e.target.value),
192
191
  className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary",
193
192
  children: [/* @__PURE__ */ S("option", {
194
193
  value: "",
195
194
  children: "Select agent"
196
- }), (G?.vibecontrolsAgents ?? []).map((e) => /* @__PURE__ */ S("option", {
195
+ }), (K?.vibecontrolsAgents ?? []).map((e) => /* @__PURE__ */ S("option", {
197
196
  value: e.id,
198
197
  children: e.name
199
198
  }, e.id))]
@@ -204,7 +203,7 @@ function D() {
204
203
  children: /* @__PURE__ */ S("input", {
205
204
  type: "text",
206
205
  value: N.name,
207
- onChange: (e) => J("name", e.target.value),
206
+ onChange: (e) => Y("name", e.target.value),
208
207
  placeholder: "My dev tunnel",
209
208
  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"
210
209
  })
@@ -214,7 +213,7 @@ function D() {
214
213
  children: /* @__PURE__ */ S("input", {
215
214
  type: "text",
216
215
  value: N.description,
217
- onChange: (e) => J("description", e.target.value),
216
+ onChange: (e) => Y("description", e.target.value),
218
217
  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"
219
218
  })
220
219
  })
@@ -231,9 +230,9 @@ function D() {
231
230
  label: "Protocol",
232
231
  children: /* @__PURE__ */ S("div", {
233
232
  className: "flex gap-2",
234
- children: $.map((e) => /* @__PURE__ */ S("button", {
233
+ children: ee.map((e) => /* @__PURE__ */ S("button", {
235
234
  type: "button",
236
- onClick: () => J("protocol", e),
235
+ onClick: () => Y("protocol", e),
237
236
  className: `rounded-md border px-4 py-2 text-sm ${N.protocol === e ? "border-action-primary-bg bg-bg-sunken text-text-primary" : "border-border-subtle text-text-secondary hover:border-border-default"}`,
238
237
  children: e
239
238
  }, e))
@@ -244,7 +243,7 @@ function D() {
244
243
  children: /* @__PURE__ */ S("input", {
245
244
  type: "text",
246
245
  value: N.localHost,
247
- onChange: (e) => J("localHost", e.target.value),
246
+ onChange: (e) => Y("localHost", e.target.value),
248
247
  className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
249
248
  })
250
249
  }),
@@ -253,7 +252,7 @@ function D() {
253
252
  children: /* @__PURE__ */ S("input", {
254
253
  type: "number",
255
254
  value: N.localPort,
256
- onChange: (e) => J("localPort", parseInt(e.target.value, 10) || 0),
255
+ onChange: (e) => Y("localPort", parseInt(e.target.value, 10) || 0),
257
256
  min: 1,
258
257
  max: 65535,
259
258
  className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
@@ -273,22 +272,22 @@ function D() {
273
272
  children: [
274
273
  /* @__PURE__ */ S(k, {
275
274
  active: N.hostnameKind === "managed",
276
- onClick: () => J("hostnameKind", "managed"),
277
- disabled: !Z?.supportsManagedSubdomains,
275
+ onClick: () => Y("hostnameKind", "managed"),
276
+ disabled: !Q?.supportsManagedSubdomains,
278
277
  label: "Managed subdomain",
279
278
  hint: "*.vibetunnels.com"
280
279
  }),
281
280
  /* @__PURE__ */ S(k, {
282
281
  active: N.hostnameKind === "custom",
283
- onClick: () => J("hostnameKind", "custom"),
284
- disabled: !Z?.supportsCustomDomains,
282
+ onClick: () => Y("hostnameKind", "custom"),
283
+ disabled: !Q?.supportsCustomDomains,
285
284
  label: "Custom domain",
286
285
  hint: "TXT verification"
287
286
  }),
288
287
  /* @__PURE__ */ S(k, {
289
288
  active: N.hostnameKind === "auto",
290
- onClick: () => J("hostnameKind", "auto"),
291
- disabled: !Q,
289
+ onClick: () => Y("hostnameKind", "auto"),
290
+ disabled: !$,
292
291
  label: "Auto",
293
292
  hint: "provider default"
294
293
  })
@@ -301,12 +300,12 @@ function D() {
301
300
  className: "flex gap-2",
302
301
  children: [/* @__PURE__ */ S("button", {
303
302
  type: "button",
304
- onClick: () => J("environment", c.Prod),
303
+ onClick: () => Y("environment", c.Prod),
305
304
  className: `rounded-md border px-4 py-2 text-sm ${N.environment === c.Prod ? "border-action-primary-bg bg-bg-sunken text-text-primary" : "border-border-subtle text-text-secondary hover:border-border-default"}`,
306
305
  children: "Prod (vibetunnels.com)"
307
306
  }), /* @__PURE__ */ S("button", {
308
307
  type: "button",
309
- onClick: () => J("environment", c.Alpha),
308
+ onClick: () => Y("environment", c.Alpha),
310
309
  className: `rounded-md border px-4 py-2 text-sm ${N.environment === c.Alpha ? "border-action-primary-bg bg-bg-sunken text-text-primary" : "border-border-subtle text-text-secondary hover:border-border-default"}`,
311
310
  children: "Alpha"
312
311
  })]
@@ -317,7 +316,7 @@ function D() {
317
316
  children: /* @__PURE__ */ S("input", {
318
317
  type: "text",
319
318
  value: N.subdomain,
320
- onChange: (e) => J("subdomain", e.target.value.toLowerCase()),
319
+ onChange: (e) => Y("subdomain", e.target.value.toLowerCase()),
321
320
  placeholder: "demo",
322
321
  className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
323
322
  })
@@ -339,7 +338,7 @@ function D() {
339
338
  children: [/* @__PURE__ */ S("input", {
340
339
  type: "text",
341
340
  value: N.customDomain,
342
- onChange: (e) => J("customDomain", e.target.value.toLowerCase()),
341
+ onChange: (e) => Y("customDomain", e.target.value.toLowerCase()),
343
342
  placeholder: "preview.example.com",
344
343
  className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
345
344
  }), /* @__PURE__ */ S("p", {
@@ -361,7 +360,7 @@ function D() {
361
360
  children: /* @__PURE__ */ S("input", {
362
361
  type: "text",
363
362
  value: N.basicAuthUser,
364
- onChange: (e) => J("basicAuthUser", e.target.value),
363
+ onChange: (e) => Y("basicAuthUser", e.target.value),
365
364
  className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
366
365
  })
367
366
  }),
@@ -370,7 +369,7 @@ function D() {
370
369
  children: /* @__PURE__ */ S("input", {
371
370
  type: "password",
372
371
  value: N.basicAuthPassword,
373
- onChange: (e) => J("basicAuthPassword", e.target.value),
372
+ onChange: (e) => Y("basicAuthPassword", e.target.value),
374
373
  className: "w-full rounded-md border border-border-default bg-bg-surface px-3 py-2 text-sm text-text-primary"
375
374
  })
376
375
  })
@@ -453,9 +452,9 @@ function D() {
453
452
  children: [/* @__PURE__ */ S("h4", {
454
453
  className: "mb-2 text-sm font-medium text-text-secondary",
455
454
  children: D("vibecontrols.tunnels.tags", "Tags")
456
- }), /* @__PURE__ */ S(m, {
455
+ }), /* @__PURE__ */ S(p, {
457
456
  entityType: "VIBECONTROLS_TUNNEL",
458
- onDraftChange: B
457
+ onDraftChange: V
459
458
  })]
460
459
  }),
461
460
  F && /* @__PURE__ */ S("div", {
@@ -472,28 +471,28 @@ function D() {
472
471
  children: [/* @__PURE__ */ S("button", {
473
472
  type: "button",
474
473
  disabled: j === 0,
475
- onClick: ne,
474
+ onClick: re,
476
475
  className: "rounded-md border border-border-default px-4 py-2 text-sm text-text-primary hover:bg-bg-elevated disabled:opacity-50",
477
476
  children: D("vibecontrols.tunnels.back", "Back")
478
477
  }), j < E.length - 1 ? /* @__PURE__ */ S("button", {
479
478
  type: "button",
480
- disabled: !ee,
481
- onClick: te,
479
+ disabled: !te,
480
+ onClick: ne,
482
481
  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",
483
482
  children: D("vibecontrols.tunnels.next", "Next")
484
483
  }) : /* @__PURE__ */ S("button", {
485
484
  type: "button",
486
- disabled: q,
487
- onClick: () => void re(),
485
+ disabled: J || L,
486
+ onClick: () => void ie(),
488
487
  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",
489
- children: q ? D("vibecontrols.tunnels.creating", "Creating...") : D("vibecontrols.tunnels.createTunnel", "Create tunnel")
488
+ children: J ? D("vibecontrols.tunnels.creating", "Creating...") : D("vibecontrols.tunnels.createTunnel", "Create tunnel")
490
489
  })]
491
490
  }),
492
491
  /* @__PURE__ */ S(s, {
493
- isOpen: L,
494
- onClose: () => R(!1),
492
+ isOpen: z,
493
+ onClose: () => B(!1),
495
494
  onConfirm: () => {
496
- R(!1), A("/billing");
495
+ B(!1), A("/billing");
497
496
  },
498
497
  ...d("tunnel-subdomain")
499
498
  })
@@ -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 { useTagEntity } from '@/hooks/useEntityTags';\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 [showQuotaExhausted, setShowQuotaExhausted] = useState(false);\n // Tags selected before the tunnel exists — persisted right after create.\n const [draftTagIds, setDraftTagIds] = useState<string[]>([]);\n const [tagEntity] = useTagEntity();\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 // Persist any tags chosen during creation. Best-effort: the tunnel\n // already exists, so a tag failure must not block navigation —\n // the detail page picker lets the user retry.\n if (draftTagIds.length > 0) {\n try {\n await Promise.all(\n draftTagIds.map((tagId) =>\n tagEntity({\n variables: {\n input: { entityType: 'VIBECONTROLS_TUNNEL', entityId: id, tagId },\n },\n })\n )\n );\n } catch {\n // Swallow — tunnel creation succeeded; tags can be added later.\n }\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}\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 <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":";;;;;;;;;;;;;;;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;UACE,kBAAC,GAAD;WACE,QAAQ,EAAM,iBAAiB;WAC/B,eAAe,EAAO,gBAAgB,UAAU;WAChD,UAAU,CAAC,GAAM;WACjB,OAAM;WACN,MAAK;WACL,CAAA;UACF,kBAAC,GAAD;WACE,QAAQ,EAAM,iBAAiB;WAC/B,eAAe,EAAO,gBAAgB,SAAS;WAC/C,UAAU,CAAC,GAAM;WACjB,OAAM;WACN,MAAK;WACL,CAAA;UACF,kBAAC,GAAD;WACE,QAAQ,EAAM,iBAAiB;WAC/B,eAAe,EAAO,gBAAgB,OAAO;WAC7C,UAAU,CAAC;WACX,OAAM;WACN,MAAK;WACL,CAAA;UACE;;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;OAlRN,EAkRM;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,aACA,YACA,UACA,WAOC;AACD,QACE,kBAAC,UAAD;EACE,MAAK;EACK;EACD;EACT,WAAW,4DACT,IACI,0CACA,mDACL,GAAG,IAAW,kCAAkC;YARnD,CAUE,kBAAC,OAAD;GAAK,WAAU;aAAyC;GAAY,CAAA,EACpE,kBAAC,OAAD;GAAK,WAAU;aAAkC;GAAW,CAAA,CACrD"}
@@ -0,0 +1,29 @@
1
+ import { useTagEntity as e } from "./useEntityTags.js";
2
+ import { useCallback as t, useState as n } from "react";
3
+ //#region src/hooks/useDraftEntityTags.ts
4
+ function r(r) {
5
+ let [i, a] = n([]), [o] = e();
6
+ return {
7
+ draftTagIds: i,
8
+ setDraftTagIds: a,
9
+ flushTags: t(async (e) => {
10
+ if (!(!e || i.length === 0)) try {
11
+ await Promise.allSettled(i.map((t) => o({ variables: { input: {
12
+ entityType: r,
13
+ entityId: e,
14
+ tagId: t
15
+ } } })));
16
+ } finally {
17
+ a([]);
18
+ }
19
+ }, [
20
+ i,
21
+ r,
22
+ o
23
+ ])
24
+ };
25
+ }
26
+ //#endregion
27
+ export { r as useDraftEntityTags };
28
+
29
+ //# sourceMappingURL=useDraftEntityTags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDraftEntityTags.js","names":[],"sources":["../../src/hooks/useDraftEntityTags.ts"],"sourcesContent":["import { useState, useCallback } from 'react';\n\nimport { useTagEntity } from './useEntityTags';\n\nexport type VibecontrolsEntityType =\n | 'VIBECONTROLS_NOTE'\n | 'VIBECONTROLS_VIBE'\n | 'VIBECONTROLS_SESSION'\n | 'VIBECONTROLS_AGENT'\n | 'VIBECONTROLS_TARGET'\n | 'VIBECONTROLS_VIBEDECK'\n | 'VIBECONTROLS_DOCSSITE'\n | 'VIBECONTROLS_DOCSPAGE'\n | 'VIBECONTROLS_TUNNEL'\n | 'VIBECONTROLS_TUNNELDOMAIN'\n | 'VIBECONTROLS_CATALOG'\n | 'VIBECONTROLS_COMPONENT'\n | 'VIBECONTROLS_TEMPLATE'\n | 'VIBECONTROLS_WEBHOOK'\n | 'VIBECONTROLS_SCORECARD'\n | 'VIBECONTROLS_GITOPSPROVIDER'\n | 'VIBECONTROLS_CONFIGURATION'\n | 'VIBECONTROLS_AIPROMPT'\n | 'VIBECONTROLS_UISESSION'\n | 'VIBECONTROLS_ACTION';\n\nexport function useDraftEntityTags(entityType: VibecontrolsEntityType) {\n const [draftTagIds, setDraftTagIds] = useState<string[]>([]);\n const [tagEntity] = useTagEntity();\n\n const flushTags = useCallback(\n async (entityId: string): Promise<void> => {\n if (!entityId || draftTagIds.length === 0) return;\n try {\n await Promise.allSettled(\n draftTagIds.map((tagId) =>\n tagEntity({ variables: { input: { entityType, entityId, tagId } } })\n )\n );\n } finally {\n setDraftTagIds([]);\n }\n },\n [draftTagIds, entityType, tagEntity]\n );\n\n return { draftTagIds, setDraftTagIds, flushTags };\n}\n"],"mappings":";;;AA0BA,SAAgB,EAAmB,GAAoC;CACrE,IAAM,CAAC,GAAa,KAAkB,EAAmB,EAAE,CAAC,EACtD,CAAC,KAAa,GAAc;AAkBlC,QAAO;EAAE;EAAa;EAAgB,WAhBpB,EAChB,OAAO,MAAoC;AACrC,UAAC,KAAY,EAAY,WAAW,GACxC,KAAI;AACF,UAAM,QAAQ,WACZ,EAAY,KAAK,MACf,EAAU,EAAE,WAAW,EAAE,OAAO;KAAE;KAAY;KAAU;KAAO,EAAE,EAAE,CAAC,CACrE,CACF;aACO;AACR,MAAe,EAAE,CAAC;;KAGtB;GAAC;GAAa;GAAY;GAAU,CACrC;EAEgD"}