/** * Agents Module - Interactive Agent Management * @module modules/agents * * Managed Agents pattern: card grid list → detail view with 6 tabs * (Config, Persona, Tools, Activity, Validation, History). * SmartStore pattern: reportPageContext for agent awareness. */ /* eslint-env browser */ import { API, type MultiAgentAgent } from '../utils/api.js'; import { DebugLogger } from '../utils/debug-logger.js'; import { showToast, escapeAttr, escapeHtml } from '../utils/dom.js'; import { reportPageContext } from '../utils/ui-commands.js'; const logger = new DebugLogger('Agents'); const DEFAULT_VALIDATION_TRIGGER = 'agent_test' as const; const C = { pri: '#1A1A1A', sec: '#6B6560', ter: '#9E9891', bdr: '#EDE9E1', bg: '#FAFAF8', agent: '#8b5cf6', green: '#3A9E7E', red: '#D94F4F', yellow: '#FFCE00', } as const; type AgentWithVersion = MultiAgentAgent & { system?: string; version?: number }; type DetailTab = 'config' | 'persona' | 'tools' | 'activity' | 'validation' | 'history'; const LEGACY_SWARM_AGENT_IDS = new Set(['developer', 'reviewer', 'architect', 'pm']); const SYSTEM_AGENT_IDS = new Set([ 'os-agent', 'conductor', 'memory', 'dashboard-agent', 'wiki-agent', ]); const CLAUDE_MODEL_OPTIONS = ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001']; const CODEX_MODEL_OPTIONS = ['gpt-5.3-codex', 'gpt-5.4-mini']; const GEMINI_MODEL_OPTIONS = ['gemini-2.5-pro', 'gemini-2.5-flash']; function getModelsForBackend(backend: string): string[] { if (backend === 'codex-mcp' || backend === 'codex') { return CODEX_MODEL_OPTIONS; } if (backend === 'gemini') { return GEMINI_MODEL_OPTIONS; } return CLAUDE_MODEL_OPTIONS; } export class AgentsModule { private container: HTMLElement | null = null; private initialized = false; private agents: AgentWithVersion[] = []; private selectedAgent: AgentWithVersion | null = null; private activeTab: DetailTab = 'config'; private detailRequestId = 0; private listRequestId = 0; private currentDetailContext: Record | null = null; init(): void { if (this.initialized) { return; } this.initialized = true; this.container = document.getElementById('agents-content'); if (!this.container) { return; } } // ── List View ─────────────────────────────────────────────────────────── private alerts: string[] = []; private validationStates: Map = new Map(); private buildListPageContext(): Record { return { pageType: 'agent-list', total: this.agents.length, agents: this.agents.map((a) => ({ id: a.id, name: a.display_name || a.name, enabled: a.enabled !== false, tier: a.tier, model: a.model, validation: this.validationStates.get(a.id ?? '') ?? null, system: SYSTEM_AGENT_IDS.has(a.id ?? ''), })), alerts: this.alerts, summary: `${this.agents.length} agents: ${this.agents.map((a) => `${a.display_name || a.id}(${this.validationStates.get(a.id ?? '') ?? 'no-data'})`).join(', ')}`, }; } private buildDetailValidationContext( validationSummary: Record | null ): Record | null { if (!validationSummary) { return null; } return { outcome: validationSummary.validation_outcome, execution: validationSummary.execution_status, baseline_version: validationSummary.baseline_version, trigger_type: validationSummary.trigger_type, ended_at: validationSummary.ended_at, }; } private buildDetailPageContext( agent: AgentWithVersion, validationSummary: Record | null ): Record { const validation = this.buildDetailValidationContext(validationSummary); const validationOutcome = typeof validation?.outcome === 'string' ? validation.outcome : 'none'; return { pageType: 'agent-detail', selectedAgent: agent.id, activeTab: this.activeTab, agent: { id: agent.id, name: agent.display_name || agent.name, model: agent.model, tier: agent.tier, enabled: agent.enabled !== false, version: agent.version, backend: agent.backend, }, validation, summary: `${agent.display_name || agent.name} v${agent.version ?? 0} | validation: ${validationOutcome} | tab: ${this.activeTab}`, }; } private updateDetailPageContext( agent: AgentWithVersion, patch: Record = {} ): void { const existingContext = this.currentDetailContext ?? this.buildDetailPageContext(agent, null); const nextContext: Record = { ...existingContext, ...patch, activeTab: patch.activeTab ?? this.activeTab, }; const validation = nextContext.validation && typeof nextContext.validation === 'object' && !Array.isArray(nextContext.validation) ? (nextContext.validation as Record) : null; const validationOutcome = typeof validation?.outcome === 'string' ? validation.outcome : 'none'; nextContext.summary = `${agent.display_name || agent.name} v${agent.version ?? 0} | validation: ${validationOutcome} | tab: ${String(nextContext.activeTab ?? this.activeTab)}`; this.currentDetailContext = nextContext; reportPageContext( 'agents', nextContext, agent.id ? { type: 'agent', id: agent.id } : undefined ); } private async loadAgents(): Promise { if (!this.container) { return; } const requestId = ++this.listRequestId; try { const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10); const [{ agents }, summaryRes] = await Promise.all([ API.getAgents(), API.getActivitySummary(yesterday), ]); if (requestId !== this.listRequestId) { return; } this.agents = agents.filter((agent) => { const id = agent.id ?? ''; return !(LEGACY_SWARM_AGENT_IDS.has(id) && agent.enabled === false); }); this.alerts = summaryRes.alerts; this.validationStates.clear(); this.renderList(); void Promise.all( this.agents.map((a) => API.getValidationSummary(a.id ?? '', DEFAULT_VALIDATION_TRIGGER).catch(() => ({ summary: null, })) ) ).then((valResults) => { if (requestId !== this.listRequestId) { return; } this.validationStates.clear(); for (let i = 0; i < this.agents.length; i++) { const vs = valResults[i]?.summary as Record | null; if (vs?.validation_outcome) { this.validationStates.set(this.agents[i].id ?? '', String(vs.validation_outcome)); } } this.renderList(); reportPageContext('agents', this.buildListPageContext()); }); } catch (err) { const message = err instanceof Error ? err.message : String(err); const wrapped = new Error(`Failed fetching agents or activity summary: ${message}`); logger.error(wrapped.message, err); throw wrapped; } } private static relativeTime(dateStr: string): string { const diff = Date.now() - new Date(dateStr).getTime(); if (diff < 60000) { return 'just now'; } if (diff < 3600000) { return `${Math.floor(diff / 60000)}m ago`; } if (diff < 86400000) { return `${Math.floor(diff / 3600000)}h ago`; } return `${Math.floor(diff / 86400000)}d ago`; } private renderList(): void { if (!this.container) { return; } const cards = this.agents .map((a) => { const lastAct = (a as unknown as Record).last_activity as | Record | null | undefined; // Status badge: disabled > error > active > idle let badgeColor: string; let badgeText: string; if (a.enabled === false) { badgeColor = C.ter; badgeText = 'Disabled'; } else if (lastAct?.type === 'task_error') { badgeColor = C.red; badgeText = 'Error'; } else if ( lastAct?.created_at && Date.now() - new Date(String(lastAct.created_at)).getTime() < 300000 ) { badgeColor = C.green; badgeText = 'Active'; } else { badgeColor = '#EAB308'; badgeText = 'Idle'; } const lastRunStr = lastAct?.created_at ? AgentsModule.relativeTime(String(lastAct.created_at)) : ''; return `
${escapeHtml(a.display_name || a.name || a.id || '')}
T${a.tier ?? 1} ${SYSTEM_AGENT_IDS.has(a.id ?? '') ? `system` : ''}
${escapeHtml(a.model || 'No model')}
\u25CF ${badgeText}${lastRunStr ? ` \u00B7 ${lastRunStr}` : ''} ${(() => { const vo = this.validationStates.get(a.id ?? ''); if (!vo) { return ''; } const vc: Record = { healthy: '#22c55e', improved: '#3b82f6', regressed: '#ef4444', inconclusive: '#f59e0b', }; return `${vo}`; })()}
`; }) .join(''); const alertBanner = this.alerts.length > 0 ? `
\u26A0 ${this.alerts.length} agent(s) need attention: ${escapeHtml(this.alerts.slice(0, 3).join(', '))}
` : ''; this.container.innerHTML = `

