// settings-agents.js — 설정: AI 에이전트 관리(AGENTS.md 에디터), 스킬 설치/토글/편집.
// extracted from app.js (v4.3.19-3 split).
function closeSettings() {
document.getElementById('settings-modal').style.display = 'none';
_searchSettingsLoaded = false;
_modelSettingsLoaded = false;
}
function ensureAgentMdEditor() {
if (agentMdEditor || typeof CodeMirror === 'undefined') return;
const wrap = document.getElementById('agent-md-editor-wrap');
if (!wrap) return;
agentMdEditor = CodeMirror(wrap, {
value: '',
mode: 'markdown',
lineNumbers: true,
lineWrapping: true,
tabSize: 2,
indentWithTabs: false,
theme: 'default',
});
agentMdEditor.setSize('100%', 240);
}
function getAgentFromForm() {
const profile = document.getElementById('agent-edit-profile').value.trim();
const maxStepsRaw = document.getElementById('agent-edit-max-steps').value;
const maxSteps = Number(maxStepsRaw);
const spawnAllowlist = (document.getElementById('agent-edit-spawn-allowlist').value || '')
.split(',')
.map(s => s.trim())
.filter(Boolean);
const agent = {
id: document.getElementById('agent-edit-id').value.trim(),
name: document.getElementById('agent-edit-name').value.trim(),
emoji: document.getElementById('agent-edit-emoji').value.trim(),
description: document.getElementById('agent-edit-description').value.trim(),
workspace: document.getElementById('agent-edit-workspace').value.trim(),
model: document.getElementById('agent-edit-model').value.trim(),
cronSchedule: document.getElementById('agent-edit-cron').value.trim(),
minimalPrompt: document.getElementById('agent-edit-minimal').checked,
default: document.getElementById('agent-edit-default').checked,
canSpawn: document.getElementById('agent-edit-can-spawn').checked,
spawnAllowlist,
tools: {},
};
if (profile) agent.tools.profile = profile;
if (Number.isFinite(maxSteps) && maxSteps > 0) agent.maxSteps = Math.floor(maxSteps);
if (!agent.tools.profile) delete agent.tools;
if (!agent.spawnAllowlist.length) delete agent.spawnAllowlist;
return agent;
}
function setAgentForm(agent) {
const a = agent || {};
document.getElementById('agent-edit-id').value = a.id || '';
document.getElementById('agent-edit-name').value = a.name || '';
document.getElementById('agent-edit-emoji').value = a.emoji || '';
document.getElementById('agent-edit-description').value = a.description || '';
document.getElementById('agent-edit-workspace').value = a.workspace || '';
document.getElementById('agent-edit-model').value = a.model || '';
document.getElementById('agent-edit-cron').value = a.cronSchedule || '';
document.getElementById('agent-edit-max-steps').value = (a.maxSteps || '') + '';
document.getElementById('agent-edit-default').checked = a.default === true;
document.getElementById('agent-edit-minimal').checked = a.minimalPrompt === true;
document.getElementById('agent-edit-can-spawn').checked = a.canSpawn === true;
document.getElementById('agent-edit-spawn-allowlist').value = Array.isArray(a.spawnAllowlist) ? a.spawnAllowlist.join(',') : '';
document.getElementById('agent-edit-profile').value = a.tools?.profile || '';
}
function renderAgentsList() {
const el = document.getElementById('settings-agents-list');
if (!el) return;
if (!agentsConfigList.length) {
el.innerHTML = '
No agents found.
';
return;
}
el.innerHTML = agentsConfigList.map((a) => {
const selected = a.id === selectedAgentId;
const lastRun = a.lastRun?.finishedAt ? new Date(a.lastRun.finishedAt).toLocaleString() : 'never';
const heartbeat = a.lastHeartbeatAt ? new Date(a.lastHeartbeatAt).toLocaleString() : 'never';
const badge = a.default ? 'default' : '';
return `
`;
}).join('');
}
function findSelectedAgent() {
return agentsConfigList.find(a => a.id === selectedAgentId) || null;
}
function agentFormNew() {
selectedAgentId = '';
setAgentForm({
id: '',
name: '',
emoji: '',
description: '',
workspace: '',
model: '',
cronSchedule: '',
maxSteps: '',
default: false,
minimalPrompt: true,
canSpawn: false,
spawnAllowlist: [],
tools: {},
});
if (agentMdEditor) agentMdEditor.setValue('');
const mdPath = document.getElementById('agent-md-path');
if (mdPath) mdPath.textContent = '에이전트를 선택하거나 저장하면 AGENTS.md를 불러옵니다';
const resultEl = document.getElementById('agent-spawn-result');
if (resultEl) resultEl.textContent = '';
renderAgentsList();
}
async function selectAgent(id) {
selectedAgentId = id;
const selected = findSelectedAgent();
setAgentForm(selected);
renderAgentsList();
await loadSelectedAgentMd();
await loadAgentRunHistory();
}
async function loadAgentsTab() {
ensureAgentMdEditor();
try {
const data = await api('/api/agents');
agentsConfigList = Array.isArray(data?.agents) ? data.agents : [];
if (!selectedAgentId || !agentsConfigList.some(a => a.id === selectedAgentId)) {
selectedAgentId = data?.defaultAgentId || agentsConfigList[0]?.id || '';
}
renderAgentsList();
const selected = findSelectedAgent();
setAgentForm(selected);
if (selected) {
await loadSelectedAgentMd();
await loadAgentRunHistory();
} else {
agentFormNew();
}
} catch (err) {
addProcessEntry('error', `Failed to load agents: ${err.message}`);
}
}
async function saveAgentFromForm() {
const agent = getAgentFromForm();
if (!agent.id) {
alert('Agent ID is required');
return;
}
if (!agent.name) {
alert('Agent name is required');
return;
}
try {
const exists = agentsConfigList.some(a => a.id === agent.id);
const endpoint = exists ? `/api/agents/${encodeURIComponent(agent.id)}` : '/api/agents';
const method = exists ? 'PUT' : 'POST';
const data = await api(endpoint, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ agent }),
});
if (!data?.success) throw new Error(data?.error || 'save failed');
selectedAgentId = data?.agent?.id || agent.id;
addProcessEntry('info', `Agent "${selectedAgentId}" saved.`);
await loadAgentsTab();
} catch (err) {
alert(`Failed to save agent: ${err.message}`);
}
}
async function deleteSelectedAgent() {
if (!selectedAgentId) return;
if (!confirm(`Delete agent "${selectedAgentId}"?`)) return;
try {
await api(`/api/agents/${encodeURIComponent(selectedAgentId)}`, { method: 'DELETE' });
addProcessEntry('info', `Agent "${selectedAgentId}" deleted.`);
selectedAgentId = '';
await loadAgentsTab();
} catch (err) {
alert(`Failed to delete agent: ${err.message}`);
}
}
async function loadSelectedAgentMd() {
ensureAgentMdEditor();
if (!selectedAgentId || !agentMdEditor) return;
try {
const data = await api(`/api/agents/${encodeURIComponent(selectedAgentId)}/agents-md`);
agentMdEditor.setValue(data?.content || '');
const pathEl = document.getElementById('agent-md-path');
if (pathEl) pathEl.textContent = data?.path || '';
} catch (err) {
addProcessEntry('error', `Failed to load AGENTS.md: ${err.message}`);
}
}
async function saveSelectedAgentMd() {
if (!selectedAgentId || !agentMdEditor) return;
try {
const content = agentMdEditor.getValue();
await api(`/api/agents/${encodeURIComponent(selectedAgentId)}/agents-md`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content }),
});
addProcessEntry('final', `Saved AGENTS.md for "${selectedAgentId}".`);
} catch (err) {
alert(`Failed to save AGENTS.md: ${err.message}`);
}
}
async function runSelectedAgentOnce() {
if (!selectedAgentId) return;
const task = (document.getElementById('agent-spawn-task').value || '').trim();
if (!task) {
alert('Provide a task first.');
return;
}
const out = document.getElementById('agent-spawn-result');
if (out) out.textContent = '실행 중...';
try {
const data = await api(`/api/agents/${encodeURIComponent(selectedAgentId)}/spawn`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ task }),
});
const result = data?.result || {};
if (out) {
const head = result.success ? `Success (${result.durationMs || 0}ms)` : `Failed: ${result.error || 'unknown error'}`;
out.textContent = `${head}\n\n${String(result.result || '').slice(0, 2500)}`;
}
await loadAgentsTab();
} catch (err) {
if (out) out.textContent = `Run failed: ${err.message}`;
}
}
async function loadAgentRunHistory() {
const el = document.getElementById('agent-run-history');
if (!el) return;
if (!selectedAgentId) { el.innerHTML = ''; return; }
try {
const data = await api(`/api/agents/history?agentId=${encodeURIComponent(selectedAgentId)}&limit=12`);
const rows = Array.isArray(data?.history) ? data.history : [];
if (!rows.length) {
el.innerHTML = 'No runs yet.
';
return;
}
el.innerHTML = rows.map((r) => {
const color = r.success ? '#1a6e35' : '#9c1a1a';
const when = new Date(r.finishedAt || r.startedAt || Date.now()).toLocaleString();
const label = `${r.trigger || 'manual'} • ${r.durationMs || 0}ms`;
const preview = String(r.resultPreview || r.error || '').slice(0, 140);
return `
${r.success ? 'success' : 'failed'}
${escHtml(when)}
${escHtml(label)}
${escHtml(preview)}
`;
}).join('');
} catch (err) {
el.innerHTML = `Failed to load history: ${escHtml(err.message)}
`;
}
}
const logLines = [];
function log(text, type = 'log') {
const ts = new Date().toLocaleTimeString();
logLines.push({ text: `[${ts}] ${text}`, type });
if (logLines.length > 100) logLines.shift();
const lp = document.getElementById('log-panel');
if (lp) {
lp.innerHTML = logLines.map(l => `${l.text}
`).join('');
lp.scrollTop = lp.scrollHeight;
}
}
// ---- Skills management (v2 - local skills system) ----
let skillsCache = [];
let editingSkillId = null;
let skillsTargetUser = ''; // '' = global (admin) or own username (regular user)
async function initSkillsUserSelector() {
const selector = document.getElementById('skills-user-selector');
if (!selector) return;
if (!currentUser) { selector.style.display = 'none'; return; }
if (currentUser.role !== 'admin') {
selector.style.display = 'none';
skillsTargetUser = currentUser.username || '';
return;
}
selector.style.display = 'block';
try {
const data = await api('/api/users');
const sel = document.getElementById('skills-target-user');
if (!sel || !data.users) return;
const opts = [''];
for (const u of data.users) {
const selected = u.username === skillsTargetUser ? ' selected' : '';
opts.push(``);
}
sel.innerHTML = opts.join('');
} catch {}
}
function onSkillsUserChange() {
const sel = document.getElementById('skills-target-user');
skillsTargetUser = sel ? sel.value : '';
loadInstalledSkills();
}
async function loadInstalledSkills() {
const el = document.getElementById('skills-list');
el.innerHTML = ' Loading...
';
try {
const effectiveUser = skillsTargetUser || currentUser?.username || null;
const url = effectiveUser ? `/api/skills?username=${encodeURIComponent(effectiveUser)}` : '/api/skills';
const data = await api(url);
const skills = data.skills || [];
skillsCache = skills;
if (skills.length === 0) {
el.innerHTML = 'No skills yet.
Click "+ Create" to make one!
';
} else {
el.innerHTML = skills.map(s => {
const skillEmoji = (s.emoji && s.emoji !== '🦞' && s.emoji !== '⚠️') ? s.emoji : '🧩';
const globalBadge = s.globalOnly ? '[전체]' : '';
return `
${escHtml(skillEmoji)}
${escHtml(s.name)}${globalBadge}
${escHtml(s.description || 'No description')}
`;
}).join('');
renderSkillEligibility(skills);
}
} catch (err) {
el.innerHTML = `Error: ${err.message}
`;
}
}
async function toggleSkill(id) {
const skillMeta = (skillsCache || []).find(s => s.id === id);
if (skillMeta && skillMeta.eligible === false && !skillMeta.enabled) {
addProcessEntry('error', skillMeta.eligibleReason || 'This skill is currently unavailable.');
return;
}
// globalOnly 스킬은 per-user 모드에서 토글 불가 (서버가 전역으로 처리)
if (skillMeta?.globalOnly && skillsTargetUser) {
addProcessEntry('error', `"${skillMeta.name}" 스킬은 글로벌 전용입니다.`);
return;
}
try {
const effectiveUser = skillsTargetUser || currentUser?.username || null;
const body = effectiveUser ? JSON.stringify({ username: effectiveUser }) : undefined;
const res = await api(`/api/skills/${encodeURIComponent(id)}/toggle`, { method: 'POST', body });
if (!res.success) {
addProcessEntry('error', res.error || 'Failed to toggle skill.');
return;
}
const label = res.targetUser ? ` (${res.targetUser})` : '';
addProcessEntry('info', `Skill "${res.skill.name}"${label}: ${res.skill.enabled ? 'ENABLED' : 'DISABLED'}`);
loadInstalledSkills();
} catch (err) {
addProcessEntry('error', `Failed to toggle skill: ${err.message}`);
}
}
async function deleteSkill(id) {
if (!confirm(`Delete skill "${id}"?`)) return;
try {
const res = await api(`/api/skills/${encodeURIComponent(id)}`, { method: 'DELETE' });
if (res.success) {
addProcessEntry('info', `Skill "${id}" deleted.`);
loadInstalledSkills();
} else {
alert(`Delete failed: ${res.error}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
}
}
function openCreateSkillModal() {
editingSkillId = null;
document.getElementById('skill-modal-title').textContent = '스킬 생성';
document.getElementById('skill-modal-save-btn').textContent = '스킬 생성';
document.getElementById('skill-modal-name').value = '';
document.getElementById('skill-modal-emoji').value = '';
document.getElementById('skill-modal-desc').value = '';
document.getElementById('skill-modal-instructions').value = '';
document.getElementById('skill-modal').style.display = 'flex';
document.getElementById('skill-modal-name').focus();
}
async function editSkill(id) {
try {
const res = await api(`/api/skills/${encodeURIComponent(id)}`);
if (!res.success) { alert('Skill not found'); return; }
const s = res.skill;
editingSkillId = id;
document.getElementById('skill-modal-title').textContent = '스킬 편집';
document.getElementById('skill-modal-save-btn').textContent = '변경 저장';
document.getElementById('skill-modal-name').value = s.name || '';
document.getElementById('skill-modal-emoji').value = s.emoji || '';
document.getElementById('skill-modal-desc').value = s.description || '';
document.getElementById('skill-modal-instructions').value = s.instructions || '';
document.getElementById('skill-modal').style.display = 'flex';
document.getElementById('skill-modal-name').focus();
} catch (err) {
alert(`Error loading skill: ${err.message}`);
}
}
function closeSkillModal() {
document.getElementById('skill-modal').style.display = 'none';
editingSkillId = null;
}
async function saveSkillFromModal() {
const name = document.getElementById('skill-modal-name').value.trim();
const rawEmoji = document.getElementById('skill-modal-emoji').value.trim();
const emoji = (rawEmoji === '🧩' || rawEmoji === '🦞' || rawEmoji === '⚠️') ? rawEmoji : '🧩';
const description = document.getElementById('skill-modal-desc').value.trim();
const instructions = document.getElementById('skill-modal-instructions').value.trim();
if (!name) { alert('Name is required'); return; }
if (!instructions) { alert('Instructions are required'); return; }
const btn = document.getElementById('skill-modal-save-btn');
btn.disabled = true;
btn.textContent = '저장 중...';
try {
let res;
if (editingSkillId) {
// Update existing
res = await api(`/api/skills/${encodeURIComponent(editingSkillId)}`, {
method: 'PUT',
body: JSON.stringify({ name, emoji, description, instructions }),
});
} else {
// Create new
res = await api('/api/skills', {
method: 'POST',
body: JSON.stringify({ name, emoji, description, instructions }),
});
}
if (res.success) {
addProcessEntry('info', `Skill "${name}" ${editingSkillId ? 'updated' : 'created'}!`);
closeSkillModal();
loadInstalledSkills();
} else {
alert(`Failed: ${res.error}`);
}
} catch (err) {
alert(`Error: ${err.message}`);
} finally {
btn.disabled = false;
btn.textContent = editingSkillId ? '변경 저장' : '스킬 생성';
}
}