@burdenoff/microfe-vibecontrols 2026.524.1 → 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.
@@ -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 flex-wrap",
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
- async function f(e, t, n, r, i) {
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 n = await l(e, "/api/code-server/status", t);
22
- if (!n.ok) throw Error(`Status check failed: ${n.status}`);
23
- let a = await n.json(), o = i(a);
24
- if (o) throw Error(o);
25
- if (r(a)) return a;
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 p({ tunnelUrl: u, agentApiKey: d, profile: p, workspacePath: m, mode: h = "newtab", onIframeUrl: g, className: _ }) {
31
- let v = e(), [y, b] = n("idle"), [x, S] = n(null), C = t(async () => {
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
- S("Agent tunnel or API key not available"), b("error");
36
+ C("Agent tunnel or API key not available"), x("error");
34
37
  return;
35
38
  }
36
- S(null);
39
+ C(null);
37
40
  try {
38
- b("checking");
39
- let e = await l(u, "/api/code-server/status", d);
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
- b("installing");
44
- let e = await l(u, "/api/code-server/install", d, { method: "POST" });
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 f(u, d, p, (e) => e.installed, (e) => e.error || null);
47
- } else t.installing && (b("installing"), await f(u, d, p, (e) => e.installed, (e) => e.error || null));
48
- if (t.running) m && t.running && (b("starting"), await l(u, "/api/code-server/restart", d, {
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: m })
54
+ body: JSON.stringify({ workspacePath: h })
51
55
  }), await new Promise((e) => setTimeout(e, 1e3)));
52
56
  else {
53
- b("starting");
54
- let e = await l(u, "/api/code-server/start", d, {
57
+ x("starting");
58
+ let e = await l(u, f(m, "/start"), d, {
55
59
  method: "POST",
56
- body: JSON.stringify({ workspacePath: m })
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
- b("ready");
68
+ x("ready");
65
69
  let n = `${u.replace(/\/$/, "")}/code-server/?apiKey=${encodeURIComponent(d)}`;
66
- h === "iframe" && g ? g(n) : window.open(n, "_blank"), setTimeout(() => b("idle"), 2e3);
70
+ g === "iframe" && _ ? _(n) : window.open(n, "_blank"), setTimeout(() => x("idle"), 2e3);
67
71
  } catch (e) {
68
- S(e instanceof Error ? e.message : "Failed to open editor"), b("error");
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
- ]), w = !u || !d || y !== "idle" && y !== "error" && y !== "ready", T = (() => {
78
- switch (y) {
79
- case "checking": return v("vibecontrols.agents.editor.checking", "Checking...");
80
- case "installing": return v("vibecontrols.agents.editor.installing", "Installing...");
81
- case "starting": return v("vibecontrols.agents.editor.starting", "Starting...");
82
- case "ready": return v("vibecontrols.agents.editor.opened", "Opened");
83
- case "error": return v("vibecontrols.agents.editor.retry", "Retry");
84
- default: return v("vibecontrols.agents.editor.openInBrowser", "Open in Browser");
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
- })(), E = (() => {
87
- switch (y) {
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
- })(), D = y === "checking" || y === "installing" || y === "starting";
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: C,
100
- disabled: w,
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 ${y === "error" ? "bg-status-error-bg text-status-error-text hover:opacity-80" : y === "ready" ? "bg-status-success-bg text-status-success-text" : "bg-bg-sunken text-text-primary hover:bg-bg-sunken/80"} ${_ || ""}`,
102
- title: x || "Open VS Code in browser via code-server",
103
- children: [/* @__PURE__ */ s(E, { className: `w-3.5 h-3.5 sm:w-4 sm:h-4 ${D ? "animate-spin" : ""}` }), /* @__PURE__ */ s("span", {
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: T
110
+ children: E
106
111
  })]
107
- }), y === "error" && x && /* @__PURE__ */ s("div", {
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: x
114
+ children: S
110
115
  })]
111
116
  });
112
117
  }
113
118
  //#endregion
114
- export { p as OpenInBrowserEditorButton };
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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"}