App.renderSkills = function() {
var self = this;
var tab = self._skillsTab || 'installed';
if (tab !== 'installed' && tab !== 'catalog') tab = 'installed';
var tabs = '<div class="tabs" style="margin-bottom:1rem">'
+ '<button class="' + (tab === 'installed' ? 'active' : '') + '" data-skills-tab="installed">Installed</button>'
+ '<button class="' + (tab === 'catalog' ? 'active' : '') + '" data-skills-tab="catalog">Catalog</button>'
+ '</div>';
return Promise.all([
api('/api/skills'),
fetchWithFallback('/api/skills/catalog', { items: [] }, 'skills-catalog'),
api('/api/plugins').catch(function() { return { plugins: [] }; })
]).then(function(results) {
var data = results[0] || {};
var catalogData = (results[1] && results[1].data) ? results[1].data : { items: [] };
var installedPlugins = (results[2] && results[2].plugins) || [];
var skills = data.skills || [];
var catalogItems = (catalogData.items || []).filter(function(i) { return (i.source || '') === 'registry'; });
var installedSkillSet = {};
skills.forEach(function(s) {
var skillName = (s && s.name) ? String(s.name).trim().toLowerCase() : '';
if (skillName) installedSkillSet[skillName] = true;
});
var catalogHtml = '';
if (catalogItems.length > 0) {
var catRows = catalogItems.map(function(item) {
var name = item.name || item.filename || '';
var description = String(item.description || '').trim();
var summary = description || 'No description provided for this catalog entry yet.';
var metaParts = [];
if (item.kind) metaParts.push(String(item.kind));
if (item.version) metaParts.push('v' + String(item.version));
if (item.tags && item.tags.length) metaParts.push(String(item.tags.slice(0, 4).join(', ')));
var metaLine = metaParts.length ? metaParts.join(' • ') : '';
var searchText = [
name,
summary,
item.kind || '',
item.version || '',
(item.tags && item.tags.length) ? item.tags.join(' ') : ''
].join(' ').toLowerCase();
var isInstalled = !!installedSkillSet[String(name).trim().toLowerCase()];
var installedMark = isInstalled
? '<span class="badge success" style="font-size:0.62rem;padding:0.08rem 0.35rem">installed</span>'
: '';
return '<label data-catalog-row="1" data-catalog-search="' + esc(searchText) + '" style="display:flex;align-items:flex-start;gap:0.6rem;padding:0.6rem 0.65rem;border:1px solid var(--border-soft);border-radius:6px;background:rgba(255,255,255,0.01)">'
+ '<input type="checkbox" class="cat-skill-check" value="' + esc(name) + '"' + (isInstalled ? ' checked' : '') + ' data-installed="' + (isInstalled ? '1' : '0') + '">'
+ '<div style="min-width:0;flex:1">'
+ '<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap">'
+ '<span style="font-family:var(--font-mono);font-size:0.82rem;color:var(--text)">' + esc(name) + '</span>'
+ installedMark
+ '</div>'
+ '<div style="margin-top:0.18rem;font-size:0.75rem;color:var(--muted);line-height:1.35">' + esc(summary) + '</div>'
+ (metaLine ? '<div style="margin-top:0.2rem;font-size:0.68rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.04em">' + esc(metaLine) + '</div>' : '')
+ '</div>'
+ '</label>';
}).join('');
catalogHtml = ''
+ '<div class="card" style="margin-bottom:1rem">'
+ ' <div style="display:flex;justify-content:space-between;align-items:center;gap:0.75rem;flex-wrap:wrap">'
+ ' <div><div class="card-title">catalog</div><div style="font-size:0.75rem;color:var(--muted)">registry skills (' + catalogItems.length + ')</div></div>'
+ ' <div style="display:flex;gap:0.5rem;flex-wrap:wrap">'
+ ' <button class="btn secondary" id="btn-catalog-refresh">' + uiBtnLabel('refresh', 'Refresh Catalog') + '</button>'
+ ' <button class="btn secondary" id="btn-catalog-install">' + uiBtnLabel('download', 'Install selected') + '</button>'
+ ' <button class="btn secondary" id="btn-catalog-activate">' + uiBtnLabel('power', 'Activate selected') + '</button>'
+ ' <button class="btn" id="btn-catalog-install-activate">' + uiBtnLabel('spark', 'Install + Activate') + '</button>'
+ ' </div>'
+ ' </div>'
+ ' <div style="margin-top:0.75rem">'
+ ' <input type="text" id="catalog-filter-input" placeholder="Search catalog by name, description, tags..."'
+ ' style="width:100%;padding:0.5rem 0.65rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:var(--font)">'
+ ' </div>'
+ ' <div style="margin-top:0.75rem;max-height:min(62vh,680px);overflow:auto;padding-right:0.25rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:0.55rem">' + catRows + '</div>'
+ ' <div id="catalog-filter-empty" style="display:none;margin-top:0.5rem;font-size:0.78rem;color:var(--muted)">No catalog skills match this filter.</div>'
+ '</div>';
} else {
catalogHtml = ''
+ '<div class="card" style="margin-bottom:1rem">'
+ ' <div style="display:flex;justify-content:space-between;align-items:center;gap:0.75rem;flex-wrap:wrap">'
+ ' <div><div class="card-title">catalog</div><div style="font-size:0.75rem;color:var(--muted)">registry skills (0)</div></div>'
+ ' <div style="display:flex;gap:0.5rem;flex-wrap:wrap">'
+ ' <button class="btn secondary" id="btn-catalog-refresh">' + uiBtnLabel('refresh', 'Refresh Catalog') + '</button>'
+ ' </div>'
+ ' </div>'
+ ' <div style="margin-top:0.75rem;font-size:0.8125rem;color:var(--muted)">No registry catalog skills available.</div>'
+ '</div>';
}
// ── Plugins section ──
// Merge catalog plugins with locally installed plugins
var pluginItems = catalogData.plugins || [];
var catalogPluginNames = {};
pluginItems.forEach(function(p) { catalogPluginNames[(p.name || '').toLowerCase()] = true; });
// Add installed plugins not already in catalog
installedPlugins.forEach(function(p) {
if (!catalogPluginNames[(p.name || '').toLowerCase()]) {
pluginItems.push({
name: p.name,
description: (p.tools && p.tools.length > 0 ? p.tools.map(function(t) { return t.name; }).join(', ') : 'Installed plugin'),
version: '',
tier: 'installed',
author: '',
status: p.status,
tools: p.tools,
});
}
});
var pluginsHtml = '';
if (pluginItems.length > 0) {
var pluginRows = pluginItems.map(function(p) {
var name = p.name || '';
var desc = p.description || '';
var version = p.version || '';
var tier = p.tier || '';
var author = p.author || '';
var tierBadge = tier === 'official'
? '<span class="badge success" style="font-size:0.62rem;padding:0.08rem 0.35rem">official</span>'
: tier === 'community'
? '<span class="badge" style="font-size:0.62rem;padding:0.08rem 0.35rem;background:var(--accent);color:#fff">community</span>'
: '<span class="badge" style="font-size:0.62rem;padding:0.08rem 0.35rem">' + esc(tier) + '</span>';
return '<div style="display:flex;align-items:flex-start;gap:0.6rem;padding:0.6rem 0.65rem;border:1px solid var(--border-soft);border-radius:6px;background:rgba(255,255,255,0.01)">'
+ '<div style="min-width:0;flex:1">'
+ '<div style="display:flex;align-items:center;gap:0.4rem;flex-wrap:wrap">'
+ '<span style="font-family:var(--font-mono);font-size:0.82rem;color:var(--text)">' + esc(name) + '</span>'
+ tierBadge
+ '<span style="font-size:0.68rem;color:var(--text-dim)">v' + esc(version) + '</span>'
+ '</div>'
+ '<div style="margin-top:0.18rem;font-size:0.75rem;color:var(--muted);line-height:1.35">' + esc(desc) + '</div>'
+ (author ? '<div style="margin-top:0.15rem;font-size:0.68rem;color:var(--text-dim)">by ' + esc(author) + '</div>' : '')
+ '</div>'
+ (tier === 'installed'
? '<span class="badge success" style="font-size:0.7rem;padding:0.25rem 0.5rem">\u2714 Installed</span>'
: '<button class="btn secondary" data-plugin-install="' + esc(name) + '" style="font-size:0.75rem;padding:0.3rem 0.6rem;white-space:nowrap">' + uiBtnLabel('download', 'Install') + '</button>')
+ '</div>';
}).join('');
pluginsHtml = ''
+ '<div class="card" style="margin-bottom:1rem">'
+ ' <div style="display:flex;justify-content:space-between;align-items:center;gap:0.75rem;flex-wrap:wrap">'
+ ' <div><div class="card-title">plugins</div><div style="font-size:0.75rem;color:var(--muted)">available plugins (' + pluginItems.length + ')</div></div>'
+ ' </div>'
+ ' <div style="margin-top:0.75rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(360px,1fr));gap:0.55rem">' + pluginRows + '</div>'
+ '</div>';
}
if (tab === 'catalog') {
return tabs + catalogHtml + pluginsHtml;
}
if (skills.length === 0) {
return tabs + '<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap"><button class="btn" id="btn-reload-skills">' + uiBtnLabel('refresh', 'Reload skills') + '</button><button class="btn secondary" id="btn-create-skill">' + uiBtnLabel('add', 'Create Skill') + '</button><span style="font-size:0.8125rem;color:var(--muted)">0 / 0 enabled</span></div><div class="card" style="margin-top:1rem;color:var(--muted)">No skills registered. Add skills on disk and click Reload skills.</div>';
}
var enabledCount = skills.filter(function(s) { return s.enabled; }).length;
var sortedSkills = skills.slice().sort(function(a, b) {
var aBuiltIn = !!a.built_in || String(a.kind || '').toLowerCase() === 'builtin';
var bBuiltIn = !!b.built_in || String(b.kind || '').toLowerCase() === 'builtin';
if (aBuiltIn !== bBuiltIn) return aBuiltIn ? 1 : -1;
return String(a.name || '').localeCompare(String(b.name || ''), undefined, { sensitivity: 'base' });
});
var cards = sortedSkills.map(function(s) {
var kindColor = s.kind === 'tool' ? 'var(--accent)' : s.kind === 'cognitive' ? 'var(--success)' : s.kind === 'multimodal' ? 'var(--warning)' : 'var(--muted)';
var actions = '';
var builtIn = !!s.built_in || String(s.kind || '').toLowerCase() === 'builtin';
var kindLabel = builtIn ? 'builtin' : (s.kind || '');
var builtInBadge = '';
var builtInNameTag = '';
var toggleDisabled = '';
var toggleTitle = '';
var toggleClass = 'toggle';
if (s.id && !builtIn) {
actions = '<div style="display:flex;justify-content:flex-end;gap:0.5rem;margin-top:0.75rem">'
+ '<button class="btn secondary" data-skill-edit-open="' + esc(s.id || '') + '" data-skill-name="' + esc(s.name || '') + '" style="font-size:0.75rem;padding:0.25rem 0.6rem">' + uiBtnLabel('edit', 'Edit') + '</button>'
+ '<button class="btn secondary" data-skill-delete="' + esc(s.id || '') + '" data-skill-name="' + esc(s.name || '') + '" style="border-color:var(--error);color:var(--error);font-size:0.75rem;padding:0.25rem 0.6rem">' + uiBtnLabel('trash', 'Delete') + '</button>'
+ '</div>';
}
var usageCount = s.usage_count != null ? s.usage_count : 0;
var lastUsedLabel = formatTimestampLabel(s.last_used_at);
var toggleHtml = builtIn
? '<span class="badge" style="background:var(--success);color:#fff;font-size:0.7rem;padding:0.15rem 0.5rem;border-radius:0.25rem">ALWAYS ON</span>'
: '<label class="' + toggleClass + '"><input type="checkbox" data-skill-toggle="' + esc(s.id || '') + '" data-skill-name="' + esc(s.name || '') + '"' + (s.enabled ? ' checked' : '') + toggleDisabled + toggleTitle + '><span class="toggle-track"></span></label>';
return '<div class="card skill-card" data-skill-open="' + esc(s.id || '') + '" data-skill-name="' + esc(s.name || '') + '"><div class="skill-header"><div class="skill-info"><div class="card-title" style="color:' + kindColor + '">' + esc(kindLabel) + '</div><div class="card-value">' + esc(s.name || '') + '</div></div>' + toggleHtml + '</div><p style="font-size:0.8125rem;color:var(--muted);margin-top:0.5rem">' + esc(s.description || '') + '</p><div style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:0.5rem;margin-top:0.75rem"><div style="padding:0.55rem 0.65rem;background:var(--surface);border:1px solid var(--border-soft);border-radius:6px"><div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em">Usage</div><div style="font-size:1rem;font-weight:700;color:var(--text)">' + esc(String(usageCount)) + '</div></div><div style="padding:0.55rem 0.65rem;background:var(--surface);border:1px solid var(--border-soft);border-radius:6px"><div style="font-size:0.65rem;color:var(--muted);text-transform:uppercase;letter-spacing:0.05em">Last Used</div><div style="font-size:0.78rem;font-weight:600;color:var(--text)">' + esc(lastUsedLabel) + '</div></div></div>' + actions + '</div>';
}).join('');
return tabs + '<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap"><button class="btn" id="btn-reload-skills">' + uiBtnLabel('refresh', 'Reload skills') + '</button><button class="btn secondary" id="btn-create-skill">' + uiBtnLabel('add', 'Create Skill') + '</button><span style="font-size:0.8125rem;color:var(--muted)">' + enabledCount + ' / ' + skills.length + ' enabled</span></div><div class="skills-grid" style="margin-top:1rem">' + cards + '</div>';
});
};
App._closeSkillDeleteModal = function() {
var modal = document.getElementById('skill-delete-modal');
if (modal) modal.remove();
};
App._closeSkillDetailModal = function() {
var modal = document.getElementById('skill-detail-modal');
if (modal) modal.remove();
};
App._openSkillDetailModal = function(skillId, skillName) {
var self = this;
if (!skillId) {
toast('Skill definition is unavailable for this entry');
return;
}
self._closeSkillDetailModal();
api('/api/skills/' + encodeURIComponent(skillId)).then(function(skill) {
var modal = document.createElement('div');
modal.id = 'skill-detail-modal';
modal.style.position = 'fixed';
modal.style.inset = '0';
modal.style.zIndex = '1000';
modal.style.background = 'rgba(0,0,0,0.65)';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
modal.style.padding = '1.5rem';
var sourceContent = skill.source_content || '';
var sourcePath = skill.source_path || '';
var summary = skill.description || 'No description available.';
var kindLabel = skill.built_in ? 'builtin' : (skill.kind || 'skill');
var canEdit = !!sourcePath && !skill.built_in;
var metaBadges = ''
+ '<span class="badge muted">' + esc(kindLabel) + '</span>'
+ '<span class="badge">' + esc(skill.risk_level || 'Unknown') + '</span>'
+ '<span class="badge ' + (skill.enabled ? 'success' : 'muted') + '">' + (skill.enabled ? 'enabled' : 'disabled') + '</span>';
var html = ''
+ '<div style="width:min(960px, 100%);max-height:calc(100vh - 3rem);overflow:hidden;background:var(--bg);border:1px solid var(--border);border-radius:8px;box-shadow:0 20px 40px rgba(0,0,0,0.45);display:flex;flex-direction:column">'
+ ' <div style="padding:1rem 1.25rem;border-bottom:1px solid var(--border);display:flex;align-items:flex-start;justify-content:space-between;gap:1rem">'
+ ' <div style="min-width:0">'
+ ' <div style="font-weight:700;font-size:1rem">' + esc(skill.name || skillName || 'Skill') + '</div>'
+ ' <div style="margin-top:0.35rem;display:flex;gap:0.4rem;flex-wrap:wrap">' + metaBadges + '</div>'
+ ' <div style="margin-top:0.6rem;font-size:0.85rem;color:var(--muted);line-height:1.45">' + esc(summary) + '</div>'
+ ' </div>'
+ ' <div style="display:flex;gap:0.5rem;align-items:center">'
+ (canEdit ? '<button class="btn secondary" data-skill-edit-start style="padding:0.25rem 0.6rem">Edit</button>' : '')
+ ' <button class="btn secondary" data-skill-detail-close style="padding:0.25rem 0.6rem">Close</button>'
+ ' </div>'
+ ' </div>'
+ ' <div style="padding:1rem 1.25rem;overflow:auto">'
+ (sourcePath ? '<div style="margin-bottom:0.75rem;font-size:0.75rem;color:var(--muted)">Source: <span style="font-family:var(--font-mono);color:var(--text)">' + esc(sourcePath) + '</span></div>' : '')
+ (sourceContent
? '<div data-skill-source-view>'
+ '<div style="padding:1rem;background:var(--surface);border:1px solid var(--border);border-radius:6px;line-height:1.6;font-size:0.85rem" class="md-content">' + renderSafeMarkdown(sourceContent) + '</div>'
+ '</div>'
: '<div class="card" style="color:var(--muted)">No source definition is available for this skill.</div>')
+ (canEdit
? '<div data-skill-source-editor style="display:none">'
+ '<textarea id="skill-detail-editor" style="width:100%;min-height:420px;padding:1rem;background:var(--surface);border:1px solid var(--border);border-radius:6px;white-space:pre;font-family:var(--font-mono);font-size:0.78rem;line-height:1.45;color:var(--text);resize:vertical">' + esc(sourceContent) + '</textarea>'
+ '<div style="display:flex;justify-content:flex-end;gap:0.5rem;margin-top:0.75rem">'
+ '<button class="btn secondary" data-skill-edit-cancel>Cancel</button>'
+ '<button class="btn" data-skill-edit-save="' + esc(skill.id || '') + '">Save</button>'
+ '</div>'
+ '</div>'
: '')
+ ' </div>'
+ '</div>';
setHtml(modal, html);
document.body.appendChild(modal);
modal.addEventListener('click', function(ev) {
if (ev.target.closest('[data-skill-edit-start]')) {
var view = modal.querySelector('[data-skill-source-view]');
var editor = modal.querySelector('[data-skill-source-editor]');
var editBtn = modal.querySelector('[data-skill-edit-start]');
if (view) view.style.display = 'none';
if (editor) editor.style.display = 'block';
if (editBtn) editBtn.style.display = 'none';
var textarea = document.getElementById('skill-detail-editor');
if (textarea) textarea.focus();
return;
}
if (ev.target.closest('[data-skill-edit-cancel]')) {
var viewCancel = modal.querySelector('[data-skill-source-view]');
var editorCancel = modal.querySelector('[data-skill-source-editor]');
var editBtnCancel = modal.querySelector('[data-skill-edit-start]');
if (viewCancel) viewCancel.style.display = '';
if (editorCancel) editorCancel.style.display = 'none';
if (editBtnCancel) editBtnCancel.style.display = '';
return;
}
var saveBtn = ev.target.closest('[data-skill-edit-save]');
if (saveBtn) {
var skillEditId = saveBtn.getAttribute('data-skill-edit-save') || '';
var textareaSave = document.getElementById('skill-detail-editor');
var nextSource = textareaSave ? textareaSave.value : '';
saveBtn.disabled = true;
api('/api/skills/' + encodeURIComponent(skillEditId), {
method: 'PUT',
headers: authHeaders({ 'Content-Type': 'application/json', 'Accept': 'application/json' }),
body: JSON.stringify({ source_content: nextSource })
}).then(function() {
toast('Skill updated');
self._closeSkillDetailModal();
App.navigate('skills');
self._openSkillDetailModal(skillEditId, skill.name || skillName);
}).catch(function(err) {
toast(err.message || 'Failed to update skill');
}).finally(function() {
saveBtn.disabled = false;
});
return;
}
if (ev.target === modal || ev.target.closest('[data-skill-detail-close]')) {
self._closeSkillDetailModal();
}
});
}).catch(function(err) {
toast(err.message || 'Failed to load skill definition');
});
};
App._closeSkillEditorModal = function() {
var modal = document.getElementById('skill-editor-modal');
if (modal) modal.remove();
};
App._openSkillEditorModal = function(skillId, existingContent) {
// skillId === null → create mode (POST)
// skillId !== null → edit mode (PUT)
var self = this;
self._closeSkillEditorModal();
var isCreate = !skillId;
var defaultTemplate = '---\n# Skill name: lowercase, hyphenated (e.g., "combat-tactics")\nname: my-new-skill\n\n# Brief description of what this skill teaches the agent\ndescription: Describe what this skill does\n\n# Kind: "instruction" (behavioral guidance), "operational" (tool/process), or "knowledge" (reference material)\nkind: instruction\n\n# Triggers: keywords that activate this skill when they appear in user messages\ntriggers:\n keywords:\n - "keyword1"\n - "keyword2"\n---\n\n# Skill Title\n\nWrite your skill instructions here. The agent will follow these\ninstructions when the trigger keywords match a user\\\'s message.\n\n## Section Heading\n\nUse markdown formatting for structure. The agent reads this content\nas guidance for how to behave, what to prioritize, and what patterns\nto follow.\n';
var initialContent = existingContent != null ? existingContent : defaultTemplate;
var modal = document.createElement('div');
modal.id = 'skill-editor-modal';
modal.style.position = 'fixed';
modal.style.inset = '0';
modal.style.zIndex = '1000';
modal.style.background = 'rgba(0,0,0,0.65)';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
modal.style.padding = '1.5rem';
var html = ''
+ '<div style="width:min(780px,100%);max-height:calc(100vh - 3rem);overflow:hidden;background:var(--bg);border:1px solid var(--border);border-radius:8px;box-shadow:0 20px 40px rgba(0,0,0,0.45);display:flex;flex-direction:column">'
+ ' <div style="padding:1rem 1.25rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:1rem">'
+ ' <div style="font-weight:700;font-size:1rem">' + (isCreate ? 'Create Skill' : 'Edit Skill') + '</div>'
+ ' <button class="btn secondary" data-skill-editor-close style="padding:0.25rem 0.6rem">Close</button>'
+ ' </div>'
+ ' <div style="padding:1rem 1.25rem;overflow:auto;display:flex;flex-direction:column;gap:0.75rem">'
+ ' <div style="font-size:0.78rem;color:var(--muted);line-height:1.45">Write your skill in YAML+Markdown format. The YAML frontmatter (between <code>---</code> markers) defines metadata; the body below is the skill content.</div>'
+ ' <textarea id="skill-editor-textarea"'
+ ' style="width:100%;min-height:420px;padding:1rem;background:var(--surface);border:1px solid var(--border);border-radius:6px;font-family:var(--font-mono);font-size:0.78rem;line-height:1.55;color:var(--text);resize:vertical;tab-size:2"'
+ ' >' + esc(initialContent) + '</textarea>'
+ ' <div style="display:flex;justify-content:flex-end;gap:0.5rem">'
+ ' <button class="btn secondary" data-skill-editor-cancel>Cancel</button>'
+ ' <button class="btn" data-skill-editor-save="' + esc(skillId || '') + '" data-skill-editor-mode="' + (isCreate ? 'create' : 'edit') + '">' + (isCreate ? 'Create' : 'Save') + '</button>'
+ ' </div>'
+ ' </div>'
+ '</div>';
setHtml(modal, html);
document.body.appendChild(modal);
var textarea = document.getElementById('skill-editor-textarea');
if (textarea) textarea.focus();
modal.addEventListener('click', function(ev) {
if (ev.target === modal || ev.target.closest('[data-skill-editor-close]') || ev.target.closest('[data-skill-editor-cancel]')) {
self._closeSkillEditorModal();
return;
}
var saveBtn = ev.target.closest('[data-skill-editor-save]');
if (saveBtn) {
var editId = saveBtn.getAttribute('data-skill-editor-save') || '';
var mode = saveBtn.getAttribute('data-skill-editor-mode') || 'create';
var ta = document.getElementById('skill-editor-textarea');
var content = ta ? ta.value : '';
saveBtn.disabled = true;
var url = mode === 'create' ? '/api/skills' : '/api/skills/' + encodeURIComponent(editId);
var method = mode === 'create' ? 'POST' : 'PUT';
fetch(url, {
method: method,
headers: authHeaders({ 'Content-Type': 'application/json', 'Accept': 'application/json' }),
body: JSON.stringify({ source_content: content })
}).then(function(resp) {
if (!resp.ok) {
return resp.text().then(function(t) { throw new Error(t || resp.statusText); });
}
return resp.json();
}).then(function() {
toast(mode === 'create' ? 'Skill created' : 'Skill updated');
self._closeSkillEditorModal();
App.refreshSkills();
}).catch(function(err) {
toast(err.message || 'Failed to save skill');
}).finally(function() {
saveBtn.disabled = false;
});
return;
}
});
};
App._openSkillDeleteModal = function(skillId, skillName) {
var self = this;
self._closeSkillDeleteModal();
var modal = document.createElement('div');
modal.id = 'skill-delete-modal';
modal.style.position = 'fixed';
modal.style.inset = '0';
modal.style.zIndex = '1000';
modal.style.background = 'rgba(0,0,0,0.65)';
modal.style.display = 'flex';
modal.style.alignItems = 'center';
modal.style.justifyContent = 'center';
modal.style.padding = '1.5rem';
var html = ''
+ '<div style="width:min(520px, 100%);background:var(--bg);border:1px solid var(--border);border-radius:8px;box-shadow:0 20px 40px rgba(0,0,0,0.45)">'
+ ' <div style="padding:1rem 1.25rem;border-bottom:1px solid var(--border);display:flex;align-items:center;justify-content:space-between;gap:1rem">'
+ ' <div style="font-weight:700">Delete Skill</div>'
+ ' <button class="btn secondary" data-skill-delete-cancel style="padding:0.25rem 0.6rem">Close</button>'
+ ' </div>'
+ ' <div style="padding:1rem 1.25rem;color:var(--muted);font-size:0.9rem;line-height:1.45">'
+ ' This action permanently removes the skill record from the runtime database.'
+ ' </div>'
+ ' <div style="padding:0 1.25rem 1rem 1.25rem">'
+ ' <div style="margin-bottom:0.45rem;font-size:0.75rem;text-transform:uppercase;color:var(--muted);letter-spacing:0.05em">Confirm skill name</div>'
+ ' <div style="margin-bottom:0.6rem;font-family:var(--font-mono);font-size:0.9rem;color:var(--text)">' + esc(skillName) + '</div>'
+ ' <input id="skill-delete-confirm-input" type="text" placeholder="Type exact skill name"'
+ ' style="width:100%;padding:0.55rem 0.7rem;background:var(--surface);border:1px solid var(--border);border-radius:4px;color:var(--text);font-family:var(--font-mono)">'
+ ' <div id="skill-delete-confirm-hint" style="margin-top:0.5rem;font-size:0.75rem;color:var(--muted)">Type the exact name to enable deletion.</div>'
+ ' </div>'
+ ' <div style="padding:0.9rem 1.25rem;border-top:1px solid var(--border);display:flex;gap:0.6rem;justify-content:flex-end">'
+ ' <button class="btn secondary" data-skill-delete-cancel>Cancel</button>'
+ ' <button class="btn" id="skill-delete-confirm-btn" disabled style="border-color:var(--error);color:var(--error);opacity:0.45;pointer-events:none">Delete Skill</button>'
+ ' </div>'
+ '</div>';
setHtml(modal, html);
document.body.appendChild(modal);
var input = document.getElementById('skill-delete-confirm-input');
var btn = document.getElementById('skill-delete-confirm-btn');
var hint = document.getElementById('skill-delete-confirm-hint');
if (input) input.focus();
function syncState() {
if (!input || !btn) return;
var matches = input.value === skillName;
btn.disabled = !matches;
btn.style.opacity = matches ? '' : '0.45';
btn.style.pointerEvents = matches ? '' : 'none';
if (hint) {
hint.style.color = matches ? 'var(--success)' : 'var(--muted)';
hint.textContent = matches ? 'Name matches. You can delete this skill.' : 'Type the exact name to enable deletion.';
}
}
if (input) {
input.addEventListener('input', syncState);
input.addEventListener('keydown', function(ev) {
if (ev.key === 'Enter' && btn && !btn.disabled) btn.click();
});
}
modal.addEventListener('click', function(ev) {
if (ev.target === modal || ev.target.closest('[data-skill-delete-cancel]')) {
self._closeSkillDeleteModal();
return;
}
if (ev.target.closest('#skill-delete-confirm-btn')) {
api('/api/skills/' + encodeURIComponent(skillId), { method: 'DELETE' })
.then(function(resp) {
self._closeSkillDeleteModal();
toast('Deleted skill: ' + (resp.name || skillName));
App.navigate('skills');
})
.catch(function(err) {
toast(err.message || 'Failed to delete skill');
});
}
});
};