var collapseBtn = document.getElementById('sidebar-collapse');
if (collapseBtn) {
collapseBtn.addEventListener('click', function() {
var sb = document.getElementById('sidebar');
if (sb) { sb.classList.toggle('collapsed'); collapseBtn.title = sb.classList.contains('collapsed') ? 'Expand sidebar' : 'Collapse sidebar'; }
});
}
window.addEventListener('hashchange', onHash);
var content = document.getElementById('content');
if (content) content.addEventListener('click', function(e) {
var evidenceToggleBtn = e.target.closest('[data-rec-evidence-toggle]');
if (evidenceToggleBtn) {
var evidenceId = evidenceToggleBtn.getAttribute('data-rec-evidence-toggle');
var evidenceEl = evidenceId ? document.getElementById(evidenceId) : null;
if (!evidenceEl) return;
var isOpen = evidenceEl.style.display !== 'none';
evidenceEl.style.display = isOpen ? 'none' : 'block';
evidenceToggleBtn.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
evidenceToggleBtn.textContent = (isOpen ? '\u25b6' : '\u25bc') + evidenceToggleBtn.textContent.slice(1);
return;
}
var enableCacheBtn = e.target.closest('[data-action="enable-semantic-cache"]');
if (enableCacheBtn) {
if (enableCacheBtn.disabled) return;
var modelName = enableCacheBtn.getAttribute('data-model') || 'model';
var oldText = enableCacheBtn.textContent;
enableCacheBtn.disabled = true;
enableCacheBtn.textContent = 'Applying...';
api('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ cache: { enabled: true } })
}).then(function() {
if (!_cachedConfig) _cachedConfig = {};
if (!_cachedConfig.cache) _cachedConfig.cache = {};
_cachedConfig.cache.enabled = true;
toast('Semantic caching enabled for runtime (' + modelName + ').');
enableCacheBtn.textContent = 'Enabled';
}).catch(function(err) {
enableCacheBtn.disabled = false;
enableCacheBtn.textContent = oldText;
toast(err.message || 'Failed to enable semantic caching');
});
return;
}
var navBtn = e.target.closest('[data-page-nav]');
if (navBtn) {
var nextPage = navBtn.getAttribute('data-page-nav');
if (nextPage === 'efficiency') App._effTab = 'performance';
App.navigate(nextPage);
return;
}
var graphNode = e.target.closest('[data-node-model]');
if (graphNode) {
App._modelGraphFocusModel = graphNode.getAttribute('data-node-model');
App._modelGraphFocusEdge = null;
App.navigate('efficiency');
return;
}
var graphEdge = e.target.closest('[data-edge-key]');
if (graphEdge) {
App._modelGraphFocusEdge = graphEdge.getAttribute('data-edge-key');
App._modelGraphFocusModel = null;
App.navigate('efficiency');
return;
}
if (e.target.closest('#model-graph-clear-focus')) {
App._modelGraphFocusModel = null;
App._modelGraphFocusEdge = null;
App.navigate('efficiency');
return;
}
if (e.target.closest('#routing-profile-reset')) {
App._routingProfileDraft = normalizeRoutingProfile(App._routingProfileDefaults
? { correctness: App._routingProfileDefaults.correctness, cost: App._routingProfileDefaults.cost, speed: App._routingProfileDefaults.speed }
: { correctness: 0.0, cost: 0.0, speed: 0.0 });
App.navigate('efficiency');
return;
}
if (e.target.closest('#routing-profile-apply')) {
var applyBtn = e.target.closest('#routing-profile-apply');
if (applyBtn) { applyBtn.disabled = true; applyBtn.textContent = 'Applying...'; }
var patch = projectRoutingPatchFromProfile(App._routingProfileDraft || App._routingProfileDefaults || { correctness: 0, cost: 0, speed: 0 });
api('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(patch)
}).then(function(resp) {
App._routingProfileDefaults = normalizeRoutingProfile(App._routingProfileDraft || App._routingProfileDefaults || { correctness: 0, cost: 0, speed: 0 });
App._routingProfileDraft = normalizeRoutingProfile(App._routingProfileDefaults);
App._routingProfilePersistedAt = new Date().toISOString();
App._routingProfilePersisted = !!(resp && resp.persisted);
toast(App._routingProfilePersisted ? 'Routing profile applied and persisted.' : 'Routing profile applied.');
App.navigate('efficiency');
}).catch(function(err) {
if (applyBtn) { applyBtn.disabled = false; applyBtn.textContent = 'Apply profile'; }
toast(err.message || 'Failed to apply routing profile');
});
return;
}
if (e.target.closest('#budget-reset')) {
App._contextBudgetDraft = normalizeContextBudget(App._contextBudgetDefaults || {});
App.navigate('efficiency');
return;
}
if (e.target.closest('#budget-apply')) {
var bApplyBtn = e.target.closest('#budget-apply');
if (bApplyBtn) { bApplyBtn.disabled = true; bApplyBtn.textContent = 'Applying...'; }
var budgetDraft = normalizeContextBudget(App._contextBudgetDraft || App._contextBudgetDefaults || {});
var bPatch = { context_budget: budgetDraft };
api('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bPatch)
}).then(function(resp) {
App._contextBudgetDefaults = normalizeContextBudget(budgetDraft);
App._contextBudgetDraft = normalizeContextBudget(budgetDraft);
App._contextBudgetPersistedAt = new Date().toISOString();
App._contextBudgetPersisted = !!(resp && resp.persisted);
toast(App._contextBudgetPersisted ? 'Context budget applied and persisted.' : 'Context budget applied.');
App.navigate('efficiency');
}).catch(function(err) {
if (bApplyBtn) { bApplyBtn.disabled = false; bApplyBtn.textContent = 'Apply budget'; }
toast(err.message || 'Failed to apply context budget');
});
return;
}
if (e.target.closest('.channel-test-btn')) {
var testBtn = e.target.closest('.channel-test-btn');
var channelName = testBtn.getAttribute('data-test-channel');
if (!channelName) return;
testBtn.disabled = true; testBtn.textContent = 'Testing...';
api('/api/channels/' + encodeURIComponent(channelName) + '/test', { method: 'POST' })
.then(function(resp) {
var ok = resp && resp.ok;
var details = resp && resp.diagnostics && resp.diagnostics.details;
toast(ok ? (channelName + ': ' + (details || 'OK')) : (channelName + ': ' + (details || 'Failed')));
testBtn.disabled = false; testBtn.textContent = 'Test';
})
.catch(function(err) {
toast(channelName + ' test failed: ' + (err.message || 'unknown error'));
testBtn.disabled = false; testBtn.textContent = 'Test';
});
return;
}
if (e.target.closest('.integration-test-btn')) {
var iBtn = e.target.closest('.integration-test-btn');
var platform = iBtn.getAttribute('data-test-platform');
if (!platform) return;
iBtn.disabled = true; iBtn.textContent = 'Testing...';
api('/api/channels/' + encodeURIComponent(platform) + '/test', { method: 'POST' })
.then(function(resp) {
var ok = resp && resp.ok;
var details = resp && resp.diagnostics && resp.diagnostics.details;
toast(ok ? (platform + ': ' + (details || 'OK')) : (platform + ': ' + (details || 'Failed')));
iBtn.disabled = false; iBtn.textContent = 'Test';
})
.catch(function(err) {
toast(platform + ' test failed: ' + (err.message || 'unknown error'));
iBtn.disabled = false; iBtn.textContent = 'Test';
});
return;
}
if (e.target.closest('#dismiss-onboarding')) {
window.localStorage.setItem('ic_dash_onboarding_dismissed', '1');
var onboarding = document.getElementById('ov-onboarding');
if (onboarding) onboarding.style.display = 'none';
return;
}
var dismissHintBtn = e.target.closest('[data-dismiss-hint]');
if (dismissHintBtn) {
var hintId = dismissHintBtn.getAttribute('data-dismiss-hint');
dismissHint(hintId);
var hintNode = dismissHintBtn.closest('.hint-banner');
if (hintNode) hintNode.remove();
return;
}
if (e.target.closest('#btn-back-sessions')) {
App._activeSession = null; App.navigate('sessions'); return;
}
if (e.target.closest('#sess-prev')) { App._sessionsPage = Math.max(0, (App._sessionsPage || 0) - 1); App.navigate('sessions'); return; }
if (e.target.closest('#sess-next')) { App._sessionsPage = (App._sessionsPage || 0) + 1; App.navigate('sessions'); return; }
if (e.target.closest('#mem-prev')) { App._memoryPage = Math.max(0, (App._memoryPage || 0) - 1); App.navigate('memory'); return; }
if (e.target.closest('#mem-next')) { App._memoryPage = (App._memoryPage || 0) + 1; App.navigate('memory'); return; }
if (e.target.closest('.session-delete-btn')) {
var delId = e.target.closest('.session-delete-btn').getAttribute('data-delete-session');
if (delId && confirm('Delete session ' + delId.substring(0, 8) + '…?')) {
api('/api/sessions/' + encodeURIComponent(delId), { method: 'DELETE' }).then(function() { toast('Session deleted'); App.navigate('sessions'); }).catch(function(err) { toast(err.message || 'Failed'); });
}
return;
}
if (e.target.closest('.session-archive-btn')) {
var archiveId = e.target.closest('.session-archive-btn').getAttribute('data-archive-session');
if (archiveId && confirm('Archive session ' + archiveId.substring(0, 8) + '…?')) {
api('/api/sessions/' + encodeURIComponent(archiveId) + '/archive', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ reason: 'Archived from sessions dashboard' })
}).then(function() {
toast('Session archived');
App.navigate('sessions');
}).catch(function(err) { toast(err.message || 'Failed'); });
}
return;
}
if (e.target.closest('#btn-new-session')) {
App._resolveActiveAgentId().then(function(agentId) {
return api('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agent_id: agentId }) }).then(function(r) {
var sid = r.session_id || r.id;
App._activeSession = { id: sid, agent_id: agentId, agent_name: AGENT_DISPLAY_NAME || null };
dismissHint('sessions-helper');
try { window.localStorage.setItem('ic_sessions_helper_dismissed', '1'); } catch (_) {}
toast('Session created');
App.navigate('sessions');
});
}).catch(function(err) { toast(err.message || 'Failed'); });
return;
}
if (e.target.closest('#btn-send-msg')) {
if (App._sendingMessage) return;
var input = document.getElementById('session-msg-input');
var msg = input ? input.value.trim() : '';
if (!msg || !App._activeSession) return;
App._sendingMessage = true;
input.value = '';
input.disabled = true;
var sendBtn = document.getElementById('btn-send-msg');
if (sendBtn) { sendBtn.disabled = true; sendBtn.textContent = 'Thinking\u2026'; }
var thread = document.querySelector('.message-thread');
if (thread) {
var userBubble = document.createElement('div');
userBubble.className = 'message user';
setHtml(userBubble, '<div class="message-role">user</div><div>' + renderSafeMarkdown(msg) + '</div>');
thread.appendChild(userBubble);
var thinkEl = document.createElement('div');
thinkEl.className = 'thinking-indicator';
thinkEl.id = 'thinking-bubble';
setHtml(thinkEl, '<span class="thinking-brain">\uD83E\uDDE0</span><span class="thinking-dots"><span></span><span></span><span></span></span>');
thread.appendChild(thinkEl);
thread.scrollTop = thread.scrollHeight;
}
function unlockChat() {
App._sendingMessage = false;
var tb = document.getElementById('thinking-bubble');
if (tb) tb.remove();
if (input) { input.disabled = false; input.focus(); }
if (sendBtn) { sendBtn.disabled = false; sendBtn.textContent = 'Send'; }
}
(async function streamMessage() {
var asstBubble = null;
var contentEl = null;
var streamModel = '';
var accumulated = '';
var streamIntercepted = false;
try {
var resp = await fetch('/api/agent/message/stream', {
method: 'POST',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ content: msg, session_id: App._activeSession.id })
});
if (!resp.ok) {
var errBody = await resp.text();
try { var p = JSON.parse(errBody); throw new Error(p.error || errBody); } catch(ex) { if (ex.message) throw ex; throw new Error(errBody); }
}
var reader = resp.body.getReader();
var decoder = new TextDecoder();
var sseBuffer = '';
while (true) {
var result = await reader.read();
if (result.done) break;
sseBuffer += decoder.decode(result.value, { stream: true });
var lines = sseBuffer.split('\n');
sseBuffer = lines.pop() || '';
for (var li = 0; li < lines.length; li++) {
var line = lines[li].trim();
if (!line.startsWith('data:')) continue;
var payload = line.substring(5).trim();
if (!payload || payload === '[DONE]') continue;
try { var d = JSON.parse(payload); } catch(ex) { continue; }
if (d.type === 'stream_start') {
if (d.session_id) App._activeSession.id = d.session_id;
App._liveStreamTurn = {
turn_id: d.turn_id || '',
session_id: d.session_id || App._activeSession.id || '',
model: d.model || ''
};
if (App.page === 'context') App.navigate('context');
streamModel = d.model || '';
var tb2 = document.getElementById('thinking-bubble');
if (tb2) tb2.remove();
if (thread) {
asstBubble = document.createElement('div');
asstBubble.className = 'message assistant';
var assistantLabel = sessionAssistantLabel(App._activeSession, 'assistant');
var roleHtml = '<div class="message-role">' + esc(assistantLabel);
if (streamModel) roleHtml += ' <span class="badge muted" style="font-size:0.5625rem;margin-right:0.375rem">' + esc(streamModel) + '</span>';
roleHtml += ' <span class="badge" style="font-size:0.5rem;background:var(--accent);color:#fff">streaming</span>';
roleHtml += '</div>';
contentEl = document.createElement('div');
contentEl.className = 'streaming-content';
setHtml(asstBubble, roleHtml);
asstBubble.appendChild(contentEl);
thread.appendChild(asstBubble);
}
} else if (d.type === 'chunk' && d.delta) {
accumulated += d.delta;
if (contentEl) { setHtml(contentEl, renderSafeMarkdown(accumulated)); thread.scrollTop = thread.scrollHeight; }
if (sendBtn) sendBtn.textContent = 'Streaming\u2026';
} else if (d.type === 'stream_end') {
if (!d.turn_id || (App._liveStreamTurn && App._liveStreamTurn.turn_id === d.turn_id)) {
App._liveStreamTurn = null;
if (App.page === 'context') App.navigate('context');
}
var wasIntercepted = d.aborted || d.reason === 'aborted' || d.reason === 'content_blocked' || streamIntercepted;
if (asstBubble && !wasIntercepted) {
var endModel = d.model || streamModel;
var finalMeta = '';
if (endModel) finalMeta += '<span class="badge muted" style="font-size:0.5625rem;margin-right:0.375rem">' + esc(endModel) + '</span>';
if (d.tokens_in || d.tokens_out) finalMeta += '<span class="badge muted" style="font-size:0.5rem;margin-right:0.375rem">' + (d.tokens_in||0) + '/' + (d.tokens_out||0) + ' tokens</span>';
var assistantLabelFinal = sessionAssistantLabel(App._activeSession, 'assistant');
setHtml(asstBubble, '<div class="message-role">' + esc(assistantLabelFinal) + (finalMeta ? ' ' + finalMeta : '') + '</div><div>' + renderSafeMarkdown(accumulated) + '</div>');
}
} else if (d.type === 'stream_retry') {
accumulated = '';
streamIntercepted = true;
if (contentEl) setHtml(contentEl, '<em style="color:var(--warning)">Response intercepted — retrying\u2026</em>');
if (sendBtn) sendBtn.textContent = 'Retrying\u2026';
} else if (d.type === 'stream_replace') {
accumulated = d.replacement || '';
streamIntercepted = false; if (contentEl) setHtml(contentEl, renderSafeMarkdown(accumulated));
} else if (d.type === 'stream_blocked') {
accumulated = '';
streamIntercepted = true;
if (contentEl) setHtml(contentEl, '<em style="color:var(--error)">Response blocked by safety filter.</em>');
} else if (d.type === 'error') {
streamIntercepted = true;
if (contentEl) setHtml(contentEl, '<em style="color:var(--error)">' + esc(d.error || 'Provider error') + '</em>');
toast(d.error || 'Stream error');
}
}
}
} catch(err) {
var errMsg2 = err.message || 'Agent failed to respond';
toast(errMsg2);
}
unlockChat();
})();
return;
}
var rosterCard = e.target.closest('.roster-card[data-roster-id]');
if (rosterCard) {
var agentId = rosterCard.getAttribute('data-roster-id');
Promise.all([
api('/api/roster'),
App._loadAvailableModels({ nonBlocking: true, skipFetch: true }),
api('/api/config').catch(function() { return { models: {} }; }),
fetchWithFallback('/api/skills', { skills: [] }, 'skills')
]).then(function(arr) {
var data = arr[0] || { roster: [] };
var availableModels = arr[1] || [];
var cfgModels = (arr[2] && arr[2].models) || {};
var skillsData = (arr[3] && arr[3].data) ? arr[3].data : { skills: [] };
var skillIdByName = {};
(skillsData.skills || []).forEach(function(skill) {
if (!skill || !skill.name || !skill.id) return;
skillIdByName[String(skill.name).toLowerCase()] = String(skill.id);
});
var agent = (data.roster || []).find(function(a) { return (a.name || a.id) === agentId; });
if (!agent) return;
var modal = document.getElementById('roster-modal');
if (!modal) return;
var isCmd = agent.role === 'orchestrator';
var color = agent.color || 'var(--accent)';
var displayName = agent.display_name || agent.name;
var stateColor = agent.state === 'Running' ? '#22c55e' : (agent.state === 'Error' ? '#ef4444' : (agent.state === 'Disabled' ? '#9baad6' : '#eab308'));
var roleLabel = isCmd ? 'ORCHESTRATOR' : (agent.role === 'model-proxy' ? 'MODEL PROXY' : 'SUBAGENT');
var orchestratorOrder = [];
var orchestratorOrderState = [];
var rosterDragModelIndex = null;
function pushOrchestratorModel(v) {
var s = String(v || '').trim();
if (!s) return;
if (orchestratorOrder.indexOf(s) === -1) orchestratorOrder.push(s);
}
if (isCmd) {
pushOrchestratorModel(cfgModels.primary || agent.model);
(cfgModels.fallbacks || []).forEach(pushOrchestratorModel);
if (!orchestratorOrder.length) pushOrchestratorModel(agent.model);
orchestratorOrderState = orchestratorOrder.slice();
}
function renderOrchestratorOrderRows(order) {
if (!order.length) {
return '<div class="card" style="padding:0.6rem;color:var(--muted)">No models added yet.</div>';
}
return order.map(function(name, idx) {
var roleBadge = idx === 0
? '<span class="badge success" style="font-size:0.6rem">Primary</span>'
: '<span class="badge muted" style="font-size:0.6rem">Fallback ' + idx + '</span>';
var makePrimaryBtn = idx === 0 ? '' : '<button class="model-order-btn" data-roster-model-order-primary="' + idx + '">Make primary</button>';
var removeBtn = '<button class="model-order-btn" data-roster-model-order-remove="' + idx + '">Remove</button>';
return '<div class="model-order-item" draggable="true" data-roster-model-order-item="' + idx + '">'
+ '<div class="model-order-handle" title="Drag to reorder">\u22ee\u22ee</div>'
+ '<div class="model-order-name" title="' + esc(name) + '">' + esc(name) + '</div>'
+ roleBadge
+ '<div class="model-order-actions">' + makePrimaryBtn + '</div>'
+ removeBtn
+ '</div>';
}).join('');
}
function rerenderOrchestratorOrderList() {
var listEl = document.getElementById('roster-model-order-list');
if (listEl) setHtml(listEl, renderOrchestratorOrderRows(orchestratorOrderState));
}
var voice = agent.voice || {};
var voiceHtml = '';
if (voice.formality || voice.proactiveness) {
var attrs = [
{ label: 'Formality', val: voice.formality },
{ label: 'Proactive', val: voice.proactiveness },
{ label: 'Verbosity', val: voice.verbosity },
{ label: 'Humor', val: voice.humor },
{ label: 'Domain', val: voice.domain }
];
voiceHtml = '<div style="margin-top:1rem"><div style="font-size:0.75rem;font-weight:600;color:var(--muted);letter-spacing:0.05em;margin-bottom:0.5rem">VOICE PROFILE</div>';
attrs.forEach(function(a) {
if (!a.val) return;
var barVal = { balanced: 50, formal: 85, casual: 20, concise: 30, verbose: 80, suggest: 50, wait: 15, initiative: 90, dry: 25, robotic: 40, witty: 75 };
var pct = barVal[a.val] || 50;
voiceHtml += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:4px;font-size:0.75rem">'
+ '<span style="width:75px;color:var(--muted)">' + a.label + '</span>'
+ '<div style="flex:1;height:6px;background:var(--surface);border-radius:3px;overflow:hidden"><div style="width:' + pct + '%;height:100%;background:' + color + ';border-radius:3px"></div></div>'
+ '<span style="width:60px;text-align:right;color:var(--text)">' + esc(a.val) + '</span></div>';
});
voiceHtml += '</div>';
}
var missionsHtml = '';
if (agent.missions && agent.missions.length > 0) {
missionsHtml = '<div style="margin-top:1rem"><div style="font-size:0.75rem;font-weight:600;color:var(--muted);letter-spacing:0.05em;margin-bottom:0.5rem">MISSIONS</div>';
agent.missions.forEach(function(m) {
var prioColor = m.priority === 'high' ? '#ef4444' : (m.priority === 'medium' ? '#eab308' : '#22c55e');
missionsHtml += '<div style="display:flex;align-items:baseline;gap:0.5rem;margin-bottom:0.4rem;font-size:0.8125rem">'
+ '<span style="width:6px;height:6px;border-radius:50%;background:' + prioColor + ';flex-shrink:0;margin-top:4px"></span>'
+ '<div><strong style="color:var(--text)">' + esc(m.name) + '</strong>'
+ (m.timeframe ? ' <span style="color:var(--muted);font-size:0.7rem">(' + esc(m.timeframe) + ')</span>' : '')
+ '<div style="color:var(--muted);font-size:0.75rem">' + esc(m.description || '') + '</div></div></div>';
});
missionsHtml += '</div>';
}
var skillsHtml = '';
var skills = agent.skills || [];
if (skills.length > 0) {
var breakdown = agent.skill_breakdown || {};
skillsHtml = '<div style="margin-top:1rem"><div style="font-size:0.75rem;font-weight:600;color:var(--muted);letter-spacing:0.05em;margin-bottom:0.5rem">SKILLS (' + skills.length + ')</div>';
var kindColors = { tool: 'var(--accent)', cognitive: '#22c55e', format: '#06b6d4', multimodal: '#f59e0b', agent: '#8b5cf6' };
var hasBreakdown = Object.keys(breakdown).length > 0;
if (hasBreakdown) {
Object.keys(breakdown).forEach(function(kind) {
var items = breakdown[kind];
var kc = kindColors[kind] || 'var(--muted)';
skillsHtml += '<div style="margin-bottom:0.5rem"><span style="font-size:0.7rem;font-weight:600;color:' + kc + ';letter-spacing:0.03em">' + esc(kind.toUpperCase()) + '</span>'
+ '<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:3px">';
items.forEach(function(s) {
var skillName = String(s || '');
var skillId = skillIdByName[skillName.toLowerCase()] || '';
skillsHtml += '<span data-skill-open="' + esc(skillId) + '" data-skill-name="' + esc(skillName) + '" style="cursor:pointer;font-size:0.7rem;padding:1px 6px;border-radius:3px;background:rgba(255,255,255,0.05);color:var(--text);border:1px solid rgba(255,255,255,0.08)">' + esc(skillName) + '</span>';
});
skillsHtml += '</div></div>';
});
} else {
skillsHtml += '<div style="display:flex;flex-wrap:wrap;gap:4px">';
skills.forEach(function(s) {
var name = typeof s === 'string' ? s : (s.name || String(s));
var desc = (typeof s === 'object' && s.description) ? s.description : '';
var skillId = skillIdByName[String(name).toLowerCase()] || '';
skillsHtml += '<span data-skill-open="' + esc(skillId) + '" data-skill-name="' + esc(name) + '" title="' + esc(desc) + '" style="cursor:pointer;font-size:0.7rem;padding:1px 6px;border-radius:3px;background:rgba(255,255,255,0.05);color:var(--text);border:1px solid rgba(255,255,255,0.08)">' + esc(name) + '</span>';
});
skillsHtml += '</div>';
}
skillsHtml += '</div>';
}
var rulesHtml = '';
if (agent.firmware_rules && agent.firmware_rules.length > 0) {
rulesHtml = '<div style="margin-top:1rem"><div style="font-size:0.75rem;font-weight:600;color:var(--muted);letter-spacing:0.05em;margin-bottom:0.5rem">FIRMWARE RULES</div>';
agent.firmware_rules.forEach(function(r) {
var rColor = r.type === 'must' ? '#22c55e' : '#ef4444';
var rLabel = r.type === 'must' ? 'MUST' : 'MUST NOT';
rulesHtml += '<div style="display:flex;gap:0.5rem;margin-bottom:0.3rem;font-size:0.75rem;align-items:baseline">'
+ '<span style="font-weight:700;color:' + rColor + ';font-size:0.65rem;flex-shrink:0">' + rLabel + '</span>'
+ '<span style="color:var(--text)">' + esc(r.rule) + '</span></div>';
});
rulesHtml += '</div>';
}
var statsHtml = '';
if (agent.stats) {
var s = agent.stats;
statsHtml = '<div style="margin-top:1rem;display:grid;grid-template-columns:repeat(2,1fr);gap:0.5rem">';
var statItems = [
{ label: 'Subagents', val: s.subordinate_count },
{ label: 'Running', val: s.running_subordinates },
{ label: 'Total Skills', val: s.total_skills },
{ label: 'Enabled Skills', val: s.enabled_skills }
];
statItems.forEach(function(si) {
if (si.val == null) return;
statsHtml += '<div style="text-align:center;padding:0.5rem;background:var(--surface);border-radius:var(--radius)">'
+ '<div style="font-size:1.25rem;font-weight:700;color:' + color + '">' + si.val + '</div>'
+ '<div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em">' + si.label + '</div></div>';
});
statsHtml += '</div>';
}
if (agent.session_count != null && !agent.stats) {
statsHtml = '<div style="margin-top:1rem;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem">'
+ '<div style="text-align:center;padding:0.5rem;background:var(--surface);border-radius:var(--radius);flex:1">'
+ '<div style="font-size:1.25rem;font-weight:700;color:' + color + '">' + agent.session_count + '</div>'
+ '<div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em">Sessions</div></div>'
+ '<div style="text-align:center;padding:0.5rem;background:var(--surface);border-radius:var(--radius);flex:1">'
+ '<div style="font-size:0.82rem;font-weight:700;color:' + color + '">' + esc(formatTimestampLabel(agent.last_used_at)) + '</div>'
+ '<div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em">Last Used</div></div></div>';
}
var subordinates = agent.subordinates || [];
var proxyLookup = {};
(data.model_proxies || []).forEach(function(a) {
if (!a) return;
if (a.id) proxyLookup[String(a.id)] = true;
if (a.name) proxyLookup[String(a.name)] = true;
});
subordinates = subordinates.filter(function(sid) { return !proxyLookup[String(sid)]; });
var subsHtml = '';
if (subordinates.length > 0) {
subsHtml = '<div style="margin-top:1rem"><div style="font-size:0.75rem;font-weight:600;color:var(--muted);letter-spacing:0.05em;margin-bottom:0.5rem">SUBAGENTS (' + subordinates.length + ')</div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:4px">';
subordinates.forEach(function(sid) {
subsHtml += '<span class="badge" style="cursor:pointer" data-roster-sub="' + esc(sid) + '">' + esc(sid) + '</span>';
});
subsHtml += '</div></div>';
}
var html = '<div style="max-width:520px;margin:auto;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden">'
+ '<div style="background:linear-gradient(135deg,' + color + '22,' + color + '08);padding:1.5rem;border-bottom:1px solid var(--border);position:relative">'
+ '<button id="roster-modal-close" style="position:absolute;top:12px;right:12px;background:none;border:none;color:var(--muted);cursor:pointer;font-size:1.2rem">\u2715</button>'
+ '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.25rem">'
+ '<span style="display:inline-block;width:10px;height:10px;border-radius:50%;background:' + stateColor + '"></span>'
+ '<span style="font-size:0.65rem;font-weight:600;letter-spacing:0.05em;color:' + color + '">' + roleLabel + '</span>'
+ '</div>'
+ '<div style="font-size:1.5rem;font-weight:800;color:var(--text);margin-bottom:0.25rem">' + esc(displayName) + '</div>'
+ '<div style="font-family:var(--mono);font-size:0.75rem;color:var(--muted)">' + esc(agent.name || '') + '</div>'
+ '<div id="roster-model-display" style="display:flex;align-items:center;gap:0.5rem;margin-top:0.5rem">'
+ '<span style="font-family:var(--mono);font-size:0.75rem;color:' + color + '">' + esc(agent.model || '—') + '</span>'
+ '<button id="roster-change-model-btn" style="font-size:0.6rem;padding:1px 6px;border-radius:3px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);color:var(--muted);cursor:pointer" title="Change model">change</button>'
+ (!isCmd ? '<button id="roster-edit-subagent-btn" style="font-size:0.6rem;padding:1px 6px;border-radius:3px;background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.15);color:var(--muted);cursor:pointer" title="Edit agent">edit</button>' : '')
+ '</div>'
+ '<div id="roster-model-editor" style="display:none;margin-top:0.5rem">'
+ (function() {
var allowControlModes = !isCmd && String(agent.role || '').toLowerCase() !== 'model-proxy';
var opts = App._renderModelOptionsHtml({
discoveredModels: availableModels,
roster: data.roster || [],
config: { models: cfgModels },
selected: orchestratorOrderState.concat([agent.model || '']),
includeControlModes: allowControlModes
});
if (isCmd) {
return '<div style="font-size:0.6875rem;color:var(--muted);margin-bottom:0.375rem">Model order (top = primary, next = fallback sequence)</div>'
+ '<div class="model-order-list" id="roster-model-order-list">' + renderOrchestratorOrderRows(orchestratorOrderState) + '</div>'
+ '<div style="display:flex;gap:0.4rem;align-items:center;margin-top:6px">'
+ '<select id="roster-model-select" style="flex:1;font-size:0.75rem;font-family:var(--mono);padding:4px 6px;border-radius:4px;background:var(--surface);color:var(--text);border:1px solid var(--border)">'
+ '<option value="">-- quick add known model --</option>' + opts + '</select>'
+ '<button id="roster-model-order-add" class="btn secondary roster-model-add-btn" style="font-size:0.7rem;padding:3px 8px">Add</button>'
+ '</div>'
+ '<div style="display:flex;gap:0.4rem;align-items:center;margin-top:4px">'
+ '<input id="roster-model-custom" type="text" placeholder="or type provider/model and click Add" style="flex:1;font-size:0.75rem;font-family:var(--mono);padding:4px 6px;border-radius:4px;background:var(--surface);color:var(--text);border:1px solid var(--border)">'
+ '<button class="btn secondary roster-model-add-btn" style="font-size:0.7rem;padding:3px 8px">Add</button>'
+ '</div>'
+ '<div style="display:flex;gap:0.4rem;margin-top:6px">'
+ '<button id="roster-model-save" class="btn" style="font-size:0.7rem;padding:3px 10px">Apply</button>'
+ '<button id="roster-model-cancel" class="btn secondary" style="font-size:0.7rem;padding:3px 10px">Cancel</button>'
+ '</div>';
}
var selectedModel = String(agent.model || '');
var singleOpts = App._buildModelOptionEntries({
discoveredModels: availableModels,
roster: data.roster || [],
config: { models: cfgModels },
selected: [selectedModel],
includeControlModes: allowControlModes
}).map(function(entry) {
var sel = entry.value === selectedModel ? ' selected' : '';
return '<option value="' + esc(entry.value) + '"' + sel + '>' + esc(entry.label) + '</option>';
}).join('');
return '<div style="display:flex;gap:0.4rem;align-items:center">'
+ '<select id="roster-model-select" style="flex:1;font-size:0.75rem;font-family:var(--mono);padding:4px 6px;border-radius:4px;background:var(--surface);color:var(--text);border:1px solid var(--border)">'
+ '<option value="">-- select model --</option>' + singleOpts + '</select>'
+ '</div>'
+ '<div style="display:flex;gap:0.4rem;align-items:center;margin-top:4px">'
+ '<input id="roster-model-custom" type="text" placeholder="or type provider/model" style="flex:1;font-size:0.75rem;font-family:var(--mono);padding:4px 6px;border-radius:4px;background:var(--surface);color:var(--text);border:1px solid var(--border)">'
+ '</div>'
+ '<div style="display:flex;gap:0.4rem;margin-top:6px">'
+ '<button id="roster-model-save" class="btn" style="font-size:0.7rem;padding:3px 10px">Apply</button>'
+ '<button id="roster-model-cancel" class="btn secondary" style="font-size:0.7rem;padding:3px 10px">Cancel</button>'
+ '</div>';
})()
+ '</div>'
+ (agent.description ? '<div style="font-size:0.8125rem;color:var(--text);margin-top:0.75rem;line-height:1.4;max-height:4.5em;overflow:hidden;text-overflow:ellipsis">' + esc(agent.description) + '</div>' : '')
+ '</div>'
+ '<div style="padding:1.25rem">'
+ voiceHtml + missionsHtml + skillsHtml + rulesHtml + statsHtml + subsHtml
+ (agent.supervisor ? '<div style="margin-top:1rem;font-size:0.75rem;color:var(--muted)">Reports to: <strong style="color:var(--text)">' + esc(agent.supervisor) + '</strong></div>' : '')
+ '</div></div>';
modal.style.display = 'flex';
modal.style.alignItems = 'flex-start';
modal.style.justifyContent = 'center';
setHtml(modal, html);
if (isCmd) rerenderOrchestratorOrderList();
modal.onclick = function(ev) {
if (ev.target === modal || ev.target.closest('#roster-modal-close')) {
modal.style.display = 'none';
}
var subLink = ev.target.closest('[data-roster-sub]');
if (subLink) {
var subId = subLink.getAttribute('data-roster-sub');
modal.style.display = 'none';
var subCard = document.querySelector('.roster-card[data-roster-id="' + subId + '"]');
if (subCard) subCard.click();
}
if (ev.target.closest('#roster-change-model-btn')) {
var disp = document.getElementById('roster-model-display');
var ed = document.getElementById('roster-model-editor');
if (disp) disp.style.display = 'none';
if (ed) ed.style.display = 'block';
}
if (ev.target.closest('#roster-edit-subagent-btn')) {
App._agentsTab = 'list';
App._pendingSubagentEdit = {
name: agent.name || '',
editing: agent.name || '',
display: agent.display_name || '',
model: agent.model || '',
fallbacks: (agent.fallback_models || []).join(','),
role: agent.role || 'subagent',
skills: (agent.skills || []).join(','),
desc: agent.description || ''
};
modal.style.display = 'none';
App.navigate('agents');
return;
}
if (ev.target.closest('#roster-model-cancel')) {
var disp2 = document.getElementById('roster-model-display');
var ed2 = document.getElementById('roster-model-editor');
if (disp2) disp2.style.display = 'flex';
if (ed2) ed2.style.display = 'none';
}
if (ev.target.closest('.roster-model-add-btn')) {
var selAdd = document.getElementById('roster-model-select');
var customAdd = document.getElementById('roster-model-custom');
var toAdd = (customAdd && customAdd.value.trim()) || (selAdd && selAdd.value) || '';
if (!toAdd) { toast('Select or enter a model'); return; }
if (orchestratorOrderState.indexOf(toAdd) !== -1) { toast('Model already listed'); return; }
orchestratorOrderState.push(toAdd);
rerenderOrchestratorOrderList();
if (customAdd) customAdd.value = '';
if (selAdd) selAdd.value = '';
}
var removeOrderBtn = ev.target.closest('[data-roster-model-order-remove]');
if (removeOrderBtn) {
var removeIdx = Number(removeOrderBtn.getAttribute('data-roster-model-order-remove'));
if (!isNaN(removeIdx) && removeIdx >= 0 && removeIdx < orchestratorOrderState.length) {
orchestratorOrderState.splice(removeIdx, 1);
rerenderOrchestratorOrderList();
}
}
var primaryOrderBtn = ev.target.closest('[data-roster-model-order-primary]');
if (primaryOrderBtn) {
var promoteIdx = Number(primaryOrderBtn.getAttribute('data-roster-model-order-primary'));
if (!isNaN(promoteIdx) && promoteIdx > 0 && promoteIdx < orchestratorOrderState.length) {
var promoted = orchestratorOrderState.splice(promoteIdx, 1)[0];
orchestratorOrderState.unshift(promoted);
rerenderOrchestratorOrderList();
}
}
if (ev.target.closest('#roster-model-save')) {
var sel = document.getElementById('roster-model-select');
var custom = document.getElementById('roster-model-custom');
var agentKey = agent.name || agent.id;
var payload;
if (isCmd) {
if (!orchestratorOrderState.length) { toast('Add at least one model in order list'); return; }
payload = { model: orchestratorOrderState[0], fallbacks: orchestratorOrderState.slice(1) };
} else {
var newModel = (custom && custom.value.trim()) || (sel && sel.value) || '';
if (!newModel) { toast('Select or enter a model'); return; }
payload = { model: newModel };
}
api('/api/roster/' + encodeURIComponent(agentKey) + '/model', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
}).then(function(resp) {
if (isCmd && Array.isArray(resp.model_order)) {
toast('Model order updated (' + resp.model_order.length + ' total)');
} else {
toast('Model changed: ' + (resp.old_model || '?') + ' \u2192 ' + resp.new_model);
}
modal.style.display = 'none';
App.navigate('agents');
}).catch(function(err) {
toast(err.message || 'Failed to change model');
});
}
};
modal.ondragstart = function(ev) {
if (!isCmd) return;
var item = ev.target.closest('[data-roster-model-order-item]');
if (!item) return;
rosterDragModelIndex = Number(item.getAttribute('data-roster-model-order-item'));
item.classList.add('dragging');
if (ev.dataTransfer) {
ev.dataTransfer.effectAllowed = 'move';
ev.dataTransfer.setData('text/plain', String(rosterDragModelIndex));
}
};
modal.ondragover = function(ev) {
if (!isCmd) return;
var item = ev.target.closest('[data-roster-model-order-item]');
if (!item) return;
ev.preventDefault();
if (ev.dataTransfer) ev.dataTransfer.dropEffect = 'move';
modal.querySelectorAll('.model-order-item.drop-target').forEach(function(el) { el.classList.remove('drop-target'); });
item.classList.add('drop-target');
};
modal.ondrop = function(ev) {
if (!isCmd) return;
var item = ev.target.closest('[data-roster-model-order-item]');
if (!item) return;
ev.preventDefault();
var toIdx = Number(item.getAttribute('data-roster-model-order-item'));
modal.querySelectorAll('.model-order-item.drop-target,.model-order-item.dragging').forEach(function(el) { el.classList.remove('drop-target'); el.classList.remove('dragging'); });
var fromIdx = rosterDragModelIndex;
rosterDragModelIndex = null;
if (isNaN(fromIdx) || isNaN(toIdx) || fromIdx === toIdx || fromIdx < 0 || toIdx < 0 || fromIdx >= orchestratorOrderState.length || toIdx >= orchestratorOrderState.length) return;
var moved = orchestratorOrderState.splice(fromIdx, 1)[0];
orchestratorOrderState.splice(toIdx, 0, moved);
rerenderOrchestratorOrderList();
};
modal.ondragend = function() {
if (!isCmd) return;
modal.querySelectorAll('.model-order-item.drop-target,.model-order-item.dragging').forEach(function(el) { el.classList.remove('drop-target'); el.classList.remove('dragging'); });
rosterDragModelIndex = null;
};
});
return;
}
if (e.target.closest('#btn-reload-skills')) {
api('/api/skills/reload', { method: 'POST' }).then(function() { toast('Skills reloaded'); App.refreshSkills(); }).catch(function(err) { toast(err.message || 'Failed'); });
return;
}
if (e.target.closest('#btn-create-skill')) {
App._openSkillEditorModal(null, null);
return;
}
var skillEditOpen = e.target.closest('[data-skill-edit-open]');
if (skillEditOpen) {
var skillEditOpenId = skillEditOpen.getAttribute('data-skill-edit-open') || '';
if (!skillEditOpenId) { toast('Missing skill id'); return; }
api('/api/skills/' + encodeURIComponent(skillEditOpenId)).then(function(skill) {
App._openSkillEditorModal(skillEditOpenId, skill.source_content || '');
}).catch(function(err) {
toast(err.message || 'Failed to load skill');
});
return;
}
if (e.target.closest('#btn-catalog-refresh')) {
App.refreshSkills();
toast('Catalog refreshed');
return;
}
var catalogInstallBtn = e.target.closest('#btn-catalog-install');
var catalogActivateBtn = e.target.closest('#btn-catalog-activate');
var catalogInstallActivateBtn = e.target.closest('#btn-catalog-install-activate');
if (catalogInstallBtn || catalogActivateBtn || catalogInstallActivateBtn) {
var selectedRows = Array.prototype.slice.call(document.querySelectorAll('.cat-skill-check:checked'))
.map(function(el) {
return {
name: (el.value || '').trim(),
installed: el.getAttribute('data-installed') === '1'
};
})
.filter(function(v) { return !!v.name; });
if (selectedRows.length === 0) {
toast('Select at least one catalog skill');
return;
}
if (catalogActivateBtn) {
var selectedForActivate = selectedRows.map(function(row) { return row.name; });
api('/api/skills/catalog/activate', {
method: 'POST',
body: { skills: selectedForActivate }
}).then(function() {
toast('Activated selected catalog skills');
App.refreshSkills();
}).catch(function(err) {
toast(err.message || 'Catalog activation failed');
});
return;
}
var activateOnInstall = !!catalogInstallActivateBtn;
var selectedForInstall = selectedRows.filter(function(row) { return !row.installed; }).map(function(row) { return row.name; });
if (selectedForInstall.length === 0) {
toast('All selected catalog skills are already installed');
return;
}
var payload = { skills: selectedForInstall, activate: activateOnInstall };
api('/api/skills/catalog/install', { method: 'POST', body: payload })
.then(function(resp) {
var installed = (resp && resp.installed) ? resp.installed.length : selectedForInstall.length;
toast('Installed ' + installed + ' skill(s)' + (activateOnInstall ? ' and reloaded' : ''));
if (!activateOnInstall) {
return api('/api/skills/reload', { method: 'POST' }).then(function() {
toast('Skills reloaded');
});
}
})
.then(function() {
App.refreshSkills();
})
.catch(function(err) {
toast(err.message || 'Catalog install failed');
});
return;
}
var pluginInstallBtn = e.target.closest('[data-plugin-install]');
if (pluginInstallBtn) {
var pluginName = pluginInstallBtn.getAttribute('data-plugin-install');
if (!pluginName) { toast('Missing plugin name'); return; }
pluginInstallBtn.disabled = true;
pluginInstallBtn.textContent = 'Installing…';
api('/api/skills/catalog/install', { method: 'POST', body: JSON.stringify({ skills: [pluginName], activate: true }) })
.then(function(resp) {
toast('Installed plugin: ' + pluginName + (resp.tools ? ' (' + resp.tools + ' tools)' : ''));
App.refreshSkills();
})
.catch(function(err) {
toast(err.message || 'Plugin install failed');
})
.finally(function() {
pluginInstallBtn.disabled = false;
pluginInstallBtn.textContent = 'Install';
});
return;
}
var skillDelete = e.target.closest('[data-skill-delete]');
if (skillDelete) {
var skillId = skillDelete.getAttribute('data-skill-delete');
var skillName = skillDelete.getAttribute('data-skill-name') || '';
if (!skillId || !skillName) { toast('Missing skill metadata'); return; }
App._openSkillDeleteModal(skillId, skillName);
return;
}
var skillOpen = e.target.closest('[data-skill-open]');
if (skillOpen) {
if (!e.target.closest('[data-skill-delete]') && !e.target.closest('[data-skill-toggle]') && !e.target.closest('[data-skill-edit-open]')) {
var skillOpenId = skillOpen.getAttribute('data-skill-open') || '';
var skillOpenName = skillOpen.getAttribute('data-skill-name') || '';
App._openSkillDetailModal(skillOpenId, skillOpenName);
return;
}
}
var skillToggle = e.target.closest('[data-skill-toggle]');
if (skillToggle) {
var skillIdToggle = skillToggle.getAttribute('data-skill-toggle') || '';
var skillNameToggle = skillToggle.getAttribute('data-skill-name') || skillIdToggle;
var nextChecked = !!skillToggle.checked;
var prevChecked = !nextChecked;
if (!skillIdToggle) {
skillToggle.checked = prevChecked;
toast('Missing skill id');
return;
}
if (skillToggle.getAttribute('data-skill-pending') === '1') return;
skillToggle.setAttribute('data-skill-pending', '1');
skillToggle.disabled = true;
api('/api/skills/' + encodeURIComponent(skillIdToggle) + '/toggle', { method: 'PUT' })
.then(function(resp) {
skillToggle.checked = !!resp.enabled;
toast((resp.enabled ? 'Skill enabled: ' : 'Skill disabled: ') + skillNameToggle);
App.navigate('skills');
})
.catch(function(err) {
skillToggle.checked = prevChecked;
toast(err.message || 'Failed to toggle skill');
})
.finally(function() {
skillToggle.removeAttribute('data-skill-pending');
skillToggle.disabled = false;
});
return;
}
if (e.target.closest('#copy-address')) {
var pre = document.getElementById('wallet-addr');
if (pre && pre.textContent) navigator.clipboard.writeText(pre.textContent).then(function() { toast('Address copied'); });
return;
}
var revenueTaskBtn = e.target.closest('[data-revenue-task-action][data-opportunity-id][data-revenue-task-kind]');
if (revenueTaskBtn) {
var taskKind = revenueTaskBtn.getAttribute('data-revenue-task-kind');
var taskAction = revenueTaskBtn.getAttribute('data-revenue-task-action');
var opportunityId = revenueTaskBtn.getAttribute('data-opportunity-id');
if (!taskKind || !taskAction || !opportunityId) { toast('Missing revenue task metadata'); return; }
var basePath = taskKind === 'tax' ? '/api/services/tax-payouts/' : '/api/services/swaps/';
var request = { method: 'POST' };
if (taskAction === 'confirm') {
var txHash = window.prompt('Enter the transaction hash to confirm:', '');
if (!txHash || !txHash.trim()) return;
request.headers = { 'Content-Type': 'application/json' };
request.body = JSON.stringify({ tx_hash: txHash.trim() });
} else if (taskAction === 'fail') {
var reason = window.prompt('Enter the failure reason:', '');
if (!reason || !reason.trim()) return;
request.headers = { 'Content-Type': 'application/json' };
request.body = JSON.stringify({ reason: reason.trim() });
} else if (taskAction === 'submit') {
var calldata = window.prompt('Enter calldata (0x...) for submission:', '');
if (!calldata || !calldata.trim()) return;
var contractAddress = window.prompt('Optional contract address override (leave blank to use configured task source):', '') || '';
request.headers = { 'Content-Type': 'application/json' };
request.body = JSON.stringify({
calldata: calldata.trim(),
contract_address: contractAddress.trim() || null
});
}
api(basePath + encodeURIComponent(opportunityId) + '/' + taskAction, request)
.then(function(resp) {
var msg = taskKind === 'tax' ? 'Tax payout ' : 'Swap ';
msg += taskAction + ' completed';
if (resp && resp.receipt_status) msg += ' — receipt ' + resp.receipt_status;
if (resp && resp.tx_hash) msg += ' — ' + String(resp.tx_hash).slice(0, 18) + '…';
toast(msg);
App.navigate('wallet');
})
.catch(function(err) {
toast(err.message || ('Revenue task ' + taskAction + ' failed'));
});
return;
}
if (e.target.closest('#memory-search-btn')) {
var q = document.getElementById('memory-search-q'); var query = (q && q.value) || '';
if (!query.trim()) { toast('Enter a search query'); return; }
api('/api/memory/search?q=' + encodeURIComponent(query)).then(function(r) {
var res = document.getElementById('memory-search-results'); if (!res) return;
var results = r.results || [];
setHtml(res, results.map(function(e) { return '<div class="card" style="margin-bottom:0.5rem"><div class="card-mono">' + esc(e.content || JSON.stringify(e)) + '</div><span class="badge muted">' + esc(e.type || '') + '</span></div>'; }).join('') || '<p style="color:var(--muted)">No results</p>');
}).catch(function(err) { var res = document.getElementById('memory-search-results'); if (res) setHtml(res, '<p style="color:var(--error)">' + esc(err.message || 'Search failed') + '</p>'); });
return;
}
var tabBtn = e.target.closest('.tabs button[data-tab]');
if (tabBtn) { App._memoryTab = tabBtn.getAttribute('data-tab'); App._memoryCategory = ''; App._memorySessionId = ''; App.navigate('memory'); return; }
var memCatBtn = e.target.closest('[data-mem-cat]');
if (memCatBtn) { App._memoryCategory = memCatBtn.getAttribute('data-mem-cat'); App._memoryPage = 0; App.navigate('memory'); return; }
var memSessBtn = e.target.closest('[data-mem-session]');
if (memSessBtn) { App._memorySessionId = memSessBtn.getAttribute('data-mem-session'); App.navigate('memory'); return; }
var effTabBtn = e.target.closest('[data-eff-tab]');
if (effTabBtn) { App._effTab = effTabBtn.getAttribute('data-eff-tab') || 'performance'; App.navigate('efficiency'); return; }
var effPeriodBtn = e.target.closest('[data-eff-period]');
if (effPeriodBtn) { App._effPeriod = effPeriodBtn.getAttribute('data-eff-period'); App.navigate('efficiency'); return; }
var agentsTab = e.target.closest('[data-agents-tab]');
if (agentsTab) { App._agentsTab = agentsTab.getAttribute('data-agents-tab'); App.navigate('agents'); return; }
var skillsTab = e.target.closest('[data-skills-tab]');
if (skillsTab) { App._skillsTab = skillsTab.getAttribute('data-skills-tab'); App.navigate('skills'); return; }
var metricsTabBtn = e.target.closest('[data-metrics-tab]');
if (metricsTabBtn) { App._metricsTab = metricsTabBtn.getAttribute('data-metrics-tab') || 'overview'; App.navigate('metrics'); return; }
var flowLink = e.target.closest('.trace-flow-link');
if (flowLink) {
e.preventDefault();
var flowTurnId = flowLink.getAttribute('data-flow-turn-id');
if (flowTurnId && App.renderTraceFlow) {
App._flowTurnId = flowTurnId;
App.renderTraceFlow(flowTurnId).then(function(html) {
var content = document.getElementById('content');
if (content) setHtml(content, html);
});
}
return;
}
var flowBackBtn = e.target.closest('#flow-back-btn');
if (flowBackBtn) { App._flowTurnId = null; App.navigate('metrics'); return; }
var svgNode = e.target.closest('.flow-svg-node');
if (svgNode) {
var idx = parseInt(svgNode.getAttribute('data-flow-idx'), 10);
var dataEl = document.getElementById('flow-node-data');
var popEl = document.getElementById('flow-popover');
var popContent = document.getElementById('flow-popover-content');
if (dataEl && popEl && popContent && !isNaN(idx)) {
try {
var allNodes = JSON.parse(dataEl.textContent);
var n = allNodes[idx];
if (!n) return;
var detail = n.detail || {};
var html = '<div style="font-weight:700;margin-bottom:0.5rem">' + esc(n.label || n.id) + '</div>'
+ '<div style="font-size:0.75rem;color:var(--muted);margin-bottom:0.5rem">'
+ esc((n.duration_ms || 0) + 'ms') + ' · ' + esc(n.status || 'executed') + '</div>';
var keys = Object.keys(detail);
if (keys.length) {
keys.forEach(function(k) {
var v = detail[k];
var vStr = typeof v === 'object' ? JSON.stringify(v, null, 2) : String(v);
html += '<div style="margin-bottom:0.35rem"><span style="color:var(--accent);font-weight:600">' + esc(k) + ':</span> '
+ '<span style="font-family:var(--font-mono);font-size:0.6875rem;word-break:break-all">' + esc(vStr.length > 300 ? vStr.substring(0, 297) + '...' : vStr) + '</span></div>';
});
} else {
html += '<div style="color:var(--muted);font-style:italic">No annotations for this stage</div>';
}
html += '<div style="margin-top:0.5rem;text-align:right"><button class="btn secondary" id="flow-popover-close" style="font-size:0.6875rem;padding:0.2rem 0.5rem">Close</button></div>';
setHtml(popContent, html);
var rect = svgNode.getBoundingClientRect();
var container = document.getElementById('flow-svg-container');
var cRect = container ? container.getBoundingClientRect() : rect;
popEl.style.display = 'block';
popEl.style.left = (rect.right - cRect.left + 12) + 'px';
popEl.style.top = (rect.top - cRect.top - 10) + 'px';
} catch(ex) {}
}
return;
}
var popClose = e.target.closest('#flow-popover-close');
if (popClose) { var pop = document.getElementById('flow-popover'); if (pop) pop.style.display = 'none'; return; }
if (!e.target.closest('#flow-popover') && !e.target.closest('.flow-svg-node')) {
var pop2 = document.getElementById('flow-popover'); if (pop2) pop2.style.display = 'none';
}
var traceRow = e.target.closest('.traces-turn-row');
if (traceRow) {
var tId = traceRow.getAttribute('data-turn-id');
var wfRow = document.getElementById('wf-' + tId);
if (!wfRow) return;
var isOpen = wfRow.style.display !== 'none';
if (isOpen) { wfRow.style.display = 'none'; return; }
wfRow.style.display = '';
var wfContent = wfRow.querySelector('.wf-content');
if (wfContent && wfContent.getAttribute('data-loaded')) return;
api('/api/observability/traces/' + encodeURIComponent(tId) + '/waterfall').catch(function() { return { stages: [] }; }).then(function(wd) {
var stages = wd.waterfall || wd.stages || [];
if (!stages.length) { if (wfContent) setHtml(wfContent, '<span style="color:var(--muted)">No stages recorded.</span>'); return; }
var maxMs = Math.max.apply(null, stages.map(function(s) { return s.duration_ms || 1; }));
var svgH = stages.length * 28 + 10;
var html = '<svg width="100%" height="' + svgH + '" style="display:block">';
stages.forEach(function(s, i) {
var w = Math.max(((s.duration_ms || 0) / maxMs) * 75, 2);
var color = s.outcome === 'Ok' ? '#22c55e' : s.outcome === 'Skipped' ? '#6b7280' : '#ef4444';
var y = i * 28 + 5;
html += '<rect x="20%" y="' + y + '" width="' + w + '%" height="22" rx="3" fill="' + color + '" opacity="0.8"/>';
html += '<text x="2%" y="' + (y + 15) + '" font-size="11" fill="var(--text)" font-family="var(--mono)">' + esc(s.name || '') + '</text>';
html += '<text x="' + (22 + w) + '%" y="' + (y + 15) + '" font-size="10" fill="var(--muted)" font-family="var(--mono)">' + (s.duration_ms || 0) + 'ms</text>';
});
html += '</svg>';
var tsAnnotations = {};
stages.forEach(function(s) {
var ann = s.annotations || {};
Object.keys(ann).forEach(function(k) {
if (k.indexOf('task_state.') === 0 || k.indexOf('delegation.') === 0) {
tsAnnotations[k] = ann[k];
}
});
});
var tsClassification = tsAnnotations['task_state.inputs.classification'] || tsAnnotations['delegation.task_mode'];
var tsAction = tsAnnotations['task_state.selected.action'] || tsAnnotations['delegation.planner_next_action'];
var tsRationale = tsAnnotations['task_state.selected.rationale'] || tsAnnotations['delegation.planner_rationale'];
var tsConfidence = tsAnnotations['task_state.candidates.count'];
if (tsClassification != null || tsAction != null || tsRationale != null) {
var plannerCard = '<div style="margin-top:0.75rem;padding:0.65rem 0.85rem;background:var(--surface-2);border:1px solid var(--border-ghost);border-left:3px solid var(--accent);border-radius:var(--radius)">'
+ '<div style="font-family:var(--font-headline);font-size:0.5625rem;font-weight:700;letter-spacing:0.15em;text-transform:uppercase;color:var(--accent);margin-bottom:0.5rem">Planner Decision</div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:0.75rem 1.5rem">'
+ (tsClassification != null ? '<div><div style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.15rem">Classification</div><div style="font-size:0.8125rem;color:var(--text);font-weight:600">' + esc(String(tsClassification)) + '</div></div>' : '')
+ (tsAction != null ? '<div><div style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.15rem">Selected Action</div><div style="font-size:0.8125rem;color:var(--tertiary);font-weight:600;font-family:var(--font-mono)">' + esc(String(tsAction)) + '</div></div>' : '')
+ (tsConfidence != null ? '<div><div style="font-size:0.625rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.1em;margin-bottom:0.15rem">Candidates</div><div style="font-size:0.8125rem;color:var(--secondary);font-weight:600">' + esc(String(tsConfidence)) + '</div></div>' : '')
+ '</div>'
+ (tsRationale != null ? '<div style="margin-top:0.5rem;font-size:0.75rem;color:var(--muted);line-height:1.5;border-top:1px solid var(--border-ghost);padding-top:0.4rem"><span style="color:var(--outline);font-size:0.625rem;text-transform:uppercase;letter-spacing:0.1em;font-weight:600">Rationale — </span>' + esc(String(tsRationale)) + '</div>' : '')
+ '</div>';
html += plannerCard;
}
if (wfContent) { setHtml(wfContent, html); wfContent.setAttribute('data-loaded', '1'); }
});
return;
}
if (e.target.closest('#flight-session-select')) {
var sel = document.getElementById('flight-session-select');
if (sel) {
App._flightSessionId = sel.value;
App._flightTurnId = ''; App.navigate('metrics');
}
return;
}
var turnPick = e.target.closest('.flight-turn-pick');
if (turnPick) {
e.preventDefault();
App._flightTurnId = turnPick.getAttribute('data-turn-id') || '';
App.navigate('metrics');
return;
}
if (e.target.closest('#flight-load-btn')) {
var flInput = document.getElementById('flight-turn-input');
var flTurn = (flInput && flInput.value || '').trim();
if (!flTurn) { toast('Enter a turn ID'); return; }
App._flightTurnId = flTurn;
App.navigate('metrics');
return;
}
var settingsMode = e.target.closest('[data-settings-mode]');
if (settingsMode) {
var newMode = settingsMode.getAttribute('data-settings-mode');
if (newMode === 'raw' && App._settingsMode !== 'raw') App._settingsRawText = null;
if (App._settingsMode === 'raw' && newMode !== 'raw') App._settingsRawText = null;
if (newMode === 'models') App._initModelOrderFromDraft();
App._settingsMode = newMode; App.navigate('settings'); return;
}
if (e.target.closest('#hints-reset-dismissed')) {
clearHintDismissals();
setHintsEnabled(true);
try { window.localStorage.setItem(HINTS_PROMPTED_KEY, '1'); } catch (_) {}
toast('Dismissed hints reset');
App.navigate('settings');
return;
}
if (e.target.closest('#trusted-sender-add')) {
var senderInput = document.getElementById('trusted-sender-input');
var sender = (senderInput && senderInput.value || '').trim();
if (!sender) { toast('Enter a sender id first'); return; }
var draftTs = App._getSettingsDraft();
if (!draftTs.channels) draftTs.channels = {};
var curTs = Array.isArray(draftTs.channels.trusted_sender_ids) ? draftTs.channels.trusted_sender_ids.slice() : [];
if (curTs.indexOf(sender) !== -1) { toast('Sender already trusted'); return; }
curTs.push(sender);
draftTs.channels.trusted_sender_ids = curTs;
if (senderInput) senderInput.value = '';
App._settingsDirty = JSON.stringify(draftTs) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
App.navigate('settings');
return;
}
var removeTrustedSenderBtn = e.target.closest('.trusted-sender-remove[data-trusted-sender]');
if (removeTrustedSenderBtn) {
var senderVal = removeTrustedSenderBtn.getAttribute('data-trusted-sender');
var draftTr = App._getSettingsDraft();
if (!draftTr.channels) draftTr.channels = {};
var curTr = Array.isArray(draftTr.channels.trusted_sender_ids) ? draftTr.channels.trusted_sender_ids.slice() : [];
draftTr.channels.trusted_sender_ids = curTr.filter(function(v) { return String(v) !== String(senderVal); });
App._settingsDirty = JSON.stringify(draftTr) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
App.navigate('settings');
return;
}
var CH_META = {
telegram: { listField: 'allowed_chat_ids', validate: function(v) { return /^-?[0-9]+$/.test(v) ? null : 'Chat id must be an integer'; }, coerce: Number },
whatsapp: { listField: 'allowed_numbers', validate: null, coerce: String },
signal: { listField: 'allowed_numbers', validate: null, coerce: String },
discord: { listField: 'allowed_guild_ids', validate: null, coerce: String },
email: { listField: 'allowed_senders', validate: null, coerce: String }
};
var chAddBtn = e.target.closest('.ch-list-add[data-ch-name]');
if (chAddBtn) {
var chName = chAddBtn.getAttribute('data-ch-name');
var chField = chAddBtn.getAttribute('data-ch-field');
var chMeta = CH_META[chName] || { listField: chField, validate: null, coerce: String };
var chInput = document.querySelector('.ch-list-input[data-ch-name="' + chName + '"][data-ch-field="' + chField + '"]');
var chRaw = (chInput && chInput.value || '').trim();
if (!chRaw) { toast('Enter a value first'); return; }
if (chMeta.validate) { var err = chMeta.validate(chRaw); if (err) { toast(err); return; } }
var chCoerced = chMeta.coerce ? chMeta.coerce(chRaw) : chRaw;
var draftCh = App._getSettingsDraft();
if (!draftCh.channels) draftCh.channels = {};
if (!draftCh.channels[chName] || typeof draftCh.channels[chName] !== 'object') draftCh.channels[chName] = {};
var chList = Array.isArray(draftCh.channels[chName][chField]) ? draftCh.channels[chName][chField].slice() : [];
var chExists = chList.some(function(v) { return String(v) === String(chCoerced); });
if (chExists) { toast('Value already in list'); return; }
chList.push(chCoerced);
draftCh.channels[chName][chField] = chList;
if (chInput) chInput.value = '';
App._settingsDirty = JSON.stringify(draftCh) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
App.navigate('settings');
return;
}
var chRemoveBtn = e.target.closest('.ch-list-remove[data-ch-name]');
if (chRemoveBtn) {
var chName = chRemoveBtn.getAttribute('data-ch-name');
var chField = chRemoveBtn.getAttribute('data-ch-field');
var chRemoveVal = chRemoveBtn.getAttribute('data-ch-value');
var draftChR = App._getSettingsDraft();
if (!draftChR.channels) draftChR.channels = {};
if (!draftChR.channels[chName] || typeof draftChR.channels[chName] !== 'object') draftChR.channels[chName] = {};
var chCur = Array.isArray(draftChR.channels[chName][chField]) ? draftChR.channels[chName][chField].slice() : [];
draftChR.channels[chName][chField] = chCur.filter(function(v) { return String(v) !== String(chRemoveVal); });
App._settingsDirty = JSON.stringify(draftChR) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
App.navigate('settings');
return;
}
if (e.target.closest('#model-order-add-btn')) {
var addInput = document.getElementById('model-order-add-input');
var modelName = (addInput && addInput.value || '').trim();
if (!modelName) { toast('Enter a model name'); return; }
if (!App._settingsModelOrder) App._initModelOrderFromDraft();
if (App._settingsModelOrder.indexOf(modelName) !== -1) { toast('Model already in order list'); return; }
App._settingsModelOrder.push(modelName);
App._syncModelOrderToDraft();
App.navigate('settings');
return;
}
if (e.target.closest('#revenue-swap-chain-add-btn')) {
var chainInput = document.getElementById('revenue-swap-chain-add');
var chainName = (chainInput && chainInput.value || '').trim().toUpperCase();
if (!chainName) { toast('Enter a chain name'); return; }
var draftSwapAdd = App._getSettingsDraft();
if (!draftSwapAdd.treasury) draftSwapAdd.treasury = {};
if (!draftSwapAdd.treasury.revenue_swap) draftSwapAdd.treasury.revenue_swap = {};
if (!Array.isArray(draftSwapAdd.treasury.revenue_swap.chains)) draftSwapAdd.treasury.revenue_swap.chains = [];
var exists = draftSwapAdd.treasury.revenue_swap.chains.some(function(entry) {
return String((entry && entry.chain) || '').trim().toUpperCase() === chainName;
});
if (exists) { toast('Chain already configured'); return; }
draftSwapAdd.treasury.revenue_swap.chains.push({
chain: chainName,
target_contract_address: '',
swap_contract_address: ''
});
if (chainInput) chainInput.value = '';
App._settingsDirty = JSON.stringify(draftSwapAdd) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
App.navigate('settings');
return;
}
var revenueSwapRemoveBtn = e.target.closest('[data-revenue-swap-remove-chain]');
if (revenueSwapRemoveBtn) {
var removeIdx = Number(revenueSwapRemoveBtn.getAttribute('data-revenue-swap-remove-chain'));
var draftSwapRemove = App._getSettingsDraft();
var swapChains = draftSwapRemove && draftSwapRemove.treasury && draftSwapRemove.treasury.revenue_swap && Array.isArray(draftSwapRemove.treasury.revenue_swap.chains)
? draftSwapRemove.treasury.revenue_swap.chains
: null;
if (!swapChains || removeIdx < 0 || removeIdx >= swapChains.length) return;
swapChains.splice(removeIdx, 1);
App._settingsDirty = JSON.stringify(draftSwapRemove) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
App.navigate('settings');
return;
}
var modelPrimaryBtn = e.target.closest('[data-model-order-make-primary]');
if (modelPrimaryBtn) {
var pIdx = Number(modelPrimaryBtn.getAttribute('data-model-order-make-primary'));
if (!App._settingsModelOrder || pIdx <= 0 || pIdx >= App._settingsModelOrder.length) return;
var primaryName = App._settingsModelOrder.splice(pIdx, 1)[0];
App._settingsModelOrder.unshift(primaryName);
App._syncModelOrderToDraft();
App.navigate('settings');
return;
}
var modelRemoveBtn = e.target.closest('[data-model-order-remove]');
if (modelRemoveBtn) {
var rIdx = Number(modelRemoveBtn.getAttribute('data-model-order-remove'));
if (!App._settingsModelOrder || rIdx < 0 || rIdx >= App._settingsModelOrder.length) return;
App._settingsModelOrder.splice(rIdx, 1);
App._syncModelOrderToDraft();
App.navigate('settings');
return;
}
if (e.target.closest('#settings-save') || e.target.closest('#settings-apply')) {
var isApply = !!e.target.closest('#settings-apply');
if (App._settingsMode === 'raw') {
api('/api/config/raw', { method: 'PUT', headers: { 'Content-Type': 'text/plain' }, body: App._settingsRawText || '' }).then(function() {
App._settingsDraft = null;
App._settingsDirty = false;
App._settingsRawText = null;
toast(isApply ? 'Configuration applied from TOML' : 'Configuration saved from TOML');
App.navigate('settings');
}).catch(function(err) { toast(err.message || 'Save failed'); });
return;
}
var draft = App._getSettingsDraft();
var patch = App._buildMutableConfigPatch(draft);
api('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(patch) }).then(function() {
var merged = JSON.parse(JSON.stringify(_cachedConfig || {}));
Object.keys(patch || {}).forEach(function(k) { merged[k] = patch[k]; });
_cachedConfig = merged;
App._settingsDirty = false; App._settingsRawText = null;
toast(isApply ? 'Configuration applied' : 'Configuration saved');
App.navigate('settings');
}).catch(function(err) { toast(err.message || 'Save failed'); });
return;
}
if (e.target.closest('#settings-cancel')) {
App._settingsDraft = null; App._settingsRawText = null; App._settingsDirty = false;
toast('Changes discarded'); App.navigate('settings'); return;
}
var chainBtn = e.target.closest('[data-chain-preset]');
if (chainBtn) {
var name = chainBtn.getAttribute('data-chain-preset');
var preset = App._CHAIN_PRESETS[name];
if (preset) {
var draft = App._getSettingsDraft();
App._setNestedValue(draft, 'wallet.chain_id', preset.chain_id);
App._setNestedValue(draft, 'wallet.rpc_url', preset.rpc_url);
App._settingsDirty = JSON.stringify(draft) !== JSON.stringify(_cachedConfig);
App._settingsRawText = null;
App.navigate('settings');
}
return;
}
if (e.target.closest('.channel-enable-btn')) {
var channelName = e.target.closest('.channel-enable-btn').getAttribute('data-enable-channel');
if (!channelName) return;
var draft = App._getSettingsDraft();
if (!draft.channels) draft.channels = {};
var defaults = { telegram: { enabled: true, token_env: 'TELEGRAM_BOT_TOKEN' }, discord: { enabled: true, token_env: 'DISCORD_BOT_TOKEN' }, whatsapp: { enabled: true, token_env: 'WHATSAPP_TOKEN' }, signal: { enabled: true, phone_number: '', daemon_url: 'http://localhost:8080' }, email: { enabled: true, imap_host: '', smtp_host: '' }, web: { enabled: true }, voice: { enabled: true } };
draft.channels[channelName] = defaults[channelName] || { enabled: true };
App._settingsDirty = true; App._settingsRawText = null;
App.navigate('settings');
return;
}
if (e.target.closest('#add-provider')) {
var providerInput = document.getElementById('add-provider-input');
var name = (providerInput && providerInput.value || '');
if (!name || !name.trim()) return;
name = name.trim().toLowerCase();
var draft = App._getSettingsDraft();
if (!draft.providers) draft.providers = {};
if (draft.providers[name]) { toast('Provider "' + name + '" already exists'); return; }
draft.providers[name] = { url: '', tier: 'T2', format: 'openai', chat_path: '/v1/chat/completions' };
if (providerInput) providerInput.value = '';
App._settingsDirty = true; App._settingsRawText = null;
App.navigate('settings');
return;
}
if (e.target.id === 'keystore-unlock-btn') {
e.target.disabled = true; e.target.textContent = 'Unlocking\u2026';
api('/api/keystore/unlock', { method: 'POST' }).then(function(res) {
toast(res.action === 'already_unlocked' ? 'Keystore already unlocked' : 'Keystore unlocked');
App._settingsCache = null; App.navigate('settings');
}).catch(function(err) {
toast('Unlock failed: ' + (err.message || 'unknown error'));
e.target.disabled = false; e.target.textContent = 'Unlock keystore';
});
return;
}
var keyBtn = e.target.closest('[data-action="save-key"]') || e.target.closest('[data-action="remove-key"]');
if (keyBtn) {
var row = keyBtn.closest('.key-manage-row');
var provName = row && row.getAttribute('data-provider');
var msgEl = row && row.querySelector('.key-manage-msg');
if (!provName) return;
var action = keyBtn.getAttribute('data-action');
if (action === 'save-key') {
var inp = row.querySelector('.key-input');
var val = inp && inp.value.trim();
if (!val) { toast('Paste an API key first'); return; }
keyBtn.disabled = true; keyBtn.textContent = 'Saving\u2026';
api('/api/providers/' + encodeURIComponent(provName) + '/key', {
method: 'PUT', body: JSON.stringify({ api_key: val })
}).then(function() {
toast('Key saved for ' + provName);
App._settingsCache = null; App.navigate('settings');
}).catch(function(err) {
var msg = (err.message || 'Save failed');
if (msg.indexOf('locked') !== -1) msg = 'Keystore is locked \u2014 unlock it first';
if (msgEl) { msgEl.textContent = msg; msgEl.style.color = 'var(--error)'; }
keyBtn.disabled = false; keyBtn.textContent = 'Save to keystore';
});
} else if (action === 'remove-key') {
if (!confirm('Remove ' + provName + ' API key from keystore?')) return;
keyBtn.disabled = true; keyBtn.textContent = 'Removing\u2026';
api('/api/providers/' + encodeURIComponent(provName) + '/key', {
method: 'DELETE'
}).then(function() {
toast('Key removed for ' + provName);
App._settingsCache = null; App.navigate('settings');
}).catch(function(err) {
if (msgEl) { msgEl.textContent = err.message || 'Remove failed'; msgEl.style.color = 'var(--error)'; }
keyBtn.disabled = false; keyBtn.textContent = 'Remove';
});
}
return;
}
var calNav = e.target.closest('[data-cal-nav]');
if (calNav) {
var dir = calNav.getAttribute('data-cal-nav');
if (dir === 'prev') { App._calMonth--; if (App._calMonth < 0) { App._calMonth = 11; App._calYear--; } }
else if (dir === 'next') { App._calMonth++; if (App._calMonth > 11) { App._calMonth = 0; App._calYear++; } }
else { App._calMonth = new Date().getMonth(); App._calYear = new Date().getFullYear(); }
App._calSelected = null; App.navigate('scheduler'); return;
}
if (e.target.closest('[data-cal-add]')) { App._calModalMode = 'add'; App._calEditJob = null; App.navigate('scheduler'); return; }
var editBtn = e.target.closest('[data-cal-edit]');
if (editBtn) {
var idx = Number(editBtn.getAttribute('data-cal-edit')); var j = _cachedCronJobs[idx];
if (j) { App._calModalMode = 'edit'; App._calEditJob = { name: j.name, description: (j.description || App._cronIntentFromJob(j) || ''), schedule_kind: j.schedule_kind, schedule_expr: j.schedule_expr, _idx: idx, _origName: j.name }; App.navigate('scheduler'); }
return;
}
var delBtn = e.target.closest('[data-cal-delete]');
var runBtn = e.target.closest('[data-cal-run]');
if (runBtn) {
var runIdx = Number(runBtn.getAttribute('data-cal-run')); var rj = _cachedCronJobs[runIdx];
if (rj && rj.id) {
api('/api/cron/jobs/' + encodeURIComponent(rj.id) + '/run', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' })
.then(function(resp) {
var msg = (resp.status === 'success' ? 'Ran ' : 'Run failed: ') + rj.name + (resp.error ? ' — ' + resp.error : '');
if (resp.output_text) msg += ' — ' + String(resp.output_text).replace(/\s+/g, ' ').trim().slice(0, 160);
toast(msg);
App.navigate('scheduler');
})
.catch(function(err) { toast(err.message || 'Run failed'); });
}
return;
}
if (delBtn) {
var idx = Number(delBtn.getAttribute('data-cal-delete')); var j = _cachedCronJobs[idx];
if (j) {
if (!confirm('Delete scheduled job "' + j.name + '" and its run history?')) return;
api('/api/cron/jobs/' + encodeURIComponent(j.id), { method: 'DELETE' }).then(function() { toast('Deleted "' + j.name + '"'); App.navigate('scheduler'); }).catch(function(err) { toast(err.message || 'Delete failed'); App.navigate('scheduler'); });
}
return;
}
if (e.target.closest('[data-cal-modal-close]') || (e.target.closest('[data-cal-overlay]') && e.target.hasAttribute('data-cal-overlay'))) { App._calModalMode = null; App._calEditJob = null; App.navigate('scheduler'); return; }
var dayBtn = e.target.closest('[data-cal-day]');
if (dayBtn) { dayBtn.classList.toggle('active'); var sched = App._readSchedFromUI(); var sumEl = document.getElementById('cal-sched-summary'); if (sumEl) sumEl.textContent = App._schedSummary(sched); return; }
if (e.target.closest('#cal-modal-save')) {
var nameEl = document.getElementById('cal-job-name'); var name = (nameEl && nameEl.value || '').trim();
if (!name) { toast('Name is required'); return; }
var sched = App._readSchedFromUI(); var cron = App._schedToCron(sched);
var descEl = document.getElementById('cal-job-description'); var intent = (descEl && descEl.value || '').trim();
var payloadObj = intent ? { action: 'agent_task', task: intent } : { action: 'log', message: ('scheduled job: ' + name) };
var jobData = { name: name, description: intent || null, schedule_kind: cron.kind, schedule_expr: cron.expr, payload_json: JSON.stringify(payloadObj), agent_id: App._activeAgentId || 'roboticus' };
if (App._calModalMode === 'add') {
api('/api/cron/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(jobData) }).then(function() { toast('Added "' + name + '"'); App._calModalMode = null; App._calEditJob = null; App.navigate('scheduler'); }).catch(function(err) { toast(err.message || 'Add failed'); });
} else {
var editIdx = App._calEditJob && App._calEditJob._idx;
var editJob = editIdx != null ? _cachedCronJobs[editIdx] : null;
if (editJob && editJob.id) {
api('/api/cron/jobs/' + encodeURIComponent(editJob.id), { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(jobData) }).then(function() { toast('Updated "' + name + '"'); App._calModalMode = null; App._calEditJob = null; App.navigate('scheduler'); }).catch(function(err) { toast(err.message || 'Update failed'); });
} else { toast('Cannot update: job ID missing'); }
}
return;
}
var evtChip = e.target.closest('[data-evt-job]');
if (evtChip) {
if (evtChip.hasAttribute('data-past')) {
var parentDay = evtChip.closest('.cal-day[data-date]');
if (parentDay) { var date = parentDay.getAttribute('data-date'); App._calSelected = date; App.navigate('scheduler'); }
return;
}
var jobName = evtChip.getAttribute('data-evt-job');
var idx = _cachedCronJobs.findIndex(function(j) { return j.name === jobName; });
if (idx >= 0) { var j = _cachedCronJobs[idx]; App._calModalMode = 'edit'; App._calEditJob = { name: j.name, description: (j.description || App._cronIntentFromJob(j) || ''), schedule_kind: j.schedule_kind, schedule_expr: j.schedule_expr, _idx: idx, _origName: j.name }; App.navigate('scheduler'); }
return;
}
var calDay = e.target.closest('.cal-day[data-date]');
if (calDay) { var date = calDay.getAttribute('data-date'); App._calSelected = App._calSelected === date ? null : date; App.navigate('scheduler'); return; }
var copyBtn = e.target.closest('.copy-id-btn[data-copy-id]');
if (copyBtn) {
e.stopPropagation();
var copyId = copyBtn.getAttribute('data-copy-id');
var svgEl = copyBtn.querySelector('svg');
navigator.clipboard.writeText(copyId).then(function() {
toast('Session ID copied');
if (svgEl) { svgEl.innerHTML = '<polyline points="4 8 7 11 12 4" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>'; setTimeout(function() { svgEl.innerHTML = '<rect x="5" y="5" width="8" height="8" rx="1" stroke="currentColor" stroke-width="1.3"/><path d="M3 11V3a1 1 0 011-1h8" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/>'; }, 1500); }
}).catch(function() { toast('Copy failed \u2014 try HTTPS'); });
return;
}
var row = e.target.closest('tbody tr[data-id]');
if (row) {
var id = row.getAttribute('data-id');
var tds = row.querySelectorAll('td');
var nickTd = tds[0]; var agentTd = tds[1];
var nick = nickTd ? nickTd.textContent.trim() : '';
dismissHint('sessions-helper');
try { window.localStorage.setItem('ic_sessions_helper_dismissed', '1'); } catch (_) {}
App._activeSession = {
id: id,
agent_id: agentTd ? agentTd.textContent : 'default',
agent_name: AGENT_DISPLAY_NAME || null,
nickname: nick !== id ? nick : null
};
App.navigate('sessions');
return;
}
var starEl = e.target.closest('.grade-stars .star');
if (starEl) {
var gradeRow = starEl.closest('.grade-stars');
var turnId = gradeRow.getAttribute('data-turn-id');
var grade = parseInt(starEl.getAttribute('data-grade'));
var allStars = gradeRow.querySelectorAll('.star');
allStars.forEach(function(s) {
var g = parseInt(s.getAttribute('data-grade'));
if (g <= grade) { s.classList.add('filled'); } else { s.classList.remove('filled'); }
});
api('/api/turns/' + encodeURIComponent(turnId) + '/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grade: grade })
}).catch(function() {
api('/api/turns/' + encodeURIComponent(turnId) + '/feedback', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grade: grade })
}).catch(function() {});
});
return;
}
var commentToggle = e.target.closest('.grade-comment-toggle');
if (commentToggle) {
var gradeRow = commentToggle.closest('.grade-stars');
var turnId = gradeRow.getAttribute('data-turn-id');
var existing = gradeRow.parentElement.querySelector('.grade-comment-row');
if (existing) { existing.remove(); return; }
var row = document.createElement('div');
row.className = 'grade-comment-row';
var inp = document.createElement('input');
inp.type = 'text';
inp.placeholder = 'Add a comment\u2026';
inp.className = 'grade-comment-input';
inp.setAttribute('data-turn-id', turnId);
var btn = document.createElement('button');
btn.className = 'grade-save-btn';
btn.setAttribute('data-turn-id', turnId);
btn.textContent = 'Save';
row.appendChild(inp);
row.appendChild(btn);
gradeRow.parentElement.insertBefore(row, gradeRow.nextSibling);
inp.focus();
return;
}
var commentSave = e.target.closest('.grade-save-btn');
if (commentSave) {
var turnId = commentSave.getAttribute('data-turn-id');
var input = commentSave.parentElement.querySelector('.grade-comment-input');
var comment = input ? input.value.trim() : '';
var gradeRow = commentSave.parentElement.previousElementSibling;
var filledStars = gradeRow ? gradeRow.querySelectorAll('.star.filled').length : 0;
var grade = filledStars > 0 ? filledStars : 3;
api('/api/turns/' + encodeURIComponent(turnId) + '/feedback', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ grade: grade, comment: comment || null })
}).then(function() {
commentSave.parentElement.remove();
toast('Comment saved');
}).catch(function() { toast('Failed to save comment'); });
return;
}
var ctxSessionRow = e.target.closest('tr[data-ctx-session]');
if (ctxSessionRow) {
try { App._ctxSession = JSON.parse(ctxSessionRow.getAttribute('data-ctx-session')); } catch(ex) {}
App._ctxActiveTurn = null;
App.navigate('context');
return;
}
var ctxTurnItem = e.target.closest('.ctx-timeline-item[data-turn-id]');
if (ctxTurnItem) {
var turnId = ctxTurnItem.getAttribute('data-turn-id');
var found = App._ctxTurns.find(function(t) { return t.id === turnId; });
if (found) { App._ctxActiveTurn = found; App.navigate('context'); }
return;
}
var ctxLiveBtn = e.target.closest('#ctx-open-live-turn');
if (ctxLiveBtn) {
var liveTurnId = ctxLiveBtn.getAttribute('data-turn-id');
var liveSessionId = ctxLiveBtn.getAttribute('data-session-id');
if (liveSessionId) {
App._ctxSession = { id: liveSessionId, nickname: liveSessionId };
}
if (liveTurnId) {
App._ctxActiveTurn = { id: liveTurnId };
}
App.navigate('context');
return;
}
var ctxCopyLiveBtn = e.target.closest('#ctx-copy-live-turn');
if (ctxCopyLiveBtn) {
var copyTurnId = ctxCopyLiveBtn.getAttribute('data-turn-id') || '';
if (!copyTurnId) {
toast('No live turn ID available');
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(copyTurnId).then(function() {
toast('Turn ID copied');
}).catch(function() {
toast('Copy failed');
});
} else {
try {
var tmp = document.createElement('textarea');
tmp.value = copyTurnId;
tmp.setAttribute('readonly', '');
tmp.style.position = 'absolute';
tmp.style.left = '-9999px';
document.body.appendChild(tmp);
tmp.select();
document.execCommand('copy');
document.body.removeChild(tmp);
toast('Turn ID copied');
} catch (ex) {
toast('Copy failed');
}
}
return;
}
if (e.target.closest('#ctx-back-btn')) {
if (App._ctxActiveTurn) { App._ctxActiveTurn = null; }
else { App._ctxSession = null; }
App.navigate('context');
return;
}
var ctxBtn = e.target.closest('.ctx-expand-btn');
if (ctxBtn) {
var msgIdx = ctxBtn.getAttribute('data-msg-idx');
var detailEl = document.getElementById('ctx-detail-' + msgIdx);
if (!detailEl) return;
if (detailEl.style.display !== 'none') { detailEl.style.display = 'none'; return; }
var sessionId = ctxBtn.getAttribute('data-session-id');
setHtml(detailEl, '<span style="color:var(--muted);font-size:0.75rem">Loading context\u2026</span>');
detailEl.style.display = 'block';
api('/api/sessions/' + encodeURIComponent(sessionId) + '/turns').then(function(r) {
var turns = r.turns || [];
var asstIdx = 0;
var allMsgs = App._sessionMessages || [];
for (var mi = 0; mi < allMsgs.length && mi <= parseInt(msgIdx); mi++) {
if (allMsgs[mi].role === 'assistant') asstIdx++;
}
var turn = turns[asstIdx - 1];
if (!turn) { setHtml(detailEl, '<span style="color:var(--muted);font-size:0.75rem">No turn data found.</span>'); return; }
return Promise.all([
api('/api/turns/' + encodeURIComponent(turn.id) + '/context').catch(function() { return null; }),
api('/api/turns/' + encodeURIComponent(turn.id) + '/tips').catch(function() { return { tips: [] }; })
]).then(function(results) {
var ctx = results[0], tipsData = results[1];
var html = '';
if (ctx && ctx.snapshot !== false) {
var budget = ctx.token_budget || 1;
var sysPct = Math.round(((ctx.system_prompt_tokens || 0) / budget) * 100);
var memPct = Math.round(((ctx.memory_tokens || 0) / budget) * 100);
var histPct = Math.round(((ctx.history_tokens || 0) / budget) * 100);
var freePct = Math.max(0, 100 - sysPct - memPct - histPct);
html += '<div style="font-size:0.6875rem;color:var(--muted);margin-bottom:0.25rem">' + esc(ctx.complexity_level) + ' \u2022 budget: ' + (ctx.token_budget || 0).toLocaleString() + ' tokens \u2022 depth: ' + (ctx.history_depth || 0) + '</div>';
html += '<div class="ctx-bar"><span class="sys" style="width:' + sysPct + '%"></span><span class="mem" style="width:' + memPct + '%"></span><span class="hist" style="width:' + histPct + '%"></span><span class="free" style="width:' + freePct + '%"></span></div>';
html += '<div class="ctx-legend"><span class="l-sys">System ' + (ctx.system_prompt_tokens || 0) + '</span><span class="l-mem">Memory ' + (ctx.memory_tokens || 0) + '</span><span class="l-hist">History ' + (ctx.history_tokens || 0) + '</span></div>';
if (ctx.model) html += '<div style="margin-top:0.25rem;font-size:0.6875rem;color:var(--muted)">Model: ' + esc(ctx.model) + '</div>';
} else {
html += '<span style="color:var(--muted)">No context snapshot.</span>';
}
if (turn.cost != null) html += '<div style="margin-top:0.25rem;font-size:0.6875rem;color:var(--success)">$' + turn.cost.toFixed(4) + ' \u2022 ' + ((turn.tokens_in || 0) + (turn.tokens_out || 0)).toLocaleString() + ' tokens</div>';
var tips = (tipsData && tipsData.tips) || [];
if (tips.length > 0) {
html += '<div class="ctx-tips">';
tips.forEach(function(tip) {
html += '<span class="ctx-tip ' + esc(tip.severity) + '" title="' + esc(tip.suggestion) + '"><span class="tip-dot"></span>' + esc(tip.message) + '</span>';
});
html += '</div>';
}
html += '<button class="btn secondary ctx-analyze-btn" data-analyze-turn="' + esc(turn.id) + '">Analyze with AI</button>';
if (turn.thinking) html += '<details class="ctx-section"><summary>Reasoning</summary><div>' + renderSafeMarkdown(turn.thinking) + '</div></details>';
setHtml(detailEl, html);
});
}).catch(function() { setHtml(detailEl, '<span style="color:var(--error);font-size:0.75rem">Failed to load context.</span>'); });
return;
}
var analyzeBtn = e.target.closest('[data-analyze-turn]');
if (analyzeBtn) {
var turnId = analyzeBtn.getAttribute('data-analyze-turn');
analyzeBtn.disabled = true; analyzeBtn.textContent = 'Analyzing\u2026';
api('/api/turns/' + encodeURIComponent(turnId) + '/analyze', { method: 'POST' }).then(function(r) {
analyzeBtn.textContent = 'Analyze with AI'; analyzeBtn.disabled = false;
var parent = analyzeBtn.parentElement;
var existing = parent.querySelector('.ctx-analyze-result');
if (existing) existing.remove();
var div = document.createElement('div');
div.className = 'ctx-analyze-result';
div.style.cssText = 'margin-top:0.375rem;padding:0.5rem;background:rgba(0,0,0,0.15);border-radius:3px;font-size:0.6875rem;border:1px solid var(--border)';
var inner = '<div style="color:var(--muted);margin-bottom:0.25rem">Turn analysis</div>';
inner += '<div style="margin-bottom:0.35rem;color:var(--text)">' + esc(r.summary || 'No summary') + '</div>';
var tips = r.tips || [];
if (tips.length > 0) {
inner += '<ul style="margin:0;padding-left:1rem">';
tips.forEach(function(t) {
inner += '<li style="margin:0.18rem 0"><strong>' + esc(t.rule_name || 'tip') + ':</strong> ' + esc(t.suggestion || t.message || '') + '</li>';
});
inner += '</ul>';
} else {
inner += '<div style="color:var(--muted)">No additional recommendations.</div>';
}
setHtml(div, inner);
parent.insertBefore(div, analyzeBtn.nextSibling);
}).catch(function(err) {
analyzeBtn.textContent = 'Analyze with AI'; analyzeBtn.disabled = false;
toast(err.message || 'Analysis failed');
});
return;
}
});
if (content) content.addEventListener('change', function(e) {
if (e.target && e.target.id === 'mem-session-select') { App._memorySessionId = e.target.value; App._memoryPage = 0; App.navigate('memory'); return; }
if (e.target && e.target.id === 'traces-session-select') { App._tracesSessionId = e.target.value; App.navigate('metrics'); return; }
if (e.target && e.target.id === 'flight-session-select') { App._flightSessionId = e.target.value; App._flightTurnId = ''; App.navigate('metrics'); return; }
if (e.target && e.target.id === 'budget-channel-min') {
var draft = normalizeContextBudget(App._contextBudgetDraft || App._contextBudgetDefaults || {});
draft.channel_minimum = String(e.target.value || 'L1').toUpperCase();
App._contextBudgetDraft = normalizeContextBudget(draft);
return;
}
if (!e.target || e.target.id !== 'model-graph-task-select') return;
App._modelGraphFocusTurn = e.target.value || null;
App._modelGraphFocusModel = null;
App._modelGraphFocusEdge = null;
App.navigate('efficiency');
});
if (content) content.addEventListener('input', function(e) {
var slider = e.target.closest('[data-routing-slider]');
if (!slider) return;
var key = slider.getAttribute('data-routing-slider');
if (!key) return;
if (!App._routingProfileDraft) {
App._routingProfileDraft = App._routingProfileDefaults
? normalizeRoutingProfile(App._routingProfileDefaults)
: normalizeRoutingProfile({});
}
App._routingProfileDraft[key] = clamp01(slider.value);
var othersSum = 0;
ROUTING_KEYS.forEach(function(k) { if (k !== key) othersSum += App._routingProfileDraft[k]; });
var headroom = Math.max(0, 1.0 - othersSum);
App._routingProfileDraft[key] = Math.min(App._routingProfileDraft[key], headroom);
ROUTING_KEYS.forEach(function(k) {
var valEl = document.getElementById('routing-slider-' + k + '-val');
if (valEl) valEl.textContent = App._routingProfileDraft[k].toFixed(2);
var sliderEl = document.getElementById('routing-slider-' + k);
if (sliderEl) sliderEl.value = App._routingProfileDraft[k].toFixed(2);
});
var totalEl = document.getElementById('routing-slider-total');
if (totalEl) {
totalEl.textContent = routingProfileTotal(App._routingProfileDraft).toFixed(2);
}
var spiderHost = document.getElementById('routing-profile-spider-host');
if (spiderHost) spiderHost.innerHTML = renderRoutingSpiderSvg(App._routingProfileDraft);
});
if (content) content.addEventListener('input', function(e) {
var bSlider = e.target.closest('[data-budget-slider]');
if (!bSlider) return;
var bKey = bSlider.getAttribute('data-budget-slider');
if (!bKey) return;
var draft = normalizeContextBudget(App._contextBudgetDraft || App._contextBudgetDefaults || {});
draft[bKey] = parseInt(bSlider.value, 10) || draft[bKey];
App._contextBudgetDraft = normalizeContextBudget(draft);
var valEl = document.getElementById('budget-' + bKey + '-val');
if (valEl) valEl.textContent = App._contextBudgetDraft[bKey];
});
if (content) content.addEventListener('input', function(e) {
if (!e.target || e.target.id !== 'catalog-filter-input') return;
var needle = String(e.target.value || '').trim().toLowerCase();
var rows = Array.prototype.slice.call(document.querySelectorAll('[data-catalog-row="1"]'));
var visible = 0;
rows.forEach(function(row) {
var hay = String(row.getAttribute('data-catalog-search') || '').toLowerCase();
var match = !needle || hay.indexOf(needle) !== -1;
row.style.display = match ? '' : 'none';
if (match) visible += 1;
});
var empty = document.getElementById('catalog-filter-empty');
if (empty) empty.style.display = visible === 0 ? '' : 'none';
});
if (content) content.addEventListener('input', function(e) {
if (e.target.id === 'mem-filter-input') { App._memoryFilter = e.target.value; App._memoryPage = 0; App.navigate('memory'); return; }
});
if (content) content.addEventListener('input', function(e) {
if (e.target.id === 'cal-sched-interval' || e.target.id === 'cal-sched-hour' || e.target.id === 'cal-sched-minute') { var sched = App._readSchedFromUI(); var sumEl = document.getElementById('cal-sched-summary'); if (sumEl) sumEl.textContent = App._schedSummary(sched); return; }
var rawEditor = e.target.closest('#settings-raw-editor');
if (rawEditor) {
App._settingsRawText = rawEditor.value; App._settingsDirty = true;
syncRawHighlight();
document.querySelectorAll('#settings-save,#settings-apply,#settings-cancel').forEach(function(b) { b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = ''; });
return;
}
var formInput = e.target.closest('[data-settings-path]');
if (formInput) {
var path = formInput.getAttribute('data-settings-path');
var draft = App._getSettingsDraft();
var rawVal = formInput.value;
var newVal;
if (formInput.type === 'checkbox') { newVal = formInput.checked; }
else if (formInput.type === 'number') { newVal = Number(rawVal); }
else if (formInput.hasAttribute('data-settings-array')) { newVal = rawVal.split('\n').map(function(s){return s.trim()}).filter(function(s){return s.length > 0}); }
else { newVal = rawVal === '' ? null : rawVal; }
App._setNestedValue(draft, path, newVal);
App._settingsDirty = JSON.stringify(draft) !== JSON.stringify(_cachedConfig);
App._settingsRawText = null;
if (formInput.type === 'checkbox') { var lbl = formInput.closest('.settings-toggle-wrap'); if (lbl) { var span = lbl.querySelector('.settings-toggle-label'); if (span) span.textContent = newVal ? 'On' : 'Off'; } }
document.querySelectorAll('#settings-save,#settings-apply,#settings-cancel').forEach(function(b) { if (App._settingsDirty) { b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = ''; } else { b.disabled = true; b.style.opacity = '0.4'; b.style.pointerEvents = 'none'; } });
return;
}
var revenueSwapChainInput = e.target.closest('[data-revenue-swap-chain-field]');
if (revenueSwapChainInput) {
var revenueField = revenueSwapChainInput.getAttribute('data-revenue-swap-chain-field');
var revenueIdx = Number(revenueSwapChainInput.getAttribute('data-revenue-swap-chain-index'));
var draftSwapField = App._getSettingsDraft();
if (!draftSwapField.treasury) draftSwapField.treasury = {};
if (!draftSwapField.treasury.revenue_swap) draftSwapField.treasury.revenue_swap = {};
if (!Array.isArray(draftSwapField.treasury.revenue_swap.chains)) draftSwapField.treasury.revenue_swap.chains = [];
if (!draftSwapField.treasury.revenue_swap.chains[revenueIdx]) draftSwapField.treasury.revenue_swap.chains[revenueIdx] = { chain: '', target_contract_address: '', swap_contract_address: '' };
var revenueVal = revenueSwapChainInput.value;
if (revenueField === 'chain') revenueVal = String(revenueVal || '').toUpperCase();
draftSwapField.treasury.revenue_swap.chains[revenueIdx][revenueField] = revenueVal;
App._settingsDirty = JSON.stringify(draftSwapField) !== JSON.stringify(_cachedConfig);
App._settingsRawText = null;
document.querySelectorAll('#settings-save,#settings-apply,#settings-cancel').forEach(function(b) { if (App._settingsDirty) { b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = ''; } else { b.disabled = true; b.style.opacity = '0.4'; b.style.pointerEvents = 'none'; } });
return;
}
});
if (content) content.addEventListener('dragstart', function(e) {
var item = e.target.closest('[data-model-order-item]');
if (!item || App._settingsMode !== 'models') return;
App._settingsDragModelIndex = Number(item.getAttribute('data-model-order-item'));
item.classList.add('dragging');
if (e.dataTransfer) {
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', String(App._settingsDragModelIndex));
}
});
if (content) content.addEventListener('dragover', function(e) {
var item = e.target.closest('[data-model-order-item]');
if (!item || App._settingsMode !== 'models') return;
e.preventDefault();
if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
document.querySelectorAll('.model-order-item.drop-target').forEach(function(el) { el.classList.remove('drop-target'); });
item.classList.add('drop-target');
});
if (content) content.addEventListener('dragleave', function(e) {
var item = e.target.closest('[data-model-order-item]');
if (!item) return;
item.classList.remove('drop-target');
});
if (content) content.addEventListener('drop', function(e) {
var item = e.target.closest('[data-model-order-item]');
if (!item || App._settingsMode !== 'models') return;
e.preventDefault();
var fromIdx = App._settingsDragModelIndex;
var toIdx = Number(item.getAttribute('data-model-order-item'));
document.querySelectorAll('.model-order-item.drop-target,.model-order-item.dragging').forEach(function(el) { el.classList.remove('drop-target'); el.classList.remove('dragging'); });
App._settingsDragModelIndex = null;
if (!App._settingsModelOrder || fromIdx == null || fromIdx === toIdx || fromIdx < 0 || toIdx < 0) return;
var moved = App._settingsModelOrder.splice(fromIdx, 1)[0];
App._settingsModelOrder.splice(toIdx, 0, moved);
App._syncModelOrderToDraft();
App.navigate('settings');
});
if (content) content.addEventListener('dragend', function() {
document.querySelectorAll('.model-order-item.drop-target,.model-order-item.dragging').forEach(function(el) { el.classList.remove('drop-target'); el.classList.remove('dragging'); });
App._settingsDragModelIndex = null;
});
if (content) content.addEventListener('keydown', function(e) {
if (e.target.id === 'session-msg-input' && e.key === 'Enter') { if (e.shiftKey) return; e.preventDefault(); if (App._sendingMessage) return; var btn = document.getElementById('btn-send-msg'); if (btn && !btn.disabled) btn.click(); return; }
if (e.target.id === 'model-order-add-input' && e.key === 'Enter') { e.preventDefault(); var addBtn = document.getElementById('model-order-add-btn'); if (addBtn) addBtn.click(); return; }
if (e.target.id === 'revenue-swap-chain-add' && e.key === 'Enter') { e.preventDefault(); var addRevenueSwapChainBtn = document.getElementById('revenue-swap-chain-add-btn'); if (addRevenueSwapChainBtn) addRevenueSwapChainBtn.click(); return; }
if (e.target.id === 'add-provider-input' && e.key === 'Enter') { e.preventDefault(); var addProviderBtn = document.getElementById('add-provider'); if (addProviderBtn) addProviderBtn.click(); return; }
if (e.target.id === 'trusted-sender-input' && e.key === 'Enter') { e.preventDefault(); var addSenderBtn = document.getElementById('trusted-sender-add'); if (addSenderBtn) addSenderBtn.click(); return; }
if (e.target.classList && e.target.classList.contains('ch-list-input') && e.key === 'Enter') { e.preventDefault(); var chN = e.target.getAttribute('data-ch-name'); var chF = e.target.getAttribute('data-ch-field'); var chAddBtn = document.querySelector('.ch-list-add[data-ch-name="' + chN + '"][data-ch-field="' + chF + '"]'); if (chAddBtn) chAddBtn.click(); return; }
if (e.target.id === 'roster-model-custom' && e.key === 'Enter') { e.preventDefault(); var rosterAddBtn = document.getElementById('roster-model-order-add'); var rosterSaveBtn = document.getElementById('roster-model-save'); if (rosterAddBtn) rosterAddBtn.click(); else if (rosterSaveBtn) rosterSaveBtn.click(); return; }
if (e.target.id === 'settings-raw-editor' && e.key === 'Tab') { e.preventDefault(); var ta = e.target; var start = ta.selectionStart, end = ta.selectionEnd; ta.value = ta.value.substring(0, start) + ' ' + ta.value.substring(end); ta.selectionStart = ta.selectionEnd = start + 2; ta.dispatchEvent(new Event('input', { bubbles: true })); }
});
if (content) content.addEventListener('change', function(e) {
if (e.target.id === 'model-order-auto') {
var draft = App._getSettingsDraft();
if (!draft.models) draft.models = {};
draft.models.auto_order = e.target.checked;
App._settingsDirty = JSON.stringify(draft) !== JSON.stringify(_cachedConfig);
App.navigate('settings');
return;
}
if (e.target.classList && e.target.classList.contains('startup-ann-check')) {
var draftSa = App._getSettingsDraft();
if (!draftSa.channels) draftSa.channels = {};
var checks = Array.prototype.slice.call(document.querySelectorAll('.startup-ann-check:checked')).map(function(el) { return String(el.getAttribute('data-startup-channel') || '').trim(); }).filter(Boolean);
draftSa.channels.startup_announcements = checks.length ? checks : null;
App._settingsDirty = JSON.stringify(draftSa) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
document.querySelectorAll('#settings-save,#settings-apply,#settings-cancel').forEach(function(b) { if (App._settingsDirty) { b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = ''; } else { b.disabled = true; b.style.opacity = '0.4'; b.style.pointerEvents = 'none'; } });
return;
}
if (e.target.id === 'cal-sched-freq') {
var job = App._calEditJob || { name: '', schedule_kind: 'cron', schedule_expr: '' };
var nameEl = document.getElementById('cal-job-name'); if (nameEl) job.name = nameEl.value;
var freq = e.target.value;
var defaults = { interval: { kind: 'cron', expr: '*/5 * * * *' }, hourly: { kind: 'cron', expr: '0 * * * *' }, daily: { kind: 'cron', expr: '0 2 * * *' }, weekly: { kind: 'cron', expr: '0 9 * * 1,2,3,4,5' } };
var d = defaults[freq] || defaults.daily; job.schedule_kind = d.kind; job.schedule_expr = d.expr; App._calEditJob = job; App.navigate('scheduler'); return;
}
if (e.target.id === 'cal-sched-interval' || e.target.id === 'cal-sched-interval-unit' || e.target.id === 'cal-sched-hour' || e.target.id === 'cal-sched-minute') { var sched = App._readSchedFromUI(); var sumEl = document.getElementById('cal-sched-summary'); if (sumEl) sumEl.textContent = App._schedSummary(sched); return; }
var formInput = e.target.closest('[data-settings-path]');
if (formInput) {
var path = formInput.getAttribute('data-settings-path'); var draft = App._getSettingsDraft();
var newVal;
if (formInput.type === 'checkbox') { newVal = formInput.checked; }
else if (formInput.tagName === 'SELECT') { newVal = formInput.value; }
else if (formInput.type === 'number') { newVal = Number(formInput.value); }
else if (formInput.hasAttribute('data-settings-array')) { newVal = formInput.value.split('\n').map(function(s){return s.trim()}).filter(function(s){return s.length > 0}); }
else { newVal = formInput.value === '' ? null : formInput.value; }
App._setNestedValue(draft, path, newVal);
App._settingsDirty = JSON.stringify(draft) !== JSON.stringify(_cachedConfig); App._settingsRawText = null;
if (formInput.type === 'checkbox') { var lbl = formInput.closest('.settings-toggle-wrap'); if (lbl) { var span = lbl.querySelector('.settings-toggle-label'); if (span) span.textContent = newVal ? 'On' : 'Off'; } }
document.querySelectorAll('#settings-save,#settings-apply,#settings-cancel').forEach(function(b) { if (App._settingsDirty) { b.disabled = false; b.style.opacity = ''; b.style.pointerEvents = ''; } else { b.disabled = true; b.style.opacity = '0.4'; b.style.pointerEvents = 'none'; } });
}
var revenueSwapChainInput = e.target.closest('[data-revenue-swap-chain-field]');
if (revenueSwapChainInput && revenueSwapChainInput.getAttribute('data-revenue-swap-chain-field') === 'chain') {
revenueSwapChainInput.value = String(revenueSwapChainInput.value || '').toUpperCase();
}
});