d93385b373
CI / Check / macos-latest (push) Waiting to run
CI / Check / ubuntu-latest (push) Waiting to run
CI / Check / windows-latest (push) Waiting to run
CI / Test / macos-latest (push) Waiting to run
CI / Test / ubuntu-latest (push) Waiting to run
CI / Test / windows-latest (push) Waiting to run
CI / Clippy (push) Waiting to run
CI / Format (push) Waiting to run
CI / Security Audit (push) Waiting to run
CI / Secrets Scan (push) Waiting to run
CI / Install Script Smoke Test (push) Waiting to run
814 lines
33 KiB
JavaScript
814 lines
33 KiB
JavaScript
// OpenFang Agents Page — Multi-step spawn wizard, detail view with tabs, file editor, personality presets
|
|
'use strict';
|
|
|
|
/** Escape a string for use inside TOML triple-quoted strings ("""\n...\n""").
|
|
* Backslashes are escaped, and runs of 3+ consecutive double-quotes are
|
|
* broken up so the TOML parser never sees an unintended closing delimiter.
|
|
*/
|
|
function tomlMultilineEscape(s) {
|
|
return s.replace(/\\/g, '\\\\').replace(/"""/g, '""\\"');
|
|
}
|
|
|
|
/** Escape a string for use inside a TOML basic (single-line) string ("...").
|
|
* Backslashes, double-quotes, and common control chars are escaped.
|
|
*/
|
|
function tomlBasicEscape(s) {
|
|
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
}
|
|
|
|
function agentsPage() {
|
|
return {
|
|
tab: 'agents',
|
|
activeChatAgent: null,
|
|
// -- Agents state --
|
|
showSpawnModal: false,
|
|
showDetailModal: false,
|
|
detailAgent: null,
|
|
spawnMode: 'wizard',
|
|
spawning: false,
|
|
spawnToml: '',
|
|
filterState: 'all',
|
|
loading: true,
|
|
loadError: '',
|
|
spawnForm: {
|
|
name: '',
|
|
provider: 'groq',
|
|
model: 'llama-3.3-70b-versatile',
|
|
systemPrompt: 'You are a helpful assistant.',
|
|
profile: 'full',
|
|
caps: { memory_read: true, memory_write: true, network: false, shell: false, agent_spawn: false }
|
|
},
|
|
|
|
// -- Multi-step wizard state --
|
|
spawnProviders: [], // populated from /api/providers on wizard open
|
|
spawnProvidersLoading: false,
|
|
spawnStep: 1,
|
|
spawnIdentity: { emoji: '', color: '#FF5C00', archetype: '' },
|
|
selectedPreset: '',
|
|
soulContent: '',
|
|
emojiOptions: [
|
|
'\u{1F916}', '\u{1F4BB}', '\u{1F50D}', '\u{270D}\uFE0F', '\u{1F4CA}', '\u{1F6E0}\uFE0F',
|
|
'\u{1F4AC}', '\u{1F393}', '\u{1F310}', '\u{1F512}', '\u{26A1}', '\u{1F680}',
|
|
'\u{1F9EA}', '\u{1F3AF}', '\u{1F4D6}', '\u{1F9D1}\u200D\u{1F4BB}', '\u{1F4E7}', '\u{1F3E2}',
|
|
'\u{2764}\uFE0F', '\u{1F31F}', '\u{1F527}', '\u{1F4DD}', '\u{1F4A1}', '\u{1F3A8}'
|
|
],
|
|
archetypeOptions: ['Assistant', 'Researcher', 'Coder', 'Writer', 'DevOps', 'Support', 'Analyst', 'Custom'],
|
|
_personalityPresetsLoaded: false,
|
|
personalityPresets: [], // Loaded dynamically with i18n
|
|
|
|
// Load personality presets with i18n
|
|
loadPersonalityPresets: function() {
|
|
if (this._personalityPresetsLoaded) return;
|
|
var t = typeof window.t === 'function' ? window.t : function(s) { return s; };
|
|
this.personalityPresets = [
|
|
{ id: 'professional', label: t('presets.professional'), soul: t('presets.professional_soul') },
|
|
{ id: 'friendly', label: t('presets.friendly'), soul: t('presets.friendly_soul') },
|
|
{ id: 'technical', label: t('presets.technical'), soul: t('presets.technical_soul') },
|
|
{ id: 'creative', label: t('presets.creative'), soul: t('presets.creative_soul') },
|
|
{ id: 'concise', label: t('presets.concise'), soul: t('presets.concise_soul') },
|
|
{ id: 'mentor', label: t('presets.mentor'), soul: t('presets.mentor_soul') }
|
|
];
|
|
this._personalityPresetsLoaded = true;
|
|
},
|
|
|
|
// -- Detail modal tabs --
|
|
detailTab: 'info',
|
|
agentFiles: [],
|
|
editingFile: null,
|
|
fileContent: '',
|
|
fileSaving: false,
|
|
filesLoading: false,
|
|
configForm: {},
|
|
configSaving: false,
|
|
// -- Tool filters --
|
|
toolFilters: { tool_allowlist: [], tool_blocklist: [] },
|
|
toolFiltersLoading: false,
|
|
newAllowTool: '',
|
|
newBlockTool: '',
|
|
// -- Model switch --
|
|
editingModel: false,
|
|
newModelValue: '',
|
|
editingProvider: false,
|
|
newProviderValue: '',
|
|
modelSaving: false,
|
|
// -- Fallback chain --
|
|
editingFallback: false,
|
|
newFallbackValue: '',
|
|
|
|
// -- Templates state --
|
|
tplTemplates: [],
|
|
tplProviders: [],
|
|
tplLoading: false,
|
|
tplLoadError: '',
|
|
selectedCategory: 'All',
|
|
searchQuery: '',
|
|
|
|
builtinTemplates: [],
|
|
|
|
// Load templates from API
|
|
async init() {
|
|
await this.loadTemplates();
|
|
// Load personality presets with i18n
|
|
this.loadPersonalityPresets();
|
|
},
|
|
|
|
// ── Profile Descriptions (loaded dynamically with i18n) ──
|
|
_profileDescriptionsLoaded: false,
|
|
profileDescriptions: {},
|
|
loadProfileDescriptions: function() {
|
|
if (this._profileDescriptionsLoaded) return;
|
|
var t = typeof window.t === 'function' ? window.t : function(s) { return s; };
|
|
this.profileDescriptions = {
|
|
minimal: { label: t('agents.profile.minimal'), desc: t('agents.profile.minimal_desc') },
|
|
coding: { label: t('agents.profile.coding'), desc: t('agents.profile.coding_desc') },
|
|
research: { label: t('agents.profile.research'), desc: t('agents.profile.research_desc') },
|
|
messaging: { label: t('agents.profile.messaging'), desc: t('agents.profile.messaging_desc') },
|
|
automation: { label: t('agents.profile.automation'), desc: t('agents.profile.automation_desc') },
|
|
balanced: { label: t('agents.profile.balanced'), desc: t('agents.profile.balanced_desc') },
|
|
precise: { label: t('agents.profile.precise'), desc: t('agents.profile.precise_desc') },
|
|
creative: { label: t('agents.profile.creative'), desc: t('agents.profile.creative_desc') },
|
|
full: { label: t('agents.profile.full'), desc: t('agents.profile.full_desc') }
|
|
};
|
|
this._profileDescriptionsLoaded = true;
|
|
},
|
|
profileInfo: function(name) {
|
|
this.loadProfileDescriptions();
|
|
return this.profileDescriptions[name] || { label: name, desc: '' };
|
|
},
|
|
|
|
// ── Tool Preview in Spawn Modal ──
|
|
spawnProfiles: [],
|
|
spawnProfilesLoaded: false,
|
|
async loadSpawnProfiles() {
|
|
if (this.spawnProfilesLoaded) return;
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/profiles');
|
|
this.spawnProfiles = data.profiles || [];
|
|
this.spawnProfilesLoaded = true;
|
|
} catch(e) { this.spawnProfiles = []; }
|
|
},
|
|
get selectedProfileTools() {
|
|
var pname = this.spawnForm.profile;
|
|
var match = this.spawnProfiles.find(function(p) { return p.name === pname; });
|
|
if (match && match.tools) return match.tools.slice(0, 15);
|
|
return [];
|
|
},
|
|
|
|
get agents() { return Alpine.store('app').agents; },
|
|
|
|
get filteredAgents() {
|
|
var f = this.filterState;
|
|
if (f === 'all') return this.agents;
|
|
return this.agents.filter(function(a) { return a.state.toLowerCase() === f; });
|
|
},
|
|
|
|
get runningCount() {
|
|
return this.agents.filter(function(a) { return a.state === 'Running'; }).length;
|
|
},
|
|
|
|
get stoppedCount() {
|
|
return this.agents.filter(function(a) { return a.state !== 'Running'; }).length;
|
|
},
|
|
|
|
// -- Templates computed --
|
|
get categories() {
|
|
var cats = { 'All': true };
|
|
this.builtinTemplates.forEach(function(t) { cats[t.category] = true; });
|
|
this.tplTemplates.forEach(function(t) { if (t.category) cats[t.category] = true; });
|
|
return Object.keys(cats);
|
|
},
|
|
|
|
get filteredBuiltins() {
|
|
var self = this;
|
|
return this.builtinTemplates.filter(function(t) {
|
|
if (self.selectedCategory !== 'All' && t.category !== self.selectedCategory) return false;
|
|
if (self.searchQuery) {
|
|
var q = self.searchQuery.toLowerCase();
|
|
if (t.name.toLowerCase().indexOf(q) === -1 &&
|
|
t.description.toLowerCase().indexOf(q) === -1) return false;
|
|
}
|
|
return true;
|
|
});
|
|
},
|
|
|
|
get filteredCustom() {
|
|
var self = this;
|
|
return this.tplTemplates.filter(function(t) {
|
|
if (self.searchQuery) {
|
|
var q = self.searchQuery.toLowerCase();
|
|
if ((t.name || '').toLowerCase().indexOf(q) === -1 &&
|
|
(t.description || '').toLowerCase().indexOf(q) === -1) return false;
|
|
}
|
|
return true;
|
|
});
|
|
},
|
|
|
|
isProviderConfigured(providerName) {
|
|
if (!providerName) return false;
|
|
var p = this.tplProviders.find(function(pr) { return pr.id === providerName; });
|
|
return p ? p.auth_status === 'configured' : false;
|
|
},
|
|
|
|
async init() {
|
|
var self = this;
|
|
this.loading = true;
|
|
this.loadError = '';
|
|
try {
|
|
await Alpine.store('app').refreshAgents();
|
|
await this.loadTemplates();
|
|
this.loadPersonalityPresets();
|
|
this.loadProfileDescriptions();
|
|
} catch(e) {
|
|
this.loadError = e.message || 'Could not load agents. Is the daemon running?';
|
|
}
|
|
this.loading = false;
|
|
|
|
// If a pending agent was set (e.g. from wizard or redirect), open chat inline
|
|
var store = Alpine.store('app');
|
|
if (store.pendingAgent) {
|
|
this.activeChatAgent = store.pendingAgent;
|
|
}
|
|
// Watch for future pendingAgent changes
|
|
this.$watch('$store.app.pendingAgent', function(agent) {
|
|
if (agent) {
|
|
self.activeChatAgent = agent;
|
|
}
|
|
});
|
|
},
|
|
|
|
async loadData() {
|
|
this.loading = true;
|
|
this.loadError = '';
|
|
try {
|
|
await Alpine.store('app').refreshAgents();
|
|
} catch(e) {
|
|
this.loadError = e.message || 'Could not load agents.';
|
|
}
|
|
this.loading = false;
|
|
},
|
|
|
|
async loadTemplates() {
|
|
this.tplLoading = true;
|
|
this.tplLoadError = '';
|
|
try {
|
|
var results = await Promise.all([
|
|
OpenFangAPI.get('/api/templates'),
|
|
OpenFangAPI.get('/api/providers').catch(function() { return { providers: [] }; })
|
|
]);
|
|
// Combine static and dynamic templates
|
|
this.builtinTemplates = [
|
|
{
|
|
name: 'General Assistant',
|
|
description: 'A versatile conversational agent that can help with everyday tasks, answer questions, and provide recommendations.',
|
|
category: 'General',
|
|
provider: 'default',
|
|
model: 'default',
|
|
profile: 'full',
|
|
system_prompt: 'You are a helpful, friendly assistant. Provide clear, accurate, and concise responses. Ask clarifying questions when needed.',
|
|
manifest_toml: 'name = "General Assistant"\ndescription = "A versatile conversational agent that can help with everyday tasks, answer questions, and provide recommendations."\nmodule = "builtin:chat"\nprofile = "full"\n\n[model]\nprovider = "default"\nmodel = "default"\nsystem_prompt = """\nYou are a helpful, friendly assistant. Provide clear, accurate, and concise responses. Ask clarifying questions when needed.\n"""'
|
|
},
|
|
{
|
|
name: 'Code Helper',
|
|
description: 'A programming-focused agent that writes, reviews, and debugs code across multiple languages.',
|
|
category: 'Development',
|
|
provider: 'default',
|
|
model: 'default',
|
|
profile: 'coding',
|
|
system_prompt: 'You are an expert programmer. Help users write clean, efficient code. Explain your reasoning. Follow best practices and conventions for the language being used.',
|
|
manifest_toml: 'name = "Code Helper"\ndescription = "A programming-focused agent that writes, reviews, and debugs code across multiple languages."\nmodule = "builtin:chat"\nprofile = "coding"\n\n[model]\nprovider = "default"\nmodel = "default"\nsystem_prompt = """\nYou are an expert programmer. Help users write clean, efficient code. Explain your reasoning. Follow best practices and conventions for the language being used.\n"""'
|
|
},
|
|
{
|
|
name: 'Researcher',
|
|
description: 'An analytical agent that breaks down complex topics, synthesizes information, and provides cited summaries.',
|
|
category: 'Research',
|
|
provider: 'default',
|
|
model: 'default',
|
|
profile: 'research',
|
|
system_prompt: 'You are a research analyst. Break down complex topics into clear explanations. Provide structured analysis with key findings. Cite sources when available.',
|
|
manifest_toml: 'name = "Researcher"\ndescription = "An analytical agent that breaks down complex topics, synthesizes information, and provides cited summaries."\nmodule = "builtin:chat"\nprofile = "research"\n\n[model]\nprovider = "default"\nmodel = "default"\nsystem_prompt = """\nYou are a research analyst. Break down complex topics into clear explanations. Provide structured analysis with key findings. Cite sources when available.\n"""'
|
|
},
|
|
{
|
|
name: 'Writer',
|
|
description: 'A creative writing agent that helps with drafting, editing, and improving written content of all kinds.',
|
|
category: 'Writing',
|
|
provider: 'default',
|
|
model: 'default',
|
|
profile: 'full',
|
|
system_prompt: 'You are a skilled writer and editor. Help users create polished content. Adapt your tone and style to match the intended audience. Offer constructive suggestions for improvement.',
|
|
manifest_toml: 'name = "Writer"\ndescription = "A creative writing agent that helps with drafting, editing, and improving written content of all kinds."\nmodule = "builtin:chat"\nprofile = "full"\n\n[model]\nprovider = "default"\nmodel = "default"\nsystem_prompt = """\nYou are a skilled writer and editor. Help users create polished content. Adapt your tone and style to match the intended audience. Offer constructive suggestions for improvement.\n"""'
|
|
},
|
|
{
|
|
name: 'Data Analyst',
|
|
description: 'A data-focused agent that helps analyze datasets, create queries, and interpret statistical results.',
|
|
category: 'Development',
|
|
provider: 'default',
|
|
model: 'default',
|
|
profile: 'coding',
|
|
system_prompt: 'You are a data analysis expert. Help users understand their data, write SQL/Python queries, and interpret results. Present findings clearly with actionable insights.',
|
|
manifest_toml: 'name = "Data Analyst"\ndescription = "A data-focused agent that helps analyze datasets, create queries, and interpret statistical results."\nmodule = "builtin:chat"\nprofile = "coding"\n\n[model]\nprovider = "default"\nmodel = "default"\nsystem_prompt = """\nYou are a data analysis expert. Help users understand their data, write SQL/Python queries, and interpret results. Present findings clearly with actionable insights.\n"""'
|
|
},
|
|
{
|
|
name: 'DevOps Engineer',
|
|
description: 'A systems-focused agent for CI/CD, infrastructure, Docker, and deployment troubleshooting.',
|
|
category: 'Development',
|
|
provider: 'default',
|
|
model: 'default',
|
|
profile: 'automation',
|
|
system_prompt: 'You are a DevOps engineer. Help with CI/CD pipelines, Docker, Kubernetes, infrastructure as code, and deployment. Prioritize reliability and security.',
|
|
manifest_toml: 'name = "DevOps Engineer"\ndescription = "A systems-focused agent for CI/CD, infrastructure, Docker, and deployment troubleshooting."\nmodule = "builtin:chat"\nprofile = "automation"\n\n[model]\nprovider = "default"\nmodel = "default"\nsystem_prompt = """\nYou are a DevOps engineer. Help with CI/CD pipelines, Docker, Kubernetes, infrastructure as code, and deployment. Prioritize reliability and security.\n"""'
|
|
},
|
|
...results[0].templates || []
|
|
];
|
|
this.tplProviders = results[1].providers || [];
|
|
} catch(e) {
|
|
this.builtinTemplates = [];
|
|
this.tplLoadError = e.message || 'Could not load templates.';
|
|
}
|
|
this.tplLoading = false;
|
|
},
|
|
|
|
chatWithAgent(agent) {
|
|
Alpine.store('app').pendingAgent = agent;
|
|
this.activeChatAgent = agent;
|
|
},
|
|
|
|
closeChat() {
|
|
this.activeChatAgent = null;
|
|
OpenFangAPI.wsDisconnect();
|
|
},
|
|
|
|
buildConfigForm(agent) {
|
|
var identity = (agent && agent.identity) || {};
|
|
return {
|
|
name: (agent && agent.name) || '',
|
|
system_prompt: (agent && agent.system_prompt) || '',
|
|
emoji: identity.emoji || '',
|
|
color: identity.color || '#FF5C00',
|
|
archetype: identity.archetype || '',
|
|
vibe: identity.vibe || ''
|
|
};
|
|
},
|
|
|
|
async showDetail(agent) {
|
|
this.detailTab = 'info';
|
|
this.agentFiles = [];
|
|
this.editingFile = null;
|
|
this.fileContent = '';
|
|
this.editingFallback = false;
|
|
this.newFallbackValue = '';
|
|
// Load the full detail payload before opening the modal so editable
|
|
// fields such as system_prompt and identity metadata are hydrated.
|
|
var detail = agent;
|
|
try {
|
|
var full = await OpenFangAPI.get('/api/agents/' + agent.id);
|
|
detail = Object.assign({}, agent, full, {
|
|
identity: Object.assign({}, (agent && agent.identity) || {}, (full && full.identity) || {})
|
|
});
|
|
} catch(e) { /* fall back to list payload */ }
|
|
this.detailAgent = detail;
|
|
this.detailAgent._fallbacks = detail.fallback_models || [];
|
|
this.configForm = this.buildConfigForm(detail);
|
|
this.showDetailModal = true;
|
|
},
|
|
|
|
killAgent(agent) {
|
|
var self = this;
|
|
OpenFangToast.confirm('Stop Agent', 'Stop agent "' + agent.name + '"? The agent will be shut down.', async function() {
|
|
try {
|
|
await OpenFangAPI.del('/api/agents/' + agent.id);
|
|
OpenFangToast.success('Agent "' + agent.name + '" stopped');
|
|
self.showDetailModal = false;
|
|
await Alpine.store('app').refreshAgents();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to stop agent: ' + e.message);
|
|
}
|
|
});
|
|
},
|
|
|
|
// Issue #1163: uninstall an agent (kill + remove ~/.openfang/agents/<name>/).
|
|
uninstallAgent(agent) {
|
|
var self = this;
|
|
OpenFangToast.confirm(
|
|
'Uninstall Agent',
|
|
'Uninstall agent "' + agent.name + '"? This stops the agent AND deletes its files from your workspace. This cannot be undone.',
|
|
async function() {
|
|
try {
|
|
var res = await OpenFangAPI.del('/api/agents/' + agent.id + '/uninstall');
|
|
var msg = 'Agent "' + agent.name + '" uninstalled';
|
|
if (res && res.dir_removed === false) {
|
|
msg += ' (no on-disk files found)';
|
|
}
|
|
OpenFangToast.success(msg);
|
|
self.showDetailModal = false;
|
|
await Alpine.store('app').refreshAgents();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to uninstall agent: ' + e.message);
|
|
}
|
|
}
|
|
);
|
|
},
|
|
|
|
killAllAgents() {
|
|
var list = this.filteredAgents;
|
|
if (!list.length) return;
|
|
OpenFangToast.confirm('Stop All Agents', 'Stop ' + list.length + ' agent(s)? All agents will be shut down.', async function() {
|
|
var errors = [];
|
|
for (var i = 0; i < list.length; i++) {
|
|
try {
|
|
await OpenFangAPI.del('/api/agents/' + list[i].id);
|
|
} catch(e) { errors.push(list[i].name + ': ' + e.message); }
|
|
}
|
|
await Alpine.store('app').refreshAgents();
|
|
if (errors.length) {
|
|
OpenFangToast.error('Some agents failed to stop: ' + errors.join(', '));
|
|
} else {
|
|
OpenFangToast.success(list.length + ' agent(s) stopped');
|
|
}
|
|
});
|
|
},
|
|
|
|
// ── Multi-step wizard navigation ──
|
|
async openSpawnWizard() {
|
|
this.showSpawnModal = true;
|
|
this.spawnStep = 1;
|
|
this.spawnMode = 'wizard';
|
|
this.spawnIdentity = { emoji: '', color: '#FF5C00', archetype: '' };
|
|
this.selectedPreset = '';
|
|
this.soulContent = '';
|
|
this.spawnForm.name = '';
|
|
this.spawnForm.provider = 'default';
|
|
this.spawnForm.model = 'default';
|
|
this.spawnForm.systemPrompt = 'You are a helpful assistant.';
|
|
this.spawnForm.profile = 'full';
|
|
// Fetch status defaults and dynamic provider list concurrently
|
|
this.spawnProvidersLoading = true;
|
|
try {
|
|
var results = await Promise.all([
|
|
OpenFangAPI.get('/api/status').catch(function() { return {}; }),
|
|
OpenFangAPI.get('/api/providers').catch(function() { return { providers: [] }; })
|
|
]);
|
|
var status = results[0];
|
|
var provData = results[1];
|
|
if (status.default_provider) this.spawnForm.provider = status.default_provider;
|
|
if (status.default_model) this.spawnForm.model = status.default_model;
|
|
this.spawnProviders = provData.providers || [];
|
|
} catch(e) {
|
|
this.spawnProviders = [];
|
|
}
|
|
this.spawnProvidersLoading = false;
|
|
},
|
|
|
|
nextStep() {
|
|
if (this.spawnStep === 1 && !this.spawnForm.name.trim()) {
|
|
OpenFangToast.warn('Please enter an agent name');
|
|
return;
|
|
}
|
|
if (this.spawnStep < 5) this.spawnStep++;
|
|
},
|
|
|
|
prevStep() {
|
|
if (this.spawnStep > 1) this.spawnStep--;
|
|
},
|
|
|
|
selectPreset(preset) {
|
|
this.selectedPreset = preset.id;
|
|
this.soulContent = preset.soul;
|
|
},
|
|
|
|
generateToml() {
|
|
var f = this.spawnForm;
|
|
var si = this.spawnIdentity;
|
|
var lines = [
|
|
'name = "' + tomlBasicEscape(f.name) + '"',
|
|
'module = "builtin:chat"'
|
|
];
|
|
if (f.profile && f.profile !== 'custom') {
|
|
lines.push('profile = "' + f.profile + '"');
|
|
}
|
|
lines.push('', '[model]');
|
|
lines.push('provider = "' + f.provider + '"');
|
|
lines.push('model = "' + f.model + '"');
|
|
lines.push('system_prompt = """\n' + tomlMultilineEscape(f.systemPrompt) + '\n"""');
|
|
if (f.profile === 'custom') {
|
|
lines.push('', '[capabilities]');
|
|
if (f.caps.memory_read) lines.push('memory_read = ["*"]');
|
|
if (f.caps.memory_write) lines.push('memory_write = ["self.*"]');
|
|
if (f.caps.network) lines.push('network = ["*"]');
|
|
if (f.caps.shell) lines.push('shell = ["*"]');
|
|
if (f.caps.agent_spawn) lines.push('agent_spawn = true');
|
|
}
|
|
return lines.join('\n');
|
|
},
|
|
|
|
async setMode(agent, mode) {
|
|
try {
|
|
await OpenFangAPI.put('/api/agents/' + agent.id + '/mode', { mode: mode });
|
|
agent.mode = mode;
|
|
OpenFangToast.success('Mode set to ' + mode);
|
|
await Alpine.store('app').refreshAgents();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to set mode: ' + e.message);
|
|
}
|
|
},
|
|
|
|
async spawnAgent() {
|
|
this.spawning = true;
|
|
var toml = this.spawnMode === 'wizard' ? this.generateToml() : this.spawnToml;
|
|
if (!toml.trim()) {
|
|
this.spawning = false;
|
|
OpenFangToast.warn('Manifest is empty \u2014 enter agent config first');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
var res = await OpenFangAPI.post('/api/agents', { manifest_toml: toml });
|
|
if (res.agent_id) {
|
|
// Post-spawn: update identity + write SOUL.md if personality preset selected
|
|
var patchBody = {};
|
|
if (this.spawnIdentity.emoji) patchBody.emoji = this.spawnIdentity.emoji;
|
|
if (this.spawnIdentity.color) patchBody.color = this.spawnIdentity.color;
|
|
if (this.spawnIdentity.archetype) patchBody.archetype = this.spawnIdentity.archetype;
|
|
if (this.selectedPreset) patchBody.vibe = this.selectedPreset;
|
|
|
|
if (Object.keys(patchBody).length) {
|
|
OpenFangAPI.patch('/api/agents/' + res.agent_id + '/config', patchBody).catch(function(e) { console.warn('Post-spawn config patch failed:', e.message); });
|
|
}
|
|
if (this.soulContent.trim()) {
|
|
OpenFangAPI.put('/api/agents/' + res.agent_id + '/files/SOUL.md', { content: '# Soul\n' + this.soulContent }).catch(function(e) { console.warn('SOUL.md write failed:', e.message); });
|
|
}
|
|
|
|
this.showSpawnModal = false;
|
|
this.spawnForm.name = '';
|
|
this.spawnToml = '';
|
|
this.spawnStep = 1;
|
|
OpenFangToast.success('Agent "' + (res.name || 'new') + '" spawned');
|
|
await Alpine.store('app').refreshAgents();
|
|
this.chatWithAgent({ id: res.agent_id, name: res.name, model_provider: '?', model_name: '?' });
|
|
} else {
|
|
OpenFangToast.error('Spawn failed: ' + (res.error || 'Unknown error'));
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to spawn agent: ' + e.message);
|
|
}
|
|
this.spawning = false;
|
|
},
|
|
|
|
// ── Detail modal: Files tab ──
|
|
async loadAgentFiles() {
|
|
if (!this.detailAgent) return;
|
|
this.filesLoading = true;
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/files');
|
|
this.agentFiles = data.files || [];
|
|
} catch(e) {
|
|
this.agentFiles = [];
|
|
OpenFangToast.error('Failed to load files: ' + e.message);
|
|
}
|
|
this.filesLoading = false;
|
|
},
|
|
|
|
async openFile(file) {
|
|
if (!file.exists) {
|
|
// Create with empty content
|
|
this.editingFile = file.name;
|
|
this.fileContent = '';
|
|
return;
|
|
}
|
|
try {
|
|
var data = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(file.name));
|
|
this.editingFile = file.name;
|
|
this.fileContent = data.content || '';
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to read file: ' + e.message);
|
|
}
|
|
},
|
|
|
|
async saveFile() {
|
|
if (!this.editingFile || !this.detailAgent) return;
|
|
this.fileSaving = true;
|
|
try {
|
|
await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/files/' + encodeURIComponent(this.editingFile), { content: this.fileContent });
|
|
OpenFangToast.success(this.editingFile + ' saved');
|
|
await this.loadAgentFiles();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to save file: ' + e.message);
|
|
}
|
|
this.fileSaving = false;
|
|
},
|
|
|
|
closeFileEditor() {
|
|
this.editingFile = null;
|
|
this.fileContent = '';
|
|
},
|
|
|
|
// ── Detail modal: Config tab ──
|
|
async saveConfig() {
|
|
if (!this.detailAgent) return;
|
|
this.configSaving = true;
|
|
try {
|
|
await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', this.configForm);
|
|
OpenFangToast.success('Config updated');
|
|
await Alpine.store('app').refreshAgents();
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to save config: ' + e.message);
|
|
}
|
|
this.configSaving = false;
|
|
},
|
|
|
|
// ── Clone agent ──
|
|
async cloneAgent(agent) {
|
|
var newName = (agent.name || 'agent') + '-copy';
|
|
try {
|
|
var res = await OpenFangAPI.post('/api/agents/' + agent.id + '/clone', { new_name: newName });
|
|
if (res.agent_id) {
|
|
OpenFangToast.success('Cloned as "' + res.name + '"');
|
|
await Alpine.store('app').refreshAgents();
|
|
this.showDetailModal = false;
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Clone failed: ' + e.message);
|
|
}
|
|
},
|
|
|
|
// -- Template methods --
|
|
async spawnFromTemplate(template) {
|
|
try {
|
|
var manifestToml = template.manifest_toml;
|
|
if (!manifestToml) {
|
|
// If template doesn't have manifest_toml, fetch it from the API
|
|
var data = await OpenFangAPI.get('/api/templates/' + encodeURIComponent(template.name));
|
|
manifestToml = data.manifest_toml;
|
|
}
|
|
if (manifestToml) {
|
|
var res = await OpenFangAPI.post('/api/agents', { manifest_toml: manifestToml });
|
|
if (res.agent_id) {
|
|
OpenFangToast.success('Agent "' + (res.name || template.name) + '" spawned from template');
|
|
await Alpine.store('app').refreshAgents();
|
|
this.chatWithAgent({ id: res.agent_id, name: res.name || template.name, model_provider: '?', model_name: '?' });
|
|
}
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to spawn from template: ' + e.message);
|
|
}
|
|
},
|
|
|
|
// ── Clear agent history ──
|
|
async clearHistory(agent) {
|
|
var self = this;
|
|
OpenFangToast.confirm('Clear History', 'Clear all conversation history for "' + agent.name + '"? This cannot be undone.', async function() {
|
|
try {
|
|
await OpenFangAPI.del('/api/agents/' + agent.id + '/history');
|
|
OpenFangToast.success('History cleared for "' + agent.name + '"');
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to clear history: ' + e.message);
|
|
}
|
|
});
|
|
},
|
|
|
|
// ── Model switch ──
|
|
async changeModel() {
|
|
if (!this.detailAgent || !this.newModelValue.trim()) return;
|
|
this.modelSaving = true;
|
|
try {
|
|
var resp = await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: this.newModelValue.trim() });
|
|
var providerInfo = (resp && resp.provider) ? ' (provider: ' + resp.provider + ')' : '';
|
|
OpenFangToast.success('Model changed' + providerInfo + ' (memory reset)');
|
|
this.editingModel = false;
|
|
await Alpine.store('app').refreshAgents();
|
|
// Refresh detailAgent
|
|
var agents = Alpine.store('app').agents;
|
|
for (var i = 0; i < agents.length; i++) {
|
|
if (agents[i].id === this.detailAgent.id) { this.detailAgent = agents[i]; break; }
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to change model: ' + e.message);
|
|
}
|
|
this.modelSaving = false;
|
|
},
|
|
|
|
// ── Provider switch ──
|
|
async changeProvider() {
|
|
if (!this.detailAgent || !this.newProviderValue.trim()) return;
|
|
this.modelSaving = true;
|
|
try {
|
|
var combined = this.newProviderValue.trim() + '/' + this.detailAgent.model_name;
|
|
var resp = await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/model', { model: combined });
|
|
OpenFangToast.success('Provider changed to ' + (resp && resp.provider ? resp.provider : this.newProviderValue.trim()));
|
|
this.editingProvider = false;
|
|
await Alpine.store('app').refreshAgents();
|
|
var agents = Alpine.store('app').agents;
|
|
for (var i = 0; i < agents.length; i++) {
|
|
if (agents[i].id === this.detailAgent.id) { this.detailAgent = agents[i]; break; }
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to change provider: ' + e.message);
|
|
}
|
|
this.modelSaving = false;
|
|
},
|
|
|
|
// ── Fallback model chain ──
|
|
async addFallback() {
|
|
if (!this.detailAgent || !this.newFallbackValue.trim()) return;
|
|
var parts = this.newFallbackValue.trim().split('/');
|
|
var provider = parts.length > 1 ? parts[0] : this.detailAgent.model_provider;
|
|
var model = parts.length > 1 ? parts.slice(1).join('/') : parts[0];
|
|
if (!this.detailAgent._fallbacks) this.detailAgent._fallbacks = [];
|
|
this.detailAgent._fallbacks.push({ provider: provider, model: model });
|
|
try {
|
|
await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', {
|
|
fallback_models: this.detailAgent._fallbacks
|
|
});
|
|
OpenFangToast.success('Fallback added: ' + provider + '/' + model);
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to save fallbacks: ' + e.message);
|
|
this.detailAgent._fallbacks.pop();
|
|
}
|
|
this.editingFallback = false;
|
|
this.newFallbackValue = '';
|
|
},
|
|
|
|
async removeFallback(idx) {
|
|
if (!this.detailAgent || !this.detailAgent._fallbacks) return;
|
|
var removed = this.detailAgent._fallbacks.splice(idx, 1);
|
|
try {
|
|
await OpenFangAPI.patch('/api/agents/' + this.detailAgent.id + '/config', {
|
|
fallback_models: this.detailAgent._fallbacks
|
|
});
|
|
OpenFangToast.success('Fallback removed');
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to save fallbacks: ' + e.message);
|
|
this.detailAgent._fallbacks.splice(idx, 0, removed[0]);
|
|
}
|
|
},
|
|
|
|
// ── Tool filters ──
|
|
async loadToolFilters() {
|
|
if (!this.detailAgent) return;
|
|
this.toolFiltersLoading = true;
|
|
try {
|
|
this.toolFilters = await OpenFangAPI.get('/api/agents/' + this.detailAgent.id + '/tools');
|
|
} catch(e) {
|
|
this.toolFilters = { tool_allowlist: [], tool_blocklist: [] };
|
|
}
|
|
this.toolFiltersLoading = false;
|
|
},
|
|
|
|
addAllowTool() {
|
|
var t = this.newAllowTool.trim();
|
|
if (t && this.toolFilters.tool_allowlist.indexOf(t) === -1) {
|
|
this.toolFilters.tool_allowlist.push(t);
|
|
this.newAllowTool = '';
|
|
this.saveToolFilters();
|
|
}
|
|
},
|
|
|
|
removeAllowTool(tool) {
|
|
this.toolFilters.tool_allowlist = this.toolFilters.tool_allowlist.filter(function(t) { return t !== tool; });
|
|
this.saveToolFilters();
|
|
},
|
|
|
|
addBlockTool() {
|
|
var t = this.newBlockTool.trim();
|
|
if (t && this.toolFilters.tool_blocklist.indexOf(t) === -1) {
|
|
this.toolFilters.tool_blocklist.push(t);
|
|
this.newBlockTool = '';
|
|
this.saveToolFilters();
|
|
}
|
|
},
|
|
|
|
removeBlockTool(tool) {
|
|
this.toolFilters.tool_blocklist = this.toolFilters.tool_blocklist.filter(function(t) { return t !== tool; });
|
|
this.saveToolFilters();
|
|
},
|
|
|
|
async saveToolFilters() {
|
|
if (!this.detailAgent) return;
|
|
try {
|
|
await OpenFangAPI.put('/api/agents/' + this.detailAgent.id + '/tools', this.toolFilters);
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to update tool filters: ' + e.message);
|
|
}
|
|
},
|
|
|
|
async spawnBuiltin(t) {
|
|
var toml = 'name = "' + tomlBasicEscape(t.name) + '"\n';
|
|
toml += 'description = "' + tomlBasicEscape(t.description) + '"\n';
|
|
toml += 'module = "builtin:chat"\n';
|
|
toml += 'profile = "' + t.profile + '"\n\n';
|
|
toml += '[model]\nprovider = "' + t.provider + '"\nmodel = "' + t.model + '"\n';
|
|
toml += 'system_prompt = """\n' + tomlMultilineEscape(t.system_prompt) + '\n"""\n';
|
|
|
|
try {
|
|
var res = await OpenFangAPI.post('/api/agents', { manifest_toml: toml });
|
|
if (res.agent_id) {
|
|
OpenFangToast.success('Agent "' + t.name + '" spawned');
|
|
await Alpine.store('app').refreshAgents();
|
|
this.chatWithAgent({ id: res.agent_id, name: t.name, model_provider: t.provider, model_name: t.model });
|
|
}
|
|
} catch(e) {
|
|
OpenFangToast.error('Failed to spawn agent: ' + e.message);
|
|
}
|
|
}
|
|
};
|
|
}
|