@burdenoff/microfe-vibecontrols 2026.610.1 → 2026.610.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.
Files changed (61) hide show
  1. package/dist/components/agents/AgentCard.js +80 -93
  2. package/dist/components/agents/AgentCard.js.map +1 -1
  3. package/dist/components/agents/AgentConnectionRecovery.js +1 -1
  4. package/dist/components/agents/AgentConnectionRecovery.js.map +1 -1
  5. package/dist/components/agents/AgentForm.js +3 -3
  6. package/dist/components/agents/AgentForm.js.map +1 -1
  7. package/dist/components/agents/AgentList.js +1 -1
  8. package/dist/components/agents/AgentList.js.map +1 -1
  9. package/dist/components/agents/AgentMaintenanceTab.js +1 -1
  10. package/dist/components/agents/AgentMaintenanceTab.js.map +1 -1
  11. package/dist/components/agents/AgentStatusBadge.js +8 -7
  12. package/dist/components/agents/AgentStatusBadge.js.map +1 -1
  13. package/dist/components/agents/AgentStorageConfig.js +1 -1
  14. package/dist/components/agents/AgentStorageConfig.js.map +1 -1
  15. package/dist/components/agents/ConnectionLogPanel.js +4 -4
  16. package/dist/components/agents/ConnectionLogPanel.js.map +1 -1
  17. package/dist/components/agents/GatewayAuthCard.js +222 -218
  18. package/dist/components/agents/GatewayAuthCard.js.map +1 -1
  19. package/dist/components/agents/OpenInBrowserEditorButton.js +1 -1
  20. package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
  21. package/dist/components/agents/OpenInEditorMenu.js +1 -1
  22. package/dist/components/agents/OpenInEditorMenu.js.map +1 -1
  23. package/dist/components/agents/PluginUIPanel.js +9 -9
  24. package/dist/components/agents/PluginUIPanel.js.map +1 -1
  25. package/dist/components/agents/RemoteTaskPermissionsCard.js +88 -83
  26. package/dist/components/agents/RemoteTaskPermissionsCard.js.map +1 -1
  27. package/dist/components/graph/ConnectionPermissionsDialog.js +3 -3
  28. package/dist/components/graph/ConnectionPermissionsDialog.js.map +1 -1
  29. package/dist/components/graph/MeshAgentNode.js +2 -2
  30. package/dist/components/graph/MeshAgentNode.js.map +1 -1
  31. package/dist/components/graph/MeshDeployWizard.js +4 -4
  32. package/dist/components/graph/MeshDeployWizard.js.map +1 -1
  33. package/dist/components/graph/MeshGraphView.js +1 -1
  34. package/dist/components/graph/MeshGraphView.js.map +1 -1
  35. package/dist/components/graph/nodeIcons.js +10 -11
  36. package/dist/components/graph/nodeIcons.js.map +1 -1
  37. package/dist/components/shared/Card.js +9 -20
  38. package/dist/components/shared/Card.js.map +1 -1
  39. package/dist/components/shared/ConfirmDialog.js +2 -2
  40. package/dist/components/shared/ConfirmDialog.js.map +1 -1
  41. package/dist/components/shared/ErrorState.js +7 -5
  42. package/dist/components/shared/ErrorState.js.map +1 -1
  43. package/dist/components/shared/LoadingState.js +1 -13
  44. package/dist/components/shared/LoadingState.js.map +1 -1
  45. package/dist/components/shared/StatCard.js +8 -8
  46. package/dist/components/shared/StatCard.js.map +1 -1
  47. package/dist/components/shared/StatusBadge.js +2 -2
  48. package/dist/components/shared/StatusBadge.js.map +1 -1
  49. package/dist/components/shared/cardMotion.js +3 -10
  50. package/dist/components/shared/cardMotion.js.map +1 -1
  51. package/dist/components/targets/TargetCard.js +78 -96
  52. package/dist/components/targets/TargetCard.js.map +1 -1
  53. package/dist/pages/AgentDetailsPage.js +6 -6
  54. package/dist/pages/AgentDetailsPage.js.map +1 -1
  55. package/dist/pages/AgentGraphPage.js +2 -2
  56. package/dist/pages/AgentGraphPage.js.map +1 -1
  57. package/dist/pages/AgentsPage.js +1 -1
  58. package/dist/pages/AgentsPage.js.map +1 -1
  59. package/dist/pages/TargetsPage.js +10 -10
  60. package/dist/pages/TargetsPage.js.map +1 -1
  61. package/package.json +1 -1
@@ -5,118 +5,105 @@ import { getAgentStatus as r } from "./agentStatus.js";
5
5
  import { classifyAgentHealth as i } from "../../utils/agentHealth.js";
6
6
  import { AgentStatusBadge as a } from "./AgentStatusBadge.js";
7
7
  import { Clock as o, Cpu as s, Globe as c, Link as l, Play as u, RefreshCw as d, Server as f, Settings as p, Square as m } from "lucide-react";
8
- import { motion as h, useReducedMotion as g } from "framer-motion";
9
- import { jsx as _, jsxs as v } from "react/jsx-runtime";
10
- import { Checkbox as y } from "@burdenoff/fe-libs/ui";
8
+ import { jsx as h, jsxs as g } from "react/jsx-runtime";
9
+ import { Checkbox as _ } from "@burdenoff/fe-libs/ui";
11
10
  //#region src/components/agents/AgentCard.tsx