Agents

${alertBanner}
${cards}
`; // Enable toggle — stop propagation so card click doesn't fire this.container.querySelectorAll('[data-toggle-id]').forEach((toggle) => { toggle.addEventListener('click', (e) => e.stopPropagation()); toggle.addEventListener('change', async () => { const agentId = toggle.dataset.toggleId; if (!agentId) { return; } const agent = this.agents.find((item) => item.id === agentId); const version = agent?.version; if (version === null || version === undefined) { showToast('Version unavailable'); toggle.checked = !toggle.checked; return; } try { await API.updateAgent(agentId, { version, changes: { enabled: toggle.checked }, change_note: toggle.checked ? 'Enabled via Agents tab' : 'Disabled via Agents tab', }); showToast(`${agentId} ${toggle.checked ? 'enabled' : 'disabled'}`); void this.loadAgents().catch((error) => { logger.error('Failed to refresh agents after toggle', error); showToast('Failed to refresh agent list'); }); } catch { showToast('Toggle failed'); toggle.checked = !toggle.checked; } }); }); this.container.querySelectorAll('.agent-toggle-label').forEach((label) => { label.addEventListener('click', (event) => event.stopPropagation()); }); this.container.querySelectorAll('.agent-card').forEach((card) => { const openCard = (event?: Event) => { const target = event?.target; if (target instanceof Element && target.closest('.agent-toggle-label')) { return; } const agentId = (card as HTMLElement).dataset.agentId; if (agentId) { this.showDetail(agentId); } }; card.addEventListener('click', (event) => openCard(event)); card.addEventListener('keydown', (event: KeyboardEvent) => { if (event.target !== card) { return; } if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openCard(event); } }); }); this.container .querySelector('#btn-create-agent') ?.addEventListener('click', () => this.showCreateModal()); } // ── Detail View ───────────────────────────────────────────────────────── private async showDetail(agentId: string, desiredTab?: DetailTab): Promise { const requestId = ++this.detailRequestId; try { const agent = await API.getAgent(agentId); if (requestId !== this.detailRequestId) { return; } this.selectedAgent = agent; this.activeTab = desiredTab ?? 'config'; this.renderDetail(); // Fetch validation to include in page context const valData = await API.getValidationSummary(agentId, DEFAULT_VALIDATION_TRIGGER).catch( () => ({ summary: null }) ); if (requestId !== this.detailRequestId || this.selectedAgent?.id !== agentId) { return; } const vs = valData.summary as Record | null; this.currentDetailContext = this.buildDetailPageContext(agent, vs); reportPageContext('agents', this.currentDetailContext, { type: 'agent', id: agentId }); } catch (err) { logger.error(`Failed to load agent ${agentId}`, err); showToast('Failed to load agent details'); } } private renderDetail(): void { if (!this.container || !this.selectedAgent) { return; } const a = this.selectedAgent; const tabs: DetailTab[] = ['config', 'persona', 'tools', 'activity', 'validation', 'history']; const tabBar = tabs .map( (t) => `` ) .join(''); this.container.innerHTML = `
${escapeHtml(a.display_name || a.name || a.id || '')} v${a.version ?? 0}
${tabBar}
`; this.container.querySelector('#btn-back')?.addEventListener('click', () => this.showList()); this.container.querySelectorAll('.detail-tab').forEach((btn) => { btn.addEventListener('click', () => { this.activeTab = (btn as HTMLElement).dataset.dtab as DetailTab; this.renderDetail(); const vo = this.validationStates.get(a.id ?? ''); const existingContext = this.currentDetailContext ?? this.buildDetailPageContext(a, null); const existingValidation = existingContext.validation && typeof existingContext.validation === 'object' && !Array.isArray(existingContext.validation) ? (existingContext.validation as Record) : null; const nextValidation = existingValidation || vo ? { ...(existingValidation ?? {}), ...(vo ? { outcome: vo } : {}), } : null; this.updateDetailPageContext(a, { activeTab: this.activeTab, validation: nextValidation, }); }); }); const content = this.container.querySelector('#detail-content') as HTMLElement; if (!content) { return; } switch (this.activeTab) { case 'config': this.renderConfigTab(content, a); break; case 'persona': this.renderPersonaTab(content, a); break; case 'tools': this.renderToolsTab(content, a); break; case 'activity': void this.renderActivityTab(content, a); break; case 'validation': void this.renderValidationTab(content, a); break; case 'history': this.renderHistoryTab(content, a); break; } } private renderConfigTab(el: HTMLElement, a: AgentWithVersion): void { const backend = String(a.backend || 'claude'); const modelOptions = getModelsForBackend(backend) .map( (m) => `` ) .join(''); const tierOptions = [1, 2, 3] .map((t) => ``) .join(''); const backendOptions = Array.from(new Set(['claude', 'codex', 'codex-mcp', 'gemini', backend])) .map( (b) => `` ) .join(''); el.innerHTML = `
${escapeHtml(a.id ?? '')}
`; // Backend change → update model options el.querySelector('#cfg-backend')?.addEventListener('change', () => { const newBackend = (el.querySelector('#cfg-backend') as HTMLSelectElement).value; const models = getModelsForBackend(newBackend); const modelSelect = el.querySelector('#cfg-model') as HTMLSelectElement; modelSelect.innerHTML = models .map((m) => ``) .join(''); }); // Save via managed-agent API so config sync + version history happen together el.querySelector('#btn-save-config')?.addEventListener('click', async () => { if (!a.id) { return; } const displayName = (el.querySelector('#cfg-name') as HTMLInputElement).value.trim(); if (!displayName) { showToast('Name is required'); return; } const changes = { name: displayName, display_name: displayName, model: (el.querySelector('#cfg-model') as HTMLSelectElement).value, backend: (el.querySelector('#cfg-backend') as HTMLSelectElement).value, tier: parseInt((el.querySelector('#cfg-tier') as HTMLSelectElement).value, 10), enabled: (el.querySelector('#cfg-enabled') as HTMLInputElement).checked, can_delegate: (el.querySelector('#cfg-delegate') as HTMLInputElement).checked, }; const version = a.version; if (version === null || version === undefined) { showToast('Version unavailable'); return; } try { await API.updateAgent(a.id, { version, changes, change_note: 'Config updated via Agents tab', }); showToast('Saved — hot reloaded'); this.showDetail(a.id); } catch { showToast('Save failed'); } }); } private renderPersonaTab(el: HTMLElement, a: AgentWithVersion): void { const text = (a as { system?: string }).system || '(No persona loaded)'; el.innerHTML = `
`; el.querySelector('#btn-save-persona')?.addEventListener('click', async () => { const textarea = el.querySelector('#persona-editor') as HTMLTextAreaElement; if (!textarea || !a.id) { return; } try { const updatePayload: { version?: number; changes: Record; change_note: string; } = { changes: { system: textarea.value }, change_note: 'Persona updated via viewer', }; if (a.version !== null && a.version !== undefined) { updatePayload.version = a.version; } const res = await API.updateAgent(a.id, updatePayload); if ((res as { new_version?: number }).new_version) { showToast(`v${(res as { new_version: number }).new_version} saved`); this.showDetail(a.id); } } catch (err) { showToast('Save failed'); logger.error('Persona save failed', err); } }); } private renderToolsTab(el: HTMLElement, a: AgentWithVersion): void { const allTools = [ 'Bash', 'Read', 'Edit', 'Write', 'Glob', 'Grep', 'WebFetch', 'WebSearch', 'NotebookEdit', ]; const allowed = a.tool_permissions?.allowed ?? []; const isAll = allowed.includes('*'); const rows = allTools .map((t) => { const checked = isAll || allowed.includes(t); return ``; }) .join(''); el.innerHTML = `
Tier ${a.tier ?? 1} preset. Toggle tools and save.
${rows}
`; el.querySelector('#btn-save-tools')?.addEventListener('click', async () => { const checked: string[] = []; el.querySelectorAll('input[data-tool]').forEach((cb) => { if (cb.checked) { checked.push(cb.dataset.tool!); } }); if (!a.id) { return; } const preserveWildcard = (isAll || (a.tier ?? 1) === 1) && checked.length === allTools.length; const normalizedAllowed = preserveWildcard ? ['*'] : checked; const existingBlocked = Array.isArray(a.tool_permissions?.blocked) ? a.tool_permissions.blocked : []; const toolPermissions = { allowed: normalizedAllowed, blocked: existingBlocked, }; const version = a.version; if (version === null || version === undefined) { showToast('Version unavailable'); return; } try { await API.updateAgent(a.id, { version, changes: { tool_permissions: toolPermissions }, change_note: preserveWildcard ? 'Tools: full access' : `Tools: ${normalizedAllowed.join(', ')}`, }); showToast('Tools saved - hot reloaded'); this.showDetail(a.id); } catch { showToast('Save failed'); } }); } private async renderActivityTab(el: HTMLElement, a: AgentWithVersion): Promise { el.innerHTML = '
Loading...
'; const requestId = this.detailRequestId; const expectedAgentId = this.selectedAgent?.id ?? a.id ?? ''; try { const { activity } = await API.getAgentActivity(a.id ?? '', 20); if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } if (!activity.length) { if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } this.updateDetailPageContext(a, { activity: [] }); el.innerHTML = '
No activity yet. Delegate a task to this agent to see logs here.
'; return; } const rows = activity .map((ev: Record) => { const typeIcons: Record = { test_run: '🧪', task_error: '❌', config_change: '⚙️', task_start: '▶️', }; const icon = typeIcons[String(ev.type)] || '✅'; const scoreStr = ev.score !== null && ev.score !== undefined ? ` — ${ev.score}/100` : ''; const summary = escapeHtml(String(ev.output_summary || ev.input_summary || ev.type)); const errorHtml = ev.error_message ? `
${escapeHtml(String(ev.error_message))}
` : ''; const meta = `
v${escapeHtml(String(ev.agent_version ?? ''))} · ${escapeHtml(String(ev.duration_ms ?? 0))}ms · ${escapeHtml(String(ev.created_at ?? ''))}
`; // Expandable card for test_run with per-item pass/fail if (ev.type === 'test_run' && ev.details) { let details: Record | null = null; try { details = typeof ev.details === 'string' ? (JSON.parse(ev.details) as Record) : (ev.details as Record); } catch { /* ignore parse errors */ } const items = (details?.items as Array>) ?? []; const itemsHtml = items .map((item) => { const badge = item.result === 'pass' ? 'PASS' : 'FAIL'; return `
${badge}${escapeHtml(String(item.input || ''))}
`; }) .join(''); return `
`; } return `
${icon}
${summary}${scoreStr}
${errorHtml} ${meta}
`; }) .join(''); if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } el.innerHTML = `
${rows}
`; const activityContext = activity.slice(0, 20).map((ev: Record) => ({ id: ev.id ?? null, type: ev.type ?? null, input_summary: ev.input_summary ?? null, output_summary: ev.output_summary ?? null, execution_status: ev.execution_status ?? null, duration_ms: ev.duration_ms ?? null, tokens_used: ev.tokens_used ?? null, score: ev.score ?? null, created_at: ev.created_at ?? null, error_message: ev.error_message ?? null, })); if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } this.updateDetailPageContext(a, { activity: activityContext }); // Expand/collapse toggle with ARIA if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } el.querySelectorAll('[data-expand]').forEach((toggle) => { const toggleExpand = () => { const id = toggle.dataset.expand; const content = el.querySelector(`#expand-${id}`); if (content) { const isHidden = content.classList.toggle('hidden'); toggle.setAttribute('aria-expanded', String(!isHidden)); } }; toggle.addEventListener('click', toggleExpand); toggle.addEventListener('keydown', (event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); toggleExpand(); } }); }); } catch { el.innerHTML = '
Failed to load activity.
'; } } // ── Validation Tab ───────────────────────────────────────────────────── private async renderValidationTab(el: HTMLElement, a: AgentWithVersion): Promise { el.innerHTML = `
Loading validation...
`; const agentId = a.id ?? ''; const requestId = this.detailRequestId; const expectedAgentId = this.selectedAgent?.id ?? agentId; const OC: Record = { healthy: '#22c55e', improved: '#3b82f6', regressed: '#ef4444', inconclusive: '#f59e0b', }; try { const [summaryRes, historyRes] = await Promise.all([ API.getValidationSummary(agentId, DEFAULT_VALIDATION_TRIGGER), API.getValidationHistory(agentId, 30, DEFAULT_VALIDATION_TRIGGER), ]); if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } const summary = summaryRes.summary as Record | null; const history = historyRes.history as Array>; if (!summary && history.length === 0) { el.innerHTML = `
No validation sessions yet.
Run agent_test("${escapeHtml(agentId)}") to create the first session.
`; return; } const latestId = String(summary?.id ?? ''); const outcome = String(summary?.validation_outcome ?? 'none'); const execStatus = String(summary?.execution_status ?? '—'); const outcomeColor = OC[outcome] ?? C.ter; const baselineVer = summary?.baseline_version !== null && summary?.baseline_version !== undefined ? `v${summary.baseline_version}` : 'none'; const endedAt = summary?.ended_at ? new Date(Number(summary.ended_at)).toLocaleString() : '—'; // ── 1. Fetch session detail with metrics + compare against baseline ── let metricsHtml = ''; let compareData: { deltas: Array<{ name: string; current: number; baseline: number | null; delta: number | null; direction: string; }>; } | null = null; if (latestId) { try { compareData = await API.getValidationCompare(agentId, latestId, 'approved'); } catch { /* no compare data available */ } } if (compareData && compareData.deltas && compareData.deltas.length > 0) { const metricRows = compareData.deltas .map((d) => { const hasBaseline = d.baseline !== null && d.delta !== null; const isGood = d.direction === 'down_good' ? (d.delta ?? 0) < 0 : (d.delta ?? 0) > 0; const deltaColor = !hasBaseline ? C.ter : isGood ? '#22c55e' : '#ef4444'; const deltaSign = (d.delta ?? 0) > 0 ? '+' : ''; const pct = hasBaseline && d.baseline ? Math.round(((d.delta ?? 0) / d.baseline) * 100) : null; const pctStr = pct !== null ? ` (${pct > 0 ? '+' : ''}${pct}%)` : ''; const baseStr = hasBaseline ? String(d.baseline) : '—'; const arrow = hasBaseline ? ' → ' : ''; return ` ${escapeHtml(d.name)} ${hasBaseline ? `${baseStr}${arrow}` : ''}${d.current} ${hasBaseline ? `${deltaSign}${d.delta}${pctStr}` : 'no baseline'} ${d.direction === 'down_good' ? '↓ lower better' : d.direction === 'up_good' ? '↑ higher better' : '—'} `; }) .join(''); metricsHtml = `
Metrics vs Baseline
${metricRows}
Metric Value Delta Direction
`; } // ── 2. Report from Conductor ── let reportHtml = ''; let reportContext: Record | null = null; if (summary?.report_json) { try { const report = JSON.parse(String(summary.report_json)); const reportLines = Array.isArray(report.lines) ? report.lines.map((line: unknown) => String(line)) : []; const reportHeadline = String(report.headline ?? report.outcome ?? reportLines[0] ?? ''); const reportDetails = String( report.details ?? (reportLines.length > 0 ? reportLines.join('\n') : '') ); reportContext = { headline: reportHeadline, details: reportDetails, outcome: String(report.outcome ?? outcome), }; reportHtml = `
Validation Report
${escapeHtml(reportHeadline)}
${escapeHtml(reportDetails)}
`; } catch { /* ignore */ } } // ── 3. History — version-by-version performance table ── let historyHtml = ''; if (history.length > 0) { // Fetch metrics for each session to show performance numbers type HistSession = Record & { _metrics: Array> }; const sessionDetails: HistSession[] = await Promise.all( history.slice(0, 10).map(async (h) => { try { const detail = await API.getValidationSessionDetail(String(h.id)); return { ...h, _metrics: detail.metrics } as HistSession; } catch { return { ...h, _metrics: [] } as HistSession; } }) ); const hRows = sessionDetails .map((h) => { const hOutcome = String(h.validation_outcome ?? '—'); const hColor = OC[hOutcome] ?? C.ter; const hTime = h.ended_at ? new Date(Number(h.ended_at)).toLocaleString() : 'running...'; const metrics = h._metrics as Array<{ name: string; value: number; delta_value: number | null; direction: string; }>; // Extract key metrics for display const durMetric = metrics.find( (m) => m.name === 'duration_ms' || m.name === 'publish_latency_ms' ); const tokenMetric = metrics.find((m) => m.name === 'token_cost'); const scoreMetric = metrics.find( (m) => m.name === 'auto_score' || m.name === 'completion_rate' ); const fmtMetric = ( m: { value: number; delta_value: number | null; direction: string } | undefined, unit: string ) => { if (!m) { return ``; } const val = unit === 'ms' ? `${(m.value / 1000).toFixed(1)}s` : unit === '%' ? `${Math.round(m.value * 100)}%` : String(Math.round(m.value)); if (m.delta_value === null) { return `${val}`; } const isGood = m.direction === 'down_good' ? m.delta_value < 0 : m.delta_value > 0; const dColor = isGood ? '#22c55e' : '#ef4444'; const sign = m.delta_value > 0 ? '+' : ''; const dVal = unit === 'ms' ? `${sign}${(m.delta_value / 1000).toFixed(1)}s` : `${sign}${Math.round(m.delta_value)}`; return `${val} ${dVal}`; }; const cmpBase = (compareData as unknown as Record | null)?.baseline as | { session?: { id?: string } } | null | undefined; const isApproved = String(summary?.baseline_session_id ?? '') === String(h.id) || cmpBase?.session?.id === String(h.id); return ` v${h.agent_version ?? '?'}${isApproved ? ` baseline` : ''} ${escapeHtml(hOutcome)} ${escapeHtml(String(h.trigger_type ?? '—'))} ${fmtMetric(durMetric, 'ms')} ${fmtMetric(tokenMetric, '')} ${fmtMetric(scoreMetric, '%')} ${escapeHtml(hTime)} `; }) .join(''); historyHtml = `
Version Performance History
${hRows}
Version Outcome Trigger Latency Tokens Score Time
`; } const validationContext = { ...this.buildDetailValidationContext(summary), latest_session_id: latestId || null, metrics: compareData?.deltas ?? [], report: reportContext, history: history.slice(0, 10).map((h) => ({ id: h.id ?? null, agent_version: h.agent_version ?? null, validation_outcome: h.validation_outcome ?? null, execution_status: h.execution_status ?? null, trigger_type: h.trigger_type ?? null, ended_at: h.ended_at ?? null, baseline_version: h.baseline_version ?? null, })), }; if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } this.updateDetailPageContext(a, { validation: validationContext }); // ── 4. Approve button ── const canApprove = outcome === 'improved' || outcome === 'healthy'; const sessionVersion = Number(summary?.agent_version ?? a.version ?? 0); const approveBtn = canApprove ? `` : ''; el.innerHTML = `
Validation Outcome
${escapeHtml(outcome)}
Execution
${escapeHtml(execStatus)}
Approved Baseline
${escapeHtml(baselineVer)}
Last Validated
${escapeHtml(endedAt)}
${metricsHtml} ${reportHtml} ${historyHtml} ${approveBtn} `; // Approve handler const approveEl = el.querySelector('#btn-approve-validation'); if (approveEl && latestId) { approveEl.addEventListener('click', async () => { try { await API.approveValidationSession(agentId, latestId); const refreshedSummary = await API.getValidationSummary( agentId, DEFAULT_VALIDATION_TRIGGER ).catch(() => ({ summary: null, })); const refreshedValidation = refreshedSummary.summary as Record | null; if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) { return; } this.currentDetailContext = this.buildDetailPageContext(a, refreshedValidation); if (refreshedValidation?.validation_outcome) { this.validationStates.set(agentId, String(refreshedValidation.validation_outcome)); } reportPageContext( 'agents', this.currentDetailContext, a.id ? { type: 'agent', id: a.id } : undefined ); void this.renderValidationTab(el, a); showToast('Approved as baseline'); } catch { showToast('Approval failed'); } }); } } catch (err) { el.innerHTML = `
Failed to load validation data: ${escapeHtml(String(err))}
`; } } private async renderHistoryTab(el: HTMLElement, a: AgentWithVersion): Promise { el.innerHTML = `
Loading versions...
`; const requestId = this.detailRequestId; const expectedAgentId = a.id ?? ''; try { const { versions } = await API.getAgentVersions(a.id ?? ''); if (this.detailRequestId !== requestId || this.selectedAgent?.id !== expectedAgentId) { return; } if (!versions.length) { el.innerHTML = `
No version history.
`; return; } const rows = (versions as Array>) .map( (v) => `
v${v.version} ${escapeHtml(String(v.created_at ?? ''))} ${escapeHtml(String(v.change_note || ''))} ${v.version === a.version ? `current` : ''}
` ) .join(''); if (this.detailRequestId !== requestId || this.selectedAgent?.id !== expectedAgentId) { return; } el.innerHTML = `
${rows}
`; } catch { if (this.detailRequestId !== requestId || this.selectedAgent?.id !== expectedAgentId) { return; } el.innerHTML = `
Failed to load versions.
`; } } // ── Create Modal ──────────────────────────────────────────────────────── private showCreateModal(): void { if (!this.container) { return; } const overlay = document.createElement('div'); overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:100;display:flex;align-items:center;justify-content:center;'; overlay.setAttribute('role', 'dialog'); overlay.setAttribute('aria-modal', 'true'); overlay.setAttribute('aria-label', 'Create new agent'); overlay.innerHTML = `

