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