@mooncompany/uplink-chat 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of @mooncompany/uplink-chat might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +185 -0
- package/bin/uplink.js +279 -0
- package/middleware/error-handler.js +69 -0
- package/package.json +93 -0
- package/public/css/agents.36b98c0f.css +1469 -0
- package/public/css/agents.css +1469 -0
- package/public/css/app.a6a7f8f5.css +2731 -0
- package/public/css/app.css +2731 -0
- package/public/css/artifacts.css +444 -0
- package/public/css/commands.css +55 -0
- package/public/css/connection.css +131 -0
- package/public/css/dashboard.css +233 -0
- package/public/css/developer.css +328 -0
- package/public/css/files.css +123 -0
- package/public/css/markdown.css +156 -0
- package/public/css/message-actions.css +278 -0
- package/public/css/mobile.css +614 -0
- package/public/css/panels-unified.css +483 -0
- package/public/css/premium.css +415 -0
- package/public/css/realtime.css +189 -0
- package/public/css/satellites.css +401 -0
- package/public/css/shortcuts.css +185 -0
- package/public/css/split-view.4def0262.css +673 -0
- package/public/css/split-view.css +673 -0
- package/public/css/theme-generator.css +391 -0
- package/public/css/themes.css +387 -0
- package/public/css/timestamps.css +54 -0
- package/public/css/variables.css +78 -0
- package/public/dist/bundle.b55050c4.js +15757 -0
- package/public/favicon.svg +24 -0
- package/public/img/agents/ada.png +0 -0
- package/public/img/agents/clarice.png +0 -0
- package/public/img/agents/dennis-nedry.png +0 -0
- package/public/img/agents/elliot-alderson.png +0 -0
- package/public/img/agents/main.png +0 -0
- package/public/img/agents/scotty.png +0 -0
- package/public/img/agents/top-flight-security.png +0 -0
- package/public/index.html +1083 -0
- package/public/js/agents-data.js +234 -0
- package/public/js/agents-ui.js +72 -0
- package/public/js/agents.js +1525 -0
- package/public/js/app.js +79 -0
- package/public/js/appearance-settings.js +111 -0
- package/public/js/artifacts.js +432 -0
- package/public/js/audio-queue.js +168 -0
- package/public/js/bootstrap.js +54 -0
- package/public/js/chat.js +1211 -0
- package/public/js/commands.js +581 -0
- package/public/js/connection-api.js +121 -0
- package/public/js/connection.js +1231 -0
- package/public/js/context-tracker.js +271 -0
- package/public/js/core.js +172 -0
- package/public/js/dashboard.js +452 -0
- package/public/js/developer.js +432 -0
- package/public/js/encryption.js +124 -0
- package/public/js/errors.js +122 -0
- package/public/js/event-bus.js +77 -0
- package/public/js/fetch-utils.js +171 -0
- package/public/js/file-handler.js +229 -0
- package/public/js/files.js +352 -0
- package/public/js/gateway-chat.js +538 -0
- package/public/js/logger.js +112 -0
- package/public/js/markdown.js +190 -0
- package/public/js/message-actions.js +431 -0
- package/public/js/message-renderer.js +288 -0
- package/public/js/missed-messages.js +235 -0
- package/public/js/mobile-debug.js +95 -0
- package/public/js/notifications.js +367 -0
- package/public/js/offline-queue.js +178 -0
- package/public/js/onboarding.js +543 -0
- package/public/js/panels.js +156 -0
- package/public/js/premium.js +412 -0
- package/public/js/realtime-voice.js +844 -0
- package/public/js/satellite-sync.js +256 -0
- package/public/js/satellite-ui.js +175 -0
- package/public/js/satellites.js +1516 -0
- package/public/js/settings.js +1087 -0
- package/public/js/shortcuts.js +381 -0
- package/public/js/split-chat.js +1234 -0
- package/public/js/split-resize.js +211 -0
- package/public/js/splitview.js +340 -0
- package/public/js/storage.js +408 -0
- package/public/js/streaming-handler.js +324 -0
- package/public/js/stt-settings.js +316 -0
- package/public/js/theme-generator.js +661 -0
- package/public/js/themes.js +164 -0
- package/public/js/timestamps.js +198 -0
- package/public/js/tts-settings.js +575 -0
- package/public/js/ui.js +267 -0
- package/public/js/update-notifier.js +143 -0
- package/public/js/utils/constants.js +165 -0
- package/public/js/utils/sanitize.js +93 -0
- package/public/js/utils/sse-parser.js +195 -0
- package/public/js/voice.js +883 -0
- package/public/manifest.json +58 -0
- package/public/moon_texture.jpg +0 -0
- package/public/sw.js +221 -0
- package/public/three.min.js +6 -0
- package/server/channel.js +529 -0
- package/server/chat.js +270 -0
- package/server/config-store.js +362 -0
- package/server/config.js +159 -0
- package/server/context.js +131 -0
- package/server/gateway-commands.js +211 -0
- package/server/gateway-proxy.js +318 -0
- package/server/index.js +22 -0
- package/server/logger.js +89 -0
- package/server/middleware/auth.js +188 -0
- package/server/middleware.js +218 -0
- package/server/openclaw-discover.js +308 -0
- package/server/premium/index.js +156 -0
- package/server/premium/license.js +140 -0
- package/server/realtime/bridge.js +837 -0
- package/server/realtime/index.js +349 -0
- package/server/realtime/tts-stream.js +446 -0
- package/server/routes/agents.js +564 -0
- package/server/routes/artifacts.js +174 -0
- package/server/routes/chat.js +311 -0
- package/server/routes/config-settings.js +345 -0
- package/server/routes/config.js +603 -0
- package/server/routes/files.js +307 -0
- package/server/routes/index.js +18 -0
- package/server/routes/media.js +451 -0
- package/server/routes/missed-messages.js +107 -0
- package/server/routes/premium.js +75 -0
- package/server/routes/push.js +156 -0
- package/server/routes/satellite.js +406 -0
- package/server/routes/status.js +251 -0
- package/server/routes/stt.js +35 -0
- package/server/routes/voice.js +260 -0
- package/server/routes/webhooks.js +203 -0
- package/server/routes.js +206 -0
- package/server/runtime-config.js +336 -0
- package/server/share.js +305 -0
- package/server/stt/faster-whisper.js +72 -0
- package/server/stt/groq.js +51 -0
- package/server/stt/index.js +196 -0
- package/server/stt/openai.js +49 -0
- package/server/sync.js +244 -0
- package/server/tailscale-https.js +175 -0
- package/server/tts.js +646 -0
- package/server/update-checker.js +172 -0
- package/server/utils/filename.js +129 -0
- package/server/utils.js +147 -0
- package/server/watchdog.js +318 -0
- package/server/websocket/broadcast.js +359 -0
- package/server/websocket/connections.js +339 -0
- package/server/websocket/index.js +215 -0
- package/server/websocket/routing.js +277 -0
- package/server/websocket/sync.js +102 -0
- package/server.js +404 -0
- package/utils/detect-tool-usage.js +93 -0
- package/utils/errors.js +158 -0
- package/utils/html-escape.js +84 -0
- package/utils/id-sanitize.js +94 -0
- package/utils/response.js +130 -0
- package/utils/with-retry.js +105 -0
|
@@ -0,0 +1,1525 @@
|
|
|
1
|
+
// ============================================
|
|
2
|
+
// AGENTS MODULE
|
|
3
|
+
// Agent management panel orchestrator
|
|
4
|
+
// Imports data API and UI modules
|
|
5
|
+
// ============================================
|
|
6
|
+
|
|
7
|
+
import * as AgentsData from './agents-data.js';
|
|
8
|
+
import * as AgentsUI from './agents-ui.js';
|
|
9
|
+
|
|
10
|
+
// Re-export for backward compat
|
|
11
|
+
const { TOOL_GROUPS, ALL_TOOLS, escapeHtml, escapeAttr, showToast, TOOL_PRESETS } = AgentsUI;
|
|
12
|
+
|
|
13
|
+
// State (now managed by data module, but keep local refs for compatibility)
|
|
14
|
+
let agents = [];
|
|
15
|
+
let defaults = {};
|
|
16
|
+
let bindings = [];
|
|
17
|
+
let globalTools = {};
|
|
18
|
+
let channels = [];
|
|
19
|
+
let configHash = null;
|
|
20
|
+
let loaded = false;
|
|
21
|
+
let loading = false;
|
|
22
|
+
let currentView = 'list'; // 'list' | 'detail' | 'create'
|
|
23
|
+
let selectedAgentId = null;
|
|
24
|
+
|
|
25
|
+
// Edit state
|
|
26
|
+
let editingSection = null; // which section is being edited
|
|
27
|
+
let pendingChanges = {}; // accumulated changes before save
|
|
28
|
+
let saving = false;
|
|
29
|
+
let restartPending = false;
|
|
30
|
+
|
|
31
|
+
// Current container reference for re-renders
|
|
32
|
+
let currentContainer = null;
|
|
33
|
+
|
|
34
|
+
// Sync local state with data module
|
|
35
|
+
function syncState() {
|
|
36
|
+
const state = AgentsData.getState();
|
|
37
|
+
agents = state.agents;
|
|
38
|
+
defaults = state.defaults;
|
|
39
|
+
bindings = state.bindings;
|
|
40
|
+
globalTools = state.globalTools;
|
|
41
|
+
channels = state.channels;
|
|
42
|
+
configHash = state.configHash;
|
|
43
|
+
loaded = state.loaded;
|
|
44
|
+
loading = state.loading;
|
|
45
|
+
saving = state.saving;
|
|
46
|
+
restartPending = state.restartPending;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// DATA FETCHING (delegates to data module)
|
|
51
|
+
// ============================================
|
|
52
|
+
|
|
53
|
+
async function fetchAgents() {
|
|
54
|
+
await AgentsData.fetchAgents();
|
|
55
|
+
syncState();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function saveAgentChanges(agentId, changes) {
|
|
59
|
+
try {
|
|
60
|
+
const result = await AgentsData.saveAgentChanges(agentId, changes, () => {
|
|
61
|
+
showToast('Config was modified externally. Refreshing...', 'warning');
|
|
62
|
+
syncState();
|
|
63
|
+
rerender();
|
|
64
|
+
});
|
|
65
|
+
syncState();
|
|
66
|
+
if (result) {
|
|
67
|
+
showToast('Changes applied', 'success');
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
} catch (err) {
|
|
71
|
+
showToast(`Save failed: ${err.message}`, 'error');
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// RENDERING
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
function render(container) {
|
|
81
|
+
if (!container) return;
|
|
82
|
+
currentContainer = container;
|
|
83
|
+
|
|
84
|
+
if (!loaded && !loading) {
|
|
85
|
+
fetchAgents().then(() => render(container));
|
|
86
|
+
container.innerHTML = renderLoading();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (loading) {
|
|
91
|
+
container.innerHTML = renderLoading();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (restartPending) {
|
|
96
|
+
container.innerHTML = renderRestartOverlay();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (currentView === 'create') {
|
|
101
|
+
container.innerHTML = renderCreateForm();
|
|
102
|
+
bindCreateEvents(container);
|
|
103
|
+
} else if (currentView === 'detail' && selectedAgentId) {
|
|
104
|
+
container.innerHTML = renderAgentDetail(selectedAgentId);
|
|
105
|
+
bindDetailEvents(container);
|
|
106
|
+
} else {
|
|
107
|
+
container.innerHTML = renderAgentList();
|
|
108
|
+
bindListEvents(container);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function rerender() {
|
|
113
|
+
if (currentContainer) render(currentContainer);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function renderLoading() {
|
|
117
|
+
return `
|
|
118
|
+
<div class="agents-loading">
|
|
119
|
+
<span class="agents-loading-dot"></span>
|
|
120
|
+
<span class="agents-loading-text">Loading agents...</span>
|
|
121
|
+
</div>
|
|
122
|
+
`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function renderAgentList() {
|
|
126
|
+
if (agents.length === 0) {
|
|
127
|
+
return `
|
|
128
|
+
<div class="agents-empty">
|
|
129
|
+
<span class="agents-empty-icon">🤖</span>
|
|
130
|
+
<span class="agents-empty-text">No agents configured</span>
|
|
131
|
+
</div>
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const items = agents.map(agent => {
|
|
136
|
+
const modelDisplay = getModelDisplay(agent);
|
|
137
|
+
const sandboxBadge = getSandboxBadge(agent);
|
|
138
|
+
const toolsSummary = getToolsSummary(agent);
|
|
139
|
+
const bindingCount = bindings.filter(b => b.agentId === agent.id).length;
|
|
140
|
+
const emoji = agent.identity?.emoji || '🤖';
|
|
141
|
+
const name = agent.identity?.name || agent.name || agent.id;
|
|
142
|
+
const theme = agent.identity?.theme || '';
|
|
143
|
+
|
|
144
|
+
const avatarHtml = `<span class="panel-item-icon agent-item-avatar" data-fallback-emoji="${emoji}">
|
|
145
|
+
<img src="/img/agents/${encodeURIComponent(agent.id)}.png" alt="" class="agent-list-avatar-img" data-agent-id="${encodeURIComponent(agent.id)}" loading="lazy">
|
|
146
|
+
</span>`;
|
|
147
|
+
|
|
148
|
+
return `
|
|
149
|
+
<div class="panel-item agent-item" data-agent-id="${escapeHtml(agent.id)}">
|
|
150
|
+
${avatarHtml}
|
|
151
|
+
<div class="agent-item-info">
|
|
152
|
+
<div class="agent-item-name-row">
|
|
153
|
+
<span class="panel-item-name agent-item-name">${escapeHtml(name)}</span>
|
|
154
|
+
${agent.default ? '<span class="agent-badge agent-badge-default">default</span>' : ''}
|
|
155
|
+
${sandboxBadge}
|
|
156
|
+
</div>
|
|
157
|
+
${theme ? `<div class="agent-item-theme">${escapeHtml(theme)}</div>` : ''}
|
|
158
|
+
<div class="agent-item-meta">
|
|
159
|
+
<span class="agent-meta-model">${escapeHtml(modelDisplay)}</span>
|
|
160
|
+
${toolsSummary ? `<span class="agent-meta-sep">·</span><span class="agent-meta-tools">${toolsSummary}</span>` : ''}
|
|
161
|
+
${bindingCount > 0 ? `<span class="agent-meta-sep">·</span><span class="agent-meta-bindings">${bindingCount} route${bindingCount !== 1 ? 's' : ''}</span>` : ''}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
<span class="agent-item-chevron">›</span>
|
|
165
|
+
</div>
|
|
166
|
+
`;
|
|
167
|
+
}).join('');
|
|
168
|
+
|
|
169
|
+
return `
|
|
170
|
+
<div class="agents-list-header">
|
|
171
|
+
<span class="agents-list-count">${agents.length} agent${agents.length !== 1 ? 's' : ''}</span>
|
|
172
|
+
<button class="agents-refresh-btn" title="Refresh">↻</button>
|
|
173
|
+
</div>
|
|
174
|
+
<div class="agents-list" role="listbox" aria-label="Agent list">
|
|
175
|
+
${items}
|
|
176
|
+
</div>
|
|
177
|
+
<div class="agents-list-actions">
|
|
178
|
+
<button class="agents-create-btn" id="createAgentBtn">+ New Agent</button>
|
|
179
|
+
</div>
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================
|
|
184
|
+
// AGENT DETAIL VIEW
|
|
185
|
+
// ============================================
|
|
186
|
+
|
|
187
|
+
function renderAgentDetail(agentId) {
|
|
188
|
+
const agent = agents.find(a => a.id === agentId);
|
|
189
|
+
if (!agent) {
|
|
190
|
+
return `<div class="agents-empty"><span>Agent not found</span></div>`;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const emoji = agent.identity?.emoji || '🤖';
|
|
194
|
+
const name = agent.identity?.name || agent.name || agent.id;
|
|
195
|
+
const theme = agent.identity?.theme || '';
|
|
196
|
+
const workspace = agent.workspace || defaults.workspace || 'default';
|
|
197
|
+
const agentBindings = bindings.filter(b => b.agentId === agent.id);
|
|
198
|
+
|
|
199
|
+
return `
|
|
200
|
+
<div class="agent-detail">
|
|
201
|
+
<div class="agent-detail-hero" data-agent-id="${escapeHtml(agent.id)}">
|
|
202
|
+
<img class="agent-detail-hero-img" src="/img/agents/${escapeHtml(agent.id)}.png" alt="">
|
|
203
|
+
<span class="agent-detail-hero-fallback">${emoji}</span>
|
|
204
|
+
<div class="agent-detail-hero-overlay">
|
|
205
|
+
<div class="agent-detail-hero-nav">
|
|
206
|
+
<button class="agent-detail-back" aria-label="Back to agent list">← Back</button>
|
|
207
|
+
</div>
|
|
208
|
+
<div class="agent-detail-hero-info">
|
|
209
|
+
<span class="agent-detail-name">${escapeHtml(name)}</span>
|
|
210
|
+
<span class="agent-detail-id">${escapeHtml(agent.id)}</span>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
<span class="agent-avatar-overlay">📷</span>
|
|
214
|
+
<input type="file" class="agent-avatar-input" accept="image/png,image/jpeg,image/webp" style="display:none">
|
|
215
|
+
${agent.default ? '<span class="agent-badge agent-badge-default">default</span>' : ''}
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
${theme ? `<div class="agent-detail-theme-block"><span class="agent-detail-theme">${escapeHtml(theme)}</span></div>` : ''}
|
|
219
|
+
|
|
220
|
+
${renderEditableSection('identity', 'Identity', renderIdentityView(agent), renderIdentityEdit(agent))}
|
|
221
|
+
${renderEditableSection('model', 'Model', renderModelView(agent), renderModelEdit(agent))}
|
|
222
|
+
|
|
223
|
+
${renderDetailSection('Workspace', `
|
|
224
|
+
<div class="agent-detail-row">
|
|
225
|
+
<span class="agent-detail-label">Path</span>
|
|
226
|
+
<span class="agent-detail-value agent-detail-mono">${escapeHtml(workspace)}${!agent.workspace ? ' <span class="agent-detail-inherited">(inherited)</span>' : ''}</span>
|
|
227
|
+
</div>
|
|
228
|
+
`)}
|
|
229
|
+
|
|
230
|
+
${renderEditableSection('sandbox', 'Sandbox', renderSandboxView(agent), renderSandboxEdit(agent))}
|
|
231
|
+
${renderEditableSection('tools', 'Tools', renderToolsView(agent), renderToolsEdit(agent))}
|
|
232
|
+
${renderEditableSection('subagents', 'Sub-agents', renderSubagentsView(agent), renderSubagentsEdit(agent))}
|
|
233
|
+
|
|
234
|
+
${renderEditableSection('routing', 'Routing', renderRoutingView(agent, agentBindings), renderRoutingEdit(agent, agentBindings))}
|
|
235
|
+
|
|
236
|
+
${agent.groupChat?.mentionPatterns ? renderDetailSection('Group Chat', `
|
|
237
|
+
<div class="agent-detail-row">
|
|
238
|
+
<span class="agent-detail-label">Mentions</span>
|
|
239
|
+
<span class="agent-detail-value">${agent.groupChat.mentionPatterns.map(p => escapeHtml(p)).join(', ')}</span>
|
|
240
|
+
</div>
|
|
241
|
+
`) : ''}
|
|
242
|
+
|
|
243
|
+
${renderRawJsonSection(agent)}
|
|
244
|
+
|
|
245
|
+
${agent.id !== 'main' ? `
|
|
246
|
+
<div class="agent-detail-danger-zone">
|
|
247
|
+
<button class="agent-detail-delete" aria-label="Delete agent">Delete Agent</button>
|
|
248
|
+
</div>` : ''}
|
|
249
|
+
</div>
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function renderDetailSection(title, content) {
|
|
254
|
+
return `
|
|
255
|
+
<div class="agent-section">
|
|
256
|
+
<div class="agent-section-header">
|
|
257
|
+
<span class="agent-section-title">${title}</span>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="agent-section-body">
|
|
260
|
+
${content}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function renderEditableSection(sectionId, title, viewContent, editContent) {
|
|
267
|
+
const isEditing = editingSection === sectionId;
|
|
268
|
+
return `
|
|
269
|
+
<div class="agent-section" data-section="${sectionId}">
|
|
270
|
+
<div class="agent-section-header">
|
|
271
|
+
<span class="agent-section-title">${title}</span>
|
|
272
|
+
${isEditing
|
|
273
|
+
? `<div class="agent-section-actions">
|
|
274
|
+
<button class="agent-section-btn agent-section-cancel" data-action="cancel" data-section="${sectionId}">Cancel</button>
|
|
275
|
+
<button class="agent-section-btn agent-section-save" data-action="save" data-section="${sectionId}">Save</button>
|
|
276
|
+
</div>`
|
|
277
|
+
: `<button class="agent-section-btn agent-section-edit" data-action="edit" data-section="${sectionId}">Edit</button>`
|
|
278
|
+
}
|
|
279
|
+
</div>
|
|
280
|
+
<div class="agent-section-body">
|
|
281
|
+
${isEditing ? editContent : viewContent}
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ============================================
|
|
288
|
+
// VIEW RENDERERS (read-only display)
|
|
289
|
+
// ============================================
|
|
290
|
+
|
|
291
|
+
function renderIdentityView(agent) {
|
|
292
|
+
const name = agent.identity?.name || agent.name || agent.id;
|
|
293
|
+
const emoji = agent.identity?.emoji || '🤖';
|
|
294
|
+
const theme = agent.identity?.theme || '';
|
|
295
|
+
|
|
296
|
+
let html = `
|
|
297
|
+
<div class="agent-detail-row">
|
|
298
|
+
<span class="agent-detail-label">Name</span>
|
|
299
|
+
<span class="agent-detail-value">${escapeHtml(name)}</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="agent-detail-row">
|
|
302
|
+
<span class="agent-detail-label">Emoji</span>
|
|
303
|
+
<span class="agent-detail-value">${emoji}</span>
|
|
304
|
+
</div>
|
|
305
|
+
`;
|
|
306
|
+
if (theme) {
|
|
307
|
+
html += `
|
|
308
|
+
<div class="agent-detail-row">
|
|
309
|
+
<span class="agent-detail-label">Theme</span>
|
|
310
|
+
<span class="agent-detail-value">${escapeHtml(theme)}</span>
|
|
311
|
+
</div>
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
return html;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function renderModelView(agent) {
|
|
318
|
+
const model = getModelDisplay(agent);
|
|
319
|
+
const isInherited = !agent.model;
|
|
320
|
+
const fallbacks = agent.model?.fallbacks || defaults.model?.fallbacks || [];
|
|
321
|
+
|
|
322
|
+
let html = `
|
|
323
|
+
<div class="agent-detail-row">
|
|
324
|
+
<span class="agent-detail-label">Primary</span>
|
|
325
|
+
<span class="agent-detail-value">${escapeHtml(model)}${isInherited ? ' <span class="agent-detail-inherited">(inherited)</span>' : ''}</span>
|
|
326
|
+
</div>
|
|
327
|
+
`;
|
|
328
|
+
|
|
329
|
+
if (fallbacks.length > 0) {
|
|
330
|
+
html += `
|
|
331
|
+
<div class="agent-detail-row">
|
|
332
|
+
<span class="agent-detail-label">Fallbacks</span>
|
|
333
|
+
<div class="agent-tool-tags">
|
|
334
|
+
${fallbacks.map(f => `<span class="agent-tool-tag">${escapeHtml(f)}</span>`).join('')}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
`;
|
|
338
|
+
}
|
|
339
|
+
return html;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function renderSandboxView(agent) {
|
|
343
|
+
const sandbox = agent.sandbox || defaults.sandbox;
|
|
344
|
+
if (!sandbox) {
|
|
345
|
+
return `<div class="agent-detail-row"><span class="agent-detail-value agent-detail-muted">Inherits defaults (off)</span></div>`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const mode = sandbox.mode || 'off';
|
|
349
|
+
const scope = sandbox.scope || 'session';
|
|
350
|
+
const access = sandbox.workspaceAccess || 'rw';
|
|
351
|
+
|
|
352
|
+
let html = `
|
|
353
|
+
<div class="agent-detail-row">
|
|
354
|
+
<span class="agent-detail-label">Mode</span>
|
|
355
|
+
<span class="agent-detail-value">
|
|
356
|
+
<span class="agent-badge agent-badge-${mode === 'off' ? 'off' : 'sandbox'}">${mode}</span>
|
|
357
|
+
</span>
|
|
358
|
+
</div>
|
|
359
|
+
`;
|
|
360
|
+
|
|
361
|
+
if (mode !== 'off') {
|
|
362
|
+
html += `
|
|
363
|
+
<div class="agent-detail-row">
|
|
364
|
+
<span class="agent-detail-label">Scope</span>
|
|
365
|
+
<span class="agent-detail-value">${scope}</span>
|
|
366
|
+
</div>
|
|
367
|
+
<div class="agent-detail-row">
|
|
368
|
+
<span class="agent-detail-label">Workspace Access</span>
|
|
369
|
+
<span class="agent-detail-value">${access}</span>
|
|
370
|
+
</div>
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
return html;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function renderToolsView(agent) {
|
|
377
|
+
const tools = agent.tools;
|
|
378
|
+
if (!tools) {
|
|
379
|
+
return `<div class="agent-detail-row"><span class="agent-detail-value agent-detail-muted">Full access (no restrictions)</span></div>`;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
let html = '';
|
|
383
|
+
|
|
384
|
+
if (tools.profile) {
|
|
385
|
+
html += `
|
|
386
|
+
<div class="agent-detail-row">
|
|
387
|
+
<span class="agent-detail-label">Profile</span>
|
|
388
|
+
<span class="agent-detail-value">${escapeHtml(tools.profile)}</span>
|
|
389
|
+
</div>
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (tools.allow && tools.allow.length > 0) {
|
|
394
|
+
html += `
|
|
395
|
+
<div class="agent-detail-row">
|
|
396
|
+
<span class="agent-detail-label">Allowed</span>
|
|
397
|
+
<div class="agent-tool-tags">
|
|
398
|
+
${tools.allow.map(t => `<span class="agent-tool-tag agent-tool-allow">${escapeHtml(t)}</span>`).join('')}
|
|
399
|
+
</div>
|
|
400
|
+
</div>
|
|
401
|
+
`;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (tools.deny && tools.deny.length > 0) {
|
|
405
|
+
html += `
|
|
406
|
+
<div class="agent-detail-row">
|
|
407
|
+
<span class="agent-detail-label">Denied</span>
|
|
408
|
+
<div class="agent-tool-tags">
|
|
409
|
+
${tools.deny.map(t => `<span class="agent-tool-tag agent-tool-deny">${escapeHtml(t)}</span>`).join('')}
|
|
410
|
+
</div>
|
|
411
|
+
</div>
|
|
412
|
+
`;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
return html || `<div class="agent-detail-row"><span class="agent-detail-value agent-detail-muted">Full access</span></div>`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function renderSubagentsView(agent) {
|
|
419
|
+
const sub = agent.subagents;
|
|
420
|
+
const defSub = defaults.subagents || {};
|
|
421
|
+
|
|
422
|
+
if (!sub && !defSub.maxConcurrent) {
|
|
423
|
+
return `<div class="agent-detail-row"><span class="agent-detail-value agent-detail-muted">No sub-agent configuration</span></div>`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let html = '';
|
|
427
|
+
const effectiveSub = sub || {};
|
|
428
|
+
|
|
429
|
+
if (effectiveSub.model) {
|
|
430
|
+
html += `
|
|
431
|
+
<div class="agent-detail-row">
|
|
432
|
+
<span class="agent-detail-label">Default Model</span>
|
|
433
|
+
<span class="agent-detail-value">${escapeHtml(effectiveSub.model)}</span>
|
|
434
|
+
</div>
|
|
435
|
+
`;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (effectiveSub.thinking) {
|
|
439
|
+
html += `
|
|
440
|
+
<div class="agent-detail-row">
|
|
441
|
+
<span class="agent-detail-label">Thinking</span>
|
|
442
|
+
<span class="agent-detail-value">${escapeHtml(effectiveSub.thinking)}</span>
|
|
443
|
+
</div>
|
|
444
|
+
`;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (effectiveSub.allowAgents && effectiveSub.allowAgents.length > 0) {
|
|
448
|
+
html += `
|
|
449
|
+
<div class="agent-detail-row">
|
|
450
|
+
<span class="agent-detail-label">Allowed Models</span>
|
|
451
|
+
<div class="agent-tool-tags">
|
|
452
|
+
${effectiveSub.allowAgents.map(a => `<span class="agent-tool-tag">${escapeHtml(a)}</span>`).join('')}
|
|
453
|
+
</div>
|
|
454
|
+
</div>
|
|
455
|
+
`;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const maxConc = effectiveSub.maxConcurrent || defSub.maxConcurrent;
|
|
459
|
+
if (maxConc) {
|
|
460
|
+
html += `
|
|
461
|
+
<div class="agent-detail-row">
|
|
462
|
+
<span class="agent-detail-label">Max Concurrent</span>
|
|
463
|
+
<span class="agent-detail-value">${maxConc}${!effectiveSub.maxConcurrent && defSub.maxConcurrent ? ' <span class="agent-detail-inherited">(default)</span>' : ''}</span>
|
|
464
|
+
</div>
|
|
465
|
+
`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return html || `<div class="agent-detail-row"><span class="agent-detail-value agent-detail-muted">Inherits defaults</span></div>`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function renderBindingsDetail(agentBindings) {
|
|
472
|
+
return agentBindings.map(b => {
|
|
473
|
+
const match = b.match || {};
|
|
474
|
+
let desc = match.channel || 'any';
|
|
475
|
+
if (match.accountId && match.accountId !== '*') desc += ` / ${match.accountId}`;
|
|
476
|
+
if (match.peer) desc += ` → ${match.peer.kind}: ${match.peer.id}`;
|
|
477
|
+
if (match.guildId) desc += ` (guild: ${match.guildId})`;
|
|
478
|
+
if (match.teamId) desc += ` (team: ${match.teamId})`;
|
|
479
|
+
|
|
480
|
+
return `
|
|
481
|
+
<div class="agent-detail-row">
|
|
482
|
+
<span class="agent-detail-value agent-detail-mono">${escapeHtml(desc)}</span>
|
|
483
|
+
</div>
|
|
484
|
+
`;
|
|
485
|
+
}).join('');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function renderRoutingView(agent, agentBindings) {
|
|
489
|
+
if (agentBindings.length === 0) {
|
|
490
|
+
let msg = 'No routes configured';
|
|
491
|
+
if (agent.default) {
|
|
492
|
+
msg += ' — receives all unmatched messages';
|
|
493
|
+
} else {
|
|
494
|
+
msg += ' — will not receive any messages';
|
|
495
|
+
}
|
|
496
|
+
return `<div class="agent-detail-row"><span class="agent-detail-value agent-detail-muted">${msg}</span></div>`;
|
|
497
|
+
}
|
|
498
|
+
return renderBindingsDetail(agentBindings);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function renderRoutingEdit(agent, agentBindings) {
|
|
502
|
+
const channelOptions = channels.map(ch =>
|
|
503
|
+
`<option value="${escapeAttr(ch)}">${escapeHtml(ch)}</option>`
|
|
504
|
+
).join('');
|
|
505
|
+
|
|
506
|
+
const agentOptions = agents.map(a =>
|
|
507
|
+
`<option value="${escapeAttr(a.id)}">${escapeHtml(a.identity?.name || a.name || a.id)}</option>`
|
|
508
|
+
).join('');
|
|
509
|
+
|
|
510
|
+
// Render existing bindings as editable rows
|
|
511
|
+
const rows = agentBindings.map((b, i) => renderBindingRow(b, i, channelOptions)).join('');
|
|
512
|
+
|
|
513
|
+
return `
|
|
514
|
+
<div class="agent-binding-info">
|
|
515
|
+
Routes determine which messages this agent receives. Most-specific match wins.
|
|
516
|
+
${agent.default ? '<br><em>As the default agent, unmatched messages come here automatically.</em>' : ''}
|
|
517
|
+
</div>
|
|
518
|
+
<div class="agent-bindings-list" id="bindingsList">
|
|
519
|
+
${rows || '<div class="agent-detail-muted agent-bindings-empty">No routes — add one below</div>'}
|
|
520
|
+
</div>
|
|
521
|
+
<button class="agent-binding-add" id="addBindingBtn">+ Add Route</button>
|
|
522
|
+
`;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function renderBindingRow(binding, index, _channelOptions) {
|
|
526
|
+
const match = binding.match || {};
|
|
527
|
+
const peerKind = match.peer?.kind || '';
|
|
528
|
+
const peerId = match.peer?.id || '';
|
|
529
|
+
|
|
530
|
+
// Build channel options with proper selected state
|
|
531
|
+
const chOpts = channels.map(ch =>
|
|
532
|
+
`<option value="${escapeAttr(ch)}" ${ch === (match.channel || '') ? 'selected' : ''}>${escapeHtml(ch)}</option>`
|
|
533
|
+
).join('');
|
|
534
|
+
|
|
535
|
+
return `
|
|
536
|
+
<div class="agent-binding-row" data-binding-index="${index}">
|
|
537
|
+
<div class="agent-binding-fields">
|
|
538
|
+
<div class="agent-edit-field">
|
|
539
|
+
<label class="agent-edit-label">Channel</label>
|
|
540
|
+
<select class="agent-edit-select agent-binding-channel" data-index="${index}">
|
|
541
|
+
<option value="" ${!match.channel ? 'selected' : ''}>Any channel</option>
|
|
542
|
+
${chOpts}
|
|
543
|
+
</select>
|
|
544
|
+
</div>
|
|
545
|
+
<div class="agent-edit-field">
|
|
546
|
+
<label class="agent-edit-label">Account ID</label>
|
|
547
|
+
<input type="text" class="agent-edit-input agent-binding-account" data-index="${index}"
|
|
548
|
+
value="${escapeAttr(match.accountId || '')}" placeholder="* (any)">
|
|
549
|
+
</div>
|
|
550
|
+
<div class="agent-edit-field">
|
|
551
|
+
<label class="agent-edit-label">Peer Type</label>
|
|
552
|
+
<select class="agent-edit-select agent-binding-peer-kind" data-index="${index}">
|
|
553
|
+
<option value="" ${!peerKind ? 'selected' : ''}>None</option>
|
|
554
|
+
<option value="direct" ${peerKind === 'direct' ? 'selected' : ''}>Direct (DM)</option>
|
|
555
|
+
<option value="group" ${peerKind === 'group' ? 'selected' : ''}>Group</option>
|
|
556
|
+
<option value="channel" ${peerKind === 'channel' ? 'selected' : ''}>Channel</option>
|
|
557
|
+
</select>
|
|
558
|
+
</div>
|
|
559
|
+
<div class="agent-edit-field">
|
|
560
|
+
<label class="agent-edit-label">Peer ID</label>
|
|
561
|
+
<input type="text" class="agent-edit-input agent-binding-peer-id" data-index="${index}"
|
|
562
|
+
value="${escapeAttr(peerId)}" placeholder="e.g. +15551234567 or group ID"
|
|
563
|
+
${!peerKind ? 'disabled' : ''}>
|
|
564
|
+
</div>
|
|
565
|
+
${match.guildId ? `
|
|
566
|
+
<div class="agent-edit-field">
|
|
567
|
+
<label class="agent-edit-label">Guild ID <span class="agent-edit-hint">(Discord)</span></label>
|
|
568
|
+
<input type="text" class="agent-edit-input agent-binding-guild" data-index="${index}"
|
|
569
|
+
value="${escapeAttr(match.guildId || '')}">
|
|
570
|
+
</div>` : ''}
|
|
571
|
+
${match.teamId ? `
|
|
572
|
+
<div class="agent-edit-field">
|
|
573
|
+
<label class="agent-edit-label">Team ID <span class="agent-edit-hint">(Slack)</span></label>
|
|
574
|
+
<input type="text" class="agent-edit-input agent-binding-team" data-index="${index}"
|
|
575
|
+
value="${escapeAttr(match.teamId || '')}">
|
|
576
|
+
</div>` : ''}
|
|
577
|
+
</div>
|
|
578
|
+
<button class="agent-binding-remove" data-index="${index}" title="Remove route">✕</button>
|
|
579
|
+
</div>
|
|
580
|
+
`;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// ============================================
|
|
584
|
+
// RAW JSON VIEW
|
|
585
|
+
// ============================================
|
|
586
|
+
|
|
587
|
+
function renderRawJsonSection(agent) {
|
|
588
|
+
// Build the raw config object (only non-null fields)
|
|
589
|
+
const raw = { id: agent.id };
|
|
590
|
+
if (agent.name && agent.name !== agent.id) raw.name = agent.name;
|
|
591
|
+
if (agent.default) raw.default = true;
|
|
592
|
+
if (agent.model) raw.model = agent.model;
|
|
593
|
+
if (agent.identity) raw.identity = agent.identity;
|
|
594
|
+
if (agent.workspace) raw.workspace = agent.workspace;
|
|
595
|
+
if (agent.sandbox) raw.sandbox = agent.sandbox;
|
|
596
|
+
if (agent.tools) raw.tools = agent.tools;
|
|
597
|
+
if (agent.subagents) raw.subagents = agent.subagents;
|
|
598
|
+
if (agent.groupChat) raw.groupChat = agent.groupChat;
|
|
599
|
+
|
|
600
|
+
const json = JSON.stringify(raw, null, 2);
|
|
601
|
+
|
|
602
|
+
return `
|
|
603
|
+
<div class="agent-section agent-section-collapsible" data-section="raw">
|
|
604
|
+
<div class="agent-section-header agent-raw-toggle">
|
|
605
|
+
<span class="agent-section-title">Raw Config</span>
|
|
606
|
+
<span class="agent-raw-chevron">▸</span>
|
|
607
|
+
</div>
|
|
608
|
+
<div class="agent-section-body agent-raw-body" style="display:none">
|
|
609
|
+
<pre class="agent-raw-json">${escapeHtml(json)}</pre>
|
|
610
|
+
<button class="agent-raw-copy" title="Copy to clipboard">Copy JSON</button>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ============================================
|
|
617
|
+
// EDIT RENDERERS (form inputs)
|
|
618
|
+
// ============================================
|
|
619
|
+
|
|
620
|
+
function renderIdentityEdit(agent) {
|
|
621
|
+
const name = agent.identity?.name || agent.name || '';
|
|
622
|
+
const emoji = agent.identity?.emoji || '';
|
|
623
|
+
const theme = agent.identity?.theme || '';
|
|
624
|
+
|
|
625
|
+
return `
|
|
626
|
+
<div class="agent-edit-field">
|
|
627
|
+
<label class="agent-edit-label">Display Name</label>
|
|
628
|
+
<input type="text" class="agent-edit-input" data-field="identity.name" value="${escapeAttr(name)}" placeholder="Agent name">
|
|
629
|
+
</div>
|
|
630
|
+
<div class="agent-edit-field">
|
|
631
|
+
<label class="agent-edit-label">Emoji</label>
|
|
632
|
+
<input type="text" class="agent-edit-input agent-edit-input-short" data-field="identity.emoji" value="${escapeAttr(emoji)}" placeholder="🤖" maxlength="4">
|
|
633
|
+
</div>
|
|
634
|
+
<div class="agent-edit-field">
|
|
635
|
+
<label class="agent-edit-label">Persona Theme <span class="agent-edit-hint">(injected into system prompt)</span></label>
|
|
636
|
+
<input type="text" class="agent-edit-input" data-field="identity.theme" value="${escapeAttr(theme)}" placeholder="e.g. helpful assistant, space lobster">
|
|
637
|
+
</div>
|
|
638
|
+
`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function renderModelEdit(agent) {
|
|
642
|
+
const currentModel = agent.model?.primary || agent.model || '';
|
|
643
|
+
const isObj = typeof agent.model === 'object' && agent.model !== null;
|
|
644
|
+
const fallbacks = isObj ? (agent.model.fallbacks || []) : [];
|
|
645
|
+
const availableModels = defaults.models || [];
|
|
646
|
+
|
|
647
|
+
// Build model options from the defaults catalog
|
|
648
|
+
const modelOptions = availableModels.map(m =>
|
|
649
|
+
`<option value="${escapeAttr(m)}" ${m === currentModel ? 'selected' : ''}>${escapeHtml(m)}</option>`
|
|
650
|
+
).join('');
|
|
651
|
+
|
|
652
|
+
return `
|
|
653
|
+
<div class="agent-edit-field">
|
|
654
|
+
<label class="agent-edit-label">Primary Model</label>
|
|
655
|
+
<select class="agent-edit-select" data-field="model.primary">
|
|
656
|
+
<option value="">Inherit from defaults</option>
|
|
657
|
+
${modelOptions}
|
|
658
|
+
</select>
|
|
659
|
+
</div>
|
|
660
|
+
<div class="agent-edit-field">
|
|
661
|
+
<label class="agent-edit-label">Fallbacks <span class="agent-edit-hint">(comma-separated)</span></label>
|
|
662
|
+
<input type="text" class="agent-edit-input" data-field="model.fallbacks" value="${escapeAttr(fallbacks.join(', '))}" placeholder="model/name-1, model/name-2">
|
|
663
|
+
</div>
|
|
664
|
+
`;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function renderSandboxEdit(agent) {
|
|
668
|
+
const sandbox = agent.sandbox || {};
|
|
669
|
+
const mode = sandbox.mode || 'off';
|
|
670
|
+
const scope = sandbox.scope || 'session';
|
|
671
|
+
const access = sandbox.workspaceAccess || 'rw';
|
|
672
|
+
|
|
673
|
+
return `
|
|
674
|
+
<div class="agent-edit-field">
|
|
675
|
+
<label class="agent-edit-label">Mode</label>
|
|
676
|
+
<select class="agent-edit-select" data-field="sandbox.mode">
|
|
677
|
+
<option value="off" ${mode === 'off' ? 'selected' : ''}>Off</option>
|
|
678
|
+
<option value="non-main" ${mode === 'non-main' ? 'selected' : ''}>Non-main sessions</option>
|
|
679
|
+
<option value="all" ${mode === 'all' ? 'selected' : ''}>All sessions</option>
|
|
680
|
+
</select>
|
|
681
|
+
</div>
|
|
682
|
+
<div class="agent-edit-field">
|
|
683
|
+
<label class="agent-edit-label">Scope</label>
|
|
684
|
+
<select class="agent-edit-select" data-field="sandbox.scope">
|
|
685
|
+
<option value="session" ${scope === 'session' ? 'selected' : ''}>Per session</option>
|
|
686
|
+
<option value="agent" ${scope === 'agent' ? 'selected' : ''}>Per agent</option>
|
|
687
|
+
<option value="shared" ${scope === 'shared' ? 'selected' : ''}>Shared</option>
|
|
688
|
+
</select>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="agent-edit-field">
|
|
691
|
+
<label class="agent-edit-label">Workspace Access</label>
|
|
692
|
+
<select class="agent-edit-select" data-field="sandbox.workspaceAccess">
|
|
693
|
+
<option value="rw" ${access === 'rw' ? 'selected' : ''}>Read/Write</option>
|
|
694
|
+
<option value="ro" ${access === 'ro' ? 'selected' : ''}>Read Only</option>
|
|
695
|
+
<option value="none" ${access === 'none' ? 'selected' : ''}>None</option>
|
|
696
|
+
</select>
|
|
697
|
+
</div>
|
|
698
|
+
`;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// Tool presets (imported from AgentsUI module)
|
|
702
|
+
|
|
703
|
+
function renderToolsEdit(agent) {
|
|
704
|
+
const tools = agent.tools || {};
|
|
705
|
+
const denyList = tools.deny || [];
|
|
706
|
+
const allowList = tools.allow || [];
|
|
707
|
+
const mode = allowList.length > 0 ? 'allow' : denyList.length > 0 ? 'deny' : 'full';
|
|
708
|
+
|
|
709
|
+
const presetButtons = Object.entries(TOOL_PRESETS).map(([key, preset]) =>
|
|
710
|
+
`<button class="agent-preset-btn" data-preset="${key}">${preset.label}</button>`
|
|
711
|
+
).join('');
|
|
712
|
+
|
|
713
|
+
let html = `
|
|
714
|
+
<div class="agent-edit-field">
|
|
715
|
+
<label class="agent-edit-label">Presets</label>
|
|
716
|
+
<div class="agent-preset-bar">${presetButtons}</div>
|
|
717
|
+
</div>
|
|
718
|
+
<div class="agent-edit-field">
|
|
719
|
+
<label class="agent-edit-label">Mode</label>
|
|
720
|
+
<select class="agent-edit-select" data-field="tools.mode" id="toolsModeSelect">
|
|
721
|
+
<option value="full" ${mode === 'full' ? 'selected' : ''}>Full access (no restrictions)</option>
|
|
722
|
+
<option value="allow" ${mode === 'allow' ? 'selected' : ''}>Allowlist (only selected tools)</option>
|
|
723
|
+
<option value="deny" ${mode === 'deny' ? 'selected' : ''}>Denylist (block selected tools)</option>
|
|
724
|
+
</select>
|
|
725
|
+
</div>
|
|
726
|
+
<div class="agent-tools-grid" id="toolsGrid" style="${mode === 'full' ? 'display:none' : ''}">
|
|
727
|
+
`;
|
|
728
|
+
|
|
729
|
+
// Render tool groups with checkboxes
|
|
730
|
+
for (const [groupName, group] of Object.entries(TOOL_GROUPS)) {
|
|
731
|
+
const activeList = mode === 'allow' ? allowList : denyList;
|
|
732
|
+
const groupChecked = activeList.includes(group.id);
|
|
733
|
+
const individualChecked = group.tools.filter(t => activeList.includes(t));
|
|
734
|
+
|
|
735
|
+
html += `
|
|
736
|
+
<div class="agent-tool-group">
|
|
737
|
+
<label class="agent-tool-group-label">
|
|
738
|
+
<input type="checkbox" class="agent-tool-group-check" data-group="${group.id}"
|
|
739
|
+
${groupChecked ? 'checked' : ''}>
|
|
740
|
+
<span>${groupName}</span>
|
|
741
|
+
<span class="agent-tool-group-hint">${group.tools.join(', ')}</span>
|
|
742
|
+
</label>
|
|
743
|
+
<div class="agent-tool-group-items">
|
|
744
|
+
${group.tools.map(tool => `
|
|
745
|
+
<label class="agent-tool-item-label">
|
|
746
|
+
<input type="checkbox" class="agent-tool-item-check" data-tool="${tool}" data-group-id="${group.id}"
|
|
747
|
+
${groupChecked || individualChecked.includes(tool) ? 'checked' : ''}>
|
|
748
|
+
<span>${tool}</span>
|
|
749
|
+
</label>
|
|
750
|
+
`).join('')}
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
`;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
html += '</div>';
|
|
757
|
+
return html;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function renderSubagentsEdit(agent) {
|
|
761
|
+
const sub = agent.subagents || {};
|
|
762
|
+
const allowAgents = sub.allowAgents || [];
|
|
763
|
+
const maxConc = sub.maxConcurrent || '';
|
|
764
|
+
|
|
765
|
+
return `
|
|
766
|
+
<div class="agent-edit-field">
|
|
767
|
+
<label class="agent-edit-label">Allowed Models <span class="agent-edit-hint">(one per line)</span></label>
|
|
768
|
+
<textarea class="agent-edit-textarea" data-field="subagents.allowAgents" rows="4" placeholder="anthropic/claude-sonnet-4-5 nvidia/moonshotai/kimi-k2.5">${escapeHtml(allowAgents.join('\n'))}</textarea>
|
|
769
|
+
</div>
|
|
770
|
+
<div class="agent-edit-field">
|
|
771
|
+
<label class="agent-edit-label">Max Concurrent</label>
|
|
772
|
+
<input type="number" class="agent-edit-input agent-edit-input-short" data-field="subagents.maxConcurrent" value="${maxConc}" placeholder="Default" min="1" max="100">
|
|
773
|
+
</div>
|
|
774
|
+
`;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// ============================================
|
|
778
|
+
// EDIT STATE MANAGEMENT
|
|
779
|
+
// ============================================
|
|
780
|
+
|
|
781
|
+
function collectSectionChanges(container, sectionId) {
|
|
782
|
+
const section = container.querySelector(`[data-section="${sectionId}"]`);
|
|
783
|
+
if (!section) return null;
|
|
784
|
+
|
|
785
|
+
const agent = agents.find(a => a.id === selectedAgentId);
|
|
786
|
+
if (!agent) return null;
|
|
787
|
+
|
|
788
|
+
const changes = {};
|
|
789
|
+
|
|
790
|
+
if (sectionId === 'identity') {
|
|
791
|
+
const name = section.querySelector('[data-field="identity.name"]')?.value?.trim();
|
|
792
|
+
const emoji = section.querySelector('[data-field="identity.emoji"]')?.value?.trim();
|
|
793
|
+
const theme = section.querySelector('[data-field="identity.theme"]')?.value?.trim();
|
|
794
|
+
|
|
795
|
+
changes.identity = {
|
|
796
|
+
...(agent.identity || {}),
|
|
797
|
+
name: name || undefined,
|
|
798
|
+
emoji: emoji || undefined,
|
|
799
|
+
theme: theme || undefined,
|
|
800
|
+
};
|
|
801
|
+
// Clean undefined values
|
|
802
|
+
Object.keys(changes.identity).forEach(k => {
|
|
803
|
+
if (changes.identity[k] === undefined || changes.identity[k] === '') delete changes.identity[k];
|
|
804
|
+
});
|
|
805
|
+
if (Object.keys(changes.identity).length === 0) changes.identity = null;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (sectionId === 'model') {
|
|
809
|
+
const primary = section.querySelector('[data-field="model.primary"]')?.value?.trim();
|
|
810
|
+
const fallbacksRaw = section.querySelector('[data-field="model.fallbacks"]')?.value?.trim();
|
|
811
|
+
const fallbacks = fallbacksRaw ? fallbacksRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
812
|
+
|
|
813
|
+
if (!primary) {
|
|
814
|
+
changes.model = null; // inherit from defaults
|
|
815
|
+
} else {
|
|
816
|
+
changes.model = { primary };
|
|
817
|
+
if (fallbacks.length > 0) changes.model.fallbacks = fallbacks;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
if (sectionId === 'sandbox') {
|
|
822
|
+
const mode = section.querySelector('[data-field="sandbox.mode"]')?.value;
|
|
823
|
+
if (mode === 'off') {
|
|
824
|
+
changes.sandbox = null; // remove sandbox config
|
|
825
|
+
} else {
|
|
826
|
+
changes.sandbox = {
|
|
827
|
+
mode,
|
|
828
|
+
scope: section.querySelector('[data-field="sandbox.scope"]')?.value || 'session',
|
|
829
|
+
workspaceAccess: section.querySelector('[data-field="sandbox.workspaceAccess"]')?.value || 'rw',
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
if (sectionId === 'tools') {
|
|
835
|
+
const mode = section.querySelector('[data-field="tools.mode"]')?.value;
|
|
836
|
+
if (mode === 'full') {
|
|
837
|
+
changes.tools = null; // remove tool restrictions
|
|
838
|
+
} else {
|
|
839
|
+
const checkedGroups = Array.from(section.querySelectorAll('.agent-tool-group-check:checked')).map(el => el.dataset.group);
|
|
840
|
+
const checkedTools = Array.from(section.querySelectorAll('.agent-tool-item-check:checked')).map(el => el.dataset.tool);
|
|
841
|
+
|
|
842
|
+
// Merge: use group IDs where all tools in group are checked, individual tools otherwise
|
|
843
|
+
const toolList = [...new Set([...checkedGroups, ...checkedTools])];
|
|
844
|
+
|
|
845
|
+
if (mode === 'allow') {
|
|
846
|
+
changes.tools = { allow: toolList };
|
|
847
|
+
} else {
|
|
848
|
+
changes.tools = { deny: toolList };
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
if (sectionId === 'subagents') {
|
|
854
|
+
const allowRaw = section.querySelector('[data-field="subagents.allowAgents"]')?.value?.trim();
|
|
855
|
+
const maxConc = section.querySelector('[data-field="subagents.maxConcurrent"]')?.value?.trim();
|
|
856
|
+
const allowAgents = allowRaw ? allowRaw.split('\n').map(s => s.trim()).filter(Boolean) : [];
|
|
857
|
+
|
|
858
|
+
if (allowAgents.length === 0 && !maxConc) {
|
|
859
|
+
changes.subagents = null;
|
|
860
|
+
} else {
|
|
861
|
+
changes.subagents = {};
|
|
862
|
+
if (allowAgents.length > 0) changes.subagents.allowAgents = allowAgents;
|
|
863
|
+
if (maxConc) changes.subagents.maxConcurrent = parseInt(maxConc, 10);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
return changes;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// ============================================
|
|
871
|
+
// ROUTING / BINDINGS SAVE
|
|
872
|
+
// ============================================
|
|
873
|
+
|
|
874
|
+
function collectBindingsFromEditor(container) {
|
|
875
|
+
const rows = container.querySelectorAll('.agent-binding-row');
|
|
876
|
+
const agentBindings = [];
|
|
877
|
+
|
|
878
|
+
rows.forEach(row => {
|
|
879
|
+
const channel = row.querySelector('.agent-binding-channel')?.value?.trim();
|
|
880
|
+
const accountId = row.querySelector('.agent-binding-account')?.value?.trim();
|
|
881
|
+
const peerKind = row.querySelector('.agent-binding-peer-kind')?.value?.trim();
|
|
882
|
+
const peerId = row.querySelector('.agent-binding-peer-id')?.value?.trim();
|
|
883
|
+
const guildId = row.querySelector('.agent-binding-guild')?.value?.trim();
|
|
884
|
+
const teamId = row.querySelector('.agent-binding-team')?.value?.trim();
|
|
885
|
+
|
|
886
|
+
const match = {};
|
|
887
|
+
if (channel) match.channel = channel;
|
|
888
|
+
if (accountId && accountId !== '*') match.accountId = accountId;
|
|
889
|
+
if (peerKind && peerId) match.peer = { kind: peerKind, id: peerId };
|
|
890
|
+
if (guildId) match.guildId = guildId;
|
|
891
|
+
if (teamId) match.teamId = teamId;
|
|
892
|
+
|
|
893
|
+
agentBindings.push({ agentId: selectedAgentId, match });
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
return agentBindings;
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
async function saveBindings(container) {
|
|
900
|
+
try {
|
|
901
|
+
// Collect the edited bindings for this agent
|
|
902
|
+
const editedBindings = collectBindingsFromEditor(container);
|
|
903
|
+
|
|
904
|
+
// Build the full bindings array: other agents' bindings + this agent's edited ones
|
|
905
|
+
const otherBindings = bindings.filter(b => b.agentId !== selectedAgentId);
|
|
906
|
+
const allBindings = [...otherBindings, ...editedBindings];
|
|
907
|
+
|
|
908
|
+
const result = await AgentsData.saveBindings(allBindings, () => {
|
|
909
|
+
showToast('Config was modified externally. Refreshing...', 'warning');
|
|
910
|
+
syncState();
|
|
911
|
+
rerender();
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
syncState();
|
|
915
|
+
if (result) {
|
|
916
|
+
showToast('Routes updated', 'success');
|
|
917
|
+
}
|
|
918
|
+
return result;
|
|
919
|
+
} catch (err) {
|
|
920
|
+
showToast(`Save failed: ${err.message}`, 'error');
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ============================================
|
|
926
|
+
// CREATE FORM
|
|
927
|
+
// ============================================
|
|
928
|
+
|
|
929
|
+
function renderCreateForm() {
|
|
930
|
+
const availableModels = defaults.models || [];
|
|
931
|
+
const modelOptions = availableModels.map(m =>
|
|
932
|
+
`<option value="${escapeAttr(m)}">${escapeHtml(m)}</option>`
|
|
933
|
+
).join('');
|
|
934
|
+
|
|
935
|
+
return `
|
|
936
|
+
<div class="agent-detail">
|
|
937
|
+
<div class="agent-detail-header">
|
|
938
|
+
<button class="agent-detail-back" aria-label="Back to agent list">← Back</button>
|
|
939
|
+
</div>
|
|
940
|
+
|
|
941
|
+
<div class="agent-create-title">New Agent</div>
|
|
942
|
+
<div class="agent-create-warning">
|
|
943
|
+
⚠️ Creating an agent requires a gateway restart. Active sessions will briefly disconnect.
|
|
944
|
+
</div>
|
|
945
|
+
|
|
946
|
+
<div class="agent-section">
|
|
947
|
+
<div class="agent-section-header"><span class="agent-section-title">Identity</span></div>
|
|
948
|
+
<div class="agent-section-body">
|
|
949
|
+
<div class="agent-edit-field">
|
|
950
|
+
<label class="agent-edit-label">Agent ID <span class="agent-edit-hint">(lowercase, alphanumeric + hyphens, permanent)</span></label>
|
|
951
|
+
<input type="text" class="agent-edit-input" id="newAgentId" placeholder="my-agent" pattern="[a-z0-9][a-z0-9-]*">
|
|
952
|
+
</div>
|
|
953
|
+
<div class="agent-edit-field">
|
|
954
|
+
<label class="agent-edit-label">Display Name</label>
|
|
955
|
+
<input type="text" class="agent-edit-input" id="newAgentName" placeholder="My Agent">
|
|
956
|
+
</div>
|
|
957
|
+
<div class="agent-edit-field">
|
|
958
|
+
<label class="agent-edit-label">Emoji</label>
|
|
959
|
+
<input type="text" class="agent-edit-input agent-edit-input-short" id="newAgentEmoji" placeholder="🤖" maxlength="4">
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
</div>
|
|
963
|
+
|
|
964
|
+
<div class="agent-section">
|
|
965
|
+
<div class="agent-section-header"><span class="agent-section-title">Model</span></div>
|
|
966
|
+
<div class="agent-section-body">
|
|
967
|
+
<div class="agent-edit-field">
|
|
968
|
+
<label class="agent-edit-label">Primary Model</label>
|
|
969
|
+
<select class="agent-edit-select" id="newAgentModel">
|
|
970
|
+
<option value="">Inherit from defaults</option>
|
|
971
|
+
${modelOptions}
|
|
972
|
+
</select>
|
|
973
|
+
</div>
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
|
|
977
|
+
<div class="agent-section">
|
|
978
|
+
<div class="agent-section-header"><span class="agent-section-title">Workspace</span></div>
|
|
979
|
+
<div class="agent-section-body">
|
|
980
|
+
<div class="agent-edit-field">
|
|
981
|
+
<label class="agent-edit-label">Path <span class="agent-edit-hint">(leave empty for default)</span></label>
|
|
982
|
+
<input type="text" class="agent-edit-input" id="newAgentWorkspace" placeholder="Auto-generated">
|
|
983
|
+
</div>
|
|
984
|
+
</div>
|
|
985
|
+
</div>
|
|
986
|
+
|
|
987
|
+
<div class="agents-list-actions" style="padding-top: 12px;">
|
|
988
|
+
<button class="agents-create-btn agents-create-submit" id="submitCreateAgent">Create Agent & Restart Gateway</button>
|
|
989
|
+
</div>
|
|
990
|
+
</div>
|
|
991
|
+
`;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function bindCreateEvents(container) {
|
|
995
|
+
container.querySelector('.agent-detail-back')?.addEventListener('click', () => {
|
|
996
|
+
currentView = 'list';
|
|
997
|
+
rerender();
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
container.querySelector('#submitCreateAgent')?.addEventListener('click', async () => {
|
|
1001
|
+
const id = container.querySelector('#newAgentId')?.value?.trim();
|
|
1002
|
+
const name = container.querySelector('#newAgentName')?.value?.trim();
|
|
1003
|
+
const emoji = container.querySelector('#newAgentEmoji')?.value?.trim();
|
|
1004
|
+
const model = container.querySelector('#newAgentModel')?.value?.trim();
|
|
1005
|
+
const workspace = container.querySelector('#newAgentWorkspace')?.value?.trim();
|
|
1006
|
+
|
|
1007
|
+
if (!id) {
|
|
1008
|
+
showToast('Agent ID is required', 'error');
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) {
|
|
1012
|
+
showToast('Agent ID must be lowercase alphanumeric + hyphens', 'error');
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (agents.some(a => a.id === id)) {
|
|
1016
|
+
showToast(`Agent "${id}" already exists`, 'error');
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const agent = { id };
|
|
1021
|
+
if (name) agent.name = name;
|
|
1022
|
+
if (emoji || name) {
|
|
1023
|
+
agent.identity = {};
|
|
1024
|
+
if (name) agent.identity.name = name;
|
|
1025
|
+
if (emoji) agent.identity.emoji = emoji;
|
|
1026
|
+
}
|
|
1027
|
+
if (model) agent.model = model;
|
|
1028
|
+
if (workspace) agent.workspace = workspace;
|
|
1029
|
+
|
|
1030
|
+
await createAgent(agent);
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
// ============================================
|
|
1035
|
+
// DELETE CONFIRMATION
|
|
1036
|
+
// ============================================
|
|
1037
|
+
|
|
1038
|
+
function showDeleteConfirm(agentId, container) {
|
|
1039
|
+
const agent = agents.find(a => a.id === agentId);
|
|
1040
|
+
const name = agent?.identity?.name || agent?.name || agentId;
|
|
1041
|
+
|
|
1042
|
+
const overlay = document.createElement('div');
|
|
1043
|
+
overlay.className = 'agent-confirm-overlay';
|
|
1044
|
+
overlay.innerHTML = `
|
|
1045
|
+
<div class="agent-confirm-dialog">
|
|
1046
|
+
<div class="agent-confirm-title">Delete Agent</div>
|
|
1047
|
+
<div class="agent-confirm-body">
|
|
1048
|
+
Are you sure you want to delete <strong>${escapeHtml(name)}</strong>?
|
|
1049
|
+
<div class="agent-create-warning" style="margin-top:8px;">
|
|
1050
|
+
⚠️ This requires a gateway restart. Active sessions will briefly disconnect. Any bindings routing to this agent will also be removed.
|
|
1051
|
+
</div>
|
|
1052
|
+
</div>
|
|
1053
|
+
<div class="agent-confirm-actions">
|
|
1054
|
+
<button class="agent-section-btn agent-section-cancel" id="confirmCancel">Cancel</button>
|
|
1055
|
+
<button class="agent-section-btn agent-confirm-delete" id="confirmDelete">Delete & Restart</button>
|
|
1056
|
+
</div>
|
|
1057
|
+
</div>
|
|
1058
|
+
`;
|
|
1059
|
+
|
|
1060
|
+
container.appendChild(overlay);
|
|
1061
|
+
|
|
1062
|
+
overlay.querySelector('#confirmCancel').addEventListener('click', () => overlay.remove());
|
|
1063
|
+
overlay.querySelector('#confirmDelete').addEventListener('click', async () => {
|
|
1064
|
+
overlay.remove();
|
|
1065
|
+
await deleteAgent(agentId);
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
// ============================================
|
|
1070
|
+
// RESTART OVERLAY
|
|
1071
|
+
// ============================================
|
|
1072
|
+
|
|
1073
|
+
function renderRestartOverlay() {
|
|
1074
|
+
return `
|
|
1075
|
+
<div class="agent-restart-overlay">
|
|
1076
|
+
<div class="agent-restart-content">
|
|
1077
|
+
<span class="agents-loading-dot"></span>
|
|
1078
|
+
<div class="agent-restart-title">Gateway Restarting...</div>
|
|
1079
|
+
<div class="agent-restart-sub">Active sessions will reconnect automatically.</div>
|
|
1080
|
+
</div>
|
|
1081
|
+
</div>
|
|
1082
|
+
`;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function startRestartWatcher() {
|
|
1086
|
+
restartPending = true;
|
|
1087
|
+
rerender();
|
|
1088
|
+
|
|
1089
|
+
let attempts = 0;
|
|
1090
|
+
const maxAttempts = 20;
|
|
1091
|
+
const interval = setInterval(async () => {
|
|
1092
|
+
attempts++;
|
|
1093
|
+
try {
|
|
1094
|
+
const response = await fetch('/api/agents', { signal: AbortSignal.timeout(3000) });
|
|
1095
|
+
if (response.ok) {
|
|
1096
|
+
clearInterval(interval);
|
|
1097
|
+
restartPending = false;
|
|
1098
|
+
loaded = false;
|
|
1099
|
+
await fetchAgents();
|
|
1100
|
+
currentView = 'list';
|
|
1101
|
+
selectedAgentId = null;
|
|
1102
|
+
editingSection = null;
|
|
1103
|
+
rerender();
|
|
1104
|
+
showToast('Gateway restarted successfully', 'success');
|
|
1105
|
+
}
|
|
1106
|
+
} catch (e) {
|
|
1107
|
+
// Still down, keep trying
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (attempts >= maxAttempts) {
|
|
1111
|
+
clearInterval(interval);
|
|
1112
|
+
restartPending = false;
|
|
1113
|
+
rerender();
|
|
1114
|
+
showToast('Gateway didn\'t come back. Check the terminal.', 'error');
|
|
1115
|
+
}
|
|
1116
|
+
}, 2000);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// ============================================
|
|
1120
|
+
// CREATE / DELETE API
|
|
1121
|
+
// ============================================
|
|
1122
|
+
|
|
1123
|
+
async function createAgent(agent) {
|
|
1124
|
+
try {
|
|
1125
|
+
const data = await AgentsData.createAgent(agent);
|
|
1126
|
+
syncState();
|
|
1127
|
+
|
|
1128
|
+
if (data.requiresRestart) {
|
|
1129
|
+
startRestartWatcher();
|
|
1130
|
+
} else {
|
|
1131
|
+
loaded = false;
|
|
1132
|
+
await fetchAgents();
|
|
1133
|
+
currentView = 'list';
|
|
1134
|
+
rerender();
|
|
1135
|
+
showToast(`Agent "${agent.id}" created`, 'success');
|
|
1136
|
+
}
|
|
1137
|
+
} catch (err) {
|
|
1138
|
+
showToast(`Create failed: ${err.message}`, 'error');
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
async function deleteAgent(agentId) {
|
|
1143
|
+
try {
|
|
1144
|
+
const data = await AgentsData.deleteAgent(agentId);
|
|
1145
|
+
syncState();
|
|
1146
|
+
|
|
1147
|
+
if (data.requiresRestart) {
|
|
1148
|
+
startRestartWatcher();
|
|
1149
|
+
} else {
|
|
1150
|
+
loaded = false;
|
|
1151
|
+
await fetchAgents();
|
|
1152
|
+
currentView = 'list';
|
|
1153
|
+
selectedAgentId = null;
|
|
1154
|
+
rerender();
|
|
1155
|
+
showToast(`Agent "${agentId}" deleted`, 'success');
|
|
1156
|
+
}
|
|
1157
|
+
} catch (err) {
|
|
1158
|
+
showToast(`Delete failed: ${err.message}`, 'error');
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// ============================================
|
|
1163
|
+
// HELPERS
|
|
1164
|
+
// ============================================
|
|
1165
|
+
|
|
1166
|
+
function getModelDisplay(agent) {
|
|
1167
|
+
if (!agent.model) {
|
|
1168
|
+
const primary = defaults.model?.primary || defaults.model;
|
|
1169
|
+
if (typeof primary === 'string') return shortenModel(primary);
|
|
1170
|
+
if (primary?.primary) return shortenModel(primary.primary);
|
|
1171
|
+
return 'default';
|
|
1172
|
+
}
|
|
1173
|
+
if (typeof agent.model === 'string') return shortenModel(agent.model);
|
|
1174
|
+
if (agent.model.primary) return shortenModel(agent.model.primary);
|
|
1175
|
+
return 'default';
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function shortenModel(model) {
|
|
1179
|
+
if (!model) return 'default';
|
|
1180
|
+
const parts = model.split('/');
|
|
1181
|
+
return parts.length > 1 ? parts.slice(1).join('/') : model;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function getSandboxBadge(agent) {
|
|
1185
|
+
const sandbox = agent.sandbox || defaults.sandbox;
|
|
1186
|
+
if (!sandbox || sandbox.mode === 'off') return '';
|
|
1187
|
+
if (sandbox.mode === 'all') return '<span class="agent-badge agent-badge-sandbox">sandboxed</span>';
|
|
1188
|
+
if (sandbox.mode === 'non-main') return '<span class="agent-badge agent-badge-partial">partial sandbox</span>';
|
|
1189
|
+
return '';
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
function getToolsSummary(agent) {
|
|
1193
|
+
if (!agent.tools) return '';
|
|
1194
|
+
if (agent.tools.allow && agent.tools.allow.length > 0) {
|
|
1195
|
+
return `${agent.tools.allow.length} allowed`;
|
|
1196
|
+
}
|
|
1197
|
+
if (agent.tools.deny && agent.tools.deny.length > 0) {
|
|
1198
|
+
return `${agent.tools.deny.length} denied`;
|
|
1199
|
+
}
|
|
1200
|
+
return '';
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// escapeHtml and escapeAttr now imported from AgentsUI module
|
|
1204
|
+
|
|
1205
|
+
// ============================================
|
|
1206
|
+
// EVENT BINDING
|
|
1207
|
+
// ============================================
|
|
1208
|
+
|
|
1209
|
+
function bindBindingRowEvents(container, row) {
|
|
1210
|
+
if (!row) return;
|
|
1211
|
+
|
|
1212
|
+
// Remove button
|
|
1213
|
+
const removeBtn = row.querySelector('.agent-binding-remove');
|
|
1214
|
+
if (removeBtn) {
|
|
1215
|
+
removeBtn.addEventListener('click', () => {
|
|
1216
|
+
row.remove();
|
|
1217
|
+
// If no rows left, show empty message
|
|
1218
|
+
const list = container.querySelector('#bindingsList');
|
|
1219
|
+
if (list && list.querySelectorAll('.agent-binding-row').length === 0) {
|
|
1220
|
+
list.innerHTML = '<div class="agent-detail-muted agent-bindings-empty">No routes — add one below</div>';
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
// Peer kind toggle — enable/disable peer ID field
|
|
1226
|
+
const peerKindSelect = row.querySelector('.agent-binding-peer-kind');
|
|
1227
|
+
const peerIdInput = row.querySelector('.agent-binding-peer-id');
|
|
1228
|
+
if (peerKindSelect && peerIdInput) {
|
|
1229
|
+
peerKindSelect.addEventListener('change', () => {
|
|
1230
|
+
peerIdInput.disabled = !peerKindSelect.value;
|
|
1231
|
+
if (!peerKindSelect.value) peerIdInput.value = '';
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function bindListEvents(container) {
|
|
1237
|
+
// Attach avatar error handlers
|
|
1238
|
+
container.querySelectorAll('.agent-list-avatar-img').forEach(img => {
|
|
1239
|
+
img.addEventListener('error', function() {
|
|
1240
|
+
const parent = this.parentElement;
|
|
1241
|
+
const emoji = parent.dataset.fallbackEmoji || '🤖';
|
|
1242
|
+
parent.innerHTML = emoji;
|
|
1243
|
+
});
|
|
1244
|
+
});
|
|
1245
|
+
|
|
1246
|
+
container.querySelectorAll('.agent-item').forEach(item => {
|
|
1247
|
+
item.addEventListener('click', () => {
|
|
1248
|
+
selectedAgentId = item.dataset.agentId;
|
|
1249
|
+
currentView = 'detail';
|
|
1250
|
+
editingSection = null;
|
|
1251
|
+
pendingChanges = {};
|
|
1252
|
+
render(container);
|
|
1253
|
+
});
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
const refreshBtn = container.querySelector('.agents-refresh-btn');
|
|
1257
|
+
if (refreshBtn) {
|
|
1258
|
+
refreshBtn.addEventListener('click', async (e) => {
|
|
1259
|
+
e.stopPropagation();
|
|
1260
|
+
loaded = false;
|
|
1261
|
+
render(container);
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const createBtn = container.querySelector('#createAgentBtn');
|
|
1266
|
+
if (createBtn) {
|
|
1267
|
+
createBtn.addEventListener('click', () => {
|
|
1268
|
+
currentView = 'create';
|
|
1269
|
+
render(container);
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
function bindDetailEvents(container) {
|
|
1275
|
+
// Back button
|
|
1276
|
+
const backBtn = container.querySelector('.agent-detail-back');
|
|
1277
|
+
if (backBtn) {
|
|
1278
|
+
backBtn.addEventListener('click', () => {
|
|
1279
|
+
currentView = 'list';
|
|
1280
|
+
selectedAgentId = null;
|
|
1281
|
+
editingSection = null;
|
|
1282
|
+
pendingChanges = {};
|
|
1283
|
+
render(container);
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// Delete button
|
|
1288
|
+
const deleteBtn = container.querySelector('.agent-detail-delete');
|
|
1289
|
+
if (deleteBtn) {
|
|
1290
|
+
deleteBtn.addEventListener('click', () => {
|
|
1291
|
+
showDeleteConfirm(selectedAgentId, container);
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Hero image load/error handling (no inline handlers — CSP-safe)
|
|
1296
|
+
const heroImg = container.querySelector('.agent-detail-hero-img');
|
|
1297
|
+
const heroFallback = container.querySelector('.agent-detail-hero-fallback');
|
|
1298
|
+
if (heroImg && heroFallback) {
|
|
1299
|
+
heroImg.addEventListener('load', () => {
|
|
1300
|
+
heroImg.style.display = 'block';
|
|
1301
|
+
heroFallback.style.display = 'none';
|
|
1302
|
+
});
|
|
1303
|
+
heroImg.addEventListener('error', () => {
|
|
1304
|
+
heroImg.style.display = 'none';
|
|
1305
|
+
heroFallback.style.display = 'flex';
|
|
1306
|
+
});
|
|
1307
|
+
// If already loaded/errored (cached), handle immediately
|
|
1308
|
+
if (heroImg.complete) {
|
|
1309
|
+
if (heroImg.naturalWidth > 0) {
|
|
1310
|
+
heroImg.style.display = 'block';
|
|
1311
|
+
heroFallback.style.display = 'none';
|
|
1312
|
+
} else {
|
|
1313
|
+
heroImg.style.display = 'none';
|
|
1314
|
+
heroFallback.style.display = 'flex';
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
// Avatar upload — hero banner (detail view) or circular widget (list view)
|
|
1320
|
+
const avatarUpload = container.querySelector('.agent-detail-hero') || container.querySelector('.agent-avatar-upload');
|
|
1321
|
+
if (avatarUpload) {
|
|
1322
|
+
const fileInput = avatarUpload.querySelector('.agent-avatar-input');
|
|
1323
|
+
avatarUpload.addEventListener('click', (e) => {
|
|
1324
|
+
// Don't trigger file upload when clicking Back/Delete buttons
|
|
1325
|
+
if (e.target.closest('.agent-detail-back') || e.target.closest('.agent-detail-delete')) return;
|
|
1326
|
+
if (e.target !== fileInput) fileInput.click();
|
|
1327
|
+
});
|
|
1328
|
+
fileInput.addEventListener('change', async () => {
|
|
1329
|
+
const file = fileInput.files[0];
|
|
1330
|
+
if (!file) return;
|
|
1331
|
+
const agentId = avatarUpload.dataset.agentId;
|
|
1332
|
+
try {
|
|
1333
|
+
await AgentsData.uploadAgentAvatar(agentId, file);
|
|
1334
|
+
// Refresh the preview — handle both hero and circular layouts
|
|
1335
|
+
const preview = avatarUpload.querySelector('.agent-detail-hero-img') || avatarUpload.querySelector('.agent-avatar-preview');
|
|
1336
|
+
const fallback = avatarUpload.querySelector('.agent-detail-hero-fallback') || avatarUpload.querySelector('.agent-avatar-fallback');
|
|
1337
|
+
if (preview) {
|
|
1338
|
+
preview.src = `/img/agents/${agentId}.png?t=${Date.now()}`;
|
|
1339
|
+
preview.style.display = 'block';
|
|
1340
|
+
}
|
|
1341
|
+
if (fallback) fallback.style.display = 'none';
|
|
1342
|
+
showToast('Avatar updated', 'success');
|
|
1343
|
+
} catch (err) {
|
|
1344
|
+
showToast('Failed to upload avatar', 'error');
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Edit / Cancel / Save buttons
|
|
1350
|
+
container.querySelectorAll('.agent-section-btn').forEach(btn => {
|
|
1351
|
+
btn.addEventListener('click', async (e) => {
|
|
1352
|
+
e.stopPropagation();
|
|
1353
|
+
const action = btn.dataset.action;
|
|
1354
|
+
const sectionId = btn.dataset.section;
|
|
1355
|
+
|
|
1356
|
+
if (action === 'edit') {
|
|
1357
|
+
editingSection = sectionId;
|
|
1358
|
+
render(container);
|
|
1359
|
+
} else if (action === 'cancel') {
|
|
1360
|
+
editingSection = null;
|
|
1361
|
+
pendingChanges = {};
|
|
1362
|
+
render(container);
|
|
1363
|
+
} else if (action === 'save') {
|
|
1364
|
+
let ok = false;
|
|
1365
|
+
if (sectionId === 'routing') {
|
|
1366
|
+
// Routing uses a separate save path (PUT /api/bindings)
|
|
1367
|
+
ok = await saveBindings(container);
|
|
1368
|
+
} else {
|
|
1369
|
+
const changes = collectSectionChanges(container, sectionId);
|
|
1370
|
+
if (changes && selectedAgentId) {
|
|
1371
|
+
ok = await saveAgentChanges(selectedAgentId, changes);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (ok) {
|
|
1375
|
+
editingSection = null;
|
|
1376
|
+
pendingChanges = {};
|
|
1377
|
+
render(container);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
});
|
|
1381
|
+
});
|
|
1382
|
+
|
|
1383
|
+
// Tools mode toggle — show/hide grid
|
|
1384
|
+
const toolsModeSelect = container.querySelector('#toolsModeSelect');
|
|
1385
|
+
if (toolsModeSelect) {
|
|
1386
|
+
toolsModeSelect.addEventListener('change', () => {
|
|
1387
|
+
const grid = container.querySelector('#toolsGrid');
|
|
1388
|
+
if (grid) {
|
|
1389
|
+
grid.style.display = toolsModeSelect.value === 'full' ? 'none' : '';
|
|
1390
|
+
}
|
|
1391
|
+
});
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Routing: Add binding button
|
|
1395
|
+
const addBindingBtn = container.querySelector('#addBindingBtn');
|
|
1396
|
+
if (addBindingBtn) {
|
|
1397
|
+
addBindingBtn.addEventListener('click', () => {
|
|
1398
|
+
const list = container.querySelector('#bindingsList');
|
|
1399
|
+
if (!list) return;
|
|
1400
|
+
|
|
1401
|
+
// Remove empty message if present
|
|
1402
|
+
const emptyMsg = list.querySelector('.agent-bindings-empty');
|
|
1403
|
+
if (emptyMsg) emptyMsg.remove();
|
|
1404
|
+
|
|
1405
|
+
const index = list.querySelectorAll('.agent-binding-row').length;
|
|
1406
|
+
const channelOpts = channels.map(ch =>
|
|
1407
|
+
`<option value="${escapeAttr(ch)}">${escapeHtml(ch)}</option>`
|
|
1408
|
+
).join('');
|
|
1409
|
+
|
|
1410
|
+
const newBinding = { agentId: selectedAgentId, match: {} };
|
|
1411
|
+
const rowHtml = renderBindingRow(newBinding, index, channelOpts);
|
|
1412
|
+
list.insertAdjacentHTML('beforeend', rowHtml);
|
|
1413
|
+
|
|
1414
|
+
// Bind events for the new row
|
|
1415
|
+
bindBindingRowEvents(container, list.querySelector(`[data-binding-index="${index}"]`));
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Routing: Bind events for existing rows
|
|
1420
|
+
container.querySelectorAll('.agent-binding-row').forEach(row => {
|
|
1421
|
+
bindBindingRowEvents(container, row);
|
|
1422
|
+
});
|
|
1423
|
+
|
|
1424
|
+
// Raw JSON toggle
|
|
1425
|
+
const rawToggle = container.querySelector('.agent-raw-toggle');
|
|
1426
|
+
if (rawToggle) {
|
|
1427
|
+
rawToggle.addEventListener('click', () => {
|
|
1428
|
+
const body = rawToggle.closest('.agent-section').querySelector('.agent-raw-body');
|
|
1429
|
+
const chevron = rawToggle.querySelector('.agent-raw-chevron');
|
|
1430
|
+
if (body) {
|
|
1431
|
+
const isHidden = body.style.display === 'none';
|
|
1432
|
+
body.style.display = isHidden ? '' : 'none';
|
|
1433
|
+
if (chevron) chevron.textContent = isHidden ? '▾' : '▸';
|
|
1434
|
+
}
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
// Raw JSON copy
|
|
1439
|
+
const rawCopy = container.querySelector('.agent-raw-copy');
|
|
1440
|
+
if (rawCopy) {
|
|
1441
|
+
rawCopy.addEventListener('click', () => {
|
|
1442
|
+
const json = container.querySelector('.agent-raw-json')?.textContent;
|
|
1443
|
+
if (json) {
|
|
1444
|
+
navigator.clipboard.writeText(json).then(() => showToast('Copied to clipboard', 'success'));
|
|
1445
|
+
}
|
|
1446
|
+
});
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Tool preset buttons
|
|
1450
|
+
container.querySelectorAll('.agent-preset-btn').forEach(btn => {
|
|
1451
|
+
btn.addEventListener('click', () => {
|
|
1452
|
+
const preset = TOOL_PRESETS[btn.dataset.preset];
|
|
1453
|
+
if (!preset) return;
|
|
1454
|
+
|
|
1455
|
+
const modeSelect = container.querySelector('#toolsModeSelect');
|
|
1456
|
+
const grid = container.querySelector('#toolsGrid');
|
|
1457
|
+
|
|
1458
|
+
if (modeSelect) {
|
|
1459
|
+
modeSelect.value = preset.mode;
|
|
1460
|
+
if (grid) grid.style.display = preset.mode === 'full' ? 'none' : '';
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
// Uncheck everything first
|
|
1464
|
+
container.querySelectorAll('.agent-tool-group-check, .agent-tool-item-check').forEach(cb => { cb.checked = false; });
|
|
1465
|
+
|
|
1466
|
+
// Check the preset items
|
|
1467
|
+
preset.list.forEach(item => {
|
|
1468
|
+
const groupCb = container.querySelector(`.agent-tool-group-check[data-group="${item}"]`);
|
|
1469
|
+
if (groupCb) {
|
|
1470
|
+
groupCb.checked = true;
|
|
1471
|
+
// Also check all items in that group
|
|
1472
|
+
const groupId = groupCb.dataset.group;
|
|
1473
|
+
container.querySelectorAll(`.agent-tool-item-check[data-group-id="${groupId}"]`).forEach(cb => { cb.checked = true; });
|
|
1474
|
+
} else {
|
|
1475
|
+
const toolCb = container.querySelector(`.agent-tool-item-check[data-tool="${item}"]`);
|
|
1476
|
+
if (toolCb) toolCb.checked = true;
|
|
1477
|
+
}
|
|
1478
|
+
});
|
|
1479
|
+
|
|
1480
|
+
// Highlight active preset
|
|
1481
|
+
container.querySelectorAll('.agent-preset-btn').forEach(b => b.classList.remove('active'));
|
|
1482
|
+
btn.classList.add('active');
|
|
1483
|
+
});
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
// Tool group checkboxes — toggle all items in group
|
|
1487
|
+
container.querySelectorAll('.agent-tool-group-check').forEach(groupCheck => {
|
|
1488
|
+
groupCheck.addEventListener('change', () => {
|
|
1489
|
+
const groupId = groupCheck.dataset.group;
|
|
1490
|
+
const items = container.querySelectorAll(`.agent-tool-item-check[data-group-id="${groupId}"]`);
|
|
1491
|
+
items.forEach(item => { item.checked = groupCheck.checked; });
|
|
1492
|
+
});
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// ============================================
|
|
1497
|
+
// PUBLIC API
|
|
1498
|
+
// ============================================
|
|
1499
|
+
|
|
1500
|
+
function resetView() {
|
|
1501
|
+
currentView = 'list';
|
|
1502
|
+
selectedAgentId = null;
|
|
1503
|
+
editingSection = null;
|
|
1504
|
+
pendingChanges = {};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
function refresh() {
|
|
1508
|
+
loaded = false;
|
|
1509
|
+
loading = false;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
export const UplinkAgents = {
|
|
1513
|
+
render,
|
|
1514
|
+
resetView,
|
|
1515
|
+
refresh,
|
|
1516
|
+
fetchAgents,
|
|
1517
|
+
getAgents: () => agents,
|
|
1518
|
+
getDefaults: () => defaults,
|
|
1519
|
+
isLoaded: () => loaded,
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
// Backward compat: assign to window
|
|
1523
|
+
window.UplinkAgents = UplinkAgents;
|
|
1524
|
+
|
|
1525
|
+
logger.debug('Agents: Module loaded');
|