@burdenoff/microfe-vibecontrols 2026.531.8 → 2026.531.10

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.
@@ -129,7 +129,7 @@ function m({ value: m, onChange: h, provider: g, agentId: _, profile: v }) {
129
129
  }
130
130
  x((e) => !e);
131
131
  },
132
- className: "flex items-center gap-1 px-2 py-1 text-xs font-medium rounded border border-border-default bg-bg-surface text-text-secondary hover:text-text-primary hover:border-border-strong transition-colors max-w-[160px]",
132
+ className: "flex items-center gap-1 px-2 py-1 text-xs font-medium rounded border border-border-default bg-bg-surface text-text-primary hover:bg-bg-elevated hover:border-border-strong transition-colors max-w-[160px]",
133
133
  title: m,
134
134
  children: [/* @__PURE__ */ c("span", {
135
135
  className: "truncate",
@@ -158,7 +158,7 @@ function m({ value: m, onChange: h, provider: g, agentId: _, profile: v }) {
158
158
  onClick: () => {
159
159
  h(e), x(!1);
160
160
  },
161
- className: `w-full text-left px-3 py-2 text-sm hover:bg-bg-elevated transition-colors ${e === m ? "text-action-primary-text bg-action-primary-bg/10" : "text-text-primary"}`,
161
+ className: `w-full text-left px-3 py-2 text-sm hover:bg-bg-elevated transition-colors ${e === m ? "text-text-primary bg-bg-sunken font-medium" : "text-text-primary"}`,
162
162
  children: e
163
163
  }, e))] }) }, e.sdk)),
