@burdenoff/microfe-vibecontrols 2026.815.1 → 2026.824.1

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.
@@ -4,125 +4,144 @@ import { PlatformIcon as n } from "../shared/PlatformIcon.js";
4
4
  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
- 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 { jsx as h, jsxs as g } from "react/jsx-runtime";
9
- import { Checkbox as _ } from "@burdenoff/fe-libs/ui";
7
+ import { isWorkspaceAgent as o } from "../../utils/agentVisibility.js";
8
+ import { Clock as s, Cpu as c, Globe as l, Link as u, Lock as d, Play as f, RefreshCw as p, Server as m, Settings as h, Square as g, Users as _ } from "lucide-react";
9
+ import { jsx as v, jsxs as y } from "react/jsx-runtime";
10
+ import { Checkbox as b } from "@burdenoff/fe-libs/ui";
10
11
  //#region src/components/agents/AgentCard.tsx
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");
12
+ var x = ({ agent: x, onSelect: S, onConfigure: C, onSettings: w, onStart: T, onStop: E, onDisconnect: D, onEditTunnelUrl: O, selected: k = !1, onToggleSelect: A, className: j = "", healthErrorMessage: M }) => {
13
+ let N = e(), P = C || w, F = r(x), I = (e) => {
14
+ if (!e) return N("agentsPage.never", "Never");
14
15
  let t = Date.now() - new Date(e).getTime();
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({
16
+ return t < 6e4 ? N("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" && E && L.push({
18
19
  key: "stop",
19
- label: A("agentsPage.stopAgent", "Stop Agent"),
20
- icon: m,
21
- onSelect: () => C(v)
22
- }), (M === "STOPPED" || M === "OFFLINE") && S && P.push({
20
+ label: N("agentsPage.stopAgent", "Stop Agent"),
21
+ icon: g,
22
+ onSelect: () => E(x)
23
+ }), (F === "STOPPED" || F === "OFFLINE") && T && L.push({
23
24
  key: "start",
24
- label: A("agentsPage.startAgent", "Start Agent"),
25
- icon: u,
26
- onSelect: () => S(v)
27
- }), M === "DEGRADED" && S && P.push({
25
+ label: N("agentsPage.startAgent", "Start Agent"),
26
+ icon: f,
27
+ onSelect: () => T(x)
28
+ }), F === "DEGRADED" && T && L.push({
28
29
  key: "reconnect",
29
- label: A("agentsPage.reconnectAgent", "Reconnect"),
30
- icon: d,
31
- onSelect: () => S(v)
32
- }), (M === "ACTIVE" || M === "STOPPED" || M === "OFFLINE" || M === "DEGRADED") && w && P.push({
30
+ label: N("agentsPage.reconnectAgent", "Reconnect"),
31
+ icon: p,
32
+ onSelect: () => T(x)
33
+ }), (F === "ACTIVE" || F === "STOPPED" || F === "OFFLINE" || F === "DEGRADED") && D && L.push({
33
34
  key: "disconnect",
34
- label: M === "OFFLINE" ? A("agentsPage.clearConnection", "Clear Connection") : A("agentsPage.disconnect", "Disconnect"),
35
- icon: m,
36
- onSelect: () => w(v),
35
+ label: F === "OFFLINE" ? N("agentsPage.clearConnection", "Clear Connection") : N("agentsPage.disconnect", "Disconnect"),
36
+ icon: g,
37
+ onSelect: () => D(x),
37
38
  variant: "danger"
38
- }), T && P.push({
39
+ }), O && L.push({
39
40
  key: "edit-tunnel",
40
- label: A("agentsPage.editTunnel", "Edit Tunnel"),
41
- icon: l,
42
- onSelect: () => T(v)
43
- }), j && P.push({
41
+ label: N("agentsPage.editTunnel", "Edit Tunnel"),
42
+ icon: u,
43
+ onSelect: () => O(x)
44
+ }), P && L.push({
44
45
  key: "settings",
45
- label: A("agentsPage.settings", "Settings"),
46
- icon: p,
47
- onSelect: () => j(v)
48
- }), /* @__PURE__ */ g("div", {
46
+ label: N("agentsPage.settings", "Settings"),
47
+ icon: h,
48
+ onSelect: () => P(x)
49
+ }), /* @__PURE__ */ y("div", {
49
50
  role: "button",
50
51
  tabIndex: 0,
51
- className: `bg-bg-surface border rounded-card shadow-elevation-1 p-5 h-full hover:border-border-strong hover:shadow-elevation-2 transition-all duration-200 ease-[var(--motion-easing-out)] 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-transparent ring-1 ring-[var(--color-focus-ring)] bg-[var(--color-accent-soft)]" : "border-border-subtle"} ${O}`,
52
- onClick: () => y?.(v),
52
+ className: `bg-bg-surface border rounded-card shadow-elevation-1 p-5 h-full hover:border-border-strong hover:shadow-elevation-2 transition-all duration-200 ease-[var(--motion-easing-out)] 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 ${k ? "border-transparent ring-1 ring-[var(--color-focus-ring)] bg-[var(--color-accent-soft)]" : "border-border-subtle"} ${j}`,
53
+ onClick: () => S?.(x),
53
54
  onKeyDown: (e) => {
54
- e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), y?.(v));
55
+ e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), S?.(x));
55
56
  },
56
57
  children: [
57
- /* @__PURE__ */ g("div", {
58
+ /* @__PURE__ */ y("div", {
58
59
  className: "flex items-start justify-between gap-2",
59
- children: [/* @__PURE__ */ g("div", {
60
+ children: [/* @__PURE__ */ y("div", {
60
61
  className: "flex items-start gap-2 sm:gap-3 min-w-0 flex-1",
61
62
  children: [
62
- D && /* @__PURE__ */ h("span", {
63
+ A && /* @__PURE__ */ v("span", {
63
64
  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",
64
65
  onClick: (e) => {
65
- e.stopPropagation(), e.target === e.currentTarget && D();
66
+ e.stopPropagation(), e.target === e.currentTarget && A();
66
67
  },
67
- children: /* @__PURE__ */ h(_, {
68
- checked: E,
69
- onCheckedChange: () => D(),
68
+ children: /* @__PURE__ */ v(b, {
69
+ checked: k,
70
+ onCheckedChange: () => A(),
70
71
  onClick: (e) => e.stopPropagation(),
71
- "aria-label": A("agentsPage.selectAgent", "Select {{name}}", { name: v.name })
72
+ "aria-label": N("agentsPage.selectAgent", "Select {{name}}", { name: x.name })
72
73
  })
73
74
  }),
74
- /* @__PURE__ */ h("div", {
75
+ /* @__PURE__ */ v("div", {
75
76
  className: "flex-shrink-0 size-9 sm:size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center",
76
- children: /* @__PURE__ */ h(f, {
77
+ children: /* @__PURE__ */ v(m, {
77
78
  className: "size-4 sm:size-5 text-action-primary-bg",
78
79
  "aria-hidden": "true"
79
80
  })
80
81
  }),
81
- /* @__PURE__ */ g("div", {
82
+ /* @__PURE__ */ y("div", {
82
83
  className: "min-w-0 flex-1",
83
- children: [/* @__PURE__ */ g("div", {
84
+ children: [/* @__PURE__ */ y("div", {
84
85
  className: "flex items-center gap-1.5 sm:gap-2 min-w-0",
85
- children: [/* @__PURE__ */ h("h3", {
86
- className: "font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base tracking-tight",
87
- children: v.name
88
- }), /* @__PURE__ */ h(n, {
89
- platform: v.platform,
90
- className: "size-4 flex-shrink-0 text-text-secondary"
91
- })]
92
- }), /* @__PURE__ */ h("p", {
86
+ children: [
87
+ /* @__PURE__ */ v("h3", {
88
+ className: "font-semibold text-text-primary truncate min-w-0 text-sm sm:text-base tracking-tight",
89
+ children: x.name
90
+ }),
91
+ /* @__PURE__ */ v(n, {
92
+ platform: x.platform,
93
+ className: "size-4 flex-shrink-0 text-text-secondary"
94
+ }),
95
+ o(x) ? /* @__PURE__ */ y("span", {
96
+ className: "inline-flex items-center gap-1 flex-shrink-0 text-[10px] uppercase font-semibold px-1.5 py-0.5 rounded bg-bg-sunken text-text-secondary",
97
+ title: N("agentsPage.visibility.workspaceHint", "Shared with the workspace — any member with agent access can use it"),
98
+ children: [/* @__PURE__ */ v(_, {
99
+ className: "size-3",
100
+ "aria-hidden": "true"
101
+ }), N("agentsPage.visibility.workspace", "Shared")]
102
+ }) : /* @__PURE__ */ y("span", {
103
+ className: "inline-flex items-center gap-1 flex-shrink-0 text-[10px] uppercase font-semibold px-1.5 py-0.5 rounded bg-[var(--color-accent-soft)] text-action-primary-bg",
104
+ title: N("agentsPage.visibility.privateHint", "Private to its owner — nobody else in the workspace can see or use it"),
105
+ children: [/* @__PURE__ */ v(d, {
106
+ className: "size-3",
107
+ "aria-hidden": "true"
108
+ }), N("agentsPage.visibility.private", "Private")]
109
+ })
110
+ ]
111
+ }), /* @__PURE__ */ v("p", {
93
112
  className: "text-xs sm:text-sm text-text-secondary truncate",
94
- children: v.hostname || `ID: ${v.id}`
113
+ children: x.hostname || `ID: ${x.id}`
95
114
  })]
96
115
  })
97
116
  ]
98
- }), /* @__PURE__ */ g("div", {
117
+ }), /* @__PURE__ */ y("div", {
99
118
  className: "flex items-center gap-1 sm:gap-2 flex-shrink-0",
100
- children: [/* @__PURE__ */ h(a, {
101
- status: M,
102
- reason: k ?? null,
103
- linkTo: `/vibecontrols/agents/${v.id}?tab=connection`,
119
+ children: [/* @__PURE__ */ v(a, {
120
+ status: F,
121
+ reason: M ?? null,
122
+ linkTo: `/vibecontrols/agents/${x.id}?tab=connection`,
104
123
  onClick: (e) => {
105
124
  e.stopPropagation();
106
125
  }
107
- }), (j || S || C) && P.length > 0 && /* @__PURE__ */ h(t, {
108
- ariaLabel: A("agentsPage.agentActionsMenu", "Agent actions menu"),
109
- title: A("agentsPage.agentActions", "Agent actions"),
126
+ }), (P || T || E) && L.length > 0 && /* @__PURE__ */ v(t, {
127
+ ariaLabel: N("agentsPage.agentActionsMenu", "Agent actions menu"),
128
+ title: N("agentsPage.agentActions", "Agent actions"),
110
129
  align: "end",
111
- items: P
130
+ items: L
112
131
  })]
113
132
  })]
114
133
  }),
115
134
  (() => {
116
- let e = i(M, k ?? null);
117
- return e.phase === "none" ? null : /* @__PURE__ */ g("div", {
135
+ let e = i(F, M ?? null);
136
+ return e.phase === "none" ? null : /* @__PURE__ */ y("div", {
118
137
  className: `mt-3 flex items-start gap-1.5 text-xs ${e.phase === "attention" ? "text-status-warning-text" : "text-content-tertiary"}`,
119
- children: [e.inProgress && /* @__PURE__ */ h(d, {
138
+ children: [e.inProgress && /* @__PURE__ */ v(p, {
120
139
  className: "size-3.5 mt-0.5 flex-shrink-0 animate-spin",
121
140
  "aria-hidden": "true"
122
- }), /* @__PURE__ */ g("p", {
141
+ }), /* @__PURE__ */ y("p", {
123
142
  className: "min-w-0 flex-1 break-words leading-relaxed",
124
143
  children: [
125
- /* @__PURE__ */ g("span", {
144
+ /* @__PURE__ */ y("span", {
126
145
  className: "font-medium",
127
146
  children: [e.label, "."]
128
147
  }),
@@ -132,61 +151,61 @@ var v = ({ agent: v, onSelect: y, onConfigure: b, onSettings: x, onStart: S, onS
132
151
  })]
133
152
  });
134
153
  })(),
135
- /* @__PURE__ */ g("div", {
154
+ /* @__PURE__ */ y("div", {
136
155
  className: "mt-5 grid grid-cols-2 gap-3 text-sm",
137
- children: [/* @__PURE__ */ g("div", {
156
+ children: [/* @__PURE__ */ y("div", {
138
157
  className: "flex items-center gap-2 text-text-secondary",
139
- children: [/* @__PURE__ */ h(o, {
158
+ children: [/* @__PURE__ */ v(s, {
140
159
  className: "size-4",
141
160
  "aria-hidden": "true"
142
- }), /* @__PURE__ */ h("span", {
161
+ }), /* @__PURE__ */ v("span", {
143
162
  className: "tabular-nums",
144
- children: N(v.lastHeartbeat)
163
+ children: I(x.lastHeartbeat)
145
164
  })]
146
- }), v.architecture && /* @__PURE__ */ g("div", {
165
+ }), x.architecture && /* @__PURE__ */ y("div", {
147
166
  className: "flex items-center gap-2 text-text-secondary",
148
- children: [/* @__PURE__ */ h(s, {
167
+ children: [/* @__PURE__ */ v(c, {
149
168
  className: "size-4",
150
169
  "aria-hidden": "true"
151
- }), /* @__PURE__ */ h("span", { children: v.architecture })]
170
+ }), /* @__PURE__ */ v("span", { children: x.architecture })]
152
171
  })]
153
172
  }),
154
- v.tunnelUrl && /* @__PURE__ */ g("div", {
173
+ x.tunnelUrl && /* @__PURE__ */ y("div", {
155
174
  className: "mt-4 flex items-center gap-2 text-sm",
156
175
  children: [
157
- /* @__PURE__ */ h(c, {
176
+ /* @__PURE__ */ v(l, {
158
177
  className: "size-4 flex-shrink-0 text-text-secondary",
159
178
  "aria-hidden": "true"
160
179
  }),
161
- /* @__PURE__ */ h("span", {
180
+ /* @__PURE__ */ v("span", {
162
181
  className: "text-text-secondary flex-shrink-0",
163
182
  children: "Tunnel:"
164
183
  }),
165
- /* @__PURE__ */ h("a", {
166
- href: v.tunnelUrl,
184
+ /* @__PURE__ */ v("a", {
185
+ href: x.tunnelUrl,
167
186
  target: "_blank",
168
187
  rel: "noopener noreferrer",
169
188
  onClick: (e) => e.stopPropagation(),
170
- "aria-label": A("agentsPage.tunnelOpensInNewTab", "{{url}} (opens in new tab)", { url: v.tunnelUrl }),
189
+ "aria-label": N("agentsPage.tunnelOpensInNewTab", "{{url}} (opens in new tab)", { url: x.tunnelUrl }),
171
190
  className: "min-w-0 truncate font-mono text-xs text-text-link hover:text-text-linkHover hover:underline",
172
- children: v.tunnelUrl
191
+ children: x.tunnelUrl
173
192
  })
174
193
  ]
175
194
  }),
176
- v.capabilities && v.capabilities.length > 0 && /* @__PURE__ */ g("div", {
195
+ x.capabilities && x.capabilities.length > 0 && /* @__PURE__ */ y("div", {
177
196
  className: "mt-4 flex flex-wrap gap-1.5",
178
- children: [v.capabilities.slice(0, 4).map((e) => /* @__PURE__ */ h("span", {
197
+ children: [x.capabilities.slice(0, 4).map((e) => /* @__PURE__ */ v("span", {
179
198
  className: "px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-full",
180
199
  children: e
181
- }, e)), v.capabilities.length > 4 && /* @__PURE__ */ g("span", {
200
+ }, e)), x.capabilities.length > 4 && /* @__PURE__ */ y("span", {
182
201
  className: "px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded-full",
183
- children: ["+", v.capabilities.length - 4]
202
+ children: ["+", x.capabilities.length - 4]
184
203
  })]
185
204
  })
186
205
  ]
187
206
  });
188
207
  };
189
208
  //#endregion
190
- export { v as AgentCard };
209
+ export { x as AgentCard };
191
210
 
192
211
  //# 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 { 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 shadow-elevation-1 p-5 h-full hover:border-border-strong hover:shadow-elevation-2 transition-all duration-200 ease-[var(--motion-easing-out)] 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-transparent ring-1 ring-[var(--color-focus-ring)] bg-[var(--color-accent-soft)]'\n : 'border-border-subtle'\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-9 sm:size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <Server className=\"size-4 sm:size-5 text-action-primary-bg\" 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 tracking-tight\">\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 </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 {(() => {\n // Full-width status guidance row (below the header) so the degraded /\n // recovering copy gets the whole card width to wrap on narrow screens,\n // instead of being crushed into the narrow name column next to the\n // status badge + actions menu.\n const health = classifyAgentHealth(status, healthErrorMessage ?? null);\n if (health.phase === 'none') return null;\n return (\n <div\n className={`mt-3 flex items-start gap-1.5 text-xs ${\n health.phase === 'attention' ? 'text-status-warning-text' : 'text-content-tertiary'\n }`}\n >\n {health.inProgress && (\n <RefreshCw\n className=\"size-3.5 mt-0.5 flex-shrink-0 animate-spin\"\n aria-hidden=\"true\"\n />\n )}\n <p className=\"min-w-0 flex-1 break-words leading-relaxed\">\n <span className=\"font-medium\">{health.label}.</span> {health.actionHint}\n </p>\n </div>\n );\n })()}\n\n <div className=\"mt-5 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 className=\"tabular-nums\">{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-4 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 font-mono text-xs 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-4 flex flex-wrap gap-1.5\">\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-full\"\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-full\">\n +{agent.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n </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 {\n Server,\n Clock,\n Cpu,\n Play,\n Square,\n Settings,\n Globe,\n Link,\n RefreshCw,\n Lock,\n Users,\n} 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 { isWorkspaceAgent } from '../../utils/agentVisibility';\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 shadow-elevation-1 p-5 h-full hover:border-border-strong hover:shadow-elevation-2 transition-all duration-200 ease-[var(--motion-easing-out)] 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-transparent ring-1 ring-[var(--color-focus-ring)] bg-[var(--color-accent-soft)]'\n : 'border-border-subtle'\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-9 sm:size-10 rounded-lg bg-[var(--color-accent-soft)] flex items-center justify-center\">\n <Server className=\"size-4 sm:size-5 text-action-primary-bg\" 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 tracking-tight\">\n {agent.name}\n </h3>\n <PlatformIcon\n platform={agent.platform}\n className=\"size-4 flex-shrink-0 text-text-secondary\"\n />\n {/* BOFF-6394: an agent is private to whoever added it unless they\n share it, so the card has to say which — otherwise \"who else\n can run commands on this machine\" is invisible. */}\n {isWorkspaceAgent(agent) ? (\n <span\n className=\"inline-flex items-center gap-1 flex-shrink-0 text-[10px] uppercase font-semibold px-1.5 py-0.5 rounded bg-bg-sunken text-text-secondary\"\n title={tr(\n 'agentsPage.visibility.workspaceHint',\n 'Shared with the workspace — any member with agent access can use it'\n )}\n >\n <Users className=\"size-3\" aria-hidden=\"true\" />\n {tr('agentsPage.visibility.workspace', 'Shared')}\n </span>\n ) : (\n <span\n className=\"inline-flex items-center gap-1 flex-shrink-0 text-[10px] uppercase font-semibold px-1.5 py-0.5 rounded bg-[var(--color-accent-soft)] text-action-primary-bg\"\n title={tr(\n 'agentsPage.visibility.privateHint',\n 'Private to its owner — nobody else in the workspace can see or use it'\n )}\n >\n <Lock className=\"size-3\" aria-hidden=\"true\" />\n {tr('agentsPage.visibility.private', 'Private')}\n </span>\n )}\n </div>\n <p className=\"text-xs sm:text-sm text-text-secondary truncate\">\n {agent.hostname || `ID: ${agent.id}`}\n </p>\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 {(() => {\n // Full-width status guidance row (below the header) so the degraded /\n // recovering copy gets the whole card width to wrap on narrow screens,\n // instead of being crushed into the narrow name column next to the\n // status badge + actions menu.\n const health = classifyAgentHealth(status, healthErrorMessage ?? null);\n if (health.phase === 'none') return null;\n return (\n <div\n className={`mt-3 flex items-start gap-1.5 text-xs ${\n health.phase === 'attention' ? 'text-status-warning-text' : 'text-content-tertiary'\n }`}\n >\n {health.inProgress && (\n <RefreshCw\n className=\"size-3.5 mt-0.5 flex-shrink-0 animate-spin\"\n aria-hidden=\"true\"\n />\n )}\n <p className=\"min-w-0 flex-1 break-words leading-relaxed\">\n <span className=\"font-medium\">{health.label}.</span> {health.actionHint}\n </p>\n </div>\n );\n })()}\n\n <div className=\"mt-5 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 className=\"tabular-nums\">{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-4 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 font-mono text-xs 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-4 flex flex-wrap gap-1.5\">\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-full\"\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-full\">\n +{agent.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n </div>\n );\n};\n"],"mappings":"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"}