12
- var b = ({ agent: b, onSelect: x, onConfigure: S, onSettings: C, onStart: w, onStop: T, onDisconnect: E, onEditTunnelUrl: D, selected: O = !1, onToggleSelect: k, className: A = "", healthErrorMessage: j }) => {
13
- let M = e(), N = g(), P = S || C, F = r(b), I = (e) => {
14
- if (!e) return M("agentsPage.never", "Never");
11
+ var v = ({ agent: v, onSelect: y, onConfigure: b, onSettings: x, onStart: S, onStop: C, onDisconnect: w, onEditTunnelUrl: T, selected: E = !1, onToggleSelect: D, className: O = "", healthErrorMessage: k }) => {
12
+ let A = e(), j = b || x, M = r(v), N = (e) => {
13
+ if (!e) return A("agentsPage.never", "Never");
15
14
  let t = Date.now() - new Date(e).getTime();
16
- return t < 6e4 ? M("agentsPage.justNow", "Just now") : t < 36e5 ? `${Math.floor(t / 6e4)}m ago` : t < 864e5 ? `${Math.floor(t / 36e5)}h ago` : `${Math.floor(t / 864e5)}d ago`;
17
- }, L = [];
18
- return F === "ACTIVE" && T && L.push({
15
+ return t < 6e4 ? A("agentsPage.justNow", "Just now") : t < 36e5 ? `${Math.floor(t / 6e4)}m ago` : t < 864e5 ? `${Math.floor(t / 36e5)}h ago` : `${Math.floor(t / 864e5)}d ago`;
16
+ }, P = [];
17
+ return M === "ACTIVE" && C && P.push({
19
18
  key: "stop",
20
- label: M("agentsPage.stopAgent", "Stop Agent"),
19
+ label: A("agentsPage.stopAgent", "Stop Agent"),
21
20
  icon: m,
22
- onSelect: () => T(b)
23
- }), (F === "STOPPED" || F === "OFFLINE") && w && L.push({
21
+ onSelect: () => C(v)
22
+ }), (M === "STOPPED" || M === "OFFLINE") && S && P.push({
24
23
  key: "start",
25
- label: M("agentsPage.startAgent", "Start Agent"),
24
+ label: A("agentsPage.startAgent", "Start Agent"),
26
25
  icon: u,
27
- onSelect: () => w(b)
28
- }), F === "DEGRADED" && w && L.push({
26
+ onSelect: () => S(v)
27
+ }), M === "DEGRADED" && S && P.push({
29
28
  key: "reconnect",
30
- label: M("agentsPage.reconnectAgent", "Reconnect"),
29
+ label: A("agentsPage.reconnectAgent", "Reconnect"),
31
30
  icon: d,
32
- onSelect: () => w(b)
33
- }), (F === "ACTIVE" || F === "STOPPED" || F === "OFFLINE" || F === "DEGRADED") && E && L.push({
31
+ onSelect: () => S(v)
32
+ }), (M === "ACTIVE" || M === "STOPPED" || M === "OFFLINE" || M === "DEGRADED") && w && P.push({
34
33
  key: "disconnect",
35
- label: F === "OFFLINE" ? M("agentsPage.clearConnection", "Clear Connection") : M("agentsPage.disconnect", "Disconnect"),
34
+ label: M === "OFFLINE" ? A("agentsPage.clearConnection", "Clear Connection") : A("agentsPage.disconnect", "Disconnect"),
36
35
  icon: m,
37
- onSelect: () => E(b),
36
+ onSelect: () => w(v),
38
37
  variant: "danger"
39
- }), D && L.push({
38
+ }), T && P.push({
40
39
  key: "edit-tunnel",
41
- label: M("agentsPage.editTunnel", "Edit Tunnel"),
40
+ label: A("agentsPage.editTunnel", "Edit Tunnel"),
42
41
  icon: l,
43
- onSelect: () => D(b)
44
- }), P && L.push({
42
+ onSelect: () => T(v)
43
+ }), j && P.push({
45
44
  key: "settings",
46
- label: M("agentsPage.settings", "Settings"),
45
+ label: A("agentsPage.settings", "Settings"),
47
46
  icon: p,
48
- onSelect: () => P(b)
49
- }), /* @__PURE__ */ v(h.div, {
47
+ onSelect: () => j(v)
48
+ }), /* @__PURE__ */ g("div", {
50
49
  role: "button",
51
50
  tabIndex: 0,
52
- whileHover: N ? void 0 : {
53
- y: -2,
54
- transition: {
55
- duration: .15,
56
- ease: [
57
- .4,
58
- 0,
59
- .2,
60
- 1
61
- ]
62
- }
63
- },
64
- className: `bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] transition-all cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${O ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : "border-border-default"} ${A}`,
65
- onClick: () => x?.(b),
51
+ className: `bg-bg-surface border rounded-card p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-1)] transition-[colors,box-shadow] duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${E ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : "border-border-default"} ${O}`,
52
+ onClick: () => y?.(v),
66
53
  onKeyDown: (e) => {
67
- e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), x?.(b));
54
+ e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), y?.(v));
68
55
  },
69
56
  children: [
70
- /* @__PURE__ */ v("div", {
57
+ /* @__PURE__ */ g("div", {
71
58
  className: "flex items-start justify-between gap-2",
72
- children: [/* @__PURE__ */ v("div", {
59
+ children: [/* @__PURE__ */ g("div", {
73
60
  className: "flex items-start gap-2 sm:gap-3 min-w-0 flex-1",
74
61
  children: [
75
- k && /* @__PURE__ */ _("span", {
62
+ D && /* @__PURE__ */ h("span", {
76
63
  className: "flex flex-shrink-0 items-center justify-center max-md:min-h-11 max-md:min-w-11 max-md:-m-2 md:mt-0.5",
77
64
  onClick: (e) => {
78
- e.stopPropagation(), e.target === e.currentTarget && k();
65
+ e.stopPropagation(), e.target === e.currentTarget && D();
79
66
  },
80
- children: /* @__PURE__ */ _(y, {
81
- checked: O,
82
- onCheckedChange: () => k(),
67
+ children: /* @__PURE__ */ h(_, {
68
+ checked: E,
69
+ onCheckedChange: () => D(),
83
70
  onClick: (e) => e.stopPropagation(),
84
- "aria-label": M("agentsPage.selectAgent", "Select {{name}}", { name: b.name })
71
+ "aria-label": A("agentsPage.selectAgent", "Select {{name}}", { name: v.name })
85
72
  })
86
73
  }),
87
- /* @__PURE__ */ _("div", {
74
+ /* @__PURE__ */ h("div", {
88
75
  className: "flex-shrink-0 size-8 sm:size-10 rounded-lg bg-bg-sunken flex items-center justify-center",
89
- children: /* @__PURE__ */ _(f, {
76
+ children: /* @__PURE__ */ h(f, {
90
77
  className: "size-4 sm:size-5 text-text-secondary",
91
78
  "aria-hidden": "true"
92
79
  })
93
80
  }),
94
- /* @__PURE__ */ v("div", {
81
+ /* @__PURE__ */ g("div", {
95
82
  className: "min-w-0 flex-1",
96
83
  children: [
97
- /* @__PURE__ */ v("div", {
84
+ /* @__PURE__ */ g("div", {
98
85
  className: "flex items-center gap-1.5 sm:gap-2 min-w-0",
99
- children: [/* @__PURE__ */ _("h3", {
86
+ children: [/* @__PURE__ */ h("h3", {
100
87
  className: "font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base",
101
- children: b.name
102
- }), /* @__PURE__ */ _(n, {
103
- platform: b.platform,
88
+ children: v.name
89
+ }), /* @__PURE__ */ h(n, {
90
+ platform: v.platform,
104
91
  className: "size-4 flex-shrink-0 text-text-secondary"
105
92
  })]
106
93
  }),
107
- /* @__PURE__ */ _("p", {
94
+ /* @__PURE__ */ h("p", {
108
95
  className: "text-xs sm:text-sm text-text-secondary truncate",
109
- children: b.hostname || `ID: ${b.id}`
96
+ children: v.hostname || `ID: ${v.id}`
110
97
  }),
111
98
  (() => {
112
- let e = i(F, j ?? null);
113
- return e.phase === "none" ? null : /* @__PURE__ */ v("p", {
99
+ let e = i(M, k ?? null);
100
+ return e.phase === "none" ? null : /* @__PURE__ */ g("p", {
114
101
  className: `mt-1 flex items-start gap-1 text-xs ${e.phase === "attention" ? "text-status-warning-text" : "text-content-tertiary"}`,
115
- children: [e.inProgress && /* @__PURE__ */ _(d, {
102
+ children: [e.inProgress && /* @__PURE__ */ h(d, {
116
103
  className: "size-3 mt-0.5 flex-shrink-0 animate-spin",
117
104
  "aria-hidden": "true"
118
- }), /* @__PURE__ */ v("span", { children: [
119
- /* @__PURE__ */ v("span", {
105
+ }), /* @__PURE__ */ g("span", { children: [
106
+ /* @__PURE__ */ g("span", {
120
107
  className: "font-medium",
121
108
  children: [e.label, "."]
122
109
  }),
@@ -128,75 +115,75 @@ var b = ({ agent: b, onSelect: x, onConfigure: S, onSettings: C, onStart: w, onS
128
115
  ]
129
116
  })
130
117
  ]
131
- }), /* @__PURE__ */ v("div", {
118
+ }), /* @__PURE__ */ g("div", {
132
119
  className: "flex items-center gap-1 sm:gap-2 flex-shrink-0",
133
- children: [/* @__PURE__ */ _(a, {
134
- status: F,
135
- reason: j ?? null,
136
- linkTo: `/vibecontrols/agents/${b.id}?tab=connection`,
120
+ children: [/* @__PURE__ */ h(a, {
121
+ status: M,
122
+ reason: k ?? null,
123
+ linkTo: `/vibecontrols/agents/${v.id}?tab=connection`,
137
124
  onClick: (e) => {
138
125
  e.stopPropagation();
139
126
  }
140
- }), (P || w || T) && L.length > 0 && /* @__PURE__ */ _(t, {
141
- ariaLabel: M("agentsPage.agentActionsMenu", "Agent actions menu"),
142
- title: M("agentsPage.agentActions", "Agent actions"),
127
+ }), (j || S || C) && P.length > 0 && /* @__PURE__ */ h(t, {
128
+ ariaLabel: A("agentsPage.agentActionsMenu", "Agent actions menu"),
129
+ title: A("agentsPage.agentActions", "Agent actions"),
143
130
  align: "end",
144
- items: L
131
+ items: P
145
132
  })]
146
133
  })]
147
134
  }),
148
- /* @__PURE__ */ v("div", {
135
+ /* @__PURE__ */ g("div", {
149
136
  className: "mt-4 grid grid-cols-2 gap-3 text-sm",
150
- children: [/* @__PURE__ */ v("div", {
137
+ children: [/* @__PURE__ */ g("div", {
151
138
  className: "flex items-center gap-2 text-text-secondary",
152
- children: [/* @__PURE__ */ _(o, {
139
+ children: [/* @__PURE__ */ h(o, {
153
140
  className: "size-4",
154
141
  "aria-hidden": "true"
155
- }), /* @__PURE__ */ _("span", { children: I(b.lastHeartbeat) })]
156
- }), b.architecture && /* @__PURE__ */ v("div", {
142
+ }), /* @__PURE__ */ h("span", { children: N(v.lastHeartbeat) })]
143
+ }), v.architecture && /* @__PURE__ */ g("div", {
157
144
  className: "flex items-center gap-2 text-text-secondary",
158
- children: [/* @__PURE__ */ _(s, {
145
+ children: [/* @__PURE__ */ h(s, {
159
146
  className: "size-4",
160
147
  "aria-hidden": "true"
161
- }), /* @__PURE__ */ _("span", { children: b.architecture })]
148
+ }), /* @__PURE__ */ h("span", { children: v.architecture })]
162
149
  })]
163
150
  }),
164
- b.tunnelUrl && /* @__PURE__ */ v("div", {
151
+ v.tunnelUrl && /* @__PURE__ */ g("div", {
165
152
  className: "mt-3 flex items-center gap-2 text-sm",
166
153
  children: [
167
- /* @__PURE__ */ _(c, {
154
+ /* @__PURE__ */ h(c, {
168
155
  className: "size-4 flex-shrink-0 text-text-secondary",
169
156
  "aria-hidden": "true"
170
157
  }),
171
- /* @__PURE__ */ _("span", {
158
+ /* @__PURE__ */ h("span", {
172
159
  className: "text-text-secondary flex-shrink-0",
173
160
  children: "Tunnel:"
174
161
  }),
175
- /* @__PURE__ */ _("a", {
176
- href: b.tunnelUrl,
162
+ /* @__PURE__ */ h("a", {
163
+ href: v.tunnelUrl,
177
164
  target: "_blank",
178
165
  rel: "noopener noreferrer",
179
166
  onClick: (e) => e.stopPropagation(),
180
- "aria-label": M("agentsPage.tunnelOpensInNewTab", "{{url}} (opens in new tab)", { url: b.tunnelUrl }),
167
+ "aria-label": A("agentsPage.tunnelOpensInNewTab", "{{url}} (opens in new tab)", { url: v.tunnelUrl }),
181
168
  className: "min-w-0 truncate text-text-link hover:text-text-linkHover hover:underline",
182
- children: b.tunnelUrl
169
+ children: v.tunnelUrl
183
170
  })
184
171
  ]
185
172
  }),
186
- b.capabilities && b.capabilities.length > 0 && /* @__PURE__ */ v("div", {
173
+ v.capabilities && v.capabilities.length > 0 && /* @__PURE__ */ g("div", {
187
174
  className: "mt-3 flex flex-wrap gap-1",
188
- children: [b.capabilities.slice(0, 4).map((e) => /* @__PURE__ */ _("span", {
175
+ children: [v.capabilities.slice(0, 4).map((e) => /* @__PURE__ */ h("span", {
189
176
  className: "px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill",
190
177
  children: e
191
- }, e)), b.capabilities.length > 4 && /* @__PURE__ */ v("span", {
178
+ }, e)), v.capabilities.length > 4 && /* @__PURE__ */ g("span", {
192
179
  className: "px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill",
193
- children: ["+", b.capabilities.length - 4]
180
+ children: ["+", v.capabilities.length - 4]
194
181
  })]
195
182
  })
196
183
  ]
197
184
  });
198
185
  };
199
186
  //#endregion
200
- export { b as AgentCard };
187
+ export { v as AgentCard };
201
188
 
202
189
  //# sourceMappingURL=AgentCard.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"AgentCard.js","names":[],"sources":["../../../src/components/agents/AgentCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { motion, useReducedMotion } from 'framer-motion';\nimport { Server, Clock, Cpu, Play, Square, Settings, Globe, Link, RefreshCw } from 'lucide-react';\nimport { Checkbox } from '@burdenoff/fe-libs/ui';\nimport { AgentStatusBadge } from './AgentStatusBadge';\nimport type { Agent } from './types';\nimport { getAgentStatus } from './agentStatus';\nimport { classifyAgentHealth } from '../../utils/agentHealth';\nimport { ActionMenu, type ActionMenuItem } from '../shared/ActionMenu';\nimport { PlatformIcon } from '../shared/PlatformIcon';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport interface AgentCardProps {\n agent: Agent;\n onSelect?: (agent: Agent) => void;\n onConfigure?: (agent: Agent) => void;\n onSettings?: (agent: Agent) => void;\n onStart?: (agent: Agent) => void;\n onStop?: (agent: Agent) => void;\n onDisconnect?: (agent: Agent) => void;\n onEditTunnelUrl?: (agent: Agent) => void;\n selected?: boolean;\n onToggleSelect?: () => void;\n className?: string;\n /**\n * Last error from /health/ready or the live probe — shown in the badge\n * tooltip when the agent is degraded so users know WHY without curling\n * the tunnel themselves.\n */\n healthErrorMessage?: string | null;\n}\n\n/**\n * AgentCard - Display agent information in a card format\n *\n * @example\n * ```tsx\n * <AgentCard\n * agent={myAgent}\n * onSelect={handleSelect}\n * onConfigure={handleConfigure}\n * />\n * ```\n */\nexport const AgentCard: FC<AgentCardProps> = ({\n agent,\n onSelect,\n onConfigure,\n onSettings,\n onStart,\n onStop,\n onDisconnect,\n onEditTunnelUrl,\n selected = false,\n onToggleSelect,\n className = '',\n healthErrorMessage,\n}) => {\n const tr = useTr();\n const reduceMotion = useReducedMotion();\n // Support both onConfigure and onSettings\n const handleConfigure = onConfigure || onSettings;\n\n const status = getAgentStatus(agent);\n\n const formatLastHeartbeat = (timestamp: string | null | undefined) => {\n if (!timestamp) return tr('agentsPage.never', 'Never');\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return tr('agentsPage.justNow', 'Just now');\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\n if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;\n return `${Math.floor(diff / 86400000)}d ago`;\n };\n\n const menuItems: ActionMenuItem[] = [];\n if (status === 'ACTIVE' && onStop) {\n menuItems.push({\n key: 'stop',\n label: tr('agentsPage.stopAgent', 'Stop Agent'),\n icon: Square,\n onSelect: () => onStop(agent),\n });\n }\n if ((status === 'STOPPED' || status === 'OFFLINE') && onStart) {\n menuItems.push({\n key: 'start',\n label: tr('agentsPage.startAgent', 'Start Agent'),\n icon: Play,\n onSelect: () => onStart(agent),\n });\n }\n if (status === 'DEGRADED' && onStart) {\n // Reconnect on a degraded agent fires the same startVibecontrolsAgent\n // mutation as a fresh start — svc-side that POSTs lifecycle/start to the\n // agent's tunnel, which usually clears transient failures (tunnel hiccup,\n // finalize retry). Labelled \"Reconnect\" not \"Start\" so the user knows\n // it's a recovery action, not creating a new instance.\n menuItems.push({\n key: 'reconnect',\n label: tr('agentsPage.reconnectAgent', 'Reconnect'),\n icon: RefreshCw,\n onSelect: () => onStart(agent),\n });\n }\n if (\n (status === 'ACTIVE' ||\n status === 'STOPPED' ||\n status === 'OFFLINE' ||\n status === 'DEGRADED') &&\n onDisconnect\n ) {\n menuItems.push({\n key: 'disconnect',\n label:\n status === 'OFFLINE'\n ? tr('agentsPage.clearConnection', 'Clear Connection')\n : tr('agentsPage.disconnect', 'Disconnect'),\n icon: Square,\n onSelect: () => onDisconnect(agent),\n variant: 'danger',\n });\n }\n if (onEditTunnelUrl) {\n menuItems.push({\n key: 'edit-tunnel',\n label: tr('agentsPage.editTunnel', 'Edit Tunnel'),\n icon: Link,\n onSelect: () => onEditTunnelUrl(agent),\n });\n }\n if (handleConfigure) {\n menuItems.push({\n key: 'settings',\n label: tr('agentsPage.settings', 'Settings'),\n icon: Settings,\n onSelect: () => handleConfigure(agent),\n });\n }\n\n return (\n <motion.div\n role=\"button\"\n tabIndex={0}\n whileHover={\n reduceMotion ? undefined : { y: -2, transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] } }\n }\n className={`bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-2)] transition-all cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${\n selected\n ? 'border-action-primary-bg ring-1 ring-action-primary-bg/30'\n : 'border-border-default'\n } ${className}`}\n onClick={() => onSelect?.(agent)}\n onKeyDown={(e) => {\n if (e.target !== e.currentTarget) return; // children (menu, checkbox, links) handle their own keys\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect?.(agent);\n }\n }}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-start gap-2 sm:gap-3 min-w-0 flex-1\">\n {/* Selection Checkbox */}\n {onToggleSelect && (\n <span\n className=\"flex flex-shrink-0 items-center justify-center max-md:min-h-11 max-md:min-w-11 max-md:-m-2 md:mt-0.5\"\n onClick={(e) => {\n e.stopPropagation();\n if (e.target === e.currentTarget) onToggleSelect();\n }}\n >\n <Checkbox\n checked={selected}\n onCheckedChange={() => onToggleSelect()}\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.selectAgent', 'Select {{name}}', { name: agent.name })}\n />\n </span>\n )}\n <div className=\"flex-shrink-0 size-8 sm:size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-4 sm:size-5 text-text-secondary\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 min-w-0\">\n <h3 className=\"font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base\">\n {agent.name}\n </h3>\n <PlatformIcon\n platform={agent.platform}\n className=\"size-4 flex-shrink-0 text-text-secondary\"\n />\n </div>\n <p className=\"text-xs sm:text-sm text-text-secondary truncate\">\n {agent.hostname || `ID: ${agent.id}`}\n </p>\n {(() => {\n // Always-visible (not just hover) guidance when degraded, so the\n // user knows whether it's self-healing or needs their action.\n const health = classifyAgentHealth(status, healthErrorMessage ?? null);\n if (health.phase === 'none') return null;\n return (\n <p\n className={`mt-1 flex items-start gap-1 text-xs ${\n health.phase === 'attention'\n ? 'text-status-warning-text'\n : 'text-content-tertiary'\n }`}\n >\n {health.inProgress && (\n <RefreshCw\n className=\"size-3 mt-0.5 flex-shrink-0 animate-spin\"\n aria-hidden=\"true\"\n />\n )}\n <span>\n <span className=\"font-medium\">{health.label}.</span> {health.actionHint}\n </span>\n </p>\n );\n })()}\n </div>\n </div>\n {/* Status and Actions - always visible */}\n <div className=\"flex items-center gap-1 sm:gap-2 flex-shrink-0\">\n <AgentStatusBadge\n status={status}\n reason={healthErrorMessage ?? null}\n linkTo={`/vibecontrols/agents/${agent.id}?tab=connection`}\n onClick={(event) => {\n // The card itself has a `onSelect` click target that\n // routes to the agent detail. Stop propagation so a\n // badge click doesn't *also* fire that handler — the\n // anchor's href already routes to the right tab and\n // React Router picks it up via the surrounding shell.\n event.stopPropagation();\n }}\n />\n {(handleConfigure || onStart || onStop) && menuItems.length > 0 && (\n <ActionMenu\n ariaLabel={tr('agentsPage.agentActionsMenu', 'Agent actions menu')}\n title={tr('agentsPage.agentActions', 'Agent actions')}\n align=\"end\"\n items={menuItems}\n />\n )}\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3 text-sm\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"size-4\" aria-hidden=\"true\" />\n <span>{formatLastHeartbeat(agent.lastHeartbeat)}</span>\n </div>\n {agent.architecture && (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Cpu className=\"size-4\" aria-hidden=\"true\" />\n <span>{agent.architecture}</span>\n </div>\n )}\n </div>\n\n {agent.tunnelUrl && (\n // Plain \"Tunnel: <link>\" row. The colored status chip that used\n // to sit here was a redundant second status surface — the\n // canonical state lives in the <AgentStatusBadge> top-right.\n // The Globe glyph stays as a neutral icon (no status color).\n <div className=\"mt-3 flex items-center gap-2 text-sm\">\n <Globe className=\"size-4 flex-shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"text-text-secondary flex-shrink-0\">Tunnel:</span>\n <a\n href={agent.tunnelUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.tunnelOpensInNewTab', '{{url}} (opens in new tab)', {\n url: agent.tunnelUrl,\n })}\n className=\"min-w-0 truncate text-text-link hover:text-text-linkHover hover:underline\"\n >\n {agent.tunnelUrl}\n </a>\n </div>\n )}\n\n {agent.capabilities && agent.capabilities.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1\">\n {agent.capabilities.slice(0, 4).map((cap) => (\n <span\n key={cap}\n className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\"\n >\n {cap}\n </span>\n ))}\n {agent.capabilities.length > 4 && (\n <span className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\">\n +{agent.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n </motion.div>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"AgentCard.js","names":[],"sources":["../../../src/components/agents/AgentCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { Server, Clock, Cpu, Play, Square, Settings, Globe, Link, RefreshCw } from 'lucide-react';\nimport { Checkbox } from '@burdenoff/fe-libs/ui';\nimport { AgentStatusBadge } from './AgentStatusBadge';\nimport type { Agent } from './types';\nimport { getAgentStatus } from './agentStatus';\nimport { classifyAgentHealth } from '../../utils/agentHealth';\nimport { ActionMenu, type ActionMenuItem } from '../shared/ActionMenu';\nimport { PlatformIcon } from '../shared/PlatformIcon';\nimport { useTr } from '../../shared/hooks/useTr';\n\nexport interface AgentCardProps {\n agent: Agent;\n onSelect?: (agent: Agent) => void;\n onConfigure?: (agent: Agent) => void;\n onSettings?: (agent: Agent) => void;\n onStart?: (agent: Agent) => void;\n onStop?: (agent: Agent) => void;\n onDisconnect?: (agent: Agent) => void;\n onEditTunnelUrl?: (agent: Agent) => void;\n selected?: boolean;\n onToggleSelect?: () => void;\n className?: string;\n /**\n * Last error from /health/ready or the live probe — shown in the badge\n * tooltip when the agent is degraded so users know WHY without curling\n * the tunnel themselves.\n */\n healthErrorMessage?: string | null;\n}\n\n/**\n * AgentCard - Display agent information in a card format\n *\n * @example\n * ```tsx\n * <AgentCard\n * agent={myAgent}\n * onSelect={handleSelect}\n * onConfigure={handleConfigure}\n * />\n * ```\n */\nexport const AgentCard: FC<AgentCardProps> = ({\n agent,\n onSelect,\n onConfigure,\n onSettings,\n onStart,\n onStop,\n onDisconnect,\n onEditTunnelUrl,\n selected = false,\n onToggleSelect,\n className = '',\n healthErrorMessage,\n}) => {\n const tr = useTr();\n // Support both onConfigure and onSettings\n const handleConfigure = onConfigure || onSettings;\n\n const status = getAgentStatus(agent);\n\n const formatLastHeartbeat = (timestamp: string | null | undefined) => {\n if (!timestamp) return tr('agentsPage.never', 'Never');\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return tr('agentsPage.justNow', 'Just now');\n if (diff < 3600000) return `${Math.floor(diff / 60000)}m ago`;\n if (diff < 86400000) return `${Math.floor(diff / 3600000)}h ago`;\n return `${Math.floor(diff / 86400000)}d ago`;\n };\n\n const menuItems: ActionMenuItem[] = [];\n if (status === 'ACTIVE' && onStop) {\n menuItems.push({\n key: 'stop',\n label: tr('agentsPage.stopAgent', 'Stop Agent'),\n icon: Square,\n onSelect: () => onStop(agent),\n });\n }\n if ((status === 'STOPPED' || status === 'OFFLINE') && onStart) {\n menuItems.push({\n key: 'start',\n label: tr('agentsPage.startAgent', 'Start Agent'),\n icon: Play,\n onSelect: () => onStart(agent),\n });\n }\n if (status === 'DEGRADED' && onStart) {\n // Reconnect on a degraded agent fires the same startVibecontrolsAgent\n // mutation as a fresh start — svc-side that POSTs lifecycle/start to the\n // agent's tunnel, which usually clears transient failures (tunnel hiccup,\n // finalize retry). Labelled \"Reconnect\" not \"Start\" so the user knows\n // it's a recovery action, not creating a new instance.\n menuItems.push({\n key: 'reconnect',\n label: tr('agentsPage.reconnectAgent', 'Reconnect'),\n icon: RefreshCw,\n onSelect: () => onStart(agent),\n });\n }\n if (\n (status === 'ACTIVE' ||\n status === 'STOPPED' ||\n status === 'OFFLINE' ||\n status === 'DEGRADED') &&\n onDisconnect\n ) {\n menuItems.push({\n key: 'disconnect',\n label:\n status === 'OFFLINE'\n ? tr('agentsPage.clearConnection', 'Clear Connection')\n : tr('agentsPage.disconnect', 'Disconnect'),\n icon: Square,\n onSelect: () => onDisconnect(agent),\n variant: 'danger',\n });\n }\n if (onEditTunnelUrl) {\n menuItems.push({\n key: 'edit-tunnel',\n label: tr('agentsPage.editTunnel', 'Edit Tunnel'),\n icon: Link,\n onSelect: () => onEditTunnelUrl(agent),\n });\n }\n if (handleConfigure) {\n menuItems.push({\n key: 'settings',\n label: tr('agentsPage.settings', 'Settings'),\n icon: Settings,\n onSelect: () => handleConfigure(agent),\n });\n }\n\n return (\n <div\n role=\"button\"\n tabIndex={0}\n className={`bg-bg-surface border rounded-card p-4 h-full hover:border-border-strong hover:shadow-[var(--shadow-elevation-1)] transition-[colors,box-shadow] duration-150 cursor-pointer focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus focus-visible:ring-offset-2 focus-visible:ring-offset-bg-canvas ${\n selected\n ? 'border-action-primary-bg ring-1 ring-action-primary-bg/30'\n : 'border-border-default'\n } ${className}`}\n onClick={() => onSelect?.(agent)}\n onKeyDown={(e) => {\n if (e.target !== e.currentTarget) return; // children (menu, checkbox, links) handle their own keys\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n onSelect?.(agent);\n }\n }}\n >\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"flex items-start gap-2 sm:gap-3 min-w-0 flex-1\">\n {/* Selection Checkbox */}\n {onToggleSelect && (\n <span\n className=\"flex flex-shrink-0 items-center justify-center max-md:min-h-11 max-md:min-w-11 max-md:-m-2 md:mt-0.5\"\n onClick={(e) => {\n e.stopPropagation();\n if (e.target === e.currentTarget) onToggleSelect();\n }}\n >\n <Checkbox\n checked={selected}\n onCheckedChange={() => onToggleSelect()}\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.selectAgent', 'Select {{name}}', { name: agent.name })}\n />\n </span>\n )}\n <div className=\"flex-shrink-0 size-8 sm:size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-4 sm:size-5 text-text-secondary\" aria-hidden=\"true\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 min-w-0\">\n <h3 className=\"font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base\">\n {agent.name}\n </h3>\n <PlatformIcon\n platform={agent.platform}\n className=\"size-4 flex-shrink-0 text-text-secondary\"\n />\n </div>\n <p className=\"text-xs sm:text-sm text-text-secondary truncate\">\n {agent.hostname || `ID: ${agent.id}`}\n </p>\n {(() => {\n // Always-visible (not just hover) guidance when degraded, so the\n // user knows whether it's self-healing or needs their action.\n const health = classifyAgentHealth(status, healthErrorMessage ?? null);\n if (health.phase === 'none') return null;\n return (\n <p\n className={`mt-1 flex items-start gap-1 text-xs ${\n health.phase === 'attention'\n ? 'text-status-warning-text'\n : 'text-content-tertiary'\n }`}\n >\n {health.inProgress && (\n <RefreshCw\n className=\"size-3 mt-0.5 flex-shrink-0 animate-spin\"\n aria-hidden=\"true\"\n />\n )}\n <span>\n <span className=\"font-medium\">{health.label}.</span> {health.actionHint}\n </span>\n </p>\n );\n })()}\n </div>\n </div>\n {/* Status and Actions - always visible */}\n <div className=\"flex items-center gap-1 sm:gap-2 flex-shrink-0\">\n <AgentStatusBadge\n status={status}\n reason={healthErrorMessage ?? null}\n linkTo={`/vibecontrols/agents/${agent.id}?tab=connection`}\n onClick={(event) => {\n // The card itself has a `onSelect` click target that\n // routes to the agent detail. Stop propagation so a\n // badge click doesn't *also* fire that handler — the\n // anchor's href already routes to the right tab and\n // React Router picks it up via the surrounding shell.\n event.stopPropagation();\n }}\n />\n {(handleConfigure || onStart || onStop) && menuItems.length > 0 && (\n <ActionMenu\n ariaLabel={tr('agentsPage.agentActionsMenu', 'Agent actions menu')}\n title={tr('agentsPage.agentActions', 'Agent actions')}\n align=\"end\"\n items={menuItems}\n />\n )}\n </div>\n </div>\n\n <div className=\"mt-4 grid grid-cols-2 gap-3 text-sm\">\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Clock className=\"size-4\" aria-hidden=\"true\" />\n <span>{formatLastHeartbeat(agent.lastHeartbeat)}</span>\n </div>\n {agent.architecture && (\n <div className=\"flex items-center gap-2 text-text-secondary\">\n <Cpu className=\"size-4\" aria-hidden=\"true\" />\n <span>{agent.architecture}</span>\n </div>\n )}\n </div>\n\n {agent.tunnelUrl && (\n // Plain \"Tunnel: <link>\" row. The colored status chip that used\n // to sit here was a redundant second status surface — the\n // canonical state lives in the <AgentStatusBadge> top-right.\n // The Globe glyph stays as a neutral icon (no status color).\n <div className=\"mt-3 flex items-center gap-2 text-sm\">\n <Globe className=\"size-4 flex-shrink-0 text-text-secondary\" aria-hidden=\"true\" />\n <span className=\"text-text-secondary flex-shrink-0\">Tunnel:</span>\n <a\n href={agent.tunnelUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={(e) => e.stopPropagation()}\n aria-label={tr('agentsPage.tunnelOpensInNewTab', '{{url}} (opens in new tab)', {\n url: agent.tunnelUrl,\n })}\n className=\"min-w-0 truncate text-text-link hover:text-text-linkHover hover:underline\"\n >\n {agent.tunnelUrl}\n </a>\n </div>\n )}\n\n {agent.capabilities && agent.capabilities.length > 0 && (\n <div className=\"mt-3 flex flex-wrap gap-1\">\n {agent.capabilities.slice(0, 4).map((cap) => (\n <span\n key={cap}\n className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\"\n >\n {cap}\n </span>\n ))}\n {agent.capabilities.length > 4 && (\n <span className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-pill\">\n +{agent.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":";;;;;;;;;;AA2CA,IAAa,KAAiC,EAC5C,UACA,aACA,gBACA,eACA,YACA,WACA,iBACA,oBACA,cAAW,IACX,mBACA,eAAY,IACZ,4BACI;CACJ,IAAM,IAAK,GAAO,EAEZ,IAAkB,KAAe,GAEjC,IAAS,EAAe,EAAM,EAE9B,KAAuB,MAAyC;AACpE,MAAI,CAAC,EAAW,QAAO,EAAG,oBAAoB,QAAQ;EACtD,IAAM,IAAO,KAAK,KAAK,GAAG,IAAI,KAAK,EAAU,CAAC,SAAS;AAIvD,SAHI,IAAO,MAAc,EAAG,sBAAsB,WAAW,GACzD,IAAO,OAAgB,GAAG,KAAK,MAAM,IAAO,IAAM,CAAC,SACnD,IAAO,QAAiB,GAAG,KAAK,MAAM,IAAO,KAAQ,CAAC,SACnD,GAAG,KAAK,MAAM,IAAO,MAAS,CAAC;IAGlC,IAA8B,EAAE;AAiEtC,QAhEI,MAAW,YAAY,KACzB,EAAU,KAAK;EACb,KAAK;EACL,OAAO,EAAG,wBAAwB,aAAa;EAC/C,MAAM;EACN,gBAAgB,EAAO,EAAM;EAC9B,CAAC,GAEC,MAAW,aAAa,MAAW,cAAc,KACpD,EAAU,KAAK;EACb,KAAK;EACL,OAAO,EAAG,yBAAyB,cAAc;EACjD,MAAM;EACN,gBAAgB,EAAQ,EAAM;EAC/B,CAAC,EAEA,MAAW,cAAc,KAM3B,EAAU,KAAK;EACb,KAAK;EACL,OAAO,EAAG,6BAA6B,YAAY;EACnD,MAAM;EACN,gBAAgB,EAAQ,EAAM;EAC/B,CAAC,GAGD,MAAW,YACV,MAAW,aACX,MAAW,aACX,MAAW,eACb,KAEA,EAAU,KAAK;EACb,KAAK;EACL,OACE,MAAW,YACP,EAAG,8BAA8B,mBAAmB,GACpD,EAAG,yBAAyB,aAAa;EAC/C,MAAM;EACN,gBAAgB,EAAa,EAAM;EACnC,SAAS;EACV,CAAC,EAEA,KACF,EAAU,KAAK;EACb,KAAK;EACL,OAAO,EAAG,yBAAyB,cAAc;EACjD,MAAM;EACN,gBAAgB,EAAgB,EAAM;EACvC,CAAC,EAEA,KACF,EAAU,KAAK;EACb,KAAK;EACL,OAAO,EAAG,uBAAuB,WAAW;EAC5C,MAAM;EACN,gBAAgB,EAAgB,EAAM;EACvC,CAAC,EAIF,kBAAC,OAAD;EACE,MAAK;EACL,UAAU;EACV,WAAW,+TACT,IACI,8DACA,wBACL,GAAG;EACJ,eAAe,IAAW,EAAM;EAChC,YAAY,MAAM;AACZ,KAAE,WAAW,EAAE,kBACf,EAAE,QAAQ,WAAW,EAAE,QAAQ,SACjC,EAAE,gBAAgB,EAClB,IAAW,EAAM;;YAbvB;GAiBE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf;MAEG,KACC,kBAAC,QAAD;OACE,WAAU;OACV,UAAU,MAAM;AAEd,QADA,EAAE,iBAAiB,EACf,EAAE,WAAW,EAAE,iBAAe,GAAgB;;iBAGpD,kBAAC,GAAD;QACE,SAAS;QACT,uBAAuB,GAAgB;QACvC,UAAU,MAAM,EAAE,iBAAiB;QACnC,cAAY,EAAG,0BAA0B,mBAAmB,EAAE,MAAM,EAAM,MAAM,CAAC;QACjF,CAAA;OACG,CAAA;MAET,kBAAC,OAAD;OAAK,WAAU;iBACb,kBAAC,GAAD;QAAQ,WAAU;QAAuC,eAAY;QAAS,CAAA;OAC1E,CAAA;MACN,kBAAC,OAAD;OAAK,WAAU;iBAAf;QACE,kBAAC,OAAD;SAAK,WAAU;mBAAf,CACE,kBAAC,MAAD;UAAI,WAAU;oBACX,EAAM;UACJ,CAAA,EACL,kBAAC,GAAD;UACE,UAAU,EAAM;UAChB,WAAU;UACV,CAAA,CACE;;QACN,kBAAC,KAAD;SAAG,WAAU;mBACV,EAAM,YAAY,OAAO,EAAM;SAC9B,CAAA;eACI;SAGN,IAAM,IAAS,EAAoB,GAAQ,KAAsB,KAAK;AAEtE,gBADI,EAAO,UAAU,SAAe,OAElC,kBAAC,KAAD;UACE,WAAW,uCACT,EAAO,UAAU,cACb,6BACA;oBAJR,CAOG,EAAO,cACN,kBAAC,GAAD;WACE,WAAU;WACV,eAAY;WACZ,CAAA,EAEJ,kBAAC,QAAD,EAAA,UAAA;WACE,kBAAC,QAAD;YAAM,WAAU;sBAAhB,CAA+B,EAAO,OAAM,IAAQ;;;WAAE,EAAO;WACxD,EAAA,CAAA,CACL;;YAEJ;QACA;;MACF;QAEN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MACU;MACR,QAAQ,KAAsB;MAC9B,QAAQ,wBAAwB,EAAM,GAAG;MACzC,UAAU,MAAU;AAMlB,SAAM,iBAAiB;;MAEzB,CAAA,GACA,KAAmB,KAAW,MAAW,EAAU,SAAS,KAC5D,kBAAC,GAAD;MACE,WAAW,EAAG,+BAA+B,qBAAqB;MAClE,OAAO,EAAG,2BAA2B,gBAAgB;MACrD,OAAM;MACN,OAAO;MACP,CAAA,CAEA;OACF;;GAEN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAO,WAAU;MAAS,eAAY;MAAS,CAAA,EAC/C,kBAAC,QAAD,EAAA,UAAO,EAAoB,EAAM,cAAc,EAAQ,CAAA,CACnD;QACL,EAAM,gBACL,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD;MAAK,WAAU;MAAS,eAAY;MAAS,CAAA,EAC7C,kBAAC,QAAD,EAAA,UAAO,EAAM,cAAoB,CAAA,CAC7B;OAEJ;;GAEL,EAAM,aAKL,kBAAC,OAAD;IAAK,WAAU;cAAf;KACE,kBAAC,GAAD;MAAO,WAAU;MAA2C,eAAY;MAAS,CAAA;KACjF,kBAAC,QAAD;MAAM,WAAU;gBAAoC;MAAc,CAAA;KAClE,kBAAC,KAAD;MACE,MAAM,EAAM;MACZ,QAAO;MACP,KAAI;MACJ,UAAU,MAAM,EAAE,iBAAiB;MACnC,cAAY,EAAG,kCAAkC,8BAA8B,EAC7E,KAAK,EAAM,WACZ,CAAC;MACF,WAAU;gBAET,EAAM;MACL,CAAA;KACA;;GAGP,EAAM,gBAAgB,EAAM,aAAa,SAAS,KACjD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAM,aAAa,MAAM,GAAG,EAAE,CAAC,KAAK,MACnC,kBAAC,QAAD;KAEE,WAAU;eAET;KACI,EAJA,EAIA,CACP,EACD,EAAM,aAAa,SAAS,KAC3B,kBAAC,QAAD;KAAM,WAAU;eAAhB,CAAoF,KAChF,EAAM,aAAa,SAAS,EACzB;OAEL;;GAEJ"}
@@ -5,7 +5,7 @@ import { useState as i } from "react";
5
5
  import { AlertTriangle as a, Eye as o, EyeOff as s, KeyRound as c, Link2 as l, Loader2 as u, ShieldCheck as d } from "lucide-react";
6
6
  import { jsx as f, jsxs as p } from "react/jsx-runtime";
7
7
  //#region src/components/agents/AgentConnectionRecovery.tsx
8
- var m = "rounded-lg border border-border-default bg-bg-surface p-4 sm:p-5", h = "inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", g = "w-full px-3 py-2 text-base md:text-sm rounded-md bg-bg-sunken border border-border-default text-text-primary focus:outline-none focus:ring-2 focus:ring-border-focus font-mono", _ = "mb-1 block text-xs font-medium text-text-muted";
8
+ var m = "rounded-card border border-border-default bg-bg-surface p-4", h = "inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus", g = "w-full px-3 py-2 text-base md:text-sm rounded-input bg-bg-sunken border border-border-default text-text-primary focus:outline-none focus:ring-2 focus:ring-border-focus font-mono", _ = "mb-1 block text-xs font-medium text-text-muted";
9
9
  function v(e) {
10
10
  return e instanceof Error ? e.message : String(e);
11
11
  }
@@ -1 +1 @@
1
- {"version":3,"file":"AgentConnectionRecovery.js","names":[],"sources":["../../../src/components/agents/AgentConnectionRecovery.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Eye, EyeOff, KeyRound, Link2, Loader2, ShieldCheck, AlertTriangle } from 'lucide-react';\n\nimport {\n useSetAgentApiKeyMutation,\n useUpdateAgentTunnelUrlMutation,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { useField } from '../../shared/hooks/useField';\n\ninterface AgentConnectionRecoveryProps {\n agentId: string;\n currentTunnelUrl?: string | null;\n /** Refetch the agent after a successful in-place update. */\n onUpdated?: () => void;\n}\n\nconst CARD = 'rounded-lg border border-border-default bg-bg-surface p-4 sm:p-5';\nconst BTN =\n 'inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus';\nconst INPUT =\n 'w-full px-3 py-2 text-base md:text-sm rounded-md bg-bg-sunken border border-border-default text-text-primary focus:outline-none focus:ring-2 focus:ring-border-focus font-mono';\nconst LABEL = 'mb-1 block text-xs font-medium text-text-muted';\n\nfunction errMsg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Emergency connection recovery for an existing agent — used when gateway\n * auto-report is broken (so the agent can't refresh its own key/tunnel) and\n * the operator must fix it in place WITHOUT delete + re-add (which loses\n * sessions / notes / bookmarks).\n * - \"Update API key\": paste the value from `vibe key`; the backend probes the\n * agent with it and only saves if the agent accepts it.\n * - \"Update tunnel URL\": paste the new quick-tunnel URL; the backend verifies\n * it's the same machine (hostname) before saving.\n */\nexport function AgentConnectionRecovery({\n agentId,\n currentTunnelUrl,\n onUpdated,\n}: AgentConnectionRecoveryProps) {\n const tr = useTr();\n\n // ── API key ─────────────────────────────────────────────────────────────\n const [keyInput, setKeyInput] = useState('');\n const [showKey, setShowKey] = useState(false);\n const [setKey, { loading: settingKey }] = useSetAgentApiKeyMutation();\n const [keyResult, setKeyResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const keyField = useField();\n\n const handleSetKey = async () => {\n setKeyResult(null);\n const key = keyInput.trim();\n if (!key) return;\n try {\n await setKey({ variables: { agentId, apiKey: key } });\n setKeyResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.keySaved',\n 'API key validated and saved. The agent is reachable again.'\n ),\n });\n setKeyInput('');\n onUpdated?.();\n } catch (e) {\n setKeyResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n // ── Tunnel URL ──────────────────────────────────────────────────────────\n const [urlInput, setUrlInput] = useState(currentTunnelUrl ?? '');\n const [updateUrl, { loading: updatingUrl }] = useUpdateAgentTunnelUrlMutation();\n const [urlResult, setUrlResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const urlField = useField();\n\n const handleUpdateUrl = async () => {\n setUrlResult(null);\n const url = urlInput.trim();\n if (!url) return;\n try {\n await updateUrl({ variables: { agentId, tunnelUrl: url } });\n setUrlResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.urlSaved',\n 'Tunnel URL verified and updated. The agent is back online.'\n ),\n });\n onUpdated?.();\n } catch (e) {\n setUrlResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n const result = (r: { ok: boolean; msg: string } | null) =>\n r ? (\n <p\n role=\"status\"\n className={`mt-2 flex items-start gap-2 text-xs ${\n r.ok ? 'text-status-success-text' : 'text-status-error-text'\n }`}\n >\n {r.ok ? (\n <ShieldCheck className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n ) : (\n <AlertTriangle className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n )}\n <span>{r.msg}</span>\n </p>\n ) : null;\n\n return (\n <div className=\"space-y-4\">\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <KeyRound className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.keyTitle', 'Update API key (no re-add)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.keyHint',\n 'If the agent’s API key changed and it can’t auto-report (gateway auth down), paste the value from `vibe key` on the host. It’s validated against the agent before saving — your sessions and data stay intact.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={keyField.id} className={LABEL}>\n {tr('agentDetails.recovery.keyLabel', 'Agent API key')}\n </label>\n <div className=\"relative\">\n <input\n {...keyField.inputProps}\n type={showKey ? 'text' : 'password'}\n autoComplete=\"off\"\n value={keyInput}\n onChange={(e) => setKeyInput(e.target.value)}\n placeholder=\"vcak_…\"\n className={`${INPUT} pr-10`}\n />\n <button\n type=\"button\"\n onClick={() => setShowKey((v) => !v)}\n aria-label={\n showKey\n ? tr('agentDetails.recovery.hideKey', 'Hide API key')\n : tr('agentDetails.recovery.showKey', 'Show API key')\n }\n className=\"absolute right-1 top-1/2 -translate-y-1/2 inline-flex size-8 items-center justify-center rounded-button text-text-muted transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n {showKey ? (\n <EyeOff className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <Eye className=\"size-4\" aria-hidden=\"true\" />\n )}\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={handleSetKey}\n disabled={settingKey || keyInput.trim().length === 0}\n aria-busy={settingKey}\n className={`${BTN} shrink-0`}\n >\n {settingKey ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <ShieldCheck className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.validateSave', 'Validate & save')}\n </button>\n </div>\n {result(keyResult)}\n </section>\n\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <Link2 className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.urlTitle', 'Update tunnel URL (re-pair)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.urlHint',\n 'After a quick-tunnel rotation, paste the new URL here. The backend verifies it points at the same machine before saving — no re-registration, no data loss.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={urlField.id} className={LABEL}>\n {tr('agentDetails.recovery.urlLabel', 'Tunnel URL')}\n </label>\n <input\n {...urlField.inputProps}\n type=\"url\"\n value={urlInput}\n onChange={(e) => setUrlInput(e.target.value)}\n placeholder=\"https://….trycloudflare.com\"\n className={INPUT}\n />\n </div>\n <button\n type=\"button\"\n onClick={handleUpdateUrl}\n disabled={updatingUrl || urlInput.trim().length === 0}\n aria-busy={updatingUrl}\n className={`${BTN} shrink-0`}\n >\n {updatingUrl ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Link2 className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.verifyUpdate', 'Verify & update')}\n </button>\n </div>\n {result(urlResult)}\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AgentConnectionRecovery.js","names":[],"sources":["../../../src/components/agents/AgentConnectionRecovery.tsx"],"sourcesContent":["import { useState } from 'react';\nimport { Eye, EyeOff, KeyRound, Link2, Loader2, ShieldCheck, AlertTriangle } from 'lucide-react';\n\nimport {\n useSetAgentApiKeyMutation,\n useUpdateAgentTunnelUrlMutation,\n} from '@/generated/wspace-operations';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { useField } from '../../shared/hooks/useField';\n\ninterface AgentConnectionRecoveryProps {\n agentId: string;\n currentTunnelUrl?: string | null;\n /** Refetch the agent after a successful in-place update. */\n onUpdated?: () => void;\n}\n\nconst CARD = 'rounded-card border border-border-default bg-bg-surface p-4';\nconst BTN =\n 'inline-flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-button bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bgHover transition-colors disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus';\nconst INPUT =\n 'w-full px-3 py-2 text-base md:text-sm rounded-input bg-bg-sunken border border-border-default text-text-primary focus:outline-none focus:ring-2 focus:ring-border-focus font-mono';\nconst LABEL = 'mb-1 block text-xs font-medium text-text-muted';\n\nfunction errMsg(e: unknown): string {\n return e instanceof Error ? e.message : String(e);\n}\n\n/**\n * Emergency connection recovery for an existing agent — used when gateway\n * auto-report is broken (so the agent can't refresh its own key/tunnel) and\n * the operator must fix it in place WITHOUT delete + re-add (which loses\n * sessions / notes / bookmarks).\n * - \"Update API key\": paste the value from `vibe key`; the backend probes the\n * agent with it and only saves if the agent accepts it.\n * - \"Update tunnel URL\": paste the new quick-tunnel URL; the backend verifies\n * it's the same machine (hostname) before saving.\n */\nexport function AgentConnectionRecovery({\n agentId,\n currentTunnelUrl,\n onUpdated,\n}: AgentConnectionRecoveryProps) {\n const tr = useTr();\n\n // ── API key ─────────────────────────────────────────────────────────────\n const [keyInput, setKeyInput] = useState('');\n const [showKey, setShowKey] = useState(false);\n const [setKey, { loading: settingKey }] = useSetAgentApiKeyMutation();\n const [keyResult, setKeyResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const keyField = useField();\n\n const handleSetKey = async () => {\n setKeyResult(null);\n const key = keyInput.trim();\n if (!key) return;\n try {\n await setKey({ variables: { agentId, apiKey: key } });\n setKeyResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.keySaved',\n 'API key validated and saved. The agent is reachable again.'\n ),\n });\n setKeyInput('');\n onUpdated?.();\n } catch (e) {\n setKeyResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n // ── Tunnel URL ──────────────────────────────────────────────────────────\n const [urlInput, setUrlInput] = useState(currentTunnelUrl ?? '');\n const [updateUrl, { loading: updatingUrl }] = useUpdateAgentTunnelUrlMutation();\n const [urlResult, setUrlResult] = useState<{ ok: boolean; msg: string } | null>(null);\n const urlField = useField();\n\n const handleUpdateUrl = async () => {\n setUrlResult(null);\n const url = urlInput.trim();\n if (!url) return;\n try {\n await updateUrl({ variables: { agentId, tunnelUrl: url } });\n setUrlResult({\n ok: true,\n msg: tr(\n 'agentDetails.recovery.urlSaved',\n 'Tunnel URL verified and updated. The agent is back online.'\n ),\n });\n onUpdated?.();\n } catch (e) {\n setUrlResult({ ok: false, msg: errMsg(e) });\n }\n };\n\n const result = (r: { ok: boolean; msg: string } | null) =>\n r ? (\n <p\n role=\"status\"\n className={`mt-2 flex items-start gap-2 text-xs ${\n r.ok ? 'text-status-success-text' : 'text-status-error-text'\n }`}\n >\n {r.ok ? (\n <ShieldCheck className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n ) : (\n <AlertTriangle className=\"size-4 flex-shrink-0 mt-0.5\" aria-hidden=\"true\" />\n )}\n <span>{r.msg}</span>\n </p>\n ) : null;\n\n return (\n <div className=\"space-y-4\">\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <KeyRound className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.keyTitle', 'Update API key (no re-add)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.keyHint',\n 'If the agent’s API key changed and it can’t auto-report (gateway auth down), paste the value from `vibe key` on the host. It’s validated against the agent before saving — your sessions and data stay intact.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={keyField.id} className={LABEL}>\n {tr('agentDetails.recovery.keyLabel', 'Agent API key')}\n </label>\n <div className=\"relative\">\n <input\n {...keyField.inputProps}\n type={showKey ? 'text' : 'password'}\n autoComplete=\"off\"\n value={keyInput}\n onChange={(e) => setKeyInput(e.target.value)}\n placeholder=\"vcak_…\"\n className={`${INPUT} pr-10`}\n />\n <button\n type=\"button\"\n onClick={() => setShowKey((v) => !v)}\n aria-label={\n showKey\n ? tr('agentDetails.recovery.hideKey', 'Hide API key')\n : tr('agentDetails.recovery.showKey', 'Show API key')\n }\n className=\"absolute right-1 top-1/2 -translate-y-1/2 inline-flex size-8 items-center justify-center rounded-button text-text-muted transition-colors hover:text-text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-border-focus\"\n >\n {showKey ? (\n <EyeOff className=\"size-4\" aria-hidden=\"true\" />\n ) : (\n <Eye className=\"size-4\" aria-hidden=\"true\" />\n )}\n </button>\n </div>\n </div>\n <button\n type=\"button\"\n onClick={handleSetKey}\n disabled={settingKey || keyInput.trim().length === 0}\n aria-busy={settingKey}\n className={`${BTN} shrink-0`}\n >\n {settingKey ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <ShieldCheck className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.validateSave', 'Validate & save')}\n </button>\n </div>\n {result(keyResult)}\n </section>\n\n <section className={CARD}>\n <div className=\"flex items-center gap-2 mb-1\">\n <Link2 className=\"size-5 text-text-secondary\" aria-hidden=\"true\" />\n <h3 className=\"text-base font-semibold text-text-primary\">\n {tr('agentDetails.recovery.urlTitle', 'Update tunnel URL (re-pair)')}\n </h3>\n </div>\n <p className=\"text-xs text-text-muted mb-3\">\n {tr(\n 'agentDetails.recovery.urlHint',\n 'After a quick-tunnel rotation, paste the new URL here. The backend verifies it points at the same machine before saving — no re-registration, no data loss.'\n )}\n </p>\n <div className=\"flex flex-col sm:flex-row sm:items-end gap-2\">\n <div className=\"flex-1 min-w-0\">\n <label htmlFor={urlField.id} className={LABEL}>\n {tr('agentDetails.recovery.urlLabel', 'Tunnel URL')}\n </label>\n <input\n {...urlField.inputProps}\n type=\"url\"\n value={urlInput}\n onChange={(e) => setUrlInput(e.target.value)}\n placeholder=\"https://….trycloudflare.com\"\n className={INPUT}\n />\n </div>\n <button\n type=\"button\"\n onClick={handleUpdateUrl}\n disabled={updatingUrl || urlInput.trim().length === 0}\n aria-busy={updatingUrl}\n className={`${BTN} shrink-0`}\n >\n {updatingUrl ? (\n <Loader2 className=\"size-4 animate-spin\" aria-hidden=\"true\" />\n ) : (\n <Link2 className=\"size-4\" aria-hidden=\"true\" />\n )}\n {tr('agentDetails.recovery.verifyUpdate', 'Verify & update')}\n </button>\n </div>\n {result(urlResult)}\n </section>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -181,7 +181,7 @@ function Oe({ onSubmit: Oe, onCancel: h, isLoading: g, onNavigate: ke, targets:
181
181
  children: [/* @__PURE__ */ d("div", {
182
182
  className: "flex items-center gap-2",
183
183
  children: [/* @__PURE__ */ u(ue, {
184
- className: "size-4 text-text-link",
184
+ className: "size-4 text-text-secondary",
185
185
  "aria-hidden": "true"
186
186
  }), /* @__PURE__ */ u("span", {
187
187
  className: "text-sm font-medium text-text-primary",
@@ -508,7 +508,7 @@ function Oe({ onSubmit: Oe, onCancel: h, isLoading: g, onNavigate: ke, targets:
508
508
  className: "space-y-3",
509
509
  children: [/* @__PURE__ */ d("div", {
510
510
  role: "status",
511
- className: `flex items-center gap-3 p-3 rounded-lg border ${R === null ? "border-border-default bg-bg-sunken" : R.ok ? "border-status-success-border bg-status-success-bgSubtle" : "border-status-error-border bg-status-error-bg-subtle"}`,
511
+ className: `flex items-center gap-3 p-3 rounded-lg border ${R === null ? "border-border-default bg-bg-sunken" : R.ok ? "border-status-success-border bg-status-success-bg-subtle" : "border-status-error-border bg-status-error-bg-subtle"}`,
512
512
  children: [L && !R ? /* @__PURE__ */ u(s, {
513
513
  className: "size-5 text-text-secondary animate-spin",
514
514
  "aria-hidden": "true"
@@ -548,7 +548,7 @@ function Oe({ onSubmit: Oe, onCancel: h, isLoading: g, onNavigate: ke, targets:
548
548
  })]
549
549
  }), /* @__PURE__ */ d("div", {
550
550
  role: "status",
551
- className: `flex items-center gap-3 p-3 rounded-lg border ${B === null ? "border-border-default bg-bg-sunken" : B.ok ? "border-status-success-border bg-status-success-bgSubtle" : "border-status-error-border bg-status-error-bg-subtle"}`,
551
+ className: `flex items-center gap-3 p-3 rounded-lg border ${B === null ? "border-border-default bg-bg-sunken" : B.ok ? "border-status-success-border bg-status-success-bg-subtle" : "border-status-error-border bg-status-error-bg-subtle"}`,
552
552
  children: [L && !B ? /* @__PURE__ */ u(s, {
553
553
  className: "size-5 text-text-secondary animate-spin",
554
554
  "aria-hidden": "true"