New Agent

`; document.body.appendChild(overlay); overlay.querySelector('#btn-cancel')?.addEventListener('click', () => overlay.remove()); overlay.addEventListener('click', (e) => { if (e.target === overlay) { overlay.remove(); } }); overlay.querySelector('#btn-create')?.addEventListener('click', async () => { const id = (overlay.querySelector('#new-id') as HTMLInputElement).value.trim(); const name = (overlay.querySelector('#new-name') as HTMLInputElement).value.trim(); const model = (overlay.querySelector('#new-model') as HTMLInputElement).value.trim(); const tier = parseInt((overlay.querySelector('#new-tier') as HTMLSelectElement).value, 10); if (!id || !name) { showToast('ID and Name are required'); return; } try { await API.createAgent({ id, name, model, tier }); overlay.remove(); showToast(`Agent '${name}' created`); await this.showDetail(id); } catch (err) { showToast('Create failed'); logger.error('Create agent failed', err); } }); } // ── Navigation ────────────────────────────────────────────────────────── /** * Deep navigation from viewer_navigate command. * Opens agent detail and optionally switches to a specific tab. */ async navigateTo(agentId: string, tab?: string): Promise { const tryNav = async (): Promise => { const agent = this.agents.find((a) => a.id === agentId); if (agent) { const desiredTab = tab && ['config', 'persona', 'tools', 'activity', 'validation', 'history'].includes(tab) ? (tab as DetailTab) : undefined; await this.showDetail(agentId, desiredTab); return true; } return false; }; let navigated = await tryNav(); if (!navigated) { try { await this.loadAgents(); } catch (error) { logger.error(`Failed to load agents while navigating to ${agentId}`, error); showToast('Failed to load agents'); return; } navigated = await tryNav(); } if (!navigated) { logger.warn(`Agent not found during navigation: ${agentId}`); this.showList(); showToast('Agent not found'); } } showList(): void { this.detailRequestId++; this.selectedAgent = null; this.currentDetailContext = null; reportPageContext('agents', { ...this.buildListPageContext(), selectedAgent: null, activeTab: null, }); void this.loadAgents().catch((error) => { logger.error('Failed to load agents list', error); showToast('Failed to load agents'); }); } }