164
164
  N.every((e) => e.models.length === 0) && /* @__PURE__ */ c("div", {
@@ -1 +1 @@
1
- {"version":3,"file":"ModelSelector.js","names":[],"sources":["../../../../src/components/agent-manager/controls/ModelSelector.tsx"],"sourcesContent":["/**\n * Model selector dropdown.\n *\n * Displays a compact button with the current model name that opens a\n * grouped dropdown of models filtered by SDK provider.\n */\n\nimport { useState, useRef, useEffect, useCallback } from 'react';\nimport { ChevronDown } from 'lucide-react';\nimport { fetchModels } from '@/services/aiApi';\nimport type { AgentSdkType } from '@/types/agentManager';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface ModelSelectorProps {\n value: string;\n onChange: (model: string) => void;\n provider?: AgentSdkType;\n agentId?: string;\n /** Canonical CLI profile name on the remote agent. */\n profile?: string;\n}\n\ninterface ModelGroup {\n label: string;\n sdk: AgentSdkType;\n models: string[];\n}\n\nconst MODEL_GROUPS: ModelGroup[] = [\n { label: 'Claude', sdk: 'claude', models: [] },\n { label: 'Codex', sdk: 'codex', models: [] },\n { label: 'Gemini', sdk: 'gemini', models: [] },\n { label: 'OpenCode', sdk: 'opencode', models: [] },\n { label: 'OpenAI-compatible', sdk: 'openai-compat', models: [] },\n { label: 'Goose', sdk: 'goose', models: [] },\n { label: 'Amp', sdk: 'amp', models: [] },\n { label: 'Copilot', sdk: 'copilot', models: [] },\n { label: 'Crush', sdk: 'crush', models: [] },\n { label: 'Pi', sdk: 'pi', models: [] },\n { label: 'Aider', sdk: 'aider', models: [] },\n { label: 'Plandex', sdk: 'plandex', models: [] },\n];\n\n/** Truncate model name for compact display */\nfunction displayName(model: string): string {\n // Remove common prefixes for compact display\n return model\n .replace(/^opencode\\//, '')\n .replace('claude-', '')\n .replace('-20250514', '')\n .replace('-20251001', '');\n}\n\nfunction modelGroupLabel(provider: AgentSdkType): string {\n return MODEL_GROUPS.find((group) => group.sdk === provider)?.label ?? provider;\n}\n\nfunction uniqueModels(models: string[], currentValue: string): string[] {\n return Array.from(new Set([currentValue, ...models].filter(Boolean)));\n}\n\nexport function ModelSelector({ value, onChange, provider, agentId, profile }: ModelSelectorProps) {\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [agentModels, setAgentModels] = useState<string[] | null>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const [pos, setPos] = useState({ bottom: 0, left: 0 });\n\n const filteredGroups =\n provider && agentModels\n ? [\n {\n label: modelGroupLabel(provider),\n sdk: provider,\n models: uniqueModels(agentModels, value),\n },\n ]\n : provider\n ? MODEL_GROUPS.filter((g) => g.sdk === provider)\n : MODEL_GROUPS;\n\n const handleClickOutside = useCallback((e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n }, []);\n\n useEffect(() => {\n if (open) {\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }\n }, [open, handleClickOutside]);\n\n useEffect(() => {\n if (!open) setSearch('');\n }, [open]);\n\n useEffect(() => {\n if (!open || !agentId || !provider) return;\n\n let cancelled = false;\n setAgentModels(null);\n void fetchModels({ id: agentId, profile: profile ?? 'default' }, provider)\n .then((models) => {\n if (cancelled) return;\n const modelIds = models.map((model) => model.id).filter(Boolean);\n setAgentModels(modelIds);\n })\n .catch(() => {\n if (!cancelled) setAgentModels([]);\n });\n\n return () => {\n cancelled = true;\n };\n }, [agentId, open, profile, provider]);\n\n const searchLower = search.toLowerCase();\n const searchedGroups = filteredGroups.map((g) => ({\n ...g,\n models: search ? g.models.filter((m) => m.toLowerCase().includes(searchLower)) : g.models,\n }));\n\n const handleToggle = () => {\n if (!open && buttonRef.current) {\n const rect = buttonRef.current.getBoundingClientRect();\n setPos({ bottom: window.innerHeight - rect.top + 4, left: rect.left });\n }\n setOpen((prev) => !prev);\n };\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n ref={buttonRef}\n type=\"button\"\n onClick={handleToggle}\n className=\"flex items-center gap-1 px-2 py-1 text-xs font-medium rounded border border-border-default bg-bg-surface text-text-secondary hover:text-text-primary hover:border-border-strong transition-colors max-w-[160px]\"\n title={value}\n >\n <span className=\"truncate\">{displayName(value)}</span>\n <ChevronDown className=\"size-3 flex-shrink-0\" />\n </button>\n\n {open && (\n <div\n className=\"fixed w-64 max-h-72 overflow-y-auto rounded-lg border border-border-default bg-bg-surface shadow-lg z-[100]\"\n style={{ bottom: pos.bottom, left: pos.left }}\n >\n <input\n aria-label=\"Search...\"\n type=\"text\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search...\"\n className=\"w-full px-3 py-1.5 text-xs border-b border-border-default bg-transparent text-text-primary placeholder:text-text-tertiary focus:outline-none sticky top-0 bg-bg-surface\"\n />\n {searchedGroups.map((group) => (\n <div key={group.sdk}>\n {group.models.length > 0 && (\n <>\n <div className=\"px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-text-tertiary bg-bg-sunken\">\n {group.label}\n </div>\n {group.models.map((model) => (\n <button\n key={model}\n type=\"button\"\n onClick={() => {\n onChange(model);\n setOpen(false);\n }}\n className={`w-full text-left px-3 py-2 text-sm hover:bg-bg-elevated transition-colors ${\n model === value\n ? 'text-action-primary-text bg-action-primary-bg/10'\n : 'text-text-primary'\n }`}\n >\n {model}\n </button>\n ))}\n </>\n )}\n </div>\n ))}\n {searchedGroups.every((g) => g.models.length === 0) && (\n <div className=\"px-3 py-4 text-sm text-text-tertiary text-center\">\n {tr('vibecontrols.agentManager.controls.noModelsAvailable', 'No models available')}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"ModelSelector.js","names":[],"sources":["../../../../src/components/agent-manager/controls/ModelSelector.tsx"],"sourcesContent":["/**\n * Model selector dropdown.\n *\n * Displays a compact button with the current model name that opens a\n * grouped dropdown of models filtered by SDK provider.\n */\n\nimport { useState, useRef, useEffect, useCallback } from 'react';\nimport { ChevronDown } from 'lucide-react';\nimport { fetchModels } from '@/services/aiApi';\nimport type { AgentSdkType } from '@/types/agentManager';\nimport { useTr } from '../../../shared/hooks/useTr';\n\ninterface ModelSelectorProps {\n value: string;\n onChange: (model: string) => void;\n provider?: AgentSdkType;\n agentId?: string;\n /** Canonical CLI profile name on the remote agent. */\n profile?: string;\n}\n\ninterface ModelGroup {\n label: string;\n sdk: AgentSdkType;\n models: string[];\n}\n\nconst MODEL_GROUPS: ModelGroup[] = [\n { label: 'Claude', sdk: 'claude', models: [] },\n { label: 'Codex', sdk: 'codex', models: [] },\n { label: 'Gemini', sdk: 'gemini', models: [] },\n { label: 'OpenCode', sdk: 'opencode', models: [] },\n { label: 'OpenAI-compatible', sdk: 'openai-compat', models: [] },\n { label: 'Goose', sdk: 'goose', models: [] },\n { label: 'Amp', sdk: 'amp', models: [] },\n { label: 'Copilot', sdk: 'copilot', models: [] },\n { label: 'Crush', sdk: 'crush', models: [] },\n { label: 'Pi', sdk: 'pi', models: [] },\n { label: 'Aider', sdk: 'aider', models: [] },\n { label: 'Plandex', sdk: 'plandex', models: [] },\n];\n\n/** Truncate model name for compact display */\nfunction displayName(model: string): string {\n // Remove common prefixes for compact display\n return model\n .replace(/^opencode\\//, '')\n .replace('claude-', '')\n .replace('-20250514', '')\n .replace('-20251001', '');\n}\n\nfunction modelGroupLabel(provider: AgentSdkType): string {\n return MODEL_GROUPS.find((group) => group.sdk === provider)?.label ?? provider;\n}\n\nfunction uniqueModels(models: string[], currentValue: string): string[] {\n return Array.from(new Set([currentValue, ...models].filter(Boolean)));\n}\n\nexport function ModelSelector({ value, onChange, provider, agentId, profile }: ModelSelectorProps) {\n const tr = useTr();\n const [open, setOpen] = useState(false);\n const [search, setSearch] = useState('');\n const [agentModels, setAgentModels] = useState<string[] | null>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n const buttonRef = useRef<HTMLButtonElement>(null);\n const [pos, setPos] = useState({ bottom: 0, left: 0 });\n\n const filteredGroups =\n provider && agentModels\n ? [\n {\n label: modelGroupLabel(provider),\n sdk: provider,\n models: uniqueModels(agentModels, value),\n },\n ]\n : provider\n ? MODEL_GROUPS.filter((g) => g.sdk === provider)\n : MODEL_GROUPS;\n\n const handleClickOutside = useCallback((e: MouseEvent) => {\n if (containerRef.current && !containerRef.current.contains(e.target as Node)) {\n setOpen(false);\n }\n }, []);\n\n useEffect(() => {\n if (open) {\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }\n }, [open, handleClickOutside]);\n\n useEffect(() => {\n if (!open) setSearch('');\n }, [open]);\n\n useEffect(() => {\n if (!open || !agentId || !provider) return;\n\n let cancelled = false;\n setAgentModels(null);\n void fetchModels({ id: agentId, profile: profile ?? 'default' }, provider)\n .then((models) => {\n if (cancelled) return;\n const modelIds = models.map((model) => model.id).filter(Boolean);\n setAgentModels(modelIds);\n })\n .catch(() => {\n if (!cancelled) setAgentModels([]);\n });\n\n return () => {\n cancelled = true;\n };\n }, [agentId, open, profile, provider]);\n\n const searchLower = search.toLowerCase();\n const searchedGroups = filteredGroups.map((g) => ({\n ...g,\n models: search ? g.models.filter((m) => m.toLowerCase().includes(searchLower)) : g.models,\n }));\n\n const handleToggle = () => {\n if (!open && buttonRef.current) {\n const rect = buttonRef.current.getBoundingClientRect();\n setPos({ bottom: window.innerHeight - rect.top + 4, left: rect.left });\n }\n setOpen((prev) => !prev);\n };\n\n return (\n <div ref={containerRef} className=\"relative\">\n <button\n ref={buttonRef}\n type=\"button\"\n onClick={handleToggle}\n className=\"flex items-center gap-1 px-2 py-1 text-xs font-medium rounded border border-border-default bg-bg-surface text-text-primary hover:bg-bg-elevated hover:border-border-strong transition-colors max-w-[160px]\"\n title={value}\n >\n <span className=\"truncate\">{displayName(value)}</span>\n <ChevronDown className=\"size-3 flex-shrink-0\" />\n </button>\n\n {open && (\n <div\n className=\"fixed w-64 max-h-72 overflow-y-auto rounded-lg border border-border-default bg-bg-surface shadow-lg z-[100]\"\n style={{ bottom: pos.bottom, left: pos.left }}\n >\n <input\n aria-label=\"Search...\"\n type=\"text\"\n value={search}\n onChange={(e) => setSearch(e.target.value)}\n placeholder=\"Search...\"\n className=\"w-full px-3 py-1.5 text-xs border-b border-border-default bg-transparent text-text-primary placeholder:text-text-tertiary focus:outline-none sticky top-0 bg-bg-surface\"\n />\n {searchedGroups.map((group) => (\n <div key={group.sdk}>\n {group.models.length > 0 && (\n <>\n <div className=\"px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-text-tertiary bg-bg-sunken\">\n {group.label}\n </div>\n {group.models.map((model) => (\n <button\n key={model}\n type=\"button\"\n onClick={() => {\n onChange(model);\n setOpen(false);\n }}\n className={`w-full text-left px-3 py-2 text-sm hover:bg-bg-elevated transition-colors ${\n model === value\n ? 'text-text-primary bg-bg-sunken font-medium'\n : 'text-text-primary'\n }`}\n >\n {model}\n </button>\n ))}\n </>\n )}\n </div>\n ))}\n {searchedGroups.every((g) => g.models.length === 0) && (\n <div className=\"px-3 py-4 text-sm text-text-tertiary text-center\">\n {tr('vibecontrols.agentManager.controls.noModelsAvailable', 'No models available')}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;AA4BA,IAAM,IAA6B;CACjC;EAAE,OAAO;EAAU,KAAK;EAAU,QAAQ,EAAE;EAAE;CAC9C;EAAE,OAAO;EAAS,KAAK;EAAS,QAAQ,EAAE;EAAE;CAC5C;EAAE,OAAO;EAAU,KAAK;EAAU,QAAQ,EAAE;EAAE;CAC9C;EAAE,OAAO;EAAY,KAAK;EAAY,QAAQ,EAAE;EAAE;CAClD;EAAE,OAAO;EAAqB,KAAK;EAAiB,QAAQ,EAAE;EAAE;CAChE;EAAE,OAAO;EAAS,KAAK;EAAS,QAAQ,EAAE;EAAE;CAC5C;EAAE,OAAO;EAAO,KAAK;EAAO,QAAQ,EAAE;EAAE;CACxC;EAAE,OAAO;EAAW,KAAK;EAAW,QAAQ,EAAE;EAAE;CAChD;EAAE,OAAO;EAAS,KAAK;EAAS,QAAQ,EAAE;EAAE;CAC5C;EAAE,OAAO;EAAM,KAAK;EAAM,QAAQ,EAAE;EAAE;CACtC;EAAE,OAAO;EAAS,KAAK;EAAS,QAAQ,EAAE;EAAE;CAC5C;EAAE,OAAO;EAAW,KAAK;EAAW,QAAQ,EAAE;EAAE;CACjD;AAGD,SAAS,EAAY,GAAuB;AAE1C,QAAO,EACJ,QAAQ,eAAe,GAAG,CAC1B,QAAQ,WAAW,GAAG,CACtB,QAAQ,aAAa,GAAG,CACxB,QAAQ,aAAa,GAAG;;AAG7B,SAAS,EAAgB,GAAgC;AACvD,QAAO,EAAa,MAAM,MAAU,EAAM,QAAQ,EAAS,EAAE,SAAS;;AAGxE,SAAS,EAAa,GAAkB,GAAgC;AACtE,QAAO,MAAM,KAAK,IAAI,IAAI,CAAC,GAAc,GAAG,EAAO,CAAC,OAAO,QAAQ,CAAC,CAAC;;AAGvE,SAAgB,EAAc,EAAE,UAAO,aAAU,aAAU,YAAS,cAA+B;CACjG,IAAM,IAAK,GAAO,EACZ,CAAC,GAAM,KAAW,EAAS,GAAM,EACjC,CAAC,GAAQ,KAAa,EAAS,GAAG,EAClC,CAAC,GAAa,KAAkB,EAA0B,KAAK,EAC/D,IAAe,EAAuB,KAAK,EAC3C,IAAY,EAA0B,KAAK,EAC3C,CAAC,GAAK,KAAU,EAAS;EAAE,QAAQ;EAAG,MAAM;EAAG,CAAC,EAEhD,IACJ,KAAY,IACR,CACE;EACE,OAAO,EAAgB,EAAS;EAChC,KAAK;EACL,QAAQ,EAAa,GAAa,EAAM;EACzC,CACF,GACD,IACE,EAAa,QAAQ,MAAM,EAAE,QAAQ,EAAS,GAC9C,GAEF,IAAqB,GAAa,MAAkB;AACxD,EAAI,EAAa,WAAW,CAAC,EAAa,QAAQ,SAAS,EAAE,OAAe,IAC1E,EAAQ,GAAM;IAEf,EAAE,CAAC;AAaN,CAXA,QAAgB;AACd,MAAI,EAEF,QADA,SAAS,iBAAiB,aAAa,EAAmB,QAC7C,SAAS,oBAAoB,aAAa,EAAmB;IAE3E,CAAC,GAAM,EAAmB,CAAC,EAE9B,QAAgB;AACd,EAAK,KAAM,EAAU,GAAG;IACvB,CAAC,EAAK,CAAC,EAEV,QAAgB;AACd,MAAI,CAAC,KAAQ,CAAC,KAAW,CAAC,EAAU;EAEpC,IAAI,IAAY;AAYhB,SAXA,EAAe,KAAK,EACf,EAAY;GAAE,IAAI;GAAS,SAAS,KAAW;GAAW,EAAE,EAAS,CACvE,MAAM,MAAW;AACZ,QAEJ,EADiB,EAAO,KAAK,MAAU,EAAM,GAAG,CAAC,OAAO,QAAQ,CACxC;IACxB,CACD,YAAY;AACX,GAAK,KAAW,EAAe,EAAE,CAAC;IAClC,QAES;AACX,OAAY;;IAEb;EAAC;EAAS;EAAM;EAAS;EAAS,CAAC;CAEtC,IAAM,IAAc,EAAO,aAAa,EAClC,IAAiB,EAAe,KAAK,OAAO;EAChD,GAAG;EACH,QAAQ,IAAS,EAAE,OAAO,QAAQ,MAAM,EAAE,aAAa,CAAC,SAAS,EAAY,CAAC,GAAG,EAAE;EACpF,EAAE;AAUH,QACE,kBAAC,OAAD;EAAK,KAAK;EAAc,WAAU;YAAlC,CACE,kBAAC,UAAD;GACE,KAAK;GACL,MAAK;GACL,eAbqB;AACzB,QAAI,CAAC,KAAQ,EAAU,SAAS;KAC9B,IAAM,IAAO,EAAU,QAAQ,uBAAuB;AACtD,OAAO;MAAE,QAAQ,OAAO,cAAc,EAAK,MAAM;MAAG,MAAM,EAAK;MAAM,CAAC;;AAExE,OAAS,MAAS,CAAC,EAAK;;GASpB,WAAU;GACV,OAAO;aALT,CAOE,kBAAC,QAAD;IAAM,WAAU;cAAY,EAAY,EAAM;IAAQ,CAAA,EACtD,kBAAC,GAAD,EAAa,WAAU,wBAAyB,CAAA,CACzC;MAER,KACC,kBAAC,OAAD;GACE,WAAU;GACV,OAAO;IAAE,QAAQ,EAAI;IAAQ,MAAM,EAAI;IAAM;aAF/C;IAIE,kBAAC,SAAD;KACE,cAAW;KACX,MAAK;KACL,OAAO;KACP,WAAW,MAAM,EAAU,EAAE,OAAO,MAAM;KAC1C,aAAY;KACZ,WAAU;KACV,CAAA;IACD,EAAe,KAAK,MACnB,kBAAC,OAAD,EAAA,UACG,EAAM,OAAO,SAAS,KACrB,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAM;KACH,CAAA,EACL,EAAM,OAAO,KAAK,MACjB,kBAAC,UAAD;KAEE,MAAK;KACL,eAAe;AAEb,MADA,EAAS,EAAM,EACf,EAAQ,GAAM;;KAEhB,WAAW,6EACT,MAAU,IACN,+CACA;eAGL;KACM,EAbF,EAaE,CACT,CACD,EAAA,CAAA,EAED,EAzBI,EAAM,IAyBV,CACN;IACD,EAAe,OAAO,MAAM,EAAE,OAAO,WAAW,EAAE,IACjD,kBAAC,OAAD;KAAK,WAAU;eACZ,EAAG,wDAAwD,sBAAsB;KAC9E,CAAA;IAEJ;KAEJ"}
@@ -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: `ui:${e}`,
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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"}
@@ -1 +1 @@
1
- {"version":3,"file":"RemoteTaskPermissionsCard.js","names":[],"sources":["../../../src/components/agents/RemoteTaskPermissionsCard.tsx"],"sourcesContent":["/**\n * RemoteTaskPermissionsCard\n *\n * Toggles the per-agent \"allow remote shell tasks\" and \"allow remote\n * file-mutation tasks\" gates that the agent enforces in\n * `core/request-security.ts:denyNonLocalMutation`. The agent's task\n * plugin (POST /api/profiles/<n>/tasks/) refuses non-loopback callers by\n * default because remote shell execution + file writes are high-blast\n * radius; this card lets operators opt in per-agent without having to\n * SSH in and restart the daemon with the matching env var\n * (VIBECONTROLS_ALLOW_REMOTE_SHELL_TASK / _FILE_TASK).\n *\n * Storage: writes to the agent's encrypted config bag under\n * `security:vibecontrols_allow_remote_shell_task` and\n * `security:vibecontrols_allow_remote_file_task`. The same keys are\n * read by the agent's gate via `db.getConfig(...)`. Persisted across\n * agent restarts.\n */\nimport { useCallback, useEffect, useState } from 'react';\nimport { ShieldAlert, Loader2 } from 'lucide-react';\n\nimport { Card } from '@burdenoff/fe-libs/ui';\nimport { Switch } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n deleteAgentConfigValue,\n getAgentConfigValue,\n setAgentConfigValue,\n type AgentRef,\n} from '@/services/aiApi';\n\ninterface RemoteTaskPermissionsCardProps {\n agent: AgentRef;\n}\n\ninterface GateDef {\n key: string;\n title: string;\n description: string;\n warning?: string;\n}\n\nfunction buildGates(tr: ReturnType<typeof useTr>): GateDef[] {\n return [\n {\n key: 'security:vibecontrols_allow_remote_shell_task',\n title: tr(\n 'agentDetails.remoteShellTask.title',\n 'Allow remote shell + script tasks'\n ),\n description: tr(\n 'agentDetails.remoteShellTask.description',\n 'Enable VibeDeck buttons, scheduled jobs, and the UI to run shell commands and scripts on this agent over its tunnel. Required for any UI-triggered \"execute action\" that runs a command.'\n ),\n warning: tr(\n 'agentDetails.remoteShellTask.warning',\n 'Anyone with workspace access can execute commands on the host machine. Audit log records every invocation; reviewers can revert this flag at any time.'\n ),\n },\n {\n key: 'security:vibecontrols_allow_remote_file_task',\n title: tr(\n 'agentDetails.remoteFileTask.title',\n 'Allow remote file writes + deletes'\n ),\n description: tr(\n 'agentDetails.remoteFileTask.description',\n 'Enable UI-triggered file_operation write/delete tasks on this agent. Reads are always allowed; this gate covers mutations only.'\n ),\n warning: tr(\n 'agentDetails.remoteFileTask.warning',\n 'Writes and deletes are confined to the cwd / homedir / tmpdir allowlist via resolveSafePath + isSensitivePath, but a malicious caller could still corrupt files in those scopes.'\n ),\n },\n ];\n}\n\nfunction isTruthy(raw: string | null | undefined): boolean {\n if (!raw) return false;\n const v = raw.trim().toLowerCase();\n return v === '1' || v === 'true' || v === 'yes';\n}\n\nexport function RemoteTaskPermissionsCard({ agent }: RemoteTaskPermissionsCardProps) {\n const tr = useTr();\n const gates = buildGates(tr);\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState<string | null>(null);\n const [values, setValues] = useState<Record<string, boolean>>({});\n const [error, setError] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const entries = await Promise.all(\n gates.map(async (g) => [g.key, isTruthy(await getAgentConfigValue(agent, g.key))] as const)\n );\n setValues(Object.fromEntries(entries));\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('agentDetails.remoteTaskPermissions.loadFailed', 'Failed to load permissions')\n );\n } finally {\n setLoading(false);\n }\n // gates is stable across renders (derived from tr); intentionally not in deps.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agent, tr]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const toggle = useCallback(\n async (key: string, next: boolean) => {\n setSaving(key);\n setError(null);\n try {\n if (next) {\n await setAgentConfigValue(agent, key, '1');\n } else {\n await deleteAgentConfigValue(agent, key);\n }\n setValues((prev) => ({ ...prev, [key]: next }));\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr(\n 'agentDetails.remoteTaskPermissions.saveFailed',\n 'Failed to save permission'\n )\n );\n } finally {\n setSaving(null);\n }\n },\n [agent, tr]\n );\n\n return (\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 <ShieldAlert className=\"size-5\" />\n {tr(\n 'agentDetails.remoteTaskPermissions.title',\n 'Remote Task Permissions'\n )}\n </h3>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tr(\n 'agentDetails.remoteTaskPermissions.subtitle',\n 'These gates default to deny so an agent on a public tunnel can’t be coerced into shell execution. Toggle them on per-agent if you want VibeDeck buttons / scheduled jobs to run shell or file-mutation tasks here.'\n )}\n </p>\n </div>\n <div className=\"p-4 space-y-4\">\n {error ? (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n ) : null}\n {loading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('agentDetails.remoteTaskPermissions.loading', 'Loading…')}\n </div>\n ) : (\n gates.map((g) => {\n const current = values[g.key] ?? false;\n const busy = saving === g.key;\n return (\n <div\n key={g.key}\n className=\"flex items-start gap-3 rounded-lg border border-border-default bg-bg-primary p-3\"\n >\n <div className=\"flex-1 min-w-0 space-y-1\">\n <p className=\"text-sm font-medium text-text-primary\">{g.title}</p>\n <p className=\"text-xs text-text-secondary\">{g.description}</p>\n {g.warning ? (\n <p className=\"text-xs text-status-warning-text\">{g.warning}</p>\n ) : null}\n <p className=\"text-[11px] font-mono text-text-tertiary\">{g.key}</p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {busy ? (\n <Loader2 className=\"size-4 animate-spin text-text-secondary\" />\n ) : null}\n <Switch\n checked={current}\n onCheckedChange={(checked) => void toggle(g.key, checked)}\n disabled={busy}\n aria-label={g.title}\n />\n </div>\n </div>\n );\n })\n )}\n </div>\n </Card>\n );\n}\n"],"mappings":";;;;;;;AA2CA,SAAS,EAAW,GAAyC;AAC3D,QAAO,CACL;EACE,KAAK;EACL,OAAO,EACL,sCACA,oCACD;EACD,aAAa,EACX,4CACA,6LACD;EACD,SAAS,EACP,wCACA,yJACD;EACF,EACD;EACE,KAAK;EACL,OAAO,EACL,qCACA,qCACD;EACD,aAAa,EACX,2CACA,kIACD;EACD,SAAS,EACP,uCACA,mLACD;EACF,CACF;;AAGH,SAAS,EAAS,GAAyC;AACzD,KAAI,CAAC,EAAK,QAAO;CACjB,IAAM,IAAI,EAAI,MAAM,CAAC,aAAa;AAClC,QAAO,MAAM,OAAO,MAAM,UAAU,MAAM;;AAG5C,SAAgB,EAA0B,EAAE,YAAyC;CACnF,IAAM,IAAK,GAAO,EACZ,IAAQ,EAAW,EAAG,EACtB,CAAC,GAAS,KAAc,EAAS,GAAK,EACtC,CAAC,GAAQ,KAAa,EAAwB,KAAK,EACnD,CAAC,GAAQ,KAAa,EAAkC,EAAE,CAAC,EAC3D,CAAC,GAAO,KAAY,EAAwB,KAAK,EAEjD,IAAO,EAAY,YAAY;AAEnC,EADA,EAAW,GAAK,EAChB,EAAS,KAAK;AACd,MAAI;GACF,IAAM,IAAU,MAAM,QAAQ,IAC5B,EAAM,IAAI,OAAO,MAAM,CAAC,EAAE,KAAK,EAAS,MAAM,EAAoB,GAAO,EAAE,IAAI,CAAC,CAAC,CAAU,CAC5F;AACD,KAAU,OAAO,YAAY,EAAQ,CAAC;WAC/B,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EAAG,iDAAiD,6BAA6B,CACtF;YACO;AACR,KAAW,GAAM;;IAIlB,CAAC,GAAO,EAAG,CAAC;AAEf,SAAgB;AACT,KAAM;IACV,CAAC,EAAK,CAAC;CAEV,IAAM,IAAS,EACb,OAAO,GAAa,MAAkB;AAEpC,EADA,EAAU,EAAI,EACd,EAAS,KAAK;AACd,MAAI;AAMF,GALI,IACF,MAAM,EAAoB,GAAO,GAAK,IAAI,GAE1C,MAAM,EAAuB,GAAO,EAAI,EAE1C,GAAW,OAAU;IAAE,GAAG;KAAO,IAAM;IAAM,EAAE;WACxC,GAAK;AACZ,KACE,aAAe,QACX,EAAI,UACJ,EACE,iDACA,4BACD,CACN;YACO;AACR,KAAU,KAAK;;IAGnB,CAAC,GAAO,EAAG,CACZ;AAED,QACE,kBAAC,GAAD,EAAA,UAAA,CACE,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,MAAD;GAAI,WAAU;aAAd,CACE,kBAAC,GAAD,EAAa,WAAU,UAAW,CAAA,EACjC,EACC,4CACA,0BACD,CACE;MACL,kBAAC,KAAD;GAAG,WAAU;aACV,EACC,+CACA,qNACD;GACC,CAAA,CACA;KACN,kBAAC,OAAD;EAAK,WAAU;YAAf,CACG,IACC,kBAAC,OAAD;GAAK,WAAU;aACZ;GACG,CAAA,GACJ,MACH,IACC,kBAAC,OAAD;GAAK,WAAU;aAAf,CACE,kBAAC,GAAD,EAAS,WAAU,uBAAwB,CAAA,EAC1C,EAAG,8CAA8C,WAAW,CACzD;OAEN,EAAM,KAAK,MAAM;GACf,IAAM,IAAU,EAAO,EAAE,QAAQ,IAC3B,IAAO,MAAW,EAAE;AAC1B,UACE,kBAAC,OAAD;IAEE,WAAU;cAFZ,CAIE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,KAAD;OAAG,WAAU;iBAAyC,EAAE;OAAU,CAAA;MAClE,kBAAC,KAAD;OAAG,WAAU;iBAA+B,EAAE;OAAgB,CAAA;MAC7D,EAAE,UACD,kBAAC,KAAD;OAAG,WAAU;iBAAoC,EAAE;OAAY,CAAA,GAC7D;MACJ,kBAAC,KAAD;OAAG,WAAU;iBAA4C,EAAE;OAAQ,CAAA;MAC/D;QACN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACG,IACC,kBAAC,GAAD,EAAS,WAAU,2CAA4C,CAAA,GAC7D,MACJ,kBAAC,GAAD;MACE,SAAS;MACT,kBAAkB,MAAY,KAAK,EAAO,EAAE,KAAK,EAAQ;MACzD,UAAU;MACV,cAAY,EAAE;MACd,CAAA,CACE;OACF;MAtBC,EAAE,IAsBH;IAER,CAEA;IACD,EAAA,CAAA"}
1
+ {"version":3,"file":"RemoteTaskPermissionsCard.js","names":[],"sources":["../../../src/components/agents/RemoteTaskPermissionsCard.tsx"],"sourcesContent":["/**\n * RemoteTaskPermissionsCard\n *\n * Toggles the per-agent \"allow remote shell tasks\" and \"allow remote\n * file-mutation tasks\" gates that the agent enforces in\n * `core/request-security.ts:denyNonLocalMutation`. The agent's task\n * plugin (POST /api/profiles/<n>/tasks/) refuses non-loopback callers by\n * default because remote shell execution + file writes are high-blast\n * radius; this card lets operators opt in per-agent without having to\n * SSH in and restart the daemon with the matching env var\n * (VIBECONTROLS_ALLOW_REMOTE_SHELL_TASK / _FILE_TASK).\n *\n * Storage: writes to the agent's encrypted config bag under\n * `security:vibecontrols_allow_remote_shell_task` and\n * `security:vibecontrols_allow_remote_file_task`. The same keys are\n * read by the agent's gate via `db.getConfig(...)`. Persisted across\n * agent restarts.\n */\nimport { useCallback, useEffect, useState } from 'react';\nimport { ShieldAlert, Loader2 } from 'lucide-react';\n\nimport { Card } from '@burdenoff/fe-libs/ui';\nimport { Switch } from '@burdenoff/fe-libs/ui';\n\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n deleteAgentConfigValue,\n getAgentConfigValue,\n setAgentConfigValue,\n type AgentRef,\n} from '@/services/aiApi';\n\ninterface RemoteTaskPermissionsCardProps {\n agent: AgentRef;\n}\n\ninterface GateDef {\n key: string;\n title: string;\n description: string;\n warning?: string;\n}\n\nfunction buildGates(tr: ReturnType<typeof useTr>): GateDef[] {\n return [\n {\n key: 'security:vibecontrols_allow_remote_shell_task',\n title: tr('agentDetails.remoteShellTask.title', 'Allow remote shell + script tasks'),\n description: tr(\n 'agentDetails.remoteShellTask.description',\n 'Enable VibeDeck buttons, scheduled jobs, and the UI to run shell commands and scripts on this agent over its tunnel. Required for any UI-triggered \"execute action\" that runs a command.'\n ),\n warning: tr(\n 'agentDetails.remoteShellTask.warning',\n 'Anyone with workspace access can execute commands on the host machine. Audit log records every invocation; reviewers can revert this flag at any time.'\n ),\n },\n {\n key: 'security:vibecontrols_allow_remote_file_task',\n title: tr('agentDetails.remoteFileTask.title', 'Allow remote file writes + deletes'),\n description: tr(\n 'agentDetails.remoteFileTask.description',\n 'Enable UI-triggered file_operation write/delete tasks on this agent. Reads are always allowed; this gate covers mutations only.'\n ),\n warning: tr(\n 'agentDetails.remoteFileTask.warning',\n 'Writes and deletes are confined to the cwd / homedir / tmpdir allowlist via resolveSafePath + isSensitivePath, but a malicious caller could still corrupt files in those scopes.'\n ),\n },\n ];\n}\n\nfunction isTruthy(raw: string | null | undefined): boolean {\n if (!raw) return false;\n const v = raw.trim().toLowerCase();\n return v === '1' || v === 'true' || v === 'yes';\n}\n\nexport function RemoteTaskPermissionsCard({ agent }: RemoteTaskPermissionsCardProps) {\n const tr = useTr();\n const gates = buildGates(tr);\n const [loading, setLoading] = useState(true);\n const [saving, setSaving] = useState<string | null>(null);\n const [values, setValues] = useState<Record<string, boolean>>({});\n const [error, setError] = useState<string | null>(null);\n\n const load = useCallback(async () => {\n setLoading(true);\n setError(null);\n try {\n const entries = await Promise.all(\n gates.map(async (g) => [g.key, isTruthy(await getAgentConfigValue(agent, g.key))] as const)\n );\n setValues(Object.fromEntries(entries));\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('agentDetails.remoteTaskPermissions.loadFailed', 'Failed to load permissions')\n );\n } finally {\n setLoading(false);\n }\n // gates is stable across renders (derived from tr); intentionally not in deps.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agent, tr]);\n\n useEffect(() => {\n void load();\n }, [load]);\n\n const toggle = useCallback(\n async (key: string, next: boolean) => {\n setSaving(key);\n setError(null);\n try {\n if (next) {\n await setAgentConfigValue(agent, key, '1');\n } else {\n await deleteAgentConfigValue(agent, key);\n }\n setValues((prev) => ({ ...prev, [key]: next }));\n } catch (err) {\n setError(\n err instanceof Error\n ? err.message\n : tr('agentDetails.remoteTaskPermissions.saveFailed', 'Failed to save permission')\n );\n } finally {\n setSaving(null);\n }\n },\n [agent, tr]\n );\n\n return (\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 <ShieldAlert className=\"size-5\" />\n {tr('agentDetails.remoteTaskPermissions.title', 'Remote Task Permissions')}\n </h3>\n <p className=\"mt-1 text-xs text-text-secondary\">\n {tr(\n 'agentDetails.remoteTaskPermissions.subtitle',\n 'These gates default to deny so an agent on a public tunnel can’t be coerced into shell execution. Toggle them on per-agent if you want VibeDeck buttons / scheduled jobs to run shell or file-mutation tasks here.'\n )}\n </p>\n </div>\n <div className=\"p-4 space-y-4\">\n {error ? (\n <div className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-sm text-status-error-text\">\n {error}\n </div>\n ) : null}\n {loading ? (\n <div className=\"flex items-center gap-2 text-sm text-text-secondary\">\n <Loader2 className=\"size-4 animate-spin\" />\n {tr('agentDetails.remoteTaskPermissions.loading', 'Loading…')}\n </div>\n ) : (\n gates.map((g) => {\n const current = values[g.key] ?? false;\n const busy = saving === g.key;\n return (\n <div\n key={g.key}\n className=\"flex items-start gap-3 rounded-lg border border-border-default bg-bg-primary p-3\"\n >\n <div className=\"flex-1 min-w-0 space-y-1\">\n <p className=\"text-sm font-medium text-text-primary\">{g.title}</p>\n <p className=\"text-xs text-text-secondary\">{g.description}</p>\n {g.warning ? (\n <p className=\"text-xs text-status-warning-text\">{g.warning}</p>\n ) : null}\n <p className=\"text-[11px] font-mono text-text-tertiary\">{g.key}</p>\n </div>\n <div className=\"flex items-center gap-2 flex-shrink-0\">\n {busy ? <Loader2 className=\"size-4 animate-spin text-text-secondary\" /> : null}\n <Switch\n checked={current}\n onCheckedChange={(checked) => void toggle(g.key, checked)}\n disabled={busy}\n aria-label={g.title}\n />\n </div>\n </div>\n );\n })\n )}\n </div>\n </Card>\n );\n}\n"],"mappings":"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"}
@@ -128,7 +128,7 @@ function p(p, m) {
128
128
  if (t?.installAgentPlugin?.success) return C((t) => ({
129
129
  ...t,
130
130
  [e]: !0
131
- })), setTimeout(() => void M(), 3e3), !0;
131
+ })), setTimeout(() => void M(), 1200), !0;
132
132
  throw Error(t?.installAgentPlugin?.error || "Install failed");
133
133
  } catch (e) {
134
134
  if (r(e)) return i(), k(!0), !1;
@@ -1 +1 @@
1
- {"version":3,"file":"usePluginAvailability.js","names":[],"sources":["../../src/hooks/usePluginAvailability.ts"],"sourcesContent":["/**\n * usePluginAvailability — query + install helpers for vibecontrols-agent plugins.\n *\n * Wraps the existing backend-proxied GraphQL ops (AgentPlugins query +\n * InstallAgentPlugin mutation) and exposes a small state surface that\n * gating components can render against.\n *\n * Live updates: subscribes to `vibecontrolsAgentPluginStream` so the\n * installed map reflects out-of-band installs (CLI, other tabs,\n * agent-side changes) without per-tab polling. The initial AgentPlugins\n * query is still used for first paint so the gate can render before\n * the WebSocket completes its handshake.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport {\n AgentPluginsDocument,\n useInstallAgentPluginMutation,\n useVibecontrolsAgentPluginStreamSubscription,\n type AgentPluginsQuery,\n} from '@/generated/wspace-operations';\nimport { isQuotaExhaustedError, markQuotaHandledLocally } from '@/utils/quotaUtils';\n\n/**\n * Classifies transient \"the agent itself isn't ready\" errors so the UI can\n * render a dedicated waiting state instead of an install CTA. These errors\n * are not the user's to fix via an install click — they resolve on their\n * own (or require Agents-page intervention).\n */\nfunction classifyAgentStateError(message: string | null | undefined): boolean {\n if (!message) return false;\n const m = message.toLowerCase();\n return (\n m.includes('agent not yet configured') ||\n m.includes('awaiting-config') ||\n m.includes('initializing') ||\n m.includes('agent unreachable') ||\n m.includes('agent returned 503') ||\n m.includes('http 503') ||\n m.includes('plugin routes are unavailable') ||\n m.includes('econnrefused') ||\n m.includes('rate_limited') ||\n m.includes('rate limited') ||\n m.includes('operation timeout') ||\n m.includes('operation_timeout') ||\n // Bun's fetch surfaces these human-friendly strings when the agent\n // socket isn't accepting connections. Without this branch, the raw\n // text would leak into the install card as the error chip.\n m.includes('typo in the url') ||\n m.includes('unable to connect') ||\n m.includes('failed to open socket') ||\n m.includes('failedtoopensocket') ||\n m.includes('fetch failed') ||\n m.includes('enotfound') ||\n m.includes('getaddrinfo')\n // Note: \"operation was aborted\" is deliberately NOT classified as an\n // agent-state error. Apollo aborts previous in-flight queries when\n // the same lazy query is re-fired (common in React Strict Mode\n // double-mount). Those aborts are transient and not about agent\n // state; the follow-up fetch succeeds normally.\n );\n}\n\n/**\n * Translate raw error strings (especially Bun fetch's human-friendly but\n * jargon-y errors) into a short, user-safe message. Used right before we\n * stash anything into the surfaced `error` field — guarantees we never\n * render \"Was there a typo in the url or port?\" or similar into the DOM.\n */\nfunction sanitizeAgentStateError(raw: string): string {\n const m = raw.toLowerCase();\n if (\n m.includes('typo in the url') ||\n m.includes('unable to connect') ||\n m.includes('failed to open socket') ||\n m.includes('failedtoopensocket') ||\n m.includes('econnrefused') ||\n m.includes('enotfound') ||\n m.includes('getaddrinfo') ||\n m.includes('fetch failed') ||\n m.includes('agent unreachable')\n ) {\n return 'Agent unreachable. Make sure the vibecontrols-agent is running locally.';\n }\n if (m.includes('agent not yet configured') || m.includes('awaiting-config')) {\n return 'Agent is waiting for configuration. Finish setup in the Agents page.';\n }\n if (m.includes('initializing')) {\n return 'Agent is starting up. Try again in a moment.';\n }\n if (m.includes('http 503') || m.includes('agent returned 503')) {\n return 'Agent service unavailable. It may be restarting.';\n }\n return raw.length > 200 ? `${raw.slice(0, 200)}…` : raw;\n}\n\nexport interface UsePluginAvailabilityResult {\n loading: boolean;\n /**\n * True while the very first fetch is in flight (or before it has begun).\n * Consumers should render a loading spinner instead of the install CTA in\n * this state so users don't see a flash of \"plugin required / Install\"\n * followed by a flash of the real content.\n */\n initialLoading: boolean;\n installed: Record<string, boolean>;\n missing: string[];\n anyInstalled: boolean;\n allInstalled: boolean;\n installing: string | null;\n install: (packageName: string) => Promise<boolean>;\n error: string | null;\n /** True when `error` points to the agent itself not being ready. */\n errorIsAgentState: boolean;\n quotaExhausted: boolean;\n clearQuotaExhausted: () => void;\n refetch: () => Promise<void>;\n}\n\nexport function usePluginAvailability(\n packages: readonly string[],\n agentId: string | null\n): UsePluginAvailabilityResult {\n const apollo = useApolloClient();\n const [installPluginMutation] = useInstallAgentPluginMutation();\n\n // Read cached data synchronously so the first render already has the\n // installed map populated when another consumer has primed the cache.\n const cachedInstalledSet = useMemo(() => {\n if (!agentId) return null;\n try {\n const cached = apollo.readQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n });\n const installedList = cached?.agentPlugins?.installed ?? null;\n if (!installedList) return null;\n return new Set(installedList.map((p: { packageName: string }) => p.packageName));\n } catch {\n return null;\n }\n }, [apollo, agentId]);\n\n const [loading, setLoading] = useState(false);\n const [hasFetchedOnce, setHasFetchedOnce] = useState(() => cachedInstalledSet !== null);\n const [installedMap, setInstalledMap] = useState<Record<string, boolean>>(() => {\n const next: Record<string, boolean> = {};\n if (cachedInstalledSet) {\n for (const pkg of packages) next[pkg] = cachedInstalledSet.has(pkg);\n }\n return next;\n });\n const [installing, setInstalling] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [quotaExhausted, setQuotaExhausted] = useState(false);\n const errorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Stable key so we re-run only when the *set* of requested packages changes.\n const packagesKey = useMemo(() => packages.slice().sort().join('|'), [packages]);\n\n // Re-seed local state whenever the agent changes so consumers never\n // render the previous agent's installed map during the refetch window.\n useEffect(() => {\n let cached: Set<string> | null = null;\n if (agentId) {\n try {\n const c = apollo.readQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n });\n const list = c?.agentPlugins?.installed ?? null;\n if (list) cached = new Set(list.map((p: { packageName: string }) => p.packageName));\n } catch {\n cached = null;\n }\n }\n const next: Record<string, boolean> = {};\n if (cached) for (const pkg of packages) next[pkg] = cached.has(pkg);\n setInstalledMap(next);\n setHasFetchedOnce(cached !== null);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agentId, packagesKey, apollo]);\n\n const refetch = useCallback(async () => {\n if (!agentId) {\n setInstalledMap({});\n return;\n }\n setLoading(true);\n try {\n // Use the Apollo client directly with `network-only` so out-of-band\n // installs (CLI, other tabs, agent-side changes) are always picked\n // up. `useAgentPluginsLazyQuery`'s lazy execute() doesn't accept\n // per-call fetchPolicy in Apollo v4; `apollo.query()` does.\n const { data } = await apollo.query<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n fetchPolicy: 'network-only',\n });\n const next: Record<string, boolean> = {};\n const installedList = data?.agentPlugins?.installed ?? [];\n const pluginError = data?.agentPlugins?.error ?? null;\n const installedSet = new Set(\n installedList.map((p: { packageName: string }) => p.packageName)\n );\n for (const pkg of packages) next[pkg] = installedSet.has(pkg);\n if (pluginError) {\n if (!classifyAgentStateError(pluginError)) {\n setInstalledMap(next);\n }\n // Log the raw error for debugging, but only surface a sanitized\n // version to the UI. Raw Bun fetch errors (\"Was there a typo in\n // the url or port?\") would otherwise leak into the install card.\n // eslint-disable-next-line no-console -- intentional: keep technical detail for debugging\n console.error('[ai] agentPlugins returned error', pluginError);\n setError(sanitizeAgentStateError(pluginError));\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n if (!classifyAgentStateError(pluginError)) {\n errorTimerRef.current = setTimeout(() => setError(null), 5000);\n }\n return;\n }\n setInstalledMap(next);\n // Clear any lingering error from a previously-aborted fetch so the\n // gate doesn't keep showing \"Agent not ready\" after a successful\n // subsequent request (e.g. React Strict Mode double-mount).\n setError(null);\n if (errorTimerRef.current) {\n clearTimeout(errorTimerRef.current);\n errorTimerRef.current = null;\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : 'Agent unreachable — unable to read plugin list';\n const isAgentStateError = classifyAgentStateError(msg);\n // Keep the last known installed state while the agent is restarting or\n // finalizing. Otherwise a transient 503 flashes an install CTA for an\n // already-installed plugin.\n if (!isAgentStateError) {\n setInstalledMap({});\n }\n // eslint-disable-next-line no-console -- intentional: keep technical detail for debugging\n console.error('[ai] agentPlugins fetch failed', err);\n setError(sanitizeAgentStateError(msg));\n // State-related errors stay visible until resolved; transient\n // network/GraphQL errors auto-dismiss so the UI stays tidy.\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n if (!isAgentStateError) {\n errorTimerRef.current = setTimeout(() => setError(null), 5000);\n }\n } finally {\n setLoading(false);\n setHasFetchedOnce(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agentId, apollo, packagesKey]);\n\n useEffect(() => {\n void refetch();\n }, [refetch]);\n\n // Subscribe to the push stream. The svc emits the installed list\n // whenever it diverges from its previous snapshot, so we only update\n // local state on payloads — and we also write through to the Apollo\n // cache so other consumers reading `agentPlugins` directly see fresh\n // data without their own refetch.\n useVibecontrolsAgentPluginStreamSubscription({\n variables: { agentId: agentId ?? '' },\n skip: !agentId,\n onData: ({ data }) => {\n const installedList = data.data?.vibecontrolsAgentPluginStream;\n if (!installedList || !agentId) return;\n const installedSet = new Set(\n installedList.map((p: { packageName: string }) => p.packageName)\n );\n setInstalledMap((prev) => {\n const next: Record<string, boolean> = { ...prev };\n let changed = false;\n for (const pkg of packages) {\n const has = installedSet.has(pkg);\n if (next[pkg] !== has) {\n next[pkg] = has;\n changed = true;\n }\n }\n return changed ? next : prev;\n });\n setHasFetchedOnce(true);\n // Mirror into Apollo cache so consumers that read AgentPluginsQuery\n // directly (e.g. PluginHarnessPicker) see the same fresh list.\n try {\n const existing = apollo.readQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n });\n apollo.writeQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n data: {\n agentPlugins: {\n __typename: 'AgentPluginListResult',\n installed: installedList,\n available: existing?.agentPlugins?.available ?? [],\n error: null,\n },\n },\n });\n } catch {\n // Cache may not have been primed yet — the next refetch will\n // populate `available` and we'll write again on the next event.\n }\n },\n });\n\n // Also refetch when the tab regains focus so installs made elsewhere flow in.\n useEffect(() => {\n const onFocus = () => void refetch();\n window.addEventListener('focus', onFocus);\n return () => window.removeEventListener('focus', onFocus);\n }, [refetch]);\n\n useEffect(() => {\n return () => {\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n };\n }, []);\n\n const install = useCallback(\n async (packageName: string): Promise<boolean> => {\n if (!agentId) return false;\n setInstalling(packageName);\n setError(null);\n try {\n const { data } = await installPluginMutation({\n variables: { agentId, packageName },\n });\n if (data?.installAgentPlugin?.success) {\n setInstalledMap((prev) => ({ ...prev, [packageName]: true }));\n // Don't immediately refetch: the mutation returns success before the agent\n // finishes installing the npm package. An immediate refetch races the agent\n // and overwrites this optimistic update with a stale \"not installed\" result,\n // putting the install card back up. The subscription stream delivers the\n // confirmed update when the agent is done; a delayed refetch is a fallback\n // for environments where the WebSocket push is slow.\n setTimeout(() => void refetch(), 3000);\n return true;\n }\n throw new Error(data?.installAgentPlugin?.error || 'Install failed');\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setQuotaExhausted(true);\n return false;\n }\n const msg = err instanceof Error ? err.message : 'Failed to install plugin';\n // eslint-disable-next-line no-console -- intentional: keep technical detail for debugging\n console.error('[ai] plugin install failed', err);\n setError(sanitizeAgentStateError(msg));\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n if (!classifyAgentStateError(msg)) {\n errorTimerRef.current = setTimeout(() => setError(null), 5000);\n }\n return false;\n } finally {\n setInstalling(null);\n }\n },\n [agentId, installPluginMutation, refetch]\n );\n\n const clearQuotaExhausted = useCallback(() => setQuotaExhausted(false), []);\n\n const missing = useMemo(\n () => packages.filter((pkg) => !installedMap[pkg]),\n [packages, installedMap]\n );\n const anyInstalled = useMemo(\n () => packages.some((pkg) => installedMap[pkg]),\n [packages, installedMap]\n );\n const allInstalled = missing.length === 0 && packages.length > 0;\n\n return {\n loading,\n initialLoading: !hasFetchedOnce,\n installed: installedMap,\n missing,\n anyInstalled,\n allInstalled,\n installing,\n install,\n error,\n errorIsAgentState: classifyAgentStateError(error),\n quotaExhausted,\n clearQuotaExhausted,\n refetch,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"usePluginAvailability.js","names":[],"sources":["../../src/hooks/usePluginAvailability.ts"],"sourcesContent":["/**\n * usePluginAvailability — query + install helpers for vibecontrols-agent plugins.\n *\n * Wraps the existing backend-proxied GraphQL ops (AgentPlugins query +\n * InstallAgentPlugin mutation) and exposes a small state surface that\n * gating components can render against.\n *\n * Live updates: subscribes to `vibecontrolsAgentPluginStream` so the\n * installed map reflects out-of-band installs (CLI, other tabs,\n * agent-side changes) without per-tab polling. The initial AgentPlugins\n * query is still used for first paint so the gate can render before\n * the WebSocket completes its handshake.\n */\n\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react';\nimport { useApolloClient } from '@apollo/client/react';\nimport {\n AgentPluginsDocument,\n useInstallAgentPluginMutation,\n useVibecontrolsAgentPluginStreamSubscription,\n type AgentPluginsQuery,\n} from '@/generated/wspace-operations';\nimport { isQuotaExhaustedError, markQuotaHandledLocally } from '@/utils/quotaUtils';\n\n/**\n * Classifies transient \"the agent itself isn't ready\" errors so the UI can\n * render a dedicated waiting state instead of an install CTA. These errors\n * are not the user's to fix via an install click — they resolve on their\n * own (or require Agents-page intervention).\n */\nfunction classifyAgentStateError(message: string | null | undefined): boolean {\n if (!message) return false;\n const m = message.toLowerCase();\n return (\n m.includes('agent not yet configured') ||\n m.includes('awaiting-config') ||\n m.includes('initializing') ||\n m.includes('agent unreachable') ||\n m.includes('agent returned 503') ||\n m.includes('http 503') ||\n m.includes('plugin routes are unavailable') ||\n m.includes('econnrefused') ||\n m.includes('rate_limited') ||\n m.includes('rate limited') ||\n m.includes('operation timeout') ||\n m.includes('operation_timeout') ||\n // Bun's fetch surfaces these human-friendly strings when the agent\n // socket isn't accepting connections. Without this branch, the raw\n // text would leak into the install card as the error chip.\n m.includes('typo in the url') ||\n m.includes('unable to connect') ||\n m.includes('failed to open socket') ||\n m.includes('failedtoopensocket') ||\n m.includes('fetch failed') ||\n m.includes('enotfound') ||\n m.includes('getaddrinfo')\n // Note: \"operation was aborted\" is deliberately NOT classified as an\n // agent-state error. Apollo aborts previous in-flight queries when\n // the same lazy query is re-fired (common in React Strict Mode\n // double-mount). Those aborts are transient and not about agent\n // state; the follow-up fetch succeeds normally.\n );\n}\n\n/**\n * Translate raw error strings (especially Bun fetch's human-friendly but\n * jargon-y errors) into a short, user-safe message. Used right before we\n * stash anything into the surfaced `error` field — guarantees we never\n * render \"Was there a typo in the url or port?\" or similar into the DOM.\n */\nfunction sanitizeAgentStateError(raw: string): string {\n const m = raw.toLowerCase();\n if (\n m.includes('typo in the url') ||\n m.includes('unable to connect') ||\n m.includes('failed to open socket') ||\n m.includes('failedtoopensocket') ||\n m.includes('econnrefused') ||\n m.includes('enotfound') ||\n m.includes('getaddrinfo') ||\n m.includes('fetch failed') ||\n m.includes('agent unreachable')\n ) {\n return 'Agent unreachable. Make sure the vibecontrols-agent is running locally.';\n }\n if (m.includes('agent not yet configured') || m.includes('awaiting-config')) {\n return 'Agent is waiting for configuration. Finish setup in the Agents page.';\n }\n if (m.includes('initializing')) {\n return 'Agent is starting up. Try again in a moment.';\n }\n if (m.includes('http 503') || m.includes('agent returned 503')) {\n return 'Agent service unavailable. It may be restarting.';\n }\n return raw.length > 200 ? `${raw.slice(0, 200)}…` : raw;\n}\n\nexport interface UsePluginAvailabilityResult {\n loading: boolean;\n /**\n * True while the very first fetch is in flight (or before it has begun).\n * Consumers should render a loading spinner instead of the install CTA in\n * this state so users don't see a flash of \"plugin required / Install\"\n * followed by a flash of the real content.\n */\n initialLoading: boolean;\n installed: Record<string, boolean>;\n missing: string[];\n anyInstalled: boolean;\n allInstalled: boolean;\n installing: string | null;\n install: (packageName: string) => Promise<boolean>;\n error: string | null;\n /** True when `error` points to the agent itself not being ready. */\n errorIsAgentState: boolean;\n quotaExhausted: boolean;\n clearQuotaExhausted: () => void;\n refetch: () => Promise<void>;\n}\n\nexport function usePluginAvailability(\n packages: readonly string[],\n agentId: string | null\n): UsePluginAvailabilityResult {\n const apollo = useApolloClient();\n const [installPluginMutation] = useInstallAgentPluginMutation();\n\n // Read cached data synchronously so the first render already has the\n // installed map populated when another consumer has primed the cache.\n const cachedInstalledSet = useMemo(() => {\n if (!agentId) return null;\n try {\n const cached = apollo.readQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n });\n const installedList = cached?.agentPlugins?.installed ?? null;\n if (!installedList) return null;\n return new Set(installedList.map((p: { packageName: string }) => p.packageName));\n } catch {\n return null;\n }\n }, [apollo, agentId]);\n\n const [loading, setLoading] = useState(false);\n const [hasFetchedOnce, setHasFetchedOnce] = useState(() => cachedInstalledSet !== null);\n const [installedMap, setInstalledMap] = useState<Record<string, boolean>>(() => {\n const next: Record<string, boolean> = {};\n if (cachedInstalledSet) {\n for (const pkg of packages) next[pkg] = cachedInstalledSet.has(pkg);\n }\n return next;\n });\n const [installing, setInstalling] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [quotaExhausted, setQuotaExhausted] = useState(false);\n const errorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n\n // Stable key so we re-run only when the *set* of requested packages changes.\n const packagesKey = useMemo(() => packages.slice().sort().join('|'), [packages]);\n\n // Re-seed local state whenever the agent changes so consumers never\n // render the previous agent's installed map during the refetch window.\n useEffect(() => {\n let cached: Set<string> | null = null;\n if (agentId) {\n try {\n const c = apollo.readQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n });\n const list = c?.agentPlugins?.installed ?? null;\n if (list) cached = new Set(list.map((p: { packageName: string }) => p.packageName));\n } catch {\n cached = null;\n }\n }\n const next: Record<string, boolean> = {};\n if (cached) for (const pkg of packages) next[pkg] = cached.has(pkg);\n setInstalledMap(next);\n setHasFetchedOnce(cached !== null);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agentId, packagesKey, apollo]);\n\n const refetch = useCallback(async () => {\n if (!agentId) {\n setInstalledMap({});\n return;\n }\n setLoading(true);\n try {\n // Use the Apollo client directly with `network-only` so out-of-band\n // installs (CLI, other tabs, agent-side changes) are always picked\n // up. `useAgentPluginsLazyQuery`'s lazy execute() doesn't accept\n // per-call fetchPolicy in Apollo v4; `apollo.query()` does.\n const { data } = await apollo.query<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n fetchPolicy: 'network-only',\n });\n const next: Record<string, boolean> = {};\n const installedList = data?.agentPlugins?.installed ?? [];\n const pluginError = data?.agentPlugins?.error ?? null;\n const installedSet = new Set(\n installedList.map((p: { packageName: string }) => p.packageName)\n );\n for (const pkg of packages) next[pkg] = installedSet.has(pkg);\n if (pluginError) {\n if (!classifyAgentStateError(pluginError)) {\n setInstalledMap(next);\n }\n // Log the raw error for debugging, but only surface a sanitized\n // version to the UI. Raw Bun fetch errors (\"Was there a typo in\n // the url or port?\") would otherwise leak into the install card.\n // eslint-disable-next-line no-console -- intentional: keep technical detail for debugging\n console.error('[ai] agentPlugins returned error', pluginError);\n setError(sanitizeAgentStateError(pluginError));\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n if (!classifyAgentStateError(pluginError)) {\n errorTimerRef.current = setTimeout(() => setError(null), 5000);\n }\n return;\n }\n setInstalledMap(next);\n // Clear any lingering error from a previously-aborted fetch so the\n // gate doesn't keep showing \"Agent not ready\" after a successful\n // subsequent request (e.g. React Strict Mode double-mount).\n setError(null);\n if (errorTimerRef.current) {\n clearTimeout(errorTimerRef.current);\n errorTimerRef.current = null;\n }\n } catch (err) {\n const msg =\n err instanceof Error ? err.message : 'Agent unreachable — unable to read plugin list';\n const isAgentStateError = classifyAgentStateError(msg);\n // Keep the last known installed state while the agent is restarting or\n // finalizing. Otherwise a transient 503 flashes an install CTA for an\n // already-installed plugin.\n if (!isAgentStateError) {\n setInstalledMap({});\n }\n // eslint-disable-next-line no-console -- intentional: keep technical detail for debugging\n console.error('[ai] agentPlugins fetch failed', err);\n setError(sanitizeAgentStateError(msg));\n // State-related errors stay visible until resolved; transient\n // network/GraphQL errors auto-dismiss so the UI stays tidy.\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n if (!isAgentStateError) {\n errorTimerRef.current = setTimeout(() => setError(null), 5000);\n }\n } finally {\n setLoading(false);\n setHasFetchedOnce(true);\n }\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [agentId, apollo, packagesKey]);\n\n useEffect(() => {\n void refetch();\n }, [refetch]);\n\n // Subscribe to the push stream. The svc emits the installed list\n // whenever it diverges from its previous snapshot, so we only update\n // local state on payloads — and we also write through to the Apollo\n // cache so other consumers reading `agentPlugins` directly see fresh\n // data without their own refetch.\n useVibecontrolsAgentPluginStreamSubscription({\n variables: { agentId: agentId ?? '' },\n skip: !agentId,\n onData: ({ data }) => {\n const installedList = data.data?.vibecontrolsAgentPluginStream;\n if (!installedList || !agentId) return;\n const installedSet = new Set(\n installedList.map((p: { packageName: string }) => p.packageName)\n );\n setInstalledMap((prev) => {\n const next: Record<string, boolean> = { ...prev };\n let changed = false;\n for (const pkg of packages) {\n const has = installedSet.has(pkg);\n if (next[pkg] !== has) {\n next[pkg] = has;\n changed = true;\n }\n }\n return changed ? next : prev;\n });\n setHasFetchedOnce(true);\n // Mirror into Apollo cache so consumers that read AgentPluginsQuery\n // directly (e.g. PluginHarnessPicker) see the same fresh list.\n try {\n const existing = apollo.readQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n });\n apollo.writeQuery<AgentPluginsQuery>({\n query: AgentPluginsDocument,\n variables: { agentId },\n data: {\n agentPlugins: {\n __typename: 'AgentPluginListResult',\n installed: installedList,\n available: existing?.agentPlugins?.available ?? [],\n error: null,\n },\n },\n });\n } catch {\n // Cache may not have been primed yet — the next refetch will\n // populate `available` and we'll write again on the next event.\n }\n },\n });\n\n // Also refetch when the tab regains focus so installs made elsewhere flow in.\n useEffect(() => {\n const onFocus = () => void refetch();\n window.addEventListener('focus', onFocus);\n return () => window.removeEventListener('focus', onFocus);\n }, [refetch]);\n\n useEffect(() => {\n return () => {\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n };\n }, []);\n\n const install = useCallback(\n async (packageName: string): Promise<boolean> => {\n if (!agentId) return false;\n setInstalling(packageName);\n setError(null);\n try {\n const { data } = await installPluginMutation({\n variables: { agentId, packageName },\n });\n if (data?.installAgentPlugin?.success) {\n setInstalledMap((prev) => ({ ...prev, [packageName]: true }));\n // Don't immediately refetch — the mutation returns success the\n // moment the agent's plugin manager accepts the install, but the\n // npm-level fetch + manifest publish takes a beat. An eager\n // refetch races that and overwrites our optimistic flip with a\n // stale \"not installed\" result, putting the install card right\n // back up.\n //\n // The subscription stream is the primary signal; this delayed\n // refetch is the fallback for environments where the SSE upgrade\n // is slow. 1200ms is enough for the agent to publish the new\n // entry on the plugin stream in the common case, short enough\n // that the user doesn't notice when AgentManagerPage's\n // onInstalled fires a window.location.reload() at 1500ms.\n setTimeout(() => void refetch(), 1200);\n return true;\n }\n throw new Error(data?.installAgentPlugin?.error || 'Install failed');\n } catch (err) {\n if (isQuotaExhaustedError(err)) {\n markQuotaHandledLocally();\n setQuotaExhausted(true);\n return false;\n }\n const msg = err instanceof Error ? err.message : 'Failed to install plugin';\n // eslint-disable-next-line no-console -- intentional: keep technical detail for debugging\n console.error('[ai] plugin install failed', err);\n setError(sanitizeAgentStateError(msg));\n if (errorTimerRef.current) clearTimeout(errorTimerRef.current);\n if (!classifyAgentStateError(msg)) {\n errorTimerRef.current = setTimeout(() => setError(null), 5000);\n }\n return false;\n } finally {\n setInstalling(null);\n }\n },\n [agentId, installPluginMutation, refetch]\n );\n\n const clearQuotaExhausted = useCallback(() => setQuotaExhausted(false), []);\n\n const missing = useMemo(\n () => packages.filter((pkg) => !installedMap[pkg]),\n [packages, installedMap]\n );\n const anyInstalled = useMemo(\n () => packages.some((pkg) => installedMap[pkg]),\n [packages, installedMap]\n );\n const allInstalled = missing.length === 0 && packages.length > 0;\n\n return {\n loading,\n initialLoading: !hasFetchedOnce,\n installed: installedMap,\n missing,\n anyInstalled,\n allInstalled,\n installing,\n install,\n error,\n errorIsAgentState: classifyAgentStateError(error),\n quotaExhausted,\n clearQuotaExhausted,\n refetch,\n };\n}\n"],"mappings":"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"}