@jungjaehoon/mama-os 0.18.2 → 0.19.0

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.
Files changed (171) hide show
  1. package/dist/agent/agent-loop.d.ts +25 -0
  2. package/dist/agent/agent-loop.d.ts.map +1 -1
  3. package/dist/agent/agent-loop.js +67 -14
  4. package/dist/agent/agent-loop.js.map +1 -1
  5. package/dist/agent/code-act/host-bridge.d.ts.map +1 -1
  6. package/dist/agent/code-act/host-bridge.js +98 -0
  7. package/dist/agent/code-act/host-bridge.js.map +1 -1
  8. package/dist/agent/code-act/type-definition-generator.d.ts.map +1 -1
  9. package/dist/agent/code-act/type-definition-generator.js +0 -1
  10. package/dist/agent/code-act/type-definition-generator.js.map +1 -1
  11. package/dist/agent/gateway-tool-executor.d.ts +36 -1
  12. package/dist/agent/gateway-tool-executor.d.ts.map +1 -1
  13. package/dist/agent/gateway-tool-executor.js +938 -54
  14. package/dist/agent/gateway-tool-executor.js.map +1 -1
  15. package/dist/agent/gateway-tools.md +9 -0
  16. package/dist/agent/managed-agent-runtime-sync.d.ts +36 -0
  17. package/dist/agent/managed-agent-runtime-sync.d.ts.map +1 -0
  18. package/dist/agent/managed-agent-runtime-sync.js +207 -0
  19. package/dist/agent/managed-agent-runtime-sync.js.map +1 -0
  20. package/dist/agent/managed-agent-validation.d.ts +4 -0
  21. package/dist/agent/managed-agent-validation.d.ts.map +1 -0
  22. package/dist/agent/managed-agent-validation.js +84 -0
  23. package/dist/agent/managed-agent-validation.js.map +1 -0
  24. package/dist/agent/os-agent-capabilities.md +400 -0
  25. package/dist/agent/skill-loader.d.ts +2 -0
  26. package/dist/agent/skill-loader.d.ts.map +1 -1
  27. package/dist/agent/skill-loader.js +28 -0
  28. package/dist/agent/skill-loader.js.map +1 -1
  29. package/dist/agent/tool-registry.d.ts.map +1 -1
  30. package/dist/agent/tool-registry.js +66 -0
  31. package/dist/agent/tool-registry.js.map +1 -1
  32. package/dist/agent/types.d.ts +2 -1
  33. package/dist/agent/types.d.ts.map +1 -1
  34. package/dist/agent/types.js.map +1 -1
  35. package/dist/api/agent-handler.d.ts +34 -0
  36. package/dist/api/agent-handler.d.ts.map +1 -0
  37. package/dist/api/agent-handler.js +216 -0
  38. package/dist/api/agent-handler.js.map +1 -0
  39. package/dist/api/graph-api-types.d.ts +4 -0
  40. package/dist/api/graph-api-types.d.ts.map +1 -1
  41. package/dist/api/graph-api.d.ts +2 -2
  42. package/dist/api/graph-api.d.ts.map +1 -1
  43. package/dist/api/graph-api.js +480 -51
  44. package/dist/api/graph-api.js.map +1 -1
  45. package/dist/api/index.d.ts.map +1 -1
  46. package/dist/api/index.js +4 -0
  47. package/dist/api/index.js.map +1 -1
  48. package/dist/api/token-handler.d.ts +1 -0
  49. package/dist/api/token-handler.d.ts.map +1 -1
  50. package/dist/api/token-handler.js +4 -3
  51. package/dist/api/token-handler.js.map +1 -1
  52. package/dist/api/ui-command-handler.d.ts +48 -0
  53. package/dist/api/ui-command-handler.d.ts.map +1 -0
  54. package/dist/api/ui-command-handler.js +160 -0
  55. package/dist/api/ui-command-handler.js.map +1 -0
  56. package/dist/cli/commands/start.d.ts.map +1 -1
  57. package/dist/cli/commands/start.js +127 -1
  58. package/dist/cli/commands/start.js.map +1 -1
  59. package/dist/cli/config/config-manager.d.ts.map +1 -1
  60. package/dist/cli/config/config-manager.js +16 -31
  61. package/dist/cli/config/config-manager.js.map +1 -1
  62. package/dist/cli/runtime/agent-loop-init.d.ts.map +1 -1
  63. package/dist/cli/runtime/agent-loop-init.js +31 -7
  64. package/dist/cli/runtime/agent-loop-init.js.map +1 -1
  65. package/dist/cli/runtime/api-routes-init.d.ts +3 -0
  66. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  67. package/dist/cli/runtime/api-routes-init.js +283 -34
  68. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  69. package/dist/cli/runtime/gateway-init.d.ts +2 -1
  70. package/dist/cli/runtime/gateway-init.d.ts.map +1 -1
  71. package/dist/cli/runtime/gateway-init.js +5 -1
  72. package/dist/cli/runtime/gateway-init.js.map +1 -1
  73. package/dist/connectors/framework/raw-store.d.ts +4 -0
  74. package/dist/connectors/framework/raw-store.d.ts.map +1 -1
  75. package/dist/connectors/framework/raw-store.js +33 -10
  76. package/dist/connectors/framework/raw-store.js.map +1 -1
  77. package/dist/db/agent-store.d.ts +115 -0
  78. package/dist/db/agent-store.d.ts.map +1 -0
  79. package/dist/db/agent-store.js +248 -0
  80. package/dist/db/agent-store.js.map +1 -0
  81. package/dist/db/migrations/agent-activity-validation-columns.d.ts +3 -0
  82. package/dist/db/migrations/agent-activity-validation-columns.d.ts.map +1 -0
  83. package/dist/db/migrations/agent-activity-validation-columns.js +22 -0
  84. package/dist/db/migrations/agent-activity-validation-columns.js.map +1 -0
  85. package/dist/db/migrations/agent-metrics-response-avg.d.ts +3 -0
  86. package/dist/db/migrations/agent-metrics-response-avg.d.ts.map +1 -0
  87. package/dist/db/migrations/agent-metrics-response-avg.js +19 -0
  88. package/dist/db/migrations/agent-metrics-response-avg.js.map +1 -0
  89. package/dist/db/migrations/agent-store-tables.d.ts +3 -0
  90. package/dist/db/migrations/agent-store-tables.d.ts.map +1 -0
  91. package/dist/db/migrations/agent-store-tables.js +59 -0
  92. package/dist/db/migrations/agent-store-tables.js.map +1 -0
  93. package/dist/db/migrations/token-usage-agent-version.d.ts +3 -0
  94. package/dist/db/migrations/token-usage-agent-version.d.ts.map +1 -0
  95. package/dist/db/migrations/token-usage-agent-version.js +16 -0
  96. package/dist/db/migrations/token-usage-agent-version.js.map +1 -0
  97. package/dist/db/migrations/validation-session-tables.d.ts +3 -0
  98. package/dist/db/migrations/validation-session-tables.d.ts.map +1 -0
  99. package/dist/db/migrations/validation-session-tables.js +59 -0
  100. package/dist/db/migrations/validation-session-tables.js.map +1 -0
  101. package/dist/gateways/message-router.d.ts +10 -0
  102. package/dist/gateways/message-router.d.ts.map +1 -1
  103. package/dist/gateways/message-router.js +188 -14
  104. package/dist/gateways/message-router.js.map +1 -1
  105. package/dist/gateways/types.d.ts +1 -1
  106. package/dist/gateways/types.d.ts.map +1 -1
  107. package/dist/multi-agent/agent-process-manager.js +1 -1
  108. package/dist/multi-agent/agent-process-manager.js.map +1 -1
  109. package/dist/multi-agent/conductor-persona.d.ts +13 -0
  110. package/dist/multi-agent/conductor-persona.d.ts.map +1 -0
  111. package/dist/multi-agent/conductor-persona.js +157 -0
  112. package/dist/multi-agent/conductor-persona.js.map +1 -0
  113. package/dist/multi-agent/dashboard-agent-persona.d.ts +1 -1
  114. package/dist/multi-agent/dashboard-agent-persona.d.ts.map +1 -1
  115. package/dist/multi-agent/dashboard-agent-persona.js +7 -3
  116. package/dist/multi-agent/dashboard-agent-persona.js.map +1 -1
  117. package/dist/multi-agent/delegation-manager.d.ts +5 -0
  118. package/dist/multi-agent/delegation-manager.d.ts.map +1 -1
  119. package/dist/multi-agent/delegation-manager.js +37 -0
  120. package/dist/multi-agent/delegation-manager.js.map +1 -1
  121. package/dist/multi-agent/ultrawork.d.ts +3 -0
  122. package/dist/multi-agent/ultrawork.d.ts.map +1 -1
  123. package/dist/multi-agent/ultrawork.js +9 -0
  124. package/dist/multi-agent/ultrawork.js.map +1 -1
  125. package/dist/validation/session-service.d.ts +72 -0
  126. package/dist/validation/session-service.d.ts.map +1 -0
  127. package/dist/validation/session-service.js +298 -0
  128. package/dist/validation/session-service.js.map +1 -0
  129. package/dist/validation/store.d.ts +25 -0
  130. package/dist/validation/store.d.ts.map +1 -0
  131. package/dist/validation/store.js +200 -0
  132. package/dist/validation/store.js.map +1 -0
  133. package/dist/validation/types.d.ts +119 -0
  134. package/dist/validation/types.d.ts.map +1 -0
  135. package/dist/validation/types.js +57 -0
  136. package/dist/validation/types.js.map +1 -0
  137. package/package.json +3 -3
  138. package/public/viewer/js/modules/agents.js +1148 -0
  139. package/public/viewer/js/modules/chat.js +20 -11
  140. package/public/viewer/js/modules/connector-feed.js +35 -0
  141. package/public/viewer/js/modules/dashboard.js +49 -0
  142. package/public/viewer/js/modules/memory.js +32 -0
  143. package/public/viewer/js/modules/settings.js +34 -79
  144. package/public/viewer/js/modules/wiki.js +59 -4
  145. package/public/viewer/js/utils/api.js +70 -0
  146. package/public/viewer/js/utils/dom.js +3 -0
  147. package/public/viewer/js/utils/ui-commands.js +93 -0
  148. package/public/viewer/log-viewer.html +2 -2
  149. package/public/viewer/src/modules/agents.ts +1299 -0
  150. package/public/viewer/src/modules/chat.ts +23 -14
  151. package/public/viewer/src/modules/connector-feed.ts +35 -0
  152. package/public/viewer/src/modules/dashboard.ts +50 -0
  153. package/public/viewer/src/modules/memory.ts +31 -0
  154. package/public/viewer/src/modules/settings.ts +36 -96
  155. package/public/viewer/src/modules/wiki.ts +73 -6
  156. package/public/viewer/src/types/global.d.ts +0 -9
  157. package/public/viewer/src/utils/api.ts +156 -2
  158. package/public/viewer/src/utils/dom.ts +6 -1
  159. package/public/viewer/src/utils/ui-commands.ts +118 -0
  160. package/public/viewer/viewer.css +105 -10
  161. package/public/viewer/viewer.html +1868 -777
  162. package/scripts/generate-gateway-tools.ts +5 -1
  163. package/public/viewer/js/modules/playground.js +0 -148
  164. package/public/viewer/js/modules/skills.js +0 -451
  165. package/public/viewer/src/modules/playground.ts +0 -173
  166. package/public/viewer/src/modules/skills.ts +0 -491
  167. package/templates/playgrounds/cron-workflow-lab.html +0 -1601
  168. package/templates/playgrounds/mama-log-viewer.html +0 -1341
  169. package/templates/playgrounds/skill-lab-playground.html +0 -1625
  170. package/templates/playgrounds/wave-visualizer.html +0 -694
  171. package/templates/skills/playground.md +0 -197
