claudity 1.0.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.
@@ -0,0 +1,1240 @@
1
+ let agents = [];
2
+ let activeAgent = null;
3
+ let eventSource = null;
4
+ let connectionsPoller = null;
5
+
6
+ async function api(method, path, body) {
7
+ const opts = { method, headers: { 'content-type': 'application/json' } };
8
+ if (body) opts.body = JSON.stringify(body);
9
+ const res = await fetch(path, opts);
10
+ if (!res.ok) {
11
+ const err = await res.json().catch(() => ({ error: res.statusText }));
12
+ throw new Error(err.error || res.statusText);
13
+ }
14
+ return res.json();
15
+ }
16
+
17
+ function $(sel, ctx = document) { return ctx.querySelector(sel); }
18
+ function $$(sel, ctx = document) { return [...ctx.querySelectorAll(sel)]; }
19
+
20
+ const setupSection = $('section[aria-label="setup"]');
21
+ const emptySection = $('section[aria-label="empty"]');
22
+ const chatSection = $('section[aria-label="chat"]');
23
+ const connectionsSection = $('section[aria-label="connections"]');
24
+ const agentList = $('nav[aria-label="agents"] ul');
25
+ const createDialog = $('dialog[aria-label="create agent"]');
26
+ const editDialog = $('dialog[aria-label="edit agent"]');
27
+ const filesDialog = $('dialog[aria-label="agent records"]');
28
+ const fileList = $('[data-file-list]');
29
+ const fileEditor = $('[data-file-editor]');
30
+ const fileListFooter = $('[data-list-footer]');
31
+ const messagesDiv = $('div[aria-label="messages"]');
32
+ const chatForm = $('form[aria-label="input"]');
33
+ const chatInput = $('form[aria-label="input"] textarea');
34
+ const chatSubmit = $('form[aria-label="input"] button[type="submit"]');
35
+ const connectionsBtn = $('button[data-action="connections"]');
36
+ const platformsDiv = $('[data-platforms]');
37
+
38
+ const iconRobot = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512"><path fill="currentColor" d="M352 0c0-17.7-14.3-32-32-32S288-17.7 288 0l0 64-96 0c-53 0-96 43-96 96l0 224c0 53 43 96 96 96l256 0c53 0 96-43 96-96l0-224c0-53-43-96-96-96l-96 0 0-64zM160 368c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zm120 0c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zm120 0c0-13.3 10.7-24 24-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24zM224 176a48 48 0 1 1 0 96 48 48 0 1 1 0-96zm144 48a48 48 0 1 1 96 0 48 48 0 1 1 -96 0zM64 224c0-17.7-14.3-32-32-32S0 206.3 0 224l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96zm544-32c-17.7 0-32 14.3-32 32l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96c0-17.7-14.3-32-32-32z"/></svg>';
39
+
40
+ const platforms = [
41
+ {
42
+ id: 'discord',
43
+ name: 'discord',
44
+ description: 'talk to agents via dm or @mention',
45
+ icon: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M492.5 69.8c-.2-.3-.4-.6-.8-.7-38.1-17.5-78.4-30-119.7-37.1-.4-.1-.8 0-1.1 .1s-.6 .4-.8 .8c-5.5 9.9-10.5 20.2-14.9 30.6-44.6-6.8-89.9-6.8-134.4 0-4.5-10.5-9.5-20.7-15.1-30.6-.2-.3-.5-.6-.8-.8s-.7-.2-1.1-.2c-41.3 7.1-81.6 19.6-119.7 37.1-.3 .1-.6 .4-.8 .7-76.2 113.8-97.1 224.9-86.9 334.5 0 .3 .1 .5 .2 .8s.3 .4 .5 .6c44.4 32.9 94 58 146.8 74.2 .4 .1 .8 .1 1.1 0s.7-.4 .9-.7c11.3-15.4 21.4-31.8 30-48.8 .1-.2 .2-.5 .2-.8s0-.5-.1-.8-.2-.5-.4-.6-.4-.3-.7-.4c-15.8-6.1-31.2-13.4-45.9-21.9-.3-.2-.5-.4-.7-.6s-.3-.6-.3-.9 0-.6 .2-.9 .3-.5 .6-.7c3.1-2.3 6.2-4.7 9.1-7.1 .3-.2 .6-.4 .9-.4s.7 0 1 .1c96.2 43.9 200.4 43.9 295.5 0 .3-.1 .7-.2 1-.2s.7 .2 .9 .4c2.9 2.4 6 4.9 9.1 7.2 .2 .2 .4 .4 .6 .7s.2 .6 .2 .9-.1 .6-.3 .9-.4 .5-.6 .6c-14.7 8.6-30 15.9-45.9 21.8-.2 .1-.5 .2-.7 .4s-.3 .4-.4 .7-.1 .5-.1 .8 .1 .5 .2 .8c8.8 17 18.8 33.3 30 48.8 .2 .3 .6 .6 .9 .7s.8 .1 1.1 0c52.9-16.2 102.6-41.3 147.1-74.2 .2-.2 .4-.4 .5-.6s.2-.5 .2-.8c12.3-126.8-20.5-236.9-86.9-334.5zm-302 267.7c-29 0-52.8-26.6-52.8-59.2s23.4-59.2 52.8-59.2c29.7 0 53.3 26.8 52.8 59.2 0 32.7-23.4 59.2-52.8 59.2zm195.4 0c-29 0-52.8-26.6-52.8-59.2s23.4-59.2 52.8-59.2c29.7 0 53.3 26.8 52.8 59.2 0 32.7-23.2 59.2-52.8 59.2z"/></svg>',
46
+ fields: [
47
+ { key: 'token', label: 'bot token', placeholder: 'from developer portal → bot → reset token' },
48
+ { key: 'app_id', label: 'application id', placeholder: 'from developer portal → general information' }
49
+ ],
50
+ setup: [
51
+ 'go to <a href="https://discord.com/developers/applications" target="_blank" rel="noopener">discord developer portal</a> and create a new application',
52
+ 'go to bot → reset token, copy it and paste below',
53
+ 'enable message content intent under bot → privileged gateway intents',
54
+ 'copy the application id from general information',
55
+ 'after connecting, use the invite link to add the bot to your server'
56
+ ],
57
+ usage: 'dm the bot or @mention it: <code>agent_name: your message</code>'
58
+ },
59
+ {
60
+ id: 'imessage',
61
+ name: 'imessage',
62
+ description: 'talk to agents by texting yourself',
63
+ icon: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 134"><path fill="currentColor" d="M75 0C33.579 0 0 27.903 0 62.322C0.037 84.19 13.865 104.442 36.436 115.687C33.481 122.288 29.048 128.477 23.322 134C34.426 132.055 44.85 127.97 53.782 122.062C60.67 123.762 67.815 124.632 75 124.643C116.421 124.643 150 96.741 150 62.322C150 27.903 116.421 0 75 0Z"/></svg>',
64
+ fields: [
65
+ { key: 'phone', label: 'your phone number', placeholder: '+1234567890' }
66
+ ],
67
+ setup: [
68
+ 'enter your phone number (the one registered with imessage)',
69
+ 'claudity polls your self-chat for new messages',
70
+ 'text yourself: <code>agent_name: your message</code>'
71
+ ],
72
+ usage: 'text yourself: <code>agent_name: your message</code>'
73
+ },
74
+ {
75
+ id: 'signal',
76
+ name: 'signal',
77
+ description: 'encrypted messaging via signal',
78
+ icon: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 0c-13.3 0-26.3 1-39.1 3l3.7 23.7C232.1 24.9 244 24 256 24s23.9 .9 35.4 2.7L295.1 3C282.3 1 269.3 0 256 0zm60.8 7.3l-5.7 23.3c23.4 5.7 45.4 14.9 65.4 27.1l12.5-20.5c-22.1-13.4-46.4-23.6-72.2-29.9zm76.4 61.6c19.1 14 36 30.9 50.1 50.1l19.4-14.2C447 83.6 428.4 65 407.3 49.5L393.1 68.8zm81.7 54.2l-20.5 12.5c12.2 20 21.4 42 27.1 65.4l23.3-5.7c-6.3-25.8-16.5-50.1-29.9-72.2zm10.5 97.5c1.8 11.5 2.7 23.4 2.7 35.4s-.9 23.9-2.7 35.4l23.7 3.7c1.9-12.7 3-25.8 3-39.1s-1-26.3-3-39.1l-23.7 3.7zm-31 155.9l20.5 12.5c13.4-22.1 23.6-46.4 29.9-72.2l-23.3-5.7c-5.7 23.4-14.9 45.4-27.1 65.4zm8.2 30.8l-19.4-14.2c-14 19.1-30.9 36-50.1 50.1l14.2 19.4c21.1-15.5 39.8-34.1 55.2-55.2zm-86.1 47c-20 12.2-42 21.4-65.4 27.1l5.7 23.3c25.8-6.3 50.1-16.5 72.2-29.9l-12.5-20.5zM295.1 509l-3.7-23.7C279.9 487.1 268 488 256 488s-23.9-.9-35.4-2.7L216.9 509c12.7 1.9 25.8 3 39.1 3s26.3-1 39.1-3zm-94.1-27.6c-17.6-4.3-34.4-10.6-50.1-18.6l-7.8-4-32.8 7.7 5.5 23.4 24.3-5.7c17.4 8.9 35.9 15.8 55.3 20.5l5.7-23.3zM95.4 494.6L90 471.3 48.3 481c-10.4 2.4-19.7-6.9-17.3-17.3l9.7-41.6-23.4-5.5-9.7 41.6C1.2 486 26 510.8 53.8 504.4l41.6-9.7zm-50-92.9l7.7-32.8-4-7.8c-8-15.7-14.3-32.5-18.6-50.1L7.3 316.7C12 336.1 18.9 354.7 27.7 372l-5.7 24.3 23.4 5.5zM3 295.1l23.7-3.7C24.9 279.9 24 268 24 256s.9-23.9 2.7-35.4L3 216.9C1 229.7 0 242.7 0 256s1 26.3 3 39.1zm27.6-94.1c5.7-23.4 14.9-45.4 27.1-65.4L37.2 123.1c-13.4 22.1-23.6 46.4-29.9 72.2l23.3 5.7zm18.9-96.2l19.4 14.2c14-19.1 30.9-36 50.1-50.1L104.7 49.5C83.6 65 65 83.6 49.5 104.7zm86.1-47c20-12.2 42-21.4 65.4-27.1L195.2 7.3c-25.8 6.3-50.1 16.5-72.2 29.9l12.5 20.5zM256 464c114.9 0 208-93.1 208-208S370.9 48 256 48 48 141.1 48 256c0 36.4 9.4 70.7 25.8 100.5 1.6 2.9 2.1 6.2 1.4 9.4l-21.6 92.5 92.5-21.6c3.2-.7 6.5-.2 9.4 1.4 29.8 16.5 64 25.8 100.5 25.8z"/></svg>',
79
+ qrAuth: true,
80
+ fields: [],
81
+ setup: [
82
+ 'requires <code>signal-cli</code> — install with <code>brew install signal-cli</code>',
83
+ 'click connect below — a qr code will appear',
84
+ 'open signal on your phone → settings → linked devices → link new device',
85
+ 'scan the qr code with your phone camera',
86
+ 'after linking, reconnects automatically on restart'
87
+ ],
88
+ usage: 'message your linked number: <code>agent_name: your message</code>'
89
+ },
90
+ {
91
+ id: 'slack',
92
+ name: 'slack',
93
+ description: 'workspace bot via socket mode',
94
+ icon: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M94.1 315.1c0 25.9-21.2 47.1-47.1 47.1S0 341 0 315.1 21.2 268 47.1 268l47.1 0 0 47.1zm23.7 0c0-25.9 21.2-47.1 47.1-47.1S212 289.2 212 315.1l0 117.8c0 25.9-21.2 47.1-47.1 47.1s-47.1-21.2-47.1-47.1l0-117.8zm47.1-189c-25.9 0-47.1-21.2-47.1-47.1S139 32 164.9 32 212 53.2 212 79.1l0 47.1-47.1 0zm0 23.7c25.9 0 47.1 21.2 47.1 47.1S190.8 244 164.9 244L47.1 244C21.2 244 0 222.8 0 196.9s21.2-47.1 47.1-47.1l117.8 0zm189 47.1c0-25.9 21.2-47.1 47.1-47.1S448 171 448 196.9 426.8 244 400.9 244l-47.1 0 0-47.1zm-23.7 0c0 25.9-21.2 47.1-47.1 47.1S236 222.8 236 196.9l0-117.8C236 53.2 257.2 32 283.1 32s47.1 21.2 47.1 47.1l0 117.8zm-47.1 189c25.9 0 47.1 21.2 47.1 47.1S309 480 283.1 480 236 458.8 236 432.9l0-47.1 47.1 0zm0-23.7c-25.9 0-47.1-21.2-47.1-47.1S257.2 268 283.1 268l117.8 0c25.9 0 47.1 21.2 47.1 47.1s-21.2 47.1-47.1 47.1l-117.8 0z"/></svg>',
95
+ fields: [
96
+ { key: 'bot_token', label: 'bot token', placeholder: 'xoxb-...', type: 'password' },
97
+ { key: 'app_token', label: 'app-level token', placeholder: 'xapp-...', type: 'password' }
98
+ ],
99
+ setup: [
100
+ 'go to <a href="https://api.slack.com/apps" target="_blank" rel="noopener">api.slack.com/apps</a> and create a new app from scratch',
101
+ 'under oauth & permissions, add bot scopes: <code>chat:write</code> <code>app_mentions:read</code> <code>im:history</code> <code>im:read</code> <code>im:write</code>',
102
+ 'under socket mode, enable it and generate an app-level token with <code>connections:write</code> scope',
103
+ 'under event subscriptions, subscribe to <code>message.im</code> and <code>app_mention</code>',
104
+ 'under app home, enable the <b>messages tab</b> and check "allow users to send slash commands and messages from the messages tab"',
105
+ 'install the app to your workspace and copy the bot token (xoxb-)'
106
+ ],
107
+ usage: 'dm the bot or @mention it: <code>agent_name: your message</code>'
108
+ },
109
+ {
110
+ id: 'telegram',
111
+ name: 'telegram',
112
+ description: 'bot via long polling',
113
+ icon: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M256 8a248 248 0 1 0 0 496 248 248 0 1 0 0-496zM371 176.7c-3.7 39.2-19.9 134.4-28.1 178.3-3.5 18.6-10.3 24.8-16.9 25.4-14.4 1.3-25.3-9.5-39.3-18.7-21.8-14.3-34.2-23.2-55.3-37.2-24.5-16.1-8.6-25 5.3-39.5 3.7-3.8 67.1-61.5 68.3-66.7 .2-.7 .3-3.1-1.2-4.4s-3.6-.8-5.1-.5c-2.2 .5-37.1 23.5-104.6 69.1-9.9 6.8-18.9 10.1-26.9 9.9-8.9-.2-25.9-5-38.6-9.1-15.5-5-27.9-7.7-26.8-16.3 .6-4.5 6.7-9 18.4-13.7 72.3-31.5 120.5-52.3 144.6-62.3 68.9-28.6 83.2-33.6 92.5-33.8 2.1 0 6.6 .5 9.6 2.9 2 1.7 3.2 4.1 3.5 6.7 .5 3.2 .6 6.5 .4 9.8z"/></svg>',
114
+ fields: [
115
+ { key: 'token', label: 'bot token', placeholder: 'from @botfather', type: 'password' }
116
+ ],
117
+ setup: [
118
+ 'message <a href="https://t.me/BotFather" target="_blank" rel="noopener">@botfather</a> on telegram',
119
+ 'send /newbot and follow the prompts to create a bot',
120
+ 'copy the token botfather gives you and paste below'
121
+ ],
122
+ usage: 'message the bot: <code>agent_name: your message</code>'
123
+ },
124
+ {
125
+ id: 'whatsapp',
126
+ name: 'whatsapp',
127
+ description: 'talk to agents via whatsapp messages',
128
+ icon: '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M380.9 97.1c-41.9-42-97.7-65.1-157-65.1-122.4 0-222 99.6-222 222 0 39.1 10.2 77.3 29.6 111L0 480 117.7 449.1c32.4 17.7 68.9 27 106.1 27l.1 0c122.3 0 224.1-99.6 224.1-222 0-59.3-25.2-115-67.1-157zm-157 341.6c-33.2 0-65.7-8.9-94-25.7l-6.7-4-69.8 18.3 18.6-68.1-4.4-7c-18.5-29.4-28.2-63.3-28.2-98.2 0-101.7 82.8-184.5 184.6-184.5 49.3 0 95.6 19.2 130.4 54.1s56.2 81.2 56.1 130.5c0 101.8-84.9 184.6-186.6 184.6zM325.1 300.5c-5.5-2.8-32.8-16.2-37.9-18-5.1-1.9-8.8-2.8-12.5 2.8s-14.3 18-17.6 21.8c-3.2 3.7-6.5 4.2-12 1.4-32.6-16.3-54-29.1-75.5-66-5.7-9.8 5.7-9.1 16.3-30.3 1.8-3.7 .9-6.9-.5-9.7s-12.5-30.1-17.1-41.2c-4.5-10.8-9.1-9.3-12.5-9.5-3.2-.2-6.9-.2-10.6-.2s-9.7 1.4-14.8 6.9c-5.1 5.6-19.4 19-19.4 46.3s19.9 53.7 22.6 57.4c2.8 3.7 39.1 59.7 94.8 83.8 35.2 15.2 49 16.5 66.6 13.9 10.7-1.6 32.8-13.4 37.4-26.4s4.6-24.1 3.2-26.4c-1.3-2.5-5-3.9-10.5-6.6z"/></svg>',
129
+ fields: [],
130
+ qrAuth: true,
131
+ setup: [
132
+ 'click connect below — a qr code will appear',
133
+ 'open whatsapp on your phone → linked devices → link a device',
134
+ 'scan the qr code with your phone camera',
135
+ 'after first scan, reconnects automatically on restart'
136
+ ],
137
+ usage: 'message the linked number: <code>agent_name: your message</code>'
138
+ }
139
+ ];
140
+
141
+ function esc(str) {
142
+ const el = document.createElement('span');
143
+ el.textContent = str;
144
+ return el.innerHTML;
145
+ }
146
+
147
+ function renderMarkdown(text) {
148
+ let html = esc(text);
149
+ html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
150
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
151
+ html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
152
+ html = html.replace(/(?<!\w)\*([^*]+)\*(?!\w)/g, '<em>$1</em>');
153
+ html = html.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
154
+ html = html.replace(/(<a[^>]*>[\s\S]*?<\/a>)|(https?:\/\/[^\s<)]+)/g, (m, anchor, url) => {
155
+ if (anchor) return anchor;
156
+ return '<a href="' + url + '" target="_blank" rel="noopener">' + url + '</a>';
157
+ });
158
+ html = html.replace(/(^|\n)([-*]) (.+)/g, '$1<li>$3</li>');
159
+ html = html.replace(/(^|\n)(\d+)\. (.+)/g, '$1<li>$3</li>');
160
+ html = html.replace(/(<li>[\s\S]*?<\/li>)/g, '<ul>$1</ul>');
161
+ html = html.replace(/<\/ul>\s*<ul>/g, '');
162
+ html = html.replace(/\n{2,}/g, '</p><p>');
163
+ html = html.replace(/\n/g, '<br>');
164
+ return `<p>${html}</p>`;
165
+ }
166
+
167
+ function showSection(name) {
168
+ setupSection.hidden = name !== 'setup';
169
+ emptySection.hidden = name !== 'empty';
170
+ chatSection.hidden = name !== 'chat';
171
+ connectionsSection.hidden = name !== 'connections';
172
+
173
+ connectionsBtn.setAttribute('aria-selected', name === 'connections');
174
+ document.body.dataset.screen = name;
175
+
176
+ if (name === 'connections') {
177
+ startConnectionsPolling();
178
+ } else {
179
+ stopConnectionsPolling();
180
+ }
181
+ }
182
+
183
+ function bubbleAgent(id) {
184
+ const idx = agents.findIndex(a => a.id === id);
185
+ if (idx > 0) {
186
+ agents.unshift(agents.splice(idx, 1)[0]);
187
+ renderAgents();
188
+ }
189
+ }
190
+
191
+ async function refreshAgentOrder() {
192
+ try {
193
+ const fresh = await api('GET', '/api/agents');
194
+ const order = fresh.map(a => a.id);
195
+ const current = agents.map(a => a.id);
196
+ if (order.join() !== current.join()) {
197
+ for (const f of fresh) {
198
+ const existing = agents.find(a => a.id === f.id);
199
+ if (existing) Object.assign(existing, f);
200
+ }
201
+ agents.sort((a, b) => order.indexOf(a.id) - order.indexOf(b.id));
202
+ renderAgents();
203
+ }
204
+ } catch {}
205
+ }
206
+
207
+ setInterval(refreshAgentOrder, 10000);
208
+
209
+ function renderAgents() {
210
+ agentList.innerHTML = agents.map(a => `
211
+ <li data-id="${a.id}" ${activeAgent && activeAgent.id === a.id ? 'aria-selected="true"' : 'aria-selected="false"'} title="${esc(a.name)}${a.is_default ? ' (default)' : ''}">
212
+ ${iconRobot}
213
+ <span>${esc(a.name)}</span>
214
+ ${a.is_default ? '<i data-default aria-label="default agent"></i>' : ''}
215
+ </li>
216
+ `).join('');
217
+
218
+ agentList.querySelectorAll('li').forEach(li => {
219
+ li.setAttribute('role', 'option');
220
+ li.setAttribute('tabindex', '0');
221
+ li.addEventListener('click', () => { selectAgent(li.dataset.id); closeNav(); });
222
+ li.addEventListener('keydown', (e) => {
223
+ if (e.key === 'Enter' || e.key === ' ') {
224
+ e.preventDefault();
225
+ selectAgent(li.dataset.id);
226
+ closeNav();
227
+ }
228
+ });
229
+ });
230
+ }
231
+
232
+ async function selectAgent(id) {
233
+ activeAgent = agents.find(a => a.id === id);
234
+ if (eventSource) { eventSource.close(); eventSource = null; }
235
+
236
+ renderAgents();
237
+
238
+ if (!activeAgent) {
239
+ showSection('empty');
240
+ return;
241
+ }
242
+
243
+ showSection('chat');
244
+ $('section[aria-label="chat"] > header h2').textContent = activeAgent.name;
245
+
246
+ messagesDiv.innerHTML = '';
247
+ chatInput.value = '';
248
+ chatSubmit.disabled = false;
249
+
250
+
251
+ try {
252
+ const messages = await api('GET', `/api/agents/${id}/messages`);
253
+ for (const msg of messages) {
254
+ if (msg.type === 'heartbeat') {
255
+ if (!activeAgent.show_heartbeat) continue;
256
+ const div = document.createElement('div');
257
+ div.dataset.role = 'heartbeat';
258
+ const body = document.createElement('div');
259
+ body.className = 'msg-body';
260
+ body.innerHTML = renderMarkdown(cleanToolLeaks(msg.content));
261
+ div.appendChild(body);
262
+ messagesDiv.appendChild(div);
263
+ } else {
264
+ appendMessage(msg.role, msg.content, msg.tool_calls ? JSON.parse(msg.tool_calls) : null);
265
+ }
266
+ }
267
+ scrollToBottom();
268
+ } catch {}
269
+
270
+ connectSSE(id);
271
+ }
272
+
273
+ function cleanToolLeaks(text) {
274
+ return text.replace(/\n*\[used \w+:[\s\S]*$/, '').trim();
275
+ }
276
+
277
+ function appendMessage(role, content, toolCalls, intermediate) {
278
+ const div = document.createElement('div');
279
+ div.dataset.role = role;
280
+
281
+ if (role === 'assistant') {
282
+ const cleaned = cleanToolLeaks(content);
283
+ if (cleaned) {
284
+ const textEl = document.createElement('div');
285
+ textEl.className = 'msg-body';
286
+ textEl.innerHTML = renderMarkdown(cleaned);
287
+ div.appendChild(textEl);
288
+ }
289
+ } else {
290
+ const textEl = document.createElement('p');
291
+ textEl.textContent = content;
292
+ div.appendChild(textEl);
293
+ }
294
+
295
+ if (toolCalls && toolCalls.length) {
296
+ const details = document.createElement('details');
297
+ const summary = document.createElement('summary');
298
+ summary.textContent = `${toolCalls.length} tool ${toolCalls.length === 1 ? 'call' : 'calls'}`;
299
+ details.appendChild(summary);
300
+ for (const tc of toolCalls) {
301
+ const pre = document.createElement('pre');
302
+ const inputStr = JSON.stringify(tc.input, null, 2);
303
+ let outputStr = typeof tc.output === 'string' ? tc.output : JSON.stringify(tc.output, null, 2);
304
+ if (outputStr.length > 500) outputStr = outputStr.slice(0, 500) + '\n...';
305
+ pre.textContent = `${tc.name}(${inputStr})\n→ ${outputStr}`;
306
+ details.appendChild(pre);
307
+ }
308
+ div.appendChild(details);
309
+ }
310
+
311
+ messagesDiv.appendChild(div);
312
+ return div;
313
+ }
314
+
315
+ function scrollToBottom() {
316
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
317
+ }
318
+
319
+ function showActivity() {
320
+ let el = messagesDiv.querySelector('[data-activity]');
321
+ if (el) return el;
322
+ el = document.createElement('div');
323
+ el.dataset.activity = '';
324
+ el.dataset.role = 'assistant';
325
+ el.setAttribute('role', 'status');
326
+ el.setAttribute('aria-label', 'agent is thinking');
327
+ el.innerHTML = '<p data-dots><span></span><span></span><span></span></p>';
328
+ messagesDiv.appendChild(el);
329
+ scrollToBottom();
330
+ return el;
331
+ }
332
+
333
+ function clearActivity() {
334
+ const el = messagesDiv.querySelector('[data-activity]');
335
+ if (el) el.remove();
336
+ }
337
+
338
+ function updateThinking(content) {
339
+ const activity = showActivity();
340
+ let thinking = activity.querySelector('[data-thinking]');
341
+ if (!thinking) {
342
+ thinking = document.createElement('p');
343
+ thinking.dataset.thinking = '';
344
+ activity.prepend(thinking);
345
+ }
346
+ thinking.textContent = content;
347
+ const dots = activity.querySelector('[data-dots]');
348
+ if (dots) dots.remove();
349
+ scrollToBottom();
350
+ }
351
+
352
+ function updateToolStatus(toolName) {
353
+ const activity = showActivity();
354
+ let status = activity.querySelector('[data-status]');
355
+ if (!status) {
356
+ status = document.createElement('p');
357
+ status.dataset.status = '';
358
+ activity.appendChild(status);
359
+ }
360
+ status.textContent = `using ${toolName}...`;
361
+ const dots = activity.querySelector('[data-dots]');
362
+ if (dots) dots.remove();
363
+ scrollToBottom();
364
+ }
365
+
366
+ function connectSSE(agentId) {
367
+ if (eventSource) eventSource.close();
368
+ eventSource = new EventSource(`/api/agents/${agentId}/stream`);
369
+
370
+ let suppressNextUserMsg = false;
371
+
372
+ eventSource.addEventListener('connected', () => {
373
+ if (activeAgent && activeAgent.bootstrapped === 0 && !activeAgent._kickedOff) {
374
+ activeAgent._kickedOff = true;
375
+ suppressNextUserMsg = true;
376
+ api('POST', `/api/agents/${agentId}/chat`, { content: 'hello' }).catch(() => {});
377
+ }
378
+ });
379
+
380
+ eventSource.addEventListener('typing', (e) => {
381
+ try {
382
+ const data = JSON.parse(e.data);
383
+ if (data.active) showActivity();
384
+ else clearActivity();
385
+ } catch {}
386
+ });
387
+
388
+ eventSource.addEventListener('ack_message', (e) => {
389
+ try {
390
+ clearActivity();
391
+ const data = JSON.parse(e.data);
392
+ appendMessage('assistant', data.content);
393
+ scrollToBottom();
394
+ } catch {}
395
+ });
396
+
397
+ eventSource.addEventListener('intermediate', (e) => {
398
+ try {
399
+ const data = JSON.parse(e.data);
400
+ updateThinking(data.content);
401
+ } catch {}
402
+ });
403
+
404
+ eventSource.addEventListener('tool_call', (e) => {
405
+ try {
406
+ const data = JSON.parse(e.data);
407
+ updateToolStatus(data.name);
408
+ } catch {}
409
+ });
410
+
411
+ eventSource.addEventListener('assistant_message', (e) => {
412
+ try {
413
+ clearActivity();
414
+ const data = JSON.parse(e.data);
415
+ appendMessage('assistant', data.content, data.tool_calls);
416
+ scrollToBottom();
417
+ chatInput.focus();
418
+ } catch {}
419
+ });
420
+
421
+ eventSource.addEventListener('user_message', (e) => {
422
+ try {
423
+ const data = JSON.parse(e.data);
424
+ if (suppressNextUserMsg) {
425
+ suppressNextUserMsg = false;
426
+ return;
427
+ }
428
+ const existing = messagesDiv.querySelector(`[data-msg-id="${data.id}"]`);
429
+ if (existing) return;
430
+ const pending = messagesDiv.querySelector('[data-msg-id="pending"]');
431
+ if (pending) {
432
+ pending.dataset.msgId = data.id;
433
+ return;
434
+ }
435
+ appendMessage('user', data.content).dataset.msgId = data.id;
436
+ scrollToBottom();
437
+ bubbleAgent(agentId);
438
+ } catch {}
439
+ });
440
+
441
+ eventSource.addEventListener('heartbeat_alert', (e) => {
442
+ try {
443
+ if (!activeAgent || !activeAgent.show_heartbeat) return;
444
+ const data = JSON.parse(e.data);
445
+ const div = document.createElement('div');
446
+ div.dataset.role = 'heartbeat';
447
+ const body = document.createElement('div');
448
+ body.className = 'msg-body';
449
+ body.innerHTML = renderMarkdown(cleanToolLeaks(data.content));
450
+ div.appendChild(body);
451
+ messagesDiv.appendChild(div);
452
+ scrollToBottom();
453
+ } catch {}
454
+ });
455
+
456
+ eventSource.addEventListener('bootstrap_complete', async () => {
457
+ if (activeAgent) {
458
+ try {
459
+ const updated = await api('GET', `/api/agents/${activeAgent.id}`);
460
+ const idx = agents.findIndex(a => a.id === activeAgent.id);
461
+ if (idx !== -1) agents[idx] = updated;
462
+ activeAgent = updated;
463
+ } catch {}
464
+ }
465
+ });
466
+
467
+ eventSource.addEventListener('error', (e) => {
468
+ clearActivity();
469
+ try {
470
+ const data = JSON.parse(e.data);
471
+ const div = document.createElement('div');
472
+ div.dataset.role = 'error';
473
+ div.setAttribute('role', 'alert');
474
+ div.innerHTML = `<p>${esc(data.error)}</p>`;
475
+ messagesDiv.appendChild(div);
476
+ scrollToBottom();
477
+ } catch {}
478
+ });
479
+
480
+ eventSource.onerror = () => {};
481
+ }
482
+
483
+ const connectionDialog = $('dialog[aria-label="connection"]');
484
+ const connectionBody = $('dialog[aria-label="connection"] [data-body]');
485
+ const connectionHeader = $('dialog[aria-label="connection"] > header');
486
+ const connectionFooter = $('dialog[aria-label="connection"] > footer');
487
+
488
+ let connectionsData = {};
489
+
490
+ async function renderConnections() {
491
+ let conns = [];
492
+ try {
493
+ conns = await api('GET', '/api/connections');
494
+ } catch {}
495
+
496
+ connectionsData = {};
497
+ for (const c of conns) connectionsData[c.platform] = c;
498
+
499
+ platformsDiv.innerHTML = platforms.map(p => {
500
+ const conn = connectionsData[p.id];
501
+ const status = conn ? conn.status : 'disconnected';
502
+
503
+ let statusLabel = status;
504
+ if (status === 'connected' && conn.status_detail) statusLabel = conn.status_detail;
505
+
506
+ return `<article data-platform="${p.id}" data-status="${status}"${p.comingSoon ? ' data-coming-soon' : ''} title="${p.comingSoon ? p.name + ' — coming soon' : p.name + ' — ' + statusLabel}" role="${p.comingSoon ? 'presentation' : 'button'}" ${p.comingSoon ? '' : 'tabindex="0"'}>
507
+ ${p.icon}
508
+ <div><h3>${p.name}</h3><p>${p.comingSoon ? 'coming soon' : statusLabel}</p></div>
509
+ <span data-indicator="${status}" aria-label="${status}"></span>
510
+ </article>`;
511
+ }).join('');
512
+
513
+ platformsDiv.querySelectorAll('article:not([data-coming-soon])').forEach(card => {
514
+ card.addEventListener('click', () => openConnectionDialog(card.dataset.platform));
515
+ card.addEventListener('keydown', (e) => {
516
+ if (e.key === 'Enter' || e.key === ' ') {
517
+ e.preventDefault();
518
+ openConnectionDialog(card.dataset.platform);
519
+ }
520
+ });
521
+ });
522
+ }
523
+
524
+ function cleanConnectionError(msg) {
525
+ if (!msg) return 'unknown error';
526
+ return msg.replace(/\/[\w\/.-]+\//g, '').replace(/\s+/g, ' ').trim();
527
+ }
528
+
529
+ function openConnectionDialog(platformId) {
530
+ const p = platforms.find(x => x.id === platformId);
531
+ if (!p) return;
532
+ const conn = connectionsData[p.id];
533
+ const status = conn ? conn.status : 'disconnected';
534
+ const config = conn ? conn.config : {};
535
+ const isConnected = status === 'connected';
536
+
537
+ connectionHeader.innerHTML = `${p.icon}<div><h2>${p.name}</h2><p>${p.description}</p></div><span data-indicator="${status}"></span>`;
538
+
539
+ let body = '';
540
+
541
+ if (isConnected) {
542
+ if (conn.status_detail) {
543
+ body += `<p data-detail>${esc(conn.status_detail)}</p>`;
544
+ }
545
+ if (p.usage) {
546
+ body += `<p data-usage>${p.usage}</p>`;
547
+ }
548
+ if (p.id === 'discord' && config.app_id) {
549
+ body += `<p data-invite-label>invite link</p>`;
550
+ body += `<pre data-invite>https://discord.com/oauth2/authorize?client_id=${esc(config.app_id)}&scope=bot&permissions=274877975552</pre>`;
551
+ }
552
+ } else {
553
+ if (status === 'error' && conn && conn.status_detail) {
554
+ body += `<p data-detail="error">${esc(cleanConnectionError(conn.status_detail))}</p>`;
555
+ }
556
+ if (p.setup && p.setup.length) {
557
+ body += '<ol data-setup>';
558
+ for (const step of p.setup) body += `<li>${step}</li>`;
559
+ body += '</ol>';
560
+ }
561
+ if (p.fields.length) {
562
+ body += '<form>';
563
+ for (const f of p.fields) {
564
+ const val = config[f.key] || '';
565
+ body += `<label><span>${f.label}</span><input type="${f.type || 'text'}" data-field="${f.key}" placeholder="${f.placeholder}" value="${esc(val)}"></label>`;
566
+ }
567
+ body += '</form>';
568
+ }
569
+ }
570
+
571
+ connectionBody.innerHTML = body;
572
+
573
+ let footerHtml = '';
574
+ if (isConnected) {
575
+ footerHtml = `<button type="button" data-disconnect data-platform="${p.id}" title="disconnect ${p.name}">disconnect</button>`;
576
+ } else {
577
+ const connectLabel = status === 'error' ? 'retry' : 'connect';
578
+ footerHtml = `<button type="button" data-connect data-platform="${p.id}" title="connect to ${p.name}">${connectLabel}</button>`;
579
+ }
580
+ footerHtml += '<button type="button" data-action="cancel-dialog" title="close">close</button>';
581
+ connectionFooter.innerHTML = footerHtml;
582
+
583
+ connectionFooter.querySelector('button[data-action="cancel-dialog"]').addEventListener('click', () => connectionDialog.close());
584
+
585
+ const connectBtn = connectionFooter.querySelector('[data-connect]');
586
+ if (connectBtn) {
587
+ connectBtn.addEventListener('click', async () => {
588
+ const cfg = {};
589
+ connectionBody.querySelectorAll('input[data-field]').forEach(input => {
590
+ if (input.value.trim()) cfg[input.dataset.field] = input.value.trim();
591
+ });
592
+ connectBtn.disabled = true;
593
+ connectBtn.textContent = 'connecting...';
594
+ try {
595
+ await api('POST', `/api/connections/${p.id}/enable`, cfg);
596
+ if (p.qrAuth) {
597
+ let qrPoller = setInterval(async () => {
598
+ try {
599
+ const conns = await api('GET', '/api/connections');
600
+ const conn = conns.find(c => c.platform === p.id);
601
+ if (!conn) return;
602
+ if (conn.status === 'qr' && conn.status_detail) {
603
+ connectionBody.innerHTML = `<img data-qr src="${conn.status_detail}" alt="scan this qr code">`;
604
+ connectionHeader.querySelector('[data-indicator]').setAttribute('data-indicator', 'qr');
605
+ } else if (conn.status === 'connected') {
606
+ clearInterval(qrPoller);
607
+ connectionHeader.querySelector('[data-indicator]').setAttribute('data-indicator', 'connected');
608
+ connectionBody.innerHTML = `<p data-detail>${esc(conn.status_detail || 'connected')}</p>` +
609
+ (p.usage ? `<p data-usage>${p.usage}</p>` : '');
610
+ connectionFooter.innerHTML = `<button type="button" data-disconnect data-platform="${p.id}">disconnect</button>` +
611
+ '<button type="button" data-action="cancel-dialog">close</button>';
612
+ connectionFooter.querySelector('[data-disconnect]').addEventListener('click', async () => {
613
+ try {
614
+ await api('POST', `/api/connections/${p.id}/disable`);
615
+ connectionDialog.close();
616
+ await renderConnections();
617
+ } catch (err) { connectionBody.insertAdjacentHTML('afterbegin', `<p data-detail="error">${esc(cleanConnectionError(err.message))}</p>`); }
618
+ });
619
+ connectionFooter.querySelector('[data-action="cancel-dialog"]').addEventListener('click', () => connectionDialog.close());
620
+ await renderConnections();
621
+ } else if (conn.status === 'error') {
622
+ clearInterval(qrPoller);
623
+ connectionBody.innerHTML = `<p data-detail="error">${esc(cleanConnectionError(conn.status_detail))}</p>`;
624
+ connectBtn.disabled = false;
625
+ connectBtn.textContent = 'connect';
626
+ connectionFooter.innerHTML = `<button type="button" data-connect data-platform="${p.id}">retry</button>` +
627
+ '<button type="button" data-action="cancel-dialog">close</button>';
628
+ connectionFooter.querySelector('[data-action="cancel-dialog"]').addEventListener('click', () => connectionDialog.close());
629
+ }
630
+ } catch {}
631
+ }, 2000);
632
+ connectionDialog.addEventListener('close', () => clearInterval(qrPoller), { once: true });
633
+ } else {
634
+ connectionDialog.close();
635
+ await renderConnections();
636
+ }
637
+ } catch (err) {
638
+ const existing = connectionBody.querySelector('[data-detail="error"]');
639
+ if (existing) existing.remove();
640
+ connectionBody.insertAdjacentHTML('afterbegin', `<p data-detail="error">${esc(cleanConnectionError(err.message))}</p>`);
641
+ connectBtn.disabled = false;
642
+ connectBtn.textContent = 'retry';
643
+ }
644
+ });
645
+ }
646
+
647
+ const disconnectBtn = connectionFooter.querySelector('[data-disconnect]');
648
+ if (disconnectBtn) {
649
+ disconnectBtn.addEventListener('click', async () => {
650
+ disconnectBtn.disabled = true;
651
+ try {
652
+ await api('POST', `/api/connections/${p.id}/disable`);
653
+ connectionDialog.close();
654
+ await renderConnections();
655
+ } catch (err) {
656
+ const existing = connectionBody.querySelector('[data-detail="error"]');
657
+ if (existing) existing.remove();
658
+ connectionBody.insertAdjacentHTML('afterbegin', `<p data-detail="error">${esc(cleanConnectionError(err.message))}</p>`);
659
+ disconnectBtn.disabled = false;
660
+ }
661
+ });
662
+ }
663
+
664
+ connectionDialog.showModal();
665
+ }
666
+
667
+ function startConnectionsPolling() {
668
+ stopConnectionsPolling();
669
+ renderConnections();
670
+ connectionsPoller = setInterval(renderConnections, 3000);
671
+ }
672
+
673
+ function stopConnectionsPolling() {
674
+ if (connectionsPoller) {
675
+ clearInterval(connectionsPoller);
676
+ connectionsPoller = null;
677
+ }
678
+ }
679
+
680
+ connectionsBtn.addEventListener('click', () => {
681
+ activeAgent = null;
682
+ if (eventSource) { eventSource.close(); eventSource = null; }
683
+ renderAgents();
684
+ showSection('connections');
685
+ closeNav();
686
+ });
687
+
688
+ chatForm.addEventListener('submit', async (e) => {
689
+ e.preventDefault();
690
+ if (!activeAgent) return;
691
+ const content = chatInput.value.trim();
692
+ if (!content) return;
693
+
694
+ chatSubmit.disabled = true;
695
+ chatInput.value = '';
696
+ autoResize();
697
+
698
+ appendMessage('user', content).dataset.msgId = 'pending';
699
+ scrollToBottom();
700
+
701
+ bubbleAgent(activeAgent.id);
702
+
703
+ try {
704
+ await api('POST', `/api/agents/${activeAgent.id}/chat`, { content });
705
+ } catch (err) {
706
+ const div = document.createElement('div');
707
+ div.dataset.role = 'error';
708
+ div.setAttribute('role', 'alert');
709
+ div.innerHTML = `<p>${esc(err.message)}</p>`;
710
+ messagesDiv.appendChild(div);
711
+ scrollToBottom();
712
+ } finally {
713
+ chatSubmit.disabled = false;
714
+ chatInput.focus();
715
+ }
716
+ });
717
+
718
+ chatInput.addEventListener('keydown', (e) => {
719
+ if (e.key === 'Enter' && !e.shiftKey) {
720
+ e.preventDefault();
721
+ chatForm.dispatchEvent(new Event('submit'));
722
+ }
723
+ });
724
+
725
+ function autoResize() {
726
+ chatInput.style.height = 'auto';
727
+ chatInput.style.height = Math.min(chatInput.scrollHeight, 150) + 'px';
728
+ }
729
+
730
+ chatInput.addEventListener('input', autoResize);
731
+
732
+ $('nav[aria-label="agents"] header button').addEventListener('click', () => {
733
+ $('dialog[aria-label="create agent"] form').reset();
734
+ createDialog.showModal();
735
+ });
736
+
737
+ $('dialog[aria-label="create agent"] form').addEventListener('submit', async (e) => {
738
+ e.preventDefault();
739
+ const form = e.target;
740
+ const data = {
741
+ name: form.name.value.trim(),
742
+ is_default: form.is_default.checked,
743
+ model: form.model.value,
744
+ thinking: form.thinking.value
745
+ };
746
+ try {
747
+ const agent = await api('POST', '/api/agents', data);
748
+ if (agent.is_default) agents.forEach(a => a.is_default = 0);
749
+ agents.unshift(agent);
750
+ renderAgents();
751
+ selectAgent(agent.id);
752
+ createDialog.close();
753
+ } catch (err) {
754
+ showDialogError(createDialog, err.message);
755
+ }
756
+ });
757
+
758
+ $$('button[data-action="cancel-dialog"]').forEach(btn => {
759
+ btn.addEventListener('click', () => {
760
+ btn.closest('dialog').close();
761
+ });
762
+ });
763
+
764
+ $$('dialog').forEach(d => {
765
+ d.addEventListener('click', (e) => {
766
+ if (e.target === d) d.close();
767
+ });
768
+ });
769
+
770
+ $('button[data-action="edit-agent"]').addEventListener('click', () => {
771
+ if (!activeAgent) return;
772
+ const form = $('dialog[aria-label="edit agent"] form');
773
+ form.querySelector('[name="id"]').value = activeAgent.id;
774
+ form.querySelector('[name="name"]').value = activeAgent.name;
775
+ form.querySelector('[name="is_default"]').checked = !!activeAgent.is_default;
776
+ form.querySelector('[name="heartbeat_enabled"]').checked = activeAgent.heartbeat_interval !== null;
777
+ const intervalSelect = form.querySelector('[name="heartbeat_interval"]');
778
+ if (activeAgent.heartbeat_interval) {
779
+ intervalSelect.value = String(activeAgent.heartbeat_interval);
780
+ }
781
+ form.querySelector('[name="show_heartbeat"]').checked = !!activeAgent.show_heartbeat;
782
+ form.querySelector('[name="model"]').value = activeAgent.model || 'opus';
783
+ form.querySelector('[name="thinking"]').value = activeAgent.thinking || 'high';
784
+ editDialog.showModal();
785
+ });
786
+
787
+ $('dialog[aria-label="edit agent"] form').addEventListener('submit', async (e) => {
788
+ e.preventDefault();
789
+ const form = e.target;
790
+ const id = form.querySelector('[name="id"]').value;
791
+ const heartbeatEnabled = form.querySelector('[name="heartbeat_enabled"]').checked;
792
+ const data = {
793
+ name: form.name.value.trim(),
794
+ is_default: form.is_default.checked,
795
+ heartbeat_interval: heartbeatEnabled ? parseInt(form.querySelector('[name="heartbeat_interval"]').value) : null,
796
+ show_heartbeat: form.querySelector('[name="show_heartbeat"]').checked,
797
+ model: form.querySelector('[name="model"]').value,
798
+ thinking: form.querySelector('[name="thinking"]').value
799
+ };
800
+ try {
801
+ const updated = await api('PATCH', `/api/agents/${id}`, data);
802
+ if (updated.is_default) agents.forEach(a => a.is_default = 0);
803
+ const idx = agents.findIndex(a => a.id === id);
804
+ if (idx !== -1) agents[idx] = updated;
805
+ activeAgent = updated;
806
+ renderAgents();
807
+ $('section[aria-label="chat"] > header h2').textContent = updated.name;
808
+ editDialog.close();
809
+ } catch (err) {
810
+ showDialogError(editDialog, err.message);
811
+ }
812
+ });
813
+
814
+ const deleteDialog = $('dialog[aria-label="confirm delete"]');
815
+
816
+ $('button[data-action="delete-agent"]').addEventListener('click', () => {
817
+ if (!activeAgent) return;
818
+ deleteDialog.querySelector('[data-confirm-msg]').textContent = `are you sure you want to delete "${activeAgent.name}"? this cannot be undone.`;
819
+ deleteDialog.showModal();
820
+ });
821
+
822
+ $('button[data-action="confirm-delete"]').addEventListener('click', async () => {
823
+ if (!activeAgent) return;
824
+ try {
825
+ await api('DELETE', `/api/agents/${activeAgent.id}`);
826
+ agents = agents.filter(a => a.id !== activeAgent.id);
827
+ activeAgent = null;
828
+ if (eventSource) { eventSource.close(); eventSource = null; }
829
+ renderAgents();
830
+ showSection('empty');
831
+ deleteDialog.close();
832
+ } catch (err) {
833
+ showDialogError(deleteDialog, err.message);
834
+ }
835
+ });
836
+
837
+
838
+ $('button[data-action="view-files"]').addEventListener('click', async () => {
839
+ if (!activeAgent) return;
840
+ await openFilesDialog();
841
+ });
842
+
843
+ const recordIcons = {
844
+ 'SOUL.md': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M288 32c-80.8 0-145.5 36.8-192.6 80.6-46.8 43.5-78.1 95.4-93 131.1-3.3 7.9-3.3 16.7 0 24.6 14.9 35.7 46.2 87.7 93 131.1 47.1 43.7 111.8 80.6 192.6 80.6s145.5-36.8 192.6-80.6c46.8-43.5 78.1-95.4 93-131.1 3.3-7.9 3.3-16.7 0-24.6-14.9-35.7-46.2-87.7-93-131.1-47.1-43.7-111.8-80.6-192.6-80.6zM144 256a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm144-64c0 35.3-28.7 64-64 64-11.5 0-22.3-3-31.7-8.4-1 10.9-.1 22.1 2.9 33.2 13.7 51.2 66.4 81.6 117.6 67.9s81.6-66.4 67.9-117.6c-12.2-45.7-55.5-74.8-101.1-70.8 5.3 9.3 8.4 20.1 8.4 31.7z"/></svg>',
845
+ 'IDENTITY.md': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M48 256c0-114.9 93.1-208 208-208 63.1 0 119.6 28.1 157.8 72.5 8.6 10.1 23.8 11.2 33.8 2.6s11.2-23.8 2.6-33.8C403.3 34.6 333.7 0 256 0 114.6 0 0 114.6 0 256l0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40zm458.5-52.9c-2.7-13-15.5-21.3-28.4-18.5s-21.3 15.5-18.5 28.4c2.9 13.9 4.5 28.3 4.5 43.1l0 40c0 13.3 10.7 24 24 24s24-10.7 24-24l0-40c0-18.1-1.9-35.8-5.5-52.9zM256 80c-19 0-37.4 3-54.5 8.6-15.2 5-18.7 23.7-8.3 35.9 7.1 8.3 18.8 10.8 29.4 7.9 10.6-2.9 21.8-4.4 33.4-4.4 70.7 0 128 57.3 128 128l0 24.9c0 25.2-1.5 50.3-4.4 75.3-1.7 14.6 9.4 27.8 24.2 27.8 11.8 0 21.9-8.6 23.3-20.3 3.3-27.4 5-55 5-82.7l0-24.9c0-97.2-78.8-176-176-176zM150.7 148.7c-9.1-10.6-25.3-11.4-33.9-.4-23.1 29.8-36.8 67.1-36.8 107.7l0 24.9c0 24.2-2.6 48.4-7.8 71.9-3.4 15.6 7.9 31.1 23.9 31.1 10.5 0 19.9-7 22.2-17.3 6.4-28.1 9.7-56.8 9.7-85.8l0-24.9c0-27.2 8.5-52.4 22.9-73.1 7.2-10.4 8-24.6-.2-34.2zM256 160c-53 0-96 43-96 96l0 24.9c0 35.9-4.6 71.5-13.8 106.1-3.8 14.3 6.7 29 21.5 29 9.5 0 17.9-6.2 20.4-15.4 10.5-39 15.9-79.2 15.9-119.7l0-24.9c0-28.7 23.3-52 52-52s52 23.3 52 52l0 24.9c0 36.3-3.5 72.4-10.4 107.9-2.7 13.9 7.7 27.2 21.8 27.2 10.2 0 19-7 21-17 7.7-38.8 11.6-78.3 11.6-118.1l0-24.9c0-53-43-96-96-96zm24 96c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 24.9c0 59.9-11 119.3-32.5 175.2l-5.9 15.3c-4.8 12.4 1.4 26.3 13.8 31s26.3-1.4 31-13.8l5.9-15.3C267.9 411.9 280 346.7 280 280.9l0-24.9z"/></svg>',
846
+ 'USER.md': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512"><path fill="currentColor" d="M224 248a120 120 0 1 0 0-240 120 120 0 1 0 0 240zm-29.7 56C95.8 304 16 383.8 16 482.3 16 498.7 29.3 512 45.7 512l356.6 0c16.4 0 29.7-13.3 29.7-29.7 0-98.5-79.8-178.3-178.3-178.3l-59.4 0z"/></svg>',
847
+ 'MEMORY.md': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M120 56c0-30.9 25.1-56 56-56l24 0c17.7 0 32 14.3 32 32l0 448c0 17.7-14.3 32-32 32l-32 0c-29.8 0-54.9-20.4-62-48-.7 0-1.3 0-2 0-44.2 0-80-35.8-80-80 0-18 6-34.6 16-48-19.4-14.6-32-37.8-32-64 0-30.9 17.6-57.8 43.2-71.1-7.1-12-11.2-26-11.2-40.9 0-44.2 35.8-80 80-80l0-24zm272 0l0 24c44.2 0 80 35.8 80 80 0 15-4.1 29-11.2 40.9 25.7 13.3 43.2 40.1 43.2 71.1 0 26.2-12.6 49.4-32 64 10 13.4 16 30 16 48 0 44.2-35.8 80-80 80-.7 0-1.3 0-2 0-7.1 27.6-32.2 48-62 48l-32 0c-17.7 0-32-14.3-32-32l0-448c0-17.7 14.3-32 32-32l24 0c30.9 0 56 25.1 56 56z"/></svg>',
848
+ 'HEARTBEAT.md': '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M241 87.1l15 20.7 15-20.7C296 52.5 336.2 32 378.9 32 452.4 32 512 91.6 512 165.1l0 2.6c0 112.2-139.9 242.5-212.9 298.2-12.4 9.4-27.6 14.1-43.1 14.1s-30.8-4.6-43.1-14.1C139.9 410.2 0 279.9 0 167.7l0-2.6C0 91.6 59.6 32 133.1 32 175.8 32 216 52.5 241 87.1z"/></svg>'
849
+ };
850
+ const iconLogs = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path fill="currentColor" d="M56 225.6L32.4 296.2 32.4 96c0-35.3 28.7-64 64-64l138.7 0c13.8 0 27.3 4.5 38.4 12.8l38.4 28.8c5.5 4.2 12.3 6.4 19.2 6.4l117.3 0c35.3 0 64 28.7 64 64l0 16-365.4 0c-41.3 0-78 26.4-91.1 65.6zM477.8 448L99 448c-32.8 0-55.9-32.1-45.5-63.2l48-144C108 221.2 126.4 208 147 208l378.8 0c32.8 0 55.9 32.1 45.5 63.2l-48 144c-6.5 19.6-24.9 32.8-45.5 32.8z"/></svg>';
851
+ const recordLabels = {
852
+ 'SOUL.md': 'soul',
853
+ 'IDENTITY.md': 'identity',
854
+ 'USER.md': 'user',
855
+ 'MEMORY.md': 'memory',
856
+ 'HEARTBEAT.md': 'heartbeat'
857
+ };
858
+ const recordOrder = ['HEARTBEAT.md', 'IDENTITY.md', 'MEMORY.md', 'SOUL.md', 'USER.md'];
859
+
860
+ async function openFilesDialog() {
861
+ fileEditor.hidden = true;
862
+ fileListFooter.hidden = false;
863
+ fileList.hidden = false;
864
+ $('dialog[aria-label="agent records"] > h2').hidden = false;
865
+ fileList.innerHTML = '';
866
+
867
+ try {
868
+ const files = await api('GET', `/api/agents/${activeAgent.id}/workspace`);
869
+ const known = recordOrder.filter(f => files.includes(f));
870
+ const other = files.filter(f => !recordOrder.includes(f) && !f.startsWith('memory/'));
871
+
872
+ for (const f of known) {
873
+ const btn = document.createElement('button');
874
+ btn.type = 'button';
875
+ btn.title = `edit ${recordLabels[f]}`;
876
+ btn.innerHTML = `${recordIcons[f]}<span>${recordLabels[f]}</span>`;
877
+ btn.addEventListener('click', () => openFileEditor(f));
878
+ fileList.appendChild(btn);
879
+ }
880
+
881
+ const logsBtn = document.createElement('button');
882
+ logsBtn.type = 'button';
883
+ logsBtn.title = 'view memory logs';
884
+ logsBtn.innerHTML = `${iconLogs}<span>logs</span>`;
885
+ logsBtn.addEventListener('click', () => openLogsView());
886
+ fileList.appendChild(logsBtn);
887
+
888
+ const fileIcon = '<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><path fill="currentColor" d="M64 0C28.7 0 0 28.7 0 64L0 448c0 35.3 28.7 64 64 64l256 0c35.3 0 64-28.7 64-64l0-277.5c0-17-6.7-33.3-18.7-45.3L258.7 18.7C246.7 6.7 230.5 0 213.5 0L64 0zM325.5 176L232 176c-13.3 0-24-10.7-24-24L208 58.5 325.5 176z"/></svg>';
889
+ for (const f of other) {
890
+ const btn = document.createElement('button');
891
+ btn.type = 'button';
892
+ btn.title = `edit ${f}`;
893
+ btn.innerHTML = `${fileIcon}<span>${esc(f)}</span>`;
894
+ btn.addEventListener('click', () => openFileEditor(f));
895
+ fileList.appendChild(btn);
896
+ }
897
+ } catch (err) {
898
+ fileList.innerHTML = `<p style="color:var(--muted);font-size:12px">${err.message}</p>`;
899
+ }
900
+
901
+ filesDialog.showModal();
902
+ }
903
+
904
+ async function openLogsView() {
905
+ fileList.hidden = true;
906
+ fileListFooter.hidden = true;
907
+ $('dialog[aria-label="agent records"] > h2').hidden = true;
908
+ fileEditor.hidden = false;
909
+
910
+ const textarea = fileEditor.querySelector('[data-file-content]');
911
+ const nameEl = fileEditor.querySelector('[data-file-name]');
912
+ nameEl.textContent = 'logs';
913
+ textarea.value = 'loading...';
914
+ textarea.disabled = true;
915
+
916
+ try {
917
+ const dates = await api('GET', `/api/agents/${activeAgent.id}/logs`);
918
+ if (!dates.length) {
919
+ textarea.value = 'no logs yet';
920
+ textarea.disabled = true;
921
+
922
+ const saveBtn = fileEditor.querySelector('[data-action="save-file"]');
923
+ saveBtn.hidden = true;
924
+ } else {
925
+ const logList = fileEditor.querySelector('[data-file-content]');
926
+ fileEditor.querySelector('[data-action="save-file"]').hidden = true;
927
+ logList.hidden = true;
928
+
929
+ const listDiv = document.createElement('div');
930
+ listDiv.dataset.logList = '';
931
+ for (const date of dates) {
932
+ const btn = document.createElement('button');
933
+ btn.type = 'button';
934
+ btn.title = `view log for ${date}`;
935
+ btn.innerHTML = `${iconLogs}<span>${date}</span>`;
936
+ btn.addEventListener('click', () => openFileEditor(`memory/${date}.md`));
937
+ listDiv.appendChild(btn);
938
+ }
939
+ fileEditor.insertBefore(listDiv, fileEditor.querySelector('footer'));
940
+ }
941
+ } catch (err) {
942
+ textarea.value = err.message;
943
+ textarea.disabled = true;
944
+ }
945
+
946
+ const backBtn = fileEditor.querySelector('[data-action="file-back"]');
947
+ const newBack = backBtn.cloneNode(true);
948
+ backBtn.parentNode.replaceChild(newBack, backBtn);
949
+ newBack.addEventListener('click', () => {
950
+ const logListEl = fileEditor.querySelector('[data-log-list]');
951
+ if (logListEl) logListEl.remove();
952
+ fileEditor.querySelector('[data-file-content]').hidden = false;
953
+ fileEditor.querySelector('[data-action="save-file"]').hidden = false;
954
+ fileEditor.hidden = true;
955
+ fileListFooter.hidden = false;
956
+ fileList.hidden = false;
957
+ $('dialog[aria-label="agent records"] > h2').hidden = false;
958
+ });
959
+ }
960
+
961
+ async function openFileEditor(filePath) {
962
+ const logListEl = fileEditor.querySelector('[data-log-list]');
963
+ if (logListEl) logListEl.remove();
964
+ const isLog = filePath.startsWith('memory/');
965
+ const label = recordLabels[filePath] || filePath.replace('memory/', '').replace('.md', '');
966
+
967
+ fileEditor.querySelector('[data-file-name]').textContent = label;
968
+ fileEditor.hidden = false;
969
+ fileListFooter.hidden = true;
970
+ fileList.hidden = true;
971
+ $('dialog[aria-label="agent records"] > h2').hidden = true;
972
+
973
+ const textarea = fileEditor.querySelector('[data-file-content]');
974
+ textarea.hidden = false;
975
+ textarea.value = 'loading...';
976
+ textarea.disabled = true;
977
+
978
+ const saveBtn = fileEditor.querySelector('[data-action="save-file"]');
979
+ saveBtn.hidden = false;
980
+
981
+ try {
982
+ const data = await api('GET', `/api/agents/${activeAgent.id}/workspace/${filePath}`);
983
+ textarea.value = data.content;
984
+ textarea.disabled = false;
985
+ } catch {
986
+ textarea.value = '';
987
+ textarea.disabled = false;
988
+ }
989
+
990
+ const newSave = saveBtn.cloneNode(true);
991
+ saveBtn.parentNode.replaceChild(newSave, saveBtn);
992
+ newSave.addEventListener('click', async () => {
993
+ try {
994
+ await api('PUT', `/api/agents/${activeAgent.id}/workspace/${filePath}`, { content: textarea.value });
995
+ newSave.textContent = 'saved';
996
+ setTimeout(() => { newSave.textContent = 'save'; }, 1500);
997
+ } catch (err) {
998
+ showDialogError(filesDialog, err.message);
999
+ }
1000
+ });
1001
+
1002
+ const backBtn = fileEditor.querySelector('[data-action="file-back"]');
1003
+ const newBack = backBtn.cloneNode(true);
1004
+ backBtn.parentNode.replaceChild(newBack, backBtn);
1005
+ newBack.addEventListener('click', () => {
1006
+ if (isLog) {
1007
+ openLogsView();
1008
+ } else {
1009
+ fileEditor.hidden = true;
1010
+ fileListFooter.hidden = false;
1011
+ fileList.hidden = false;
1012
+ $('dialog[aria-label="agent records"] > h2').hidden = false;
1013
+ }
1014
+ });
1015
+ }
1016
+
1017
+ const setupTokenDialog = $('dialog[aria-label="setup token"]');
1018
+ const apiKeyDialog = $('dialog[aria-label="api key"]');
1019
+
1020
+ $('button[data-action="open-setup-token"]').addEventListener('click', () => { clearDialogError(setupTokenDialog); setupTokenDialog.showModal(); });
1021
+ $('button[data-action="open-api-key"]').addEventListener('click', () => { clearDialogError(apiKeyDialog); apiKeyDialog.showModal(); });
1022
+
1023
+ function showDialogError(dialog, msg) {
1024
+ const el = $('[data-error]', dialog);
1025
+ el.textContent = msg;
1026
+ el.hidden = false;
1027
+ }
1028
+
1029
+ function clearDialogError(dialog) {
1030
+ const el = $('[data-error]', dialog);
1031
+ el.textContent = '';
1032
+ el.hidden = true;
1033
+ }
1034
+
1035
+ $('form[aria-label="setup-token"]').addEventListener('submit', async (e) => {
1036
+ e.preventDefault();
1037
+ clearDialogError(setupTokenDialog);
1038
+ const input = $('form[aria-label="setup-token"] input');
1039
+ const token = input.value.trim();
1040
+ if (!token) return;
1041
+ if (!token.startsWith('sk-ant-oat')) return showDialogError(setupTokenDialog, 'invalid setup token — must start with sk-ant-oat. if you have an api key, use the api key option instead.');
1042
+ try {
1043
+ await api('POST', '/api/auth/setup-token', { token });
1044
+ setupTokenDialog.close();
1045
+ init();
1046
+ } catch (err) {
1047
+ showDialogError(setupTokenDialog, err.message);
1048
+ }
1049
+ });
1050
+
1051
+ $('form[aria-label="api-key"]').addEventListener('submit', async (e) => {
1052
+ e.preventDefault();
1053
+ clearDialogError(apiKeyDialog);
1054
+ const input = $('form[aria-label="api-key"] input');
1055
+ const key = input.value.trim();
1056
+ if (!key) return;
1057
+ if (!key.startsWith('sk-ant-api')) return showDialogError(apiKeyDialog, 'invalid api key — must start with sk-ant-api. if you have a setup token, use the setup token option instead.');
1058
+ try {
1059
+ await api('POST', '/api/auth/api-key', { key });
1060
+ apiKeyDialog.close();
1061
+ init();
1062
+ } catch (err) {
1063
+ showDialogError(apiKeyDialog, err.message);
1064
+ }
1065
+ });
1066
+
1067
+ $('button[data-action="check-auth"]').addEventListener('click', () => init());
1068
+
1069
+ const navEl = $('nav[aria-label="agents"]');
1070
+ const navOverlay = $('[data-overlay]');
1071
+ const navToggle = $('button[data-action="toggle-nav"]');
1072
+
1073
+ function openNav() {
1074
+ navEl.setAttribute('data-open', '');
1075
+ navOverlay.hidden = false;
1076
+ navToggle.setAttribute('aria-expanded', 'true');
1077
+ }
1078
+
1079
+ function closeNav() {
1080
+ navEl.removeAttribute('data-open');
1081
+ navOverlay.hidden = true;
1082
+ navToggle.setAttribute('aria-expanded', 'false');
1083
+ }
1084
+
1085
+ navToggle.addEventListener('click', () => {
1086
+ if (navEl.hasAttribute('data-open')) closeNav();
1087
+ else openNav();
1088
+ });
1089
+
1090
+ navOverlay.addEventListener('click', closeNav);
1091
+
1092
+ const themeToggle = $('button[data-action="toggle-theme"]');
1093
+ const themeColorMeta = $('meta[name="theme-color"]');
1094
+ const colorSchemeMeta = $('meta[name="color-scheme"]');
1095
+
1096
+ function setTheme(theme) {
1097
+ document.body.setAttribute('data-theme', theme);
1098
+ localStorage.setItem('claudity-theme', theme);
1099
+ themeColorMeta.content = theme === 'light' ? '#f5f5f5' : '#000000';
1100
+ colorSchemeMeta.content = theme;
1101
+ }
1102
+
1103
+ themeToggle.addEventListener('click', () => {
1104
+ const current = document.body.getAttribute('data-theme') || 'dark';
1105
+ setTheme(current === 'dark' ? 'light' : 'dark');
1106
+ });
1107
+
1108
+ setTheme(localStorage.getItem('claudity-theme') || 'dark');
1109
+
1110
+ const sfx = (() => {
1111
+ let ctx = null;
1112
+ let enabled = localStorage.getItem('claudity-sound') !== 'off';
1113
+
1114
+ function getCtx() {
1115
+ if (!ctx) ctx = new AudioContext();
1116
+ return ctx;
1117
+ }
1118
+
1119
+ function play(fn) {
1120
+ if (!enabled) return;
1121
+ try { fn(getCtx()); } catch {}
1122
+ }
1123
+
1124
+ return {
1125
+ get enabled() { return enabled; },
1126
+ set enabled(v) {
1127
+ enabled = v;
1128
+ localStorage.setItem('claudity-sound', v ? 'on' : 'off');
1129
+ document.body.setAttribute('data-sound', v ? 'on' : 'off');
1130
+ },
1131
+
1132
+ hover() {
1133
+ play(c => {
1134
+ const t = c.currentTime;
1135
+ const osc = c.createOscillator();
1136
+ const gain = c.createGain();
1137
+ const filter = c.createBiquadFilter();
1138
+ osc.type = 'triangle';
1139
+ osc.frequency.setValueAtTime(1800 + Math.random() * 200, t);
1140
+ osc.frequency.exponentialRampToValueAtTime(1200, t + 0.07);
1141
+ filter.type = 'lowpass';
1142
+ filter.frequency.value = 3000;
1143
+ filter.Q.value = 2;
1144
+ gain.gain.setValueAtTime(0.04, t);
1145
+ gain.gain.exponentialRampToValueAtTime(0.001, t + 0.07);
1146
+ osc.connect(filter).connect(gain).connect(c.destination);
1147
+ osc.start(t);
1148
+ osc.stop(t + 0.07);
1149
+ });
1150
+ },
1151
+
1152
+ click() {
1153
+ play(c => {
1154
+ const osc = c.createOscillator();
1155
+ const gain = c.createGain();
1156
+ osc.type = 'square';
1157
+ osc.frequency.value = 800 + Math.random() * 200;
1158
+ osc.frequency.exponentialRampToValueAtTime(400, c.currentTime + 0.08);
1159
+ gain.gain.value = 0.06;
1160
+ gain.gain.exponentialRampToValueAtTime(0.001, c.currentTime + 0.08);
1161
+ osc.connect(gain).connect(c.destination);
1162
+ osc.start();
1163
+ osc.stop(c.currentTime + 0.08);
1164
+ });
1165
+ },
1166
+
1167
+ type() {
1168
+ play(c => {
1169
+ const buf = c.createBuffer(1, c.sampleRate * 0.04, c.sampleRate);
1170
+ const data = buf.getChannelData(0);
1171
+ for (let i = 0; i < data.length; i++) {
1172
+ data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (data.length * 0.15));
1173
+ }
1174
+ const src = c.createBufferSource();
1175
+ const gain = c.createGain();
1176
+ const filter = c.createBiquadFilter();
1177
+ src.buffer = buf;
1178
+ filter.type = 'bandpass';
1179
+ filter.frequency.value = 2000 + Math.random() * 3000;
1180
+ filter.Q.value = 1.5;
1181
+ gain.gain.value = 0.08 + Math.random() * 0.04;
1182
+ src.connect(filter).connect(gain).connect(c.destination);
1183
+ src.start();
1184
+ });
1185
+ }
1186
+ };
1187
+ })();
1188
+
1189
+ document.body.setAttribute('data-sound', sfx.enabled ? 'on' : 'off');
1190
+
1191
+ const soundToggle = $('button[data-action="toggle-sound"]');
1192
+ soundToggle.setAttribute('aria-pressed', String(sfx.enabled));
1193
+ soundToggle.addEventListener('click', () => {
1194
+ sfx.enabled = !sfx.enabled;
1195
+ soundToggle.setAttribute('aria-pressed', String(sfx.enabled));
1196
+ if (sfx.enabled) sfx.click();
1197
+ });
1198
+
1199
+ let lastHovered = null;
1200
+ document.addEventListener('pointerover', (e) => {
1201
+ if (!e.target.closest) return;
1202
+ const btn = e.target.closest('button, nav li');
1203
+ if (btn && btn !== lastHovered) {
1204
+ lastHovered = btn;
1205
+ sfx.hover();
1206
+ } else if (!btn) {
1207
+ lastHovered = null;
1208
+ }
1209
+ });
1210
+
1211
+ document.addEventListener('pointerdown', (e) => {
1212
+ if (!e.target.closest) return;
1213
+ const btn = e.target.closest('button, nav li');
1214
+ if (btn) sfx.click();
1215
+ }, true);
1216
+
1217
+ chatInput.addEventListener('keydown', (e) => {
1218
+ if (e.key === 'Enter' || e.key === 'Backspace' || e.key === 'Tab') return;
1219
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
1220
+ if (e.key.length === 1) sfx.type();
1221
+ });
1222
+
1223
+ async function init() {
1224
+ try {
1225
+ const status = await api('GET', '/api/auth/status');
1226
+
1227
+ if (!status.authenticated) {
1228
+ showSection('setup');
1229
+ return;
1230
+ }
1231
+
1232
+ agents = await api('GET', '/api/agents');
1233
+ renderAgents();
1234
+ showSection('empty');
1235
+ } catch {
1236
+ showSection('setup');
1237
+ }
1238
+ }
1239
+
1240
+ init();