@burdenoff/microfe-vibecontrols 2026.526.1 → 2026.526.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"AgentBackupTab.js","names":[],"sources":["../../../../src/components/agents/backup/AgentBackupTab.tsx"],"sourcesContent":["/**\n * AgentBackupTab — UI for managing SQLite database backups of a VibeControls agent.\n * Talks directly to the agent's REST API via its tunnel URL.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport {\n Database,\n Upload,\n RefreshCw,\n Settings,\n Play,\n Clock,\n CheckCircle2,\n XCircle,\n Loader2,\n Trash2,\n RotateCcw,\n Wifi,\n} from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\ninterface BackupConfig {\n enabled: boolean;\n scheduleIntervalMs: number;\n changeOnlyBackup: boolean;\n storageTarget: 'wspace' | 'custom-s3';\n customS3?: {\n endpoint: string;\n bucket: string;\n region: string;\n accessKeyId: string;\n secretAccessKey: string;\n pathPrefix?: string;\n };\n retentionDays?: number;\n maxBackups?: number;\n}\n\ninterface BackupRecord {\n id: string;\n timestamp: string;\n agentName: string;\n dbSizeBytes: number;\n sha256Hash: string;\n storageTarget: string;\n storagePath: string;\n durationMs: number;\n status: string;\n error?: string;\n}\n\ninterface BackupStatus {\n enabled: boolean;\n schedulerRunning: boolean;\n lastBackupTime: string | null;\n lastBackupHash: string | null;\n nextScheduledBackup: string | null;\n totalBackups: number;\n storageTarget: string;\n}\n\ninterface AgentBackupTabProps {\n agentTunnelUrl: string;\n agentApiKey: string;\n /** Canonical CLI profile name on the remote agent. */\n profile: string;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatTime(iso: string): string {\n return new Date(iso).toLocaleString();\n}\n\nexport function AgentBackupTab({ agentTunnelUrl, agentApiKey, profile }: AgentBackupTabProps) {\n const tr = useTr();\n const [config, setConfig] = useState<BackupConfig | null>(null);\n const [status, setStatus] = useState<BackupStatus | null>(null);\n const [backups, setBackups] = useState<BackupRecord[]>([]);\n const [loading, setLoading] = useState(true);\n const [actionLoading, setActionLoading] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [successMsg, setSuccessMsg] = useState<string | null>(null);\n const [showConfig, setShowConfig] = useState(false);\n\n const apiUrl = agentTunnelUrl.replace(/\\/+$/, '');\n\n const apiFetch = useCallback(\n async (path: string, options?: RequestInit) => {\n const res = await fetch(`${apiUrl}${path}`, {\n ...options,\n headers: {\n 'Content-Type': 'application/json',\n 'x-agent-api-key': agentApiKey,\n ...options?.headers,\n },\n });\n if (!res.ok) {\n const text = await res.text();\n throw new Error(text || `HTTP ${res.status}`);\n }\n return res.json();\n },\n [apiUrl, agentApiKey]\n );\n\n const refresh = useCallback(async () => {\n try {\n setLoading(true);\n setError(null);\n const [configRes, statusRes, listRes] = await Promise.all([\n apiFetch(agentApiPath(profile, '/backup/config')),\n apiFetch(agentApiPath(profile, '/backup/status')),\n apiFetch(`${agentApiPath(profile, '/backup/list')}?limit=20`),\n ]);\n setConfig(configRes.config);\n setStatus(statusRes.status);\n setBackups(listRes.backups || []);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n setError(\n msg === 'Failed to fetch'\n ? `Cannot reach agent REST API at ${apiUrl}. Ensure the agent is running and its tunnel URL is accessible.`\n : msg\n );\n } finally {\n setLoading(false);\n }\n }, [apiFetch, apiUrl]);\n\n useEffect(() => {\n refresh();\n }, [refresh]);\n\n // Clear success message after 3s\n useEffect(() => {\n if (successMsg) {\n const t = setTimeout(() => setSuccessMsg(null), 3000);\n return () => clearTimeout(t);\n }\n }, [successMsg]);\n\n const handleRunBackup = async (force: boolean) => {\n try {\n setActionLoading('backup');\n setError(null);\n const res = await apiFetch(`${agentApiPath(profile, '/backup/run')}?force=${force}`, {\n method: 'POST',\n });\n const b = res.backup;\n if (b.error === 'Skipped: no changes') {\n setSuccessMsg('No changes detected — backup skipped');\n } else {\n setSuccessMsg(\n `Backup completed: ${formatBytes(b.dbSizeBytes)} in ${formatDuration(b.durationMs)}`\n );\n }\n await refresh();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleRestore = async (backupId: string, dryRun: boolean) => {\n try {\n setActionLoading(`restore-${backupId}`);\n setError(null);\n const res = await apiFetch(agentApiPath(profile, '/backup/restore'), {\n method: 'POST',\n body: JSON.stringify({ backupId, dryRun }),\n });\n if (res.success) {\n setSuccessMsg(res.message);\n } else {\n setError(res.message);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleToggleEnabled = async () => {\n try {\n setActionLoading('toggle');\n const res = await apiFetch(agentApiPath(profile, '/backup/config'), {\n method: 'PUT',\n body: JSON.stringify({ enabled: !config?.enabled }),\n });\n setConfig(res.config);\n setSuccessMsg(res.config.enabled ? 'Backup enabled' : 'Backup disabled');\n await refresh();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleTestConnection = async () => {\n try {\n setActionLoading('test');\n const res = await apiFetch(agentApiPath(profile, '/backup/test-connection'), {\n method: 'POST',\n });\n if (res.ok) {\n setSuccessMsg(`Connection OK: ${res.message}`);\n } else {\n setError(`Connection failed: ${res.message}`);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleDeleteBackup = async (backupId: string) => {\n try {\n setActionLoading(`delete-${backupId}`);\n await apiFetch(agentApiPath(profile, `/backup/backups/${backupId}`), {\n method: 'DELETE',\n });\n setSuccessMsg('Backup deleted');\n await refresh();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center py-12 text-text-secondary\">\n <Loader2 className=\"w-5 h-5 animate-spin mr-2\" />\n {tr('vibecontrols.agents.backup.loading', 'Loading backup settings...')}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n {/* Status Messages */}\n {error && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-3 text-sm text-status-error-text\">\n <XCircle className=\"w-4 h-4 shrink-0\" />\n {error}\n <button onClick={() => setError(null)} className=\"ml-auto text-xs underline\">\n Dismiss\n </button>\n </div>\n )}\n {successMsg && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-success-border bg-status-success-bg-subtle p-3 text-sm text-status-success-text\">\n <CheckCircle2 className=\"w-4 h-4 shrink-0\" />\n {successMsg}\n </div>\n )}\n\n {/* Status Card */}\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-5\">\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2\">\n <Database className=\"w-5 h-5 text-action-primary-bg\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('vibecontrols.agents.backup.title', 'Database Backup')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={refresh}\n className=\"p-2 rounded-lg hover:bg-bg-sunken transition-colors text-text-secondary\"\n title=\"Refresh\"\n >\n <RefreshCw className=\"w-4 h-4\" />\n </button>\n <button\n onClick={() => setShowConfig(!showConfig)}\n className=\"p-2 rounded-lg hover:bg-bg-sunken transition-colors text-text-secondary\"\n title=\"Settings\"\n >\n <Settings className=\"w-4 h-4\" />\n </button>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-4\">\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.status', 'Status')}\n </p>\n <div className=\"flex items-center gap-1.5\">\n {config?.enabled ? (\n <>\n <div className=\"h-2 w-2 rounded-full bg-status-success-bg\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('vibecontrols.agents.backup.enabled', 'Enabled')}\n </span>\n </>\n ) : (\n <>\n <div className=\"h-2 w-2 rounded-full bg-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-secondary\">\n {tr('vibecontrols.agents.backup.disabled', 'Disabled')}\n </span>\n </>\n )}\n </div>\n </div>\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.scheduler', 'Scheduler')}\n </p>\n <span className=\"text-sm font-medium text-text-primary\">\n {status?.schedulerRunning ? 'Running' : 'Stopped'}\n </span>\n </div>\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.totalBackups', 'Total Backups')}\n </p>\n <span className=\"text-sm font-medium text-text-primary\">\n {status?.totalBackups ?? 0}\n </span>\n </div>\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.target', 'Target')}\n </p>\n <span className=\"text-sm font-medium text-text-primary\">\n {status?.storageTarget ?? 'N/A'}\n </span>\n </div>\n </div>\n\n {status?.lastBackupTime && (\n <div className=\"flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n <span>\n <Clock className=\"w-3 h-3 inline mr-1\" />\n Last: {formatTime(status.lastBackupTime)}\n </span>\n {status.nextScheduledBackup && (\n <span>Next: {formatTime(status.nextScheduledBackup)}</span>\n )}\n </div>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex flex-wrap gap-2 mt-4 pt-4 border-t border-border-subtle\">\n <button\n onClick={() => handleRunBackup(true)}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-action-primary-bg text-action-primary-text text-sm font-medium hover:bg-action-primary-bg-hover disabled:opacity-50 transition-colors\"\n >\n {actionLoading === 'backup' ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <Upload className=\"w-4 h-4\" />\n )}\n {tr('vibecontrols.agents.backup.backupNow', 'Backup Now')}\n </button>\n <button\n onClick={() => handleRunBackup(false)}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-sunken text-text-primary text-sm font-medium hover:bg-bg-canvas border border-border-default disabled:opacity-50 transition-colors\"\n >\n <Play className=\"w-4 h-4\" />\n {tr('vibecontrols.agents.backup.backupIfChanged', 'Backup if Changed')}\n </button>\n <button\n onClick={handleToggleEnabled}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-sunken text-text-primary text-sm font-medium hover:bg-bg-canvas border border-border-default disabled:opacity-50 transition-colors\"\n >\n {actionLoading === 'toggle' ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : config?.enabled ? (\n <XCircle className=\"w-4 h-4\" />\n ) : (\n <CheckCircle2 className=\"w-4 h-4\" />\n )}\n {config?.enabled\n ? tr('vibecontrols.agents.backup.disableSchedule', 'Disable Schedule')\n : tr('vibecontrols.agents.backup.enableSchedule', 'Enable Schedule')}\n </button>\n <button\n onClick={handleTestConnection}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-sunken text-text-primary text-sm font-medium hover:bg-bg-canvas border border-border-default disabled:opacity-50 transition-colors\"\n >\n {actionLoading === 'test' ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <Wifi className=\"w-4 h-4\" />\n )}\n {tr('vibecontrols.agents.backup.testConnection', 'Test Connection')}\n </button>\n </div>\n </div>\n\n {/* Configuration Panel (collapsible) */}\n {showConfig && config && (\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-5\">\n <h4 className=\"font-semibold text-text-primary mb-3 flex items-center gap-2\">\n <Settings className=\"w-4 h-4\" />\n {tr('vibecontrols.agents.backup.configuration', 'Configuration')}\n </h4>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 text-sm\">\n <div>\n <span className=\"text-text-muted\">Schedule Interval:</span>{' '}\n <span className=\"text-text-primary font-medium\">\n {config.scheduleIntervalMs >= 3600000\n ? `${config.scheduleIntervalMs / 3600000}h`\n : `${config.scheduleIntervalMs / 60000}m`}\n </span>\n </div>\n <div>\n <span className=\"text-text-muted\">Change Detection:</span>{' '}\n <span className=\"text-text-primary font-medium\">\n {config.changeOnlyBackup ? 'On' : 'Off'}\n </span>\n </div>\n <div>\n <span className=\"text-text-muted\">Storage Target:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.storageTarget}</span>\n </div>\n {config.customS3 && (\n <>\n <div>\n <span className=\"text-text-muted\">S3 Bucket:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.customS3.bucket}</span>\n </div>\n <div>\n <span className=\"text-text-muted\">S3 Region:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.customS3.region}</span>\n </div>\n </>\n )}\n {config.retentionDays && (\n <div>\n <span className=\"text-text-muted\">Retention:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.retentionDays} days</span>\n </div>\n )}\n {config.maxBackups && (\n <div>\n <span className=\"text-text-muted\">Max Backups:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.maxBackups}</span>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Backup History */}\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-5\">\n <h4 className=\"font-semibold text-text-primary mb-3 flex items-center gap-2\">\n <Clock className=\"w-4 h-4\" />\n {tr('vibecontrols.agents.backup.history', 'Backup History')}\n <span className=\"text-xs text-text-muted font-normal\">({backups.length} shown)</span>\n </h4>\n\n {backups.length === 0 ? (\n <p className=\"text-sm text-text-muted py-4 text-center\">\n {tr(\n 'vibecontrols.agents.backup.noBackups',\n 'No backups yet. Click \"Backup Now\" to create one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {backups.map((b) => (\n <div\n key={b.id}\n className=\"flex items-center justify-between p-3 rounded-lg border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div\n className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${\n b.status === 'completed'\n ? 'border border-status-success-border bg-status-success-bg-subtle'\n : 'border border-status-error-border bg-status-error-bg-subtle'\n }`}\n >\n {b.status === 'completed' ? (\n <CheckCircle2 className=\"w-4 h-4 text-status-success-text\" />\n ) : (\n <XCircle className=\"w-4 h-4 text-status-error-text\" />\n )}\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {formatTime(b.timestamp)}\n </p>\n <p className=\"text-xs text-text-muted truncate\">\n {formatBytes(b.dbSizeBytes)} &middot; {formatDuration(b.durationMs)} &middot;{' '}\n {b.storageTarget}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-1 shrink-0\">\n <button\n onClick={() => handleRestore(b.id, true)}\n disabled={!!actionLoading}\n className=\"p-1.5 rounded hover:bg-bg-canvas text-text-secondary transition-colors\"\n title=\"Validate (dry run)\"\n >\n {actionLoading === `restore-${b.id}` ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"w-4 h-4\" />\n )}\n </button>\n <button\n onClick={() => {\n if (confirm('Restore this backup? The agent will need to restart.')) {\n handleRestore(b.id, false);\n }\n }}\n disabled={!!actionLoading}\n className=\"p-1.5 rounded hover:bg-bg-canvas text-text-secondary transition-colors\"\n title=\"Restore\"\n >\n <RotateCcw className=\"w-4 h-4\" />\n </button>\n <button\n onClick={() => handleDeleteBackup(b.id)}\n disabled={!!actionLoading}\n className=\"p-1.5 rounded text-text-secondary transition-colors hover:bg-status-error-bg/80 hover:text-status-error-text\"\n title=\"Delete\"\n >\n {actionLoading === `delete-${b.id}` ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <Trash2 className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AgentBackupTab.js","names":[],"sources":["../../../../src/components/agents/backup/AgentBackupTab.tsx"],"sourcesContent":["/**\n * AgentBackupTab — UI for managing SQLite database backups of a VibeControls agent.\n * Talks directly to the agent's REST API via its tunnel URL.\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport {\n Database,\n Upload,\n RefreshCw,\n Settings,\n Play,\n Clock,\n CheckCircle2,\n XCircle,\n Loader2,\n Trash2,\n RotateCcw,\n Wifi,\n} from 'lucide-react';\nimport { useTr } from '../../../shared/hooks/useTr';\nimport { agentApiPath } from '@/utils/agentApiPath';\n\n/**\n * Detect error strings that look like a raw JavaScript runtime exception\n * the agent leaked through to the platform — e.g.\n * `undefined is not an object (evaluating 'createResult.data.createFolder')`.\n * The actual root cause lives in the agent's backup plugin (it doesn't\n * forward x-workspace-id when calling files-svc, so the GraphQL response\n * has no `data` and the plugin blindly dereferences it). Until the agent\n * fix lands we strip the JS-frame and show the user something they can act\n * on instead of pasting a stack-trace fragment into the UI. [BOFF-2613]\n */\nfunction sanitizeBackupError(raw: string): string {\n if (!raw) return 'Backup failed.';\n const m = raw.toLowerCase();\n // JS runtime exception leaked from the agent.\n if (\n m.includes('undefined is not an object') ||\n m.includes('cannot read property') ||\n m.includes('cannot read properties of') ||\n /\\btypeerror:/.test(m) ||\n /\\breferenceerror:/.test(m)\n ) {\n // Specific, actionable hint for the known x-workspace-id case.\n if (raw.includes('createResult.data.createFolder')) {\n return \"Backup failed: the agent could not reach the workspace files service (missing workspace context). Re-push credentials from the agent's Security tab and try again. See agent logs for the full error.\";\n }\n return 'Backup failed: the agent threw an internal error. See agent logs for the full error.';\n }\n if (raw.length > 200) return raw.slice(0, 200) + '…';\n return raw;\n}\n\ninterface BackupConfig {\n enabled: boolean;\n scheduleIntervalMs: number;\n changeOnlyBackup: boolean;\n storageTarget: 'wspace' | 'custom-s3';\n customS3?: {\n endpoint: string;\n bucket: string;\n region: string;\n accessKeyId: string;\n secretAccessKey: string;\n pathPrefix?: string;\n };\n retentionDays?: number;\n maxBackups?: number;\n}\n\ninterface BackupRecord {\n id: string;\n timestamp: string;\n agentName: string;\n dbSizeBytes: number;\n sha256Hash: string;\n storageTarget: string;\n storagePath: string;\n durationMs: number;\n status: string;\n error?: string;\n}\n\ninterface BackupStatus {\n enabled: boolean;\n schedulerRunning: boolean;\n lastBackupTime: string | null;\n lastBackupHash: string | null;\n nextScheduledBackup: string | null;\n totalBackups: number;\n storageTarget: string;\n}\n\ninterface AgentBackupTabProps {\n agentTunnelUrl: string;\n agentApiKey: string;\n /** Canonical CLI profile name on the remote agent. */\n profile: string;\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;\n return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;\n}\n\nfunction formatDuration(ms: number): string {\n if (ms < 1000) return `${ms}ms`;\n return `${(ms / 1000).toFixed(1)}s`;\n}\n\nfunction formatTime(iso: string): string {\n return new Date(iso).toLocaleString();\n}\n\nexport function AgentBackupTab({ agentTunnelUrl, agentApiKey, profile }: AgentBackupTabProps) {\n const tr = useTr();\n const [config, setConfig] = useState<BackupConfig | null>(null);\n const [status, setStatus] = useState<BackupStatus | null>(null);\n const [backups, setBackups] = useState<BackupRecord[]>([]);\n const [loading, setLoading] = useState(true);\n const [actionLoading, setActionLoading] = useState<string | null>(null);\n const [error, setError] = useState<string | null>(null);\n const [successMsg, setSuccessMsg] = useState<string | null>(null);\n const [showConfig, setShowConfig] = useState(false);\n\n const apiUrl = agentTunnelUrl.replace(/\\/+$/, '');\n\n const apiFetch = useCallback(\n async (path: string, options?: RequestInit) => {\n const res = await fetch(`${apiUrl}${path}`, {\n ...options,\n headers: {\n 'Content-Type': 'application/json',\n 'x-agent-api-key': agentApiKey,\n ...options?.headers,\n },\n });\n if (!res.ok) {\n const text = await res.text();\n throw new Error(text || `HTTP ${res.status}`);\n }\n return res.json();\n },\n [apiUrl, agentApiKey]\n );\n\n const refresh = useCallback(async () => {\n try {\n setLoading(true);\n setError(null);\n const [configRes, statusRes, listRes] = await Promise.all([\n apiFetch(agentApiPath(profile, '/backup/config')),\n apiFetch(agentApiPath(profile, '/backup/status')),\n apiFetch(`${agentApiPath(profile, '/backup/list')}?limit=20`),\n ]);\n setConfig(configRes.config);\n setStatus(statusRes.status);\n setBackups(listRes.backups || []);\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n setError(\n msg === 'Failed to fetch'\n ? `Cannot reach agent REST API at ${apiUrl}. Ensure the agent is running and its tunnel URL is accessible.`\n : msg\n );\n } finally {\n setLoading(false);\n }\n }, [apiFetch, apiUrl]);\n\n useEffect(() => {\n refresh();\n }, [refresh]);\n\n // Clear success message after 3s\n useEffect(() => {\n if (successMsg) {\n const t = setTimeout(() => setSuccessMsg(null), 3000);\n return () => clearTimeout(t);\n }\n }, [successMsg]);\n\n const handleRunBackup = async (force: boolean) => {\n try {\n setActionLoading('backup');\n setError(null);\n const res = await apiFetch(`${agentApiPath(profile, '/backup/run')}?force=${force}`, {\n method: 'POST',\n });\n // The agent's `/backup/run` returns HTTP 200 even when the backup\n // failed internally — the failure surfaces as `res.error` (top-level)\n // or as `res.backup === null`. Treat both as failures here and surface\n // a sanitized message. [BOFF-2613]\n const b = res?.backup;\n const failureMessage: string | null =\n (typeof res?.error === 'string' && res.error) ||\n (b && typeof b.error === 'string' && b.error !== 'Skipped: no changes' ? b.error : null);\n\n if (failureMessage) {\n setError(sanitizeBackupError(failureMessage));\n return;\n }\n if (!b) {\n // No backup payload AND no error string — defensive.\n setError(sanitizeBackupError(''));\n return;\n }\n if (b.error === 'Skipped: no changes') {\n setSuccessMsg('No changes detected — backup skipped');\n } else {\n setSuccessMsg(\n `Backup completed: ${formatBytes(b.dbSizeBytes)} in ${formatDuration(b.durationMs)}`\n );\n }\n await refresh();\n } catch (err) {\n setError(sanitizeBackupError(err instanceof Error ? err.message : String(err)));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleRestore = async (backupId: string, dryRun: boolean) => {\n try {\n setActionLoading(`restore-${backupId}`);\n setError(null);\n const res = await apiFetch(agentApiPath(profile, '/backup/restore'), {\n method: 'POST',\n body: JSON.stringify({ backupId, dryRun }),\n });\n if (res.success) {\n setSuccessMsg(res.message);\n } else {\n setError(res.message);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleToggleEnabled = async () => {\n try {\n setActionLoading('toggle');\n const res = await apiFetch(agentApiPath(profile, '/backup/config'), {\n method: 'PUT',\n body: JSON.stringify({ enabled: !config?.enabled }),\n });\n setConfig(res.config);\n setSuccessMsg(res.config.enabled ? 'Backup enabled' : 'Backup disabled');\n await refresh();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleTestConnection = async () => {\n try {\n setActionLoading('test');\n const res = await apiFetch(agentApiPath(profile, '/backup/test-connection'), {\n method: 'POST',\n });\n if (res.ok) {\n setSuccessMsg(`Connection OK: ${res.message}`);\n } else {\n setError(`Connection failed: ${res.message}`);\n }\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n const handleDeleteBackup = async (backupId: string) => {\n try {\n setActionLoading(`delete-${backupId}`);\n await apiFetch(agentApiPath(profile, `/backup/backups/${backupId}`), {\n method: 'DELETE',\n });\n setSuccessMsg('Backup deleted');\n await refresh();\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setActionLoading(null);\n }\n };\n\n if (loading) {\n return (\n <div className=\"flex items-center justify-center py-12 text-text-secondary\">\n <Loader2 className=\"w-5 h-5 animate-spin mr-2\" />\n {tr('vibecontrols.agents.backup.loading', 'Loading backup settings...')}\n </div>\n );\n }\n\n return (\n <div className=\"space-y-6\">\n {/* Status Messages */}\n {error && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-error-border bg-status-error-bg-subtle p-3 text-sm text-status-error-text\">\n <XCircle className=\"w-4 h-4 shrink-0\" />\n {error}\n <button onClick={() => setError(null)} className=\"ml-auto text-xs underline\">\n Dismiss\n </button>\n </div>\n )}\n {successMsg && (\n <div className=\"flex items-center gap-2 rounded-lg border border-status-success-border bg-status-success-bg-subtle p-3 text-sm text-status-success-text\">\n <CheckCircle2 className=\"w-4 h-4 shrink-0\" />\n {successMsg}\n </div>\n )}\n\n {/* Status Card */}\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-5\">\n <div className=\"flex items-center justify-between mb-4\">\n <div className=\"flex items-center gap-2\">\n <Database className=\"w-5 h-5 text-action-primary-bg\" />\n <h3 className=\"font-semibold text-text-primary\">\n {tr('vibecontrols.agents.backup.title', 'Database Backup')}\n </h3>\n </div>\n <div className=\"flex items-center gap-2\">\n <button\n onClick={refresh}\n className=\"p-2 rounded-lg hover:bg-bg-sunken transition-colors text-text-secondary\"\n title=\"Refresh\"\n >\n <RefreshCw className=\"w-4 h-4\" />\n </button>\n <button\n onClick={() => setShowConfig(!showConfig)}\n className=\"p-2 rounded-lg hover:bg-bg-sunken transition-colors text-text-secondary\"\n title=\"Settings\"\n >\n <Settings className=\"w-4 h-4\" />\n </button>\n </div>\n </div>\n\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-4\">\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.status', 'Status')}\n </p>\n <div className=\"flex items-center gap-1.5\">\n {config?.enabled ? (\n <>\n <div className=\"h-2 w-2 rounded-full bg-status-success-bg\" />\n <span className=\"text-sm font-medium text-text-primary\">\n {tr('vibecontrols.agents.backup.enabled', 'Enabled')}\n </span>\n </>\n ) : (\n <>\n <div className=\"h-2 w-2 rounded-full bg-text-tertiary\" />\n <span className=\"text-sm font-medium text-text-secondary\">\n {tr('vibecontrols.agents.backup.disabled', 'Disabled')}\n </span>\n </>\n )}\n </div>\n </div>\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.scheduler', 'Scheduler')}\n </p>\n <span className=\"text-sm font-medium text-text-primary\">\n {status?.schedulerRunning ? 'Running' : 'Stopped'}\n </span>\n </div>\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.totalBackups', 'Total Backups')}\n </p>\n <span className=\"text-sm font-medium text-text-primary\">\n {status?.totalBackups ?? 0}\n </span>\n </div>\n <div>\n <p className=\"text-xs text-text-muted mb-1\">\n {tr('vibecontrols.agents.backup.target', 'Target')}\n </p>\n <span className=\"text-sm font-medium text-text-primary\">\n {status?.storageTarget ?? 'N/A'}\n </span>\n </div>\n </div>\n\n {status?.lastBackupTime && (\n <div className=\"flex flex-wrap items-center gap-3 text-xs text-text-muted\">\n <span>\n <Clock className=\"w-3 h-3 inline mr-1\" />\n Last: {formatTime(status.lastBackupTime)}\n </span>\n {status.nextScheduledBackup && (\n <span>Next: {formatTime(status.nextScheduledBackup)}</span>\n )}\n </div>\n )}\n\n {/* Action Buttons */}\n <div className=\"flex flex-wrap gap-2 mt-4 pt-4 border-t border-border-subtle\">\n <button\n onClick={() => handleRunBackup(true)}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-action-primary-bg text-action-primary-text text-sm font-medium hover:bg-action-primary-bg-hover disabled:opacity-50 transition-colors\"\n >\n {actionLoading === 'backup' ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <Upload className=\"w-4 h-4\" />\n )}\n {tr('vibecontrols.agents.backup.backupNow', 'Backup Now')}\n </button>\n <button\n onClick={() => handleRunBackup(false)}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-sunken text-text-primary text-sm font-medium hover:bg-bg-canvas border border-border-default disabled:opacity-50 transition-colors\"\n >\n <Play className=\"w-4 h-4\" />\n {tr('vibecontrols.agents.backup.backupIfChanged', 'Backup if Changed')}\n </button>\n <button\n onClick={handleToggleEnabled}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-sunken text-text-primary text-sm font-medium hover:bg-bg-canvas border border-border-default disabled:opacity-50 transition-colors\"\n >\n {actionLoading === 'toggle' ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : config?.enabled ? (\n <XCircle className=\"w-4 h-4\" />\n ) : (\n <CheckCircle2 className=\"w-4 h-4\" />\n )}\n {config?.enabled\n ? tr('vibecontrols.agents.backup.disableSchedule', 'Disable Schedule')\n : tr('vibecontrols.agents.backup.enableSchedule', 'Enable Schedule')}\n </button>\n <button\n onClick={handleTestConnection}\n disabled={!!actionLoading}\n className=\"flex items-center gap-2 px-3 py-1.5 rounded-lg bg-bg-sunken text-text-primary text-sm font-medium hover:bg-bg-canvas border border-border-default disabled:opacity-50 transition-colors\"\n >\n {actionLoading === 'test' ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <Wifi className=\"w-4 h-4\" />\n )}\n {tr('vibecontrols.agents.backup.testConnection', 'Test Connection')}\n </button>\n </div>\n </div>\n\n {/* Configuration Panel (collapsible) */}\n {showConfig && config && (\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-5\">\n <h4 className=\"font-semibold text-text-primary mb-3 flex items-center gap-2\">\n <Settings className=\"w-4 h-4\" />\n {tr('vibecontrols.agents.backup.configuration', 'Configuration')}\n </h4>\n <div className=\"grid grid-cols-1 md:grid-cols-2 gap-4 text-sm\">\n <div>\n <span className=\"text-text-muted\">Schedule Interval:</span>{' '}\n <span className=\"text-text-primary font-medium\">\n {config.scheduleIntervalMs >= 3600000\n ? `${config.scheduleIntervalMs / 3600000}h`\n : `${config.scheduleIntervalMs / 60000}m`}\n </span>\n </div>\n <div>\n <span className=\"text-text-muted\">Change Detection:</span>{' '}\n <span className=\"text-text-primary font-medium\">\n {config.changeOnlyBackup ? 'On' : 'Off'}\n </span>\n </div>\n <div>\n <span className=\"text-text-muted\">Storage Target:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.storageTarget}</span>\n </div>\n {config.customS3 && (\n <>\n <div>\n <span className=\"text-text-muted\">S3 Bucket:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.customS3.bucket}</span>\n </div>\n <div>\n <span className=\"text-text-muted\">S3 Region:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.customS3.region}</span>\n </div>\n </>\n )}\n {config.retentionDays && (\n <div>\n <span className=\"text-text-muted\">Retention:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.retentionDays} days</span>\n </div>\n )}\n {config.maxBackups && (\n <div>\n <span className=\"text-text-muted\">Max Backups:</span>{' '}\n <span className=\"text-text-primary font-medium\">{config.maxBackups}</span>\n </div>\n )}\n </div>\n </div>\n )}\n\n {/* Backup History */}\n <div className=\"rounded-lg border border-border-default bg-bg-surface p-5\">\n <h4 className=\"font-semibold text-text-primary mb-3 flex items-center gap-2\">\n <Clock className=\"w-4 h-4\" />\n {tr('vibecontrols.agents.backup.history', 'Backup History')}\n <span className=\"text-xs text-text-muted font-normal\">({backups.length} shown)</span>\n </h4>\n\n {backups.length === 0 ? (\n <p className=\"text-sm text-text-muted py-4 text-center\">\n {tr(\n 'vibecontrols.agents.backup.noBackups',\n 'No backups yet. Click \"Backup Now\" to create one.'\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {backups.map((b) => (\n <div\n key={b.id}\n className=\"flex items-center justify-between p-3 rounded-lg border border-border-subtle hover:bg-bg-sunken transition-colors\"\n >\n <div className=\"flex items-center gap-3 min-w-0\">\n <div\n className={`w-8 h-8 rounded-lg flex items-center justify-center shrink-0 ${\n b.status === 'completed'\n ? 'border border-status-success-border bg-status-success-bg-subtle'\n : 'border border-status-error-border bg-status-error-bg-subtle'\n }`}\n >\n {b.status === 'completed' ? (\n <CheckCircle2 className=\"w-4 h-4 text-status-success-text\" />\n ) : (\n <XCircle className=\"w-4 h-4 text-status-error-text\" />\n )}\n </div>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-medium text-text-primary truncate\">\n {formatTime(b.timestamp)}\n </p>\n <p className=\"text-xs text-text-muted truncate\">\n {formatBytes(b.dbSizeBytes)} &middot; {formatDuration(b.durationMs)} &middot;{' '}\n {b.storageTarget}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-1 shrink-0\">\n <button\n onClick={() => handleRestore(b.id, true)}\n disabled={!!actionLoading}\n className=\"p-1.5 rounded hover:bg-bg-canvas text-text-secondary transition-colors\"\n title=\"Validate (dry run)\"\n >\n {actionLoading === `restore-${b.id}` ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <CheckCircle2 className=\"w-4 h-4\" />\n )}\n </button>\n <button\n onClick={() => {\n if (confirm('Restore this backup? The agent will need to restart.')) {\n handleRestore(b.id, false);\n }\n }}\n disabled={!!actionLoading}\n className=\"p-1.5 rounded hover:bg-bg-canvas text-text-secondary transition-colors\"\n title=\"Restore\"\n >\n <RotateCcw className=\"w-4 h-4\" />\n </button>\n <button\n onClick={() => handleDeleteBackup(b.id)}\n disabled={!!actionLoading}\n className=\"p-1.5 rounded text-text-secondary transition-colors hover:bg-status-error-bg/80 hover:text-status-error-text\"\n title=\"Delete\"\n >\n {actionLoading === `delete-${b.id}` ? (\n <Loader2 className=\"w-4 h-4 animate-spin\" />\n ) : (\n <Trash2 className=\"w-4 h-4\" />\n )}\n </button>\n </div>\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
@@ -1,119 +1,118 @@
1
1
  import { useRemoveAgentPluginMutation as e } from "../../generated/wspace-operations.js";
2
2
  import { Card as t } from "../shared/Card.js";
3
3
  import { usePluginAvailability as n } from "../../hooks/usePluginAvailability.js";
4
- import { useActiveAgentProfile as r } from "../../hooks/useActiveAgentProfile.js";
5
- import { getPluginMeta as i } from "../../constants/pluginCatalog.js";
6
- import { useCallback as a, useEffect as o, useRef as s, useState as c } from "react";
7
- import { Check as l, CheckCircle2 as u, Download as d, Loader2 as f, Puzzle as p, Trash2 as m } from "lucide-react";
8
- import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
4
+ import { getPluginMeta as r } from "../../constants/pluginCatalog.js";
5
+ import { useCallback as i, useEffect as a, useRef as o, useState as s } from "react";
6
+ import { Check as c, CheckCircle2 as l, Download as u, Loader2 as d, Puzzle as f, Trash2 as p } from "lucide-react";
7
+ import { Fragment as m, jsx as h, jsxs as g } from "react/jsx-runtime";
9
8
  //#region src/components/ai/PluginHarnessPicker.tsx
10
- function v({ packages: v, agentId: y, title: b, description: x, onChange: S }) {
11
- let { active: C } = r(y), { installed: w, installing: T, install: E, refetch: D, loading: O, error: k } = n(v, y, C), [A] = e(), [j, M] = c(null), [N, P] = c({}), [F, I] = c({}), L = s(/* @__PURE__ */ new Map());
12
- o(() => {
13
- let e = L.current;
9
+ function _({ packages: _, agentId: v, title: y, description: b, onChange: x }) {
10
+ let { installed: S, installing: C, install: w, refetch: T, loading: E, error: D } = n(_, v), [O] = e(), [k, A] = s(null), [j, M] = s({}), [N, P] = s({}), F = o(/* @__PURE__ */ new Map());
11
+ a(() => {
12
+ let e = F.current;
14
13
  return () => {
15
14
  for (let t of e.values()) clearTimeout(t);
16
15
  e.clear();
17
16
  };
18
17
  }, []);
19
- let R = a(async (e) => {
20
- if (await E(e)) {
21
- I((t) => ({
18
+ let I = i(async (e) => {
19
+ if (await w(e)) {
20
+ P((t) => ({
22
21
  ...t,
23
22
  [e]: !0
24
23
  }));
25
- let t = L.current.get(e);
24
+ let t = F.current.get(e);
26
25
  t && clearTimeout(t);
27
26
  let n = setTimeout(() => {
28
- I((t) => {
27
+ P((t) => {
29
28
  if (!t[e]) return t;
30
29
  let n = { ...t };
31
30
  return delete n[e], n;
32
- }), L.current.delete(e);
31
+ }), F.current.delete(e);
33
32
  }, 1500);
34
- L.current.set(e, n), S?.();
33
+ F.current.set(e, n), x?.();
35
34
  }
36
- }, [E, S]), z = a(async (e) => {
37
- if (y) {
38
- M(e), P((t) => ({
35
+ }, [w, x]), L = i(async (e) => {
36
+ if (v) {
37
+ A(e), M((t) => ({
39
38
  ...t,
40
39
  [e]: ""
41
40
  }));
42
41
  try {
43
- let { data: t } = await A({ variables: {
44
- agentId: y,
42
+ let { data: t } = await O({ variables: {
43
+ agentId: v,
45
44
  packageName: e
46
45
  } });
47
46
  if (!t?.removeAgentPlugin.success) {
48
- P((n) => ({
47
+ M((n) => ({
49
48
  ...n,
50
49
  [e]: t?.removeAgentPlugin.error || "Remove failed"
51
50
  }));
52
51
  return;
53
52
  }
54
- await D(), S?.();
53
+ await T(), x?.();
55
54
  } catch (t) {
56
- P((n) => ({
55
+ M((n) => ({
57
56
  ...n,
58
57
  [e]: t instanceof Error ? t.message : String(t)
59
58
  }));
60
59
  } finally {
61
- M(null);
60
+ A(null);
62
61
  }
63
62
  }
64
63
  }, [
65
- y,
66
- A,
67
- D,
68
- S
64
+ v,
65
+ O,
66
+ T,
67
+ x
69
68
  ]);
70
- return v.length === 0 ? null : /* @__PURE__ */ _("section", {
69
+ return _.length === 0 ? null : /* @__PURE__ */ g("section", {
71
70
  className: "flex flex-col gap-3",
72
71
  "data-testid": "plugin-harness-picker",
73
72
  children: [
74
- (b || x) && /* @__PURE__ */ _("div", { children: [b && /* @__PURE__ */ g("h3", {
73
+ (y || b) && /* @__PURE__ */ g("div", { children: [y && /* @__PURE__ */ h("h3", {
75
74
  className: "text-base font-semibold text-text-primary",
76
- children: b
77
- }), x && /* @__PURE__ */ g("p", {
75
+ children: y
76
+ }), b && /* @__PURE__ */ h("p", {
78
77
  className: "text-sm text-text-secondary mt-1",
79
- children: x
78
+ children: b
80
79
  })] }),
81
- k && /* @__PURE__ */ g("div", {
80
+ D && /* @__PURE__ */ h("div", {
82
81
  role: "alert",
83
82
  className: "rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text",
84
- children: k
83
+ children: D
85
84
  }),
86
- /* @__PURE__ */ g("div", {
85
+ /* @__PURE__ */ h("div", {
87
86
  className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3",
88
- children: v.map((e) => {
89
- let n = i(e), r = !!w[e], a = T === e, o = j === e, s = a || o || O, c = !!F[e], v = N[e];
90
- return /* @__PURE__ */ _(t, {
87
+ children: _.map((e) => {
88
+ let n = r(e), i = !!S[e], a = C === e, o = k === e, s = a || o || E, _ = !!N[e], y = j[e];
89
+ return /* @__PURE__ */ g(t, {
91
90
  padding: "md",
92
91
  className: "flex flex-col gap-3",
93
92
  children: [
94
- /* @__PURE__ */ _("div", {
93
+ /* @__PURE__ */ g("div", {
95
94
  className: "flex items-start gap-3",
96
- children: [/* @__PURE__ */ g("div", {
95
+ children: [/* @__PURE__ */ h("div", {
97
96
  className: "flex-shrink-0 w-9 h-9 rounded-md bg-bg-sunken flex items-center justify-center",
98
- children: /* @__PURE__ */ g(p, { className: "w-4 h-4 text-text-secondary" })
99
- }), /* @__PURE__ */ _("div", {
97
+ children: /* @__PURE__ */ h(f, { className: "w-4 h-4 text-text-secondary" })
98
+ }), /* @__PURE__ */ g("div", {
100
99
  className: "min-w-0 flex-1",
101
100
  children: [
102
- /* @__PURE__ */ _("div", {
101
+ /* @__PURE__ */ g("div", {
103
102
  className: "flex items-start gap-2 flex-wrap",
104
- children: [/* @__PURE__ */ g("h4", {
103
+ children: [/* @__PURE__ */ h("h4", {
105
104
  className: "font-medium text-text-primary break-words max-w-[100ch]",
106
105
  children: n.displayName
107
- }), r && /* @__PURE__ */ _("span", {
106
+ }), i && /* @__PURE__ */ g("span", {
108
107
  className: "inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-status-success-bg text-status-success-text shrink-0",
109
- children: [/* @__PURE__ */ g(l, { className: "w-3 h-3" }), " installed"]
108
+ children: [/* @__PURE__ */ h(c, { className: "w-3 h-3" }), " installed"]
110
109
  })]
111
110
  }),
112
- /* @__PURE__ */ g("p", {
111
+ /* @__PURE__ */ h("p", {
113
112
  className: "text-sm text-text-secondary mt-0.5",
114
113
  children: n.description
115
114
  }),
116
- /* @__PURE__ */ g("p", {
115
+ /* @__PURE__ */ h("p", {
117
116
  className: "text-[10px] font-mono text-text-placeholder mt-1 break-all",
118
117
  title: e,
119
118
  children: e
@@ -121,41 +120,41 @@ function v({ packages: v, agentId: y, title: b, description: x, onChange: S }) {
121
120
  ]
122
121
  })]
123
122
  }),
124
- /* @__PURE__ */ g("div", {
123
+ /* @__PURE__ */ h("div", {
125
124
  className: "flex items-center gap-2",
126
- children: r && c ? /* @__PURE__ */ _("button", {
125
+ children: i && _ ? /* @__PURE__ */ g("button", {
127
126
  type: "button",
128
127
  disabled: !0,
129
128
  className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg text-status-success-text border border-status-success-border cursor-default",
130
129
  "data-testid": `plugin-installed-${e}`,
131
130
  "aria-live": "polite",
132
- children: [/* @__PURE__ */ g(u, { className: "w-3.5 h-3.5" }), "Installed"]
133
- }) : r ? /* @__PURE__ */ _("button", {
131
+ children: [/* @__PURE__ */ h(l, { className: "w-3.5 h-3.5" }), "Installed"]
132
+ }) : i ? /* @__PURE__ */ g("button", {
134
133
  type: "button",
135
- disabled: s || !y,
136
- onClick: () => z(e),
134
+ disabled: s || !v,
135
+ onClick: () => L(e),
137
136
  className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border border-border-default text-text-secondary hover:border-status-error-border hover:text-status-error-text transition disabled:opacity-60 disabled:cursor-not-allowed",
138
137
  "data-testid": `plugin-remove-${e}`,
139
- children: [o ? /* @__PURE__ */ g(f, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ g(m, { className: "w-3.5 h-3.5" }), "Remove"]
140
- }) : /* @__PURE__ */ g("button", {
138
+ children: [o ? /* @__PURE__ */ h(d, { className: "w-3.5 h-3.5 animate-spin" }) : /* @__PURE__ */ h(p, { className: "w-3.5 h-3.5" }), "Remove"]
139
+ }) : /* @__PURE__ */ h("button", {
141
140
  type: "button",
142
- disabled: s || !y,
143
- onClick: () => R(e),
141
+ disabled: s || !v,
142
+ onClick: () => I(e),
144
143
  className: "inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition disabled:opacity-60 disabled:cursor-not-allowed",
145
144
  "data-testid": `plugin-install-${e}`,
146
145
  "aria-busy": a,
147
- children: a ? /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(f, { className: "w-3.5 h-3.5 animate-spin" }), "Installing…"] }) : /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(d, { className: "w-3.5 h-3.5" }), "Install"] })
146
+ children: a ? /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(d, { className: "w-3.5 h-3.5 animate-spin" }), "Installing…"] }) : /* @__PURE__ */ g(m, { children: [/* @__PURE__ */ h(u, { className: "w-3.5 h-3.5" }), "Install"] })
148
147
  })
149
148
  }),
150
- v && /* @__PURE__ */ g("p", {
149
+ y && /* @__PURE__ */ h("p", {
151
150
  className: "text-xs text-status-error-text",
152
- children: v
151
+ children: y
153
152
  })
154
153
  ]
155
154
  }, e);
156
155
  })
157
156
  }),
158
- !y && /* @__PURE__ */ g("p", {
157
+ !v && /* @__PURE__ */ h("p", {
159
158
  className: "text-xs text-text-muted",
160
159
  children: "Select an agent to manage plugins."
161
160
  })
@@ -163,6 +162,6 @@ function v({ packages: v, agentId: y, title: b, description: x, onChange: S }) {
163
162
  });
164
163
  }
165
164
  //#endregion
166
- export { v as PluginHarnessPicker };
165
+ export { _ as PluginHarnessPicker };
167
166
 
168
167
  //# sourceMappingURL=PluginHarnessPicker.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"PluginHarnessPicker.js","names":[],"sources":["../../../src/components/ai/PluginHarnessPicker.tsx"],"sourcesContent":["/**\n * <PluginHarnessPicker />\n *\n * Grid of installable VibeControls agent plugins with per-card\n * Install / Installed / Remove state. Driven by the existing\n * `usePluginAvailability` hook (same code path as `<PluginGate>`), so\n * the Plugins tab on the Agent details page and this picker stay in\n * sync automatically.\n *\n * The default use-case is the AI workbench: once the base\n * `@vibecontrols/vibe-plugin-ai` plugin is installed, this picker lists\n * every AI provider harness (claude, codex, copilot, gemini, …) so the\n * user can add / remove harnesses without leaving the AI page.\n *\n * The component is feature-neutral — pass any `packages` array (e.g.\n * session providers, tunnel providers) to reuse the same UI.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Check, CheckCircle2, Download, Loader2, Puzzle, Trash2 } from 'lucide-react';\nimport { useRemoveAgentPluginMutation } from '@/generated/wspace-operations';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { useActiveAgentProfile } from '@/hooks/useActiveAgentProfile';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { Card } from '../shared/Card';\n\nexport interface PluginHarnessPickerProps {\n /** Package names to offer. Metadata is resolved via `getPluginMeta`. */\n packages: readonly string[];\n /** Active agent id. When null, the picker renders in a disabled state. */\n agentId: string | null;\n /** Optional section heading (rendered above the grid). */\n title?: string;\n /** Optional section description. */\n description?: string;\n /** Fired after a successful install or remove so parents can refetch. */\n onChange?: () => void;\n}\n\nexport function PluginHarnessPicker({\n packages,\n agentId,\n title,\n description,\n onChange,\n}: PluginHarnessPickerProps) {\n const { active: activeProfile } = useActiveAgentProfile(agentId);\n const { installed, installing, install, refetch, loading, error } = usePluginAvailability(\n packages,\n agentId,\n activeProfile\n );\n const [removePlugin] = useRemoveAgentPluginMutation();\n const [removingPkg, setRemovingPkg] = useState<string | null>(null);\n const [removeError, setRemoveError] = useState<Record<string, string>>({});\n // Per-package \"just installed\" pulse — flips on for ~1.5s after a\n // successful install so the button shows a clear success affordance\n // (checkmark + \"Installed\") before the card transitions to its\n // installed/Remove state. Without this the button silently flips from\n // spinner to Remove and users miss the success signal.\n const [justInstalled, setJustInstalled] = useState<Record<string, boolean>>({});\n const justInstalledTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());\n\n useEffect(() => {\n const timers = justInstalledTimers.current;\n return () => {\n for (const t of timers.values()) clearTimeout(t);\n timers.clear();\n };\n }, []);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n const ok = await install(pkg);\n if (ok) {\n setJustInstalled((prev) => ({ ...prev, [pkg]: true }));\n const existing = justInstalledTimers.current.get(pkg);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n setJustInstalled((prev) => {\n if (!prev[pkg]) return prev;\n const next = { ...prev };\n delete next[pkg];\n return next;\n });\n justInstalledTimers.current.delete(pkg);\n }, 1500);\n justInstalledTimers.current.set(pkg, timer);\n onChange?.();\n }\n },\n [install, onChange]\n );\n\n const handleRemove = useCallback(\n async (pkg: string) => {\n if (!agentId) return;\n setRemovingPkg(pkg);\n setRemoveError((e) => ({ ...e, [pkg]: '' }));\n try {\n const { data } = await removePlugin({\n variables: { agentId, packageName: pkg },\n });\n if (!data?.removeAgentPlugin.success) {\n setRemoveError((e) => ({\n ...e,\n [pkg]: data?.removeAgentPlugin.error || 'Remove failed',\n }));\n return;\n }\n await refetch();\n onChange?.();\n } catch (e) {\n setRemoveError((err) => ({\n ...err,\n [pkg]: e instanceof Error ? e.message : String(e),\n }));\n } finally {\n setRemovingPkg(null);\n }\n },\n [agentId, removePlugin, refetch, onChange]\n );\n\n if (packages.length === 0) return null;\n\n return (\n <section className=\"flex flex-col gap-3\" data-testid=\"plugin-harness-picker\">\n {(title || description) && (\n <div>\n {title && <h3 className=\"text-base font-semibold text-text-primary\">{title}</h3>}\n {description && <p className=\"text-sm text-text-secondary mt-1\">{description}</p>}\n </div>\n )}\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n {error}\n </div>\n )}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {packages.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalled = !!installed[pkg];\n const isInstalling = installing === pkg;\n const isRemoving = removingPkg === pkg;\n const isBusy = isInstalling || isRemoving || loading;\n const showJustInstalled = !!justInstalled[pkg];\n const err = removeError[pkg];\n return (\n <Card key={pkg} padding=\"md\" className=\"flex flex-col gap-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-shrink-0 w-9 h-9 rounded-md bg-bg-sunken flex items-center justify-center\">\n <Puzzle className=\"w-4 h-4 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start gap-2 flex-wrap\">\n {/*\n Plugin display name must wrap freely up to a\n reasonable hard cap so the column never blows up\n the modal layout. break-words allows wrapping on\n arbitrary characters when a name has no spaces;\n max-w bound corresponds to ~100 char-cells (a\n generous upper bound on real-world display names).\n */}\n <h4 className=\"font-medium text-text-primary break-words max-w-[100ch]\">\n {meta.displayName}\n </h4>\n {isInstalled && (\n <span className=\"inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-status-success-bg text-status-success-text shrink-0\">\n <Check className=\"w-3 h-3\" /> installed\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-0.5\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder mt-1 break-all\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {isInstalled && showJustInstalled ? (\n <button\n type=\"button\"\n disabled\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg text-status-success-text border border-status-success-border cursor-default\"\n data-testid={`plugin-installed-${pkg}`}\n aria-live=\"polite\"\n >\n <CheckCircle2 className=\"w-3.5 h-3.5\" />\n Installed\n </button>\n ) : isInstalled ? (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleRemove(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border border-border-default text-text-secondary hover:border-status-error-border hover:text-status-error-text transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-remove-${pkg}`}\n >\n {isRemoving ? (\n <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n ) : (\n <Trash2 className=\"w-3.5 h-3.5\" />\n )}\n Remove\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleInstall(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n aria-busy={isInstalling}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"w-3.5 h-3.5\" />\n Install\n </>\n )}\n </button>\n )}\n </div>\n {err && <p className=\"text-xs text-status-error-text\">{err}</p>}\n </Card>\n );\n })}\n </div>\n {!agentId && <p className=\"text-xs text-text-muted\">Select an agent to manage plugins.</p>}\n </section>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"PluginHarnessPicker.js","names":[],"sources":["../../../src/components/ai/PluginHarnessPicker.tsx"],"sourcesContent":["/**\n * <PluginHarnessPicker />\n *\n * Grid of installable VibeControls agent plugins with per-card\n * Install / Installed / Remove state. Driven by the existing\n * `usePluginAvailability` hook (same code path as `<PluginGate>`), so\n * the Plugins tab on the Agent details page and this picker stay in\n * sync automatically.\n *\n * The default use-case is the AI workbench: once the base\n * `@vibecontrols/vibe-plugin-ai` plugin is installed, this picker lists\n * every AI provider harness (claude, codex, copilot, gemini, …) so the\n * user can add / remove harnesses without leaving the AI page.\n *\n * The component is feature-neutral — pass any `packages` array (e.g.\n * session providers, tunnel providers) to reuse the same UI.\n */\n\nimport { useCallback, useEffect, useRef, useState } from 'react';\nimport { Check, CheckCircle2, Download, Loader2, Puzzle, Trash2 } from 'lucide-react';\nimport { useRemoveAgentPluginMutation } from '@/generated/wspace-operations';\nimport { usePluginAvailability } from '@/hooks/usePluginAvailability';\nimport { getPluginMeta } from '@/constants/pluginCatalog';\nimport { Card } from '../shared/Card';\n\nexport interface PluginHarnessPickerProps {\n /** Package names to offer. Metadata is resolved via `getPluginMeta`. */\n packages: readonly string[];\n /** Active agent id. When null, the picker renders in a disabled state. */\n agentId: string | null;\n /** Optional section heading (rendered above the grid). */\n title?: string;\n /** Optional section description. */\n description?: string;\n /** Fired after a successful install or remove so parents can refetch. */\n onChange?: () => void;\n}\n\nexport function PluginHarnessPicker({\n packages,\n agentId,\n title,\n description,\n onChange,\n}: PluginHarnessPickerProps) {\n // Plugins are agent-wide (BOFF-2612); no profile threading needed.\n const { installed, installing, install, refetch, loading, error } = usePluginAvailability(\n packages,\n agentId\n );\n const [removePlugin] = useRemoveAgentPluginMutation();\n const [removingPkg, setRemovingPkg] = useState<string | null>(null);\n const [removeError, setRemoveError] = useState<Record<string, string>>({});\n // Per-package \"just installed\" pulse — flips on for ~1.5s after a\n // successful install so the button shows a clear success affordance\n // (checkmark + \"Installed\") before the card transitions to its\n // installed/Remove state. Without this the button silently flips from\n // spinner to Remove and users miss the success signal.\n const [justInstalled, setJustInstalled] = useState<Record<string, boolean>>({});\n const justInstalledTimers = useRef<Map<string, ReturnType<typeof setTimeout>>>(new Map());\n\n useEffect(() => {\n const timers = justInstalledTimers.current;\n return () => {\n for (const t of timers.values()) clearTimeout(t);\n timers.clear();\n };\n }, []);\n\n const handleInstall = useCallback(\n async (pkg: string) => {\n const ok = await install(pkg);\n if (ok) {\n setJustInstalled((prev) => ({ ...prev, [pkg]: true }));\n const existing = justInstalledTimers.current.get(pkg);\n if (existing) clearTimeout(existing);\n const timer = setTimeout(() => {\n setJustInstalled((prev) => {\n if (!prev[pkg]) return prev;\n const next = { ...prev };\n delete next[pkg];\n return next;\n });\n justInstalledTimers.current.delete(pkg);\n }, 1500);\n justInstalledTimers.current.set(pkg, timer);\n onChange?.();\n }\n },\n [install, onChange]\n );\n\n const handleRemove = useCallback(\n async (pkg: string) => {\n if (!agentId) return;\n setRemovingPkg(pkg);\n setRemoveError((e) => ({ ...e, [pkg]: '' }));\n try {\n const { data } = await removePlugin({\n variables: { agentId, packageName: pkg },\n });\n if (!data?.removeAgentPlugin.success) {\n setRemoveError((e) => ({\n ...e,\n [pkg]: data?.removeAgentPlugin.error || 'Remove failed',\n }));\n return;\n }\n await refetch();\n onChange?.();\n } catch (e) {\n setRemoveError((err) => ({\n ...err,\n [pkg]: e instanceof Error ? e.message : String(e),\n }));\n } finally {\n setRemovingPkg(null);\n }\n },\n [agentId, removePlugin, refetch, onChange]\n );\n\n if (packages.length === 0) return null;\n\n return (\n <section className=\"flex flex-col gap-3\" data-testid=\"plugin-harness-picker\">\n {(title || description) && (\n <div>\n {title && <h3 className=\"text-base font-semibold text-text-primary\">{title}</h3>}\n {description && <p className=\"text-sm text-text-secondary mt-1\">{description}</p>}\n </div>\n )}\n {error && (\n <div\n role=\"alert\"\n className=\"rounded-md border border-status-error-border bg-status-error-bg px-3 py-2 text-xs text-status-error-text\"\n >\n {error}\n </div>\n )}\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {packages.map((pkg) => {\n const meta = getPluginMeta(pkg);\n const isInstalled = !!installed[pkg];\n const isInstalling = installing === pkg;\n const isRemoving = removingPkg === pkg;\n const isBusy = isInstalling || isRemoving || loading;\n const showJustInstalled = !!justInstalled[pkg];\n const err = removeError[pkg];\n return (\n <Card key={pkg} padding=\"md\" className=\"flex flex-col gap-3\">\n <div className=\"flex items-start gap-3\">\n <div className=\"flex-shrink-0 w-9 h-9 rounded-md bg-bg-sunken flex items-center justify-center\">\n <Puzzle className=\"w-4 h-4 text-text-secondary\" />\n </div>\n <div className=\"min-w-0 flex-1\">\n <div className=\"flex items-start gap-2 flex-wrap\">\n {/*\n Plugin display name must wrap freely up to a\n reasonable hard cap so the column never blows up\n the modal layout. break-words allows wrapping on\n arbitrary characters when a name has no spaces;\n max-w bound corresponds to ~100 char-cells (a\n generous upper bound on real-world display names).\n */}\n <h4 className=\"font-medium text-text-primary break-words max-w-[100ch]\">\n {meta.displayName}\n </h4>\n {isInstalled && (\n <span className=\"inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded bg-status-success-bg text-status-success-text shrink-0\">\n <Check className=\"w-3 h-3\" /> installed\n </span>\n )}\n </div>\n <p className=\"text-sm text-text-secondary mt-0.5\">{meta.description}</p>\n <p\n className=\"text-[10px] font-mono text-text-placeholder mt-1 break-all\"\n title={pkg}\n >\n {pkg}\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {isInstalled && showJustInstalled ? (\n <button\n type=\"button\"\n disabled\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-status-success-bg text-status-success-text border border-status-success-border cursor-default\"\n data-testid={`plugin-installed-${pkg}`}\n aria-live=\"polite\"\n >\n <CheckCircle2 className=\"w-3.5 h-3.5\" />\n Installed\n </button>\n ) : isInstalled ? (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleRemove(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md border border-border-default text-text-secondary hover:border-status-error-border hover:text-status-error-text transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-remove-${pkg}`}\n >\n {isRemoving ? (\n <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n ) : (\n <Trash2 className=\"w-3.5 h-3.5\" />\n )}\n Remove\n </button>\n ) : (\n <button\n type=\"button\"\n disabled={isBusy || !agentId}\n onClick={() => handleInstall(pkg)}\n className=\"inline-flex items-center gap-1.5 px-3 py-1.5 text-xs rounded-md bg-action-primary-bg text-action-primary-text hover:bg-action-primary-bg-hover transition disabled:opacity-60 disabled:cursor-not-allowed\"\n data-testid={`plugin-install-${pkg}`}\n aria-busy={isInstalling}\n >\n {isInstalling ? (\n <>\n <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n Installing…\n </>\n ) : (\n <>\n <Download className=\"w-3.5 h-3.5\" />\n Install\n </>\n )}\n </button>\n )}\n </div>\n {err && <p className=\"text-xs text-status-error-text\">{err}</p>}\n </Card>\n );\n })}\n </div>\n {!agentId && <p className=\"text-xs text-text-muted\">Select an agent to manage plugins.</p>}\n </section>\n );\n}\n"],"mappings":"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"}