@@ -0,0 +1,1148 @@
1
+ /**
2
+ * Agents Module - Interactive Agent Management
3
+ * @module modules/agents
4
+ *
5
+ * Managed Agents pattern: card grid list → detail view with 6 tabs
6
+ * (Config, Persona, Tools, Activity, Validation, History).
7
+ * SmartStore pattern: reportPageContext for agent awareness.
8
+ */
9
+ /* eslint-env browser */
10
+ import { API } from '../utils/api.js';
11
+ import { DebugLogger } from '../utils/debug-logger.js';
12
+ import { showToast, escapeAttr, escapeHtml } from '../utils/dom.js';
13
+ import { reportPageContext } from '../utils/ui-commands.js';
14
+ const logger = new DebugLogger('Agents');
15
+ const DEFAULT_VALIDATION_TRIGGER = 'agent_test';
16
+ const C = {
17
+ pri: '#1A1A1A',
18
+ sec: '#6B6560',
19
+ ter: '#9E9891',
20
+ bdr: '#EDE9E1',
21
+ bg: '#FAFAF8',
22
+ agent: '#8b5cf6',
23
+ green: '#3A9E7E',
24
+ red: '#D94F4F',
25
+ yellow: '#FFCE00',
26
+ };
27
+ const LEGACY_SWARM_AGENT_IDS = new Set(['developer', 'reviewer', 'architect', 'pm']);
28
+ const SYSTEM_AGENT_IDS = new Set([
29
+ 'os-agent',
30
+ 'conductor',
31
+ 'memory',
32
+ 'dashboard-agent',
33
+ 'wiki-agent',
34
+ ]);
35
+ const CLAUDE_MODEL_OPTIONS = ['claude-sonnet-4-6', 'claude-opus-4-6', 'claude-haiku-4-5-20251001'];
36
+ const CODEX_MODEL_OPTIONS = ['gpt-5.3-codex', 'gpt-5.4-mini'];
37
+ const GEMINI_MODEL_OPTIONS = ['gemini-2.5-pro', 'gemini-2.5-flash'];
38
+ function getModelsForBackend(backend) {
39
+ if (backend === 'codex-mcp' || backend === 'codex') {
40
+ return CODEX_MODEL_OPTIONS;
41
+ }
42
+ if (backend === 'gemini') {
43
+ return GEMINI_MODEL_OPTIONS;
44
+ }
45
+ return CLAUDE_MODEL_OPTIONS;
46
+ }
47
+ export class AgentsModule {
48
+ container = null;
49
+ initialized = false;
50
+ agents = [];
51
+ selectedAgent = null;
52
+ activeTab = 'config';
53
+ detailRequestId = 0;
54
+ listRequestId = 0;
55
+ currentDetailContext = null;
56
+ init() {
57
+ if (this.initialized) {
58
+ return;
59
+ }
60
+ this.initialized = true;
61
+ this.container = document.getElementById('agents-content');
62
+ if (!this.container) {
63
+ return;
64
+ }
65
+ }
66
+ // ── List View ───────────────────────────────────────────────────────────
67
+ alerts = [];
68
+ validationStates = new Map();
69
+ buildListPageContext() {
70
+ return {
71
+ pageType: 'agent-list',
72
+ total: this.agents.length,
73
+ agents: this.agents.map((a) => ({
74
+ id: a.id,
75
+ name: a.display_name || a.name,
76
+ enabled: a.enabled !== false,
77
+ tier: a.tier,
78
+ model: a.model,
79
+ validation: this.validationStates.get(a.id ?? '') ?? null,
80
+ system: SYSTEM_AGENT_IDS.has(a.id ?? ''),
81
+ })),
82
+ alerts: this.alerts,
83
+ summary: `${this.agents.length} agents: ${this.agents.map((a) => `${a.display_name || a.id}(${this.validationStates.get(a.id ?? '') ?? 'no-data'})`).join(', ')}`,
84
+ };
85
+ }
86
+ buildDetailValidationContext(validationSummary) {
87
+ if (!validationSummary) {
88
+ return null;
89
+ }
90
+ return {
91
+ outcome: validationSummary.validation_outcome,
92
+ execution: validationSummary.execution_status,
93
+ baseline_version: validationSummary.baseline_version,
94
+ trigger_type: validationSummary.trigger_type,
95
+ ended_at: validationSummary.ended_at,
96
+ };
97
+ }
98
+ buildDetailPageContext(agent, validationSummary) {
99
+ const validation = this.buildDetailValidationContext(validationSummary);
100
+ const validationOutcome = typeof validation?.outcome === 'string' ? validation.outcome : 'none';
101
+ return {
102
+ pageType: 'agent-detail',
103
+ selectedAgent: agent.id,
104
+ activeTab: this.activeTab,
105
+ agent: {
106
+ id: agent.id,
107
+ name: agent.display_name || agent.name,
108
+ model: agent.model,
109
+ tier: agent.tier,
110
+ enabled: agent.enabled !== false,
111
+ version: agent.version,
112
+ backend: agent.backend,
113
+ },
114
+ validation,
115
+ summary: `${agent.display_name || agent.name} v${agent.version ?? 0} | validation: ${validationOutcome} | tab: ${this.activeTab}`,
116
+ };
117
+ }
118
+ updateDetailPageContext(agent, patch = {}) {
119
+ const existingContext = this.currentDetailContext ?? this.buildDetailPageContext(agent, null);
120
+ const nextContext = {
121
+ ...existingContext,
122
+ ...patch,
123
+ activeTab: patch.activeTab ?? this.activeTab,
124
+ };
125
+ const validation = nextContext.validation &&
126
+ typeof nextContext.validation === 'object' &&
127
+ !Array.isArray(nextContext.validation)
128
+ ? nextContext.validation
129
+ : null;
130
+ const validationOutcome = typeof validation?.outcome === 'string' ? validation.outcome : 'none';
131
+ nextContext.summary = `${agent.display_name || agent.name} v${agent.version ?? 0} | validation: ${validationOutcome} | tab: ${String(nextContext.activeTab ?? this.activeTab)}`;
132
+ this.currentDetailContext = nextContext;
133
+ reportPageContext('agents', nextContext, agent.id ? { type: 'agent', id: agent.id } : undefined);
134
+ }
135
+ async loadAgents() {
136
+ if (!this.container) {
137
+ return;
138
+ }
139
+ const requestId = ++this.listRequestId;
140
+ try {
141
+ const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
142
+ const [{ agents }, summaryRes] = await Promise.all([
143
+ API.getAgents(),
144
+ API.getActivitySummary(yesterday),
145
+ ]);
146
+ if (requestId !== this.listRequestId) {
147
+ return;
148
+ }
149
+ this.agents = agents.filter((agent) => {
150
+ const id = agent.id ?? '';
151
+ return !(LEGACY_SWARM_AGENT_IDS.has(id) && agent.enabled === false);
152
+ });
153
+ this.alerts = summaryRes.alerts;
154
+ this.validationStates.clear();
155
+ this.renderList();
156
+ void Promise.all(this.agents.map((a) => API.getValidationSummary(a.id ?? '', DEFAULT_VALIDATION_TRIGGER).catch(() => ({
157
+ summary: null,
158
+ })))).then((valResults) => {
159
+ if (requestId !== this.listRequestId) {
160
+ return;
161
+ }
162
+ this.validationStates.clear();
163
+ for (let i = 0; i < this.agents.length; i++) {
164
+ const vs = valResults[i]?.summary;
165
+ if (vs?.validation_outcome) {
166
+ this.validationStates.set(this.agents[i].id ?? '', String(vs.validation_outcome));
167
+ }
168
+ }
169
+ this.renderList();
170
+ reportPageContext('agents', this.buildListPageContext());
171
+ });
172
+ }
173
+ catch (err) {
174
+ const message = err instanceof Error ? err.message : String(err);
175
+ const wrapped = new Error(`Failed fetching agents or activity summary: ${message}`);
176
+ logger.error(wrapped.message, err);
177
+ throw wrapped;
178
+ }
179
+ }
180
+ static relativeTime(dateStr) {
181
+ const diff = Date.now() - new Date(dateStr).getTime();
182
+ if (diff < 60000) {
183
+ return 'just now';
184
+ }
185
+ if (diff < 3600000) {
186
+ return `${Math.floor(diff / 60000)}m ago`;
187
+ }
188
+ if (diff < 86400000) {
189
+ return `${Math.floor(diff / 3600000)}h ago`;
190
+ }
191
+ return `${Math.floor(diff / 86400000)}d ago`;
192
+ }
193
+ renderList() {
194
+ if (!this.container) {
195
+ return;
196
+ }
197
+ const cards = this.agents
198
+ .map((a) => {
199
+ const lastAct = a.last_activity;
200
+ // Status badge: disabled > error > active > idle
201
+ let badgeColor;
202
+ let badgeText;
203
+ if (a.enabled === false) {
204
+ badgeColor = C.ter;
205
+ badgeText = 'Disabled';
206
+ }
207
+ else if (lastAct?.type === 'task_error') {
208
+ badgeColor = C.red;
209
+ badgeText = 'Error';
210
+ }
211
+ else if (lastAct?.created_at &&
212
+ Date.now() - new Date(String(lastAct.created_at)).getTime() < 300000) {
213
+ badgeColor = C.green;
214
+ badgeText = 'Active';
215
+ }
216
+ else {
217
+ badgeColor = '#EAB308';
218
+ badgeText = 'Idle';
219
+ }
220
+ const lastRunStr = lastAct?.created_at
221
+ ? AgentsModule.relativeTime(String(lastAct.created_at))
222
+ : '';
223
+ return `
224
+ <div class="agent-card" data-agent-id="${escapeHtml(a.id ?? '')}" tabindex="0" role="button"
225
+ style="background:#fff;border:1px solid ${C.bdr};border-radius:12px;padding:16px;cursor:pointer;transition:box-shadow 0.15s,transform 0.15s;">
226
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
227
+ <span style="font-size:15px;font-weight:600;color:${C.pri}">${escapeHtml(a.display_name || a.name || a.id || '')}</span>
228
+ <div style="display:flex;align-items:center;gap:6px;">
229
+ <span style="font-size:11px;font-weight:600;padding:2px 8px;border-radius:4px;background:${C.agent}15;color:${C.agent}">T${a.tier ?? 1}</span>
230
+ ${SYSTEM_AGENT_IDS.has(a.id ?? '') ? `<span style="font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;background:${C.bg};color:${C.sec};border:1px solid ${C.bdr};">system</span>` : ''}
231
+ <label class="agent-toggle-label" style="position:relative;display:inline-flex;align-items:center;cursor:pointer;" title="${a.enabled !== false ? 'Disable' : 'Enable'} agent">
232
+ <input type="checkbox" data-toggle-id="${escapeHtml(a.id ?? '')}" ${a.enabled !== false ? 'checked' : ''} style="position:absolute;opacity:0;width:0;height:0;" />
233
+ <div style="width:28px;height:16px;background:${a.enabled !== false ? C.green : '#D1D5DB'};border-radius:8px;position:relative;transition:background 0.2s;">
234
+ <div style="position:absolute;top:2px;left:${a.enabled !== false ? '14px' : '2px'};width:12px;height:12px;background:#fff;border-radius:50%;transition:left 0.2s;"></div>
235
+ </div>
236
+ </label>
237
+ </div>
238
+ </div>
239
+ <div style="font-size:12px;color:${C.sec};margin-bottom:6px;">${escapeHtml(a.model || 'No model')}</div>
240
+ <div style="display:flex;justify-content:space-between;align-items:center;">
241
+ <span style="font-size:11px;color:${badgeColor};font-weight:500;">\u25CF ${badgeText}${lastRunStr ? ` \u00B7 ${lastRunStr}` : ''}</span>
242
+ ${(() => {
243
+ const vo = this.validationStates.get(a.id ?? '');
244
+ if (!vo) {
245
+ return '';
246
+ }
247
+ const vc = {
248
+ healthy: '#22c55e',
249
+ improved: '#3b82f6',
250
+ regressed: '#ef4444',
251
+ inconclusive: '#f59e0b',
252
+ };
253
+ return `<span style="font-size:10px;font-weight:600;padding:2px 6px;border-radius:4px;background:${vc[vo] ?? C.ter}15;color:${vc[vo] ?? C.ter};">${vo}</span>`;
254
+ })()}
255
+ </div>
256
+ </div>`;
257
+ })
258
+ .join('');
259
+ const alertBanner = this.alerts.length > 0
260
+ ? `<div class="mb-3 px-3 py-2 rounded-lg bg-red-50 border border-red-200 text-[12px] text-red-700">\u26A0 ${this.alerts.length} agent(s) need attention: ${escapeHtml(this.alerts.slice(0, 3).join(', '))}</div>`
261
+ : '';
262
+ this.container.innerHTML = `
263
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;">
264
+ <h2 style="font-size:18px;font-weight:600;color:${C.pri};margin:0;">Agents</h2>
265
+ <button id="btn-create-agent"
266
+ style="font-size:12px;padding:6px 14px;border-radius:6px;border:none;background:${C.agent};color:#fff;cursor:pointer;font-weight:500;">
267
+ + New Agent
268
+ </button>
269
+ </div>
270
+ ${alertBanner}
271
+ <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:12px;">
272
+ ${cards}
273
+ </div>`;
274
+ // Enable toggle — stop propagation so card click doesn't fire
275
+ this.container.querySelectorAll('[data-toggle-id]').forEach((toggle) => {
276
+ toggle.addEventListener('click', (e) => e.stopPropagation());
277
+ toggle.addEventListener('change', async () => {
278
+ const agentId = toggle.dataset.toggleId;
279
+ if (!agentId) {
280
+ return;
281
+ }
282
+ const agent = this.agents.find((item) => item.id === agentId);
283
+ const version = agent?.version;
284
+ if (version === null || version === undefined) {
285
+ showToast('Version unavailable');
286
+ toggle.checked = !toggle.checked;
287
+ return;
288
+ }
289
+ try {
290
+ await API.updateAgent(agentId, {
291
+ version,
292
+ changes: { enabled: toggle.checked },
293
+ change_note: toggle.checked ? 'Enabled via Agents tab' : 'Disabled via Agents tab',
294
+ });
295
+ showToast(`${agentId} ${toggle.checked ? 'enabled' : 'disabled'}`);
296
+ void this.loadAgents().catch((error) => {
297
+ logger.error('Failed to refresh agents after toggle', error);
298
+ showToast('Failed to refresh agent list');
299
+ });
300
+ }
301
+ catch {
302
+ showToast('Toggle failed');
303
+ toggle.checked = !toggle.checked;
304
+ }
305
+ });
306
+ });
307
+ this.container.querySelectorAll('.agent-toggle-label').forEach((label) => {
308
+ label.addEventListener('click', (event) => event.stopPropagation());
309
+ });
310
+ this.container.querySelectorAll('.agent-card').forEach((card) => {
311
+ const openCard = (event) => {
312
+ const target = event?.target;
313
+ if (target instanceof Element && target.closest('.agent-toggle-label')) {
314
+ return;
315
+ }
316
+ const agentId = card.dataset.agentId;
317
+ if (agentId) {
318
+ this.showDetail(agentId);
319
+ }
320
+ };
321
+ card.addEventListener('click', (event) => openCard(event));
322
+ card.addEventListener('keydown', (event) => {
323
+ if (event.target !== card) {
324
+ return;
325
+ }
326
+ if (event.key === 'Enter' || event.key === ' ') {
327
+ event.preventDefault();
328
+ openCard(event);
329
+ }
330
+ });
331
+ });
332
+ this.container
333
+ .querySelector('#btn-create-agent')
334
+ ?.addEventListener('click', () => this.showCreateModal());
335
+ }
336
+ // ── Detail View ─────────────────────────────────────────────────────────
337
+ async showDetail(agentId, desiredTab) {
338
+ const requestId = ++this.detailRequestId;
339
+ try {
340
+ const agent = await API.getAgent(agentId);
341
+ if (requestId !== this.detailRequestId) {
342
+ return;
343
+ }
344
+ this.selectedAgent = agent;
345
+ this.activeTab = desiredTab ?? 'config';
346
+ this.renderDetail();
347
+ // Fetch validation to include in page context
348
+ const valData = await API.getValidationSummary(agentId, DEFAULT_VALIDATION_TRIGGER).catch(() => ({ summary: null }));
349
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== agentId) {
350
+ return;
351
+ }
352
+ const vs = valData.summary;
353
+ this.currentDetailContext = this.buildDetailPageContext(agent, vs);
354
+ reportPageContext('agents', this.currentDetailContext, { type: 'agent', id: agentId });
355
+ }
356
+ catch (err) {
357
+ logger.error(`Failed to load agent ${agentId}`, err);
358
+ showToast('Failed to load agent details');
359
+ }
360
+ }
361
+ renderDetail() {
362
+ if (!this.container || !this.selectedAgent) {
363
+ return;
364
+ }
365
+ const a = this.selectedAgent;
366
+ const tabs = ['config', 'persona', 'tools', 'activity', 'validation', 'history'];
367
+ const tabBar = tabs
368
+ .map((t) => `<button class="detail-tab" data-dtab="${t}" style="padding:6px 14px;border:none;border-bottom:2px solid ${this.activeTab === t ? C.agent : 'transparent'};background:none;cursor:pointer;font-size:12px;font-weight:${this.activeTab === t ? '600' : '400'};color:${this.activeTab === t ? C.agent : C.sec};transition:all 0.15s;">${t.charAt(0).toUpperCase() + t.slice(1)}</button>`)
369
+ .join('');
370
+ this.container.innerHTML = `
371
+ <div style="margin-bottom:16px;display:flex;align-items:center;gap:8px;">
372
+ <button id="btn-back" style="background:none;border:none;cursor:pointer;color:${C.sec};font-size:13px;">\u2190 Agents</button>
373
+ <span style="font-size:16px;font-weight:600;color:${C.pri}">${escapeHtml(a.display_name || a.name || a.id || '')}</span>
374
+ <span style="font-size:11px;color:${C.ter};background:${C.bg};padding:2px 8px;border-radius:4px;">v${a.version ?? 0}</span>
375
+ </div>
376
+ <div style="border-bottom:1px solid ${C.bdr};margin-bottom:16px;display:flex;gap:0;overflow-x:auto;-webkit-overflow-scrolling:touch;">
377
+ ${tabBar}
378
+ </div>
379
+ <div id="detail-content"></div>`;
380
+ this.container.querySelector('#btn-back')?.addEventListener('click', () => this.showList());
381
+ this.container.querySelectorAll('.detail-tab').forEach((btn) => {
382
+ btn.addEventListener('click', () => {
383
+ this.activeTab = btn.dataset.dtab;
384
+ this.renderDetail();
385
+ const vo = this.validationStates.get(a.id ?? '');
386
+ const existingContext = this.currentDetailContext ?? this.buildDetailPageContext(a, null);
387
+ const existingValidation = existingContext.validation &&
388
+ typeof existingContext.validation === 'object' &&
389
+ !Array.isArray(existingContext.validation)
390
+ ? existingContext.validation
391
+ : null;
392
+ const nextValidation = existingValidation || vo
393
+ ? {
394
+ ...(existingValidation ?? {}),
395
+ ...(vo ? { outcome: vo } : {}),
396
+ }
397
+ : null;
398
+ this.updateDetailPageContext(a, {
399
+ activeTab: this.activeTab,
400
+ validation: nextValidation,
401
+ });
402
+ });
403
+ });
404
+ const content = this.container.querySelector('#detail-content');
405
+ if (!content) {
406
+ return;
407
+ }
408
+ switch (this.activeTab) {
409
+ case 'config':
410
+ this.renderConfigTab(content, a);
411
+ break;
412
+ case 'persona':
413
+ this.renderPersonaTab(content, a);
414
+ break;
415
+ case 'tools':
416
+ this.renderToolsTab(content, a);
417
+ break;
418
+ case 'activity':
419
+ void this.renderActivityTab(content, a);
420
+ break;
421
+ case 'validation':
422
+ void this.renderValidationTab(content, a);
423
+ break;
424
+ case 'history':
425
+ this.renderHistoryTab(content, a);
426
+ break;
427
+ }
428
+ }
429
+ renderConfigTab(el, a) {
430
+ const backend = String(a.backend || 'claude');
431
+ const modelOptions = getModelsForBackend(backend)
432
+ .map((m) => `<option value="${escapeAttr(m)}" ${a.model === m ? 'selected' : ''}>${escapeHtml(m)}</option>`)
433
+ .join('');
434
+ const tierOptions = [1, 2, 3]
435
+ .map((t) => `<option value="${t}" ${(a.tier ?? 1) === t ? 'selected' : ''}>T${t}</option>`)
436
+ .join('');
437
+ const backendOptions = Array.from(new Set(['claude', 'codex', 'codex-mcp', 'gemini', backend]))
438
+ .map((b) => `<option value="${escapeAttr(b)}" ${backend === b ? 'selected' : ''}>${escapeHtml(b)}</option>`)
439
+ .join('');
440
+ el.innerHTML = `
441
+ <div class="space-y-3">
442
+ <div>
443
+ <label class="block text-[11px] text-gray-400 mb-1">ID</label>
444
+ <div class="text-[13px] text-gray-800 px-2.5 py-1.5 border border-gray-200 rounded-md bg-gray-50">${escapeHtml(a.id ?? '')}</div>
445
+ </div>
446
+ <div>
447
+ <label class="block text-[11px] text-gray-400 mb-1">Name</label>
448
+ <input id="cfg-name" class="agent-input w-full px-2.5 py-1.5 border border-gray-200 rounded-md text-[13px]" value="${escapeAttr(a.display_name || a.name || '')}" />
449
+ </div>
450
+ <div>
451
+ <label class="block text-[11px] text-gray-400 mb-1">Backend</label>
452
+ <select id="cfg-backend" class="agent-input w-full px-2.5 py-1.5 border border-gray-200 rounded-md text-[13px]">${backendOptions}</select>
453
+ </div>
454
+ <div>
455
+ <label class="block text-[11px] text-gray-400 mb-1">Model</label>
456
+ <select id="cfg-model" class="agent-input w-full px-2.5 py-1.5 border border-gray-200 rounded-md text-[13px]">${modelOptions}</select>
457
+ </div>
458
+ <div>
459
+ <label class="block text-[11px] text-gray-400 mb-1">Tier</label>
460
+ <select id="cfg-tier" class="agent-input w-full px-2.5 py-1.5 border border-gray-200 rounded-md text-[13px]">${tierOptions}</select>
461
+ </div>
462
+ <div class="flex items-center gap-3">
463
+ <label class="flex items-center gap-2 cursor-pointer">
464
+ <input type="checkbox" id="cfg-enabled" ${a.enabled !== false ? 'checked' : ''} class="accent-[#FFCE00] w-4 h-4" />
465
+ <span class="text-[13px]">Enabled</span>
466
+ </label>
467
+ <label class="flex items-center gap-2 cursor-pointer">
468
+ <input type="checkbox" id="cfg-delegate" ${a.can_delegate ? 'checked' : ''} class="accent-[#8b5cf6] w-4 h-4" />
469
+ <span class="text-[13px]">Can Delegate</span>
470
+ </label>
471
+ </div>
472
+ <div class="pt-2">
473
+ <button id="btn-save-config" class="px-4 py-1.5 rounded-md text-[12px] font-medium text-white bg-[#8b5cf6] hover:bg-[#7c3aed] transition-colors">Save</button>
474
+ </div>
475
+ </div>`;
476
+ // Backend change → update model options
477
+ el.querySelector('#cfg-backend')?.addEventListener('change', () => {
478
+ const newBackend = el.querySelector('#cfg-backend').value;
479
+ const models = getModelsForBackend(newBackend);
480
+ const modelSelect = el.querySelector('#cfg-model');
481
+ modelSelect.innerHTML = models
482
+ .map((m) => `<option value="${escapeAttr(m)}">${escapeHtml(m)}</option>`)
483
+ .join('');
484
+ });
485
+ // Save via managed-agent API so config sync + version history happen together
486
+ el.querySelector('#btn-save-config')?.addEventListener('click', async () => {
487
+ if (!a.id) {
488
+ return;
489
+ }
490
+ const displayName = el.querySelector('#cfg-name').value.trim();
491
+ if (!displayName) {
492
+ showToast('Name is required');
493
+ return;
494
+ }
495
+ const changes = {
496
+ name: displayName,
497
+ display_name: displayName,
498
+ model: el.querySelector('#cfg-model').value,
499
+ backend: el.querySelector('#cfg-backend').value,
500
+ tier: parseInt(el.querySelector('#cfg-tier').value, 10),
501
+ enabled: el.querySelector('#cfg-enabled').checked,
502
+ can_delegate: el.querySelector('#cfg-delegate').checked,
503
+ };
504
+ const version = a.version;
505
+ if (version === null || version === undefined) {
506
+ showToast('Version unavailable');
507
+ return;
508
+ }
509
+ try {
510
+ await API.updateAgent(a.id, {
511
+ version,
512
+ changes,
513
+ change_note: 'Config updated via Agents tab',
514
+ });
515
+ showToast('Saved — hot reloaded');
516
+ this.showDetail(a.id);
517
+ }
518
+ catch {
519
+ showToast('Save failed');
520
+ }
521
+ });
522
+ }
523
+ renderPersonaTab(el, a) {
524
+ const text = a.system || '(No persona loaded)';
525
+ el.innerHTML = `
526
+ <textarea id="persona-editor" style="width:100%;min-height:300px;font-family:monospace;font-size:12px;padding:10px;border:1px solid ${C.bdr};border-radius:6px;resize:vertical;line-height:1.5;color:${C.pri};background:#fff;">${escapeHtml(text)}</textarea>
527
+ <div style="margin-top:12px;display:flex;gap:8px;">
528
+ <button id="btn-save-persona" style="padding:6px 14px;border:none;border-radius:6px;background:${C.agent};color:#fff;cursor:pointer;font-size:12px;font-weight:500;">Save \u2014 creates v${(a.version ?? 0) + 1}</button>
529
+ </div>`;
530
+ el.querySelector('#btn-save-persona')?.addEventListener('click', async () => {
531
+ const textarea = el.querySelector('#persona-editor');
532
+ if (!textarea || !a.id) {
533
+ return;
534
+ }
535
+ try {
536
+ const updatePayload = {
537
+ changes: { system: textarea.value },
538
+ change_note: 'Persona updated via viewer',
539
+ };
540
+ if (a.version !== null && a.version !== undefined) {
541
+ updatePayload.version = a.version;
542
+ }
543
+ const res = await API.updateAgent(a.id, updatePayload);
544
+ if (res.new_version) {
545
+ showToast(`v${res.new_version} saved`);
546
+ this.showDetail(a.id);
547
+ }
548
+ }
549
+ catch (err) {
550
+ showToast('Save failed');
551
+ logger.error('Persona save failed', err);
552
+ }
553
+ });
554
+ }
555
+ renderToolsTab(el, a) {
556
+ const allTools = [
557
+ 'Bash',
558
+ 'Read',
559
+ 'Edit',
560
+ 'Write',
561
+ 'Glob',
562
+ 'Grep',
563
+ 'WebFetch',
564
+ 'WebSearch',
565
+ 'NotebookEdit',
566
+ ];
567
+ const allowed = a.tool_permissions?.allowed ?? [];
568
+ const isAll = allowed.includes('*');
569
+ const rows = allTools
570
+ .map((t) => {
571
+ const checked = isAll || allowed.includes(t);
572
+ return `<label class="flex items-center gap-2 py-1.5 border-b border-gray-100 text-[13px] cursor-pointer">
573
+ <input type="checkbox" ${checked ? 'checked' : ''} data-tool="${t}" class="accent-[#8b5cf6] w-4 h-4" /> ${t}
574
+ </label>`;
575
+ })
576
+ .join('');
577
+ el.innerHTML = `
578
+ <div class="text-[11px] text-gray-400 mb-2">Tier ${a.tier ?? 1} preset. Toggle tools and save.</div>
579
+ <div>${rows}</div>
580
+ <div class="pt-3">
581
+ <button id="btn-save-tools" class="px-4 py-1.5 rounded-md text-[12px] font-medium text-white bg-[#8b5cf6] hover:bg-[#7c3aed] transition-colors">Save Tools</button>
582
+ </div>`;
583
+ el.querySelector('#btn-save-tools')?.addEventListener('click', async () => {
584
+ const checked = [];
585
+ el.querySelectorAll('input[data-tool]').forEach((cb) => {
586
+ if (cb.checked) {
587
+ checked.push(cb.dataset.tool);
588
+ }
589
+ });
590
+ if (!a.id) {
591
+ return;
592
+ }
593
+ const preserveWildcard = (isAll || (a.tier ?? 1) === 1) && checked.length === allTools.length;
594
+ const normalizedAllowed = preserveWildcard ? ['*'] : checked;
595
+ const existingBlocked = Array.isArray(a.tool_permissions?.blocked)
596
+ ? a.tool_permissions.blocked
597
+ : [];
598
+ const toolPermissions = {
599
+ allowed: normalizedAllowed,
600
+ blocked: existingBlocked,
601
+ };
602
+ const version = a.version;
603
+ if (version === null || version === undefined) {
604
+ showToast('Version unavailable');
605
+ return;
606
+ }
607
+ try {
608
+ await API.updateAgent(a.id, {
609
+ version,
610
+ changes: { tool_permissions: toolPermissions },
611
+ change_note: preserveWildcard
612
+ ? 'Tools: full access'
613
+ : `Tools: ${normalizedAllowed.join(', ')}`,
614
+ });
615
+ showToast('Tools saved - hot reloaded');
616
+ this.showDetail(a.id);
617
+ }
618
+ catch {
619
+ showToast('Save failed');
620
+ }
621
+ });
622
+ }
623
+ async renderActivityTab(el, a) {
624
+ el.innerHTML = '<div class="text-[12px] text-gray-400">Loading...</div>';
625
+ const requestId = this.detailRequestId;
626
+ const expectedAgentId = this.selectedAgent?.id ?? a.id ?? '';
627
+ try {
628
+ const { activity } = await API.getAgentActivity(a.id ?? '', 20);
629
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
630
+ return;
631
+ }
632
+ if (!activity.length) {
633
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
634
+ return;
635
+ }
636
+ this.updateDetailPageContext(a, { activity: [] });
637
+ el.innerHTML =
638
+ '<div class="text-[12px] text-gray-400 py-4 text-center">No activity yet. Delegate a task to this agent to see logs here.</div>';
639
+ return;
640
+ }
641
+ const rows = activity
642
+ .map((ev) => {
643
+ const typeIcons = {
644
+ test_run: '&#x1F9EA;',
645
+ task_error: '&#x274C;',
646
+ config_change: '&#x2699;&#xFE0F;',
647
+ task_start: '&#x25B6;&#xFE0F;',
648
+ };
649
+ const icon = typeIcons[String(ev.type)] || '&#x2705;';
650
+ const scoreStr = ev.score !== null && ev.score !== undefined ? ` &mdash; ${ev.score}/100` : '';
651
+ const summary = escapeHtml(String(ev.output_summary || ev.input_summary || ev.type));
652
+ const errorHtml = ev.error_message
653
+ ? `<div class="text-[11px] text-red-500 mt-0.5">${escapeHtml(String(ev.error_message))}</div>`
654
+ : '';
655
+ const meta = `<div class="text-[10px] text-gray-400 mt-0.5">v${escapeHtml(String(ev.agent_version ?? ''))} &middot; ${escapeHtml(String(ev.duration_ms ?? 0))}ms &middot; ${escapeHtml(String(ev.created_at ?? ''))}</div>`;
656
+ // Expandable card for test_run with per-item pass/fail
657
+ if (ev.type === 'test_run' && ev.details) {
658
+ let details = null;
659
+ try {
660
+ details =
661
+ typeof ev.details === 'string'
662
+ ? JSON.parse(ev.details)
663
+ : ev.details;
664
+ }
665
+ catch {
666
+ /* ignore parse errors */
667
+ }
668
+ const items = details?.items ?? [];
669
+ const itemsHtml = items
670
+ .map((item) => {
671
+ const badge = item.result === 'pass'
672
+ ? '<span class="text-[10px] px-1.5 py-0.5 rounded bg-green-100 text-green-700">PASS</span>'
673
+ : '<span class="text-[10px] px-1.5 py-0.5 rounded bg-red-100 text-red-700">FAIL</span>';
674
+ return `<div class="flex items-center gap-2 py-1 text-[11px]">${badge}<span class="text-gray-600 truncate">${escapeHtml(String(item.input || ''))}</span></div>`;
675
+ })
676
+ .join('');
677
+ return `<div class="py-2 border-b border-gray-100">
678
+ <div role="button" tabindex="0" aria-expanded="false" aria-controls="expand-${Number(ev.id)}" data-expand="${Number(ev.id)}" class="flex items-center gap-2 cursor-pointer">
679
+ <span class="text-[14px] flex-shrink-0">${icon}</span>
680
+ <div class="flex-1 min-w-0">
681
+ <div class="text-[12px] font-medium text-gray-800">${summary}${scoreStr}</div>
682
+ ${meta}
683
+ </div>
684
+ <span class="text-[10px] text-gray-400">&#x25BC;</span>
685
+ </div>
686
+ <div id="expand-${Number(ev.id)}" class="hidden mt-2 ml-6 pl-2 border-l-2 border-gray-200">${itemsHtml}</div>
687
+ </div>`;
688
+ }
689
+ return `<div class="flex items-start gap-2 py-2 border-b border-gray-100">
690
+ <span class="text-[14px] flex-shrink-0">${icon}</span>
691
+ <div class="flex-1 min-w-0">
692
+ <div class="text-[12px] font-medium text-gray-800">${summary}${scoreStr}</div>
693
+ ${errorHtml}
694
+ ${meta}
695
+ </div>
696
+ </div>`;
697
+ })
698
+ .join('');
699
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
700
+ return;
701
+ }
702
+ el.innerHTML = `<div>${rows}</div>`;
703
+ const activityContext = activity.slice(0, 20).map((ev) => ({
704
+ id: ev.id ?? null,
705
+ type: ev.type ?? null,
706
+ input_summary: ev.input_summary ?? null,
707
+ output_summary: ev.output_summary ?? null,
708
+ execution_status: ev.execution_status ?? null,
709
+ duration_ms: ev.duration_ms ?? null,
710
+ tokens_used: ev.tokens_used ?? null,
711
+ score: ev.score ?? null,
712
+ created_at: ev.created_at ?? null,
713
+ error_message: ev.error_message ?? null,
714
+ }));
715
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
716
+ return;
717
+ }
718
+ this.updateDetailPageContext(a, { activity: activityContext });
719
+ // Expand/collapse toggle with ARIA
720
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
721
+ return;
722
+ }
723
+ el.querySelectorAll('[data-expand]').forEach((toggle) => {
724
+ const toggleExpand = () => {
725
+ const id = toggle.dataset.expand;
726
+ const content = el.querySelector(`#expand-${id}`);
727
+ if (content) {
728
+ const isHidden = content.classList.toggle('hidden');
729
+ toggle.setAttribute('aria-expanded', String(!isHidden));
730
+ }
731
+ };
732
+ toggle.addEventListener('click', toggleExpand);
733
+ toggle.addEventListener('keydown', (event) => {
734
+ if (event.key === 'Enter' || event.key === ' ') {
735
+ event.preventDefault();
736
+ toggleExpand();
737
+ }
738
+ });
739
+ });
740
+ }
741
+ catch {
742
+ el.innerHTML = '<div class="text-[12px] text-red-500">Failed to load activity.</div>';
743
+ }
744
+ }
745
+ // ── Validation Tab ─────────────────────────────────────────────────────
746
+ async renderValidationTab(el, a) {
747
+ el.innerHTML = `<div style="color:${C.ter};font-size:12px;">Loading validation...</div>`;
748
+ const agentId = a.id ?? '';
749
+ const requestId = this.detailRequestId;
750
+ const expectedAgentId = this.selectedAgent?.id ?? agentId;
751
+ const OC = {
752
+ healthy: '#22c55e',
753
+ improved: '#3b82f6',
754
+ regressed: '#ef4444',
755
+ inconclusive: '#f59e0b',
756
+ };
757
+ try {
758
+ const [summaryRes, historyRes] = await Promise.all([
759
+ API.getValidationSummary(agentId, DEFAULT_VALIDATION_TRIGGER),
760
+ API.getValidationHistory(agentId, 30, DEFAULT_VALIDATION_TRIGGER),
761
+ ]);
762
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
763
+ return;
764
+ }
765
+ const summary = summaryRes.summary;
766
+ const history = historyRes.history;
767
+ if (!summary && history.length === 0) {
768
+ el.innerHTML = `<div style="color:${C.ter};font-size:13px;padding:32px 0;text-align:center;">
769
+ No validation sessions yet.<br/>
770
+ <span style="font-size:12px;">Run <code style="background:${C.bg};padding:2px 6px;border-radius:4px;">agent_test("${escapeHtml(agentId)}")</code> to create the first session.</span>
771
+ </div>`;
772
+ return;
773
+ }
774
+ const latestId = String(summary?.id ?? '');
775
+ const outcome = String(summary?.validation_outcome ?? 'none');
776
+ const execStatus = String(summary?.execution_status ?? '—');
777
+ const outcomeColor = OC[outcome] ?? C.ter;
778
+ const baselineVer = summary?.baseline_version !== null && summary?.baseline_version !== undefined
779
+ ? `v${summary.baseline_version}`
780
+ : 'none';
781
+ const endedAt = summary?.ended_at ? new Date(Number(summary.ended_at)).toLocaleString() : '—';
782
+ // ── 1. Fetch session detail with metrics + compare against baseline ──
783
+ let metricsHtml = '';
784
+ let compareData = null;
785
+ if (latestId) {
786
+ try {
787
+ compareData = await API.getValidationCompare(agentId, latestId, 'approved');
788
+ }
789
+ catch {
790
+ /* no compare data available */
791
+ }
792
+ }
793
+ if (compareData && compareData.deltas && compareData.deltas.length > 0) {
794
+ const metricRows = compareData.deltas
795
+ .map((d) => {
796
+ const hasBaseline = d.baseline !== null && d.delta !== null;
797
+ const isGood = d.direction === 'down_good' ? (d.delta ?? 0) < 0 : (d.delta ?? 0) > 0;
798
+ const deltaColor = !hasBaseline ? C.ter : isGood ? '#22c55e' : '#ef4444';
799
+ const deltaSign = (d.delta ?? 0) > 0 ? '+' : '';
800
+ const pct = hasBaseline && d.baseline ? Math.round(((d.delta ?? 0) / d.baseline) * 100) : null;
801
+ const pctStr = pct !== null ? ` (${pct > 0 ? '+' : ''}${pct}%)` : '';
802
+ const baseStr = hasBaseline ? String(d.baseline) : '—';
803
+ const arrow = hasBaseline ? ' → ' : '';
804
+ return `<tr style="border-bottom:1px solid ${C.bdr};">
805
+ <td style="padding:8px;font-size:12px;color:${C.sec};font-weight:500;">${escapeHtml(d.name)}</td>
806
+ <td style="padding:8px;font-size:13px;font-weight:600;color:${C.pri};">
807
+ ${hasBaseline ? `<span style="color:${C.ter};">${baseStr}</span>${arrow}` : ''}${d.current}
808
+ </td>
809
+ <td style="padding:8px;font-size:12px;font-weight:600;color:${deltaColor};">
810
+ ${hasBaseline ? `${deltaSign}${d.delta}${pctStr}` : 'no baseline'}
811
+ </td>
812
+ <td style="padding:8px;font-size:11px;color:${C.ter};">${d.direction === 'down_good' ? '↓ lower better' : d.direction === 'up_good' ? '↑ higher better' : '—'}</td>
813
+ </tr>`;
814
+ })
815
+ .join('');
816
+ metricsHtml = `
817
+ <div style="margin-top:20px;">
818
+ <div style="font-size:13px;font-weight:600;color:${C.pri};margin-bottom:8px;">Metrics vs Baseline</div>
819
+ <table style="width:100%;border-collapse:collapse;">
820
+ <thead><tr style="border-bottom:2px solid ${C.bdr};">
821
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Metric</th>
822
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Value</th>
823
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Delta</th>
824
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Direction</th>
825
+ </tr></thead>
826
+ <tbody>${metricRows}</tbody>
827
+ </table>
828
+ </div>`;
829
+ }
830
+ // ── 2. Report from Conductor ──
831
+ let reportHtml = '';
832
+ let reportContext = null;
833
+ if (summary?.report_json) {
834
+ try {
835
+ const report = JSON.parse(String(summary.report_json));
836
+ const reportLines = Array.isArray(report.lines)
837
+ ? report.lines.map((line) => String(line))
838
+ : [];
839
+ const reportHeadline = String(report.headline ?? report.outcome ?? reportLines[0] ?? '');
840
+ const reportDetails = String(report.details ?? (reportLines.length > 0 ? reportLines.join('\n') : ''));
841
+ reportContext = {
842
+ headline: reportHeadline,
843
+ details: reportDetails,
844
+ outcome: String(report.outcome ?? outcome),
845
+ };
846
+ reportHtml = `
847
+ <div style="margin-top:20px;">
848
+ <div style="font-size:13px;font-weight:600;color:${C.pri};margin-bottom:8px;">Validation Report</div>
849
+ <div style="background:${C.bg};padding:12px 16px;border-radius:8px;border-left:3px solid ${outcomeColor};">
850
+ <div style="font-size:13px;font-weight:600;color:${outcomeColor};margin-bottom:6px;">${escapeHtml(reportHeadline)}</div>
851
+ <div style="font-size:12px;color:${C.sec};white-space:pre-wrap;line-height:1.6;">${escapeHtml(reportDetails)}</div>
852
+ </div>
853
+ </div>`;
854
+ }
855
+ catch {
856
+ /* ignore */
857
+ }
858
+ }
859
+ // ── 3. History — version-by-version performance table ──
860
+ let historyHtml = '';
861
+ if (history.length > 0) {
862
+ const sessionDetails = await Promise.all(history.slice(0, 10).map(async (h) => {
863
+ try {
864
+ const detail = await API.getValidationSessionDetail(String(h.id));
865
+ return { ...h, _metrics: detail.metrics };
866
+ }
867
+ catch {
868
+ return { ...h, _metrics: [] };
869
+ }
870
+ }));
871
+ const hRows = sessionDetails
872
+ .map((h) => {
873
+ const hOutcome = String(h.validation_outcome ?? '—');
874
+ const hColor = OC[hOutcome] ?? C.ter;
875
+ const hTime = h.ended_at ? new Date(Number(h.ended_at)).toLocaleString() : 'running...';
876
+ const metrics = h._metrics;
877
+ // Extract key metrics for display
878
+ const durMetric = metrics.find((m) => m.name === 'duration_ms' || m.name === 'publish_latency_ms');
879
+ const tokenMetric = metrics.find((m) => m.name === 'token_cost');
880
+ const scoreMetric = metrics.find((m) => m.name === 'auto_score' || m.name === 'completion_rate');
881
+ const fmtMetric = (m, unit) => {
882
+ if (!m) {
883
+ return `<span style="color:${C.ter};">—</span>`;
884
+ }
885
+ const val = unit === 'ms'
886
+ ? `${(m.value / 1000).toFixed(1)}s`
887
+ : unit === '%'
888
+ ? `${Math.round(m.value * 100)}%`
889
+ : String(Math.round(m.value));
890
+ if (m.delta_value === null) {
891
+ return `<span>${val}</span>`;
892
+ }
893
+ const isGood = m.direction === 'down_good' ? m.delta_value < 0 : m.delta_value > 0;
894
+ const dColor = isGood ? '#22c55e' : '#ef4444';
895
+ const sign = m.delta_value > 0 ? '+' : '';
896
+ const dVal = unit === 'ms'
897
+ ? `${sign}${(m.delta_value / 1000).toFixed(1)}s`
898
+ : `${sign}${Math.round(m.delta_value)}`;
899
+ return `<span>${val}</span> <span style="color:${dColor};font-size:10px;">${dVal}</span>`;
900
+ };
901
+ const cmpBase = compareData?.baseline;
902
+ const isApproved = String(summary?.baseline_session_id ?? '') === String(h.id) ||
903
+ cmpBase?.session?.id === String(h.id);
904
+ return `<tr style="border-bottom:1px solid ${C.bdr};${String(h.id) === latestId ? `background:${C.bg};` : ''}">
905
+ <td style="padding:6px 8px;font-size:12px;color:${C.sec};">v${h.agent_version ?? '?'}${isApproved ? ` <span style="font-size:9px;padding:1px 4px;border-radius:3px;background:${C.agent}20;color:${C.agent};">baseline</span>` : ''}</td>
906
+ <td style="padding:6px 8px;font-size:12px;"><span style="color:${hColor};font-weight:600;">${escapeHtml(hOutcome)}</span></td>
907
+ <td style="padding:6px 8px;font-size:12px;color:${C.sec};">${escapeHtml(String(h.trigger_type ?? '—'))}</td>
908
+ <td style="padding:6px 8px;font-size:12px;">${fmtMetric(durMetric, 'ms')}</td>
909
+ <td style="padding:6px 8px;font-size:12px;">${fmtMetric(tokenMetric, '')}</td>
910
+ <td style="padding:6px 8px;font-size:12px;">${fmtMetric(scoreMetric, '%')}</td>
911
+ <td style="padding:6px 8px;font-size:11px;color:${C.ter};">${escapeHtml(hTime)}</td>
912
+ </tr>`;
913
+ })
914
+ .join('');
915
+ historyHtml = `
916
+ <div style="margin-top:20px;">
917
+ <div style="font-size:13px;font-weight:600;color:${C.pri};margin-bottom:8px;">Version Performance History</div>
918
+ <div style="overflow-x:auto;">
919
+ <table style="width:100%;border-collapse:collapse;min-width:600px;">
920
+ <thead><tr style="border-bottom:2px solid ${C.bdr};">
921
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Version</th>
922
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Outcome</th>
923
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Trigger</th>
924
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Latency</th>
925
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Tokens</th>
926
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Score</th>
927
+ <th style="text-align:left;padding:6px 8px;font-size:11px;color:${C.ter};">Time</th>
928
+ </tr></thead>
929
+ <tbody>${hRows}</tbody>
930
+ </table>
931
+ </div>
932
+ </div>`;
933
+ }
934
+ const validationContext = {
935
+ ...this.buildDetailValidationContext(summary),
936
+ latest_session_id: latestId || null,
937
+ metrics: compareData?.deltas ?? [],
938
+ report: reportContext,
939
+ history: history.slice(0, 10).map((h) => ({
940
+ id: h.id ?? null,
941
+ agent_version: h.agent_version ?? null,
942
+ validation_outcome: h.validation_outcome ?? null,
943
+ execution_status: h.execution_status ?? null,
944
+ trigger_type: h.trigger_type ?? null,
945
+ ended_at: h.ended_at ?? null,
946
+ baseline_version: h.baseline_version ?? null,
947
+ })),
948
+ };
949
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
950
+ return;
951
+ }
952
+ this.updateDetailPageContext(a, { validation: validationContext });
953
+ // ── 4. Approve button ──
954
+ const canApprove = outcome === 'improved' || outcome === 'healthy';
955
+ const sessionVersion = Number(summary?.agent_version ?? a.version ?? 0);
956
+ const approveBtn = canApprove
957
+ ? `<button id="btn-approve-validation" style="margin-top:16px;padding:8px 20px;background:${C.agent};color:#131313;border:none;border-radius:6px;font-size:13px;font-weight:600;cursor:pointer;transition:opacity 0.15s;" onmouseover="this.style.opacity='0.85'" onmouseout="this.style.opacity='1'">Approve v${sessionVersion} as Baseline</button>`
958
+ : '';
959
+ el.innerHTML = `
960
+ <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(130px,1fr));gap:12px;margin-bottom:4px;">
961
+ <div style="background:${C.bg};padding:14px;border-radius:8px;border-left:3px solid ${outcomeColor};">
962
+ <div style="font-size:11px;color:${C.ter};margin-bottom:4px;">Validation Outcome</div>
963
+ <div style="font-size:18px;font-weight:700;color:${outcomeColor};">${escapeHtml(outcome)}</div>
964
+ </div>
965
+ <div style="background:${C.bg};padding:14px;border-radius:8px;">
966
+ <div style="font-size:11px;color:${C.ter};margin-bottom:4px;">Execution</div>
967
+ <div style="font-size:15px;font-weight:600;color:${execStatus === 'completed' ? '#22c55e' : execStatus === 'failed' ? '#ef4444' : C.pri};">${escapeHtml(execStatus)}</div>
968
+ </div>
969
+ <div style="background:${C.bg};padding:14px;border-radius:8px;">
970
+ <div style="font-size:11px;color:${C.ter};margin-bottom:4px;">Approved Baseline</div>
971
+ <div style="font-size:15px;font-weight:600;color:${C.pri};">${escapeHtml(baselineVer)}</div>
972
+ </div>
973
+ <div style="background:${C.bg};padding:14px;border-radius:8px;">
974
+ <div style="font-size:11px;color:${C.ter};margin-bottom:4px;">Last Validated</div>
975
+ <div style="font-size:12px;color:${C.sec};">${escapeHtml(endedAt)}</div>
976
+ </div>
977
+ </div>
978
+
979
+ ${metricsHtml}
980
+ ${reportHtml}
981
+ ${historyHtml}
982
+ ${approveBtn}
983
+ `;
984
+ // Approve handler
985
+ const approveEl = el.querySelector('#btn-approve-validation');
986
+ if (approveEl && latestId) {
987
+ approveEl.addEventListener('click', async () => {
988
+ try {
989
+ await API.approveValidationSession(agentId, latestId);
990
+ const refreshedSummary = await API.getValidationSummary(agentId, DEFAULT_VALIDATION_TRIGGER).catch(() => ({
991
+ summary: null,
992
+ }));
993
+ const refreshedValidation = refreshedSummary.summary;
994
+ if (requestId !== this.detailRequestId || this.selectedAgent?.id !== expectedAgentId) {
995
+ return;
996
+ }
997
+ this.currentDetailContext = this.buildDetailPageContext(a, refreshedValidation);
998
+ if (refreshedValidation?.validation_outcome) {
999
+ this.validationStates.set(agentId, String(refreshedValidation.validation_outcome));
1000
+ }
1001
+ reportPageContext('agents', this.currentDetailContext, a.id ? { type: 'agent', id: a.id } : undefined);
1002
+ void this.renderValidationTab(el, a);
1003
+ showToast('Approved as baseline');
1004
+ }
1005
+ catch {
1006
+ showToast('Approval failed');
1007
+ }
1008
+ });
1009
+ }
1010
+ }
1011
+ catch (err) {
1012
+ el.innerHTML = `<div style="color:#ef4444;font-size:12px;">Failed to load validation data: ${escapeHtml(String(err))}</div>`;
1013
+ }
1014
+ }
1015
+ async renderHistoryTab(el, a) {
1016
+ el.innerHTML = `<div style="color:${C.ter};font-size:12px;">Loading versions...</div>`;
1017
+ const requestId = this.detailRequestId;
1018
+ const expectedAgentId = a.id ?? '';
1019
+ try {
1020
+ const { versions } = await API.getAgentVersions(a.id ?? '');
1021
+ if (this.detailRequestId !== requestId || this.selectedAgent?.id !== expectedAgentId) {
1022
+ return;
1023
+ }
1024
+ if (!versions.length) {
1025
+ el.innerHTML = `<div style="color:${C.ter};font-size:12px;">No version history.</div>`;
1026
+ return;
1027
+ }
1028
+ const rows = versions
1029
+ .map((v) => `<div style="display:flex;align-items:center;gap:12px;padding:8px 0;border-bottom:1px solid ${C.bdr};">
1030
+ <span style="font-size:13px;font-weight:600;color:${C.pri};min-width:32px;">v${v.version}</span>
1031
+ <span style="font-size:11px;color:${C.ter};">${escapeHtml(String(v.created_at ?? ''))}</span>
1032
+ <span style="font-size:12px;color:${C.sec};flex:1;">${escapeHtml(String(v.change_note || ''))}</span>
1033
+ ${v.version === a.version ? `<span style="font-size:10px;padding:2px 6px;border-radius:4px;background:${C.agent}15;color:${C.agent};font-weight:600;">current</span>` : ''}
1034
+ </div>`)
1035
+ .join('');
1036
+ if (this.detailRequestId !== requestId || this.selectedAgent?.id !== expectedAgentId) {
1037
+ return;
1038
+ }
1039
+ el.innerHTML = `<div>${rows}</div>`;
1040
+ }
1041
+ catch {
1042
+ if (this.detailRequestId !== requestId || this.selectedAgent?.id !== expectedAgentId) {
1043
+ return;
1044
+ }
1045
+ el.innerHTML = `<div style="color:${C.red};font-size:12px;">Failed to load versions.</div>`;
1046
+ }
1047
+ }
1048
+ // ── Create Modal ────────────────────────────────────────────────────────
1049
+ showCreateModal() {
1050
+ if (!this.container) {
1051
+ return;
1052
+ }
1053
+ const overlay = document.createElement('div');
1054
+ overlay.style.cssText =
1055
+ 'position:fixed;inset:0;background:rgba(0,0,0,0.3);z-index:100;display:flex;align-items:center;justify-content:center;';
1056
+ overlay.setAttribute('role', 'dialog');
1057
+ overlay.setAttribute('aria-modal', 'true');
1058
+ overlay.setAttribute('aria-label', 'Create new agent');
1059
+ overlay.innerHTML = `
1060
+ <div style="background:#fff;border-radius:12px;padding:24px;width:380px;max-width:90vw;box-shadow:0 8px 32px rgba(0,0,0,0.15);">
1061
+ <h3 style="font-size:16px;font-weight:600;color:${C.pri};margin:0 0 16px 0;">New Agent</h3>
1062
+ <div style="margin-bottom:10px;"><label for="new-id" style="font-size:11px;color:${C.ter};display:block;margin-bottom:4px;">ID (slug)</label><input id="new-id" class="agent-input" style="width:100%;padding:8px 10px;border:1px solid ${C.bdr};border-radius:6px;font-size:13px;" placeholder="qa-specialist" /></div>
1063
+ <div style="margin-bottom:10px;"><label for="new-name" style="font-size:11px;color:${C.ter};display:block;margin-bottom:4px;">Name</label><input id="new-name" class="agent-input" style="width:100%;padding:8px 10px;border:1px solid ${C.bdr};border-radius:6px;font-size:13px;" placeholder="QA Specialist" /></div>
1064
+ <div style="margin-bottom:10px;"><label for="new-model" style="font-size:11px;color:${C.ter};display:block;margin-bottom:4px;">Model</label><input id="new-model" class="agent-input" style="width:100%;padding:8px 10px;border:1px solid ${C.bdr};border-radius:6px;font-size:13px;" value="claude-sonnet-4-6" /></div>
1065
+ <div style="margin-bottom:16px;"><label for="new-tier" style="font-size:11px;color:${C.ter};display:block;margin-bottom:4px;">Tier</label><select id="new-tier" class="agent-input" style="width:100%;padding:8px 10px;border:1px solid ${C.bdr};border-radius:6px;font-size:13px;"><option value="1">T1 (Full)</option><option value="2" selected>T2 (Read/Search)</option><option value="3">T3 (Read only)</option></select></div>
1066
+ <div style="display:flex;gap:8px;justify-content:flex-end;">
1067
+ <button id="btn-cancel" style="padding:8px 14px;border:1px solid ${C.bdr};border-radius:6px;background:#fff;cursor:pointer;font-size:12px;">Cancel</button>
1068
+ <button id="btn-create" style="padding:8px 14px;border:none;border-radius:6px;background:${C.agent};color:#fff;cursor:pointer;font-size:12px;font-weight:500;">Create</button>
1069
+ </div>
1070
+ </div>`;
1071
+ document.body.appendChild(overlay);
1072
+ overlay.querySelector('#btn-cancel')?.addEventListener('click', () => overlay.remove());
1073
+ overlay.addEventListener('click', (e) => {
1074
+ if (e.target === overlay) {
1075
+ overlay.remove();
1076
+ }
1077
+ });
1078
+ overlay.querySelector('#btn-create')?.addEventListener('click', async () => {
1079
+ const id = overlay.querySelector('#new-id').value.trim();
1080
+ const name = overlay.querySelector('#new-name').value.trim();
1081
+ const model = overlay.querySelector('#new-model').value.trim();
1082
+ const tier = parseInt(overlay.querySelector('#new-tier').value, 10);
1083
+ if (!id || !name) {
1084
+ showToast('ID and Name are required');
1085
+ return;
1086
+ }
1087
+ try {
1088
+ await API.createAgent({ id, name, model, tier });
1089
+ overlay.remove();
1090
+ showToast(`Agent '${name}' created`);
1091
+ await this.showDetail(id);
1092
+ }
1093
+ catch (err) {
1094
+ showToast('Create failed');
1095
+ logger.error('Create agent failed', err);
1096
+ }
1097
+ });
1098
+ }
1099
+ // ── Navigation ──────────────────────────────────────────────────────────
1100
+ /**
1101
+ * Deep navigation from viewer_navigate command.
1102
+ * Opens agent detail and optionally switches to a specific tab.
1103
+ */
1104
+ async navigateTo(agentId, tab) {
1105
+ const tryNav = async () => {
1106
+ const agent = this.agents.find((a) => a.id === agentId);
1107
+ if (agent) {
1108
+ const desiredTab = tab && ['config', 'persona', 'tools', 'activity', 'validation', 'history'].includes(tab)
1109
+ ? tab
1110
+ : undefined;
1111
+ await this.showDetail(agentId, desiredTab);
1112
+ return true;
1113
+ }
1114
+ return false;
1115
+ };
1116
+ let navigated = await tryNav();
1117
+ if (!navigated) {
1118
+ try {
1119
+ await this.loadAgents();
1120
+ }
1121
+ catch (error) {
1122
+ logger.error(`Failed to load agents while navigating to ${agentId}`, error);
1123
+ showToast('Failed to load agents');
1124
+ return;
1125
+ }
1126
+ navigated = await tryNav();
1127
+ }
1128
+ if (!navigated) {
1129
+ logger.warn(`Agent not found during navigation: ${agentId}`);
1130
+ this.showList();
1131
+ showToast('Agent not found');
1132
+ }
1133
+ }
1134
+ showList() {
1135
+ this.detailRequestId++;
1136
+ this.selectedAgent = null;
1137
+ this.currentDetailContext = null;
1138
+ reportPageContext('agents', {
1139
+ ...this.buildListPageContext(),
1140
+ selectedAgent: null,
1141
+ activeTab: null,
1142
+ });
1143
+ void this.loadAgents().catch((error) => {
1144
+ logger.error('Failed to load agents list', error);
1145
+ showToast('Failed to load agents');
1146
+ });
1147
+ }
1148
+ }