@burdenoff/microfe-vibecontrols 2026.524.2 → 2026.524.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/agents/AgentCard.js +3 -3
- package/dist/components/agents/AgentCard.js.map +1 -1
- package/dist/components/agents/OpenInBrowserEditorButton.js +53 -48
- package/dist/components/agents/OpenInBrowserEditorButton.js.map +1 -1
- package/dist/components/targets/TargetCard.js +5 -2
- package/dist/components/targets/TargetCard.js.map +1 -1
- package/dist/pages/AgentDetailsPage.js +757 -726
- package/dist/pages/AgentDetailsPage.js.map +1 -1
- package/dist/pages/AgentsPage.js +17 -14
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/pages/TargetsPage.js +16 -13
- package/dist/pages/TargetsPage.js.map +1 -1
- package/dist/utils/agentStatus.js +1 -1
- package/dist/utils/agentStatus.js.map +1 -1
- package/package.json +1 -1
|
@@ -40,7 +40,7 @@ var g = ({ agent: g, onSelect: _, onConfigure: v, onSettings: y, onStart: b, onS
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
whileTap: { scale: .98 },
|
|
43
|
-
className: `bg-bg-surface border rounded-lg p-4 hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${w ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : "border-border-default"} ${E}`,
|
|
43
|
+
className: `bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${w ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : "border-border-default"} ${E}`,
|
|
44
44
|
onClick: () => _?.(g),
|
|
45
45
|
children: [
|
|
46
46
|
/* @__PURE__ */ h("div", {
|
|
@@ -73,9 +73,9 @@ var g = ({ agent: g, onSelect: _, onConfigure: v, onSettings: y, onStart: b, onS
|
|
|
73
73
|
/* @__PURE__ */ h("div", {
|
|
74
74
|
className: "min-w-0 flex-1",
|
|
75
75
|
children: [/* @__PURE__ */ h("div", {
|
|
76
|
-
className: "flex items-center gap-1.5 sm:gap-2
|
|
76
|
+
className: "flex items-center gap-1.5 sm:gap-2 min-w-0",
|
|
77
77
|
children: [/* @__PURE__ */ m("h3", {
|
|
78
|
-
className: "font-semibold text-text-primary truncate max-w-[120px] sm:max-w-none text-sm sm:text-base",
|
|
78
|
+
className: "font-semibold text-text-primary truncate min-w-0 max-w-[120px] sm:max-w-none text-sm sm:text-base",
|
|
79
79
|
children: g.name
|
|
80
80
|
}), /* @__PURE__ */ m("span", {
|
|
81
81
|
className: "text-base sm:text-lg flex-shrink-0",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentCard.js","names":[],"sources":["../../../src/components/agents/AgentCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport {\n Server,\n Clock,\n Cpu,\n MoreVertical,\n Play,\n Square,\n Settings,\n Globe,\n Link,\n} from 'lucide-react';\nimport { AgentStatusBadge } from './AgentStatusBadge';\nimport type { Agent, AgentStatus } from './types';\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 { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n // Support both onConfigure and onSettings\n const handleConfigure = onConfigure || onSettings;\n const [showMenu, setShowMenu] = useState(false);\n\n const getStatus = (): AgentStatus => {\n if (agent.status) return agent.status;\n if (!agent.isActive) return 'OFFLINE';\n\n if (agent.lastHeartbeat) {\n const lastHeartbeatTime = new Date(agent.lastHeartbeat).getTime();\n const now = Date.now();\n const diff = now - lastHeartbeatTime;\n\n // If heartbeat is older than 5 minutes, consider it inactive\n if (diff > 5 * 60 * 1000) return 'DEGRADED';\n }\n\n return 'ACTIVE';\n };\n\n const status = getStatus();\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 platformIcon =\n {\n darwin: '🍎',\n linux: '🐧',\n windows: '🪟',\n }[agent.platform || ''] || '💻';\n\n return (\n <motion.div\n whileHover={{ y: -2, transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] } }}\n whileTap={{ scale: 0.98 }}\n className={`bg-bg-surface border rounded-lg p-4 hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${\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 >\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 <button\n onClick={(e) => {\n e.stopPropagation();\n onToggleSelect();\n }}\n className={`flex-shrink-0 w-5 h-5 min-w-[1.25rem] min-h-[1.25rem] rounded border transition-colors mt-0.5 ${\n selected\n ? 'bg-action-primary-bg border-action-primary-bg text-action-primary-text'\n : 'border-border-default hover:border-border-strong bg-bg-surface'\n } flex items-center justify-center`}\n >\n {selected && (\n <svg\n className=\"w-3 h-3\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </button>\n )}\n <div className=\"flex-shrink-0 w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"w-4 h-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 flex-wrap\">\n <h3 className=\"font-semibold text-text-primary truncate max-w-[120px] sm:max-w-none text-sm sm:text-base\">\n {agent.name}\n </h3>\n <span className=\"text-base sm:text-lg flex-shrink-0\">{platformIcon}</span>\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 status={status} reason={healthErrorMessage ?? null} />\n {(handleConfigure || onStart || onStop) && (\n <div className=\"relative\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setShowMenu(!showMenu);\n }}\n className=\"p-1 sm:p-1.5 rounded hover:bg-bg-sunken transition-colors flex items-center justify-center\"\n aria-label=\"Agent actions menu\"\n >\n <MoreVertical className=\"w-4 h-4 text-text-primary opacity-70 hover:opacity-100\" />\n </button>\n <AnimatePresence>\n {showMenu && (\n <motion.div\n initial={{ opacity: 0, scale: 0.95, y: -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n exit={{ opacity: 0, scale: 0.95, y: -4 }}\n transition={{ duration: 0.15 }}\n className=\"absolute right-0 mt-1 w-40 bg-bg-surface border border-border-default rounded-lg shadow-lg z-10\"\n >\n {status === 'ACTIVE' && onStop && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onStop(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-t-lg\"\n >\n <Square className=\"w-4 h-4\" />\n {tr('agentsPage.stopAgent', 'Stop Agent')}\n </motion.button>\n )}\n {(status === 'STOPPED' || status === 'OFFLINE') && onStart && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onStart(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-t-lg\"\n >\n <Play className=\"w-4 h-4\" />\n {tr('agentsPage.startAgent', 'Start Agent')}\n </motion.button>\n )}\n {(status === 'ACTIVE' || status === 'STOPPED' || status === 'OFFLINE') &&\n onDisconnect && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onDisconnect(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-action-danger-text hover:bg-bg-sunken\"\n >\n <Square className=\"w-4 h-4\" />\n {status === 'OFFLINE' ? 'Clear Connection' : 'Disconnect'}\n </motion.button>\n )}\n {onEditTunnelUrl && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onEditTunnelUrl(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken\"\n >\n <Link className=\"w-4 h-4\" />\n Edit Tunnel\n </motion.button>\n )}\n {handleConfigure && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n handleConfigure(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-b-lg\"\n >\n <Settings className=\"w-4 h-4\" />\n Settings\n </motion.button>\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\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=\"w-4 h-4\" />\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=\"w-4 h-4\" />\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=\"w-4 h-4 flex-shrink-0 text-text-secondary\" />\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 className=\"text-action-primary-text hover:underline truncate\"\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\"\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\">\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 { useState } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport {\n Server,\n Clock,\n Cpu,\n MoreVertical,\n Play,\n Square,\n Settings,\n Globe,\n Link,\n} from 'lucide-react';\nimport { AgentStatusBadge } from './AgentStatusBadge';\nimport type { Agent, AgentStatus } from './types';\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 { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n // Support both onConfigure and onSettings\n const handleConfigure = onConfigure || onSettings;\n const [showMenu, setShowMenu] = useState(false);\n\n const getStatus = (): AgentStatus => {\n if (agent.status) return agent.status;\n if (!agent.isActive) return 'OFFLINE';\n\n if (agent.lastHeartbeat) {\n const lastHeartbeatTime = new Date(agent.lastHeartbeat).getTime();\n const now = Date.now();\n const diff = now - lastHeartbeatTime;\n\n // If heartbeat is older than 5 minutes, consider it inactive\n if (diff > 5 * 60 * 1000) return 'DEGRADED';\n }\n\n return 'ACTIVE';\n };\n\n const status = getStatus();\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 platformIcon =\n {\n darwin: '🍎',\n linux: '🐧',\n windows: '🪟',\n }[agent.platform || ''] || '💻';\n\n return (\n <motion.div\n whileHover={{ y: -2, transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] } }}\n whileTap={{ scale: 0.98 }}\n className={`bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${\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 >\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 <button\n onClick={(e) => {\n e.stopPropagation();\n onToggleSelect();\n }}\n className={`flex-shrink-0 w-5 h-5 min-w-[1.25rem] min-h-[1.25rem] rounded border transition-colors mt-0.5 ${\n selected\n ? 'bg-action-primary-bg border-action-primary-bg text-action-primary-text'\n : 'border-border-default hover:border-border-strong bg-bg-surface'\n } flex items-center justify-center`}\n >\n {selected && (\n <svg\n className=\"w-3 h-3\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </button>\n )}\n <div className=\"flex-shrink-0 w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"w-4 h-4 sm:w-5 sm:h-5 text-text-secondary\" />\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 max-w-[120px] sm:max-w-none text-sm sm:text-base\">\n {agent.name}\n </h3>\n <span className=\"text-base sm:text-lg flex-shrink-0\">{platformIcon}</span>\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 status={status} reason={healthErrorMessage ?? null} />\n {(handleConfigure || onStart || onStop) && (\n <div className=\"relative\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setShowMenu(!showMenu);\n }}\n className=\"p-1 sm:p-1.5 rounded hover:bg-bg-sunken transition-colors flex items-center justify-center\"\n aria-label=\"Agent actions menu\"\n >\n <MoreVertical className=\"w-4 h-4 text-text-primary opacity-70 hover:opacity-100\" />\n </button>\n <AnimatePresence>\n {showMenu && (\n <motion.div\n initial={{ opacity: 0, scale: 0.95, y: -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n exit={{ opacity: 0, scale: 0.95, y: -4 }}\n transition={{ duration: 0.15 }}\n className=\"absolute right-0 mt-1 w-40 bg-bg-surface border border-border-default rounded-lg shadow-lg z-10\"\n >\n {status === 'ACTIVE' && onStop && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onStop(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-t-lg\"\n >\n <Square className=\"w-4 h-4\" />\n {tr('agentsPage.stopAgent', 'Stop Agent')}\n </motion.button>\n )}\n {(status === 'STOPPED' || status === 'OFFLINE') && onStart && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onStart(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-t-lg\"\n >\n <Play className=\"w-4 h-4\" />\n {tr('agentsPage.startAgent', 'Start Agent')}\n </motion.button>\n )}\n {(status === 'ACTIVE' || status === 'STOPPED' || status === 'OFFLINE') &&\n onDisconnect && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onDisconnect(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-action-danger-text hover:bg-bg-sunken\"\n >\n <Square className=\"w-4 h-4\" />\n {status === 'OFFLINE' ? 'Clear Connection' : 'Disconnect'}\n </motion.button>\n )}\n {onEditTunnelUrl && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n onEditTunnelUrl(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken\"\n >\n <Link className=\"w-4 h-4\" />\n Edit Tunnel\n </motion.button>\n )}\n {handleConfigure && (\n <motion.button\n whileTap={{ scale: 0.97 }}\n onClick={(e) => {\n e.stopPropagation();\n handleConfigure(agent);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-b-lg\"\n >\n <Settings className=\"w-4 h-4\" />\n Settings\n </motion.button>\n )}\n </motion.div>\n )}\n </AnimatePresence>\n </div>\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=\"w-4 h-4\" />\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=\"w-4 h-4\" />\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=\"w-4 h-4 flex-shrink-0 text-text-secondary\" />\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 className=\"text-action-primary-text hover:underline truncate\"\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\"\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\">\n +{agent.capabilities.length - 4}\n </span>\n )}\n </div>\n )}\n </motion.div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -4,56 +4,60 @@ import { AlertCircle as r, Check as i, Code as a, Loader2 as o } from "lucide-re
|
|
|
4
4
|
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
5
5
|
//#region src/components/agents/OpenInBrowserEditorButton.tsx
|
|
6
6
|
async function l(e, t, n, r) {
|
|
7
|
-
let i = e.replace(/\/$/, "");
|
|
7
|
+
let i = e.replace(/\/$/, ""), a = r?.body != null;
|
|
8
8
|
return fetch(`${i}${t}`, {
|
|
9
9
|
...r,
|
|
10
10
|
headers: {
|
|
11
|
-
"Content-Type": "application/json",
|
|
11
|
+
...a ? { "Content-Type": "application/json" } : {},
|
|
12
12
|
"x-agent-api-key": n,
|
|
13
13
|
...r?.headers
|
|
14
14
|
}
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
var u = 2e3, d = 12e4;
|
|
18
|
-
|
|
18
|
+
function f(e, t) {
|
|
19
|
+
return `/api/profiles/${encodeURIComponent(e)}/code-server${t}`;
|
|
20
|
+
}
|
|
21
|
+
async function p(e, t, n, r, i) {
|
|
19
22
|
let a = Date.now() + d;
|
|
20
23
|
for (; Date.now() < a;) {
|
|
21
|
-
let
|
|
22
|
-
if (!
|
|
23
|
-
let
|
|
24
|
-
if (
|
|
25
|
-
if (r(
|
|
24
|
+
let a = await l(e, f(n, "/status"), t);
|
|
25
|
+
if (!a.ok) throw Error(`Status check failed: ${a.status}`);
|
|
26
|
+
let o = await a.json(), s = i(o);
|
|
27
|
+
if (s) throw Error(s);
|
|
28
|
+
if (r(o)) return o;
|
|
26
29
|
await new Promise((e) => setTimeout(e, u));
|
|
27
30
|
}
|
|
28
31
|
throw Error("Operation timed out");
|
|
29
32
|
}
|
|
30
|
-
function
|
|
31
|
-
let
|
|
33
|
+
function m({ tunnelUrl: u, agentApiKey: d, profile: m, workspacePath: h, mode: g = "newtab", onIframeUrl: _, className: v }) {
|
|
34
|
+
let y = e(), [b, x] = n("idle"), [S, C] = n(null), w = t(async () => {
|
|
32
35
|
if (!u || !d) {
|
|
33
|
-
|
|
36
|
+
C("Agent tunnel or API key not available"), x("error");
|
|
34
37
|
return;
|
|
35
38
|
}
|
|
36
|
-
|
|
39
|
+
C(null);
|
|
37
40
|
try {
|
|
38
|
-
|
|
39
|
-
let e = await l(u, "/
|
|
41
|
+
x("checking");
|
|
42
|
+
let e = await l(u, f(m, "/status"), d);
|
|
43
|
+
if (e.status === 404) throw Error(y("vibecontrols.agents.editor.notInstalled", "Code-server isn't available on this agent. Install the code-server plugin (Plugins tab) to enable Open in Browser."));
|
|
40
44
|
if (!e.ok) throw Error(`Status check failed: ${e.status}`);
|
|
41
45
|
let t = await e.json();
|
|
42
46
|
if (!t.installed && !t.installing) {
|
|
43
|
-
|
|
44
|
-
let e = await l(u, "/
|
|
47
|
+
x("installing");
|
|
48
|
+
let e = await l(u, f(m, "/install"), d, { method: "POST" });
|
|
45
49
|
if (!e.ok) throw Error(`Install failed: ${e.status}`);
|
|
46
|
-
await
|
|
47
|
-
} else t.installing && (
|
|
48
|
-
if (t.running)
|
|
50
|
+
await p(u, d, m, (e) => e.installed, (e) => e.error || null);
|
|
51
|
+
} else t.installing && (x("installing"), await p(u, d, m, (e) => e.installed, (e) => e.error || null));
|
|
52
|
+
if (t.running) h && t.running && (x("starting"), await l(u, f(m, "/restart"), d, {
|
|
49
53
|
method: "POST",
|
|
50
|
-
body: JSON.stringify({ workspacePath:
|
|
54
|
+
body: JSON.stringify({ workspacePath: h })
|
|
51
55
|
}), await new Promise((e) => setTimeout(e, 1e3)));
|
|
52
56
|
else {
|
|
53
|
-
|
|
54
|
-
let e = await l(u, "/
|
|
57
|
+
x("starting");
|
|
58
|
+
let e = await l(u, f(m, "/start"), d, {
|
|
55
59
|
method: "POST",
|
|
56
|
-
body: JSON.stringify({ workspacePath:
|
|
60
|
+
...h ? { body: JSON.stringify({ workspacePath: h }) } : {}
|
|
57
61
|
});
|
|
58
62
|
if (!e.ok) {
|
|
59
63
|
let t = await e.json().catch(() => ({}));
|
|
@@ -61,30 +65,31 @@ function p({ tunnelUrl: u, agentApiKey: d, profile: p, workspacePath: m, mode: h
|
|
|
61
65
|
}
|
|
62
66
|
await new Promise((e) => setTimeout(e, 1e3));
|
|
63
67
|
}
|
|
64
|
-
|
|
68
|
+
x("ready");
|
|
65
69
|
let n = `${u.replace(/\/$/, "")}/code-server/?apiKey=${encodeURIComponent(d)}`;
|
|
66
|
-
|
|
70
|
+
g === "iframe" && _ ? _(n) : window.open(n, "_blank"), setTimeout(() => x("idle"), 2e3);
|
|
67
71
|
} catch (e) {
|
|
68
|
-
|
|
72
|
+
C(e instanceof Error ? e.message : "Failed to open editor"), x("error");
|
|
69
73
|
}
|
|
70
74
|
}, [
|
|
71
75
|
u,
|
|
72
76
|
d,
|
|
73
|
-
p,
|
|
74
77
|
m,
|
|
75
78
|
h,
|
|
76
|
-
g
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
case "
|
|
82
|
-
case "
|
|
83
|
-
case "
|
|
84
|
-
|
|
79
|
+
g,
|
|
80
|
+
_,
|
|
81
|
+
y
|
|
82
|
+
]), T = !u || !d || b !== "idle" && b !== "error" && b !== "ready", E = (() => {
|
|
83
|
+
switch (b) {
|
|
84
|
+
case "checking": return y("vibecontrols.agents.editor.checking", "Checking...");
|
|
85
|
+
case "installing": return y("vibecontrols.agents.editor.installing", "Installing...");
|
|
86
|
+
case "starting": return y("vibecontrols.agents.editor.starting", "Starting...");
|
|
87
|
+
case "ready": return y("vibecontrols.agents.editor.opened", "Opened");
|
|
88
|
+
case "error": return y("vibecontrols.agents.editor.retry", "Retry");
|
|
89
|
+
default: return y("vibecontrols.agents.editor.openInBrowser", "Open in Browser");
|
|
85
90
|
}
|
|
86
|
-
})(),
|
|
87
|
-
switch (
|
|
91
|
+
})(), D = (() => {
|
|
92
|
+
switch (b) {
|
|
88
93
|
case "checking":
|
|
89
94
|
case "installing":
|
|
90
95
|
case "starting": return o;
|
|
@@ -92,25 +97,25 @@ function p({ tunnelUrl: u, agentApiKey: d, profile: p, workspacePath: m, mode: h
|
|
|
92
97
|
case "error": return r;
|
|
93
98
|
default: return a;
|
|
94
99
|
}
|
|
95
|
-
})(),
|
|
100
|
+
})(), O = b === "checking" || b === "installing" || b === "starting";
|
|
96
101
|
return /* @__PURE__ */ c("div", {
|
|
97
102
|
className: "relative inline-flex",
|
|
98
103
|
children: [/* @__PURE__ */ c("button", {
|
|
99
|
-
onClick:
|
|
100
|
-
disabled:
|
|
101
|
-
className: `flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${
|
|
102
|
-
title:
|
|
103
|
-
children: [/* @__PURE__ */ s(
|
|
104
|
+
onClick: w,
|
|
105
|
+
disabled: T,
|
|
106
|
+
className: `flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${b === "error" ? "bg-status-error-bg text-status-error-text hover:opacity-80" : b === "ready" ? "bg-status-success-bg text-status-success-text" : "bg-bg-sunken text-text-primary hover:bg-bg-sunken/80"} ${v || ""}`,
|
|
107
|
+
title: S || "Open VS Code in browser via code-server",
|
|
108
|
+
children: [/* @__PURE__ */ s(D, { className: `w-3.5 h-3.5 sm:w-4 sm:h-4 ${O ? "animate-spin" : ""}` }), /* @__PURE__ */ s("span", {
|
|
104
109
|
className: "hidden sm:inline",
|
|
105
|
-
children:
|
|
110
|
+
children: E
|
|
106
111
|
})]
|
|
107
|
-
}),
|
|
112
|
+
}), b === "error" && S && /* @__PURE__ */ s("div", {
|
|
108
113
|
className: "absolute top-full left-0 mt-1 z-50 bg-bg-surface border border-border-default rounded-md shadow-lg p-2 text-xs text-status-error-text max-w-[250px]",
|
|
109
|
-
children:
|
|
114
|
+
children: S
|
|
110
115
|
})]
|
|
111
116
|
});
|
|
112
117
|
}
|
|
113
118
|
//#endregion
|
|
114
|
-
export {
|
|
119
|
+
export { m as OpenInBrowserEditorButton };
|
|
115
120
|
|
|
116
121
|
//# sourceMappingURL=OpenInBrowserEditorButton.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"OpenInBrowserEditorButton.js","names":[],"sources":["../../../src/components/agents/OpenInBrowserEditorButton.tsx"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { Code, Loader2, AlertCircle, Check } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /** Agent API key for authentication */\n agentApiKey: string | null;\n /**\n * Canonical CLI profile name on the remote agent. Maps to the\n * `/api/profiles/<profile>/code-server/...` URL prefix.\n */\n profile: string;\n /** Directory to open as workspace root. Defaults to agent home if not set. */\n workspacePath?: string;\n /** How to open — new browser tab or call back with URL for iframe embedding */\n mode?: 'newtab' | 'iframe';\n /** Called with the ready URL when mode is 'iframe' */\n onIframeUrl?: (url: string) => void;\n /** Additional CSS class names */\n className?: string;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\nasync function agentFetch(\n tunnelUrl: string,\n path: string,\n apiKey: string,\n options?: RequestInit\n): Promise<Response> {\n const base = tunnelUrl.replace(/\\/$/, '');\n return fetch(`${base}${path}`, {\n ...options,\n headers: {\n 'Content-Type': 'application/json',\n 'x-agent-api-key': apiKey,\n ...options?.headers,\n },\n });\n}\n\ninterface CodeServerStatus {\n installed: boolean;\n installing: boolean;\n running: boolean;\n port?: number;\n error?: string;\n}\n\nconst POLL_INTERVAL_MS = 2000;\nconst POLL_TIMEOUT_MS = 120_000; // 2 minutes max\n\n/**\n * Poll until a condition is met on the status response.\n */\nasync function pollUntil(\n tunnelUrl: string,\n apiKey: string,\n _profile: string,\n predicate: (status: CodeServerStatus) => boolean,\n errorCheck: (status: CodeServerStatus) => string | null\n): Promise<CodeServerStatus> {\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n const res = await agentFetch(tunnelUrl, '/api/code-server/status', apiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n const err = errorCheck(status);\n if (err) throw new Error(err);\n\n if (predicate(status)) return status;\n\n await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));\n }\n\n throw new Error('Operation timed out');\n}\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n tunnelUrl,\n agentApiKey,\n profile,\n workspacePath,\n mode = 'newtab',\n onIframeUrl,\n className,\n}: OpenInBrowserEditorButtonProps) {\n const tr = useTr();\n const [state, setState] = useState<EditorState>('idle');\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n const handleClick = useCallback(async () => {\n if (!tunnelUrl || !agentApiKey) {\n setErrorMsg('Agent tunnel or API key not available');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Check status\n setState('checking');\n const res = await agentFetch(tunnelUrl, '/api/code-server/status', agentApiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n // Step 2: Install if needed\n if (!status.installed && !status.installing) {\n setState('installing');\n const installRes = await agentFetch(tunnelUrl, '/api/code-server/install', agentApiKey, {\n method: 'POST',\n });\n if (!installRes.ok) throw new Error(`Install failed: ${installRes.status}`);\n\n // Poll until installed\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n } else if (status.installing) {\n setState('installing');\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n }\n\n // Step 3: Start if not running\n if (!status.running) {\n setState('starting');\n const startRes = await agentFetch(tunnelUrl, '/api/code-server/start', agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n if (!startRes.ok) {\n const err = await startRes.json().catch(() => ({}));\n throw new Error((err as { error?: string }).error || `Start failed: ${startRes.status}`);\n }\n\n // Brief wait for code-server to be ready\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath && status.running) {\n // Restart with new workspace path if different\n setState('starting');\n await agentFetch(tunnelUrl, '/api/code-server/restart', agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Open the editor\n setState('ready');\n const base = tunnelUrl.replace(/\\/$/, '');\n const editorUrl = `${base}/code-server/?apiKey=${encodeURIComponent(agentApiKey)}`;\n\n if (mode === 'iframe' && onIframeUrl) {\n onIframeUrl(editorUrl);\n } else {\n window.open(editorUrl, '_blank');\n }\n\n // Reset after a moment\n setTimeout(() => setState('idle'), 2000);\n } catch (err) {\n setErrorMsg(err instanceof Error ? err.message : 'Failed to open editor');\n setState('error');\n }\n }, [tunnelUrl, agentApiKey, profile, workspacePath, mode, onIframeUrl]);\n\n const isDisabled =\n !tunnelUrl || !agentApiKey || (state !== 'idle' && state !== 'error' && state !== 'ready');\n\n const label = (() => {\n switch (state) {\n case 'checking':\n return tr('vibecontrols.agents.editor.checking', 'Checking...');\n case 'installing':\n return tr('vibecontrols.agents.editor.installing', 'Installing...');\n case 'starting':\n return tr('vibecontrols.agents.editor.starting', 'Starting...');\n case 'ready':\n return tr('vibecontrols.agents.editor.opened', 'Opened');\n case 'error':\n return tr('vibecontrols.agents.editor.retry', 'Retry');\n default:\n return tr('vibecontrols.agents.editor.openInBrowser', 'Open in Browser');\n }\n })();\n\n const Icon = (() => {\n switch (state) {\n case 'checking':\n case 'installing':\n case 'starting':\n return Loader2;\n case 'ready':\n return Check;\n case 'error':\n return AlertCircle;\n default:\n return Code;\n }\n })();\n\n const isSpinning = state === 'checking' || state === 'installing' || state === 'starting';\n\n return (\n <div className=\"relative inline-flex\">\n <button\n onClick={handleClick}\n disabled={isDisabled}\n className={`flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${\n state === 'error'\n ? 'bg-status-error-bg text-status-error-text hover:opacity-80'\n : state === 'ready'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n } ${className || ''}`}\n title={errorMsg || 'Open VS Code in browser via code-server'}\n >\n <Icon className={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error tooltip */}\n {state === 'error' && errorMsg && (\n <div className=\"absolute top-full left-0 mt-1 z-50 bg-bg-surface border border-border-default rounded-md shadow-lg p-2 text-xs text-status-error-text max-w-[250px]\">\n {errorMsg}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"OpenInBrowserEditorButton.js","names":[],"sources":["../../../src/components/agents/OpenInBrowserEditorButton.tsx"],"sourcesContent":["import { useState, useCallback } from 'react';\nimport { Code, Loader2, AlertCircle, Check } from 'lucide-react';\nimport { useTr } from '../../shared/hooks/useTr';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\ntype EditorState = 'idle' | 'checking' | 'installing' | 'starting' | 'ready' | 'error';\n\nexport interface OpenInBrowserEditorButtonProps {\n /** Agent tunnel URL (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /** Agent API key for authentication */\n agentApiKey: string | null;\n /**\n * Canonical CLI profile name on the remote agent. Maps to the\n * `/api/profiles/<profile>/code-server/...` URL prefix.\n */\n profile: string;\n /** Directory to open as workspace root. Defaults to agent home if not set. */\n workspacePath?: string;\n /** How to open — new browser tab or call back with URL for iframe embedding */\n mode?: 'newtab' | 'iframe';\n /** Called with the ready URL when mode is 'iframe' */\n onIframeUrl?: (url: string) => void;\n /** Additional CSS class names */\n className?: string;\n}\n\n// ── Helpers ────────────────────────────────────────────────────────────\n\nasync function agentFetch(\n tunnelUrl: string,\n path: string,\n apiKey: string,\n options?: RequestInit\n): Promise<Response> {\n const base = tunnelUrl.replace(/\\/$/, '');\n // Only advertise a JSON body when we actually send one. The agent's\n // `/api/*` delegation consumes the request body once; a POST that carries a\n // Content-Type but an (already-read) body fails on the agent with\n // \"ReadableStream has already been used\". No body → no Content-Type → safe.\n const hasBody = options?.body != null;\n return fetch(`${base}${path}`, {\n ...options,\n headers: {\n ...(hasBody ? { 'Content-Type': 'application/json' } : {}),\n 'x-agent-api-key': apiKey,\n ...options?.headers,\n },\n });\n}\n\ninterface CodeServerStatus {\n installed: boolean;\n installing: boolean;\n running: boolean;\n port?: number;\n error?: string;\n}\n\nconst POLL_INTERVAL_MS = 2000;\nconst POLL_TIMEOUT_MS = 120_000; // 2 minutes max\n\n/**\n * Build a profile-scoped agent endpoint path. The agent moved every\n * data-plane endpoint under `/api/profiles/<profile>/…` and now returns\n * 410 Gone for bare `/api/code-server/*` paths — hitting the old paths\n * surfaced in the browser as a misleading CORS error (the 410 response\n * omits Access-Control-Allow-Origin).\n */\nfunction codeServerPath(profile: string, sub: string): string {\n return `/api/profiles/${encodeURIComponent(profile)}/code-server${sub}`;\n}\n\n/**\n * Poll until a condition is met on the status response.\n */\nasync function pollUntil(\n tunnelUrl: string,\n apiKey: string,\n profile: string,\n predicate: (status: CodeServerStatus) => boolean,\n errorCheck: (status: CodeServerStatus) => string | null\n): Promise<CodeServerStatus> {\n const deadline = Date.now() + POLL_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n const res = await agentFetch(tunnelUrl, codeServerPath(profile, '/status'), apiKey);\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n const err = errorCheck(status);\n if (err) throw new Error(err);\n\n if (predicate(status)) return status;\n\n await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));\n }\n\n throw new Error('Operation timed out');\n}\n\n// ── Component ──────────────────────────────────────────────────────────\n\nexport function OpenInBrowserEditorButton({\n tunnelUrl,\n agentApiKey,\n profile,\n workspacePath,\n mode = 'newtab',\n onIframeUrl,\n className,\n}: OpenInBrowserEditorButtonProps) {\n const tr = useTr();\n const [state, setState] = useState<EditorState>('idle');\n const [errorMsg, setErrorMsg] = useState<string | null>(null);\n\n const handleClick = useCallback(async () => {\n if (!tunnelUrl || !agentApiKey) {\n setErrorMsg('Agent tunnel or API key not available');\n setState('error');\n return;\n }\n\n setErrorMsg(null);\n\n try {\n // Step 1: Check status\n setState('checking');\n const res = await agentFetch(tunnelUrl, codeServerPath(profile, '/status'), agentApiKey);\n if (res.status === 404) {\n throw new Error(\n tr(\n 'vibecontrols.agents.editor.notInstalled',\n \"Code-server isn't available on this agent. Install the code-server plugin (Plugins tab) to enable Open in Browser.\"\n )\n );\n }\n if (!res.ok) throw new Error(`Status check failed: ${res.status}`);\n const status: CodeServerStatus = await res.json();\n\n // Step 2: Install if needed\n if (!status.installed && !status.installing) {\n setState('installing');\n const installRes = await agentFetch(\n tunnelUrl,\n codeServerPath(profile, '/install'),\n agentApiKey,\n {\n method: 'POST',\n }\n );\n if (!installRes.ok) throw new Error(`Install failed: ${installRes.status}`);\n\n // Poll until installed\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n } else if (status.installing) {\n setState('installing');\n await pollUntil(\n tunnelUrl,\n agentApiKey,\n profile,\n (s) => s.installed,\n (s) => s.error || null\n );\n }\n\n // Step 3: Start if not running\n if (!status.running) {\n setState('starting');\n const startRes = await agentFetch(\n tunnelUrl,\n codeServerPath(profile, '/start'),\n agentApiKey,\n {\n method: 'POST',\n ...(workspacePath ? { body: JSON.stringify({ workspacePath }) } : {}),\n }\n );\n if (!startRes.ok) {\n const err = await startRes.json().catch(() => ({}));\n throw new Error((err as { error?: string }).error || `Start failed: ${startRes.status}`);\n }\n\n // Brief wait for code-server to be ready\n await new Promise((r) => setTimeout(r, 1000));\n } else if (workspacePath && status.running) {\n // Restart with new workspace path if different\n setState('starting');\n // workspacePath is guaranteed truthy by the `else if` guard above.\n await agentFetch(tunnelUrl, codeServerPath(profile, '/restart'), agentApiKey, {\n method: 'POST',\n body: JSON.stringify({ workspacePath }),\n });\n await new Promise((r) => setTimeout(r, 1000));\n }\n\n // Step 4: Open the editor\n setState('ready');\n const base = tunnelUrl.replace(/\\/$/, '');\n const editorUrl = `${base}/code-server/?apiKey=${encodeURIComponent(agentApiKey)}`;\n\n if (mode === 'iframe' && onIframeUrl) {\n onIframeUrl(editorUrl);\n } else {\n window.open(editorUrl, '_blank');\n }\n\n // Reset after a moment\n setTimeout(() => setState('idle'), 2000);\n } catch (err) {\n setErrorMsg(err instanceof Error ? err.message : 'Failed to open editor');\n setState('error');\n }\n }, [tunnelUrl, agentApiKey, profile, workspacePath, mode, onIframeUrl, tr]);\n\n const isDisabled =\n !tunnelUrl || !agentApiKey || (state !== 'idle' && state !== 'error' && state !== 'ready');\n\n const label = (() => {\n switch (state) {\n case 'checking':\n return tr('vibecontrols.agents.editor.checking', 'Checking...');\n case 'installing':\n return tr('vibecontrols.agents.editor.installing', 'Installing...');\n case 'starting':\n return tr('vibecontrols.agents.editor.starting', 'Starting...');\n case 'ready':\n return tr('vibecontrols.agents.editor.opened', 'Opened');\n case 'error':\n return tr('vibecontrols.agents.editor.retry', 'Retry');\n default:\n return tr('vibecontrols.agents.editor.openInBrowser', 'Open in Browser');\n }\n })();\n\n const Icon = (() => {\n switch (state) {\n case 'checking':\n case 'installing':\n case 'starting':\n return Loader2;\n case 'ready':\n return Check;\n case 'error':\n return AlertCircle;\n default:\n return Code;\n }\n })();\n\n const isSpinning = state === 'checking' || state === 'installing' || state === 'starting';\n\n return (\n <div className=\"relative inline-flex\">\n <button\n onClick={handleClick}\n disabled={isDisabled}\n className={`flex items-center gap-1.5 px-2.5 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm rounded-md transition-colors disabled:opacity-50 ${\n state === 'error'\n ? 'bg-status-error-bg text-status-error-text hover:opacity-80'\n : state === 'ready'\n ? 'bg-status-success-bg text-status-success-text'\n : 'bg-bg-sunken text-text-primary hover:bg-bg-sunken/80'\n } ${className || ''}`}\n title={errorMsg || 'Open VS Code in browser via code-server'}\n >\n <Icon className={`w-3.5 h-3.5 sm:w-4 sm:h-4 ${isSpinning ? 'animate-spin' : ''}`} />\n <span className=\"hidden sm:inline\">{label}</span>\n </button>\n\n {/* Error tooltip */}\n {state === 'error' && errorMsg && (\n <div className=\"absolute top-full left-0 mt-1 z-50 bg-bg-surface border border-border-default rounded-md shadow-lg p-2 text-xs text-status-error-text max-w-[250px]\">\n {errorMsg}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -41,7 +41,7 @@ var _ = {
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
whileTap: { scale: .99 },
|
|
44
|
-
className: `bg-bg-surface border rounded-lg p-4 hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${D ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : k ? "border-action-primary-bg bg-action-primary-bg/5 shadow-sm" : "border-border-default"}`,
|
|
44
|
+
className: `bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${D ? "border-action-primary-bg ring-1 ring-action-primary-bg/30" : k ? "border-action-primary-bg bg-action-primary-bg/5 shadow-sm" : "border-border-default"}`,
|
|
45
45
|
onClick: () => (S ?? x)?.(y.id),
|
|
46
46
|
children: [
|
|
47
47
|
/* @__PURE__ */ g("div", {
|
|
@@ -174,13 +174,16 @@ var _ = {
|
|
|
174
174
|
/* @__PURE__ */ g("div", {
|
|
175
175
|
className: "mt-3 flex items-center gap-3 text-sm",
|
|
176
176
|
children: [
|
|
177
|
-
N
|
|
177
|
+
N ? /* @__PURE__ */ g("span", {
|
|
178
178
|
className: "inline-flex items-center gap-1 rounded-full border border-status-success-border px-2 py-0.5 text-xs bg-status-success-bg-subtle text-status-success-text",
|
|
179
179
|
children: [
|
|
180
180
|
/* @__PURE__ */ h(r, { size: 10 }),
|
|
181
181
|
"Agent: ",
|
|
182
182
|
y.agents[0].name
|
|
183
183
|
]
|
|
184
|
+
}) : /* @__PURE__ */ h("span", {
|
|
185
|
+
className: "inline-flex items-center gap-1 rounded-full border border-border-default px-2 py-0.5 text-xs bg-bg-sunken text-text-tertiary",
|
|
186
|
+
children: "No agent"
|
|
184
187
|
}),
|
|
185
188
|
y.label && !y.host && /* @__PURE__ */ h("span", {
|
|
186
189
|
className: "text-xs text-text-tertiary",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TargetCard.js","names":[],"sources":["../../../src/components/targets/TargetCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport {\n Server,\n MoreVertical,\n FlaskConical,\n Terminal,\n Download,\n Trash2,\n CheckCircle,\n X,\n Pencil,\n} from 'lucide-react';\nimport { StatusBadge } from '@/components/shared';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype TargetStatus = 'connected' | 'disconnected' | 'unknown';\ntype TargetKind = 'DIRECT' | 'SSH' | 'RDP' | 'VNC';\n\nexport interface TargetCardTarget {\n id: string;\n type: TargetKind;\n name: string;\n label: string | null;\n host: string | null;\n port: number | null;\n username: string | null;\n status: TargetStatus;\n connectionId: string | null;\n tags: string[];\n metadata: Record<string, unknown> | null;\n createdAt: string;\n agents: Array<{ id: string; name: string; version: string; isActive: boolean }>;\n}\n\nexport interface TargetCardProps {\n target: TargetCardTarget;\n onTest?: (id: string) => void;\n onTerminal?: (id: string) => void;\n /**\n * Called when the card body itself is clicked (anywhere outside the\n * inline action buttons). Used to drill into the target detail page.\n * Falls back to `onTerminal` when omitted to preserve existing behaviour.\n */\n onOpenDetail?: (id: string) => void;\n onInstallAgent?: (target: TargetCardTarget) => void;\n onUninstallAgent?: (target: TargetCardTarget) => void;\n onDelete?: (id: string) => void;\n onEdit?: (target: TargetCardTarget) => void;\n selected?: boolean;\n onToggleSelect?: () => void;\n highlighted?: boolean;\n}\n\nconst statusConfig: Record<\n TargetStatus,\n { variant: 'success' | 'error' | 'neutral'; label: string }\n> = {\n connected: { variant: 'success', label: 'Connected' },\n disconnected: { variant: 'error', label: 'Disconnected' },\n unknown: { variant: 'neutral', label: 'Unknown' },\n};\n\nconst typeBadgeClass: Record<TargetKind, string> = {\n DIRECT: 'bg-accent-blue-subtle text-accent-blue',\n SSH: 'bg-status-success-bg-subtle text-status-success-text',\n RDP: 'bg-status-warning-bg-subtle text-status-warning-text',\n VNC: 'bg-accent-purple-subtle text-accent-purple',\n};\n\nexport const TargetCard: FC<TargetCardProps> = ({\n target,\n onTest,\n onTerminal,\n onOpenDetail,\n onInstallAgent,\n onUninstallAgent,\n onDelete,\n onEdit,\n selected = false,\n onToggleSelect,\n highlighted = false,\n}) => {\n const tr = useTr();\n const [showMenu, setShowMenu] = useState(false);\n const installed = target.agents.length > 0;\n\n return (\n <motion.div\n id={`target-row-${target.id}`}\n whileHover={{ y: -1, transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] } }}\n whileTap={{ scale: 0.99 }}\n className={`bg-bg-surface border rounded-lg p-4 hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${\n selected\n ? 'border-action-primary-bg ring-1 ring-action-primary-bg/30'\n : highlighted\n ? 'border-action-primary-bg bg-action-primary-bg/5 shadow-sm'\n : 'border-border-default'\n }`}\n onClick={() => (onOpenDetail ?? onTerminal)?.(target.id)}\n >\n {/* Top row: checkbox + icon + name + badges */}\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 {onToggleSelect && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onToggleSelect();\n }}\n className={`flex-shrink-0 w-5 h-5 min-w-[1.25rem] min-h-[1.25rem] rounded border transition-colors mt-0.5 ${\n selected\n ? 'bg-action-primary-bg border-action-primary-bg text-action-primary-text'\n : 'border-border-default hover:border-border-strong bg-bg-surface'\n } flex items-center justify-center`}\n >\n {selected && (\n <svg\n className=\"w-3 h-3\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </button>\n )}\n <div className=\"flex-shrink-0 w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"w-4 h-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 flex-wrap\">\n <h3 className=\"font-semibold text-text-primary truncate max-w-[120px] sm:max-w-none text-sm sm:text-base\">\n {target.name}\n </h3>\n <span\n className={`inline-flex items-center px-1.5 py-0.5 text-xs font-semibold rounded ${typeBadgeClass[target.type]}`}\n >\n {target.type}\n </span>\n </div>\n <p className=\"text-xs sm:text-sm text-text-secondary truncate\">\n {target.host\n ? `${target.username ? `${target.username}@` : ''}${target.host}${target.port ? `:${target.port}` : ''}`\n : target.label || `ID: ${target.id.slice(0, 8)}`}\n </p>\n </div>\n </div>\n\n {/* Status + actions menu */}\n <div className=\"flex items-center gap-1 sm:gap-2 flex-shrink-0\">\n <StatusBadge\n status={statusConfig[target.status].label}\n variant={statusConfig[target.status].variant}\n />\n <div className=\"relative\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setShowMenu(!showMenu);\n }}\n className=\"p-1 sm:p-1.5 rounded hover:bg-bg-sunken transition-colors flex items-center justify-center\"\n aria-label=\"Target actions menu\"\n >\n <MoreVertical className=\"w-4 h-4 text-text-primary opacity-70 hover:opacity-100\" />\n </button>\n <AnimatePresence>\n {showMenu && (\n <>\n <div className=\"fixed inset-0 z-10\" onClick={() => setShowMenu(false)} />\n <motion.div\n initial={{ opacity: 0, scale: 0.95, y: -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n exit={{ opacity: 0, scale: 0.95, y: -4 }}\n transition={{ duration: 0.15 }}\n className=\"absolute right-0 mt-1 w-44 bg-bg-surface border border-border-default rounded-lg shadow-lg z-20\"\n >\n {onTest && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onTest(target.id);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-t-lg transition-colors\"\n >\n <FlaskConical className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.testConnection', 'Test Connection')}\n </button>\n )}\n {onTerminal && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onTerminal(target.id);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n <Terminal className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.openTerminal', 'Open Terminal')}\n </button>\n )}\n {onEdit && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onEdit(target);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n <Pencil className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.editTarget', 'Edit Target')}\n </button>\n )}\n {target.type !== 'DIRECT' && !installed && onInstallAgent && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onInstallAgent(target);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-action-primary-bg hover:bg-bg-sunken transition-colors\"\n >\n <Download className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.installAgent', 'Install Agent')}\n </button>\n )}\n {target.type !== 'DIRECT' && installed && onUninstallAgent && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onUninstallAgent(target);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-status-error-text hover:bg-bg-sunken transition-colors\"\n >\n <Trash2 className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.uninstallAgent', 'Uninstall Agent')}\n </button>\n )}\n {onDelete && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onDelete(target.id);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-status-error-text hover:bg-bg-sunken rounded-b-lg transition-colors\"\n >\n <X className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.deleteTarget', 'Delete Target')}\n </button>\n )}\n </motion.div>\n </>\n )}\n </AnimatePresence>\n </div>\n </div>\n </div>\n\n {/* Agent info */}\n <div className=\"mt-3 flex items-center gap-3 text-sm\">\n {installed && (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border px-2 py-0.5 text-xs bg-status-success-bg-subtle text-status-success-text\">\n <CheckCircle size={10} />\n Agent: {target.agents[0].name}\n </span>\n )}\n {target.label && !target.host && (\n <span className=\"text-xs text-text-tertiary\">{target.label}</span>\n )}\n {target.host && target.label && (\n <span className=\"text-xs text-text-tertiary\">{target.label}</span>\n )}\n </div>\n\n {/* Tags */}\n {target.tags.length > 0 && (\n <div className=\"mt-2 flex flex-wrap gap-1\">\n {target.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded\"\n >\n {tag}\n </span>\n ))}\n {target.tags.length > 4 && (\n <span className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded\">\n +{target.tags.length - 4}\n </span>\n )}\n </div>\n )}\n </motion.div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TargetCard.js","names":[],"sources":["../../../src/components/targets/TargetCard.tsx"],"sourcesContent":["import type { FC } from 'react';\nimport { useState } from 'react';\nimport { motion, AnimatePresence } from 'framer-motion';\nimport {\n Server,\n MoreVertical,\n FlaskConical,\n Terminal,\n Download,\n Trash2,\n CheckCircle,\n X,\n Pencil,\n} from 'lucide-react';\nimport { StatusBadge } from '@/components/shared';\nimport { useTr } from '../../shared/hooks/useTr';\n\ntype TargetStatus = 'connected' | 'disconnected' | 'unknown';\ntype TargetKind = 'DIRECT' | 'SSH' | 'RDP' | 'VNC';\n\nexport interface TargetCardTarget {\n id: string;\n type: TargetKind;\n name: string;\n label: string | null;\n host: string | null;\n port: number | null;\n username: string | null;\n status: TargetStatus;\n connectionId: string | null;\n tags: string[];\n metadata: Record<string, unknown> | null;\n createdAt: string;\n agents: Array<{ id: string; name: string; version: string; isActive: boolean }>;\n}\n\nexport interface TargetCardProps {\n target: TargetCardTarget;\n onTest?: (id: string) => void;\n onTerminal?: (id: string) => void;\n /**\n * Called when the card body itself is clicked (anywhere outside the\n * inline action buttons). Used to drill into the target detail page.\n * Falls back to `onTerminal` when omitted to preserve existing behaviour.\n */\n onOpenDetail?: (id: string) => void;\n onInstallAgent?: (target: TargetCardTarget) => void;\n onUninstallAgent?: (target: TargetCardTarget) => void;\n onDelete?: (id: string) => void;\n onEdit?: (target: TargetCardTarget) => void;\n selected?: boolean;\n onToggleSelect?: () => void;\n highlighted?: boolean;\n}\n\nconst statusConfig: Record<\n TargetStatus,\n { variant: 'success' | 'error' | 'neutral'; label: string }\n> = {\n connected: { variant: 'success', label: 'Connected' },\n disconnected: { variant: 'error', label: 'Disconnected' },\n unknown: { variant: 'neutral', label: 'Unknown' },\n};\n\nconst typeBadgeClass: Record<TargetKind, string> = {\n DIRECT: 'bg-accent-blue-subtle text-accent-blue',\n SSH: 'bg-status-success-bg-subtle text-status-success-text',\n RDP: 'bg-status-warning-bg-subtle text-status-warning-text',\n VNC: 'bg-accent-purple-subtle text-accent-purple',\n};\n\nexport const TargetCard: FC<TargetCardProps> = ({\n target,\n onTest,\n onTerminal,\n onOpenDetail,\n onInstallAgent,\n onUninstallAgent,\n onDelete,\n onEdit,\n selected = false,\n onToggleSelect,\n highlighted = false,\n}) => {\n const tr = useTr();\n const [showMenu, setShowMenu] = useState(false);\n const installed = target.agents.length > 0;\n\n return (\n <motion.div\n id={`target-row-${target.id}`}\n whileHover={{ y: -1, transition: { duration: 0.15, ease: [0.4, 0, 0.2, 1] } }}\n whileTap={{ scale: 0.99 }}\n className={`bg-bg-surface border rounded-lg p-4 h-full hover:border-border-strong hover:shadow-md transition-all cursor-pointer ${\n selected\n ? 'border-action-primary-bg ring-1 ring-action-primary-bg/30'\n : highlighted\n ? 'border-action-primary-bg bg-action-primary-bg/5 shadow-sm'\n : 'border-border-default'\n }`}\n onClick={() => (onOpenDetail ?? onTerminal)?.(target.id)}\n >\n {/* Top row: checkbox + icon + name + badges */}\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 {onToggleSelect && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onToggleSelect();\n }}\n className={`flex-shrink-0 w-5 h-5 min-w-[1.25rem] min-h-[1.25rem] rounded border transition-colors mt-0.5 ${\n selected\n ? 'bg-action-primary-bg border-action-primary-bg text-action-primary-text'\n : 'border-border-default hover:border-border-strong bg-bg-surface'\n } flex items-center justify-center`}\n >\n {selected && (\n <svg\n className=\"w-3 h-3\"\n fill=\"none\"\n viewBox=\"0 0 24 24\"\n stroke=\"currentColor\"\n strokeWidth={3}\n >\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" d=\"M5 13l4 4L19 7\" />\n </svg>\n )}\n </button>\n )}\n <div className=\"flex-shrink-0 w-8 h-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"w-4 h-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-center gap-1.5 sm:gap-2 flex-wrap\">\n <h3 className=\"font-semibold text-text-primary truncate max-w-[120px] sm:max-w-none text-sm sm:text-base\">\n {target.name}\n </h3>\n <span\n className={`inline-flex items-center px-1.5 py-0.5 text-xs font-semibold rounded ${typeBadgeClass[target.type]}`}\n >\n {target.type}\n </span>\n </div>\n <p className=\"text-xs sm:text-sm text-text-secondary truncate\">\n {target.host\n ? `${target.username ? `${target.username}@` : ''}${target.host}${target.port ? `:${target.port}` : ''}`\n : target.label || `ID: ${target.id.slice(0, 8)}`}\n </p>\n </div>\n </div>\n\n {/* Status + actions menu */}\n <div className=\"flex items-center gap-1 sm:gap-2 flex-shrink-0\">\n <StatusBadge\n status={statusConfig[target.status].label}\n variant={statusConfig[target.status].variant}\n />\n <div className=\"relative\">\n <button\n onClick={(e) => {\n e.stopPropagation();\n setShowMenu(!showMenu);\n }}\n className=\"p-1 sm:p-1.5 rounded hover:bg-bg-sunken transition-colors flex items-center justify-center\"\n aria-label=\"Target actions menu\"\n >\n <MoreVertical className=\"w-4 h-4 text-text-primary opacity-70 hover:opacity-100\" />\n </button>\n <AnimatePresence>\n {showMenu && (\n <>\n <div className=\"fixed inset-0 z-10\" onClick={() => setShowMenu(false)} />\n <motion.div\n initial={{ opacity: 0, scale: 0.95, y: -4 }}\n animate={{ opacity: 1, scale: 1, y: 0 }}\n exit={{ opacity: 0, scale: 0.95, y: -4 }}\n transition={{ duration: 0.15 }}\n className=\"absolute right-0 mt-1 w-44 bg-bg-surface border border-border-default rounded-lg shadow-lg z-20\"\n >\n {onTest && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onTest(target.id);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken rounded-t-lg transition-colors\"\n >\n <FlaskConical className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.testConnection', 'Test Connection')}\n </button>\n )}\n {onTerminal && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onTerminal(target.id);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n <Terminal className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.openTerminal', 'Open Terminal')}\n </button>\n )}\n {onEdit && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onEdit(target);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-text-primary hover:bg-bg-sunken transition-colors\"\n >\n <Pencil className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.editTarget', 'Edit Target')}\n </button>\n )}\n {target.type !== 'DIRECT' && !installed && onInstallAgent && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onInstallAgent(target);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-action-primary-bg hover:bg-bg-sunken transition-colors\"\n >\n <Download className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.installAgent', 'Install Agent')}\n </button>\n )}\n {target.type !== 'DIRECT' && installed && onUninstallAgent && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onUninstallAgent(target);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-status-error-text hover:bg-bg-sunken transition-colors\"\n >\n <Trash2 className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.uninstallAgent', 'Uninstall Agent')}\n </button>\n )}\n {onDelete && (\n <button\n onClick={(e) => {\n e.stopPropagation();\n onDelete(target.id);\n setShowMenu(false);\n }}\n className=\"flex items-center gap-2 w-full px-3 py-2 text-sm text-status-error-text hover:bg-bg-sunken rounded-b-lg transition-colors\"\n >\n <X className=\"w-4 h-4\" />\n {tr('vibecontrols.targets.deleteTarget', 'Delete Target')}\n </button>\n )}\n </motion.div>\n </>\n )}\n </AnimatePresence>\n </div>\n </div>\n </div>\n\n {/* Agent info */}\n <div className=\"mt-3 flex items-center gap-3 text-sm\">\n {installed ? (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-status-success-border px-2 py-0.5 text-xs bg-status-success-bg-subtle text-status-success-text\">\n <CheckCircle size={10} />\n Agent: {target.agents[0].name}\n </span>\n ) : (\n <span className=\"inline-flex items-center gap-1 rounded-full border border-border-default px-2 py-0.5 text-xs bg-bg-sunken text-text-tertiary\">\n No agent\n </span>\n )}\n {target.label && !target.host && (\n <span className=\"text-xs text-text-tertiary\">{target.label}</span>\n )}\n {target.host && target.label && (\n <span className=\"text-xs text-text-tertiary\">{target.label}</span>\n )}\n </div>\n\n {/* Tags */}\n {target.tags.length > 0 && (\n <div className=\"mt-2 flex flex-wrap gap-1\">\n {target.tags.slice(0, 4).map((tag) => (\n <span\n key={tag}\n className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded\"\n >\n {tag}\n </span>\n ))}\n {target.tags.length > 4 && (\n <span className=\"px-2 py-0.5 text-xs bg-bg-sunken text-text-secondary rounded\">\n +{target.tags.length - 4}\n </span>\n )}\n </div>\n )}\n </motion.div>\n );\n};\n"],"mappings":"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"}
|