@controlflow-ai/daemon 0.1.0 → 0.1.2

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.
package/src/web.ts CHANGED
@@ -63,8 +63,9 @@ export function dashboardHtml(): string {
63
63
  grid-template-columns: 300px minmax(360px, 1fr) 330px;
64
64
  gap: 14px;
65
65
  width: min(1540px, calc(100vw - 28px));
66
- min-height: calc(100vh - 28px);
66
+ height: calc(100vh - 28px);
67
67
  margin: 14px auto;
68
+ overflow: hidden;
68
69
  }
69
70
  .panel {
70
71
  min-width: 0;
@@ -115,7 +116,7 @@ export function dashboardHtml(): string {
115
116
  }
116
117
  .pill.good { color: var(--active); border-color: #a7d7c4; background: var(--active-soft); }
117
118
  .pill.blue { color: var(--blue); border-color: #b7c8f6; background: var(--blue-soft); }
118
- .chat { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
119
+ .chat { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; min-height: 0; }
119
120
  .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 74px; min-width: 0; background: var(--panel); }
120
121
  .panel-head > div { min-width: 0; }
121
122
  .panel-head h2 { margin: 0; font-size: 22px; letter-spacing: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
@@ -138,6 +139,94 @@ export function dashboardHtml(): string {
138
139
  .split { display: grid; grid-template-columns: minmax(0, 1fr) 120px; gap: 8px; }
139
140
  .toolbox { display: grid; gap: 8px; background: var(--panel-2); }
140
141
  .section-title { margin: 0; color: var(--muted); font-size: 12px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }
142
+ .field { display: grid; gap: 5px; }
143
+ .field span { color: var(--muted); font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; }
144
+ .settings-trigger { white-space: nowrap; }
145
+ .settings-backdrop {
146
+ position: fixed;
147
+ inset: 0;
148
+ z-index: 40;
149
+ display: none;
150
+ padding: 18px;
151
+ background: rgba(24, 23, 20, .28);
152
+ }
153
+ .settings-backdrop.open { display: grid; place-items: center; }
154
+ .settings-dialog {
155
+ display: grid;
156
+ grid-template-rows: auto 1fr;
157
+ width: min(1120px, 100%);
158
+ max-height: min(860px, calc(100vh - 36px));
159
+ border: 1px solid var(--ink);
160
+ background: var(--panel);
161
+ box-shadow: var(--shadow);
162
+ overflow: hidden;
163
+ }
164
+ .settings-head {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ gap: 14px;
168
+ padding: 16px;
169
+ border-bottom: 1px solid var(--line);
170
+ background: #fffdf8;
171
+ }
172
+ .settings-head h2 { margin: 0; font-size: 24px; letter-spacing: 0; }
173
+ .settings-body {
174
+ display: grid;
175
+ grid-template-columns: 210px minmax(0, 1fr);
176
+ min-height: 0;
177
+ overflow: hidden;
178
+ }
179
+ .settings-nav {
180
+ display: grid;
181
+ align-content: start;
182
+ gap: 8px;
183
+ padding: 14px;
184
+ border-right: 1px solid var(--line);
185
+ background: var(--panel-2);
186
+ overflow: auto;
187
+ }
188
+ .settings-tab {
189
+ width: 100%;
190
+ background: transparent;
191
+ color: var(--ink);
192
+ border-color: var(--line);
193
+ text-align: left;
194
+ }
195
+ .settings-tab.active { background: var(--ink); color: #fffaf1; border-color: var(--ink); }
196
+ .settings-content { overflow: auto; padding: 16px; }
197
+ .settings-pane { display: none; gap: 12px; }
198
+ .settings-pane.active { display: grid; }
199
+ .settings-section-head {
200
+ display: flex;
201
+ align-items: start;
202
+ justify-content: space-between;
203
+ gap: 12px;
204
+ padding-bottom: 10px;
205
+ border-bottom: 1px solid var(--line);
206
+ }
207
+ .settings-section-head h3 { margin: 0; font-size: 18px; letter-spacing: 0; }
208
+ .settings-section-head .meta { max-width: 620px; }
209
+ .setup-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
210
+ .setup-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: #fffdf8; }
211
+ .setup-panel.collapsed { display: none; }
212
+ .setup-panel .section-title { color: var(--ink); }
213
+ .setup-actions { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
214
+ .summary-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: var(--active-soft); }
215
+ .settings-list { display: grid; gap: 8px; }
216
+ .settings-row {
217
+ display: grid;
218
+ grid-template-columns: minmax(0, 1fr) auto;
219
+ gap: 10px;
220
+ align-items: start;
221
+ padding: 11px;
222
+ border: 1px solid var(--line);
223
+ background: #fffdf8;
224
+ }
225
+ .settings-row strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
226
+ .settings-row .meta { overflow-wrap: anywhere; word-break: break-word; }
227
+ .command-wrap { display: grid; gap: 7px; }
228
+ .command-wrap textarea { font-family: var(--mono); font-size: 11px; min-height: 104px; }
229
+ .secret-note { color: var(--faint); font-size: 11px; line-height: 1.4; }
141
230
  .empty, .error, .readonly-note { border: 1px dashed var(--line); padding: 18px; text-align: center; background: rgba(255, 253, 248, .65); }
142
231
  .error { display: none; color: var(--danger); border-color: rgba(163, 58, 43, .35); background: #fff1ee; }
143
232
  .readonly-note { color: var(--muted); line-height: 1.5; }
@@ -188,12 +277,14 @@ export function dashboardHtml(): string {
188
277
  .app { grid-template-columns: 280px minmax(0, 1fr); }
189
278
  .inspector { grid-column: 1 / -1; grid-template-rows: auto auto; }
190
279
  .inspector .agents { max-height: 320px; }
280
+ .settings-body, .setup-grid, .settings-row { grid-template-columns: 1fr; }
281
+ .settings-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); border-right: 0; border-bottom: 1px solid var(--line); }
191
282
  }
192
283
  @media (max-width: 760px) {
193
- .app { width: 100%; min-height: 100vh; margin: 0; grid-template-columns: 1fr; }
284
+ .app { width: 100%; height: 100vh; margin: 0; grid-template-columns: 1fr; overflow: auto; }
194
285
  .sidebar, .inspector { max-height: none; }
195
- .chat { min-height: 68vh; }
196
- .composer-controls, .split, .form-row { grid-template-columns: 1fr; }
286
+ .chat { height: 100vh; min-height: 0; }
287
+ .composer-controls, .split, .form-row, .setup-actions { grid-template-columns: 1fr; }
197
288
  .message { max-width: 100%; }
198
289
  }
199
290
  </style>
@@ -246,6 +337,7 @@ export function dashboardHtml(): string {
246
337
  <h2>Agents</h2>
247
338
  <div class="meta">Use runtime=codex for executable demo agents.</div>
248
339
  </div>
340
+ <button id="open-settings" class="secondary settings-trigger" type="button">Settings</button>
249
341
  </header>
250
342
  <div class="toolbox">
251
343
  <form id="invite-form" class="split">
@@ -259,29 +351,135 @@ export function dashboardHtml(): string {
259
351
  </select>
260
352
  <button>Invite</button>
261
353
  </form>
262
- <form id="agent-form" class="create-agent">
263
- <p class="section-title">Create or Update Agent</p>
264
- <input id="agent-key" placeholder="codex" autocomplete="off" required>
265
- <input id="agent-name" placeholder="Codex" autocomplete="off" required>
266
- <input id="agent-runtime" placeholder="codex" autocomplete="off">
267
- <input id="agent-computer" placeholder="machine id" autocomplete="off">
268
- <button>Create agent</button>
269
- </form>
270
- <form id="computer-form" class="create-agent">
271
- <p class="section-title">Provision Computer</p>
272
- <input id="computer-name" placeholder="Bill's Team" autocomplete="off">
273
- <button>Generate command</button>
274
- <textarea id="computer-command" readonly rows="4" placeholder="Daemon command"></textarea>
275
- </form>
276
354
  <div id="members" class="members"></div>
277
355
  </div>
278
356
  <div id="agents" class="agents"></div>
279
357
  </aside>
280
358
  </main>
359
+ <section id="settings-backdrop" class="settings-backdrop" aria-hidden="true">
360
+ <div class="settings-dialog" role="dialog" aria-modal="true" aria-labelledby="settings-title">
361
+ <header class="settings-head">
362
+ <div>
363
+ <h2 id="settings-title">Settings</h2>
364
+ <div class="meta">Configure access, agents, computers, and Feishu/Lark from one place.</div>
365
+ </div>
366
+ <button id="close-settings" class="secondary icon" type="button" aria-label="Close settings">×</button>
367
+ </header>
368
+ <div class="settings-body">
369
+ <nav class="settings-nav" aria-label="Settings sections">
370
+ <button class="settings-tab active" type="button" data-settings-tab="access">Access</button>
371
+ <button class="settings-tab" type="button" data-settings-tab="agents">Agents</button>
372
+ <button class="settings-tab" type="button" data-settings-tab="computers">Computers</button>
373
+ <button class="settings-tab" type="button" data-settings-tab="lark">Lark</button>
374
+ </nav>
375
+ <div class="settings-content">
376
+ <section id="settings-access" class="settings-pane active">
377
+ <div class="summary-panel">
378
+ <p class="section-title">Server Access</p>
379
+ <div id="server-access" class="meta">Listening locally. Tailscale address is detected on load.</div>
380
+ </div>
381
+ <div class="settings-section-head">
382
+ <div>
383
+ <h3>Lark Users</h3>
384
+ <div class="meta">Only listed Lark user IDs may trigger inbound bot handling.</div>
385
+ </div>
386
+ <button class="secondary" type="button" data-add-panel="lark-user-form">Add User</button>
387
+ </div>
388
+ <div class="setup-grid">
389
+ <div id="settings-lark-user-list" class="settings-list"></div>
390
+ <form id="lark-user-form" class="setup-panel collapsed">
391
+ <p class="section-title">Authorized Lark User</p>
392
+ <label class="field"><span>User ID</span><input id="lark-user-id" placeholder="on_xxx union id" autocomplete="off" required></label>
393
+ <label class="field"><span>Display name</span><input id="lark-user-name" placeholder="Optional" autocomplete="off"></label>
394
+ <div class="setup-actions">
395
+ <button>Save user</button>
396
+ <button class="secondary" type="button" data-cancel-panel="lark-user-form">Cancel</button>
397
+ </div>
398
+ </form>
399
+ </div>
400
+ </section>
401
+ <section id="settings-agents" class="settings-pane">
402
+ <div class="settings-section-head">
403
+ <div>
404
+ <h3>Agents</h3>
405
+ <div class="meta">Manage logical agents and assign them to an available computer.</div>
406
+ </div>
407
+ <button class="secondary" type="button" data-add-panel="agent-form">Add Agent</button>
408
+ </div>
409
+ <div class="setup-grid">
410
+ <div id="settings-agent-list" class="settings-list"></div>
411
+ <form id="agent-form" class="setup-panel collapsed">
412
+ <p class="section-title">Agent Onboard</p>
413
+ <label class="field"><span>Key</span><input id="agent-key" placeholder="codex" autocomplete="off" required></label>
414
+ <label class="field"><span>Name</span><input id="agent-name" placeholder="Codex" autocomplete="off" required></label>
415
+ <label class="field"><span>Runtime</span><select id="agent-runtime"><option value="codex">codex</option><option value="neeko">neeko</option><option value="coco">coco</option><option value="coco-stream-json">coco-stream-json</option></select></label>
416
+ <label class="field"><span>Computer</span><select id="agent-computer"><option value="">No assignment</option></select></label>
417
+ <label class="field"><span>Description</span><input id="agent-desc" placeholder="Optional" autocomplete="off"></label>
418
+ <div class="setup-actions">
419
+ <button>Onboard agent</button>
420
+ <button class="secondary" type="button" data-cancel-panel="agent-form">Cancel</button>
421
+ </div>
422
+ </form>
423
+ </div>
424
+ </section>
425
+ <section id="settings-computers" class="settings-pane">
426
+ <div class="settings-section-head">
427
+ <div>
428
+ <h3>Computers</h3>
429
+ <div class="meta">Provision daemon credentials and see connected machines.</div>
430
+ </div>
431
+ <button class="secondary" type="button" data-add-panel="computer-form">Add Computer</button>
432
+ </div>
433
+ <div class="setup-grid">
434
+ <div id="settings-computer-list" class="settings-list"></div>
435
+ <form id="computer-form" class="setup-panel collapsed">
436
+ <p class="section-title">Computer Onboard</p>
437
+ <label class="field"><span>Name</span><input id="computer-name" placeholder="Local computer" autocomplete="off"></label>
438
+ <label class="field"><span>Server URL</span><input id="computer-server" autocomplete="off"></label>
439
+ <label class="field"><span>Daemon package</span><input id="computer-package" value="@controlflow-ai/daemon@latest" autocomplete="off"></label>
440
+ <div class="setup-actions">
441
+ <button>Generate command</button>
442
+ <button id="copy-command" class="secondary" type="button">Copy</button>
443
+ </div>
444
+ <button class="secondary" type="button" data-cancel-panel="computer-form">Cancel</button>
445
+ <div class="command-wrap">
446
+ <textarea id="computer-command" readonly rows="4" placeholder="Daemon command"></textarea>
447
+ </div>
448
+ </form>
449
+ </div>
450
+ </section>
451
+ <section id="settings-lark" class="settings-pane">
452
+ <div class="settings-section-head">
453
+ <div>
454
+ <h3>Lark</h3>
455
+ <div class="meta">Bind Feishu/Lark bot credentials to a Pal agent. Secrets stay in the local runtime profile.</div>
456
+ </div>
457
+ <button class="secondary" type="button" data-add-panel="lark-form">Add Lark Bot</button>
458
+ </div>
459
+ <div class="setup-grid">
460
+ <div id="settings-lark-list" class="settings-list"></div>
461
+ <form id="lark-form" class="setup-panel collapsed">
462
+ <p class="section-title">Lark Setup</p>
463
+ <label class="field"><span>Bind agent</span><select id="lark-agent"></select></label>
464
+ <label class="field"><span>Label</span><input id="lark-label" placeholder="Team bot" autocomplete="off"></label>
465
+ <label class="field"><span>App ID</span><input id="lark-app-id" placeholder="cli_xxx" autocomplete="off" required></label>
466
+ <label class="field"><span>App Secret</span><input id="lark-app-secret" type="password" autocomplete="off" required></label>
467
+ <div class="secret-note">Secret is sent only to this local Pal server, validated with Feishu, and stored in the local Lark config file.</div>
468
+ <div class="setup-actions">
469
+ <button>Save Lark bot</button>
470
+ <button class="secondary" type="button" data-cancel-panel="lark-form">Cancel</button>
471
+ </div>
472
+ </form>
473
+ </div>
474
+ </section>
475
+ </div>
476
+ </div>
477
+ </div>
478
+ </section>
281
479
  <div id="error" class="error" role="alert"></div>
282
480
  <div id="toast" class="toast" role="status"></div>
283
481
  <script>
284
- const state = { rooms: [], agents: [], members: [], mentionables: [], messages: [], selectedRoomId: null, mentionIndex: 0 };
482
+ const state = { rooms: [], agents: [], computers: [], lark: null, larkUsers: [], serverAccess: null, members: [], mentionables: [], messages: [], selectedRoomId: null, mentionIndex: 0 };
285
483
  const root = (id) => document.getElementById(id);
286
484
  const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', "'":'&#39;', '"':'&quot;' }[char]));
287
485
  async function api(path, options) {
@@ -358,11 +556,58 @@ export function dashboardHtml(): string {
358
556
  }
359
557
  function renderAgents() {
360
558
  root('invite-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">No agents</option>';
559
+ root('lark-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">Onboard an agent first</option>';
361
560
  root('agents').innerHTML = state.agents.length ? state.agents.map((agent) => (
362
561
  '<article class="agent"><div class="agent-title"><span>' + escapeHtml(agent.display_name) + '</span>' + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div>' +
363
562
  '<div class="pills">' + pill(agent.agent_key) + pill(agent.id) + '</div>' +
364
563
  '<div class="meta">' + escapeHtml(agent.description || 'No description') + '</div></article>'
365
564
  )).join('') : empty('No agents yet. Create codex to start.');
565
+ root('settings-agent-list').innerHTML = state.agents.length
566
+ ? state.agents.map((agent) => '<article class="settings-row"><div><strong>' + escapeHtml(agent.display_name) + '</strong><div class="meta">' + escapeHtml(agent.description || 'No description') + '</div><div class="pills">' + pill(agent.agent_key, 'blue') + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div></div></article>').join('')
567
+ : empty('No agents yet.');
568
+ }
569
+ function renderComputers() {
570
+ root('agent-computer').innerHTML = '<option value="">No assignment</option>' + state.computers.map((computer) => '<option value="' + escapeHtml(computer.id) + '">' + escapeHtml(computer.name) + ' · ' + escapeHtml(computer.id) + '</option>').join('');
571
+ root('settings-computer-list').innerHTML = state.computers.length
572
+ ? state.computers.map((computer) => '<article class="settings-row"><div><strong>' + escapeHtml(computer.name) + '</strong><div class="meta">' + escapeHtml(computer.id) + '</div><div class="pills">' + pill(computer.status, computer.status === 'online' ? 'good' : '') + (computer.last_seen_at ? pill('last seen ' + computer.last_seen_at) : '') + '</div></div></article>').join('')
573
+ : empty('No computers yet.');
574
+ }
575
+ function renderLarkConfig() {
576
+ const el = root('settings-lark-list');
577
+ if (!el) return;
578
+ const bots = state.lark?.bots || [];
579
+ el.innerHTML = bots.length
580
+ ? bots.map((bot) => '<article class="settings-row"><div><strong>' + escapeHtml(bot.label || bot.appId) + '</strong><div class="meta">' + escapeHtml(bot.appId) + '</div><div class="pills">' + pill('@' + (bot.agent || '-'), 'blue') + pill(bot.botOpenId ? 'open_id resolved' : 'open_id missing', bot.botOpenId ? 'good' : '') + pill(bot.hasSecret ? 'secret stored' : 'secret missing') + '</div></div></article>').join('') + '<div class="meta">Config: ' + escapeHtml(state.lark.path || '') + '</div>'
581
+ : empty('No Lark bots configured yet.');
582
+ }
583
+ function renderLarkUsers() {
584
+ const el = root('settings-lark-user-list');
585
+ if (!el) return;
586
+ el.innerHTML = state.larkUsers.length
587
+ ? state.larkUsers.map((user) => '<article class="settings-row"><div><strong>' + escapeHtml(user.display_name || user.user_id) + '</strong><div class="meta">' + escapeHtml(user.user_id) + '</div><div class="pills">' + pill('authorized', 'good') + '</div></div><button class="secondary" type="button" data-delete-lark-user="' + escapeHtml(user.user_id) + '">Delete</button></article>').join('')
588
+ : empty('No authorized Lark users. Inbound bot messages will be ignored.');
589
+ [...el.querySelectorAll('[data-delete-lark-user]')].forEach((button) => {
590
+ button.addEventListener('click', async () => {
591
+ const userId = button.dataset.deleteLarkUser;
592
+ if (!userId) return;
593
+ const deleted = await api('/api/lark/authorized-users/' + encodeURIComponent(userId), { method: 'DELETE' }).catch((error) => { showError(error); return null; });
594
+ if (!deleted) return;
595
+ await loadLarkUsers();
596
+ showToast('Lark user removed');
597
+ });
598
+ });
599
+ }
600
+ function renderServerAccess() {
601
+ const access = state.serverAccess;
602
+ const el = root('server-access');
603
+ if (!el) return;
604
+ if (!access) {
605
+ el.textContent = 'Listening locally. Tailscale address is detected on load.';
606
+ return;
607
+ }
608
+ el.textContent = access.tailscaleUrl
609
+ ? 'Listening on local ' + access.localUrl + ' and Tailscale ' + access.tailscaleUrl
610
+ : 'Listening on local ' + access.localUrl + '. No Tailscale interface detected.';
366
611
  }
367
612
  function renderMembers() {
368
613
  root('members').innerHTML = state.members.length ? state.members.map((member) => (
@@ -370,6 +615,25 @@ export function dashboardHtml(): string {
370
615
  '<div class="pills">' + pill(member.source) + pill(member.status) + '</div></article>'
371
616
  )).join('') : empty('No known members in this room.');
372
617
  }
618
+ function openSettings(tab) {
619
+ root('settings-backdrop').classList.add('open');
620
+ root('settings-backdrop').setAttribute('aria-hidden', 'false');
621
+ selectSettingsTab(tab || 'access');
622
+ }
623
+ function closeSettings() {
624
+ root('settings-backdrop').classList.remove('open');
625
+ root('settings-backdrop').setAttribute('aria-hidden', 'true');
626
+ }
627
+ function selectSettingsTab(tab) {
628
+ document.querySelectorAll('.settings-tab').forEach((button) => button.classList.toggle('active', button.dataset.settingsTab === tab));
629
+ document.querySelectorAll('.settings-pane').forEach((pane) => pane.classList.toggle('active', pane.id === 'settings-' + tab));
630
+ }
631
+ function showPanel(id) {
632
+ root(id)?.classList.remove('collapsed');
633
+ }
634
+ function hidePanel(id) {
635
+ root(id)?.classList.add('collapsed');
636
+ }
373
637
  function currentMentionQuery() {
374
638
  const input = root('message-content');
375
639
  if (!input) return null;
@@ -461,6 +725,26 @@ export function dashboardHtml(): string {
461
725
  state.agents = data.agents || [];
462
726
  renderAgents();
463
727
  }
728
+ async function loadComputers() {
729
+ const data = await api('/api/computers');
730
+ state.computers = data.computers || [];
731
+ renderComputers();
732
+ }
733
+ async function loadLarkConfig() {
734
+ const data = await api('/api/lark/config');
735
+ state.lark = data;
736
+ renderLarkConfig();
737
+ }
738
+ async function loadLarkUsers() {
739
+ const data = await api('/api/lark/authorized-users');
740
+ state.larkUsers = data.users || [];
741
+ renderLarkUsers();
742
+ }
743
+ async function loadServerAccess() {
744
+ const data = await api('/api/server/access');
745
+ state.serverAccess = data;
746
+ renderServerAccess();
747
+ }
464
748
  async function loadMessages() {
465
749
  const room = activeRoom();
466
750
  if (!room) {
@@ -483,7 +767,7 @@ export function dashboardHtml(): string {
483
767
  renderMembers();
484
768
  }
485
769
  async function refresh() {
486
- await Promise.all([loadRooms(), loadAgents()]);
770
+ await Promise.all([loadRooms(), loadAgents(), loadComputers(), loadLarkConfig(), loadLarkUsers(), loadServerAccess()]);
487
771
  await loadMessages();
488
772
  }
489
773
  window.selectRoom = async (id) => {
@@ -492,6 +776,23 @@ export function dashboardHtml(): string {
492
776
  await loadMessages().catch(showError);
493
777
  };
494
778
  root('refresh').addEventListener('click', () => refresh().catch(showError));
779
+ root('open-settings').addEventListener('click', () => openSettings('access'));
780
+ root('close-settings').addEventListener('click', closeSettings);
781
+ root('settings-backdrop').addEventListener('mousedown', (event) => {
782
+ if (event.target === root('settings-backdrop')) closeSettings();
783
+ });
784
+ document.addEventListener('keydown', (event) => {
785
+ if (event.key === 'Escape' && root('settings-backdrop').classList.contains('open')) closeSettings();
786
+ });
787
+ document.querySelectorAll('.settings-tab').forEach((button) => {
788
+ button.addEventListener('click', () => selectSettingsTab(button.dataset.settingsTab));
789
+ });
790
+ document.querySelectorAll('[data-add-panel]').forEach((button) => {
791
+ button.addEventListener('click', () => showPanel(button.dataset.addPanel));
792
+ });
793
+ document.querySelectorAll('[data-cancel-panel]').forEach((button) => {
794
+ button.addEventListener('click', () => hidePanel(button.dataset.cancelPanel));
795
+ });
495
796
  root('room-form').addEventListener('submit', async (event) => {
496
797
  event.preventDefault();
497
798
  const name = root('room-name').value.trim();
@@ -507,29 +808,67 @@ export function dashboardHtml(): string {
507
808
  event.preventDefault();
508
809
  const agent_key = root('agent-key').value.trim();
509
810
  const display_name = root('agent-name').value.trim();
510
- const runtime = root('agent-runtime').value.trim() || null;
511
- const computer_id = root('agent-computer').value.trim();
811
+ const runtime = root('agent-runtime').value.trim() || 'codex';
812
+ const computer_id = root('agent-computer').value.trim() || undefined;
813
+ const description = root('agent-desc').value.trim() || null;
512
814
  if (!agent_key || !display_name) return;
513
- const saved = await api('/api/agents', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent_key, display_name, runtime }) }).catch((error) => { showError(error); return null; });
815
+ const saved = await api('/api/agents/onboard', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent_key, display_name, runtime, description, computer_id }) }).catch((error) => { showError(error); return null; });
514
816
  if (!saved) return;
515
- if (computer_id) {
516
- await api('/api/agents/' + encodeURIComponent(agent_key) + '/assignment', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ computer_id }) }).catch((error) => { showError(error); return null; });
517
- }
518
817
  root('agent-key').value = '';
519
818
  root('agent-name').value = '';
520
- root('agent-runtime').value = '';
819
+ root('agent-runtime').value = 'codex';
521
820
  root('agent-computer').value = '';
821
+ root('agent-desc').value = '';
522
822
  await loadAgents();
823
+ hidePanel('agent-form');
523
824
  showToast('Agent saved: @' + agent_key);
524
825
  });
525
826
  root('computer-form').addEventListener('submit', async (event) => {
526
827
  event.preventDefault();
527
828
  const name = root('computer-name').value.trim();
528
- const data = await api('/api/computers/provision', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: name || undefined, server_url: location.origin }) }).catch((error) => { showError(error); return null; });
829
+ const server_url = root('computer-server').value.trim() || location.origin;
830
+ const package_name = root('computer-package').value.trim() || undefined;
831
+ const data = await api('/api/computers/provision', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: name || undefined, server_url, package_name }) }).catch((error) => { showError(error); return null; });
529
832
  if (!data) return;
530
833
  root('computer-command').value = data.command;
834
+ await loadComputers();
531
835
  showToast('Computer provisioned: ' + data.computer.id);
532
836
  });
837
+ root('copy-command').addEventListener('click', async () => {
838
+ const command = root('computer-command').value;
839
+ if (!command) return;
840
+ await navigator.clipboard?.writeText(command).catch(() => null);
841
+ showToast('Daemon command copied');
842
+ });
843
+ root('computer-server').value = location.origin;
844
+ root('lark-user-form').addEventListener('submit', async (event) => {
845
+ event.preventDefault();
846
+ const user_id = root('lark-user-id').value.trim();
847
+ const display_name = root('lark-user-name').value.trim() || null;
848
+ if (!user_id) return;
849
+ const saved = await api('/api/lark/authorized-users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ user_id, display_name }) }).catch((error) => { showError(error); return null; });
850
+ if (!saved) return;
851
+ root('lark-user-id').value = '';
852
+ root('lark-user-name').value = '';
853
+ await loadLarkUsers();
854
+ hidePanel('lark-user-form');
855
+ showToast('Lark user authorized');
856
+ });
857
+ root('lark-form').addEventListener('submit', async (event) => {
858
+ event.preventDefault();
859
+ const agent = root('lark-agent').value;
860
+ const app_id = root('lark-app-id').value.trim();
861
+ const app_secret = root('lark-app-secret').value.trim();
862
+ const label = root('lark-label').value.trim() || undefined;
863
+ if (!agent || !app_id || !app_secret) return;
864
+ const data = await api('/api/lark/setup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent, app_id, app_secret, label }) }).catch((error) => { showError(error); return null; });
865
+ if (!data) return;
866
+ root('lark-app-secret').value = '';
867
+ await loadLarkConfig();
868
+ const reload = await api('/api/lark/reload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }).catch((error) => ({ reloadError: error.message }));
869
+ if (!reload.reloadError) hidePanel('lark-form');
870
+ showToast(reload.reloadError ? 'Lark saved, reload failed: ' + reload.reloadError : 'Lark bot saved and reloaded: ' + data.appId);
871
+ });
533
872
  root('invite-form').addEventListener('submit', async (event) => {
534
873
  event.preventDefault();
535
874
  const room = activeRoom();