@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.
- package/dist/components/agents/AgentCard.js +110 -91
- package/dist/components/agents/AgentCard.js.map +1 -1
- package/dist/components/agents/AgentList.js +147 -143
- package/dist/components/agents/AgentList.js.map +1 -1
- package/dist/components/agents/AgentVisibilityCard.js +88 -0
- package/dist/components/agents/AgentVisibilityCard.js.map +1 -0
- package/dist/components/agents/index.js +1 -0
- package/dist/components/graph/MeshGraphView.js +1 -1
- package/dist/components/graph/MeshGraphView.js.map +1 -1
- package/dist/components/graph/meshTypes.js +5 -4
- package/dist/components/graph/meshTypes.js.map +1 -1
- package/dist/components/vibe/VibeAgentSwitcher.js.map +1 -1
- package/dist/generated/wspace-operations.js +1899 -1883
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +11 -11
- package/dist/pages/AgentDetailsPage.js +707 -701
- package/dist/pages/AgentDetailsPage.js.map +1 -1
- package/dist/pages/AgentGraphPage.js +1 -0
- package/dist/pages/AgentGraphPage.js.map +1 -1
- package/dist/pages/AgentsPage.js +2 -0
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/utils/agentVisibility.js +14 -0
- package/dist/utils/agentVisibility.js.map +1 -0
- package/package.json +1 -1
|
@@ -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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
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
|
|
12
|
-
let
|
|
13
|
-
if (!e) return
|
|
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 ?
|
|
16
|
-
},
|
|
17
|
-
return
|
|
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:
|
|
20
|
-
icon:
|
|
21
|
-
onSelect: () =>
|
|
22
|
-
}), (
|
|
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:
|
|
25
|
-
icon:
|
|
26
|
-
onSelect: () =>
|
|
27
|
-
}),
|
|
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:
|
|
30
|
-
icon:
|
|
31
|
-
onSelect: () =>
|
|
32
|
-
}), (
|
|
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:
|
|
35
|
-
icon:
|
|
36
|
-
onSelect: () =>
|
|
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
|
-
}),
|
|
39
|
+
}), O && L.push({
|
|
39
40
|
key: "edit-tunnel",
|
|
40
|
-
label:
|
|
41
|
-
icon:
|
|
42
|
-
onSelect: () =>
|
|
43
|
-
}),
|
|
41
|
+
label: N("agentsPage.editTunnel", "Edit Tunnel"),
|
|
42
|
+
icon: u,
|
|
43
|
+
onSelect: () => O(x)
|
|
44
|
+
}), P && L.push({
|
|
44
45
|
key: "settings",
|
|
45
|
-
label:
|
|
46
|
-
icon:
|
|
47
|
-
onSelect: () =>
|
|
48
|
-
}), /* @__PURE__ */
|
|
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 ${
|
|
52
|
-
onClick: () =>
|
|
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(),
|
|
55
|
+
e.target === e.currentTarget && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), S?.(x));
|
|
55
56
|
},
|
|
56
57
|
children: [
|
|
57
|
-
/* @__PURE__ */
|
|
58
|
+
/* @__PURE__ */ y("div", {
|
|
58
59
|
className: "flex items-start justify-between gap-2",
|
|
59
|
-
children: [/* @__PURE__ */
|
|
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
|
-
|
|
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 &&
|
|
66
|
+
e.stopPropagation(), e.target === e.currentTarget && A();
|
|
66
67
|
},
|
|
67
|
-
children: /* @__PURE__ */
|
|
68
|
-
checked:
|
|
69
|
-
onCheckedChange: () =>
|
|
68
|
+
children: /* @__PURE__ */ v(b, {
|
|
69
|
+
checked: k,
|
|
70
|
+
onCheckedChange: () => A(),
|
|
70
71
|
onClick: (e) => e.stopPropagation(),
|
|
71
|
-
"aria-label":
|
|
72
|
+
"aria-label": N("agentsPage.selectAgent", "Select {{name}}", { name: x.name })
|
|
72
73
|
})
|
|
73
74
|
}),
|
|
74
|
-
/* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
82
|
+
/* @__PURE__ */ y("div", {
|
|
82
83
|
className: "min-w-0 flex-1",
|
|
83
|
-
children: [/* @__PURE__ */
|
|
84
|
+
children: [/* @__PURE__ */ y("div", {
|
|
84
85
|
className: "flex items-center gap-1.5 sm:gap-2 min-w-0",
|
|
85
|
-
children: [
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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:
|
|
113
|
+
children: x.hostname || `ID: ${x.id}`
|
|
95
114
|
})]
|
|
96
115
|
})
|
|
97
116
|
]
|
|
98
|
-
}), /* @__PURE__ */
|
|
117
|
+
}), /* @__PURE__ */ y("div", {
|
|
99
118
|
className: "flex items-center gap-1 sm:gap-2 flex-shrink-0",
|
|
100
|
-
children: [/* @__PURE__ */
|
|
101
|
-
status:
|
|
102
|
-
reason:
|
|
103
|
-
linkTo: `/vibecontrols/agents/${
|
|
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
|
-
}), (
|
|
108
|
-
ariaLabel:
|
|
109
|
-
title:
|
|
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:
|
|
130
|
+
items: L
|
|
112
131
|
})]
|
|
113
132
|
})]
|
|
114
133
|
}),
|
|
115
134
|
(() => {
|
|
116
|
-
let e = i(
|
|
117
|
-
return e.phase === "none" ? null : /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
141
|
+
}), /* @__PURE__ */ y("p", {
|
|
123
142
|
className: "min-w-0 flex-1 break-words leading-relaxed",
|
|
124
143
|
children: [
|
|
125
|
-
/* @__PURE__ */
|
|
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__ */
|
|
154
|
+
/* @__PURE__ */ y("div", {
|
|
136
155
|
className: "mt-5 grid grid-cols-2 gap-3 text-sm",
|
|
137
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ y("div", {
|
|
138
157
|
className: "flex items-center gap-2 text-text-secondary",
|
|
139
|
-
children: [/* @__PURE__ */
|
|
158
|
+
children: [/* @__PURE__ */ v(s, {
|
|
140
159
|
className: "size-4",
|
|
141
160
|
"aria-hidden": "true"
|
|
142
|
-
}), /* @__PURE__ */
|
|
161
|
+
}), /* @__PURE__ */ v("span", {
|
|
143
162
|
className: "tabular-nums",
|
|
144
|
-
children:
|
|
163
|
+
children: I(x.lastHeartbeat)
|
|
145
164
|
})]
|
|
146
|
-
}),
|
|
165
|
+
}), x.architecture && /* @__PURE__ */ y("div", {
|
|
147
166
|
className: "flex items-center gap-2 text-text-secondary",
|
|
148
|
-
children: [/* @__PURE__ */
|
|
167
|
+
children: [/* @__PURE__ */ v(c, {
|
|
149
168
|
className: "size-4",
|
|
150
169
|
"aria-hidden": "true"
|
|
151
|
-
}), /* @__PURE__ */
|
|
170
|
+
}), /* @__PURE__ */ v("span", { children: x.architecture })]
|
|
152
171
|
})]
|
|
153
172
|
}),
|
|
154
|
-
|
|
173
|
+
x.tunnelUrl && /* @__PURE__ */ y("div", {
|
|
155
174
|
className: "mt-4 flex items-center gap-2 text-sm",
|
|
156
175
|
children: [
|
|
157
|
-
/* @__PURE__ */
|
|
176
|
+
/* @__PURE__ */ v(l, {
|
|
158
177
|
className: "size-4 flex-shrink-0 text-text-secondary",
|
|
159
178
|
"aria-hidden": "true"
|
|
160
179
|
}),
|
|
161
|
-
/* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ v("span", {
|
|
162
181
|
className: "text-text-secondary flex-shrink-0",
|
|
163
182
|
children: "Tunnel:"
|
|
164
183
|
}),
|
|
165
|
-
/* @__PURE__ */
|
|
166
|
-
href:
|
|
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":
|
|
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:
|
|
191
|
+
children: x.tunnelUrl
|
|
173
192
|
})
|
|
174
193
|
]
|
|
175
194
|
}),
|
|
176
|
-
|
|
195
|
+
x.capabilities && x.capabilities.length > 0 && /* @__PURE__ */ y("div", {
|
|
177
196
|
className: "mt-4 flex flex-wrap gap-1.5",
|
|
178
|
-
children: [
|
|
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)),
|
|
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: ["+",
|
|
202
|
+
children: ["+", x.capabilities.length - 4]
|
|
184
203
|
})]
|
|
185
204
|
})
|
|
186
205
|
]
|
|
187
206
|
});
|
|
188
207
|
};
|
|
189
208
|
//#endregion
|
|
190
|
-
export {
|
|
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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"}
|