@burdenoff/microfe-vibecontrols 2026.531.8 → 2026.531.9
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/PluginUIPanel.js +1 -1
- package/dist/components/agents/PluginUIPanel.js.map +1 -1
- package/dist/pages/VibeDetailsPage.js +1 -5
- package/dist/pages/VibeDetailsPage.js.map +1 -1
- package/dist/services/iframeToken.js +42 -23
- package/dist/services/iframeToken.js.map +1 -1
- package/package.json +1 -1
|
@@ -23,7 +23,7 @@ function w({ availablePlugins: S, tunnelUrl: w, agentId: T, profile: E }) {
|
|
|
23
23
|
agentId: T,
|
|
24
24
|
profile: E ?? "default",
|
|
25
25
|
scope: t.IframePlugin,
|
|
26
|
-
resourceId:
|
|
26
|
+
resourceId: e,
|
|
27
27
|
ttlSeconds: 60
|
|
28
28
|
} })).data?.mintAgentCapability?.token;
|
|
29
29
|
return n ? `${a}#vt=${encodeURIComponent(n)}` : a;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluginUIPanel.js","names":[],"sources":["../../../src/components/agents/PluginUIPanel.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useState, useCallback, useRef, useEffect } from 'react';\nimport { useMintAgentCapabilityMutation } from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\nimport {\n X,\n Maximize2,\n Minimize2,\n ExternalLink,\n Loader2,\n AlertCircle,\n RotateCcw,\n Wifi,\n WifiOff,\n GripVertical,\n Plus,\n Monitor,\n ChevronDown,\n} from 'lucide-react';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\nexport interface PluginUITab {\n /** Unique tab ID (allows duplicate plugins in multiple tabs) */\n id: string;\n /** The plugin name (e.g. \"ui-ai\") — may be null if nothing selected yet */\n pluginName: string | null;\n /** Display name shown on the tab */\n name: string;\n /** The full URL to load in the iframe */\n uiUrl: string | null;\n}\n\nexport interface UIPluginOption {\n /** The plugin name key (e.g. \"ui-ai\") */\n pluginName: string;\n /** Display name */\n displayName: string;\n /** Description shown in dropdown */\n description?: string;\n /** Version */\n version?: string;\n /** Custom URL to use instead of the default /ui/{pluginName} path (e.g. for code-server proxy) */\n customUrl?: string;\n}\n\ninterface PluginUIPanelProps {\n /** Available UI plugins the user can pick from */\n availablePlugins: UIPluginOption[];\n /** Base URL of the agent tunnel (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /**\n * Agent ID — F4 Wave B replaced the legacy `?apiKey=` URL embed +\n * postMessage handoff with `mintAgentCapability(IFRAME_PLUGIN)`. Each\n * plugin tab loads with `#vt=<token>` so the long-lived agent secret\n * never reaches the browser.\n */\n agentId: string | null;\n /** Canonical CLI profile name; required for capability mint scoping. */\n profile: string;\n}\n\nlet tabIdCounter = 0;\nfunction nextTabId(): string {\n return `ui-tab-${++tabIdCounter}`;\n}\n\n/**\n * Embedded browser-like panel for plugin UIs.\n * Each tab has a dropdown to select which plugin to load. The same plugin can\n * be loaded in multiple tabs, each with its own navigation state.\n */\nexport function PluginUIPanel({\n availablePlugins,\n tunnelUrl,\n agentId,\n profile,\n}: PluginUIPanelProps) {\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const [tabs, setTabs] = useState<PluginUITab[]>(() => {\n // Start with one blank tab\n return [{ id: nextTabId(), pluginName: null, name: 'New Tab', uiUrl: null }];\n });\n const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id);\n const [maximized, setMaximized] = useState(false);\n\n // Connection status per tab\n const [connectionStatus, setConnectionStatus] = useState<\n Record<string, 'connecting' | 'connected' | 'error'>\n >({});\n\n // Dropdown open state per tab\n const [dropdownOpen, setDropdownOpen] = useState<string | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Drag-to-resize state\n const [resizeHeight, setResizeHeight] = useState<number | null>(null);\n const resizingRef = useRef(false);\n const startYRef = useRef(0);\n const startHeightRef = useRef(0);\n const panelRef = useRef<HTMLDivElement>(null);\n\n // Iframe refs for auth postMessage\n const iframeRefs = useRef<Record<string, HTMLIFrameElement | null>>({});\n\n const activeTab = tabs.find((t) => t.id === activeTabId) || tabs[0] || null;\n\n // Build UI URL for a plugin — F4 Wave B: append a short-lived\n // IFRAME_PLUGIN capability as `#vt=<token>` so the agent's `/ui/<name>`\n // bootstrap can exchange it for a scoped HttpOnly cookie. No\n // long-lived secret in the URL or postMessage.\n const buildUiUrl = useCallback(\n async (pluginName: string, customUrl?: string): Promise<string | null> => {\n if (!tunnelUrl || !agentId) return null;\n const base = tunnelUrl.replace(/\\/$/, '');\n\n // Use customUrl if provided (e.g. for code-server proxy path)\n const urlStr = customUrl\n ? customUrl.startsWith('http')\n ? customUrl\n : `${base}${customUrl}`\n : `${base}/ui/${pluginName}`;\n\n const finalUrl = new URL(urlStr).toString();\n\n try {\n const res = await mintAgentCapability({\n variables: {\n agentId,\n profile: profile ?? 'default',\n scope: AgentCapabilityScope.IframePlugin,\n resourceId: `ui:${pluginName}`,\n ttlSeconds: 60,\n },\n });\n const token = res.data?.mintAgentCapability?.token;\n if (!token) return finalUrl;\n // Fragment-only — never sent to proxies/logs/Referer.\n return `${finalUrl}#vt=${encodeURIComponent(token)}`;\n } catch {\n return finalUrl;\n }\n },\n [tunnelUrl, agentId, profile, mintAgentCapability]\n );\n\n // Display name for a plugin name\n const getDisplayName = useCallback(\n (pluginName: string): string => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n return (\n opt?.displayName ||\n pluginName\n .replace(/^ui-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\n );\n },\n [availablePlugins]\n );\n\n // ── Tab Actions ───────────────────────────────────────────────────\n\n const handleAddTab = useCallback(() => {\n const newTab: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n setTabs((prev) => [...prev, newTab]);\n setActiveTabId(newTab.id);\n setDropdownOpen(newTab.id);\n }, []);\n\n const handleCloseTab = useCallback(\n (tabId: string) => {\n setTabs((prev) => {\n const next = prev.filter((t) => t.id !== tabId);\n // If we closed the last tab, add a blank one\n if (next.length === 0) {\n const blank: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n next.push(blank);\n setActiveTabId(blank.id);\n } else if (activeTabId === tabId) {\n // Select the previous tab or the first one\n const closedIdx = prev.findIndex((t) => t.id === tabId);\n const newIdx = Math.max(0, closedIdx - 1);\n setActiveTabId(next[Math.min(newIdx, next.length - 1)].id);\n }\n return next;\n });\n // Clean up refs\n delete iframeRefs.current[tabId];\n setConnectionStatus((prev) => {\n const next = { ...prev };\n delete next[tabId];\n return next;\n });\n },\n [activeTabId]\n );\n\n const handleSelectPlugin = useCallback(\n async (tabId: string, pluginName: string) => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n const displayName = getDisplayName(pluginName);\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n setDropdownOpen(null);\n const uiUrl = await buildUiUrl(pluginName, opt?.customUrl);\n setTabs((prev) =>\n prev.map((t) => (t.id === tabId ? { ...t, pluginName, name: displayName, uiUrl } : t))\n );\n },\n [availablePlugins, buildUiUrl, getDisplayName]\n );\n\n // ── Auth ──────────────────────────────────────────────────────────\n // F4 Wave B: auth is now handled by the `#vt=` fragment in the iframe\n // src. The agent exchanges it for a scoped cookie on first load. No\n // postMessage handoff needed.\n\n const handleIframeLoad = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connected' }));\n }, []);\n\n const handleIframeError = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'error' }));\n }, []);\n\n const handleReload = useCallback((tabId: string) => {\n const iframe = iframeRefs.current[tabId];\n if (iframe) {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n const src = iframe.src;\n iframe.src = 'about:blank';\n requestAnimationFrame(() => {\n iframe.src = src;\n });\n }\n }, []);\n\n const handleOpenExternal = useCallback(\n (tabId: string) => {\n const tab = tabs.find((t) => t.id === tabId);\n if (!tab?.uiUrl) return;\n window.open(tab.uiUrl, '_blank');\n },\n [tabs]\n );\n\n // ── Close dropdown on outside click ───────────────────────────────\n\n useEffect(() => {\n if (!dropdownOpen) return;\n const handler = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setDropdownOpen(null);\n }\n };\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [dropdownOpen]);\n\n // ── Drag-to-resize ────────────────────────────────────────────────\n\n const handleResizeStart = useCallback(\n (e: React.MouseEvent) => {\n if (maximized) return;\n e.preventDefault();\n resizingRef.current = true;\n startYRef.current = e.clientY;\n startHeightRef.current = panelRef.current?.offsetHeight || 500;\n document.body.style.cursor = 'ns-resize';\n document.body.style.userSelect = 'none';\n },\n [maximized]\n );\n\n useEffect(() => {\n const handleMouseMove = (e: MouseEvent) => {\n if (!resizingRef.current) return;\n const delta = startYRef.current - e.clientY;\n const newHeight = Math.max(\n 250,\n Math.min(startHeightRef.current + delta, window.innerHeight - 150)\n );\n setResizeHeight(newHeight);\n };\n const handleMouseUp = () => {\n if (resizingRef.current) {\n resizingRef.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n }\n };\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n }, []);\n\n // ── Render helpers ────────────────────────────────────────────────\n\n const renderStatusDot = (tabId: string) => {\n const status = connectionStatus[tabId];\n if (status === 'connected')\n return <Wifi className=\"size-3 text-status-success-text shrink-0\" />;\n if (status === 'error') return <WifiOff className=\"size-3 text-status-error-text shrink-0\" />;\n if (status === 'connecting')\n return <Loader2 className=\"size-3 animate-spin text-status-warning-text shrink-0\" />;\n return <Monitor className=\"size-3 text-text-muted shrink-0\" />;\n };\n\n const panelHeight = maximized ? 'h-[calc(100vh-200px)]' : resizeHeight ? undefined : 'h-[500px]';\n const panelStyle = !maximized && resizeHeight ? { height: `${resizeHeight}px` } : undefined;\n\n return (\n <div\n ref={panelRef}\n className=\"border border-border-default rounded-lg overflow-hidden bg-bg-sunken\"\n >\n {/* Drag handle */}\n <div\n onMouseDown={handleResizeStart}\n className=\"h-1 cursor-ns-resize hover:bg-action-primary-bg/30 transition-colors flex items-center justify-center group\"\n title=\"Drag to resize\"\n >\n <GripVertical className=\"w-4 h-3 text-text-muted opacity-0 group-hover:opacity-100 transition-opacity rotate-90\" />\n </div>\n\n {/* ── Tab Bar ────────────────────────────────────────────────── */}\n <div className=\"flex items-center bg-bg-secondary border-b border-border-default overflow-x-auto\">\n {tabs.map((tab) => {\n const isActive = tab.id === activeTab?.id;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => setActiveTabId(tab.id))();\n }\n }}\n key={tab.id}\n className={`group flex items-center gap-1.5 pl-3 pr-1.5 py-1.5 text-xs border-r border-border-subtle cursor-pointer transition-colors min-w-0 max-w-[200px] shrink-0 ${\n isActive\n ? 'bg-bg-surface text-text-primary border-b-2 border-b-action-primary-bg'\n : 'text-text-secondary hover:text-text-primary hover:bg-bg-surface/50'\n }`}\n onClick={() => setActiveTabId(tab.id)}\n >\n {renderStatusDot(tab.id)}\n <span className=\"truncate\">{tab.name}</span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleCloseTab(tab.id);\n }}\n className=\"ml-auto opacity-0 group-hover:opacity-100 hover:text-status-error-text transition-opacity shrink-0 p-0.5\"\n title=\"Close tab\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n );\n })}\n\n {/* Add tab button */}\n <button\n type=\"button\"\n onClick={handleAddTab}\n className=\"flex items-center justify-center size-8 shrink-0 text-text-secondary hover:text-text-primary hover:bg-bg-surface/50 transition-colors\"\n title=\"New tab\"\n >\n <Plus className=\"size-3.5\" />\n </button>\n\n {/* Spacer */}\n <div className=\"flex-1 min-w-0\" />\n\n {/* Global panel actions */}\n <div className=\"flex items-center gap-0.5 px-2 shrink-0\">\n {activeTab?.uiUrl && (\n <>\n <button\n type=\"button\"\n onClick={() => activeTab && handleReload(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Reload\"\n >\n <RotateCcw className=\"size-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => activeTab && handleOpenExternal(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Open in new browser tab\"\n >\n <ExternalLink className=\"size-3.5\" />\n </button>\n </>\n )}\n <button\n type=\"button\"\n onClick={() => {\n setMaximized((prev) => !prev);\n setResizeHeight(null);\n }}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={maximized ? 'Restore size' : 'Maximize'}\n >\n {maximized ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </button>\n </div>\n </div>\n\n {/* ── Address Bar (Plugin Selector Dropdown) ─────────────────── */}\n {activeTab && (\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-bg-surface border-b border-border-default\">\n <Monitor className=\"size-4 text-text-muted shrink-0\" />\n <div className=\"relative flex-1 min-w-0\" ref={dropdownRef}>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(dropdownOpen === activeTab.id ? null : activeTab.id)}\n className=\"w-full flex items-center justify-between gap-2 px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-left hover:border-action-primary-bg/50 focus:outline-none focus:ring-1 focus:ring-action-primary-bg transition-colors\"\n >\n <span\n className={`truncate ${activeTab.pluginName ? 'text-text-primary' : 'text-text-muted'}`}\n >\n {activeTab.pluginName\n ? `${activeTab.name} — ${activeTab.pluginName}`\n : 'Select a plugin to load...'}\n </span>\n <ChevronDown\n className={`size-3.5 text-text-muted shrink-0 transition-transform ${dropdownOpen === activeTab.id ? 'rotate-180' : ''}`}\n />\n </button>\n\n {/* Dropdown list */}\n {dropdownOpen === activeTab.id && (\n <div className=\"absolute z-50 top-full left-0 right-0 mt-1 bg-bg-surface border border-border-default rounded-md shadow-lg overflow-hidden max-h-64 overflow-y-auto\">\n {availablePlugins.length > 0 ? (\n availablePlugins.map((opt) => {\n const isSelected = activeTab.pluginName === opt.pluginName;\n return (\n <button\n type=\"button\"\n key={opt.pluginName}\n onClick={() => handleSelectPlugin(activeTab.id, opt.pluginName)}\n className={`w-full text-left px-3 py-2.5 hover:bg-bg-sunken transition-colors border-b border-border-subtle last:border-b-0 ${\n isSelected ? 'bg-action-primary-bg/10' : ''\n }`}\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n {opt.displayName}\n </span>\n {opt.version && (\n <span className=\"text-xs text-text-muted\">\n {opt.version === 'latest' ? opt.version : `v${opt.version}`}\n </span>\n )}\n </div>\n {opt.description && (\n <p className=\"text-xs text-text-secondary mt-0.5\">{opt.description}</p>\n )}\n <p className=\"text-xs text-text-muted font-mono mt-0.5\">{opt.pluginName}</p>\n </button>\n );\n })\n ) : (\n <div className=\"px-3 py-4 text-center text-sm text-text-muted\">\n No UI plugins available\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* ── Content Area ───────────────────────────────────────────── */}\n <div\n className={`${panelHeight || ''} bg-bg-inverse text-text-inverse relative`}\n style={panelStyle}\n >\n {tabs.map((tab) => (\n <div\n key={tab.id}\n className={`absolute inset-0 ${tab.id === activeTab?.id ? 'block' : 'hidden'}`}\n >\n {!tab.pluginName ? (\n // Empty state — no plugin selected\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center max-w-sm\">\n <Monitor className=\"size-10 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm font-medium\">No plugin selected</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Use the dropdown above to pick a plugin UI to load in this tab.\n </p>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(tab.id)}\n className=\"mt-4 px-4 py-2 bg-bg-sunken text-text-primary text-sm rounded-md hover:bg-bg-muted transition-colors inline-flex items-center gap-2\"\n >\n <ChevronDown className=\"size-3.5\" />\n Select Plugin\n </button>\n </div>\n </div>\n ) : !tab.uiUrl ? (\n // No tunnel\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <AlertCircle className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm\">Agent tunnel not active</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Cannot load plugin UI without an active tunnel connection.\n </p>\n </div>\n </div>\n ) : (\n // Iframe\n <iframe\n ref={(el) => {\n iframeRefs.current[tab.id] = el;\n }}\n src={tab.uiUrl}\n title={`Plugin UI — ${tab.name}`}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n onLoad={() => handleIframeLoad(tab.id)}\n onError={() => handleIframeError(tab.id)}\n />\n )}\n </div>\n ))}\n\n {/* Connection status overlay */}\n {activeTab && connectionStatus[activeTab.id] === 'connecting' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-warning-text pointer-events-none\">\n <Loader2 className=\"size-3 animate-spin\" />\n Connecting...\n </div>\n )}\n {activeTab && connectionStatus[activeTab.id] === 'error' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-error-text\">\n <WifiOff className=\"size-3\" />\n Disconnected\n <button\n type=\"button\"\n onClick={() => handleReload(activeTab.id)}\n className=\"underline hover:text-status-error-text/80 transition-colors\"\n >\n Reconnect\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PluginUIPanel.js","names":[],"sources":["../../../src/components/agents/PluginUIPanel.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useState, useCallback, useRef, useEffect } from 'react';\nimport { useMintAgentCapabilityMutation } from '@/generated/wspace-operations';\nimport { AgentCapabilityScope } from '@/generated/wspace-types';\nimport {\n X,\n Maximize2,\n Minimize2,\n ExternalLink,\n Loader2,\n AlertCircle,\n RotateCcw,\n Wifi,\n WifiOff,\n GripVertical,\n Plus,\n Monitor,\n ChevronDown,\n} from 'lucide-react';\n\n// ── Types ──────────────────────────────────────────────────────────────\n\nexport interface PluginUITab {\n /** Unique tab ID (allows duplicate plugins in multiple tabs) */\n id: string;\n /** The plugin name (e.g. \"ui-ai\") — may be null if nothing selected yet */\n pluginName: string | null;\n /** Display name shown on the tab */\n name: string;\n /** The full URL to load in the iframe */\n uiUrl: string | null;\n}\n\nexport interface UIPluginOption {\n /** The plugin name key (e.g. \"ui-ai\") */\n pluginName: string;\n /** Display name */\n displayName: string;\n /** Description shown in dropdown */\n description?: string;\n /** Version */\n version?: string;\n /** Custom URL to use instead of the default /ui/{pluginName} path (e.g. for code-server proxy) */\n customUrl?: string;\n}\n\ninterface PluginUIPanelProps {\n /** Available UI plugins the user can pick from */\n availablePlugins: UIPluginOption[];\n /** Base URL of the agent tunnel (e.g. \"https://xxx.trycloudflare.com\") */\n tunnelUrl: string | null;\n /**\n * Agent ID — F4 Wave B replaced the legacy `?apiKey=` URL embed +\n * postMessage handoff with `mintAgentCapability(IFRAME_PLUGIN)`. Each\n * plugin tab loads with `#vt=<token>` so the long-lived agent secret\n * never reaches the browser.\n */\n agentId: string | null;\n /** Canonical CLI profile name; required for capability mint scoping. */\n profile: string;\n}\n\nlet tabIdCounter = 0;\nfunction nextTabId(): string {\n return `ui-tab-${++tabIdCounter}`;\n}\n\n/**\n * Embedded browser-like panel for plugin UIs.\n * Each tab has a dropdown to select which plugin to load. The same plugin can\n * be loaded in multiple tabs, each with its own navigation state.\n */\nexport function PluginUIPanel({\n availablePlugins,\n tunnelUrl,\n agentId,\n profile,\n}: PluginUIPanelProps) {\n const [mintAgentCapability] = useMintAgentCapabilityMutation();\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n const [tabs, setTabs] = useState<PluginUITab[]>(() => {\n // Start with one blank tab\n return [{ id: nextTabId(), pluginName: null, name: 'New Tab', uiUrl: null }];\n });\n const [activeTabId, setActiveTabId] = useState<string>(tabs[0].id);\n const [maximized, setMaximized] = useState(false);\n\n // Connection status per tab\n const [connectionStatus, setConnectionStatus] = useState<\n Record<string, 'connecting' | 'connected' | 'error'>\n >({});\n\n // Dropdown open state per tab\n const [dropdownOpen, setDropdownOpen] = useState<string | null>(null);\n const dropdownRef = useRef<HTMLDivElement>(null);\n\n // Drag-to-resize state\n const [resizeHeight, setResizeHeight] = useState<number | null>(null);\n const resizingRef = useRef(false);\n const startYRef = useRef(0);\n const startHeightRef = useRef(0);\n const panelRef = useRef<HTMLDivElement>(null);\n\n // Iframe refs for auth postMessage\n const iframeRefs = useRef<Record<string, HTMLIFrameElement | null>>({});\n\n const activeTab = tabs.find((t) => t.id === activeTabId) || tabs[0] || null;\n\n // Build UI URL for a plugin — F4 Wave B: append a short-lived\n // IFRAME_PLUGIN capability as `#vt=<token>` so the agent's `/ui/<name>`\n // bootstrap can exchange it for a scoped HttpOnly cookie. No\n // long-lived secret in the URL or postMessage.\n const buildUiUrl = useCallback(\n async (pluginName: string, customUrl?: string): Promise<string | null> => {\n if (!tunnelUrl || !agentId) return null;\n const base = tunnelUrl.replace(/\\/$/, '');\n\n // Use customUrl if provided (e.g. for code-server proxy path)\n const urlStr = customUrl\n ? customUrl.startsWith('http')\n ? customUrl\n : `${base}${customUrl}`\n : `${base}/ui/${pluginName}`;\n\n const finalUrl = new URL(urlStr).toString();\n\n try {\n const res = await mintAgentCapability({\n variables: {\n agentId,\n profile: profile ?? 'default',\n scope: AgentCapabilityScope.IframePlugin,\n // Pass the bare plugin name — the svc maps to `/ui/<plugin>`\n // which matches the agent's iframe-token allow-list. Previous\n // `ui:<plugin>` form produced an invalid `/ui/ui:<plugin>`\n // prefix that the agent now rejects (audit-B P0-SEC-03).\n resourceId: pluginName,\n ttlSeconds: 60,\n },\n });\n const token = res.data?.mintAgentCapability?.token;\n if (!token) return finalUrl;\n // Fragment-only — never sent to proxies/logs/Referer.\n return `${finalUrl}#vt=${encodeURIComponent(token)}`;\n } catch {\n return finalUrl;\n }\n },\n [tunnelUrl, agentId, profile, mintAgentCapability]\n );\n\n // Display name for a plugin name\n const getDisplayName = useCallback(\n (pluginName: string): string => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n return (\n opt?.displayName ||\n pluginName\n .replace(/^ui-/, '')\n .replace(/-/g, ' ')\n .replace(/\\b\\w/g, (c) => c.toUpperCase())\n );\n },\n [availablePlugins]\n );\n\n // ── Tab Actions ───────────────────────────────────────────────────\n\n const handleAddTab = useCallback(() => {\n const newTab: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n setTabs((prev) => [...prev, newTab]);\n setActiveTabId(newTab.id);\n setDropdownOpen(newTab.id);\n }, []);\n\n const handleCloseTab = useCallback(\n (tabId: string) => {\n setTabs((prev) => {\n const next = prev.filter((t) => t.id !== tabId);\n // If we closed the last tab, add a blank one\n if (next.length === 0) {\n const blank: PluginUITab = {\n id: nextTabId(),\n pluginName: null,\n name: 'New Tab',\n uiUrl: null,\n };\n next.push(blank);\n setActiveTabId(blank.id);\n } else if (activeTabId === tabId) {\n // Select the previous tab or the first one\n const closedIdx = prev.findIndex((t) => t.id === tabId);\n const newIdx = Math.max(0, closedIdx - 1);\n setActiveTabId(next[Math.min(newIdx, next.length - 1)].id);\n }\n return next;\n });\n // Clean up refs\n delete iframeRefs.current[tabId];\n setConnectionStatus((prev) => {\n const next = { ...prev };\n delete next[tabId];\n return next;\n });\n },\n [activeTabId]\n );\n\n const handleSelectPlugin = useCallback(\n async (tabId: string, pluginName: string) => {\n const opt = availablePlugins.find((p) => p.pluginName === pluginName);\n const displayName = getDisplayName(pluginName);\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n setDropdownOpen(null);\n const uiUrl = await buildUiUrl(pluginName, opt?.customUrl);\n setTabs((prev) =>\n prev.map((t) => (t.id === tabId ? { ...t, pluginName, name: displayName, uiUrl } : t))\n );\n },\n [availablePlugins, buildUiUrl, getDisplayName]\n );\n\n // ── Auth ──────────────────────────────────────────────────────────\n // F4 Wave B: auth is now handled by the `#vt=` fragment in the iframe\n // src. The agent exchanges it for a scoped cookie on first load. No\n // postMessage handoff needed.\n\n const handleIframeLoad = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connected' }));\n }, []);\n\n const handleIframeError = useCallback((tabId: string) => {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'error' }));\n }, []);\n\n const handleReload = useCallback((tabId: string) => {\n const iframe = iframeRefs.current[tabId];\n if (iframe) {\n setConnectionStatus((prev) => ({ ...prev, [tabId]: 'connecting' }));\n const src = iframe.src;\n iframe.src = 'about:blank';\n requestAnimationFrame(() => {\n iframe.src = src;\n });\n }\n }, []);\n\n const handleOpenExternal = useCallback(\n (tabId: string) => {\n const tab = tabs.find((t) => t.id === tabId);\n if (!tab?.uiUrl) return;\n window.open(tab.uiUrl, '_blank');\n },\n [tabs]\n );\n\n // ── Close dropdown on outside click ───────────────────────────────\n\n useEffect(() => {\n if (!dropdownOpen) return;\n const handler = (e: MouseEvent) => {\n if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {\n setDropdownOpen(null);\n }\n };\n document.addEventListener('mousedown', handler);\n return () => document.removeEventListener('mousedown', handler);\n }, [dropdownOpen]);\n\n // ── Drag-to-resize ────────────────────────────────────────────────\n\n const handleResizeStart = useCallback(\n (e: React.MouseEvent) => {\n if (maximized) return;\n e.preventDefault();\n resizingRef.current = true;\n startYRef.current = e.clientY;\n startHeightRef.current = panelRef.current?.offsetHeight || 500;\n document.body.style.cursor = 'ns-resize';\n document.body.style.userSelect = 'none';\n },\n [maximized]\n );\n\n useEffect(() => {\n const handleMouseMove = (e: MouseEvent) => {\n if (!resizingRef.current) return;\n const delta = startYRef.current - e.clientY;\n const newHeight = Math.max(\n 250,\n Math.min(startHeightRef.current + delta, window.innerHeight - 150)\n );\n setResizeHeight(newHeight);\n };\n const handleMouseUp = () => {\n if (resizingRef.current) {\n resizingRef.current = false;\n document.body.style.cursor = '';\n document.body.style.userSelect = '';\n }\n };\n document.addEventListener('mousemove', handleMouseMove);\n document.addEventListener('mouseup', handleMouseUp);\n return () => {\n document.removeEventListener('mousemove', handleMouseMove);\n document.removeEventListener('mouseup', handleMouseUp);\n };\n }, []);\n\n // ── Render helpers ────────────────────────────────────────────────\n\n const renderStatusDot = (tabId: string) => {\n const status = connectionStatus[tabId];\n if (status === 'connected')\n return <Wifi className=\"size-3 text-status-success-text shrink-0\" />;\n if (status === 'error') return <WifiOff className=\"size-3 text-status-error-text shrink-0\" />;\n if (status === 'connecting')\n return <Loader2 className=\"size-3 animate-spin text-status-warning-text shrink-0\" />;\n return <Monitor className=\"size-3 text-text-muted shrink-0\" />;\n };\n\n const panelHeight = maximized ? 'h-[calc(100vh-200px)]' : resizeHeight ? undefined : 'h-[500px]';\n const panelStyle = !maximized && resizeHeight ? { height: `${resizeHeight}px` } : undefined;\n\n return (\n <div\n ref={panelRef}\n className=\"border border-border-default rounded-lg overflow-hidden bg-bg-sunken\"\n >\n {/* Drag handle */}\n <div\n onMouseDown={handleResizeStart}\n className=\"h-1 cursor-ns-resize hover:bg-action-primary-bg/30 transition-colors flex items-center justify-center group\"\n title=\"Drag to resize\"\n >\n <GripVertical className=\"w-4 h-3 text-text-muted opacity-0 group-hover:opacity-100 transition-opacity rotate-90\" />\n </div>\n\n {/* ── Tab Bar ────────────────────────────────────────────────── */}\n <div className=\"flex items-center bg-bg-secondary border-b border-border-default overflow-x-auto\">\n {tabs.map((tab) => {\n const isActive = tab.id === activeTab?.id;\n return (\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => setActiveTabId(tab.id))();\n }\n }}\n key={tab.id}\n className={`group flex items-center gap-1.5 pl-3 pr-1.5 py-1.5 text-xs border-r border-border-subtle cursor-pointer transition-colors min-w-0 max-w-[200px] shrink-0 ${\n isActive\n ? 'bg-bg-surface text-text-primary border-b-2 border-b-action-primary-bg'\n : 'text-text-secondary hover:text-text-primary hover:bg-bg-surface/50'\n }`}\n onClick={() => setActiveTabId(tab.id)}\n >\n {renderStatusDot(tab.id)}\n <span className=\"truncate\">{tab.name}</span>\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n handleCloseTab(tab.id);\n }}\n className=\"ml-auto opacity-0 group-hover:opacity-100 hover:text-status-error-text transition-opacity shrink-0 p-0.5\"\n title=\"Close tab\"\n >\n <X className=\"size-3\" />\n </button>\n </div>\n );\n })}\n\n {/* Add tab button */}\n <button\n type=\"button\"\n onClick={handleAddTab}\n className=\"flex items-center justify-center size-8 shrink-0 text-text-secondary hover:text-text-primary hover:bg-bg-surface/50 transition-colors\"\n title=\"New tab\"\n >\n <Plus className=\"size-3.5\" />\n </button>\n\n {/* Spacer */}\n <div className=\"flex-1 min-w-0\" />\n\n {/* Global panel actions */}\n <div className=\"flex items-center gap-0.5 px-2 shrink-0\">\n {activeTab?.uiUrl && (\n <>\n <button\n type=\"button\"\n onClick={() => activeTab && handleReload(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Reload\"\n >\n <RotateCcw className=\"size-3.5\" />\n </button>\n <button\n type=\"button\"\n onClick={() => activeTab && handleOpenExternal(activeTab.id)}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title=\"Open in new browser tab\"\n >\n <ExternalLink className=\"size-3.5\" />\n </button>\n </>\n )}\n <button\n type=\"button\"\n onClick={() => {\n setMaximized((prev) => !prev);\n setResizeHeight(null);\n }}\n className=\"p-1 text-text-secondary hover:text-text-primary transition-colors\"\n title={maximized ? 'Restore size' : 'Maximize'}\n >\n {maximized ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </button>\n </div>\n </div>\n\n {/* ── Address Bar (Plugin Selector Dropdown) ─────────────────── */}\n {activeTab && (\n <div className=\"flex items-center gap-2 px-3 py-1.5 bg-bg-surface border-b border-border-default\">\n <Monitor className=\"size-4 text-text-muted shrink-0\" />\n <div className=\"relative flex-1 min-w-0\" ref={dropdownRef}>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(dropdownOpen === activeTab.id ? null : activeTab.id)}\n className=\"w-full flex items-center justify-between gap-2 px-3 py-1.5 text-sm bg-bg-sunken border border-border-default rounded-md text-left hover:border-action-primary-bg/50 focus:outline-none focus:ring-1 focus:ring-action-primary-bg transition-colors\"\n >\n <span\n className={`truncate ${activeTab.pluginName ? 'text-text-primary' : 'text-text-muted'}`}\n >\n {activeTab.pluginName\n ? `${activeTab.name} — ${activeTab.pluginName}`\n : 'Select a plugin to load...'}\n </span>\n <ChevronDown\n className={`size-3.5 text-text-muted shrink-0 transition-transform ${dropdownOpen === activeTab.id ? 'rotate-180' : ''}`}\n />\n </button>\n\n {/* Dropdown list */}\n {dropdownOpen === activeTab.id && (\n <div className=\"absolute z-50 top-full left-0 right-0 mt-1 bg-bg-surface border border-border-default rounded-md shadow-lg overflow-hidden max-h-64 overflow-y-auto\">\n {availablePlugins.length > 0 ? (\n availablePlugins.map((opt) => {\n const isSelected = activeTab.pluginName === opt.pluginName;\n return (\n <button\n type=\"button\"\n key={opt.pluginName}\n onClick={() => handleSelectPlugin(activeTab.id, opt.pluginName)}\n className={`w-full text-left px-3 py-2.5 hover:bg-bg-sunken transition-colors border-b border-border-subtle last:border-b-0 ${\n isSelected ? 'bg-action-primary-bg/10' : ''\n }`}\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-primary\">\n {opt.displayName}\n </span>\n {opt.version && (\n <span className=\"text-xs text-text-muted\">\n {opt.version === 'latest' ? opt.version : `v${opt.version}`}\n </span>\n )}\n </div>\n {opt.description && (\n <p className=\"text-xs text-text-secondary mt-0.5\">{opt.description}</p>\n )}\n <p className=\"text-xs text-text-muted font-mono mt-0.5\">{opt.pluginName}</p>\n </button>\n );\n })\n ) : (\n <div className=\"px-3 py-4 text-center text-sm text-text-muted\">\n No UI plugins available\n </div>\n )}\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* ── Content Area ───────────────────────────────────────────── */}\n <div\n className={`${panelHeight || ''} bg-bg-inverse text-text-inverse relative`}\n style={panelStyle}\n >\n {tabs.map((tab) => (\n <div\n key={tab.id}\n className={`absolute inset-0 ${tab.id === activeTab?.id ? 'block' : 'hidden'}`}\n >\n {!tab.pluginName ? (\n // Empty state — no plugin selected\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center max-w-sm\">\n <Monitor className=\"size-10 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm font-medium\">No plugin selected</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Use the dropdown above to pick a plugin UI to load in this tab.\n </p>\n <button\n type=\"button\"\n onClick={() => setDropdownOpen(tab.id)}\n className=\"mt-4 px-4 py-2 bg-bg-sunken text-text-primary text-sm rounded-md hover:bg-bg-muted transition-colors inline-flex items-center gap-2\"\n >\n <ChevronDown className=\"size-3.5\" />\n Select Plugin\n </button>\n </div>\n </div>\n ) : !tab.uiUrl ? (\n // No tunnel\n <div className=\"flex items-center justify-center h-full\">\n <div className=\"text-center\">\n <AlertCircle className=\"size-8 text-text-secondary mx-auto mb-3\" />\n <p className=\"text-text-tertiary text-sm\">Agent tunnel not active</p>\n <p className=\"text-text-secondary text-xs mt-1\">\n Cannot load plugin UI without an active tunnel connection.\n </p>\n </div>\n </div>\n ) : (\n // Iframe\n <iframe\n ref={(el) => {\n iframeRefs.current[tab.id] = el;\n }}\n src={tab.uiUrl}\n title={`Plugin UI — ${tab.name}`}\n className=\"w-full h-full border-0\"\n allow=\"clipboard-read; clipboard-write\"\n onLoad={() => handleIframeLoad(tab.id)}\n onError={() => handleIframeError(tab.id)}\n />\n )}\n </div>\n ))}\n\n {/* Connection status overlay */}\n {activeTab && connectionStatus[activeTab.id] === 'connecting' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-warning-text pointer-events-none\">\n <Loader2 className=\"size-3 animate-spin\" />\n Connecting...\n </div>\n )}\n {activeTab && connectionStatus[activeTab.id] === 'error' && activeTab.uiUrl && (\n <div className=\"absolute bottom-3 right-3 flex items-center gap-2 px-3 py-1.5 bg-bg-overlay backdrop-blur-sm rounded-md text-xs text-status-error-text\">\n <WifiOff className=\"size-3\" />\n Disconnected\n <button\n type=\"button\"\n onClick={() => handleReload(activeTab.id)}\n className=\"underline hover:text-status-error-text/80 transition-colors\"\n >\n Reconnect\n </button>\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1596,11 +1596,7 @@ function st({ vibeId: e, agentId: n, profile: r, tunnelUrl: i, type: a, tr: o })
|
|
|
1596
1596
|
p(!1);
|
|
1597
1597
|
}
|
|
1598
1598
|
}
|
|
1599
|
-
}, [y, oe] = E(null), [se, b] = E(null), S = w(() => [
|
|
1600
|
-
`/ui/${c}`,
|
|
1601
|
-
`/api/profiles/${encodeURIComponent(r)}/${c}`,
|
|
1602
|
-
`/api/${c}`
|
|
1603
|
-
], [r, c]);
|
|
1599
|
+
}, [y, oe] = E(null), [se, b] = E(null), S = w(() => [`/ui/${c}`, `/api/plugins/${c}/ui`], [c]);
|
|
1604
1600
|
C(() => {
|
|
1605
1601
|
if (!n) return;
|
|
1606
1602
|
let e = !1, t = null, i = (a) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"VibeDetailsPage.js","names":[],"sources":["../../src/pages/VibeDetailsPage.tsx"],"sourcesContent":["import { useState, useRef, useEffect, useCallback, useMemo } from 'react';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport { useParams } from 'react-router-dom';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { useVibeControlsStore } from '@/store/vibeControlsStore';\nimport {\n ArrowLeft,\n Zap,\n Play,\n Pause,\n Archive,\n ExternalLink,\n Server,\n Terminal,\n GitBranch,\n Folder,\n Variable,\n Tag,\n Clock,\n Plus,\n RefreshCw,\n Info,\n Settings,\n Link2,\n FileText,\n BookOpen,\n BarChart3,\n Globe,\n Network,\n GitMerge,\n Library,\n X,\n Check,\n ExternalLink as ExternalLinkIcon,\n Loader2,\n Shield,\n MoreVertical,\n Bot,\n} from 'lucide-react';\nimport {\n PageHeader,\n Card,\n StatusBadge,\n EmptyState,\n ErrorState,\n PluginGate,\n} from '@/components/shared';\nimport {\n GIT_PACKAGE,\n GRAPHQL_PLAYGROUND_PACKAGE,\n SESSION_MANAGER_PACKAGE,\n SESSION_PROVIDER_PACKAGES,\n} from '@/constants/pluginCatalog';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { VibeOpenInEditorMenu } from '@/components/agents';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport { NotesTabPanel } from '@/components/notes';\nimport { DocsTabPanel } from '@/components/docs';\nimport { ScorecardPanel } from '@/components/scorecard';\nimport { PermissionButton } from '@burdenoff/fe-libs/shared/components';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { useVibeControls, useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport { useQueryBackedTab } from '@/hooks/useQueryBackedTab';\nimport {\n useGetVibeQuery,\n useUpdateVibeStatusMutation,\n useArchiveVibeMutation,\n useCreateSessionMutation,\n useGetVibeCatalogsQuery,\n useAddVibeToCatalogMutation,\n useRemoveVibeFromCatalogMutation,\n useGetVibeFeatureCatalogQuery,\n useGetVibeFeatureFlagsQuery,\n useSetVibeFeatureFlagMutation,\n useAgentPlaygroundConfigQuery,\n useSetAgentPlaygroundConfigMutation,\n} from '@/generated/wspace-operations';\nimport { VibeGitOpsTab } from '@/components/gitops/VibeGitOpsTab';\nimport { VibeAgentEmptyState } from '@/components/vibe/VibeAgentEmptyState';\nimport { VibeAgentSwitcher } from '@/components/vibe/VibeAgentSwitcher';\nimport {\n DEFAULT_VIBE_FEATURE_CATALOG,\n isVibeFeatureEnabled,\n type VibeFeatureDefinition,\n} from '@/lib/vibeFeatures';\nimport { useFeatureFlagDetails } from '@burdenoff/fe-libs/shared/feature-flags';\nimport {\n PluginIframe,\n type DiscoveredContribution,\n type MountTunnel,\n usePluginContributions,\n} from '@burdenoff/fe-libs/shared/plugins';\nimport {\n mintScopedIframeToken,\n scheduleScopedIframeTokenRefresh,\n appendIframeTokenFragment,\n type IframeTokenAutoRefreshHandle,\n type IframeTokenResponse,\n} from '@/services/iframeToken';\nimport { AddTabDialog } from '@/components/plugins/AddTabDialog';\nimport { PluginIframeMount } from '@/components/plugins/PluginIframeMount';\nimport { SecurityTabPanel } from '@/components/security';\nimport { VibeLlmContextTabPanel } from '@/components/vibe/VibeLlmContextTabPanel';\nimport { VibeLinkedEntitiesPanel } from '@/components/vibe/VibeLinkedEntitiesPanel';\n\n// `PluginIframeTabBody` was the per-page helper that minted a token,\n// rendered the iframe, and lacked refresh + error handling. PR-86's\n// review surfaced the missing refresh / loading-stuck-on-error\n// problems; the consolidated logic now lives in\n// `@/components/plugins/PluginIframeMount` and is consumed by every\n// surface that mounts an iframe-served plugin (Vibe + Agent detail\n// pages and the standalone route).\n\ntype VibeTab =\n | 'overview'\n | 'config'\n | 'git'\n | 'sessions'\n | 'notes'\n | 'docs'\n | 'scorecard'\n | 'catalogs'\n | 'graphqlPlayground'\n | 'gitops'\n | 'security'\n | 'related'\n | 'llmContext'\n // F10 round-3: linked-entity tabs. Each surfaces the vibe's\n // attached vibedecks / actions / scheduledJobs / sandboxes /\n // aiPrompts / tunnels. Backend may not yet expose the\n // `Vibe.linkedEntities` federation field; in that case the panel\n // renders a \"Coming soon\" placeholder.\n | 'vibedecks'\n | 'actions'\n | 'scheduledJobs'\n | 'sandboxes'\n | 'aiPrompts'\n | 'aiContexts'\n | 'instructionFiles'\n | 'tunnels';\n\nconst VIBE_TABS: readonly VibeTab[] = [\n 'overview',\n 'config',\n 'git',\n 'sessions',\n 'notes',\n 'docs',\n 'scorecard',\n 'catalogs',\n 'graphqlPlayground',\n 'gitops',\n 'security',\n 'related',\n 'llmContext',\n 'vibedecks',\n 'actions',\n 'scheduledJobs',\n 'sandboxes',\n 'aiPrompts',\n 'aiContexts',\n 'instructionFiles',\n 'tunnels',\n];\n\n// Tabs that are always visible regardless of per-vibe feature flags.\nconst ALWAYS_VISIBLE_TABS: ReadonlySet<VibeTab> = new Set<VibeTab>(['overview', 'config']);\nconst HIDDEN_VIBE_FEATURE_IDS = new Set<string>(['apiPlayground']);\n// Tabs whose content can only be rendered against an attached agent.\nconst AGENT_REQUIRED_TABS: ReadonlySet<VibeTab> = new Set<VibeTab>([\n 'sessions',\n 'gitops',\n 'graphqlPlayground',\n 'git',\n 'security',\n]);\n\n/**\n * Vibe details page - shows comprehensive information about a single vibe\n */\nexport function VibeDetailsPage() {\n const { t } = useI18n();\n const { id } = useParams<{ id: string }>();\n const { basePath: basePathProp } = useVibeControls();\n const basePath = basePathProp || '';\n const navigate = useVibeNavigate();\n\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n // Active-tab state is declared AFTER `iframeContributions` (further\n // down in this component) so the valid-id set can include both the\n // static `VIBE_TABS` AND the discovered plugin contribution ids.\n // Forward-declaration placeholder so downstream code can reference\n // the binding via the actual hook call below.\n\n const [updateVibeStatus] = useUpdateVibeStatusMutation({ refetchQueries: ['GetVibe'] });\n const [archiveVibe] = useArchiveVibeMutation({ refetchQueries: ['GetVibe'] });\n const [createSession] = useCreateSessionMutation({ refetchQueries: ['GetVibe', 'ListSessions'] });\n\n // Per-vibe feature catalog (drives the toggles in the Config tab and the\n // tab visibility filter below). Falls back to the static catalog if the\n // query is still loading or the backend hasn't shipped this field yet.\n const { data: featureCatalogData } = useGetVibeFeatureCatalogQuery({\n fetchPolicy: 'cache-first',\n errorPolicy: 'ignore',\n });\n const rawFeatureCatalog: readonly VibeFeatureDefinition[] = useMemo(() => {\n const merged = new Map<string, VibeFeatureDefinition>(\n DEFAULT_VIBE_FEATURE_CATALOG.map((feature) => [feature.id, feature])\n );\n\n for (const feature of featureCatalogData?.vibeFeatureCatalog ?? []) {\n merged.set(feature.id, feature);\n }\n\n return Array.from(merged.values());\n }, [featureCatalogData?.vibeFeatureCatalog]);\n const featureCatalog: readonly VibeFeatureDefinition[] = useMemo(\n () => rawFeatureCatalog.filter((feature) => !HIDDEN_VIBE_FEATURE_IDS.has(feature.id)),\n [rawFeatureCatalog]\n );\n\n const [setVibeFeatureFlagMutation, { loading: setFeatureFlagLoading }] =\n useSetVibeFeatureFlagMutation({ refetchQueries: ['GetVibe'] });\n\n // Session creation modal state\n const [showSessionModal, setShowSessionModal] = useState(false);\n const [sessionCreateError, setSessionCreateError] = useState<string | null>(null);\n const [sessionCreateLoading, setSessionCreateLoading] = useState(false);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n\n // Session form refs\n const sessionNameRef = useRef<HTMLInputElement>(null);\n const sessionTypeRef = useRef<HTMLSelectElement>(null);\n const sessionCommandRef = useRef<HTMLInputElement>(null);\n const sessionWorkDirRef = useRef<HTMLInputElement>(null);\n\n // Fetch vibe by ID from GraphQL API\n const { data, loading, error, refetch } = useGetVibeQuery({\n variables: { id: id || '' },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n // Get vibe from response\n const vibe = data?.vibe;\n const { data: featureFlagsData } = useGetVibeFeatureFlagsQuery({\n variables: { id: id || '' },\n skip: !id,\n fetchPolicy: 'cache-first',\n errorPolicy: 'ignore',\n });\n // Feature flags live on the Vibe but may not be reflected in the generated\n // types until `bun run codegen` is re-run after the schema update — read\n // them through an untyped view so this file stays compilable in either state.\n const vibeFlagsView = vibe as\n | (typeof vibe & {\n featureFlags?: Record<string, boolean> | null;\n effectiveFeatureFlags?: Record<string, boolean> | null;\n })\n | undefined;\n const effectiveVibeFlags = featureFlagsData?.vibe ?? vibeFlagsView;\n\n // Per-vibe feature enablement check. Hoisted to the top of the component\n // (before any early-return guards) to keep React hook ordering stable.\n const isFeatureEnabled = useCallback(\n (featureId: string) =>\n isVibeFeatureEnabled(effectiveVibeFlags ?? null, featureId, featureCatalog),\n [effectiveVibeFlags, featureCatalog]\n );\n\n // Platform-level tab gates (component-level feature flags). Missing flag details\n // fail open so platform flag fetch gaps do not hide per-vibe enabled tabs.\n const gitTabDetails = useFeatureFlagDetails('vibes.tabs.git');\n const scorecardTabDetails = useFeatureFlagDetails('vibes.tabs.scorecard');\n const graphqlPlaygroundTabDetails = useFeatureFlagDetails('vibes.tabs.graphql-playground');\n // PR-12 migration flags. When on, the inline tab is filtered out\n // and the corresponding iframe contribution from\n // `vibe-plugin-tool-git` / `vibe-plugin-tool-graphiql` takes its\n // slot in the strip. Both flags default to OFF — the iframe\n // implementations have to prove parity in alpha before promotion.\n const gitIframeMigrationFlag = useFeatureFlagDetails('vibes.tabs.git-iframe');\n const graphiqlIframeMigrationFlag = useFeatureFlagDetails('vibes.tabs.graphiql-iframe');\n const gitopsIframeMigrationFlag = useFeatureFlagDetails('vibes.tabs.gitops-iframe');\n\n // Returns false for platform-gated tabs when the flag is off; true for all other tabs.\n const isPlatformTabEnabled = useCallback(\n (tabId: string): boolean => {\n switch (tabId) {\n case 'git':\n return gitTabDetails ? Boolean(gitTabDetails.value) : true;\n case 'scorecard':\n return scorecardTabDetails ? Boolean(scorecardTabDetails.value) : true;\n case 'graphqlPlayground':\n return graphqlPlaygroundTabDetails ? Boolean(graphqlPlaygroundTabDetails.value) : true;\n default:\n return true; // No platform gate for other tabs\n }\n },\n [gitTabDetails, scorecardTabDetails, graphqlPlaygroundTabDetails]\n );\n\n // Get related data from the vibe response (already included in the query).\n // Multi-agent: a vibe carries zero, one, or many attached agents. The\n // \"effective agent\" for THIS user on THIS vibe is picked here using the\n // store's per-vibe persisted choice plus the fallback chain (own host →\n // own dev VM → first shared → first usable).\n const attachedAgents = vibe?.agents ?? [];\n const vibeSessions = vibe?.sessions || [];\n const childVibes = vibe?.children || [];\n const parentVibe = vibe?.parent || null;\n\n // Per-Vibe default agent persisted in vibeControlsStore (write-through\n // cache against wspace-vibecontrols-svc UserPreferences.defaultAgentForVibe).\n // The user explicitly \"Set as default for this Vibe\" via the agent picker\n // and the choice follows them across machines once backend hydration\n // wires up (PR-7 ships the local cache; PR-10 / future ties in hydration).\n const defaultAgentIdForThisVibe = useVibeControlsStore((s) =>\n id ? s.defaultAgentForVibe?.[id] : undefined\n );\n\n // Auto-pick logic: explicit per-Vibe default wins; otherwise when exactly\n // one usable agent is attached, select it. When multiple, fall back to\n // the first agent with a tunnel URL. Personal agents not owned by the\n // current user are filtered out so we never accidentally route an action\n // through a teammate's laptop.\n const effectiveAgent =\n (defaultAgentIdForThisVibe\n ? (attachedAgents.find(\n (a: (typeof attachedAgents)[number]) => a.id === defaultAgentIdForThisVibe\n ) ?? null)\n : null) ??\n (attachedAgents.length === 1\n ? attachedAgents[0]\n : (attachedAgents.find((a: (typeof attachedAgents)[number]) => Boolean(a.tunnelUrl)) ??\n attachedAgents[0] ??\n null));\n // Back-compat alias for the legacy single-agent code paths below; downstream\n // sections still reference `agent` while we incrementally migrate to\n // `effectiveAgent` + the multi-agent picker UX.\n const agent = effectiveAgent;\n const effectiveTunnelUrl: string | null = effectiveAgent?.tunnelUrl ?? null;\n const { active: effectiveProfile } = useActiveAgentProfile(effectiveAgent?.id ?? null);\n\n // ── iframe-mount discovery ───────────────────────────────────────────\n // Gated by the platform feature flag `vibes.tabs.iframe-host`. When\n // off, the inline tabs render exactly as before with zero additional\n // behaviour. When on, the host fetches contributions from the\n // effective agent's tunnel and renders them as ADDITIONAL tabs in the\n // strip (alongside the inline ones). PR-12abc later replaces the\n // inline Git / GraphiQL / Ungit tabs with their iframe equivalents.\n // Hooks live HERE (above all early returns) so the React Rules of\n // Hooks order stays stable across renders.\n const iframeHostFlag = useFeatureFlagDetails('vibes.tabs.iframe-host');\n const enableIframeHost = iframeHostFlag?.value === true;\n\n const iframePluginTunnels = useMemo<MountTunnel[]>(() => {\n if (!enableIframeHost || !effectiveAgent?.tunnelUrl) return [];\n const agentIdForToken = effectiveAgent.id;\n const profile = effectiveProfile ?? 'default';\n return [\n {\n tunnelUrl: effectiveAgent.tunnelUrl,\n profile,\n label: effectiveAgent.name ?? effectiveAgent.id,\n mintIframeToken: async ({ pathPrefixes }) => {\n return mintScopedIframeToken(agentIdForToken, profile, pathPrefixes);\n },\n },\n ];\n }, [enableIframeHost, effectiveAgent, effectiveProfile]);\n\n const iframeContributions = usePluginContributions(iframePluginTunnels, {\n mountPoint: 'vibe.detailTab',\n enabled: enableIframeHost && iframePluginTunnels.length > 0,\n });\n\n // Active-tab state: the valid id set is the static `VIBE_TABS` UNION\n // the discovered plugin contribution ids. The dynamic ids are merged\n // in here so `?tab=plugin:hello` deep-links survive a reload —\n // `useQueryBackedTab` rejects unknown ids and falls back to default,\n // so a plugin id missing from this set would silently land on\n // `'overview'`. The hook re-runs every render so adding ids as\n // contributions arrive is a no-op for the activeTab transition.\n const validTabIds = useMemo<readonly string[]>(() => {\n const fromContribs = iframeContributions.data\n .filter((c) => c.contribution.mountPoint === 'vibe.detailTab')\n .map((c) => c.contribution.id);\n return [...(VIBE_TABS as readonly string[]), ...fromContribs];\n }, [iframeContributions.data]);\n const [activeTab, setActiveTab] = useQueryBackedTab<string>(validTabIds, 'overview');\n\n // \"+ Add tab\" picker open state. The dialog itself queries the\n // agent for installable contributions and triggers `refetch` once\n // an install completes so the new tab appears without a full reload.\n const [addTabOpen, setAddTabOpen] = useState(false);\n\n // ── Sync active context ────────────────────────────────────────────────\n useEffect(() => {\n if (vibe) {\n useVibeControlsStore.setState({\n activeAgentId: effectiveAgent?.id ?? null,\n selectedAgentId: effectiveAgent?.id ?? null,\n activeVibeId: vibe.id,\n selectedVibeId: vibe.id,\n activeSessionId: null,\n selectedSessionId: null,\n });\n }\n // Only sync when vibe or effective-agent identity changes, not on every field update.\n // useVibeControlsStore.setState is a stable Zustand reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [vibe?.id, effectiveAgent?.id]);\n\n // If the currently selected tab is for a feature that just got disabled\n // (either per-vibe, platform-level, or because the vibe has no attached\n // agent), bounce back to overview so the user is not stranded on an empty\n // panel. Multi-agent: gate on attachedAgents.length, not the legacy\n // singular vibe.agentId.\n const hasAttachedAgent = attachedAgents.length > 0;\n useEffect(() => {\n // Don't bounce while data is still loading — `vibe?.agents` may briefly\n // be empty during cache-and-network revalidation and we don't want to\n // snap an explicitly-selected tab back to overview just because the\n // refetch hasn't landed yet. The panel body shows its own empty state\n // when no agent is attached, so leaving the URL intact is correct.\n if (loading) return;\n if (ALWAYS_VISIBLE_TABS.has(activeTab as VibeTab)) return;\n if (!isFeatureEnabled(activeTab) || !isPlatformTabEnabled(activeTab)) {\n setActiveTab('overview');\n return;\n }\n // Previously bounced to overview when no agent attached; that broke\n // deep links and made tab clicks appear to \"do nothing\". Render the\n // empty state inside the panel instead.\n }, [activeTab, isFeatureEnabled, isPlatformTabEnabled, hasAttachedAgent, setActiveTab, loading]);\n\n const handleBack = () => {\n navigate('/vibes');\n };\n\n const handleActivateVibe = async () => {\n if (!vibe?.id) return;\n try {\n await updateVibeStatus({ variables: { id: vibe.id, status: 'ACTIVE' as const } });\n } catch (err) {\n console.error('Failed to activate vibe:', err);\n }\n };\n\n const handleDeactivateVibe = async () => {\n if (!vibe?.id) return;\n try {\n await updateVibeStatus({ variables: { id: vibe.id, status: 'INACTIVE' as const } });\n } catch (err) {\n console.error('Failed to deactivate vibe:', err);\n }\n };\n\n const [confirmArchive, setConfirmArchive] = useState(false);\n const [moreMenuOpen, setMoreMenuOpen] = useState(false);\n const moreMenuRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n if (!moreMenuOpen) return;\n const onDocClick = (e: MouseEvent) => {\n if (!moreMenuRef.current?.contains(e.target as Node)) setMoreMenuOpen(false);\n };\n document.addEventListener('mousedown', onDocClick);\n return () => document.removeEventListener('mousedown', onDocClick);\n }, [moreMenuOpen]);\n\n const handleArchiveVibe = async () => {\n if (!vibe?.id) return;\n try {\n await archiveVibe({ variables: { id: vibe.id } });\n setConfirmArchive(false);\n } catch (err) {\n console.error('Failed to archive vibe:', err);\n }\n };\n\n const handleStartSession = () => {\n setSessionCreateError(null);\n setShowSessionModal(true);\n };\n\n const handleSubmitCreateSession = async () => {\n const name = sessionNameRef.current?.value?.trim();\n const type = sessionTypeRef.current?.value as 'TERMINAL' | 'TMUX' | 'SSH' | 'SCRIPT' | 'CUSTOM';\n const command = sessionCommandRef.current?.value?.trim() || undefined;\n const workingDirectory = sessionWorkDirRef.current?.value?.trim() || undefined;\n\n if (!name) {\n setSessionCreateError(tr('vibeDetails.sessionNameRequired', 'Session name is required'));\n return;\n }\n\n // Use the effective agent already resolved from vibe.agents (multi-agent aware).\n const agentId = effectiveAgent?.id;\n if (!agentId) {\n setSessionCreateError(\n attachedAgents.length === 0\n ? tr(\n 'vibeDetails.noAgentAttached',\n 'This vibe has no agent attached. Connect an agent to this vibe before creating a session.'\n )\n : tr(\n 'vibeDetails.selectAgent',\n 'Select an agent for this vibe before creating a session.'\n )\n );\n return;\n }\n\n setSessionCreateLoading(true);\n setSessionCreateError(null);\n try {\n await createSession({\n variables: {\n input: {\n name,\n type: type || 'TERMINAL',\n agentId,\n vibeId: vibe?.id,\n command,\n workingDirectory: workingDirectory || vibe?.path || undefined,\n },\n },\n });\n setShowSessionModal(false);\n // Clear form refs to prevent stale values on next open\n if (sessionNameRef.current) sessionNameRef.current.value = '';\n if (sessionTypeRef.current) sessionTypeRef.current.selectedIndex = 0;\n if (sessionCommandRef.current) sessionCommandRef.current.value = '';\n if (sessionWorkDirRef.current) sessionWorkDirRef.current.value = '';\n setSuccessMessage(tr('sessions.createdSuccess', 'Session created successfully'));\n setTimeout(() => setSuccessMessage(null), 3000);\n } catch (err) {\n setSessionCreateError(\n err instanceof Error ? err.message : tr('sessions.failedCreate', 'Failed to create session')\n );\n } finally {\n setSessionCreateLoading(false);\n }\n };\n\n const handleAgentClick = () => {\n if (!agent) return;\n navigate(`/agents/${agent.id}`);\n };\n\n const handleSessionClick = (sessionId: string) => {\n navigate(`/sessions/${sessionId}`);\n };\n\n const handleVibeClick = (vibeId: string) => {\n navigate(`/vibes/${vibeId}`);\n };\n\n const formatTimestamp = (timestamp: string | null | undefined) => {\n if (!timestamp) return tr('health.never', 'Never');\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return tr('health.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 new Date(timestamp).toLocaleDateString();\n };\n\n const getGitStatusColor = (status: string | null | undefined) => {\n switch (status) {\n case 'clean':\n return 'text-status-success-text';\n case 'modified':\n return 'text-status-warning-text';\n case 'ahead':\n return 'text-status-info-text';\n default:\n return 'text-text-secondary';\n }\n };\n\n // Guard states wrapped in MotionPage to prevent unmount/remount animation replay\n if (loading && !vibe) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n <PageSkeleton variant=\"detail\" />\n </MotionPage>\n );\n }\n\n if (error && !vibe) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n <ErrorState\n title={tr('vibeDetails.failedLoad', 'Failed to load vibe')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n if (!vibe) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n <EmptyState\n icon={<Zap className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.notFound', 'Vibe not found')}\n description={`${tr('vibeDetails.notFoundDescriptionPrefix', 'No vibe with ID')} \"${id}\" ${tr('vibeDetails.notFoundDescriptionSuffix', 'exists')}`}\n action={\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n }\n />\n </MotionPage>\n );\n }\n\n // Types for sessions and child vibes from the query\n type SessionItem = NonNullable<typeof vibeSessions>[number];\n type ChildVibeItem = NonNullable<typeof childVibes>[number];\n\n const isActive = vibe.status === 'ACTIVE' || vibe.status === 'RUNNING';\n const runningSessions = vibeSessions.filter((s: SessionItem) => s.status === 'RUNNING').length;\n\n // Tab definitions\n const relatedCount = childVibes.length + (parentVibe ? 1 : 0) + (agent ? 1 : 0);\n const hasGitInfo = Boolean(vibe.gitBranch || vibe.gitRemote);\n\n const allVibeTabs: { id: VibeTab; label: string; icon: typeof Zap; badge?: string }[] = [\n { id: 'overview', label: tr('nav.overview', 'Overview'), icon: Info },\n { id: 'config', label: tr('vibeDetails.config', 'Config'), icon: Settings },\n {\n id: 'git',\n label: tr('vibeDetails.git', 'Git'),\n icon: GitBranch,\n badge: hasGitInfo ? vibe.gitBranch || undefined : undefined,\n },\n {\n id: 'sessions',\n label: tr('nav.sessions', 'Sessions'),\n icon: Terminal,\n badge: `${vibeSessions.length}`,\n },\n { id: 'notes', label: tr('vibeDetails.notes', 'Notes'), icon: FileText },\n { id: 'docs', label: tr('vibeDetails.docs', 'Docs'), icon: BookOpen },\n { id: 'scorecard', label: tr('vibeDetails.scorecard', 'Scorecard'), icon: BarChart3 },\n { id: 'catalogs', label: tr('vibeDetails.catalogs', 'Catalogs'), icon: Library },\n {\n id: 'graphqlPlayground',\n label: tr('vibeDetails.graphqlPlayground', 'GraphQL'),\n icon: Network,\n },\n { id: 'gitops', label: tr('vibeDetails.gitops', 'GitOps'), icon: GitMerge },\n { id: 'security', label: tr('vibeDetails.security', 'Security'), icon: Shield },\n {\n id: 'related',\n label: tr('vibeDetails.related', 'Related'),\n icon: Link2,\n badge: relatedCount > 0 ? `${relatedCount}` : undefined,\n },\n {\n id: 'llmContext',\n label: tr('vibeDetails.llmContext', 'LLM Context'),\n icon: Bot,\n },\n // F10: linked-entity tabs. Always visible; panels render their own\n // empty state when the vibe has no linked entities.\n {\n id: 'vibedecks',\n label: tr('vibeDetails.vibedecks', 'Vibe Decks'),\n icon: Zap,\n },\n {\n id: 'actions',\n label: tr('vibeDetails.actions', 'Actions'),\n icon: Zap,\n },\n {\n id: 'scheduledJobs',\n label: tr('vibeDetails.scheduledJobs', 'Scheduled'),\n icon: Zap,\n },\n {\n id: 'sandboxes',\n label: tr('vibeDetails.sandboxes', 'Sandboxes'),\n icon: Terminal,\n },\n {\n id: 'aiPrompts',\n label: tr('vibeDetails.aiPrompts', 'AI Prompts'),\n icon: Bot,\n },\n {\n id: 'aiContexts',\n label: tr('vibeDetails.aiContexts', 'AI Contexts'),\n icon: Bot,\n },\n {\n id: 'instructionFiles',\n label: tr('vibeDetails.instructionFiles', 'Instructions'),\n icon: Bot,\n },\n {\n id: 'tunnels',\n label: tr('vibeDetails.tunnels', 'Tunnels'),\n icon: Network,\n },\n ];\n\n // F10: linked-entity tabs are always visible — their panel renders an\n // empty state when the vibe has no linked entities of that kind.\n const LINKED_ENTITY_TABS = new Set<VibeTab>([\n 'vibedecks',\n 'actions',\n 'scheduledJobs',\n 'sandboxes',\n 'aiPrompts',\n 'aiContexts',\n 'instructionFiles',\n 'tunnels',\n ]);\n\n // Hide any non-core tab whose feature is disabled — either per-vibe,\n // platform-level, or because the vibe has no attached agent.\n const vibeTabs = allVibeTabs.filter((tab) => {\n if (ALWAYS_VISIBLE_TABS.has(tab.id)) return true;\n if (LINKED_ENTITY_TABS.has(tab.id)) return true;\n if (!isFeatureEnabled(tab.id) || !isPlatformTabEnabled(tab.id)) return false;\n if (!hasAttachedAgent && AGENT_REQUIRED_TABS.has(tab.id)) return false;\n if (tab.id === 'git' && gitIframeMigrationFlag?.value === true) return false;\n if (tab.id === 'graphqlPlayground' && graphiqlIframeMigrationFlag?.value === true) return false;\n if (tab.id === 'gitops' && gitopsIframeMigrationFlag?.value === true) return false;\n return true;\n });\n\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n {/* Success Toast */}\n {successMessage && (\n <div className=\"fixed top-4 left-4 right-4 sm:left-auto z-50 bg-status-success-bg text-status-success-text px-4 py-3 rounded-md shadow-lg\">\n {successMessage}\n </div>\n )}\n\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n\n {/* Header */}\n <PageHeader\n title={vibe.name}\n description={vibe.description || tr('vibeDetails.noDescription', 'No description')}\n breadcrumb={[\n { label: tr('nav.vibes', 'Vibes'), path: basePath ? `${basePath}/vibes` : '/vibes' },\n { label: vibe.name },\n ]}\n actions={\n <div className=\"flex items-center gap-2 flex-wrap\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n disabled={loading}\n className=\"flex items-center justify-center p-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n </button>\n <button\n type=\"button\"\n onClick={() => navigate(`/ai?vibeId=${vibe.id}&contextLevel=vibe`)}\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 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n title={tr('vibeDetails.openAiChat', 'Open AI chat in this vibe')}\n data-testid=\"vibe-open-ai-chat\"\n >\n <Bot className=\"size-3.5 sm:w-4 sm:h-4\" />\n <span className=\"hidden sm:inline\">{tr('vibeDetails.aiChat', 'AI Chat')}</span>\n </button>\n <VibeOpenInEditorMenu\n vibeId={vibe.id}\n workspacePath={vibe.path}\n agentId={effectiveAgent?.id ?? null}\n tunnelUrl={effectiveTunnelUrl}\n profile={effectiveProfile ?? 'default'}\n browserEditorEnabled={isFeatureEnabled('browserEditor')}\n />\n <PermissionButton\n permission={VIBE_PERMISSIONS.SESSION_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip=\"You don't have permission to create sessions\"\n >\n <button\n type=\"button\"\n onClick={handleStartSession}\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 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <Plus className=\"size-3.5 sm:w-4 sm:h-4\" />\n <span className=\"hidden sm:inline\">{tr('vibeDetails.new', 'New')}</span>\n <span className=\"hidden sm:inline\">{tr('nav.sessions', 'Session')}</span>\n </button>\n </PermissionButton>\n <PermissionButton\n permission={VIBE_PERMISSIONS.VIBE_UPDATE}\n deniedBehavior=\"disable\"\n deniedTooltip=\"You don't have permission to manage this vibe\"\n >\n <div className=\"relative\" ref={moreMenuRef}>\n <button\n type=\"button\"\n onClick={() => setMoreMenuOpen((v) => !v)}\n aria-haspopup=\"menu\"\n aria-expanded={moreMenuOpen}\n aria-label={tr('common.moreActions', 'More actions')}\n title={tr('common.moreActions', 'More actions')}\n className=\"flex items-center justify-center p-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n >\n <MoreVertical className=\"size-4\" />\n </button>\n {moreMenuOpen && (\n <div\n role=\"menu\"\n className=\"absolute right-0 mt-2 min-w-[12rem] bg-bg-elevated border border-border-default rounded-md shadow-lg z-20 py-1\"\n >\n <button\n type=\"button\"\n role=\"menuitem\"\n onClick={() => {\n setMoreMenuOpen(false);\n if (isActive) handleDeactivateVibe();\n else handleActivateVibe();\n }}\n className=\"w-full flex items-center gap-2 px-3 py-2 text-sm text-text-primary hover:bg-action-ghost-bg-hover text-left\"\n >\n {isActive ? (\n <>\n <Pause className=\"size-4 text-text-secondary\" />\n <span>\n {tr('vibeDetails.pauseVibe', 'Pause vibe (stop scheduled runs)')}\n </span>\n </>\n ) : (\n <>\n <Play className=\"size-4 text-status-success-text\" />\n <span>\n {tr('vibeDetails.resumeVibe', 'Resume vibe (re-enable scheduled runs)')}\n </span>\n </>\n )}\n </button>\n <div className=\"border-t border-border-subtle my-1\" />\n <button\n type=\"button\"\n role=\"menuitem\"\n onClick={() => {\n setMoreMenuOpen(false);\n setConfirmArchive(true);\n }}\n className=\"w-full flex items-center gap-2 px-3 py-2 text-sm text-status-warning-text hover:bg-action-ghost-bg-hover text-left\"\n >\n <Archive className=\"size-4\" />\n <span>{tr('vibeDetails.archive', 'Archive vibe…')}</span>\n </button>\n </div>\n )}\n </div>\n </PermissionButton>\n </div>\n }\n />\n\n {confirmArchive && (\n <div\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n setConfirmArchive(false);\n }\n }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"archive-vibe-title\"\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4\"\n onClick={(e) => {\n if (e.target === e.currentTarget) setConfirmArchive(false);\n }}\n >\n <div className=\"w-full max-w-md rounded-lg bg-bg-elevated border border-border-default shadow-xl p-5\">\n <h2 id=\"archive-vibe-title\" className=\"text-base font-semibold text-text-primary mb-2\">\n {tr('vibeDetails.archiveConfirmTitle', 'Archive this vibe?')}\n </h2>\n <p className=\"text-sm text-text-secondary mb-4\">\n {tr(\n 'vibeDetails.archiveConfirmBody',\n 'Archived vibes are hidden from the active list and their scheduled actions stop firing. You can restore the vibe later from the archived view. Existing sessions, tunnels, and audit history are preserved.'\n )}\n </p>\n <div className=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => setConfirmArchive(false)}\n className=\"text-sm px-3 py-1.5 rounded-md border border-border-default text-text-primary hover:bg-action-ghost-bg-hover\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleArchiveVibe}\n className=\"text-sm px-3 py-1.5 rounded-md bg-status-warning-bg text-status-warning-text hover:opacity-80\"\n >\n {tr('vibeDetails.archive', 'Archive')}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Status Cards */}\n <MotionList stagger={0.04} className=\"grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4\">\n <MotionItem>\n <Card className=\"p-3 sm:p-4\">\n <div className=\"flex items-center gap-2 sm:gap-3\">\n <div className=\"size-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <Zap className=\"size-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0\">\n <p\n className=\"text-xs sm:text-sm text-text-secondary\"\n title={tr(\n 'vibeDetails.statusTooltip',\n 'ACTIVE: vibe is in your default working set and can run sessions, tunnels and GitOps via its attached agent(s). INACTIVE: vibe is parked — still visible, no sessions auto-resume. RUNNING: at least one session is live. ARCHIVED: hidden from default lists; can be restored.'\n )}\n >\n {tr('sessions.filterStatus', 'Status')}\n </p>\n <StatusBadge status={vibe.status} size=\"md\" />\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-3 sm:p-4\">\n <div className=\"flex items-center gap-2 sm:gap-3\">\n <div className=\"size-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <Server className=\"size-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-xs sm:text-sm text-text-secondary\">\n {tr('sessions.filterAgent', 'Agent')}\n </p>\n <p className=\"font-semibold text-text-primary text-sm sm:text-base truncate\">\n {agent?.name || tr('targets.unknown', 'Unknown')}\n </p>\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-3 sm:p-4\">\n <div className=\"flex items-center gap-2 sm:gap-3\">\n <div className=\"size-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <Terminal className=\"size-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs sm:text-sm text-text-secondary\">\n {tr('nav.sessions', 'Sessions')}\n </p>\n <p className=\"font-semibold text-text-primary text-sm sm:text-base\">\n {runningSessions} {tr('health.active', 'active')}\n </p>\n </div>\n </div>\n </Card>\n </MotionItem>\n </MotionList>\n\n {/* ── Tab Navigation Bar ─────────────────────────────────────────── */}\n <div className=\"border-b border-border-default\">\n <nav\n className=\"flex gap-1 -mb-px overflow-x-auto pb-1\"\n aria-label={tr('vibeDetails.tabsAriaLabel', 'Vibe tabs')}\n >\n {vibeTabs.map((tab) => {\n const Icon = tab.icon;\n const isActiveTab = activeTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n onClick={() => setActiveTab(tab.id)}\n className={`flex shrink-0 items-center gap-1.5 px-3 py-2.5 text-xs sm:gap-2 sm:px-4 sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${\n isActiveTab\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border-default'\n }`}\n aria-selected={isActiveTab}\n role=\"tab\"\n >\n <Icon className=\"size-4\" />\n {tab.label}\n {tab.badge && (\n <span\n className={`hidden rounded-full px-1.5 py-0.5 text-[10px] sm:inline-flex sm:text-xs ${\n isActiveTab\n ? 'bg-action-primary-bg/10 text-action-primary-bg'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {tab.badge}\n </span>\n )}\n </button>\n );\n })}\n {/* iframe-mounted UI plugin tabs (PR-7). Appended after the\n inline strip so the visual order is \"builtins then\n plugins\". Gated by `vibes.tabs.iframe-host`. */}\n {enableIframeHost &&\n iframeContributions.data.map((c) => {\n const isActiveTab = activeTab === c.contribution.id;\n return (\n <button\n type=\"button\"\n key={`plugin-${c.contribution.id}`}\n onClick={() => setActiveTab(c.contribution.id)}\n className={`flex shrink-0 items-center gap-1.5 px-3 py-2.5 text-xs sm:gap-2 sm:px-4 sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${\n isActiveTab\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border-default'\n }`}\n aria-selected={isActiveTab}\n role=\"tab\"\n title={`${c.contribution.title} (plugin: ${c.pluginKey})`}\n >\n {c.contribution.title}\n </button>\n );\n })}\n {/* \"+ Add tab\" affordance — always at the trailing end of\n the strip, after both inline and plugin tabs. Standard\n UX pattern; PR-86 review flagged the misplacement. */}\n {enableIframeHost && effectiveAgent && (\n <button\n type=\"button\"\n onClick={() => setAddTabOpen(true)}\n className=\"flex shrink-0 items-center gap-1 p-2 text-xs sm:text-sm border-b-2 border-transparent text-text-secondary hover:text-text-primary hover:border-border-default\"\n title=\"Install a UI plugin tab\"\n aria-label=\"Add tab\"\n >\n <span aria-hidden>+</span>\n <span className=\"hidden sm:inline\">Add tab</span>\n </button>\n )}\n </nav>\n </div>\n\n {/* ── Tab Panels ─────────────────────────────────────────────────── */}\n <AnimatePresence mode=\"wait\">\n {/* Overview Tab */}\n {activeTab === 'overview' && (\n <motion.div\n key=\"overview\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n {/* Description */}\n {vibe.description && (\n <Card>\n <div className=\"p-4\">\n <p className=\"text-text-primary\">{vibe.description}</p>\n </div>\n </Card>\n )}\n\n {/* No-agent empty state — shown only on the overview tab so it does\n not push tab content out of frame. Agent-dependent tabs are\n already hidden when this is rendered. */}\n {!hasAttachedAgent && (\n <VibeAgentEmptyState vibeId={vibe.id} tr={tr} onAttached={() => refetch()} />\n )}\n\n {hasAttachedAgent && (\n <VibeAgentSwitcher\n vibeId={vibe.id}\n attachedAgents={attachedAgents}\n effectiveAgentId={effectiveAgent?.id ?? null}\n />\n )}\n\n {/* Path */}\n <Card>\n <div className=\"p-3 sm:p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2 text-sm sm:text-base\">\n <Folder className=\"size-4 sm:w-5 sm:h-5\" />\n {tr('vibeDetails.path', 'Path')}\n </h3>\n </div>\n <div className=\"p-3 sm:p-4\">\n <div className=\"bg-bg-sunken rounded-md p-2 sm:p-3 font-mono text-xs sm:text-sm text-text-primary overflow-x-auto break-all\">\n {vibe.path}\n </div>\n </div>\n </Card>\n\n {/* Timeline */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Clock className=\"size-5\" />\n {tr('vibeDetails.timeline', 'Timeline')}\n </h3>\n </div>\n <div className=\"p-4\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm\">\n <div>\n <p className=\"text-text-secondary\">{tr('vibeDetails.created', 'Created')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(vibe.createdAt)}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('vibeDetails.updated', 'Updated')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(vibe.updatedAt)}\n </p>\n </div>\n </div>\n </div>\n </Card>\n </motion.div>\n )}\n\n {/* Config Tab */}\n {activeTab === 'config' && (\n <motion.div\n key=\"config\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n {/* Environment Variables */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Variable className=\"size-5\" />\n {tr('vibeDetails.environmentVariables', 'Environment Variables')}\n </h3>\n </div>\n <div className=\"p-4\">\n {vibe.environmentVariables && Object.keys(vibe.environmentVariables).length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {Object.entries(vibe.environmentVariables).map(([key, value]) => (\n <span\n key={key}\n className=\"px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full font-mono\"\n >\n {key}={String(value)}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-text-secondary\">\n {tr(\n 'vibeDetails.noEnvironmentVariables',\n 'No environment variables configured'\n )}\n </p>\n )}\n </div>\n </Card>\n\n {/* Tags */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Tag className=\"size-5\" />\n {tr('vibeDetails.tags', 'Tags')}\n </h3>\n </div>\n <div className=\"p-4\">\n <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" entityId={vibe.id} />\n </div>\n </Card>\n\n {/* Features — per-vibe feature toggles */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Settings className=\"size-5\" />\n {tr('vibeDetails.features', 'Features')}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'vibeDetails.featuresDescription',\n 'Toggle which sections show up on this vibe. Changes apply to everyone viewing the vibe.'\n )}\n </p>\n </div>\n <div className=\"divide-y divide-border-default\">\n {featureCatalog.map((feature) => {\n const enabled = isFeatureEnabled(feature.id);\n return (\n <label\n key={feature.id}\n className=\"flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-bg-sunken transition-colors\"\n >\n <input\n type=\"checkbox\"\n role=\"switch\"\n aria-label={feature.label}\n aria-checked={enabled}\n checked={enabled}\n disabled={setFeatureFlagLoading || !vibe.id}\n onChange={async (e) => {\n const next = e.target.checked;\n try {\n await setVibeFeatureFlagMutation({\n variables: {\n id: vibe.id,\n featureId: feature.id,\n enabled: next,\n },\n optimisticResponse: {\n setVibeFeatureFlag: {\n __typename: 'Vibe',\n id: vibe.id,\n featureFlags: {\n ...(effectiveVibeFlags?.featureFlags ?? {}),\n [feature.id]: next,\n },\n effectiveFeatureFlags: {\n ...(effectiveVibeFlags?.effectiveFeatureFlags ?? {}),\n [feature.id]: next,\n },\n } as never,\n },\n });\n } catch (err) {\n console.error('Failed to toggle vibe feature:', err);\n }\n }}\n className=\"size-4 shrink-0 accent-action-primary-bg disabled:opacity-50\"\n />\n <span className=\"text-sm font-medium text-text-primary\">{feature.label}</span>\n </label>\n );\n })}\n </div>\n </Card>\n </motion.div>\n )}\n\n {/* Git Tab */}\n {activeTab === 'git' && isFeatureEnabled('git') && (\n <motion.div\n key=\"git\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <PluginGate\n agentId={effectiveAgent?.id ?? null}\n required={[GIT_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('vibeDetails.gitPluginEyebrow', 'Git plugin required')}\n title={tr('vibeDetails.gitPluginTitle', 'Install the git plugin')}\n description={tr(\n 'vibeDetails.gitPluginDescription',\n 'Live git branch, status, and remote data for this vibe are served by the vibe-plugin-tool-git agent plugin.'\n )}\n >\n {hasGitInfo ? (\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <GitBranch className=\"size-5\" />\n {tr('vibeDetails.gitInformation', 'Git Information')}\n </h3>\n </div>\n <div className=\"p-4 space-y-3\">\n <div className=\"flex items-center gap-4 flex-wrap\">\n {vibe.gitBranch && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.branch', 'Branch')}:\n </span>\n <span className=\"px-2 py-1 bg-bg-sunken text-text-primary text-sm rounded font-mono\">\n {vibe.gitBranch}\n </span>\n </div>\n )}\n {vibe.gitStatus && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {tr('sessions.filterStatus', 'Status')}:\n </span>\n <span className={`font-medium ${getGitStatusColor(vibe.gitStatus)}`}>\n {vibe.gitStatus}\n </span>\n </div>\n )}\n </div>\n {vibe.gitRemote && (\n <div>\n <span className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.remote', 'Remote')}:\n </span>\n <p className=\"font-mono text-sm text-text-primary mt-1 break-all\">\n {vibe.gitRemote}\n </p>\n </div>\n )}\n </div>\n </Card>\n ) : (\n <EmptyState\n icon={<GitBranch className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noGitInformation', 'No git information')}\n description={tr(\n 'vibeDetails.noGitInformationDescription',\n 'This vibe does not have any git repository linked'\n )}\n />\n )}\n </PluginGate>\n </motion.div>\n )}\n\n {/* Sessions Tab */}\n {activeTab === 'sessions' && isFeatureEnabled('sessions') && (\n <motion.div\n key=\"sessions\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n >\n <PluginGate\n agentId={effectiveAgent?.id ?? null}\n requirements={[\n {\n packages: [SESSION_MANAGER_PACKAGE],\n mode: 'all',\n eyebrow: tr('vibeDetails.sessionsManagerEyebrow', 'Session manager required'),\n title: tr('vibeDetails.sessionsManagerTitle', 'Install the session manager'),\n description: tr(\n 'vibeDetails.sessionsManagerDescription',\n 'Sessions are orchestrated by the unified session manager plugin. Install it to continue.'\n ),\n },\n {\n packages: SESSION_PROVIDER_PACKAGES,\n mode: 'any',\n eyebrow: tr('vibeDetails.sessionProviderEyebrow', 'Session provider required'),\n title: tr('vibeDetails.sessionProviderTitle', 'Pick a session provider'),\n description: tr(\n 'vibeDetails.sessionProviderDescription',\n 'Choose the terminal multiplexer your sessions should run on. You can install more providers later.'\n ),\n },\n ]}\n >\n <Card>\n <div className=\"p-4 border-b border-border-default flex items-center justify-between\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Terminal className=\"size-5\" />\n {tr('nav.sessions', 'Sessions')} ({vibeSessions.length})\n </h3>\n <button\n type=\"button\"\n onClick={handleStartSession}\n className=\"flex items-center gap-1 px-2 py-1 text-sm bg-bg-sunken text-text-primary rounded hover:bg-bg-sunken/80 transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('vibeDetails.new', 'New')}\n </button>\n </div>\n {vibeSessions.length > 0 ? (\n <MotionList className=\"divide-y divide-border-primary\">\n {vibeSessions.map((session: SessionItem) => (\n <MotionItem key={session.id} variant=\"fadeRight\">\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => handleSessionClick(session.id))();\n }\n }}\n onClick={() => handleSessionClick(session.id)}\n className=\"p-4 flex items-center justify-between cursor-pointer hover:bg-bg-sunken/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <Terminal className=\"size-5 text-text-secondary flex-shrink-0\" />\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">\n {session.name}\n </p>\n <p className=\"text-sm text-text-secondary truncate\">{session.type}</p>\n </div>\n </div>\n <StatusBadge status={session.status} />\n </div>\n </MotionItem>\n ))}\n </MotionList>\n ) : (\n <div className=\"p-6 text-center text-text-secondary\">\n {tr('vibeDetails.noSessionsForVibe', 'No sessions for this vibe')}\n </div>\n )}\n </Card>\n </PluginGate>\n </motion.div>\n )}\n\n {/* Notes Tab */}\n {activeTab === 'notes' && isFeatureEnabled('notes') && (\n <motion.div\n key=\"notes\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <NotesTabPanel context=\"vibe\" vibeId={vibe.id} />\n </motion.div>\n )}\n\n {/* Docs Tab */}\n {activeTab === 'docs' && isFeatureEnabled('docs') && (\n <motion.div\n key=\"docs\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <DocsTabPanel vibeId={vibe.id} />\n </motion.div>\n )}\n\n {/* Scorecard Tab */}\n {activeTab === 'scorecard' && isFeatureEnabled('scorecard') && (\n <motion.div\n key=\"scorecard\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <ScorecardPanel ownerType=\"VIBE\" ownerId={vibe.id} />\n </motion.div>\n )}\n\n {/* Catalogs Tab */}\n {activeTab === 'catalogs' && isFeatureEnabled('catalogs') && (\n <motion.div\n key=\"catalogs\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <VibeCatalogsTab vibeId={vibe.id} tr={tr} navigate={navigate} />\n </motion.div>\n )}\n\n {/* GraphQL Playground Tab */}\n {activeTab === 'graphqlPlayground' && isFeatureEnabled('graphqlPlayground') && (\n <motion.div\n key=\"graphqlPlayground\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <PluginGate\n agentId={effectiveAgent?.id ?? null}\n required={[GRAPHQL_PLAYGROUND_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('vibeDetails.graphqlPluginEyebrow', 'GraphQL playground plugin required')}\n title={tr('vibeDetails.graphqlPluginTitle', 'Install the GraphQL playground')}\n description={tr(\n 'vibeDetails.graphqlPluginDescription',\n 'The GraphQL Playground is served by the graphiql agent plugin. Install it to continue.'\n )}\n >\n {effectiveTunnelUrl ? (\n <PlaygroundTab\n vibeId={vibe.id}\n agentId={effectiveAgent?.id ?? null}\n profile={effectiveProfile}\n tunnelUrl={effectiveTunnelUrl}\n type=\"graphql\"\n tr={tr}\n />\n ) : (\n <EmptyState\n icon={<Network className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noAgentTunnel', 'No agent tunnel available')}\n description={tr(\n 'vibeDetails.noAgentTunnelDesc2',\n 'The GraphQL playground requires an active agent with a tunnel URL.'\n )}\n />\n )}\n </PluginGate>\n </motion.div>\n )}\n\n {/* GitOps Tab */}\n {activeTab === 'gitops' && isFeatureEnabled('gitops') && (\n <VibeGitOpsTab\n vibeId={id || ''}\n agentId={effectiveAgent?.id ?? ''}\n profile={effectiveProfile}\n tunnelUrl={effectiveTunnelUrl}\n tr={tr}\n />\n )}\n\n {activeTab === 'security' && isFeatureEnabled('security') && (\n <motion.div\n key=\"security\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <SecurityTabPanel\n vibeId={id || ''}\n agentId={effectiveAgent?.id ?? null}\n profile={effectiveProfile ?? 'default'}\n />\n </motion.div>\n )}\n\n {/* Related Tab */}\n {activeTab === 'related' && isFeatureEnabled('related') && (\n <motion.div\n key=\"related\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n {relatedCount > 0 ? (\n <>\n {/* Child Vibes */}\n {childVibes.length > 0 && (\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Zap className=\"size-5\" />\n {tr('vibeDetails.childVibes', 'Child Vibes')} ({childVibes.length})\n </h3>\n </div>\n <MotionList className=\"divide-y divide-border-primary\">\n {childVibes.map((child: ChildVibeItem) => (\n <MotionItem key={child.id} variant=\"fadeRight\">\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => handleVibeClick(child.id))();\n }\n }}\n onClick={() => handleVibeClick(child.id)}\n className=\"p-4 flex items-center justify-between cursor-pointer hover:bg-bg-sunken/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <Zap className=\"size-5 text-text-secondary flex-shrink-0\" />\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">\n {child.name}\n </p>\n <p className=\"text-sm text-text-secondary truncate\">{child.type}</p>\n </div>\n </div>\n <StatusBadge status={child.status} />\n </div>\n </MotionItem>\n ))}\n </MotionList>\n </Card>\n )}\n\n {/* Parent Vibe */}\n {parentVibe && (\n <Card onClick={() => handleVibeClick(parentVibe.id)} hoverable>\n <div className=\"p-4 flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Zap className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.parentVibe', 'Parent Vibe')}\n </p>\n <p className=\"font-medium text-text-primary\">{parentVibe.name}</p>\n </div>\n </div>\n </Card>\n )}\n\n {/* Related Agent */}\n {agent && (\n <Card onClick={handleAgentClick} hoverable>\n <div className=\"p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.runningOnAgent', 'Running on Agent')}\n </p>\n <p className=\"font-medium text-text-primary\">{agent.name}</p>\n <p className=\"text-sm text-text-secondary\">{agent.hostname}</p>\n </div>\n </div>\n <StatusBadge status={agent.isActive ? 'ACTIVE' : 'OFFLINE'} />\n </div>\n </Card>\n )}\n </>\n ) : (\n <EmptyState\n icon={<Link2 className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noRelatedEntities', 'No related entities')}\n description={tr(\n 'vibeDetails.noRelatedEntitiesDescription',\n 'This vibe has no child vibes, parent vibe, or linked agent'\n )}\n />\n )}\n </motion.div>\n )}\n\n {/* LLM Context Tab — signed-URL feature, mount the panel verbatim */}\n {activeTab === 'llmContext' && (\n <motion.div\n key=\"llmContext\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <VibeLlmContextTabPanel\n vibeId={vibe.id}\n attachedAgents={attachedAgents.map((a: { id: string; name: string }) => ({\n id: a.id,\n name: a.name,\n }))}\n />\n </motion.div>\n )}\n\n {/* F10 round-3: linked-entity tabs. Backend may not yet expose\n `Vibe.linkedEntities.{type}` federation field. Until it does,\n each panel renders a \"Coming soon\" empty state with a \"+ Link\n existing\" CTA stub. Console-warn once per panel so the\n absence is visible in dev. */}\n {(activeTab === 'vibedecks' ||\n activeTab === 'actions' ||\n activeTab === 'scheduledJobs' ||\n activeTab === 'sandboxes' ||\n activeTab === 'aiPrompts' ||\n activeTab === 'aiContexts' ||\n activeTab === 'instructionFiles' ||\n activeTab === 'tunnels') && (\n <motion.div\n key={`linked-${activeTab}`}\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <VibeLinkedEntitiesPanel vibeId={vibe.id} kind={activeTab} />\n </motion.div>\n )}\n {/* iframe-mounted UI plugin tab body (PR-7). Renders when the\n active tab id matches a discovered contribution. The host\n translates a small set of capability-gated RPCs from the\n plugin via `onRpcRequest`; for now the host returns a\n \"method not implemented\" response. PR-10 wires real\n handlers (navigate, toast, storage, getContext, …). */}\n {enableIframeHost &&\n (() => {\n const active: DiscoveredContribution | undefined = iframeContributions.data.find(\n (c) => c.contribution.id === activeTab\n );\n if (!active) return null;\n const tunnel = active.tunnel;\n const restPaths =\n active.capabilities.restPaths.length > 0\n ? active.capabilities.restPaths\n : [`/ui/${active.pluginKey}`];\n return (\n <motion.div\n key={`plugin-${active.contribution.id}`}\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <Card>\n <div className=\"p-0 h-[600px]\">\n <PluginIframeMount\n contribution={active}\n tunnel={tunnel}\n restPaths={restPaths}\n context={{ vibeId: vibe.id }}\n // Canonical agent id — `tunnel.label` is a\n // display string and plugin RPC logic that\n // looks up the agent by id would miss with that.\n agentId={effectiveAgent?.id ?? ''}\n profile={tunnel.profile}\n theme={{ colorMode: 'light', tokens: {} }}\n locale=\"en\"\n featureFlags={effectiveVibeFlags?.featureFlags ?? {}}\n // Real version string surfaced via host:init so\n // plugin authors can do version-based feature\n // detection.\n hostVersion=\"1.0.0\"\n />\n </div>\n </Card>\n </motion.div>\n );\n })()}\n </AnimatePresence>\n\n {/* PR-10 \"+ Add tab\" dialog. Mounts off the AnimatePresence so\n opening it doesn't fight the tab-body transitions. */}\n {enableIframeHost && effectiveAgent && (\n <AddTabDialog\n isOpen={addTabOpen}\n onClose={() => setAddTabOpen(false)}\n mountPoint=\"vibe.detailTab\"\n agentId={effectiveAgent.id}\n profile={effectiveProfile ?? 'default'}\n onInstalled={() => {\n void iframeContributions.refetch();\n }}\n />\n )}\n\n {/* Create Session Modal */}\n {showSessionModal && (\n <div className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4\">\n <div className=\"bg-bg-surface rounded-lg p-6 w-full max-w-lg\">\n <h2 className=\"text-lg font-semibold text-text-primary mb-4\">\n {tr('vibeDetails.newSessionFor', 'New Session for')} {vibe.name}\n </h2>\n {sessionCreateError && (\n <div className=\"mb-4 p-3 bg-status-error-bg text-status-error-text rounded-md text-sm\">\n {sessionCreateError}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('vibeDetails.sessionName', 'Session Name')}\n </label>\n <input\n aria-label=\"control\"\n ref={sessionNameRef}\n type=\"text\"\n placeholder={tr('vibeDetails.sessionNamePlaceholder', 'dev-terminal')}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('sessions.filterType', 'Type')}\n </label>\n <select\n ref={sessionTypeRef}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary\"\n >\n <option value=\"TERMINAL\">{tr('sessions.typeTerminal', 'Terminal')}</option>\n <option value=\"TMUX\">{tr('sessions.typeTmux', 'TMUX')}</option>\n <option value=\"SSH\">{tr('sessions.typeSsh', 'SSH')}</option>\n <option value=\"SCRIPT\">{tr('sessions.typeScript', 'Script')}</option>\n <option value=\"CUSTOM\">{tr('sessions.typeCustom', 'Custom')}</option>\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('sessions.command', 'Command')}\n </label>\n <input\n aria-label=\"control\"\n ref={sessionCommandRef}\n type=\"text\"\n placeholder={tr('vibeDetails.sessionCommandPlaceholder', 'bash')}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('vibeDetails.workingDirectory', 'Working Directory')}\n </label>\n <input\n aria-label=\"control\"\n ref={sessionWorkDirRef}\n type=\"text\"\n defaultValue={vibe.path || ''}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary\"\n />\n </div>\n </div>\n <div className=\"flex flex-col sm:flex-row justify-end gap-3 mt-6\">\n <button\n type=\"button\"\n onClick={() => setShowSessionModal(false)}\n disabled={sessionCreateLoading}\n className=\"px-4 py-2 text-text-primary hover:bg-bg-sunken rounded-md transition-colors disabled:opacity-50\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleSubmitCreateSession}\n disabled={sessionCreateLoading}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {sessionCreateLoading\n ? tr('sessions.creating', 'Creating...')\n : tr('sessions.createSession', 'Create Session')}\n </button>\n </div>\n </div>\n </div>\n )}\n </MotionPage>\n );\n}\n\n/* ── Vibe Catalogs Tab ─────────────────────────────────────────────────── */\n\nfunction VibeCatalogsTab({\n vibeId,\n tr,\n navigate,\n}: {\n vibeId: string;\n tr: (key: string, fallback: string) => string;\n navigate: (path: string) => void;\n}) {\n const [showAddDialog, setShowAddDialog] = useState(false);\n\n const { data, loading, refetch } = useGetVibeCatalogsQuery({ fetchPolicy: 'cache-and-network' });\n\n const [addVibeToCatalog] = useAddVibeToCatalogMutation({\n refetchQueries: ['GetVibeCatalogs'],\n });\n const [removeVibeFromCatalog] = useRemoveVibeFromCatalogMutation({\n refetchQueries: ['GetVibeCatalogs'],\n });\n\n const allCatalogs = data?.vibecontrolsCatalogs?.items ?? [];\n\n const memberCatalogs = allCatalogs.filter(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (cat: any) => cat.vibes?.some((v: any) => v.vibeId === vibeId)\n );\n\n const availableCatalogs = allCatalogs.filter(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (cat: any) => !cat.vibes?.some((v: any) => v.vibeId === vibeId)\n );\n\n const handleAdd = async (catalogId: string) => {\n await addVibeToCatalog({ variables: { catalogId, vibeId } });\n await refetch();\n setShowAddDialog(false);\n };\n\n const handleRemove = async (catalogId: string) => {\n await removeVibeFromCatalog({ variables: { catalogId, vibeId } });\n await refetch();\n };\n\n if (loading && allCatalogs.length === 0) {\n return <PageSkeleton variant=\"detail\" />;\n }\n\n return (\n <>\n <div className=\"flex items-center justify-between\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Library className=\"size-5\" />\n {tr('vibeDetails.catalogsTitle', 'Catalogs')}\n <span className=\"text-sm font-normal text-text-secondary\">({memberCatalogs.length})</span>\n </h3>\n <button\n type=\"button\"\n onClick={() => setShowAddDialog(true)}\n className=\"flex items-center gap-2 px-3 py-2 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('vibeDetails.addToCatalog', 'Add to Catalog')}\n </button>\n </div>\n\n {memberCatalogs.length > 0 ? (\n <div className=\"space-y-2\">\n {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}\n {memberCatalogs.map((cat: any) => (\n <Card key={cat.id}>\n <div className=\"p-3 flex items-center justify-between\">\n <button\n type=\"button\"\n onClick={() => navigate(`/catalogs/${cat.id}`)}\n className=\"flex items-center gap-3 text-left hover:opacity-80 transition-opacity\"\n >\n <Library className=\"size-5 text-action-primary-bg flex-shrink-0\" />\n <div>\n <p className=\"font-medium text-text-primary text-sm\">{cat.name}</p>\n <p className=\"text-xs text-text-secondary\">{cat.visibility}</p>\n </div>\n </button>\n <button\n type=\"button\"\n onClick={() => handleRemove(cat.id)}\n className=\"p-1.5 text-text-muted hover:text-status-error-text hover:bg-status-error-bg rounded-md transition-colors\"\n title={tr('vibeDetails.removeFromCatalog', 'Remove from catalog')}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n </Card>\n ))}\n </div>\n ) : (\n <EmptyState\n icon={<Library className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noCatalogs', 'Not in any catalogs')}\n description={tr(\n 'vibeDetails.noCatalogsDesc',\n 'Add this vibe to catalogs to organize and track it alongside other components'\n )}\n />\n )}\n\n {/* Add to Catalog Dialog */}\n {showAddDialog && (\n <div className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4\">\n <motion.div\n initial={{ opacity: 0, scale: 0.95 }}\n animate={{ opacity: 1, scale: 1 }}\n className=\"bg-bg-surface rounded-lg w-full max-w-md max-h-[60vh] flex flex-col\"\n >\n <div className=\"p-4 border-b border-border-default\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('vibeDetails.addToCatalogTitle', 'Add Vibe to Catalog')}\n </h2>\n </div>\n <div className=\"flex-1 overflow-y-auto p-2\">\n {availableCatalogs.length === 0 ? (\n <div className=\"p-4 text-center text-text-secondary text-sm\">\n {tr(\n 'vibeDetails.noAvailableCatalogs',\n 'No available catalogs. This vibe is already in all catalogs.'\n )}\n </div>\n ) : (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n availableCatalogs.map((cat: any) => (\n <button\n type=\"button\"\n key={cat.id}\n onClick={() => handleAdd(cat.id)}\n className=\"w-full flex items-center justify-between p-3 rounded-md hover:bg-bg-sunken transition-colors text-left\"\n >\n <div>\n <p className=\"font-medium text-text-primary text-sm\">{cat.name}</p>\n <p className=\"text-xs text-text-secondary\">{cat.visibility}</p>\n </div>\n <Plus className=\"size-4 text-text-secondary\" />\n </button>\n ))\n )}\n </div>\n <div className=\"p-4 border-t border-border-default\">\n <button\n type=\"button\"\n onClick={() => setShowAddDialog(false)}\n className=\"w-full px-4 py-2 text-text-primary hover:bg-bg-sunken rounded-md transition-colors\"\n >\n {tr('common.close', 'Close')}\n </button>\n </div>\n </motion.div>\n </div>\n )}\n </>\n );\n}\n\n/* ── Playground Tab (REST / GraphQL) ───────────────────────────────────── */\n\nfunction PlaygroundTab({\n vibeId,\n agentId,\n profile,\n tunnelUrl,\n type,\n tr,\n}: {\n vibeId: string;\n agentId: string | null;\n profile: string;\n tunnelUrl: string;\n type: 'rest' | 'graphql';\n tr: (key: string, fallback: string) => string;\n}) {\n const isRest = type === 'rest';\n const pluginPath = 'graphiql';\n\n const [endpointUrl, setEndpointUrl] = useState('');\n const [configLoaded, setConfigLoaded] = useState(false);\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n const [iframeKey, setIframeKey] = useState(0);\n\n // F4 Wave C — read playground config via svc GraphQL proxy (no more\n // direct browser fetch with x-agent-api-key).\n const { data: configQueryData, loading: configQueryLoading } = useAgentPlaygroundConfigQuery({\n variables: { agentId: agentId ?? '', profile, vibeId },\n skip: !agentId,\n fetchPolicy: 'network-only',\n });\n\n useEffect(() => {\n if (configQueryLoading) return;\n const payload = configQueryData?.agentPlaygroundConfig;\n if (payload?.ok && payload.data && typeof payload.data === 'object') {\n const obj = payload.data as Record<string, unknown>;\n const candidate = isRest ? (obj.openapiUrl ?? obj.url) : (obj.graphqlUrl ?? obj.url);\n if (typeof candidate === 'string' && candidate) setEndpointUrl(candidate);\n }\n setConfigLoaded(true);\n }, [configQueryData, configQueryLoading, isRest]);\n\n const [setAgentPlaygroundConfig] = useSetAgentPlaygroundConfigMutation();\n\n const handleSave = async () => {\n if (!endpointUrl.trim() || !agentId) return;\n setSaving(true);\n setSaved(false);\n try {\n await setAgentPlaygroundConfig({\n variables: {\n agentId,\n profile,\n vibeId,\n openapiUrl: isRest ? endpointUrl.trim() : null,\n graphqlUrl: isRest ? null : endpointUrl.trim(),\n },\n });\n setSaved(true);\n setIframeKey((k) => k + 1); // reload iframe\n setTimeout(() => setSaved(false), 3000);\n } catch {\n // silently fail\n } finally {\n setSaving(false);\n }\n };\n\n // Iframe-token (Phase C): replaces the previous ?apiKey= URL param.\n // Token TTL <= 600s; refresh ~60s before expiry.\n const [iframeToken, setIframeToken] = useState<IframeTokenResponse | null>(null);\n const [tokenError, setTokenError] = useState<string | null>(null);\n // 3 prefixes the iframe hits: /ui/<plugin> (HTML), the profile-scoped config call, and the bare /api/<plugin> the agent rewrites profile-scoped calls to (so the token must cover it). `profile` encoded to match the request path (configUrl/agentApiPath also encode it).\n const iframeTokenPrefixes = useMemo(\n () => [\n `/ui/${pluginPath}`,\n `/api/profiles/${encodeURIComponent(profile)}/${pluginPath}`,\n `/api/${pluginPath}`,\n ],\n [profile, pluginPath]\n );\n\n useEffect(() => {\n if (!agentId) return;\n let cancelled = false;\n let handle: IframeTokenAutoRefreshHandle | null = null;\n\n const onRefresh = (next: IframeTokenResponse) => {\n if (cancelled) return;\n setIframeToken(next);\n handle = scheduleScopedIframeTokenRefresh(\n agentId,\n profile,\n iframeTokenPrefixes,\n next,\n onRefresh,\n (err) => setTokenError(err.message)\n );\n };\n\n mintScopedIframeToken(agentId, profile, iframeTokenPrefixes)\n .then((tok) => {\n if (cancelled) return;\n setIframeToken(tok);\n handle = scheduleScopedIframeTokenRefresh(\n agentId,\n profile,\n iframeTokenPrefixes,\n tok,\n onRefresh,\n (err) => setTokenError(err.message)\n );\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n setTokenError(err instanceof Error ? err.message : String(err));\n });\n\n return () => {\n cancelled = true;\n handle?.cancel();\n };\n }, [agentId, profile, iframeTokenPrefixes]);\n\n // Pass the owning profile so the iframe builds profile-scoped API URLs.\n const iframeBaseUrl = `${tunnelUrl}/ui/${pluginPath}?vibeId=${vibeId}&profile=${encodeURIComponent(profile)}`;\n // F5 — iframe token rides the URL fragment, not the query string. The\n // agent's bootstrap shim at /ui/<plugin> reads `#vt=` client-side and\n // trades it for a path-scoped HttpOnly cookie via POST\n // /api/agent/ui-exchange.\n const iframeSrc = iframeToken\n ? appendIframeTokenFragment(iframeBaseUrl, iframeToken.token)\n : iframeBaseUrl;\n\n return (\n <div className=\"space-y-4\">\n {/* Config Bar */}\n <Card>\n <div className=\"p-4\">\n <div className=\"flex items-center gap-2 mb-2\">\n {isRest ? (\n <Globe className=\"size-5 text-text-secondary\" />\n ) : (\n <Network className=\"size-5 text-text-secondary\" />\n )}\n <h3 className=\"font-semibold text-text-primary\">\n {isRest\n ? tr('vibeDetails.restPlaygroundTitle', 'REST API Playground')\n : tr('vibeDetails.graphqlPlaygroundTitle', 'GraphQL Playground')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n <input\n aria-label=\"control\"\n type=\"url\"\n value={endpointUrl}\n onChange={(e) => setEndpointUrl(e.target.value)}\n placeholder={\n isRest\n ? tr(\n 'vibeDetails.enterSwaggerUrl',\n 'Enter OpenAPI/Swagger URL (e.g. https://petstore3.swagger.io/api/v3/openapi.json)'\n )\n : tr(\n 'vibeDetails.enterGraphqlUrl',\n 'Enter GraphQL endpoint URL (e.g. https://countries.trevorblades.com/graphql)'\n )\n }\n className=\"flex-1 px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder text-sm\"\n onKeyDown={(e) => e.key === 'Enter' && handleSave()}\n />\n <button\n type=\"button\"\n onClick={handleSave}\n disabled={saving || !endpointUrl.trim()}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 text-sm whitespace-nowrap\"\n >\n {saving ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : saved ? (\n <Check className=\"size-4\" />\n ) : (\n <ExternalLinkIcon className=\"size-4\" />\n )}\n {saving\n ? tr('common.saving', 'Saving...')\n : saved\n ? tr('common.saved', 'Saved!')\n : tr('vibeDetails.loadApi', 'Load API')}\n </button>\n </div>\n {!configLoaded && (\n <p className=\"text-xs text-text-muted mt-1\">\n {tr('vibeDetails.loadingConfig', 'Loading saved configuration...')}\n </p>\n )}\n </div>\n </Card>\n\n {/* Iframe (gated on iframe-token mint when agentId is present) */}\n {agentId && !iframeToken ? (\n <div className=\"p-8 text-center text-sm text-text-secondary rounded-md border border-border-default bg-bg-sunken\">\n {tokenError\n ? tr('vibeDetails.iframeTokenError', `Iframe token error: ${tokenError}`)\n : tr('vibeDetails.mintingIframeToken', 'Authorizing iframe…')}\n </div>\n ) : (\n <iframe\n key={iframeKey}\n src={iframeSrc}\n title={isRest ? 'REST API Playground' : 'GraphQL Playground'}\n className=\"w-full rounded-md border border-border-default bg-bg-sunken\"\n style={{ height: '700px' }}\n sandbox=\"allow-scripts allow-same-origin allow-forms allow-popups\"\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"VibeDetailsPage.js","names":[],"sources":["../../src/pages/VibeDetailsPage.tsx"],"sourcesContent":["import { useState, useRef, useEffect, useCallback, useMemo } from 'react';\nimport { EntityTagPicker } from '@/components/shared/EntityTagPicker';\nimport { PageSkeleton } from '@/components/shared/PageSkeleton';\nimport { useParams } from 'react-router-dom';\nimport { AnimatePresence, motion } from 'framer-motion';\nimport { useVibeControlsStore } from '@/store/vibeControlsStore';\nimport {\n ArrowLeft,\n Zap,\n Play,\n Pause,\n Archive,\n ExternalLink,\n Server,\n Terminal,\n GitBranch,\n Folder,\n Variable,\n Tag,\n Clock,\n Plus,\n RefreshCw,\n Info,\n Settings,\n Link2,\n FileText,\n BookOpen,\n BarChart3,\n Globe,\n Network,\n GitMerge,\n Library,\n X,\n Check,\n ExternalLink as ExternalLinkIcon,\n Loader2,\n Shield,\n MoreVertical,\n Bot,\n} from 'lucide-react';\nimport {\n PageHeader,\n Card,\n StatusBadge,\n EmptyState,\n ErrorState,\n PluginGate,\n} from '@/components/shared';\nimport {\n GIT_PACKAGE,\n GRAPHQL_PLAYGROUND_PACKAGE,\n SESSION_MANAGER_PACKAGE,\n SESSION_PROVIDER_PACKAGES,\n} from '@/constants/pluginCatalog';\nimport { MotionPage, MotionList, MotionItem } from '@burdenoff/fe-libs/shared/motion';\nimport { VibeOpenInEditorMenu } from '@/components/agents';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport { NotesTabPanel } from '@/components/notes';\nimport { DocsTabPanel } from '@/components/docs';\nimport { ScorecardPanel } from '@/components/scorecard';\nimport { PermissionButton } from '@burdenoff/fe-libs/shared/components';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { VIBE_PERMISSIONS } from '@/constants/permissions';\nimport { useVibeControls, useVibeNavigate } from '@/providers/VibeControlsProvider';\nimport { useQueryBackedTab } from '@/hooks/useQueryBackedTab';\nimport {\n useGetVibeQuery,\n useUpdateVibeStatusMutation,\n useArchiveVibeMutation,\n useCreateSessionMutation,\n useGetVibeCatalogsQuery,\n useAddVibeToCatalogMutation,\n useRemoveVibeFromCatalogMutation,\n useGetVibeFeatureCatalogQuery,\n useGetVibeFeatureFlagsQuery,\n useSetVibeFeatureFlagMutation,\n useAgentPlaygroundConfigQuery,\n useSetAgentPlaygroundConfigMutation,\n} from '@/generated/wspace-operations';\nimport { VibeGitOpsTab } from '@/components/gitops/VibeGitOpsTab';\nimport { VibeAgentEmptyState } from '@/components/vibe/VibeAgentEmptyState';\nimport { VibeAgentSwitcher } from '@/components/vibe/VibeAgentSwitcher';\nimport {\n DEFAULT_VIBE_FEATURE_CATALOG,\n isVibeFeatureEnabled,\n type VibeFeatureDefinition,\n} from '@/lib/vibeFeatures';\nimport { useFeatureFlagDetails } from '@burdenoff/fe-libs/shared/feature-flags';\nimport {\n PluginIframe,\n type DiscoveredContribution,\n type MountTunnel,\n usePluginContributions,\n} from '@burdenoff/fe-libs/shared/plugins';\nimport {\n mintScopedIframeToken,\n scheduleScopedIframeTokenRefresh,\n appendIframeTokenFragment,\n type IframeTokenAutoRefreshHandle,\n type IframeTokenResponse,\n} from '@/services/iframeToken';\nimport { AddTabDialog } from '@/components/plugins/AddTabDialog';\nimport { PluginIframeMount } from '@/components/plugins/PluginIframeMount';\nimport { SecurityTabPanel } from '@/components/security';\nimport { VibeLlmContextTabPanel } from '@/components/vibe/VibeLlmContextTabPanel';\nimport { VibeLinkedEntitiesPanel } from '@/components/vibe/VibeLinkedEntitiesPanel';\n\n// `PluginIframeTabBody` was the per-page helper that minted a token,\n// rendered the iframe, and lacked refresh + error handling. PR-86's\n// review surfaced the missing refresh / loading-stuck-on-error\n// problems; the consolidated logic now lives in\n// `@/components/plugins/PluginIframeMount` and is consumed by every\n// surface that mounts an iframe-served plugin (Vibe + Agent detail\n// pages and the standalone route).\n\ntype VibeTab =\n | 'overview'\n | 'config'\n | 'git'\n | 'sessions'\n | 'notes'\n | 'docs'\n | 'scorecard'\n | 'catalogs'\n | 'graphqlPlayground'\n | 'gitops'\n | 'security'\n | 'related'\n | 'llmContext'\n // F10 round-3: linked-entity tabs. Each surfaces the vibe's\n // attached vibedecks / actions / scheduledJobs / sandboxes /\n // aiPrompts / tunnels. Backend may not yet expose the\n // `Vibe.linkedEntities` federation field; in that case the panel\n // renders a \"Coming soon\" placeholder.\n | 'vibedecks'\n | 'actions'\n | 'scheduledJobs'\n | 'sandboxes'\n | 'aiPrompts'\n | 'aiContexts'\n | 'instructionFiles'\n | 'tunnels';\n\nconst VIBE_TABS: readonly VibeTab[] = [\n 'overview',\n 'config',\n 'git',\n 'sessions',\n 'notes',\n 'docs',\n 'scorecard',\n 'catalogs',\n 'graphqlPlayground',\n 'gitops',\n 'security',\n 'related',\n 'llmContext',\n 'vibedecks',\n 'actions',\n 'scheduledJobs',\n 'sandboxes',\n 'aiPrompts',\n 'aiContexts',\n 'instructionFiles',\n 'tunnels',\n];\n\n// Tabs that are always visible regardless of per-vibe feature flags.\nconst ALWAYS_VISIBLE_TABS: ReadonlySet<VibeTab> = new Set<VibeTab>(['overview', 'config']);\nconst HIDDEN_VIBE_FEATURE_IDS = new Set<string>(['apiPlayground']);\n// Tabs whose content can only be rendered against an attached agent.\nconst AGENT_REQUIRED_TABS: ReadonlySet<VibeTab> = new Set<VibeTab>([\n 'sessions',\n 'gitops',\n 'graphqlPlayground',\n 'git',\n 'security',\n]);\n\n/**\n * Vibe details page - shows comprehensive information about a single vibe\n */\nexport function VibeDetailsPage() {\n const { t } = useI18n();\n const { id } = useParams<{ id: string }>();\n const { basePath: basePathProp } = useVibeControls();\n const basePath = basePathProp || '';\n const navigate = useVibeNavigate();\n\n const tr = useCallback(\n (key: string, fallback: string): string => {\n const value = t(key);\n return value === key ? fallback : value;\n },\n [t]\n );\n\n // Active-tab state is declared AFTER `iframeContributions` (further\n // down in this component) so the valid-id set can include both the\n // static `VIBE_TABS` AND the discovered plugin contribution ids.\n // Forward-declaration placeholder so downstream code can reference\n // the binding via the actual hook call below.\n\n const [updateVibeStatus] = useUpdateVibeStatusMutation({ refetchQueries: ['GetVibe'] });\n const [archiveVibe] = useArchiveVibeMutation({ refetchQueries: ['GetVibe'] });\n const [createSession] = useCreateSessionMutation({ refetchQueries: ['GetVibe', 'ListSessions'] });\n\n // Per-vibe feature catalog (drives the toggles in the Config tab and the\n // tab visibility filter below). Falls back to the static catalog if the\n // query is still loading or the backend hasn't shipped this field yet.\n const { data: featureCatalogData } = useGetVibeFeatureCatalogQuery({\n fetchPolicy: 'cache-first',\n errorPolicy: 'ignore',\n });\n const rawFeatureCatalog: readonly VibeFeatureDefinition[] = useMemo(() => {\n const merged = new Map<string, VibeFeatureDefinition>(\n DEFAULT_VIBE_FEATURE_CATALOG.map((feature) => [feature.id, feature])\n );\n\n for (const feature of featureCatalogData?.vibeFeatureCatalog ?? []) {\n merged.set(feature.id, feature);\n }\n\n return Array.from(merged.values());\n }, [featureCatalogData?.vibeFeatureCatalog]);\n const featureCatalog: readonly VibeFeatureDefinition[] = useMemo(\n () => rawFeatureCatalog.filter((feature) => !HIDDEN_VIBE_FEATURE_IDS.has(feature.id)),\n [rawFeatureCatalog]\n );\n\n const [setVibeFeatureFlagMutation, { loading: setFeatureFlagLoading }] =\n useSetVibeFeatureFlagMutation({ refetchQueries: ['GetVibe'] });\n\n // Session creation modal state\n const [showSessionModal, setShowSessionModal] = useState(false);\n const [sessionCreateError, setSessionCreateError] = useState<string | null>(null);\n const [sessionCreateLoading, setSessionCreateLoading] = useState(false);\n const [successMessage, setSuccessMessage] = useState<string | null>(null);\n\n // Session form refs\n const sessionNameRef = useRef<HTMLInputElement>(null);\n const sessionTypeRef = useRef<HTMLSelectElement>(null);\n const sessionCommandRef = useRef<HTMLInputElement>(null);\n const sessionWorkDirRef = useRef<HTMLInputElement>(null);\n\n // Fetch vibe by ID from GraphQL API\n const { data, loading, error, refetch } = useGetVibeQuery({\n variables: { id: id || '' },\n skip: !id,\n fetchPolicy: 'cache-and-network',\n });\n\n // Get vibe from response\n const vibe = data?.vibe;\n const { data: featureFlagsData } = useGetVibeFeatureFlagsQuery({\n variables: { id: id || '' },\n skip: !id,\n fetchPolicy: 'cache-first',\n errorPolicy: 'ignore',\n });\n // Feature flags live on the Vibe but may not be reflected in the generated\n // types until `bun run codegen` is re-run after the schema update — read\n // them through an untyped view so this file stays compilable in either state.\n const vibeFlagsView = vibe as\n | (typeof vibe & {\n featureFlags?: Record<string, boolean> | null;\n effectiveFeatureFlags?: Record<string, boolean> | null;\n })\n | undefined;\n const effectiveVibeFlags = featureFlagsData?.vibe ?? vibeFlagsView;\n\n // Per-vibe feature enablement check. Hoisted to the top of the component\n // (before any early-return guards) to keep React hook ordering stable.\n const isFeatureEnabled = useCallback(\n (featureId: string) =>\n isVibeFeatureEnabled(effectiveVibeFlags ?? null, featureId, featureCatalog),\n [effectiveVibeFlags, featureCatalog]\n );\n\n // Platform-level tab gates (component-level feature flags). Missing flag details\n // fail open so platform flag fetch gaps do not hide per-vibe enabled tabs.\n const gitTabDetails = useFeatureFlagDetails('vibes.tabs.git');\n const scorecardTabDetails = useFeatureFlagDetails('vibes.tabs.scorecard');\n const graphqlPlaygroundTabDetails = useFeatureFlagDetails('vibes.tabs.graphql-playground');\n // PR-12 migration flags. When on, the inline tab is filtered out\n // and the corresponding iframe contribution from\n // `vibe-plugin-tool-git` / `vibe-plugin-tool-graphiql` takes its\n // slot in the strip. Both flags default to OFF — the iframe\n // implementations have to prove parity in alpha before promotion.\n const gitIframeMigrationFlag = useFeatureFlagDetails('vibes.tabs.git-iframe');\n const graphiqlIframeMigrationFlag = useFeatureFlagDetails('vibes.tabs.graphiql-iframe');\n const gitopsIframeMigrationFlag = useFeatureFlagDetails('vibes.tabs.gitops-iframe');\n\n // Returns false for platform-gated tabs when the flag is off; true for all other tabs.\n const isPlatformTabEnabled = useCallback(\n (tabId: string): boolean => {\n switch (tabId) {\n case 'git':\n return gitTabDetails ? Boolean(gitTabDetails.value) : true;\n case 'scorecard':\n return scorecardTabDetails ? Boolean(scorecardTabDetails.value) : true;\n case 'graphqlPlayground':\n return graphqlPlaygroundTabDetails ? Boolean(graphqlPlaygroundTabDetails.value) : true;\n default:\n return true; // No platform gate for other tabs\n }\n },\n [gitTabDetails, scorecardTabDetails, graphqlPlaygroundTabDetails]\n );\n\n // Get related data from the vibe response (already included in the query).\n // Multi-agent: a vibe carries zero, one, or many attached agents. The\n // \"effective agent\" for THIS user on THIS vibe is picked here using the\n // store's per-vibe persisted choice plus the fallback chain (own host →\n // own dev VM → first shared → first usable).\n const attachedAgents = vibe?.agents ?? [];\n const vibeSessions = vibe?.sessions || [];\n const childVibes = vibe?.children || [];\n const parentVibe = vibe?.parent || null;\n\n // Per-Vibe default agent persisted in vibeControlsStore (write-through\n // cache against wspace-vibecontrols-svc UserPreferences.defaultAgentForVibe).\n // The user explicitly \"Set as default for this Vibe\" via the agent picker\n // and the choice follows them across machines once backend hydration\n // wires up (PR-7 ships the local cache; PR-10 / future ties in hydration).\n const defaultAgentIdForThisVibe = useVibeControlsStore((s) =>\n id ? s.defaultAgentForVibe?.[id] : undefined\n );\n\n // Auto-pick logic: explicit per-Vibe default wins; otherwise when exactly\n // one usable agent is attached, select it. When multiple, fall back to\n // the first agent with a tunnel URL. Personal agents not owned by the\n // current user are filtered out so we never accidentally route an action\n // through a teammate's laptop.\n const effectiveAgent =\n (defaultAgentIdForThisVibe\n ? (attachedAgents.find(\n (a: (typeof attachedAgents)[number]) => a.id === defaultAgentIdForThisVibe\n ) ?? null)\n : null) ??\n (attachedAgents.length === 1\n ? attachedAgents[0]\n : (attachedAgents.find((a: (typeof attachedAgents)[number]) => Boolean(a.tunnelUrl)) ??\n attachedAgents[0] ??\n null));\n // Back-compat alias for the legacy single-agent code paths below; downstream\n // sections still reference `agent` while we incrementally migrate to\n // `effectiveAgent` + the multi-agent picker UX.\n const agent = effectiveAgent;\n const effectiveTunnelUrl: string | null = effectiveAgent?.tunnelUrl ?? null;\n const { active: effectiveProfile } = useActiveAgentProfile(effectiveAgent?.id ?? null);\n\n // ── iframe-mount discovery ───────────────────────────────────────────\n // Gated by the platform feature flag `vibes.tabs.iframe-host`. When\n // off, the inline tabs render exactly as before with zero additional\n // behaviour. When on, the host fetches contributions from the\n // effective agent's tunnel and renders them as ADDITIONAL tabs in the\n // strip (alongside the inline ones). PR-12abc later replaces the\n // inline Git / GraphiQL / Ungit tabs with their iframe equivalents.\n // Hooks live HERE (above all early returns) so the React Rules of\n // Hooks order stays stable across renders.\n const iframeHostFlag = useFeatureFlagDetails('vibes.tabs.iframe-host');\n const enableIframeHost = iframeHostFlag?.value === true;\n\n const iframePluginTunnels = useMemo<MountTunnel[]>(() => {\n if (!enableIframeHost || !effectiveAgent?.tunnelUrl) return [];\n const agentIdForToken = effectiveAgent.id;\n const profile = effectiveProfile ?? 'default';\n return [\n {\n tunnelUrl: effectiveAgent.tunnelUrl,\n profile,\n label: effectiveAgent.name ?? effectiveAgent.id,\n mintIframeToken: async ({ pathPrefixes }) => {\n return mintScopedIframeToken(agentIdForToken, profile, pathPrefixes);\n },\n },\n ];\n }, [enableIframeHost, effectiveAgent, effectiveProfile]);\n\n const iframeContributions = usePluginContributions(iframePluginTunnels, {\n mountPoint: 'vibe.detailTab',\n enabled: enableIframeHost && iframePluginTunnels.length > 0,\n });\n\n // Active-tab state: the valid id set is the static `VIBE_TABS` UNION\n // the discovered plugin contribution ids. The dynamic ids are merged\n // in here so `?tab=plugin:hello` deep-links survive a reload —\n // `useQueryBackedTab` rejects unknown ids and falls back to default,\n // so a plugin id missing from this set would silently land on\n // `'overview'`. The hook re-runs every render so adding ids as\n // contributions arrive is a no-op for the activeTab transition.\n const validTabIds = useMemo<readonly string[]>(() => {\n const fromContribs = iframeContributions.data\n .filter((c) => c.contribution.mountPoint === 'vibe.detailTab')\n .map((c) => c.contribution.id);\n return [...(VIBE_TABS as readonly string[]), ...fromContribs];\n }, [iframeContributions.data]);\n const [activeTab, setActiveTab] = useQueryBackedTab<string>(validTabIds, 'overview');\n\n // \"+ Add tab\" picker open state. The dialog itself queries the\n // agent for installable contributions and triggers `refetch` once\n // an install completes so the new tab appears without a full reload.\n const [addTabOpen, setAddTabOpen] = useState(false);\n\n // ── Sync active context ────────────────────────────────────────────────\n useEffect(() => {\n if (vibe) {\n useVibeControlsStore.setState({\n activeAgentId: effectiveAgent?.id ?? null,\n selectedAgentId: effectiveAgent?.id ?? null,\n activeVibeId: vibe.id,\n selectedVibeId: vibe.id,\n activeSessionId: null,\n selectedSessionId: null,\n });\n }\n // Only sync when vibe or effective-agent identity changes, not on every field update.\n // useVibeControlsStore.setState is a stable Zustand reference.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [vibe?.id, effectiveAgent?.id]);\n\n // If the currently selected tab is for a feature that just got disabled\n // (either per-vibe, platform-level, or because the vibe has no attached\n // agent), bounce back to overview so the user is not stranded on an empty\n // panel. Multi-agent: gate on attachedAgents.length, not the legacy\n // singular vibe.agentId.\n const hasAttachedAgent = attachedAgents.length > 0;\n useEffect(() => {\n // Don't bounce while data is still loading — `vibe?.agents` may briefly\n // be empty during cache-and-network revalidation and we don't want to\n // snap an explicitly-selected tab back to overview just because the\n // refetch hasn't landed yet. The panel body shows its own empty state\n // when no agent is attached, so leaving the URL intact is correct.\n if (loading) return;\n if (ALWAYS_VISIBLE_TABS.has(activeTab as VibeTab)) return;\n if (!isFeatureEnabled(activeTab) || !isPlatformTabEnabled(activeTab)) {\n setActiveTab('overview');\n return;\n }\n // Previously bounced to overview when no agent attached; that broke\n // deep links and made tab clicks appear to \"do nothing\". Render the\n // empty state inside the panel instead.\n }, [activeTab, isFeatureEnabled, isPlatformTabEnabled, hasAttachedAgent, setActiveTab, loading]);\n\n const handleBack = () => {\n navigate('/vibes');\n };\n\n const handleActivateVibe = async () => {\n if (!vibe?.id) return;\n try {\n await updateVibeStatus({ variables: { id: vibe.id, status: 'ACTIVE' as const } });\n } catch (err) {\n console.error('Failed to activate vibe:', err);\n }\n };\n\n const handleDeactivateVibe = async () => {\n if (!vibe?.id) return;\n try {\n await updateVibeStatus({ variables: { id: vibe.id, status: 'INACTIVE' as const } });\n } catch (err) {\n console.error('Failed to deactivate vibe:', err);\n }\n };\n\n const [confirmArchive, setConfirmArchive] = useState(false);\n const [moreMenuOpen, setMoreMenuOpen] = useState(false);\n const moreMenuRef = useRef<HTMLDivElement | null>(null);\n\n useEffect(() => {\n if (!moreMenuOpen) return;\n const onDocClick = (e: MouseEvent) => {\n if (!moreMenuRef.current?.contains(e.target as Node)) setMoreMenuOpen(false);\n };\n document.addEventListener('mousedown', onDocClick);\n return () => document.removeEventListener('mousedown', onDocClick);\n }, [moreMenuOpen]);\n\n const handleArchiveVibe = async () => {\n if (!vibe?.id) return;\n try {\n await archiveVibe({ variables: { id: vibe.id } });\n setConfirmArchive(false);\n } catch (err) {\n console.error('Failed to archive vibe:', err);\n }\n };\n\n const handleStartSession = () => {\n setSessionCreateError(null);\n setShowSessionModal(true);\n };\n\n const handleSubmitCreateSession = async () => {\n const name = sessionNameRef.current?.value?.trim();\n const type = sessionTypeRef.current?.value as 'TERMINAL' | 'TMUX' | 'SSH' | 'SCRIPT' | 'CUSTOM';\n const command = sessionCommandRef.current?.value?.trim() || undefined;\n const workingDirectory = sessionWorkDirRef.current?.value?.trim() || undefined;\n\n if (!name) {\n setSessionCreateError(tr('vibeDetails.sessionNameRequired', 'Session name is required'));\n return;\n }\n\n // Use the effective agent already resolved from vibe.agents (multi-agent aware).\n const agentId = effectiveAgent?.id;\n if (!agentId) {\n setSessionCreateError(\n attachedAgents.length === 0\n ? tr(\n 'vibeDetails.noAgentAttached',\n 'This vibe has no agent attached. Connect an agent to this vibe before creating a session.'\n )\n : tr(\n 'vibeDetails.selectAgent',\n 'Select an agent for this vibe before creating a session.'\n )\n );\n return;\n }\n\n setSessionCreateLoading(true);\n setSessionCreateError(null);\n try {\n await createSession({\n variables: {\n input: {\n name,\n type: type || 'TERMINAL',\n agentId,\n vibeId: vibe?.id,\n command,\n workingDirectory: workingDirectory || vibe?.path || undefined,\n },\n },\n });\n setShowSessionModal(false);\n // Clear form refs to prevent stale values on next open\n if (sessionNameRef.current) sessionNameRef.current.value = '';\n if (sessionTypeRef.current) sessionTypeRef.current.selectedIndex = 0;\n if (sessionCommandRef.current) sessionCommandRef.current.value = '';\n if (sessionWorkDirRef.current) sessionWorkDirRef.current.value = '';\n setSuccessMessage(tr('sessions.createdSuccess', 'Session created successfully'));\n setTimeout(() => setSuccessMessage(null), 3000);\n } catch (err) {\n setSessionCreateError(\n err instanceof Error ? err.message : tr('sessions.failedCreate', 'Failed to create session')\n );\n } finally {\n setSessionCreateLoading(false);\n }\n };\n\n const handleAgentClick = () => {\n if (!agent) return;\n navigate(`/agents/${agent.id}`);\n };\n\n const handleSessionClick = (sessionId: string) => {\n navigate(`/sessions/${sessionId}`);\n };\n\n const handleVibeClick = (vibeId: string) => {\n navigate(`/vibes/${vibeId}`);\n };\n\n const formatTimestamp = (timestamp: string | null | undefined) => {\n if (!timestamp) return tr('health.never', 'Never');\n const diff = Date.now() - new Date(timestamp).getTime();\n if (diff < 60000) return tr('health.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 new Date(timestamp).toLocaleDateString();\n };\n\n const getGitStatusColor = (status: string | null | undefined) => {\n switch (status) {\n case 'clean':\n return 'text-status-success-text';\n case 'modified':\n return 'text-status-warning-text';\n case 'ahead':\n return 'text-status-info-text';\n default:\n return 'text-text-secondary';\n }\n };\n\n // Guard states wrapped in MotionPage to prevent unmount/remount animation replay\n if (loading && !vibe) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n <PageSkeleton variant=\"detail\" />\n </MotionPage>\n );\n }\n\n if (error && !vibe) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n <ErrorState\n title={tr('vibeDetails.failedLoad', 'Failed to load vibe')}\n message={error.message}\n onRetry={() => refetch()}\n />\n </MotionPage>\n );\n }\n\n if (!vibe) {\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n <EmptyState\n icon={<Zap className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.notFound', 'Vibe not found')}\n description={`${tr('vibeDetails.notFoundDescriptionPrefix', 'No vibe with ID')} \"${id}\" ${tr('vibeDetails.notFoundDescriptionSuffix', 'exists')}`}\n action={\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n }\n />\n </MotionPage>\n );\n }\n\n // Types for sessions and child vibes from the query\n type SessionItem = NonNullable<typeof vibeSessions>[number];\n type ChildVibeItem = NonNullable<typeof childVibes>[number];\n\n const isActive = vibe.status === 'ACTIVE' || vibe.status === 'RUNNING';\n const runningSessions = vibeSessions.filter((s: SessionItem) => s.status === 'RUNNING').length;\n\n // Tab definitions\n const relatedCount = childVibes.length + (parentVibe ? 1 : 0) + (agent ? 1 : 0);\n const hasGitInfo = Boolean(vibe.gitBranch || vibe.gitRemote);\n\n const allVibeTabs: { id: VibeTab; label: string; icon: typeof Zap; badge?: string }[] = [\n { id: 'overview', label: tr('nav.overview', 'Overview'), icon: Info },\n { id: 'config', label: tr('vibeDetails.config', 'Config'), icon: Settings },\n {\n id: 'git',\n label: tr('vibeDetails.git', 'Git'),\n icon: GitBranch,\n badge: hasGitInfo ? vibe.gitBranch || undefined : undefined,\n },\n {\n id: 'sessions',\n label: tr('nav.sessions', 'Sessions'),\n icon: Terminal,\n badge: `${vibeSessions.length}`,\n },\n { id: 'notes', label: tr('vibeDetails.notes', 'Notes'), icon: FileText },\n { id: 'docs', label: tr('vibeDetails.docs', 'Docs'), icon: BookOpen },\n { id: 'scorecard', label: tr('vibeDetails.scorecard', 'Scorecard'), icon: BarChart3 },\n { id: 'catalogs', label: tr('vibeDetails.catalogs', 'Catalogs'), icon: Library },\n {\n id: 'graphqlPlayground',\n label: tr('vibeDetails.graphqlPlayground', 'GraphQL'),\n icon: Network,\n },\n { id: 'gitops', label: tr('vibeDetails.gitops', 'GitOps'), icon: GitMerge },\n { id: 'security', label: tr('vibeDetails.security', 'Security'), icon: Shield },\n {\n id: 'related',\n label: tr('vibeDetails.related', 'Related'),\n icon: Link2,\n badge: relatedCount > 0 ? `${relatedCount}` : undefined,\n },\n {\n id: 'llmContext',\n label: tr('vibeDetails.llmContext', 'LLM Context'),\n icon: Bot,\n },\n // F10: linked-entity tabs. Always visible; panels render their own\n // empty state when the vibe has no linked entities.\n {\n id: 'vibedecks',\n label: tr('vibeDetails.vibedecks', 'Vibe Decks'),\n icon: Zap,\n },\n {\n id: 'actions',\n label: tr('vibeDetails.actions', 'Actions'),\n icon: Zap,\n },\n {\n id: 'scheduledJobs',\n label: tr('vibeDetails.scheduledJobs', 'Scheduled'),\n icon: Zap,\n },\n {\n id: 'sandboxes',\n label: tr('vibeDetails.sandboxes', 'Sandboxes'),\n icon: Terminal,\n },\n {\n id: 'aiPrompts',\n label: tr('vibeDetails.aiPrompts', 'AI Prompts'),\n icon: Bot,\n },\n {\n id: 'aiContexts',\n label: tr('vibeDetails.aiContexts', 'AI Contexts'),\n icon: Bot,\n },\n {\n id: 'instructionFiles',\n label: tr('vibeDetails.instructionFiles', 'Instructions'),\n icon: Bot,\n },\n {\n id: 'tunnels',\n label: tr('vibeDetails.tunnels', 'Tunnels'),\n icon: Network,\n },\n ];\n\n // F10: linked-entity tabs are always visible — their panel renders an\n // empty state when the vibe has no linked entities of that kind.\n const LINKED_ENTITY_TABS = new Set<VibeTab>([\n 'vibedecks',\n 'actions',\n 'scheduledJobs',\n 'sandboxes',\n 'aiPrompts',\n 'aiContexts',\n 'instructionFiles',\n 'tunnels',\n ]);\n\n // Hide any non-core tab whose feature is disabled — either per-vibe,\n // platform-level, or because the vibe has no attached agent.\n const vibeTabs = allVibeTabs.filter((tab) => {\n if (ALWAYS_VISIBLE_TABS.has(tab.id)) return true;\n if (LINKED_ENTITY_TABS.has(tab.id)) return true;\n if (!isFeatureEnabled(tab.id) || !isPlatformTabEnabled(tab.id)) return false;\n if (!hasAttachedAgent && AGENT_REQUIRED_TABS.has(tab.id)) return false;\n if (tab.id === 'git' && gitIframeMigrationFlag?.value === true) return false;\n if (tab.id === 'graphqlPlayground' && graphiqlIframeMigrationFlag?.value === true) return false;\n if (tab.id === 'gitops' && gitopsIframeMigrationFlag?.value === true) return false;\n return true;\n });\n\n return (\n <MotionPage className=\"space-y-6 p-4 sm:p-6 bg-bg-surface min-h-screen\">\n {/* Success Toast */}\n {successMessage && (\n <div className=\"fixed top-4 left-4 right-4 sm:left-auto z-50 bg-status-success-bg text-status-success-text px-4 py-3 rounded-md shadow-lg\">\n {successMessage}\n </div>\n )}\n\n {/* Back Button */}\n <button\n type=\"button\"\n onClick={handleBack}\n className=\"flex items-center gap-2 text-text-secondary hover:text-text-primary transition-colors\"\n >\n <ArrowLeft className=\"size-4\" />\n {tr('vibeDetails.backToVibes', 'Back to Vibes')}\n </button>\n\n {/* Header */}\n <PageHeader\n title={vibe.name}\n description={vibe.description || tr('vibeDetails.noDescription', 'No description')}\n breadcrumb={[\n { label: tr('nav.vibes', 'Vibes'), path: basePath ? `${basePath}/vibes` : '/vibes' },\n { label: vibe.name },\n ]}\n actions={\n <div className=\"flex items-center gap-2 flex-wrap\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n disabled={loading}\n className=\"flex items-center justify-center p-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw className={`size-4 ${loading ? 'animate-spin' : ''}`} />\n </button>\n <button\n type=\"button\"\n onClick={() => navigate(`/ai?vibeId=${vibe.id}&contextLevel=vibe`)}\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 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n title={tr('vibeDetails.openAiChat', 'Open AI chat in this vibe')}\n data-testid=\"vibe-open-ai-chat\"\n >\n <Bot className=\"size-3.5 sm:w-4 sm:h-4\" />\n <span className=\"hidden sm:inline\">{tr('vibeDetails.aiChat', 'AI Chat')}</span>\n </button>\n <VibeOpenInEditorMenu\n vibeId={vibe.id}\n workspacePath={vibe.path}\n agentId={effectiveAgent?.id ?? null}\n tunnelUrl={effectiveTunnelUrl}\n profile={effectiveProfile ?? 'default'}\n browserEditorEnabled={isFeatureEnabled('browserEditor')}\n />\n <PermissionButton\n permission={VIBE_PERMISSIONS.SESSION_CREATE}\n deniedBehavior=\"disable\"\n deniedTooltip=\"You don't have permission to create sessions\"\n >\n <button\n type=\"button\"\n onClick={handleStartSession}\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 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed\"\n >\n <Plus className=\"size-3.5 sm:w-4 sm:h-4\" />\n <span className=\"hidden sm:inline\">{tr('vibeDetails.new', 'New')}</span>\n <span className=\"hidden sm:inline\">{tr('nav.sessions', 'Session')}</span>\n </button>\n </PermissionButton>\n <PermissionButton\n permission={VIBE_PERMISSIONS.VIBE_UPDATE}\n deniedBehavior=\"disable\"\n deniedTooltip=\"You don't have permission to manage this vibe\"\n >\n <div className=\"relative\" ref={moreMenuRef}>\n <button\n type=\"button\"\n onClick={() => setMoreMenuOpen((v) => !v)}\n aria-haspopup=\"menu\"\n aria-expanded={moreMenuOpen}\n aria-label={tr('common.moreActions', 'More actions')}\n title={tr('common.moreActions', 'More actions')}\n className=\"flex items-center justify-center p-2 bg-bg-sunken text-text-primary rounded-md hover:bg-bg-sunken/80 transition-colors\"\n >\n <MoreVertical className=\"size-4\" />\n </button>\n {moreMenuOpen && (\n <div\n role=\"menu\"\n className=\"absolute right-0 mt-2 min-w-[12rem] bg-bg-elevated border border-border-default rounded-md shadow-lg z-20 py-1\"\n >\n <button\n type=\"button\"\n role=\"menuitem\"\n onClick={() => {\n setMoreMenuOpen(false);\n if (isActive) handleDeactivateVibe();\n else handleActivateVibe();\n }}\n className=\"w-full flex items-center gap-2 px-3 py-2 text-sm text-text-primary hover:bg-action-ghost-bg-hover text-left\"\n >\n {isActive ? (\n <>\n <Pause className=\"size-4 text-text-secondary\" />\n <span>\n {tr('vibeDetails.pauseVibe', 'Pause vibe (stop scheduled runs)')}\n </span>\n </>\n ) : (\n <>\n <Play className=\"size-4 text-status-success-text\" />\n <span>\n {tr('vibeDetails.resumeVibe', 'Resume vibe (re-enable scheduled runs)')}\n </span>\n </>\n )}\n </button>\n <div className=\"border-t border-border-subtle my-1\" />\n <button\n type=\"button\"\n role=\"menuitem\"\n onClick={() => {\n setMoreMenuOpen(false);\n setConfirmArchive(true);\n }}\n className=\"w-full flex items-center gap-2 px-3 py-2 text-sm text-status-warning-text hover:bg-action-ghost-bg-hover text-left\"\n >\n <Archive className=\"size-4\" />\n <span>{tr('vibeDetails.archive', 'Archive vibe…')}</span>\n </button>\n </div>\n )}\n </div>\n </PermissionButton>\n </div>\n }\n />\n\n {confirmArchive && (\n <div\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault();\n setConfirmArchive(false);\n }\n }}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"archive-vibe-title\"\n className=\"fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4\"\n onClick={(e) => {\n if (e.target === e.currentTarget) setConfirmArchive(false);\n }}\n >\n <div className=\"w-full max-w-md rounded-lg bg-bg-elevated border border-border-default shadow-xl p-5\">\n <h2 id=\"archive-vibe-title\" className=\"text-base font-semibold text-text-primary mb-2\">\n {tr('vibeDetails.archiveConfirmTitle', 'Archive this vibe?')}\n </h2>\n <p className=\"text-sm text-text-secondary mb-4\">\n {tr(\n 'vibeDetails.archiveConfirmBody',\n 'Archived vibes are hidden from the active list and their scheduled actions stop firing. You can restore the vibe later from the archived view. Existing sessions, tunnels, and audit history are preserved.'\n )}\n </p>\n <div className=\"flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => setConfirmArchive(false)}\n className=\"text-sm px-3 py-1.5 rounded-md border border-border-default text-text-primary hover:bg-action-ghost-bg-hover\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleArchiveVibe}\n className=\"text-sm px-3 py-1.5 rounded-md bg-status-warning-bg text-status-warning-text hover:opacity-80\"\n >\n {tr('vibeDetails.archive', 'Archive')}\n </button>\n </div>\n </div>\n </div>\n )}\n\n {/* Status Cards */}\n <MotionList stagger={0.04} className=\"grid grid-cols-2 sm:grid-cols-4 gap-3 sm:gap-4\">\n <MotionItem>\n <Card className=\"p-3 sm:p-4\">\n <div className=\"flex items-center gap-2 sm:gap-3\">\n <div className=\"size-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <Zap className=\"size-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0\">\n <p\n className=\"text-xs sm:text-sm text-text-secondary\"\n title={tr(\n 'vibeDetails.statusTooltip',\n 'ACTIVE: vibe is in your default working set and can run sessions, tunnels and GitOps via its attached agent(s). INACTIVE: vibe is parked — still visible, no sessions auto-resume. RUNNING: at least one session is live. ARCHIVED: hidden from default lists; can be restored.'\n )}\n >\n {tr('sessions.filterStatus', 'Status')}\n </p>\n <StatusBadge status={vibe.status} size=\"md\" />\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-3 sm:p-4\">\n <div className=\"flex items-center gap-2 sm:gap-3\">\n <div className=\"size-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <Server className=\"size-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-xs sm:text-sm text-text-secondary\">\n {tr('sessions.filterAgent', 'Agent')}\n </p>\n <p className=\"font-semibold text-text-primary text-sm sm:text-base truncate\">\n {agent?.name || tr('targets.unknown', 'Unknown')}\n </p>\n </div>\n </div>\n </Card>\n </MotionItem>\n <MotionItem>\n <Card className=\"p-3 sm:p-4\">\n <div className=\"flex items-center gap-2 sm:gap-3\">\n <div className=\"size-8 sm:w-10 sm:h-10 rounded-lg bg-bg-sunken flex items-center justify-center flex-shrink-0\">\n <Terminal className=\"size-4 sm:w-5 sm:h-5 text-text-secondary\" />\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-xs sm:text-sm text-text-secondary\">\n {tr('nav.sessions', 'Sessions')}\n </p>\n <p className=\"font-semibold text-text-primary text-sm sm:text-base\">\n {runningSessions} {tr('health.active', 'active')}\n </p>\n </div>\n </div>\n </Card>\n </MotionItem>\n </MotionList>\n\n {/* ── Tab Navigation Bar ─────────────────────────────────────────── */}\n <div className=\"border-b border-border-default\">\n <nav\n className=\"flex gap-1 -mb-px overflow-x-auto pb-1\"\n aria-label={tr('vibeDetails.tabsAriaLabel', 'Vibe tabs')}\n >\n {vibeTabs.map((tab) => {\n const Icon = tab.icon;\n const isActiveTab = activeTab === tab.id;\n return (\n <button\n type=\"button\"\n key={tab.id}\n onClick={() => setActiveTab(tab.id)}\n className={`flex shrink-0 items-center gap-1.5 px-3 py-2.5 text-xs sm:gap-2 sm:px-4 sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${\n isActiveTab\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border-default'\n }`}\n aria-selected={isActiveTab}\n role=\"tab\"\n >\n <Icon className=\"size-4\" />\n {tab.label}\n {tab.badge && (\n <span\n className={`hidden rounded-full px-1.5 py-0.5 text-[10px] sm:inline-flex sm:text-xs ${\n isActiveTab\n ? 'bg-action-primary-bg/10 text-action-primary-bg'\n : 'bg-bg-sunken text-text-muted'\n }`}\n >\n {tab.badge}\n </span>\n )}\n </button>\n );\n })}\n {/* iframe-mounted UI plugin tabs (PR-7). Appended after the\n inline strip so the visual order is \"builtins then\n plugins\". Gated by `vibes.tabs.iframe-host`. */}\n {enableIframeHost &&\n iframeContributions.data.map((c) => {\n const isActiveTab = activeTab === c.contribution.id;\n return (\n <button\n type=\"button\"\n key={`plugin-${c.contribution.id}`}\n onClick={() => setActiveTab(c.contribution.id)}\n className={`flex shrink-0 items-center gap-1.5 px-3 py-2.5 text-xs sm:gap-2 sm:px-4 sm:text-sm font-medium border-b-2 transition-colors whitespace-nowrap ${\n isActiveTab\n ? 'border-action-primary-bg text-action-primary-bg'\n : 'border-transparent text-text-secondary hover:text-text-primary hover:border-border-default'\n }`}\n aria-selected={isActiveTab}\n role=\"tab\"\n title={`${c.contribution.title} (plugin: ${c.pluginKey})`}\n >\n {c.contribution.title}\n </button>\n );\n })}\n {/* \"+ Add tab\" affordance — always at the trailing end of\n the strip, after both inline and plugin tabs. Standard\n UX pattern; PR-86 review flagged the misplacement. */}\n {enableIframeHost && effectiveAgent && (\n <button\n type=\"button\"\n onClick={() => setAddTabOpen(true)}\n className=\"flex shrink-0 items-center gap-1 p-2 text-xs sm:text-sm border-b-2 border-transparent text-text-secondary hover:text-text-primary hover:border-border-default\"\n title=\"Install a UI plugin tab\"\n aria-label=\"Add tab\"\n >\n <span aria-hidden>+</span>\n <span className=\"hidden sm:inline\">Add tab</span>\n </button>\n )}\n </nav>\n </div>\n\n {/* ── Tab Panels ─────────────────────────────────────────────────── */}\n <AnimatePresence mode=\"wait\">\n {/* Overview Tab */}\n {activeTab === 'overview' && (\n <motion.div\n key=\"overview\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n {/* Description */}\n {vibe.description && (\n <Card>\n <div className=\"p-4\">\n <p className=\"text-text-primary\">{vibe.description}</p>\n </div>\n </Card>\n )}\n\n {/* No-agent empty state — shown only on the overview tab so it does\n not push tab content out of frame. Agent-dependent tabs are\n already hidden when this is rendered. */}\n {!hasAttachedAgent && (\n <VibeAgentEmptyState vibeId={vibe.id} tr={tr} onAttached={() => refetch()} />\n )}\n\n {hasAttachedAgent && (\n <VibeAgentSwitcher\n vibeId={vibe.id}\n attachedAgents={attachedAgents}\n effectiveAgentId={effectiveAgent?.id ?? null}\n />\n )}\n\n {/* Path */}\n <Card>\n <div className=\"p-3 sm:p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2 text-sm sm:text-base\">\n <Folder className=\"size-4 sm:w-5 sm:h-5\" />\n {tr('vibeDetails.path', 'Path')}\n </h3>\n </div>\n <div className=\"p-3 sm:p-4\">\n <div className=\"bg-bg-sunken rounded-md p-2 sm:p-3 font-mono text-xs sm:text-sm text-text-primary overflow-x-auto break-all\">\n {vibe.path}\n </div>\n </div>\n </Card>\n\n {/* Timeline */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Clock className=\"size-5\" />\n {tr('vibeDetails.timeline', 'Timeline')}\n </h3>\n </div>\n <div className=\"p-4\">\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm\">\n <div>\n <p className=\"text-text-secondary\">{tr('vibeDetails.created', 'Created')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(vibe.createdAt)}\n </p>\n </div>\n <div>\n <p className=\"text-text-secondary\">{tr('vibeDetails.updated', 'Updated')}</p>\n <p className=\"font-medium text-text-primary\">\n {formatTimestamp(vibe.updatedAt)}\n </p>\n </div>\n </div>\n </div>\n </Card>\n </motion.div>\n )}\n\n {/* Config Tab */}\n {activeTab === 'config' && (\n <motion.div\n key=\"config\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n {/* Environment Variables */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Variable className=\"size-5\" />\n {tr('vibeDetails.environmentVariables', 'Environment Variables')}\n </h3>\n </div>\n <div className=\"p-4\">\n {vibe.environmentVariables && Object.keys(vibe.environmentVariables).length > 0 ? (\n <div className=\"flex flex-wrap gap-2\">\n {Object.entries(vibe.environmentVariables).map(([key, value]) => (\n <span\n key={key}\n className=\"px-3 py-1 bg-bg-sunken text-text-primary text-sm rounded-full font-mono\"\n >\n {key}={String(value)}\n </span>\n ))}\n </div>\n ) : (\n <p className=\"text-text-secondary\">\n {tr(\n 'vibeDetails.noEnvironmentVariables',\n 'No environment variables configured'\n )}\n </p>\n )}\n </div>\n </Card>\n\n {/* Tags */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Tag className=\"size-5\" />\n {tr('vibeDetails.tags', 'Tags')}\n </h3>\n </div>\n <div className=\"p-4\">\n <EntityTagPicker entityType=\"VIBECONTROLS_VIBE\" entityId={vibe.id} />\n </div>\n </Card>\n\n {/* Features — per-vibe feature toggles */}\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Settings className=\"size-5\" />\n {tr('vibeDetails.features', 'Features')}\n </h3>\n <p className=\"text-sm text-text-secondary mt-1\">\n {tr(\n 'vibeDetails.featuresDescription',\n 'Toggle which sections show up on this vibe. Changes apply to everyone viewing the vibe.'\n )}\n </p>\n </div>\n <div className=\"divide-y divide-border-default\">\n {featureCatalog.map((feature) => {\n const enabled = isFeatureEnabled(feature.id);\n return (\n <label\n key={feature.id}\n className=\"flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-bg-sunken transition-colors\"\n >\n <input\n type=\"checkbox\"\n role=\"switch\"\n aria-label={feature.label}\n aria-checked={enabled}\n checked={enabled}\n disabled={setFeatureFlagLoading || !vibe.id}\n onChange={async (e) => {\n const next = e.target.checked;\n try {\n await setVibeFeatureFlagMutation({\n variables: {\n id: vibe.id,\n featureId: feature.id,\n enabled: next,\n },\n optimisticResponse: {\n setVibeFeatureFlag: {\n __typename: 'Vibe',\n id: vibe.id,\n featureFlags: {\n ...(effectiveVibeFlags?.featureFlags ?? {}),\n [feature.id]: next,\n },\n effectiveFeatureFlags: {\n ...(effectiveVibeFlags?.effectiveFeatureFlags ?? {}),\n [feature.id]: next,\n },\n } as never,\n },\n });\n } catch (err) {\n console.error('Failed to toggle vibe feature:', err);\n }\n }}\n className=\"size-4 shrink-0 accent-action-primary-bg disabled:opacity-50\"\n />\n <span className=\"text-sm font-medium text-text-primary\">{feature.label}</span>\n </label>\n );\n })}\n </div>\n </Card>\n </motion.div>\n )}\n\n {/* Git Tab */}\n {activeTab === 'git' && isFeatureEnabled('git') && (\n <motion.div\n key=\"git\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <PluginGate\n agentId={effectiveAgent?.id ?? null}\n required={[GIT_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('vibeDetails.gitPluginEyebrow', 'Git plugin required')}\n title={tr('vibeDetails.gitPluginTitle', 'Install the git plugin')}\n description={tr(\n 'vibeDetails.gitPluginDescription',\n 'Live git branch, status, and remote data for this vibe are served by the vibe-plugin-tool-git agent plugin.'\n )}\n >\n {hasGitInfo ? (\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <GitBranch className=\"size-5\" />\n {tr('vibeDetails.gitInformation', 'Git Information')}\n </h3>\n </div>\n <div className=\"p-4 space-y-3\">\n <div className=\"flex items-center gap-4 flex-wrap\">\n {vibe.gitBranch && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.branch', 'Branch')}:\n </span>\n <span className=\"px-2 py-1 bg-bg-sunken text-text-primary text-sm rounded font-mono\">\n {vibe.gitBranch}\n </span>\n </div>\n )}\n {vibe.gitStatus && (\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">\n {tr('sessions.filterStatus', 'Status')}:\n </span>\n <span className={`font-medium ${getGitStatusColor(vibe.gitStatus)}`}>\n {vibe.gitStatus}\n </span>\n </div>\n )}\n </div>\n {vibe.gitRemote && (\n <div>\n <span className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.remote', 'Remote')}:\n </span>\n <p className=\"font-mono text-sm text-text-primary mt-1 break-all\">\n {vibe.gitRemote}\n </p>\n </div>\n )}\n </div>\n </Card>\n ) : (\n <EmptyState\n icon={<GitBranch className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noGitInformation', 'No git information')}\n description={tr(\n 'vibeDetails.noGitInformationDescription',\n 'This vibe does not have any git repository linked'\n )}\n />\n )}\n </PluginGate>\n </motion.div>\n )}\n\n {/* Sessions Tab */}\n {activeTab === 'sessions' && isFeatureEnabled('sessions') && (\n <motion.div\n key=\"sessions\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n >\n <PluginGate\n agentId={effectiveAgent?.id ?? null}\n requirements={[\n {\n packages: [SESSION_MANAGER_PACKAGE],\n mode: 'all',\n eyebrow: tr('vibeDetails.sessionsManagerEyebrow', 'Session manager required'),\n title: tr('vibeDetails.sessionsManagerTitle', 'Install the session manager'),\n description: tr(\n 'vibeDetails.sessionsManagerDescription',\n 'Sessions are orchestrated by the unified session manager plugin. Install it to continue.'\n ),\n },\n {\n packages: SESSION_PROVIDER_PACKAGES,\n mode: 'any',\n eyebrow: tr('vibeDetails.sessionProviderEyebrow', 'Session provider required'),\n title: tr('vibeDetails.sessionProviderTitle', 'Pick a session provider'),\n description: tr(\n 'vibeDetails.sessionProviderDescription',\n 'Choose the terminal multiplexer your sessions should run on. You can install more providers later.'\n ),\n },\n ]}\n >\n <Card>\n <div className=\"p-4 border-b border-border-default flex items-center justify-between\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Terminal className=\"size-5\" />\n {tr('nav.sessions', 'Sessions')} ({vibeSessions.length})\n </h3>\n <button\n type=\"button\"\n onClick={handleStartSession}\n className=\"flex items-center gap-1 px-2 py-1 text-sm bg-bg-sunken text-text-primary rounded hover:bg-bg-sunken/80 transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('vibeDetails.new', 'New')}\n </button>\n </div>\n {vibeSessions.length > 0 ? (\n <MotionList className=\"divide-y divide-border-primary\">\n {vibeSessions.map((session: SessionItem) => (\n <MotionItem key={session.id} variant=\"fadeRight\">\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => handleSessionClick(session.id))();\n }\n }}\n onClick={() => handleSessionClick(session.id)}\n className=\"p-4 flex items-center justify-between cursor-pointer hover:bg-bg-sunken/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <Terminal className=\"size-5 text-text-secondary flex-shrink-0\" />\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">\n {session.name}\n </p>\n <p className=\"text-sm text-text-secondary truncate\">{session.type}</p>\n </div>\n </div>\n <StatusBadge status={session.status} />\n </div>\n </MotionItem>\n ))}\n </MotionList>\n ) : (\n <div className=\"p-6 text-center text-text-secondary\">\n {tr('vibeDetails.noSessionsForVibe', 'No sessions for this vibe')}\n </div>\n )}\n </Card>\n </PluginGate>\n </motion.div>\n )}\n\n {/* Notes Tab */}\n {activeTab === 'notes' && isFeatureEnabled('notes') && (\n <motion.div\n key=\"notes\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <NotesTabPanel context=\"vibe\" vibeId={vibe.id} />\n </motion.div>\n )}\n\n {/* Docs Tab */}\n {activeTab === 'docs' && isFeatureEnabled('docs') && (\n <motion.div\n key=\"docs\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <DocsTabPanel vibeId={vibe.id} />\n </motion.div>\n )}\n\n {/* Scorecard Tab */}\n {activeTab === 'scorecard' && isFeatureEnabled('scorecard') && (\n <motion.div\n key=\"scorecard\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <ScorecardPanel ownerType=\"VIBE\" ownerId={vibe.id} />\n </motion.div>\n )}\n\n {/* Catalogs Tab */}\n {activeTab === 'catalogs' && isFeatureEnabled('catalogs') && (\n <motion.div\n key=\"catalogs\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <VibeCatalogsTab vibeId={vibe.id} tr={tr} navigate={navigate} />\n </motion.div>\n )}\n\n {/* GraphQL Playground Tab */}\n {activeTab === 'graphqlPlayground' && isFeatureEnabled('graphqlPlayground') && (\n <motion.div\n key=\"graphqlPlayground\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <PluginGate\n agentId={effectiveAgent?.id ?? null}\n required={[GRAPHQL_PLAYGROUND_PACKAGE]}\n mode=\"all\"\n eyebrow={tr('vibeDetails.graphqlPluginEyebrow', 'GraphQL playground plugin required')}\n title={tr('vibeDetails.graphqlPluginTitle', 'Install the GraphQL playground')}\n description={tr(\n 'vibeDetails.graphqlPluginDescription',\n 'The GraphQL Playground is served by the graphiql agent plugin. Install it to continue.'\n )}\n >\n {effectiveTunnelUrl ? (\n <PlaygroundTab\n vibeId={vibe.id}\n agentId={effectiveAgent?.id ?? null}\n profile={effectiveProfile}\n tunnelUrl={effectiveTunnelUrl}\n type=\"graphql\"\n tr={tr}\n />\n ) : (\n <EmptyState\n icon={<Network className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noAgentTunnel', 'No agent tunnel available')}\n description={tr(\n 'vibeDetails.noAgentTunnelDesc2',\n 'The GraphQL playground requires an active agent with a tunnel URL.'\n )}\n />\n )}\n </PluginGate>\n </motion.div>\n )}\n\n {/* GitOps Tab */}\n {activeTab === 'gitops' && isFeatureEnabled('gitops') && (\n <VibeGitOpsTab\n vibeId={id || ''}\n agentId={effectiveAgent?.id ?? ''}\n profile={effectiveProfile}\n tunnelUrl={effectiveTunnelUrl}\n tr={tr}\n />\n )}\n\n {activeTab === 'security' && isFeatureEnabled('security') && (\n <motion.div\n key=\"security\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <SecurityTabPanel\n vibeId={id || ''}\n agentId={effectiveAgent?.id ?? null}\n profile={effectiveProfile ?? 'default'}\n />\n </motion.div>\n )}\n\n {/* Related Tab */}\n {activeTab === 'related' && isFeatureEnabled('related') && (\n <motion.div\n key=\"related\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n {relatedCount > 0 ? (\n <>\n {/* Child Vibes */}\n {childVibes.length > 0 && (\n <Card>\n <div className=\"p-4 border-b border-border-default\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Zap className=\"size-5\" />\n {tr('vibeDetails.childVibes', 'Child Vibes')} ({childVibes.length})\n </h3>\n </div>\n <MotionList className=\"divide-y divide-border-primary\">\n {childVibes.map((child: ChildVibeItem) => (\n <MotionItem key={child.id} variant=\"fadeRight\">\n <div\n role=\"button\"\n tabIndex={0}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n (() => handleVibeClick(child.id))();\n }\n }}\n onClick={() => handleVibeClick(child.id)}\n className=\"p-4 flex items-center justify-between cursor-pointer hover:bg-bg-sunken/50 transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <Zap className=\"size-5 text-text-secondary flex-shrink-0\" />\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary truncate\">\n {child.name}\n </p>\n <p className=\"text-sm text-text-secondary truncate\">{child.type}</p>\n </div>\n </div>\n <StatusBadge status={child.status} />\n </div>\n </MotionItem>\n ))}\n </MotionList>\n </Card>\n )}\n\n {/* Parent Vibe */}\n {parentVibe && (\n <Card onClick={() => handleVibeClick(parentVibe.id)} hoverable>\n <div className=\"p-4 flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Zap className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.parentVibe', 'Parent Vibe')}\n </p>\n <p className=\"font-medium text-text-primary\">{parentVibe.name}</p>\n </div>\n </div>\n </Card>\n )}\n\n {/* Related Agent */}\n {agent && (\n <Card onClick={handleAgentClick} hoverable>\n <div className=\"p-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <div className=\"size-10 rounded-lg bg-bg-sunken flex items-center justify-center\">\n <Server className=\"size-5 text-text-secondary\" />\n </div>\n <div>\n <p className=\"text-sm text-text-secondary\">\n {tr('vibeDetails.runningOnAgent', 'Running on Agent')}\n </p>\n <p className=\"font-medium text-text-primary\">{agent.name}</p>\n <p className=\"text-sm text-text-secondary\">{agent.hostname}</p>\n </div>\n </div>\n <StatusBadge status={agent.isActive ? 'ACTIVE' : 'OFFLINE'} />\n </div>\n </Card>\n )}\n </>\n ) : (\n <EmptyState\n icon={<Link2 className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noRelatedEntities', 'No related entities')}\n description={tr(\n 'vibeDetails.noRelatedEntitiesDescription',\n 'This vibe has no child vibes, parent vibe, or linked agent'\n )}\n />\n )}\n </motion.div>\n )}\n\n {/* LLM Context Tab — signed-URL feature, mount the panel verbatim */}\n {activeTab === 'llmContext' && (\n <motion.div\n key=\"llmContext\"\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <VibeLlmContextTabPanel\n vibeId={vibe.id}\n attachedAgents={attachedAgents.map((a: { id: string; name: string }) => ({\n id: a.id,\n name: a.name,\n }))}\n />\n </motion.div>\n )}\n\n {/* F10 round-3: linked-entity tabs. Backend may not yet expose\n `Vibe.linkedEntities.{type}` federation field. Until it does,\n each panel renders a \"Coming soon\" empty state with a \"+ Link\n existing\" CTA stub. Console-warn once per panel so the\n absence is visible in dev. */}\n {(activeTab === 'vibedecks' ||\n activeTab === 'actions' ||\n activeTab === 'scheduledJobs' ||\n activeTab === 'sandboxes' ||\n activeTab === 'aiPrompts' ||\n activeTab === 'aiContexts' ||\n activeTab === 'instructionFiles' ||\n activeTab === 'tunnels') && (\n <motion.div\n key={`linked-${activeTab}`}\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <VibeLinkedEntitiesPanel vibeId={vibe.id} kind={activeTab} />\n </motion.div>\n )}\n {/* iframe-mounted UI plugin tab body (PR-7). Renders when the\n active tab id matches a discovered contribution. The host\n translates a small set of capability-gated RPCs from the\n plugin via `onRpcRequest`; for now the host returns a\n \"method not implemented\" response. PR-10 wires real\n handlers (navigate, toast, storage, getContext, …). */}\n {enableIframeHost &&\n (() => {\n const active: DiscoveredContribution | undefined = iframeContributions.data.find(\n (c) => c.contribution.id === activeTab\n );\n if (!active) return null;\n const tunnel = active.tunnel;\n const restPaths =\n active.capabilities.restPaths.length > 0\n ? active.capabilities.restPaths\n : [`/ui/${active.pluginKey}`];\n return (\n <motion.div\n key={`plugin-${active.contribution.id}`}\n initial={{ opacity: 0, y: 8 }}\n animate={{ opacity: 1, y: 0 }}\n exit={{ opacity: 0, y: -8 }}\n transition={{ duration: 0.2 }}\n className=\"space-y-6\"\n >\n <Card>\n <div className=\"p-0 h-[600px]\">\n <PluginIframeMount\n contribution={active}\n tunnel={tunnel}\n restPaths={restPaths}\n context={{ vibeId: vibe.id }}\n // Canonical agent id — `tunnel.label` is a\n // display string and plugin RPC logic that\n // looks up the agent by id would miss with that.\n agentId={effectiveAgent?.id ?? ''}\n profile={tunnel.profile}\n theme={{ colorMode: 'light', tokens: {} }}\n locale=\"en\"\n featureFlags={effectiveVibeFlags?.featureFlags ?? {}}\n // Real version string surfaced via host:init so\n // plugin authors can do version-based feature\n // detection.\n hostVersion=\"1.0.0\"\n />\n </div>\n </Card>\n </motion.div>\n );\n })()}\n </AnimatePresence>\n\n {/* PR-10 \"+ Add tab\" dialog. Mounts off the AnimatePresence so\n opening it doesn't fight the tab-body transitions. */}\n {enableIframeHost && effectiveAgent && (\n <AddTabDialog\n isOpen={addTabOpen}\n onClose={() => setAddTabOpen(false)}\n mountPoint=\"vibe.detailTab\"\n agentId={effectiveAgent.id}\n profile={effectiveProfile ?? 'default'}\n onInstalled={() => {\n void iframeContributions.refetch();\n }}\n />\n )}\n\n {/* Create Session Modal */}\n {showSessionModal && (\n <div className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4\">\n <div className=\"bg-bg-surface rounded-lg p-6 w-full max-w-lg\">\n <h2 className=\"text-lg font-semibold text-text-primary mb-4\">\n {tr('vibeDetails.newSessionFor', 'New Session for')} {vibe.name}\n </h2>\n {sessionCreateError && (\n <div className=\"mb-4 p-3 bg-status-error-bg text-status-error-text rounded-md text-sm\">\n {sessionCreateError}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('vibeDetails.sessionName', 'Session Name')}\n </label>\n <input\n aria-label=\"control\"\n ref={sessionNameRef}\n type=\"text\"\n placeholder={tr('vibeDetails.sessionNamePlaceholder', 'dev-terminal')}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('sessions.filterType', 'Type')}\n </label>\n <select\n ref={sessionTypeRef}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary\"\n >\n <option value=\"TERMINAL\">{tr('sessions.typeTerminal', 'Terminal')}</option>\n <option value=\"TMUX\">{tr('sessions.typeTmux', 'TMUX')}</option>\n <option value=\"SSH\">{tr('sessions.typeSsh', 'SSH')}</option>\n <option value=\"SCRIPT\">{tr('sessions.typeScript', 'Script')}</option>\n <option value=\"CUSTOM\">{tr('sessions.typeCustom', 'Custom')}</option>\n </select>\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('sessions.command', 'Command')}\n </label>\n <input\n aria-label=\"control\"\n ref={sessionCommandRef}\n type=\"text\"\n placeholder={tr('vibeDetails.sessionCommandPlaceholder', 'bash')}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary mb-1\">\n {tr('vibeDetails.workingDirectory', 'Working Directory')}\n </label>\n <input\n aria-label=\"control\"\n ref={sessionWorkDirRef}\n type=\"text\"\n defaultValue={vibe.path || ''}\n className=\"w-full px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary\"\n />\n </div>\n </div>\n <div className=\"flex flex-col sm:flex-row justify-end gap-3 mt-6\">\n <button\n type=\"button\"\n onClick={() => setShowSessionModal(false)}\n disabled={sessionCreateLoading}\n className=\"px-4 py-2 text-text-primary hover:bg-bg-sunken rounded-md transition-colors disabled:opacity-50\"\n >\n {tr('common.cancel', 'Cancel')}\n </button>\n <button\n type=\"button\"\n onClick={handleSubmitCreateSession}\n disabled={sessionCreateLoading}\n className=\"px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50\"\n >\n {sessionCreateLoading\n ? tr('sessions.creating', 'Creating...')\n : tr('sessions.createSession', 'Create Session')}\n </button>\n </div>\n </div>\n </div>\n )}\n </MotionPage>\n );\n}\n\n/* ── Vibe Catalogs Tab ─────────────────────────────────────────────────── */\n\nfunction VibeCatalogsTab({\n vibeId,\n tr,\n navigate,\n}: {\n vibeId: string;\n tr: (key: string, fallback: string) => string;\n navigate: (path: string) => void;\n}) {\n const [showAddDialog, setShowAddDialog] = useState(false);\n\n const { data, loading, refetch } = useGetVibeCatalogsQuery({ fetchPolicy: 'cache-and-network' });\n\n const [addVibeToCatalog] = useAddVibeToCatalogMutation({\n refetchQueries: ['GetVibeCatalogs'],\n });\n const [removeVibeFromCatalog] = useRemoveVibeFromCatalogMutation({\n refetchQueries: ['GetVibeCatalogs'],\n });\n\n const allCatalogs = data?.vibecontrolsCatalogs?.items ?? [];\n\n const memberCatalogs = allCatalogs.filter(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (cat: any) => cat.vibes?.some((v: any) => v.vibeId === vibeId)\n );\n\n const availableCatalogs = allCatalogs.filter(\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (cat: any) => !cat.vibes?.some((v: any) => v.vibeId === vibeId)\n );\n\n const handleAdd = async (catalogId: string) => {\n await addVibeToCatalog({ variables: { catalogId, vibeId } });\n await refetch();\n setShowAddDialog(false);\n };\n\n const handleRemove = async (catalogId: string) => {\n await removeVibeFromCatalog({ variables: { catalogId, vibeId } });\n await refetch();\n };\n\n if (loading && allCatalogs.length === 0) {\n return <PageSkeleton variant=\"detail\" />;\n }\n\n return (\n <>\n <div className=\"flex items-center justify-between\">\n <h3 className=\"font-semibold text-text-primary flex items-center gap-2\">\n <Library className=\"size-5\" />\n {tr('vibeDetails.catalogsTitle', 'Catalogs')}\n <span className=\"text-sm font-normal text-text-secondary\">({memberCatalogs.length})</span>\n </h3>\n <button\n type=\"button\"\n onClick={() => setShowAddDialog(true)}\n className=\"flex items-center gap-2 px-3 py-2 text-sm bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors\"\n >\n <Plus className=\"size-4\" />\n {tr('vibeDetails.addToCatalog', 'Add to Catalog')}\n </button>\n </div>\n\n {memberCatalogs.length > 0 ? (\n <div className=\"space-y-2\">\n {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */}\n {memberCatalogs.map((cat: any) => (\n <Card key={cat.id}>\n <div className=\"p-3 flex items-center justify-between\">\n <button\n type=\"button\"\n onClick={() => navigate(`/catalogs/${cat.id}`)}\n className=\"flex items-center gap-3 text-left hover:opacity-80 transition-opacity\"\n >\n <Library className=\"size-5 text-action-primary-bg flex-shrink-0\" />\n <div>\n <p className=\"font-medium text-text-primary text-sm\">{cat.name}</p>\n <p className=\"text-xs text-text-secondary\">{cat.visibility}</p>\n </div>\n </button>\n <button\n type=\"button\"\n onClick={() => handleRemove(cat.id)}\n className=\"p-1.5 text-text-muted hover:text-status-error-text hover:bg-status-error-bg rounded-md transition-colors\"\n title={tr('vibeDetails.removeFromCatalog', 'Remove from catalog')}\n >\n <X className=\"size-4\" />\n </button>\n </div>\n </Card>\n ))}\n </div>\n ) : (\n <EmptyState\n icon={<Library className=\"size-8 text-text-secondary\" />}\n title={tr('vibeDetails.noCatalogs', 'Not in any catalogs')}\n description={tr(\n 'vibeDetails.noCatalogsDesc',\n 'Add this vibe to catalogs to organize and track it alongside other components'\n )}\n />\n )}\n\n {/* Add to Catalog Dialog */}\n {showAddDialog && (\n <div className=\"fixed inset-0 bg-bg-overlay flex items-center justify-center z-50 p-4\">\n <motion.div\n initial={{ opacity: 0, scale: 0.95 }}\n animate={{ opacity: 1, scale: 1 }}\n className=\"bg-bg-surface rounded-lg w-full max-w-md max-h-[60vh] flex flex-col\"\n >\n <div className=\"p-4 border-b border-border-default\">\n <h2 className=\"text-lg font-semibold text-text-primary\">\n {tr('vibeDetails.addToCatalogTitle', 'Add Vibe to Catalog')}\n </h2>\n </div>\n <div className=\"flex-1 overflow-y-auto p-2\">\n {availableCatalogs.length === 0 ? (\n <div className=\"p-4 text-center text-text-secondary text-sm\">\n {tr(\n 'vibeDetails.noAvailableCatalogs',\n 'No available catalogs. This vibe is already in all catalogs.'\n )}\n </div>\n ) : (\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n availableCatalogs.map((cat: any) => (\n <button\n type=\"button\"\n key={cat.id}\n onClick={() => handleAdd(cat.id)}\n className=\"w-full flex items-center justify-between p-3 rounded-md hover:bg-bg-sunken transition-colors text-left\"\n >\n <div>\n <p className=\"font-medium text-text-primary text-sm\">{cat.name}</p>\n <p className=\"text-xs text-text-secondary\">{cat.visibility}</p>\n </div>\n <Plus className=\"size-4 text-text-secondary\" />\n </button>\n ))\n )}\n </div>\n <div className=\"p-4 border-t border-border-default\">\n <button\n type=\"button\"\n onClick={() => setShowAddDialog(false)}\n className=\"w-full px-4 py-2 text-text-primary hover:bg-bg-sunken rounded-md transition-colors\"\n >\n {tr('common.close', 'Close')}\n </button>\n </div>\n </motion.div>\n </div>\n )}\n </>\n );\n}\n\n/* ── Playground Tab (REST / GraphQL) ───────────────────────────────────── */\n\nfunction PlaygroundTab({\n vibeId,\n agentId,\n profile,\n tunnelUrl,\n type,\n tr,\n}: {\n vibeId: string;\n agentId: string | null;\n profile: string;\n tunnelUrl: string;\n type: 'rest' | 'graphql';\n tr: (key: string, fallback: string) => string;\n}) {\n const isRest = type === 'rest';\n const pluginPath = 'graphiql';\n\n const [endpointUrl, setEndpointUrl] = useState('');\n const [configLoaded, setConfigLoaded] = useState(false);\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n const [iframeKey, setIframeKey] = useState(0);\n\n // F4 Wave C — read playground config via svc GraphQL proxy (no more\n // direct browser fetch with x-agent-api-key).\n const { data: configQueryData, loading: configQueryLoading } = useAgentPlaygroundConfigQuery({\n variables: { agentId: agentId ?? '', profile, vibeId },\n skip: !agentId,\n fetchPolicy: 'network-only',\n });\n\n useEffect(() => {\n if (configQueryLoading) return;\n const payload = configQueryData?.agentPlaygroundConfig;\n if (payload?.ok && payload.data && typeof payload.data === 'object') {\n const obj = payload.data as Record<string, unknown>;\n const candidate = isRest ? (obj.openapiUrl ?? obj.url) : (obj.graphqlUrl ?? obj.url);\n if (typeof candidate === 'string' && candidate) setEndpointUrl(candidate);\n }\n setConfigLoaded(true);\n }, [configQueryData, configQueryLoading, isRest]);\n\n const [setAgentPlaygroundConfig] = useSetAgentPlaygroundConfigMutation();\n\n const handleSave = async () => {\n if (!endpointUrl.trim() || !agentId) return;\n setSaving(true);\n setSaved(false);\n try {\n await setAgentPlaygroundConfig({\n variables: {\n agentId,\n profile,\n vibeId,\n openapiUrl: isRest ? endpointUrl.trim() : null,\n graphqlUrl: isRest ? null : endpointUrl.trim(),\n },\n });\n setSaved(true);\n setIframeKey((k) => k + 1); // reload iframe\n setTimeout(() => setSaved(false), 3000);\n } catch {\n // silently fail\n } finally {\n setSaving(false);\n }\n };\n\n // Iframe-token (Phase C): replaces the previous ?apiKey= URL param.\n // Token TTL <= 600s; refresh ~60s before expiry.\n const [iframeToken, setIframeToken] = useState<IframeTokenResponse | null>(null);\n const [tokenError, setTokenError] = useState<string | null>(null);\n // Agent iframe-token allow-list (audit-B P0-SEC-03 / agent commit\n // c916492) permits only `/ui/<plugin>[/...]` and\n // `/api/plugins/<plugin>/ui[/...]` for plugin surfaces. Earlier prefixes\n // (`/api/profiles/<profile>/<plugin>`, `/api/<plugin>`) are now banned\n // at mint time. Plugin REST that needs auth must route under one of\n // the sanctioned paths.\n const iframeTokenPrefixes = useMemo(\n () => [`/ui/${pluginPath}`, `/api/plugins/${pluginPath}/ui`],\n [pluginPath]\n );\n\n useEffect(() => {\n if (!agentId) return;\n let cancelled = false;\n let handle: IframeTokenAutoRefreshHandle | null = null;\n\n const onRefresh = (next: IframeTokenResponse) => {\n if (cancelled) return;\n setIframeToken(next);\n handle = scheduleScopedIframeTokenRefresh(\n agentId,\n profile,\n iframeTokenPrefixes,\n next,\n onRefresh,\n (err) => setTokenError(err.message)\n );\n };\n\n mintScopedIframeToken(agentId, profile, iframeTokenPrefixes)\n .then((tok) => {\n if (cancelled) return;\n setIframeToken(tok);\n handle = scheduleScopedIframeTokenRefresh(\n agentId,\n profile,\n iframeTokenPrefixes,\n tok,\n onRefresh,\n (err) => setTokenError(err.message)\n );\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n setTokenError(err instanceof Error ? err.message : String(err));\n });\n\n return () => {\n cancelled = true;\n handle?.cancel();\n };\n }, [agentId, profile, iframeTokenPrefixes]);\n\n // Pass the owning profile so the iframe builds profile-scoped API URLs.\n const iframeBaseUrl = `${tunnelUrl}/ui/${pluginPath}?vibeId=${vibeId}&profile=${encodeURIComponent(profile)}`;\n // F5 — iframe token rides the URL fragment, not the query string. The\n // agent's bootstrap shim at /ui/<plugin> reads `#vt=` client-side and\n // trades it for a path-scoped HttpOnly cookie via POST\n // /api/agent/ui-exchange.\n const iframeSrc = iframeToken\n ? appendIframeTokenFragment(iframeBaseUrl, iframeToken.token)\n : iframeBaseUrl;\n\n return (\n <div className=\"space-y-4\">\n {/* Config Bar */}\n <Card>\n <div className=\"p-4\">\n <div className=\"flex items-center gap-2 mb-2\">\n {isRest ? (\n <Globe className=\"size-5 text-text-secondary\" />\n ) : (\n <Network className=\"size-5 text-text-secondary\" />\n )}\n <h3 className=\"font-semibold text-text-primary\">\n {isRest\n ? tr('vibeDetails.restPlaygroundTitle', 'REST API Playground')\n : tr('vibeDetails.graphqlPlaygroundTitle', 'GraphQL Playground')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n <input\n aria-label=\"control\"\n type=\"url\"\n value={endpointUrl}\n onChange={(e) => setEndpointUrl(e.target.value)}\n placeholder={\n isRest\n ? tr(\n 'vibeDetails.enterSwaggerUrl',\n 'Enter OpenAPI/Swagger URL (e.g. https://petstore3.swagger.io/api/v3/openapi.json)'\n )\n : tr(\n 'vibeDetails.enterGraphqlUrl',\n 'Enter GraphQL endpoint URL (e.g. https://countries.trevorblades.com/graphql)'\n )\n }\n className=\"flex-1 px-3 py-2 bg-bg-sunken border border-border-default rounded-md text-text-primary placeholder:text-text-placeholder text-sm\"\n onKeyDown={(e) => e.key === 'Enter' && handleSave()}\n />\n <button\n type=\"button\"\n onClick={handleSave}\n disabled={saving || !endpointUrl.trim()}\n className=\"flex items-center gap-2 px-4 py-2 bg-action-primary-bg text-action-primary-text rounded-md hover:bg-action-primary-bg-hover transition-colors disabled:opacity-50 text-sm whitespace-nowrap\"\n >\n {saving ? (\n <Loader2 className=\"size-4 animate-spin\" />\n ) : saved ? (\n <Check className=\"size-4\" />\n ) : (\n <ExternalLinkIcon className=\"size-4\" />\n )}\n {saving\n ? tr('common.saving', 'Saving...')\n : saved\n ? tr('common.saved', 'Saved!')\n : tr('vibeDetails.loadApi', 'Load API')}\n </button>\n </div>\n {!configLoaded && (\n <p className=\"text-xs text-text-muted mt-1\">\n {tr('vibeDetails.loadingConfig', 'Loading saved configuration...')}\n </p>\n )}\n </div>\n </Card>\n\n {/* Iframe (gated on iframe-token mint when agentId is present) */}\n {agentId && !iframeToken ? (\n <div className=\"p-8 text-center text-sm text-text-secondary rounded-md border border-border-default bg-bg-sunken\">\n {tokenError\n ? tr('vibeDetails.iframeTokenError', `Iframe token error: ${tokenError}`)\n : tr('vibeDetails.mintingIframeToken', 'Authorizing iframe…')}\n </div>\n ) : (\n <iframe\n key={iframeKey}\n src={iframeSrc}\n title={isRest ? 'REST API Playground' : 'GraphQL Playground'}\n className=\"w-full rounded-md border border-border-default bg-bg-sunken\"\n style={{ height: '700px' }}\n sandbox=\"allow-scripts allow-same-origin allow-forms allow-popups\"\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1,53 +1,72 @@
|
|
|
1
1
|
import { agentAiMutate as e } from "./aiApi.js";
|
|
2
2
|
import { agentApiPath as t } from "../utils/agentApiPath.js";
|
|
3
|
-
var n = 600, r = 6e4
|
|
4
|
-
|
|
3
|
+
var n = 600, r = 6e4, i = /^\/terminal\/[A-Za-z0-9_-]{1,128}(?:\/.*)?$/, a = /^\/ui\/[a-z0-9-]{1,64}(?:\/.*)?$/, o = /^\/plan\/[A-Za-z0-9_-]{1,128}(?:\/.*)?$/, s = /^\/code-server(?:\/.*)?$/, c = /^\/api\/plugins\/[a-z0-9-]{1,64}\/ui(?:\/.*)?$/, l = new Set([
|
|
4
|
+
"/",
|
|
5
|
+
"/api",
|
|
6
|
+
"/api/agent",
|
|
7
|
+
"/api/plugins",
|
|
8
|
+
"/api/profiles",
|
|
9
|
+
"/health",
|
|
10
|
+
"/metrics",
|
|
11
|
+
"/ws",
|
|
12
|
+
"/api/ai",
|
|
13
|
+
"/api/logs"
|
|
14
|
+
]);
|
|
15
|
+
function u(e) {
|
|
16
|
+
return typeof e != "string" || e.length === 0 || e.includes("\0") || e.includes("\n") || e.includes("|") || e.includes("..") || l.has(e) ? !1 : !!(i.test(e) || a.test(e) || o.test(e) || e === "/ws/logs" || s.test(e) || c.test(e));
|
|
17
|
+
}
|
|
18
|
+
function d(e, t) {
|
|
19
|
+
let n = [];
|
|
20
|
+
for (let t of e) u(t) || n.push(t);
|
|
21
|
+
if (n.length > 0) throw Error(`iframe-token mint blocked: ${n.length} prefix(es) outside agent allow-list (${n.join(", ")}) for scope=${t}. Migrate the iframe REST surface to /api/plugins/<plugin>/ui or /ui/<plugin>.`);
|
|
22
|
+
}
|
|
23
|
+
async function f(r, i, a, o = n) {
|
|
5
24
|
let s = Math.max(1, Math.min(600, Math.floor(o)));
|
|
6
|
-
return e(r, t(i, "/agent/iframe-token"), "POST", {
|
|
25
|
+
return d([a], "mintIframeToken"), e(r, t(i, "/agent/iframe-token"), "POST", {
|
|
7
26
|
pathPrefix: a,
|
|
8
27
|
ttlSeconds: s
|
|
9
28
|
});
|
|
10
29
|
}
|
|
11
|
-
async function
|
|
30
|
+
async function p(r, i, a, o = n) {
|
|
12
31
|
let s = Math.max(1, Math.min(600, Math.floor(o)));
|
|
13
|
-
return e(r, t(i, "/agent/iframe-token"), "POST", {
|
|
32
|
+
return d(a, "mintScopedIframeToken"), e(r, t(i, "/agent/iframe-token"), "POST", {
|
|
14
33
|
pathPrefixes: a,
|
|
15
34
|
ttlSeconds: s
|
|
16
35
|
});
|
|
17
36
|
}
|
|
18
|
-
function
|
|
19
|
-
let
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
37
|
+
function m(e, t, i, a, o, s, c = n) {
|
|
38
|
+
let l = !1, u = null, d = Date.parse(a.expiresAt), p = Math.max(1e3, d - Date.now() - r);
|
|
39
|
+
return u = setTimeout(() => {
|
|
40
|
+
l || f(e, t, i, c).then((e) => {
|
|
41
|
+
l || o(e);
|
|
23
42
|
}).catch((e) => {
|
|
24
|
-
|
|
43
|
+
l || s?.(e instanceof Error ? e : Error(String(e)));
|
|
25
44
|
});
|
|
26
45
|
}, p), { cancel: () => {
|
|
27
|
-
|
|
46
|
+
l = !0, u !== null && (clearTimeout(u), u = null);
|
|
28
47
|
} };
|
|
29
48
|
}
|
|
30
|
-
function
|
|
31
|
-
let
|
|
32
|
-
return
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
function h(e, t, i, a, o, s, c = n) {
|
|
50
|
+
let l = !1, u = null, d = Date.parse(a.expiresAt), f = Math.max(1e3, d - Date.now() - r);
|
|
51
|
+
return u = setTimeout(() => {
|
|
52
|
+
l || p(e, t, i, c).then((e) => {
|
|
53
|
+
l || o(e);
|
|
35
54
|
}).catch((e) => {
|
|
36
|
-
|
|
55
|
+
l || s?.(e instanceof Error ? e : Error(String(e)));
|
|
37
56
|
});
|
|
38
|
-
},
|
|
39
|
-
|
|
57
|
+
}, f), { cancel: () => {
|
|
58
|
+
l = !0, u !== null && (clearTimeout(u), u = null);
|
|
40
59
|
} };
|
|
41
60
|
}
|
|
42
|
-
function
|
|
61
|
+
function g(e, t) {
|
|
43
62
|
let n = e.indexOf("#");
|
|
44
63
|
return `${n === -1 ? e : e.slice(0, n)}#vt=${encodeURIComponent(t)}`;
|
|
45
64
|
}
|
|
46
|
-
function
|
|
65
|
+
function _(e, t) {
|
|
47
66
|
let n = e.indexOf("#"), r = n === -1 ? e : e.slice(0, n), i = n === -1 ? "" : e.slice(n);
|
|
48
67
|
return `${r}${r.includes("?") ? "&" : "?"}vt=${encodeURIComponent(t)}${i}`;
|
|
49
68
|
}
|
|
50
69
|
//#endregion
|
|
51
|
-
export {
|
|
70
|
+
export { g as appendIframeTokenFragment, _ as appendIframeTokenQuery, f as mintIframeToken, p as mintScopedIframeToken, m as scheduleIframeTokenRefresh, h as scheduleScopedIframeTokenRefresh };
|
|
52
71
|
|
|
53
72
|
//# sourceMappingURL=iframeToken.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iframeToken.js","names":[],"sources":["../../src/services/iframeToken.ts"],"sourcesContent":["/**\n * iframe-token service\n *\n * Mints short-lived signed tokens for iframe embeds talking to the\n * vibecontrols-agent. Replaces the previous Referer/api-key-in-URL flow\n * dropped by the agent in Phase C.\n *\n * Backend route: POST /api/agent/iframe-token\n * Request: { pathPrefix: string, ttlSeconds?: number }\n * Response: { token: string, expiresAt: string } // ISO-8601\n *\n * Hard limit: ttlSeconds <= 600 (agent enforces; we cap client-side too).\n */\n\nimport { agentAiMutate } from './aiApi';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\nexport const MAX_IFRAME_TOKEN_TTL_SECONDS = 600;\nconst DEFAULT_TTL_SECONDS = 600;\nconst REFRESH_LEEWAY_MS = 60_000;\n\nexport interface IframeTokenResponse {\n token: string;\n expiresAt: string;\n}\n\nexport interface IframeTokenAutoRefreshHandle {\n /** Cancels the pending refresh timer. Idempotent. */\n cancel: () => void;\n}\n\n/**\n * Mint an iframe token for the given agent + path prefix.\n *\n * @param agentId The agent's GraphQL id.\n * @param profile Profile name on the agent that owns this iframe token (canonical URL hierarchy).\n * @param pathPrefix Iframe path prefix the token should authorize (e.g. '/ungit', '/ui/graphiql').\n * @param ttlSeconds Token lifetime; clamped to (0, 600].\n */\nexport async function mintIframeToken(\n agentId: string,\n profile: string,\n pathPrefix: string,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefix, ttlSeconds: ttl }\n );\n}\n\n/**\n * Mint a v2 scoped iframe token bound to an allow-list of path prefixes.\n * UI plugins use this so a single token can reach both `/ui/<plugin>`\n * (where the iframe HTML lives) and the plugin's REST surface\n * (e.g. `/api/profiles/*\\/<plugin>/*`) without exposing the master apiKey.\n *\n * Agent enforces the prefix list on every request via\n * `verifyIframeToken` — calls outside the declared prefixes are rejected\n * regardless of how the iframe acquired the token.\n */\nexport async function mintScopedIframeToken(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefixes, ttlSeconds: ttl }\n );\n}\n\n/**\n * Schedule an auto-refresh that re-mints the token ~60s before expiry.\n * Returns a handle whose `cancel()` clears the timer.\n *\n * Caller is responsible for re-rendering / updating iframe `src` on each\n * refresh by reacting to the `onRefresh` callback's payload.\n */\nexport function scheduleIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefix: string,\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintIframeToken(agentId, profile, pathPrefix, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Scoped-token variant of {@link scheduleIframeTokenRefresh}. Re-mints a\n * v2 token bound to the same prefix allow-list ~60s before expiry. Used by\n * UI plugins whose iframe also calls the plugin's profile-scoped REST\n * surface, so the refreshed token keeps authorizing both `/ui/<plugin>`\n * and `/api/profiles/<profile>/<plugin>/*`.\n */\nexport function scheduleScopedIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintScopedIframeToken(agentId, profile, pathPrefixes, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Append the iframe token as a `#vt=` URL fragment to the given URL\n * (F5 — security cutover, primary entry point for /ui/<plugin> embeds).\n *\n * Fragments never reach the server, never appear in proxy access logs,\n * never land in the browser's Referer header on outbound sub-requests,\n * and aren't persisted by middlebox URL loggers. The agent's bootstrap\n * shim at `/ui/<plugin>` reads the fragment client-side and POSTs it to\n * `/api/agent/ui-exchange`, which trades the single-use iframe token\n * for a path-scoped HttpOnly cookie (`vt_ui_<plugin>`). Subsequent\n * sub-resource requests under `/ui/<plugin>/` auto-attach the cookie.\n *\n * Use this for any new iframe embed. The legacy\n * `appendIframeTokenParam(url, token)` helper was removed as part of\n * the F5 residual hard-cutover — the Ungit surface at\n * `/api/plugins/<plugin>/ui` is now behind an equivalent host-level\n * cookie shim (sibling of the F5 `/ui/<plugin>` shim) that exchanges\n * the `#vt=` fragment for a path-scoped HttpOnly cookie. There are\n * no remaining `?vt=` URL-query callers.\n */\nexport function appendIframeTokenFragment(url: string, token: string): string {\n // Strip any existing fragment so we never end up with two `#`s.\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n return `${base}#vt=${encodeURIComponent(token)}`;\n}\n\n/**\n * F5 RESIDUAL — `?vt=<token>` query-string fallback for embeds whose\n * upstream reverse proxy validates the entry document via query auth and\n * issues its own session cookie (e.g. the external `vibe-plugin-tool-git`\n * Ungit proxy at `/ungit/`). Migrating these to the fragment+cookie shim\n * needs a coordinated plugin SDK contract change + republish of the\n * plugin package — out of band for the F5 cutover.\n *\n * The exposure is narrow:\n * - F6 makes every iframe token single-use (replay → 401).\n * - F7 strips the token from the proxy's upstream forwards.\n * - F10 enforces Origin/CSRF on non-GET/HEAD sub-requests.\n *\n * Do NOT use for new embeds. Prefer `appendIframeTokenFragment`.\n */\nexport function appendIframeTokenQuery(url: string, token: string): string {\n const fragmentIdx = url.indexOf('#');\n const baseAndQuery = fragmentIdx === -1 ? url : url.slice(0, fragmentIdx);\n const fragment = fragmentIdx === -1 ? '' : url.slice(fragmentIdx);\n const separator = baseAndQuery.includes('?') ? '&' : '?';\n return `${baseAndQuery}${separator}vt=${encodeURIComponent(token)}${fragment}`;\n}\n"],"mappings":";;AAkBA,IAAM,IAAsB,KACtB,IAAoB;AAoB1B,eAAsB,EACpB,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAM,IAAM,KAAK,IAAI,GAAG,KAAK,IAAA,KAAkC,KAAK,MAAM,EAAW,CAAC,CAAC;AACvF,QAAO,EACL,GACA,EAAa,GAAS,sBAAsB,EAC5C,QACA;EAAE;EAAY,YAAY;EAAK,CAChC;;AAaH,eAAsB,EACpB,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAM,IAAM,KAAK,IAAI,GAAG,KAAK,IAAA,KAAkC,KAAK,MAAM,EAAW,CAAC,CAAC;AACvF,QAAO,EACL,GACA,EAAa,GAAS,sBAAsB,EAC5C,QACA;EAAE;EAAc,YAAY;EAAK,CAClC;;AAUH,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAI,IAAY,IACZ,IAA8C,MAE5C,IAAc,KAAK,MAAM,EAAQ,UAAU,EAC3C,IAAQ,KAAK,IAAI,KAAO,IAAc,KAAK,KAAK,GAAG,EAAkB;AAe3E,QAbA,IAAQ,iBAAiB;AACnB,OACJ,EAAgB,GAAS,GAAS,GAAY,EAAW,CACtD,MAAM,MAAS;AACV,QACJ,EAAU,EAAK;IACf,CACD,OAAO,MAAiB;AACnB,QACJ,IAAU,aAAe,QAAQ,IAAU,MAAM,OAAO,EAAI,CAAC,CAAC;IAC9D;IACH,EAAM,EAEF,EACL,cAAc;AAEZ,EADA,IAAY,IACR,MAAU,SACZ,aAAa,EAAM,EACnB,IAAQ;IAGb;;AAUH,SAAgB,EACd,GACA,GACA,GACA,GACA,GACA,GACA,IAAqB,GACS;CAC9B,IAAI,IAAY,IACZ,IAA8C,MAE5C,IAAc,KAAK,MAAM,EAAQ,UAAU,EAC3C,IAAQ,KAAK,IAAI,KAAO,IAAc,KAAK,KAAK,GAAG,EAAkB;AAe3E,QAbA,IAAQ,iBAAiB;AACnB,OACJ,EAAsB,GAAS,GAAS,GAAc,EAAW,CAC9D,MAAM,MAAS;AACV,QACJ,EAAU,EAAK;IACf,CACD,OAAO,MAAiB;AACnB,QACJ,IAAU,aAAe,QAAQ,IAAU,MAAM,OAAO,EAAI,CAAC,CAAC;IAC9D;IACH,EAAM,EAEF,EACL,cAAc;AAEZ,EADA,IAAY,IACR,MAAU,SACZ,aAAa,EAAM,EACnB,IAAQ;IAGb;;AAuBH,SAAgB,EAA0B,GAAa,GAAuB;CAE5E,IAAM,IAAU,EAAI,QAAQ,IAAI;AAEhC,QAAO,GADM,MAAY,KAAK,IAAM,EAAI,MAAM,GAAG,EAAQ,CAC1C,MAAM,mBAAmB,EAAM;;AAkBhD,SAAgB,EAAuB,GAAa,GAAuB;CACzE,IAAM,IAAc,EAAI,QAAQ,IAAI,EAC9B,IAAe,MAAgB,KAAK,IAAM,EAAI,MAAM,GAAG,EAAY,EACnE,IAAW,MAAgB,KAAK,KAAK,EAAI,MAAM,EAAY;AAEjE,QAAO,GAAG,IADQ,EAAa,SAAS,IAAI,GAAG,MAAM,IAClB,KAAK,mBAAmB,EAAM,GAAG"}
|
|
1
|
+
{"version":3,"file":"iframeToken.js","names":[],"sources":["../../src/services/iframeToken.ts"],"sourcesContent":["/**\n * iframe-token service\n *\n * Mints short-lived signed tokens for iframe embeds talking to the\n * vibecontrols-agent. Replaces the previous Referer/api-key-in-URL flow\n * dropped by the agent in Phase C.\n *\n * Backend route: POST /api/agent/iframe-token\n * Request: { pathPrefix: string, ttlSeconds?: number }\n * Response: { token: string, expiresAt: string } // ISO-8601\n *\n * Hard limit: ttlSeconds <= 600 (agent enforces; we cap client-side too).\n */\n\nimport { agentAiMutate } from './aiApi';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\nexport const MAX_IFRAME_TOKEN_TTL_SECONDS = 600;\nconst DEFAULT_TTL_SECONDS = 600;\nconst REFRESH_LEEWAY_MS = 60_000;\n\n/**\n * Client-side mirror of the agent's iframe-token `pathPrefix` allow-list\n * (`vibecontrols-agent/src/core/iframe-tokens.ts::isValidIframePathPrefix`).\n * audit-B P0-SEC-03 / agent commit c916492 — anything outside this list is\n * rejected at the agent. We validate locally so the UI surfaces a clear\n * error instead of a 400 round-trip.\n */\nconst IFRAME_PREFIX_TERMINAL_RE = /^\\/terminal\\/[A-Za-z0-9_-]{1,128}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_UI_RE = /^\\/ui\\/[a-z0-9-]{1,64}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_PLAN_RE = /^\\/plan\\/[A-Za-z0-9_-]{1,128}(?:\\/.*)?$/;\nconst IFRAME_PREFIX_CODE_SERVER_RE = /^\\/code-server(?:\\/.*)?$/;\nconst IFRAME_PREFIX_PLUGIN_UI_RE = /^\\/api\\/plugins\\/[a-z0-9-]{1,64}\\/ui(?:\\/.*)?$/;\nconst BANNED_PREFIXES = new Set<string>([\n '/',\n '/api',\n '/api/agent',\n '/api/plugins',\n '/api/profiles',\n '/health',\n '/metrics',\n '/ws',\n '/api/ai',\n '/api/logs',\n]);\n\nexport function isAllowedAgentIframePrefix(prefix: string): boolean {\n if (typeof prefix !== 'string' || prefix.length === 0) return false;\n if (prefix.includes('\\0') || prefix.includes('\\n') || prefix.includes('|')) return false;\n if (prefix.includes('..')) return false;\n if (BANNED_PREFIXES.has(prefix)) return false;\n if (IFRAME_PREFIX_TERMINAL_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_UI_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_PLAN_RE.test(prefix)) return true;\n if (prefix === '/ws/logs') return true;\n if (IFRAME_PREFIX_CODE_SERVER_RE.test(prefix)) return true;\n if (IFRAME_PREFIX_PLUGIN_UI_RE.test(prefix)) return true;\n return false;\n}\n\nfunction assertAllowedPrefixes(prefixes: readonly string[], scope: string): void {\n const rejected: string[] = [];\n for (const p of prefixes) {\n if (!isAllowedAgentIframePrefix(p)) rejected.push(p);\n }\n if (rejected.length > 0) {\n // Hard-fail rather than silently passing through — the agent would\n // reject the whole mint anyway, and silently dropping prefixes\n // would let the iframe come up partially broken.\n throw new Error(\n `iframe-token mint blocked: ${rejected.length} prefix(es) outside agent allow-list ` +\n `(${rejected.join(', ')}) for scope=${scope}. Migrate the iframe REST surface to ` +\n `/api/plugins/<plugin>/ui or /ui/<plugin>.`\n );\n }\n}\n\nexport interface IframeTokenResponse {\n token: string;\n expiresAt: string;\n}\n\nexport interface IframeTokenAutoRefreshHandle {\n /** Cancels the pending refresh timer. Idempotent. */\n cancel: () => void;\n}\n\n/**\n * Mint an iframe token for the given agent + path prefix.\n *\n * @param agentId The agent's GraphQL id.\n * @param profile Profile name on the agent that owns this iframe token (canonical URL hierarchy).\n * @param pathPrefix Iframe path prefix the token should authorize (e.g. '/ungit', '/ui/graphiql').\n * @param ttlSeconds Token lifetime; clamped to (0, 600].\n */\nexport async function mintIframeToken(\n agentId: string,\n profile: string,\n pathPrefix: string,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n assertAllowedPrefixes([pathPrefix], 'mintIframeToken');\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefix, ttlSeconds: ttl }\n );\n}\n\n/**\n * Mint a v2 scoped iframe token bound to an allow-list of path prefixes.\n * UI plugins use this so a single token can reach both `/ui/<plugin>`\n * (where the iframe HTML lives) and the plugin's REST surface\n * (e.g. `/api/profiles/*\\/<plugin>/*`) without exposing the master apiKey.\n *\n * Agent enforces the prefix list on every request via\n * `verifyIframeToken` — calls outside the declared prefixes are rejected\n * regardless of how the iframe acquired the token.\n */\nexport async function mintScopedIframeToken(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): Promise<IframeTokenResponse> {\n const ttl = Math.max(1, Math.min(MAX_IFRAME_TOKEN_TTL_SECONDS, Math.floor(ttlSeconds)));\n assertAllowedPrefixes(pathPrefixes, 'mintScopedIframeToken');\n return agentAiMutate<IframeTokenResponse>(\n agentId,\n agentApiPath(profile, '/agent/iframe-token'),\n 'POST',\n { pathPrefixes, ttlSeconds: ttl }\n );\n}\n\n/**\n * Schedule an auto-refresh that re-mints the token ~60s before expiry.\n * Returns a handle whose `cancel()` clears the timer.\n *\n * Caller is responsible for re-rendering / updating iframe `src` on each\n * refresh by reacting to the `onRefresh` callback's payload.\n */\nexport function scheduleIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefix: string,\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintIframeToken(agentId, profile, pathPrefix, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Scoped-token variant of {@link scheduleIframeTokenRefresh}. Re-mints a\n * v2 token bound to the same prefix allow-list ~60s before expiry. Used by\n * UI plugins whose iframe also calls the plugin's profile-scoped REST\n * surface, so the refreshed token keeps authorizing both `/ui/<plugin>`\n * and `/api/profiles/<profile>/<plugin>/*`.\n */\nexport function scheduleScopedIframeTokenRefresh(\n agentId: string,\n profile: string,\n pathPrefixes: string[],\n current: IframeTokenResponse,\n onRefresh: (next: IframeTokenResponse) => void,\n onError?: (err: Error) => void,\n ttlSeconds: number = DEFAULT_TTL_SECONDS\n): IframeTokenAutoRefreshHandle {\n let cancelled = false;\n let timer: ReturnType<typeof setTimeout> | null = null;\n\n const expiresAtMs = Date.parse(current.expiresAt);\n const delay = Math.max(1_000, expiresAtMs - Date.now() - REFRESH_LEEWAY_MS);\n\n timer = setTimeout(() => {\n if (cancelled) return;\n mintScopedIframeToken(agentId, profile, pathPrefixes, ttlSeconds)\n .then((next) => {\n if (cancelled) return;\n onRefresh(next);\n })\n .catch((err: unknown) => {\n if (cancelled) return;\n onError?.(err instanceof Error ? err : new Error(String(err)));\n });\n }, delay);\n\n return {\n cancel: () => {\n cancelled = true;\n if (timer !== null) {\n clearTimeout(timer);\n timer = null;\n }\n },\n };\n}\n\n/**\n * Append the iframe token as a `#vt=` URL fragment to the given URL\n * (F5 — security cutover, primary entry point for /ui/<plugin> embeds).\n *\n * Fragments never reach the server, never appear in proxy access logs,\n * never land in the browser's Referer header on outbound sub-requests,\n * and aren't persisted by middlebox URL loggers. The agent's bootstrap\n * shim at `/ui/<plugin>` reads the fragment client-side and POSTs it to\n * `/api/agent/ui-exchange`, which trades the single-use iframe token\n * for a path-scoped HttpOnly cookie (`vt_ui_<plugin>`). Subsequent\n * sub-resource requests under `/ui/<plugin>/` auto-attach the cookie.\n *\n * Use this for any new iframe embed. The legacy\n * `appendIframeTokenParam(url, token)` helper was removed as part of\n * the F5 residual hard-cutover — the Ungit surface at\n * `/api/plugins/<plugin>/ui` is now behind an equivalent host-level\n * cookie shim (sibling of the F5 `/ui/<plugin>` shim) that exchanges\n * the `#vt=` fragment for a path-scoped HttpOnly cookie. There are\n * no remaining `?vt=` URL-query callers.\n */\nexport function appendIframeTokenFragment(url: string, token: string): string {\n // Strip any existing fragment so we never end up with two `#`s.\n const hashIdx = url.indexOf('#');\n const base = hashIdx === -1 ? url : url.slice(0, hashIdx);\n return `${base}#vt=${encodeURIComponent(token)}`;\n}\n\n/**\n * F5 RESIDUAL — `?vt=<token>` query-string fallback for embeds whose\n * upstream reverse proxy validates the entry document via query auth and\n * issues its own session cookie (e.g. the external `vibe-plugin-tool-git`\n * Ungit proxy at `/ungit/`). Migrating these to the fragment+cookie shim\n * needs a coordinated plugin SDK contract change + republish of the\n * plugin package — out of band for the F5 cutover.\n *\n * The exposure is narrow:\n * - F6 makes every iframe token single-use (replay → 401).\n * - F7 strips the token from the proxy's upstream forwards.\n * - F10 enforces Origin/CSRF on non-GET/HEAD sub-requests.\n *\n * Do NOT use for new embeds. Prefer `appendIframeTokenFragment`.\n */\nexport function appendIframeTokenQuery(url: string, token: string): string {\n const fragmentIdx = url.indexOf('#');\n const baseAndQuery = fragmentIdx === -1 ? url : url.slice(0, fragmentIdx);\n const fragment = fragmentIdx === -1 ? '' : url.slice(fragmentIdx);\n const separator = baseAndQuery.includes('?') ? '&' : '?';\n return `${baseAndQuery}${separator}vt=${encodeURIComponent(token)}${fragment}`;\n}\n"],"mappings":"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"}
|