a2acalling 0.6.57 → 0.6.59
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/bin/cli.js +9 -12
- package/docs/plans/2026-02-18-a2a-41-permissions-tab.md +1273 -0
- package/docs/plans/2026-02-18-dashboard-backlog-blitz.md +585 -0
- package/native/macos/src-tauri/src/lib.rs +4 -4
- package/package.json +1 -1
- package/src/dashboard/public/app.js +811 -197
- package/src/dashboard/public/index.html +209 -205
- package/src/dashboard/public/style.css +294 -103
- package/src/lib/config.js +5 -15
- package/src/lib/tokens.js +3 -3
- package/src/routes/a2a.js +0 -1
- package/src/routes/dashboard.js +0 -3
- package/.claude/commands/a2a-call.md +0 -26
- package/.claude/commands/a2a-contacts.md +0 -31
- package/.claude/commands/a2a-invite.md +0 -33
- package/.claude/commands/a2a-setup.md +0 -30
- package/.claude/commands/a2a-status.md +0 -24
|
@@ -214,13 +214,13 @@ function formatUpdaterState(stateValue) {
|
|
|
214
214
|
return state.replaceAll('_', ' ');
|
|
215
215
|
}
|
|
216
216
|
|
|
217
|
-
function
|
|
217
|
+
function badgeVariant(stateValue) {
|
|
218
218
|
const state = String(stateValue || '').trim();
|
|
219
|
-
if (state === 'failed') return '
|
|
219
|
+
if (state === 'failed') return 'danger';
|
|
220
220
|
if (state === 'waiting_for_safe_restart' || state === 'checking' || state === 'downloading' || state === 'applying' || state === 'restarting') {
|
|
221
|
-
return '
|
|
221
|
+
return 'warning';
|
|
222
222
|
}
|
|
223
|
-
return '
|
|
223
|
+
return 'success';
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
async function copyText(value) {
|
|
@@ -250,36 +250,36 @@ async function copyText(value) {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
function bindTabs() {
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
if (!target) return false;
|
|
256
|
-
const btn = Array.from(document.querySelectorAll('.tab')).find(b => b.dataset.tab === target);
|
|
257
|
-
const panel = document.getElementById(`tab-${target}`);
|
|
258
|
-
if (!btn || !panel) return false;
|
|
259
|
-
|
|
260
|
-
document.querySelectorAll('.tab').forEach(b => b.classList.remove('is-active'));
|
|
261
|
-
document.querySelectorAll('.panel').forEach(p => p.classList.remove('is-active'));
|
|
262
|
-
btn.classList.add('is-active');
|
|
263
|
-
panel.classList.add('is-active');
|
|
264
|
-
|
|
265
|
-
if (options.updateHash) {
|
|
266
|
-
try { window.location.hash = target; } catch (err) {}
|
|
267
|
-
}
|
|
268
|
-
return true;
|
|
269
|
-
};
|
|
253
|
+
const tabGroup = document.getElementById('main-tabs');
|
|
254
|
+
if (!tabGroup) return;
|
|
270
255
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
256
|
+
tabGroup.addEventListener('sl-tab-show', (e) => {
|
|
257
|
+
const tabName = e.detail.name;
|
|
258
|
+
try { window.location.hash = tabName; } catch (err) {}
|
|
259
|
+
if (typeof onTabSwitch === 'function') onTabSwitch(tabName);
|
|
275
260
|
});
|
|
276
261
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
262
|
+
// Deep-link support: activate the tab matching the URL hash
|
|
263
|
+
const activateFromHash = () => {
|
|
264
|
+
let hash = window.location.hash.slice(1);
|
|
265
|
+
// A2A-41: backward-compat alias — old bookmarks/links using #settings
|
|
266
|
+
// still work after rename to #permissions
|
|
267
|
+
if (hash === 'settings') hash = 'permissions';
|
|
268
|
+
if (hash) {
|
|
269
|
+
// Use try/catch in case the tab group isn't fully ready
|
|
270
|
+
try { tabGroup.show(hash); } catch (err) {}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
280
273
|
|
|
281
|
-
|
|
282
|
-
|
|
274
|
+
window.addEventListener('hashchange', activateFromHash);
|
|
275
|
+
|
|
276
|
+
// On initial load, activate from hash (wait for Shoelace to be ready)
|
|
277
|
+
if (tabGroup.updateComplete) {
|
|
278
|
+
tabGroup.updateComplete.then(activateFromHash);
|
|
279
|
+
} else {
|
|
280
|
+
// Fallback: try after a short delay
|
|
281
|
+
setTimeout(activateFromHash, 100);
|
|
282
|
+
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
function norm(value) {
|
|
@@ -307,6 +307,35 @@ function contactLabel(contact) {
|
|
|
307
307
|
return String(contact?.name || '').trim() || String(contact?.host || '').trim() || '-';
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
function getPinnedContacts() {
|
|
311
|
+
try {
|
|
312
|
+
const raw = localStorage.getItem('a2a-pinned-contacts');
|
|
313
|
+
if (!raw) return [];
|
|
314
|
+
const parsed = JSON.parse(raw);
|
|
315
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
316
|
+
} catch (err) {
|
|
317
|
+
return [];
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function togglePin(contactId) {
|
|
322
|
+
const id = String(contactId || '');
|
|
323
|
+
if (!id) return;
|
|
324
|
+
const pinned = getPinnedContacts();
|
|
325
|
+
const index = pinned.indexOf(id);
|
|
326
|
+
if (index >= 0) {
|
|
327
|
+
pinned.splice(index, 1);
|
|
328
|
+
} else {
|
|
329
|
+
pinned.push(id);
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
localStorage.setItem('a2a-pinned-contacts', JSON.stringify(pinned));
|
|
333
|
+
} catch (err) {
|
|
334
|
+
// localStorage may be unavailable
|
|
335
|
+
}
|
|
336
|
+
renderContacts();
|
|
337
|
+
}
|
|
338
|
+
|
|
310
339
|
function renderContacts() {
|
|
311
340
|
const el = document.getElementById('contacts-sections');
|
|
312
341
|
if (!el) return;
|
|
@@ -318,30 +347,18 @@ function renderContacts() {
|
|
|
318
347
|
.filter(c => isMine(c))
|
|
319
348
|
.sort((a, b) => contactLabel(a).localeCompare(contactLabel(b)));
|
|
320
349
|
|
|
350
|
+
const pinnedIds = getPinnedContacts();
|
|
321
351
|
const lastCalled = contacts
|
|
322
|
-
.filter(c => c && c.last_call_at)
|
|
323
|
-
.sort((a, b) =>
|
|
352
|
+
.filter(c => c && c.last_call_at && !isMine(c))
|
|
353
|
+
.sort((a, b) => {
|
|
354
|
+
const aPinned = pinnedIds.includes(String(a.id));
|
|
355
|
+
const bPinned = pinnedIds.includes(String(b.id));
|
|
356
|
+
if (aPinned && !bPinned) return -1;
|
|
357
|
+
if (!aPinned && bPinned) return 1;
|
|
358
|
+
return String(b.last_call_at || '').localeCompare(String(a.last_call_at || ''));
|
|
359
|
+
})
|
|
324
360
|
.slice(0, 12);
|
|
325
361
|
|
|
326
|
-
const groups = new Map();
|
|
327
|
-
for (const c of contacts) {
|
|
328
|
-
const owner = String(c?.owner || '').trim() || '(unknown owner)';
|
|
329
|
-
if (!groups.has(owner)) groups.set(owner, []);
|
|
330
|
-
groups.get(owner).push(c);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
const owners = Array.from(groups.keys()).sort((a, b) => {
|
|
334
|
-
// Keep local owner group near the top (after the dedicated "My agents" section).
|
|
335
|
-
const local = norm(getLocalOwnerName());
|
|
336
|
-
const aIsLocal = local && norm(a) === local;
|
|
337
|
-
const bIsLocal = local && norm(b) === local;
|
|
338
|
-
if (aIsLocal && !bIsLocal) return -1;
|
|
339
|
-
if (!aIsLocal && bIsLocal) return 1;
|
|
340
|
-
if (a === '(unknown owner)' && b !== '(unknown owner)') return 1;
|
|
341
|
-
if (a !== '(unknown owner)' && b === '(unknown owner)') return -1;
|
|
342
|
-
return a.localeCompare(b);
|
|
343
|
-
});
|
|
344
|
-
|
|
345
362
|
const rowHtml = (c, opts = {}) => {
|
|
346
363
|
const canCall = Boolean(c?.can_call);
|
|
347
364
|
const mine = Boolean(c?.is_mine);
|
|
@@ -350,13 +367,17 @@ function renderContacts() {
|
|
|
350
367
|
const lastCallAt = c?.last_call_at ? fmtDate(c.last_call_at) : '-';
|
|
351
368
|
const calls = Number.isFinite(c?.call_count) ? c.call_count : (c?.call_count || 0);
|
|
352
369
|
const isSelected = selected && String(c?.id) === selected;
|
|
370
|
+
const isPinned = pinnedIds.includes(String(c?.id));
|
|
353
371
|
|
|
354
372
|
const actionBits = [];
|
|
373
|
+
if (opts.showPin) {
|
|
374
|
+
actionBits.push(`<sl-icon-button name="${isPinned ? 'pin-fill' : 'pin'}" class="pin-btn${isPinned ? ' pinned' : ''}" data-pin-contact="${esc(c.id)}" title="${isPinned ? 'Unpin' : 'Pin to top'}"></sl-icon-button>`);
|
|
375
|
+
}
|
|
355
376
|
if (c?.last_call_id) {
|
|
356
|
-
actionBits.push(`<button data-open-call="${esc(c.last_call_id)}"
|
|
377
|
+
actionBits.push(`<sl-button size="small" data-open-call="${esc(c.last_call_id)}">Transcript</sl-button>`);
|
|
357
378
|
}
|
|
358
|
-
actionBits.push(`<button data-toggle-mine="${esc(c.id)}"
|
|
359
|
-
actionBits.push(`<button data-remove-contact="${esc(c.id)}"
|
|
379
|
+
actionBits.push(`<sl-button size="small" data-toggle-mine="${esc(c.id)}">${mine ? 'Unmark mine' : 'Mark mine'}</sl-button>`);
|
|
380
|
+
actionBits.push(`<sl-button size="small" variant="danger" data-remove-contact="${esc(c.id)}">Remove</sl-button>`);
|
|
360
381
|
|
|
361
382
|
const locationCell = opts.showLocation ? `<td>${esc(formatLocation(c))}</td>` : '';
|
|
362
383
|
const ownerCell = opts.showOwner ? `<td>${esc(c?.owner || '-')}</td>` : '';
|
|
@@ -366,8 +387,8 @@ function renderContacts() {
|
|
|
366
387
|
<tr ${isSelected ? 'data-selected="1"' : ''}>
|
|
367
388
|
<td>
|
|
368
389
|
<div class="row" style="margin:0;">
|
|
369
|
-
<button
|
|
370
|
-
<button data-contact-call="${esc(c.id)}"
|
|
390
|
+
<sl-button variant="text" size="small" data-contact-select="${esc(c.id)}">${esc(contactLabel(c))}</sl-button>
|
|
391
|
+
<sl-button size="small" variant="primary" data-contact-call="${esc(c.id)}" ${canCall ? '' : 'disabled'}>Call</sl-button>
|
|
371
392
|
</div>
|
|
372
393
|
</td>
|
|
373
394
|
${locationCell}
|
|
@@ -400,30 +421,48 @@ function renderContacts() {
|
|
|
400
421
|
};
|
|
401
422
|
|
|
402
423
|
const myAgentsSection = `
|
|
403
|
-
<
|
|
424
|
+
<sl-card>
|
|
404
425
|
<h3>My agents</h3>
|
|
405
426
|
${tableHtml(myAgents, { showLocation: true, showOwner: false, showSummary: false })}
|
|
406
|
-
</
|
|
427
|
+
</sl-card>
|
|
407
428
|
`;
|
|
408
429
|
|
|
409
430
|
const lastCalledSection = `
|
|
410
|
-
<
|
|
431
|
+
<sl-card>
|
|
411
432
|
<h3>Last called agents</h3>
|
|
412
|
-
${tableHtml(lastCalled, { showLocation: false, showOwner: true, showSummary: false })}
|
|
413
|
-
</
|
|
433
|
+
${tableHtml(lastCalled, { showLocation: false, showOwner: true, showSummary: false, showPin: true })}
|
|
434
|
+
</sl-card>
|
|
414
435
|
`;
|
|
415
436
|
|
|
416
|
-
const
|
|
417
|
-
|
|
437
|
+
const otherContacts = contacts.filter(c => !isMine(c));
|
|
438
|
+
const otherGroups = new Map();
|
|
439
|
+
for (const c of otherContacts) {
|
|
440
|
+
const owner = String(c?.owner || '').trim() || '(unknown owner)';
|
|
441
|
+
if (!otherGroups.has(owner)) otherGroups.set(owner, []);
|
|
442
|
+
otherGroups.get(owner).push(c);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const otherOwners = Array.from(otherGroups.keys()).sort((a, b) => {
|
|
446
|
+
if (a === '(unknown owner)' && b !== '(unknown owner)') return 1;
|
|
447
|
+
if (a !== '(unknown owner)' && b === '(unknown owner)') return -1;
|
|
448
|
+
return a.localeCompare(b);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
const groupedSections = otherOwners.map(owner => {
|
|
452
|
+
const rows = (otherGroups.get(owner) || []).slice().sort((a, b) => contactLabel(a).localeCompare(contactLabel(b)));
|
|
418
453
|
return `
|
|
419
|
-
<
|
|
454
|
+
<sl-card>
|
|
420
455
|
<h3>${esc(owner)}</h3>
|
|
421
456
|
${tableHtml(rows, { showLocation: false, showOwner: false, showSummary: true })}
|
|
422
|
-
</
|
|
457
|
+
</sl-card>
|
|
423
458
|
`;
|
|
424
459
|
}).join('');
|
|
425
460
|
|
|
426
|
-
|
|
461
|
+
const otherAgentsHeading = otherOwners.length
|
|
462
|
+
? `<h3 style="margin-top:1rem;">Other Agents</h3>`
|
|
463
|
+
: '';
|
|
464
|
+
|
|
465
|
+
el.innerHTML = `${myAgentsSection}${lastCalledSection}${otherAgentsHeading}${groupedSections}`;
|
|
427
466
|
}
|
|
428
467
|
|
|
429
468
|
async function loadContacts() {
|
|
@@ -437,6 +476,15 @@ function bindContactsActions() {
|
|
|
437
476
|
const form = document.getElementById('add-contact-form');
|
|
438
477
|
if (!form) return;
|
|
439
478
|
|
|
479
|
+
// Cancel button collapses the sl-details
|
|
480
|
+
const cancelBtn = document.getElementById('add-contact-cancel');
|
|
481
|
+
const addDetails = document.getElementById('add-contact-details');
|
|
482
|
+
if (cancelBtn && addDetails) {
|
|
483
|
+
cancelBtn.addEventListener('click', () => {
|
|
484
|
+
addDetails.open = false;
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
440
488
|
const urlEl = document.getElementById('add-contact-url');
|
|
441
489
|
const mineEl = document.getElementById('add-contact-mine');
|
|
442
490
|
const serverNameEl = document.getElementById('add-contact-server-name');
|
|
@@ -449,9 +497,9 @@ function bindContactsActions() {
|
|
|
449
497
|
serverNameEl.value = match[1];
|
|
450
498
|
}
|
|
451
499
|
};
|
|
452
|
-
urlEl?.addEventListener('blur', defaultServerNameFromUrl);
|
|
453
|
-
urlEl?.addEventListener('change', defaultServerNameFromUrl);
|
|
454
|
-
mineEl?.addEventListener('change', () => {
|
|
500
|
+
urlEl?.addEventListener('sl-blur', defaultServerNameFromUrl);
|
|
501
|
+
urlEl?.addEventListener('sl-change', defaultServerNameFromUrl);
|
|
502
|
+
mineEl?.addEventListener('sl-change', () => {
|
|
455
503
|
if (!serverNameEl) return;
|
|
456
504
|
serverNameEl.disabled = !mineEl.checked;
|
|
457
505
|
if (mineEl.checked) {
|
|
@@ -467,7 +515,7 @@ function bindContactsActions() {
|
|
|
467
515
|
const url = document.getElementById('add-contact-url').value.trim();
|
|
468
516
|
const name = document.getElementById('add-contact-name').value.trim();
|
|
469
517
|
const owner = document.getElementById('add-contact-owner').value.trim();
|
|
470
|
-
const
|
|
518
|
+
const isMineVal = Boolean(document.getElementById('add-contact-mine')?.checked);
|
|
471
519
|
const serverName = document.getElementById('add-contact-server-name').value.trim();
|
|
472
520
|
const tagsRaw = document.getElementById('add-contact-tags').value.trim();
|
|
473
521
|
const notes = document.getElementById('add-contact-notes').value.trim();
|
|
@@ -497,7 +545,7 @@ function bindContactsActions() {
|
|
|
497
545
|
invite_url: url,
|
|
498
546
|
name: name || undefined,
|
|
499
547
|
owner: owner || undefined,
|
|
500
|
-
is_mine:
|
|
548
|
+
is_mine: isMineVal,
|
|
501
549
|
server_name: serverName || undefined,
|
|
502
550
|
tags,
|
|
503
551
|
notes: notes || undefined,
|
|
@@ -506,15 +554,26 @@ function bindContactsActions() {
|
|
|
506
554
|
});
|
|
507
555
|
showNotice('Contact added');
|
|
508
556
|
form.reset();
|
|
557
|
+
// Collapse the sl-details after successful add
|
|
558
|
+
if (addDetails) addDetails.open = false;
|
|
509
559
|
await loadContacts();
|
|
510
560
|
} catch (err) {
|
|
511
561
|
showNotice(err.message);
|
|
512
562
|
}
|
|
513
563
|
});
|
|
514
564
|
|
|
515
|
-
|
|
565
|
+
// Event delegation on the contacts tab panel
|
|
566
|
+
const panel = document.querySelector('sl-tab-panel[name="contacts"]');
|
|
516
567
|
panel?.addEventListener('click', async (e) => {
|
|
517
|
-
const
|
|
568
|
+
const pinBtn = e.target.closest('[data-pin-contact]');
|
|
569
|
+
if (pinBtn) {
|
|
570
|
+
e.preventDefault();
|
|
571
|
+
const id = pinBtn.dataset.pinContact;
|
|
572
|
+
if (id) togglePin(id);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const selectBtn = e.target.closest('[data-contact-select]');
|
|
518
577
|
if (selectBtn) {
|
|
519
578
|
e.preventDefault();
|
|
520
579
|
const id = selectBtn.dataset.contactSelect;
|
|
@@ -524,14 +583,14 @@ function bindContactsActions() {
|
|
|
524
583
|
return;
|
|
525
584
|
}
|
|
526
585
|
|
|
527
|
-
const openBtn = e.target.closest('
|
|
586
|
+
const openBtn = e.target.closest('[data-open-call]');
|
|
528
587
|
if (openBtn) {
|
|
529
588
|
e.preventDefault();
|
|
530
589
|
openCallTranscript(openBtn.dataset.openCall);
|
|
531
590
|
return;
|
|
532
591
|
}
|
|
533
592
|
|
|
534
|
-
const mineBtn = e.target.closest('
|
|
593
|
+
const mineBtn = e.target.closest('[data-toggle-mine]');
|
|
535
594
|
if (mineBtn) {
|
|
536
595
|
e.preventDefault();
|
|
537
596
|
const id = mineBtn.dataset.toggleMine;
|
|
@@ -558,7 +617,7 @@ function bindContactsActions() {
|
|
|
558
617
|
return;
|
|
559
618
|
}
|
|
560
619
|
|
|
561
|
-
const removeBtn = e.target.closest('
|
|
620
|
+
const removeBtn = e.target.closest('[data-remove-contact]');
|
|
562
621
|
if (removeBtn) {
|
|
563
622
|
e.preventDefault();
|
|
564
623
|
const id = removeBtn.dataset.removeContact;
|
|
@@ -580,7 +639,7 @@ function bindContactsActions() {
|
|
|
580
639
|
return;
|
|
581
640
|
}
|
|
582
641
|
|
|
583
|
-
const callBtn = e.target.closest('
|
|
642
|
+
const callBtn = e.target.closest('[data-contact-call]');
|
|
584
643
|
if (callBtn) {
|
|
585
644
|
e.preventDefault();
|
|
586
645
|
const id = callBtn.dataset.contactCall;
|
|
@@ -645,15 +704,35 @@ async function loadCallDetail(conversationId) {
|
|
|
645
704
|
const payload = await request(`/calls/${encodeURIComponent(conversationId)}?messages=40`);
|
|
646
705
|
const call = payload.call;
|
|
647
706
|
const el = document.getElementById('call-detail');
|
|
648
|
-
|
|
649
|
-
|
|
707
|
+
|
|
708
|
+
// Summary: prefer agent-generated summary, fall back to owner summary
|
|
709
|
+
const summaryText = call.summary || call.ownerContext?.summary || '';
|
|
710
|
+
const summaryHtml = summaryText
|
|
711
|
+
? `<pre class="summary">${esc(summaryText)}</pre>`
|
|
712
|
+
: `<p class="summary-pending"><em>${call.status === 'active' ? 'Call in progress\u2026' : 'Summary pending\u2026'}</em></p>`;
|
|
713
|
+
|
|
714
|
+
// Full transcript in a collapsible section
|
|
715
|
+
const messages = (call.recentMessages || []);
|
|
716
|
+
const transcriptLines = messages
|
|
717
|
+
.map(msg => `[${esc(fmtDate(msg.timestamp))}] ${esc(msg.direction)}: ${esc(msg.content)}`)
|
|
650
718
|
.join('\n\n');
|
|
719
|
+
const totalMessages = call.messageCount || messages.length;
|
|
720
|
+
const countLabel = messages.length < totalMessages
|
|
721
|
+
? `${messages.length} of ${totalMessages} messages`
|
|
722
|
+
: `${messages.length} message${messages.length === 1 ? '' : 's'}`;
|
|
723
|
+
const transcriptHtml = messages.length
|
|
724
|
+
? `<sl-details class="transcript-details" summary="Full Transcript (${countLabel})">
|
|
725
|
+
<pre class="transcript">${transcriptLines}</pre>
|
|
726
|
+
</sl-details>`
|
|
727
|
+
: '';
|
|
728
|
+
|
|
651
729
|
el.innerHTML = `
|
|
652
|
-
<h3>Call Detail: ${call.id}</h3>
|
|
653
|
-
<p><strong>Contact:</strong> ${call.contact?.name || call.contact || '-'}</p>
|
|
654
|
-
<p><strong>Status:</strong> ${call.status || '-'}</p>
|
|
655
|
-
<p><strong>Summary:</strong
|
|
656
|
-
|
|
730
|
+
<h3>Call Detail: ${esc(call.id)}</h3>
|
|
731
|
+
<p><strong>Contact:</strong> ${esc(call.contact?.name || call.contact || '-')}</p>
|
|
732
|
+
<p><strong>Status:</strong> ${esc(call.status || '-')}</p>
|
|
733
|
+
<p><strong>Summary:</strong></p>
|
|
734
|
+
${summaryHtml}
|
|
735
|
+
${transcriptHtml}
|
|
657
736
|
`;
|
|
658
737
|
}
|
|
659
738
|
|
|
@@ -690,7 +769,7 @@ function renderContactDetail() {
|
|
|
690
769
|
const resultHtml = result
|
|
691
770
|
? `<div style="margin-top:0.6rem;">
|
|
692
771
|
<strong>Last call result:</strong> ${result.success ? 'success' : 'failed'}<br>
|
|
693
|
-
${result.conversation_id ? `Conversation: <span class="mono">${esc(result.conversation_id)}</span> <button data-open-call="${esc(result.conversation_id)}"
|
|
772
|
+
${result.conversation_id ? `Conversation: <span class="mono">${esc(result.conversation_id)}</span> <sl-button size="small" data-open-call="${esc(result.conversation_id)}">Transcript</sl-button><br>` : ''}
|
|
694
773
|
${result.error ? `<span class="mono">${esc(result.error)}</span><br>` : ''}
|
|
695
774
|
${result.response ? `<pre class="summary">${esc(String(result.response))}</pre>` : ''}
|
|
696
775
|
</div>`
|
|
@@ -705,7 +784,7 @@ function renderContactDetail() {
|
|
|
705
784
|
<td>${esc(call.status || '-')}</td>
|
|
706
785
|
<td>${esc(fmtDate(call.last_message_at))}</td>
|
|
707
786
|
<td title="${esc(summary)}">${esc(preview)}</td>
|
|
708
|
-
<td><button data-open-call="${esc(call.id)}"
|
|
787
|
+
<td><sl-button size="small" data-open-call="${esc(call.id)}">Transcript</sl-button></td>
|
|
709
788
|
</tr>
|
|
710
789
|
`;
|
|
711
790
|
}).join('');
|
|
@@ -713,16 +792,16 @@ function renderContactDetail() {
|
|
|
713
792
|
el.innerHTML = `
|
|
714
793
|
<div class="row">
|
|
715
794
|
<h3 style="margin:0;">Contact: ${esc(contactLabel(contact))}</h3>
|
|
716
|
-
<button data-contact-call="${esc(contact.id)}"
|
|
717
|
-
<button data-remove-contact="${esc(contact.id)}"
|
|
795
|
+
<sl-button size="small" variant="primary" data-contact-call="${esc(contact.id)}" ${canCall ? '' : 'disabled'}>Call</sl-button>
|
|
796
|
+
<sl-button size="small" variant="danger" data-remove-contact="${esc(contact.id)}">Remove</sl-button>
|
|
718
797
|
</div>
|
|
719
798
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
799
|
+
<div class="row" style="margin-bottom:0.4rem;">
|
|
800
|
+
<div><strong>Mine:</strong> ${contact.is_mine ? 'yes' : 'no'}</div>
|
|
801
|
+
<div><strong>Owner:</strong> ${esc(contact.owner || '-')}</div>
|
|
802
|
+
<div><strong>Web address:</strong> <span class="mono">${esc(contact.web_address || contact.host || '-')}</span></div>
|
|
803
|
+
<div><strong>Server name:</strong> ${esc(contact.server_name || '-')}</div>
|
|
804
|
+
</div>
|
|
726
805
|
<div class="row">
|
|
727
806
|
<div><strong>Status:</strong> ${esc(contact.status || '-')}</div>
|
|
728
807
|
<div><strong>Total calls:</strong> ${esc(String(contact.call_count || 0))}</div>
|
|
@@ -731,48 +810,45 @@ function renderContactDetail() {
|
|
|
731
810
|
|
|
732
811
|
${resultHtml}
|
|
733
812
|
|
|
734
|
-
<details style="margin-top:0.8rem;"
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
<details style="margin-top:0.8rem;" open>
|
|
751
|
-
<summary><strong>Call</strong></summary>
|
|
813
|
+
<sl-details summary="Edit contact" open style="margin-top:0.8rem;">
|
|
814
|
+
<form id="contact-edit-form" data-contact-id="${esc(contact.id)}" style="margin-top:0.6rem;">
|
|
815
|
+
<sl-input id="contact-edit-name" label="Agent name" value="${esc(contact.name || '')}"></sl-input>
|
|
816
|
+
<sl-input id="contact-edit-owner" label="Owner name" value="${esc(contact.owner || '')}"></sl-input>
|
|
817
|
+
<sl-checkbox id="contact-edit-mine" ${contact.is_mine ? 'checked' : ''}>Mark as mine (personal agent)</sl-checkbox>
|
|
818
|
+
<sl-input id="contact-edit-server-name" label="Server name (my agents only)" value="${esc(contact.server_name || '')}" ${contact.is_mine ? '' : 'disabled'}></sl-input>
|
|
819
|
+
<sl-input id="contact-edit-tags" label="Tags" value="${esc(tagsText)}" placeholder="comma,separated"></sl-input>
|
|
820
|
+
<sl-textarea id="contact-edit-notes" label="Notes" rows="3" value="${esc(contact.notes || '')}"></sl-textarea>
|
|
821
|
+
<sl-textarea id="contact-edit-fields" label="Fields (JSON)" rows="5" value="${esc(fieldsText)}"></sl-textarea>
|
|
822
|
+
<div class="row">
|
|
823
|
+
<sl-button type="submit" variant="primary" size="small">Save</sl-button>
|
|
824
|
+
</div>
|
|
825
|
+
</form>
|
|
826
|
+
</sl-details>
|
|
827
|
+
|
|
828
|
+
<sl-details summary="Call" open style="margin-top:0.8rem;">
|
|
752
829
|
<form id="contact-call-form" data-contact-id="${esc(contact.id)}" style="margin-top:0.6rem;">
|
|
753
|
-
<
|
|
830
|
+
<sl-textarea id="contact-call-message" label="Message" rows="4" placeholder="Message to send"></sl-textarea>
|
|
754
831
|
<div class="row">
|
|
755
|
-
<button type="submit" ${canCall ? '' : 'disabled'}>Call</button>
|
|
832
|
+
<sl-button type="submit" variant="primary" size="small" ${canCall ? '' : 'disabled'}>Call</sl-button>
|
|
756
833
|
</div>
|
|
757
834
|
</form>
|
|
758
|
-
</details>
|
|
835
|
+
</sl-details>
|
|
759
836
|
|
|
760
|
-
<details style="margin-top:0.8rem;">
|
|
761
|
-
<summary><strong>Call history</strong></summary>
|
|
837
|
+
<sl-details summary="Call history" style="margin-top:0.8rem;">
|
|
762
838
|
<div style="margin-top:0.6rem;">
|
|
763
839
|
<table>
|
|
764
840
|
<thead><tr><th>ID</th><th>Status</th><th>Updated</th><th>Summary</th><th>Action</th></tr></thead>
|
|
765
841
|
<tbody>${callRows || '<tr><td colspan="5">No calls found.</td></tr>'}</tbody>
|
|
766
842
|
</table>
|
|
767
843
|
</div>
|
|
768
|
-
</details>
|
|
844
|
+
</sl-details>
|
|
769
845
|
`;
|
|
770
846
|
|
|
771
847
|
const editForm = document.getElementById('contact-edit-form');
|
|
772
848
|
if (editForm) {
|
|
773
849
|
const mineEl = document.getElementById('contact-edit-mine');
|
|
774
850
|
const serverNameEl = document.getElementById('contact-edit-server-name');
|
|
775
|
-
mineEl?.addEventListener('change', () => {
|
|
851
|
+
mineEl?.addEventListener('sl-change', () => {
|
|
776
852
|
if (!serverNameEl) return;
|
|
777
853
|
serverNameEl.disabled = !mineEl.checked;
|
|
778
854
|
});
|
|
@@ -802,19 +878,19 @@ function renderContactDetail() {
|
|
|
802
878
|
}
|
|
803
879
|
}
|
|
804
880
|
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
881
|
+
try {
|
|
882
|
+
await request(`/contacts/${encodeURIComponent(id)}`, {
|
|
883
|
+
method: 'PUT',
|
|
884
|
+
body: JSON.stringify({
|
|
885
|
+
name: document.getElementById('contact-edit-name').value,
|
|
886
|
+
owner: document.getElementById('contact-edit-owner').value,
|
|
887
|
+
is_mine: Boolean(document.getElementById('contact-edit-mine')?.checked),
|
|
888
|
+
server_name: document.getElementById('contact-edit-server-name').value,
|
|
889
|
+
notes: document.getElementById('contact-edit-notes').value,
|
|
890
|
+
tags,
|
|
891
|
+
fields
|
|
892
|
+
})
|
|
893
|
+
});
|
|
818
894
|
showNotice('Contact saved');
|
|
819
895
|
await loadContacts();
|
|
820
896
|
await loadCallsForContact(id);
|
|
@@ -933,10 +1009,10 @@ function renderLogStats() {
|
|
|
933
1009
|
<strong>Total:</strong> ${stats.total || 0}
|
|
934
1010
|
</div>
|
|
935
1011
|
<div class="row">
|
|
936
|
-
<strong>By level:</strong> ${levels.map(([k, v]) => `${esc(k)}=${v}`).join('
|
|
1012
|
+
<strong>By level:</strong> ${levels.map(([k, v]) => `${esc(k)}=${v}`).join(' \u00b7 ') || '(none)'}
|
|
937
1013
|
</div>
|
|
938
1014
|
<div class="row">
|
|
939
|
-
<strong>Top components:</strong> ${components.map(([k, v]) => `${esc(k)}=${v}`).join('
|
|
1015
|
+
<strong>Top components:</strong> ${components.map(([k, v]) => `${esc(k)}=${v}`).join(' \u00b7 ') || '(none)'}
|
|
940
1016
|
</div>
|
|
941
1017
|
`;
|
|
942
1018
|
}
|
|
@@ -964,7 +1040,7 @@ function renderTraceDetail() {
|
|
|
964
1040
|
el.innerHTML = `
|
|
965
1041
|
<div class="row">
|
|
966
1042
|
<h3 style="margin:0;">Trace: <span class="mono">${esc(state.trace.trace_id || '')}</span></h3>
|
|
967
|
-
<button id="clear-trace">Clear</button>
|
|
1043
|
+
<sl-button id="clear-trace" size="small">Clear</sl-button>
|
|
968
1044
|
</div>
|
|
969
1045
|
<pre class="summary mono">${esc(lines || 'No trace logs.')}</pre>
|
|
970
1046
|
`;
|
|
@@ -990,8 +1066,8 @@ function renderLogs() {
|
|
|
990
1066
|
<td>${esc(row.component || '-')}</td>
|
|
991
1067
|
<td>${esc(row.event || '-')}</td>
|
|
992
1068
|
<td title="${esc(row.message || '')}">${esc(String(row.message || '').slice(0, 120) || '-')}</td>
|
|
993
|
-
<td class="mono">${esc(trace ? trace.slice(0, 14) + '
|
|
994
|
-
<td class="mono">${esc(row.conversation_id ? row.conversation_id.slice(0, 14) + '
|
|
1069
|
+
<td class="mono">${esc(trace ? trace.slice(0, 14) + '\u2026' : '-')}</td>
|
|
1070
|
+
<td class="mono">${esc(row.conversation_id ? row.conversation_id.slice(0, 14) + '\u2026' : '-')}</td>
|
|
995
1071
|
<td class="mono">${esc(row.token_id || '-')}</td>
|
|
996
1072
|
<td>${esc(row.error_code || '-')}</td>
|
|
997
1073
|
<td>${esc(row.status_code ?? '-')}</td>
|
|
@@ -1022,6 +1098,198 @@ async function loadTrace(traceId) {
|
|
|
1022
1098
|
renderTraceDetail();
|
|
1023
1099
|
}
|
|
1024
1100
|
|
|
1101
|
+
// A2A-41: emoji map for visual tier differentiation. Standard tiers get
|
|
1102
|
+
// recognizable icons; custom/user-created tiers get a wrench.
|
|
1103
|
+
const TIER_EMOJIS = { public: '\u{1F310}', friends: '\u{1F46B}', family: '\u{1F468}\u200D\u{1F469}\u200D\u{1F467}\u200D\u{1F466}' };
|
|
1104
|
+
|
|
1105
|
+
// A2A-41: tool descriptions for the checkbox UI. These match the tools
|
|
1106
|
+
// available in Claude Code that an agent owner might want to expose to callers.
|
|
1107
|
+
const TOOL_DESCRIPTIONS = {
|
|
1108
|
+
'Bash': 'Execute shell commands \u2014 full access, can run anything',
|
|
1109
|
+
'Bash(readonly)': 'Execute read-only shell commands \u2014 no writes, no installs',
|
|
1110
|
+
'Read': 'Read files from the workspace',
|
|
1111
|
+
'Grep': 'Search file contents with regex patterns',
|
|
1112
|
+
'Glob': 'Find files by name patterns',
|
|
1113
|
+
'WebSearch': 'Search the web for information',
|
|
1114
|
+
'WebFetch': 'Fetch and read web page content'
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
// A2A-41: standard tier order for inheritance. Custom tiers are not in this list.
|
|
1118
|
+
const TIER_ORDER = ['public', 'friends', 'family'];
|
|
1119
|
+
|
|
1120
|
+
// A2A-41: renders tool checkboxes instead of a textarea. Each tool gets
|
|
1121
|
+
// a checkbox with its description. Checked state comes from tier.allowed_tools.
|
|
1122
|
+
function renderToolCheckboxes(allowedTools) {
|
|
1123
|
+
const container = document.getElementById('tier-tools-list');
|
|
1124
|
+
container.innerHTML = Object.entries(TOOL_DESCRIPTIONS).map(([tool, desc]) => {
|
|
1125
|
+
const checked = (allowedTools || []).includes(tool) ? 'checked' : '';
|
|
1126
|
+
return `<sl-checkbox value="${esc(tool)}" ${checked}><strong>${esc(tool)}</strong> \u2014 <span class="tool-desc">${esc(desc)}</span></sl-checkbox>`;
|
|
1127
|
+
}).join('');
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// A2A-41: renders topics as expandable card rows with descriptions.
|
|
1131
|
+
// Data comes from tier.manifest.topics (array of {topic, description} objects).
|
|
1132
|
+
// Falls back to tier.topics (flat string array) for topics without manifest data.
|
|
1133
|
+
function renderTopicList(tier) {
|
|
1134
|
+
const container = document.getElementById('tier-topics-list');
|
|
1135
|
+
const manifestTopics = tier.manifest?.topics || [];
|
|
1136
|
+
const flatTopics = tier.topics || [];
|
|
1137
|
+
|
|
1138
|
+
// A2A-41: prefer manifest data (has descriptions), fall back to flat array
|
|
1139
|
+
const allTopics = manifestTopics.length > 0
|
|
1140
|
+
? manifestTopics.map(t => ({ label: t.topic, desc: t.description || '' }))
|
|
1141
|
+
: flatTopics.map(t => ({ label: t, desc: '' }));
|
|
1142
|
+
|
|
1143
|
+
const rowsHtml = allTopics.map(t => `
|
|
1144
|
+
<div class="topic-row" data-topic="${esc(t.label)}" data-type="topic">
|
|
1145
|
+
<span class="drag-handle">\u2807</span>
|
|
1146
|
+
<div class="topic-content">
|
|
1147
|
+
<div class="topic-header">
|
|
1148
|
+
<strong class="topic-label">${esc(t.label)}</strong>
|
|
1149
|
+
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
1150
|
+
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
1151
|
+
</div>
|
|
1152
|
+
<div class="topic-description" style="display:none;">
|
|
1153
|
+
<p class="topic-desc-text">${esc(t.desc) || '<em>No description</em>'}</p>
|
|
1154
|
+
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(t.desc)}"></sl-input>
|
|
1155
|
+
</div>
|
|
1156
|
+
</div>
|
|
1157
|
+
</div>
|
|
1158
|
+
`).join('');
|
|
1159
|
+
|
|
1160
|
+
container.innerHTML = rowsHtml + `<button class="add-item-btn" data-type="topic">+ Add topic</button>`;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// A2A-41: renders goals as expandable card rows, identical pattern to topics.
|
|
1164
|
+
// Data from tier.manifest.objectives (array of {objective, description}).
|
|
1165
|
+
function renderGoalList(tier) {
|
|
1166
|
+
const container = document.getElementById('tier-goals-list');
|
|
1167
|
+
const manifestGoals = tier.manifest?.objectives || [];
|
|
1168
|
+
const flatGoals = tier.goals || [];
|
|
1169
|
+
|
|
1170
|
+
const allGoals = manifestGoals.length > 0
|
|
1171
|
+
? manifestGoals.map(g => ({ label: g.objective || g.topic, desc: g.description || '' }))
|
|
1172
|
+
: flatGoals.map(g => ({ label: g, desc: '' }));
|
|
1173
|
+
|
|
1174
|
+
const rowsHtml = allGoals.map(g => `
|
|
1175
|
+
<div class="topic-row" data-topic="${esc(g.label)}" data-type="goal">
|
|
1176
|
+
<span class="drag-handle">\u2807</span>
|
|
1177
|
+
<div class="topic-content">
|
|
1178
|
+
<div class="topic-header">
|
|
1179
|
+
<strong class="topic-label">${esc(g.label)}</strong>
|
|
1180
|
+
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
1181
|
+
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
1182
|
+
</div>
|
|
1183
|
+
<div class="topic-description" style="display:none;">
|
|
1184
|
+
<p class="topic-desc-text">${esc(g.desc) || '<em>No description</em>'}</p>
|
|
1185
|
+
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(g.desc)}"></sl-input>
|
|
1186
|
+
</div>
|
|
1187
|
+
</div>
|
|
1188
|
+
</div>
|
|
1189
|
+
`).join('');
|
|
1190
|
+
|
|
1191
|
+
container.innerHTML = rowsHtml + `<button class="add-item-btn" data-type="goal">+ Add goal</button>`;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// A2A-41: event delegation for topic and goal list interactions.
|
|
1195
|
+
// Uses a single click handler on each container instead of per-row binding,
|
|
1196
|
+
// preventing listener accumulation when topics are added dynamically.
|
|
1197
|
+
function bindItemListDelegation() {
|
|
1198
|
+
['tier-topics-list', 'tier-goals-list'].forEach(containerId => {
|
|
1199
|
+
const container = document.getElementById(containerId);
|
|
1200
|
+
if (!container) return;
|
|
1201
|
+
|
|
1202
|
+
container.addEventListener('click', (e) => {
|
|
1203
|
+
// Expand/collapse
|
|
1204
|
+
const expandBtn = e.target.closest('.topic-expand-btn');
|
|
1205
|
+
if (expandBtn) {
|
|
1206
|
+
const row = expandBtn.closest('.topic-row');
|
|
1207
|
+
const desc = row.querySelector('.topic-description');
|
|
1208
|
+
if (desc) {
|
|
1209
|
+
const isHidden = desc.style.display === 'none';
|
|
1210
|
+
desc.style.display = isHidden ? '' : 'none';
|
|
1211
|
+
expandBtn.name = isHidden ? 'chevron-up' : 'chevron-down';
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
// Delete
|
|
1217
|
+
const deleteBtn = e.target.closest('.topic-delete-btn');
|
|
1218
|
+
if (deleteBtn) {
|
|
1219
|
+
deleteBtn.closest('.topic-row').remove();
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Add new item
|
|
1224
|
+
const addBtn = e.target.closest('.add-item-btn');
|
|
1225
|
+
if (addBtn) {
|
|
1226
|
+
const type = addBtn.dataset.type;
|
|
1227
|
+
const label = type === 'topic' ? 'Topic name' : 'Goal name';
|
|
1228
|
+
const newRow = document.createElement('div');
|
|
1229
|
+
newRow.className = 'topic-row';
|
|
1230
|
+
newRow.dataset.type = type;
|
|
1231
|
+
newRow.innerHTML = `
|
|
1232
|
+
<span class="drag-handle">\u2807</span>
|
|
1233
|
+
<div class="topic-content">
|
|
1234
|
+
<sl-input class="new-item-label" size="small" placeholder="${label}" autofocus></sl-input>
|
|
1235
|
+
<sl-input class="new-item-desc" size="small" placeholder="Description (optional)"></sl-input>
|
|
1236
|
+
<div class="row" style="margin-top:0.3rem;">
|
|
1237
|
+
<sl-button size="small" variant="primary" class="confirm-add-btn">Add</sl-button>
|
|
1238
|
+
<sl-button size="small" class="cancel-add-btn">Cancel</sl-button>
|
|
1239
|
+
</div>
|
|
1240
|
+
</div>
|
|
1241
|
+
`;
|
|
1242
|
+
container.insertBefore(newRow, addBtn);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
// Confirm add
|
|
1247
|
+
const confirmBtn = e.target.closest('.confirm-add-btn');
|
|
1248
|
+
if (confirmBtn) {
|
|
1249
|
+
const row = confirmBtn.closest('.topic-row');
|
|
1250
|
+
const nameInput = row.querySelector('.new-item-label');
|
|
1251
|
+
const descInput = row.querySelector('.new-item-desc');
|
|
1252
|
+
const name = nameInput.value.trim();
|
|
1253
|
+
if (!name) { nameInput.focus(); return; }
|
|
1254
|
+
|
|
1255
|
+
row.dataset.topic = name;
|
|
1256
|
+
row.innerHTML = `
|
|
1257
|
+
<span class="drag-handle">\u2807</span>
|
|
1258
|
+
<div class="topic-content">
|
|
1259
|
+
<div class="topic-header">
|
|
1260
|
+
<strong class="topic-label">${esc(name)}</strong>
|
|
1261
|
+
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
1262
|
+
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
1263
|
+
</div>
|
|
1264
|
+
<div class="topic-description" style="display:none;">
|
|
1265
|
+
<p class="topic-desc-text">${esc(descInput.value)}</p>
|
|
1266
|
+
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(descInput.value)}"></sl-input>
|
|
1267
|
+
</div>
|
|
1268
|
+
</div>
|
|
1269
|
+
`;
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// Cancel add
|
|
1274
|
+
const cancelBtn = e.target.closest('.cancel-add-btn');
|
|
1275
|
+
if (cancelBtn) {
|
|
1276
|
+
cancelBtn.closest('.topic-row').remove();
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
// Description edit via sl-change (Shoelace event, delegated)
|
|
1282
|
+
container.addEventListener('sl-change', (e) => {
|
|
1283
|
+
const input = e.target.closest('.topic-desc-edit');
|
|
1284
|
+
if (input) {
|
|
1285
|
+
const row = input.closest('.topic-row');
|
|
1286
|
+
const textEl = row.querySelector('.topic-desc-text');
|
|
1287
|
+
if (textEl) textEl.textContent = input.value || '';
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1025
1293
|
function fillTierSelects() {
|
|
1026
1294
|
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
1027
1295
|
const tierSelect = document.getElementById('tier-select');
|
|
@@ -1029,22 +1297,23 @@ function fillTierSelects() {
|
|
|
1029
1297
|
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1030
1298
|
const inviteTier = document.getElementById('invite-tier');
|
|
1031
1299
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
const option = new Option(`${tier.id} (${tier.name || tier.id})`, tier.id);
|
|
1037
|
-
tierSelect.add(option.cloneNode(true));
|
|
1038
|
-
copyFrom.add(option.cloneNode(true));
|
|
1039
|
-
inviteTier.add(option.cloneNode(true));
|
|
1040
|
-
newTierCopy.add(option.cloneNode(true));
|
|
1041
|
-
});
|
|
1300
|
+
const optionsHtml = tiers.map(tier => {
|
|
1301
|
+
const emoji = TIER_EMOJIS[tier.id] || '\u{1F527}';
|
|
1302
|
+
return `<sl-option value="${esc(tier.id)}">${emoji} ${esc(tier.name || tier.id)}</sl-option>`;
|
|
1303
|
+
}).join('');
|
|
1042
1304
|
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1305
|
+
tierSelect.innerHTML = optionsHtml;
|
|
1306
|
+
copyFrom.innerHTML = optionsHtml;
|
|
1307
|
+
inviteTier.innerHTML = optionsHtml;
|
|
1308
|
+
newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1309
|
+
|
|
1310
|
+
// A2A-41: default to 'public' — it's the base tier and most commonly edited
|
|
1311
|
+
const defaultTier = tiers.find(t => t.id === 'public') ? 'public' : tiers[0]?.id;
|
|
1312
|
+
if (defaultTier) {
|
|
1313
|
+
tierSelect.value = defaultTier;
|
|
1314
|
+
copyFrom.value = defaultTier;
|
|
1315
|
+
inviteTier.value = defaultTier;
|
|
1316
|
+
renderTierEditor(defaultTier);
|
|
1048
1317
|
}
|
|
1049
1318
|
}
|
|
1050
1319
|
|
|
@@ -1052,30 +1321,319 @@ function renderTierEditor(tierId) {
|
|
|
1052
1321
|
const tier = (state.settings?.tiers || []).find(t => t.id === tierId);
|
|
1053
1322
|
if (!tier) return;
|
|
1054
1323
|
|
|
1055
|
-
document.getElementById('tier-id').value = tier.id;
|
|
1056
1324
|
document.getElementById('tier-name').value = tier.name || tier.id;
|
|
1057
1325
|
document.getElementById('tier-description').value = tier.description || '';
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1326
|
+
renderToolCheckboxes(tier.allowed_tools);
|
|
1327
|
+
renderTopicList(tier);
|
|
1328
|
+
renderGoalList(tier);
|
|
1329
|
+
renderTierWarnings(tier);
|
|
1330
|
+
renderTierColumns();
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
// A2A-41: renders the three-column drag zone showing all standard tiers
|
|
1334
|
+
// side-by-side. Inherited topics shown as grayed-out non-draggable rows.
|
|
1335
|
+
// Custom tiers are not shown here — they don't have a defined inheritance
|
|
1336
|
+
// hierarchy. HTML5 drag-and-drop does NOT work on touch devices (mobile).
|
|
1337
|
+
function renderTierColumns() {
|
|
1338
|
+
const container = document.getElementById('tier-columns');
|
|
1339
|
+
if (!container) return;
|
|
1340
|
+
const tiers = state.settings?.tiers || [];
|
|
1341
|
+
const toggle = document.getElementById('show-drag-columns');
|
|
1342
|
+
container.style.display = toggle?.checked ? '' : 'none';
|
|
1343
|
+
|
|
1344
|
+
const html = TIER_ORDER.map(tierId => {
|
|
1345
|
+
const tier = tiers.find(t => t.id === tierId);
|
|
1346
|
+
if (!tier) return '';
|
|
1347
|
+
|
|
1348
|
+
const emoji = TIER_EMOJIS[tierId] || '\u{1F527}';
|
|
1349
|
+
const tierIdx = TIER_ORDER.indexOf(tierId);
|
|
1350
|
+
|
|
1351
|
+
// Inherited topics from lower tiers
|
|
1352
|
+
let inheritedRows = '';
|
|
1353
|
+
for (let i = 0; i < tierIdx; i++) {
|
|
1354
|
+
const lowerTier = tiers.find(t => t.id === TIER_ORDER[i]);
|
|
1355
|
+
if (!lowerTier) continue;
|
|
1356
|
+
const lowerTopics = lowerTier.manifest?.topics?.length
|
|
1357
|
+
? lowerTier.manifest.topics
|
|
1358
|
+
: (lowerTier.topics || []).map(t => ({ topic: t, description: '' }));
|
|
1359
|
+
lowerTopics.forEach(t => {
|
|
1360
|
+
inheritedRows += `
|
|
1361
|
+
<div class="topic-row inherited" data-topic="${esc(t.topic)}" data-tier="${esc(TIER_ORDER[i])}">
|
|
1362
|
+
<div class="topic-content">
|
|
1363
|
+
<div class="topic-header">
|
|
1364
|
+
<strong class="topic-label">${esc(t.topic)}</strong>
|
|
1365
|
+
<span class="inherited-badge">from ${esc(TIER_ORDER[i])}</span>
|
|
1366
|
+
</div>
|
|
1367
|
+
</div>
|
|
1368
|
+
</div>`;
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Own topics — draggable
|
|
1373
|
+
const ownTopics = tier.manifest?.topics?.length
|
|
1374
|
+
? tier.manifest.topics
|
|
1375
|
+
: (tier.topics || []).map(t => ({ topic: t, description: '' }));
|
|
1376
|
+
const ownRows = ownTopics.map(t => `
|
|
1377
|
+
<div class="topic-row" draggable="true" data-topic="${esc(t.topic)}" data-tier="${esc(tierId)}">
|
|
1378
|
+
<span class="drag-handle">\u2807</span>
|
|
1379
|
+
<div class="topic-content">
|
|
1380
|
+
<div class="topic-header">
|
|
1381
|
+
<strong class="topic-label">${esc(t.topic)}</strong>
|
|
1382
|
+
</div>
|
|
1383
|
+
</div>
|
|
1384
|
+
</div>
|
|
1385
|
+
`).join('');
|
|
1386
|
+
|
|
1387
|
+
return `
|
|
1388
|
+
<div class="tier-column" data-tier="${esc(tierId)}">
|
|
1389
|
+
<h4>${emoji} ${esc(tier.name || tierId)}</h4>
|
|
1390
|
+
<div class="tier-drop-zone" data-tier="${esc(tierId)}">
|
|
1391
|
+
${inheritedRows}${ownRows}
|
|
1392
|
+
</div>
|
|
1393
|
+
</div>`;
|
|
1394
|
+
}).join('');
|
|
1395
|
+
|
|
1396
|
+
container.innerHTML = html;
|
|
1397
|
+
bindDragEvents();
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// A2A-41: HTML5 drag-and-drop handlers for moving topics between tier columns.
|
|
1401
|
+
// On drop, both tiers are saved via Promise.all() to prevent data loss if one
|
|
1402
|
+
// request fails. On error, state is reloaded from server to reset UI.
|
|
1403
|
+
function bindDragEvents() {
|
|
1404
|
+
const zones = document.querySelectorAll('.tier-drop-zone');
|
|
1405
|
+
|
|
1406
|
+
document.querySelectorAll('.tier-columns .topic-row[draggable="true"]').forEach(row => {
|
|
1407
|
+
row.addEventListener('dragstart', (e) => {
|
|
1408
|
+
e.dataTransfer.setData('application/json', JSON.stringify({
|
|
1409
|
+
topic: row.dataset.topic,
|
|
1410
|
+
sourceTier: row.dataset.tier
|
|
1411
|
+
}));
|
|
1412
|
+
row.classList.add('dragging');
|
|
1413
|
+
});
|
|
1414
|
+
row.addEventListener('dragend', () => row.classList.remove('dragging'));
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
zones.forEach(zone => {
|
|
1418
|
+
zone.addEventListener('dragover', (e) => {
|
|
1419
|
+
e.preventDefault();
|
|
1420
|
+
zone.classList.add('drag-over');
|
|
1421
|
+
});
|
|
1422
|
+
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
|
1423
|
+
zone.addEventListener('drop', async (e) => {
|
|
1424
|
+
e.preventDefault();
|
|
1425
|
+
zone.classList.remove('drag-over');
|
|
1426
|
+
|
|
1427
|
+
let data;
|
|
1428
|
+
try { data = JSON.parse(e.dataTransfer.getData('application/json')); } catch { return; }
|
|
1429
|
+
const { topic, sourceTier } = data;
|
|
1430
|
+
const targetTier = zone.dataset.tier;
|
|
1431
|
+
|
|
1432
|
+
if (!topic || !sourceTier || !targetTier || sourceTier === targetTier) return;
|
|
1433
|
+
|
|
1434
|
+
const sourceTierData = (state.settings?.tiers || []).find(t => t.id === sourceTier);
|
|
1435
|
+
const targetTierData = (state.settings?.tiers || []).find(t => t.id === targetTier);
|
|
1436
|
+
if (!sourceTierData || !targetTierData) return;
|
|
1437
|
+
|
|
1438
|
+
const sourceTopics = (sourceTierData.topics || []).filter(t => t !== topic);
|
|
1439
|
+
const sourceManifestTopics = (sourceTierData.manifest?.topics || []).filter(t => t.topic !== topic);
|
|
1440
|
+
const movedManifest = (sourceTierData.manifest?.topics || []).find(t => t.topic === topic);
|
|
1441
|
+
const targetTopics = [...(targetTierData.topics || []), topic];
|
|
1442
|
+
const targetManifestTopics = [...(targetTierData.manifest?.topics || []), movedManifest || { topic, description: '' }];
|
|
1443
|
+
|
|
1444
|
+
// A2A-41: save both tiers atomically with Promise.all to prevent
|
|
1445
|
+
// data loss if one request fails. On error, reload from server.
|
|
1446
|
+
try {
|
|
1447
|
+
await Promise.all([
|
|
1448
|
+
request(`/settings/tiers/${encodeURIComponent(sourceTier)}`, {
|
|
1449
|
+
method: 'PUT',
|
|
1450
|
+
body: JSON.stringify({
|
|
1451
|
+
topics: sourceTopics,
|
|
1452
|
+
manifest: { topics: sourceManifestTopics, objectives: sourceTierData.manifest?.objectives || [] }
|
|
1453
|
+
})
|
|
1454
|
+
}),
|
|
1455
|
+
request(`/settings/tiers/${encodeURIComponent(targetTier)}`, {
|
|
1456
|
+
method: 'PUT',
|
|
1457
|
+
body: JSON.stringify({
|
|
1458
|
+
topics: targetTopics,
|
|
1459
|
+
manifest: { topics: targetManifestTopics, objectives: targetTierData.manifest?.objectives || [] }
|
|
1460
|
+
})
|
|
1461
|
+
})
|
|
1462
|
+
]);
|
|
1463
|
+
showNotice(`Moved "${topic}" from ${sourceTier} to ${targetTier}`);
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
showNotice(`Move failed: ${err.message}. Reloading...`);
|
|
1466
|
+
}
|
|
1467
|
+
await loadSettings();
|
|
1468
|
+
});
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// A2A-41: contextual validation warnings for the currently selected tier.
|
|
1473
|
+
// Warns about empty tiers, dangerous tool grants, and inverted tier sizes.
|
|
1474
|
+
function renderTierWarnings(tier) {
|
|
1475
|
+
const container = document.getElementById('tier-warnings');
|
|
1476
|
+
if (!container) return;
|
|
1477
|
+
const warnings = [];
|
|
1478
|
+
|
|
1479
|
+
// A2A-41: use manifest OR flat topics (not both) to avoid double-counting.
|
|
1480
|
+
// Manifest is preferred when non-empty; flat array is the fallback.
|
|
1481
|
+
// NOTE: can't use || for this because empty arrays are truthy in JS.
|
|
1482
|
+
const mTopics = tier.manifest?.topics;
|
|
1483
|
+
const topicCount = (mTopics && mTopics.length > 0 ? mTopics : (tier.topics || [])).length;
|
|
1484
|
+
if (topicCount === 0) {
|
|
1485
|
+
warnings.push({ level: 'warn', text: "This tier has no topics \u2014 callers won't have conversation context." });
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (tier.id === 'public' && (tier.allowed_tools || []).includes('Bash')) {
|
|
1489
|
+
warnings.push({ level: 'danger', text: 'Bash (full access) is granted to the public tier \u2014 any caller can execute commands.' });
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
if (tier.id === 'family') {
|
|
1493
|
+
const allTiers = state.settings?.tiers || [];
|
|
1494
|
+
const friends = allTiers.find(t => t.id === 'friends');
|
|
1495
|
+
if (friends) {
|
|
1496
|
+
const mFam = tier.manifest?.topics;
|
|
1497
|
+
const familyOwn = (mFam && mFam.length > 0 ? mFam : (tier.topics || [])).length;
|
|
1498
|
+
const mFri = friends.manifest?.topics;
|
|
1499
|
+
const friendsOwn = (mFri && mFri.length > 0 ? mFri : (friends.topics || [])).length;
|
|
1500
|
+
if (familyOwn < friendsOwn) {
|
|
1501
|
+
warnings.push({ level: 'info', text: 'Family tier has fewer topics than Friends \u2014 usually Family is the most open tier.' });
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
container.innerHTML = warnings.map(w =>
|
|
1507
|
+
`<div class="tier-warning ${w.level}">${esc(w.text)}</div>`
|
|
1508
|
+
).join('');
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// A2A-41: merges topics/goals/tools from the selected tier and all lower tiers,
|
|
1512
|
+
// mirroring the backend's getTopicsForTier() inheritance. Used by the preview dialog.
|
|
1513
|
+
function getPreviewData(tierId) {
|
|
1514
|
+
const selectedIndex = TIER_ORDER.indexOf(tierId);
|
|
1515
|
+
const tiers = state.settings?.tiers || [];
|
|
1516
|
+
const merged = { topics: [], objectives: [], tools: new Set(), do_not_discuss: [], never_disclose: [] };
|
|
1517
|
+
|
|
1518
|
+
// A2A-41: for custom tiers not in TIER_ORDER, show only own data.
|
|
1519
|
+
// No inheritance is applied because custom tiers have no defined hierarchy.
|
|
1520
|
+
if (selectedIndex < 0) {
|
|
1521
|
+
const t = tiers.find(t => t.id === tierId);
|
|
1522
|
+
if (t) {
|
|
1523
|
+
(t.manifest?.topics || []).forEach(item => merged.topics.push({ ...item, source: tierId }));
|
|
1524
|
+
(t.manifest?.objectives || []).forEach(item => merged.objectives.push({ ...item, source: tierId }));
|
|
1525
|
+
(t.allowed_tools || []).forEach(tool => merged.tools.add(tool));
|
|
1526
|
+
}
|
|
1527
|
+
merged.never_disclose = state.settings?.manifest?.never_disclose || [];
|
|
1528
|
+
return merged;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
for (let i = 0; i <= selectedIndex; i++) {
|
|
1532
|
+
const t = tiers.find(t => t.id === TIER_ORDER[i]);
|
|
1533
|
+
if (!t) continue;
|
|
1534
|
+
(t.manifest?.topics || []).forEach(item => merged.topics.push({ ...item, source: TIER_ORDER[i] }));
|
|
1535
|
+
(t.manifest?.objectives || []).forEach(item => merged.objectives.push({ ...item, source: TIER_ORDER[i] }));
|
|
1536
|
+
(t.manifest?.do_not_discuss || []).forEach(item => {
|
|
1537
|
+
if (!merged.do_not_discuss.includes(item)) merged.do_not_discuss.push(item);
|
|
1538
|
+
});
|
|
1539
|
+
(t.allowed_tools || []).forEach(tool => merged.tools.add(tool));
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
merged.never_disclose = state.settings?.manifest?.never_disclose || [];
|
|
1543
|
+
return merged;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// A2A-41: opens the caller preview dialog showing the merged effective view
|
|
1547
|
+
// for the selected tier. Helps the agent owner understand what a caller sees.
|
|
1548
|
+
function openCallerPreview() {
|
|
1549
|
+
const tierId = document.getElementById('tier-select').value;
|
|
1550
|
+
const data = getPreviewData(tierId);
|
|
1551
|
+
const emoji = TIER_EMOJIS[tierId] || '\u{1F527}';
|
|
1552
|
+
const tierName = (state.settings?.tiers || []).find(t => t.id === tierId)?.name || tierId;
|
|
1553
|
+
|
|
1554
|
+
const dialog = document.getElementById('preview-dialog');
|
|
1555
|
+
dialog.label = `\u{1F441} Caller Preview \u2014 ${emoji} ${tierName}`;
|
|
1556
|
+
|
|
1557
|
+
const topicsList = data.topics.length > 0
|
|
1558
|
+
? data.topics.map(t => `<li><strong>${esc(t.topic)}</strong>${t.description ? ` \u2014 ${esc(t.description)}` : ''}</li>`).join('')
|
|
1559
|
+
: '<li><em>None configured</em></li>';
|
|
1560
|
+
|
|
1561
|
+
const goalsList = data.objectives.length > 0
|
|
1562
|
+
? data.objectives.map(g => `<li><strong>${esc(g.objective || g.topic)}</strong>${g.description ? ` \u2014 ${esc(g.description)}` : ''}</li>`).join('')
|
|
1563
|
+
: '<li><em>None configured</em></li>';
|
|
1564
|
+
|
|
1565
|
+
const toolsList = data.tools.size > 0
|
|
1566
|
+
? Array.from(data.tools).map(t => `<li><strong>${esc(t)}</strong>${TOOL_DESCRIPTIONS[t] ? ` \u2014 ${esc(TOOL_DESCRIPTIONS[t])}` : ''}</li>`).join('')
|
|
1567
|
+
: '<li><em>None configured</em></li>';
|
|
1568
|
+
|
|
1569
|
+
const dndList = data.do_not_discuss.length > 0
|
|
1570
|
+
? data.do_not_discuss.map(d => `<li>${esc(typeof d === 'string' ? d : d.topic || '')}</li>`).join('')
|
|
1571
|
+
: '<li><em>None configured</em></li>';
|
|
1572
|
+
|
|
1573
|
+
const neverList = data.never_disclose.length > 0
|
|
1574
|
+
? data.never_disclose.map(n => `<li>${esc(n)}</li>`).join('')
|
|
1575
|
+
: '<li><em>None configured</em></li>';
|
|
1576
|
+
|
|
1577
|
+
document.getElementById('preview-content').innerHTML = `
|
|
1578
|
+
<h4>Topics this caller can discuss:</h4>
|
|
1579
|
+
<ul>${topicsList}</ul>
|
|
1580
|
+
<h4>Goals:</h4>
|
|
1581
|
+
<ul>${goalsList}</ul>
|
|
1582
|
+
<h4>Tools available:</h4>
|
|
1583
|
+
<ul>${toolsList}</ul>
|
|
1584
|
+
<h4>Will not discuss:</h4>
|
|
1585
|
+
<ul>${dndList}</ul>
|
|
1586
|
+
<h4>Never disclosed (any tier):</h4>
|
|
1587
|
+
<ul>${neverList}</ul>
|
|
1588
|
+
`;
|
|
1589
|
+
|
|
1590
|
+
dialog.show();
|
|
1062
1591
|
}
|
|
1063
1592
|
|
|
1064
1593
|
function bindSettingsActions() {
|
|
1065
|
-
document.getElementById('tier-select').addEventListener('change', (e) => {
|
|
1594
|
+
document.getElementById('tier-select').addEventListener('sl-change', (e) => {
|
|
1066
1595
|
renderTierEditor(e.target.value);
|
|
1067
1596
|
});
|
|
1068
1597
|
|
|
1069
1598
|
document.getElementById('tier-form').addEventListener('submit', async (e) => {
|
|
1070
1599
|
e.preventDefault();
|
|
1071
|
-
const tierId = document.getElementById('tier-
|
|
1600
|
+
const tierId = document.getElementById('tier-select').value;
|
|
1601
|
+
|
|
1602
|
+
// A2A-41: collect tools from checkboxes
|
|
1603
|
+
const toolCheckboxes = document.querySelectorAll('#tier-tools-list sl-checkbox');
|
|
1604
|
+
const allowed_tools = Array.from(toolCheckboxes)
|
|
1605
|
+
.filter(cb => cb.checked)
|
|
1606
|
+
.map(cb => cb.value);
|
|
1607
|
+
|
|
1608
|
+
// A2A-41: collect topics from row elements
|
|
1609
|
+
const topicRows = document.querySelectorAll('#tier-topics-list .topic-row[data-topic]');
|
|
1610
|
+
const topics = Array.from(topicRows).map(row => row.dataset.topic).filter(Boolean);
|
|
1611
|
+
const manifestTopics = Array.from(topicRows).map(row => ({
|
|
1612
|
+
topic: row.dataset.topic,
|
|
1613
|
+
description: (row.querySelector('.topic-desc-edit')?.value || row.querySelector('.topic-desc-text')?.textContent || '').trim()
|
|
1614
|
+
})).filter(t => t.topic);
|
|
1615
|
+
|
|
1616
|
+
// A2A-41: collect goals from row elements. IMPORTANT: use 'topic' key (NOT
|
|
1617
|
+
// 'objective') because parseTopicObjects() in dashboard.js:160 only reads
|
|
1618
|
+
// entry.topic. The semantic distinction 'objective' vs 'topic' is UI-only;
|
|
1619
|
+
// the storage layer uses {topic, description} uniformly for both.
|
|
1620
|
+
const goalRows = document.querySelectorAll('#tier-goals-list .topic-row[data-topic]');
|
|
1621
|
+
const goals = Array.from(goalRows).map(row => row.dataset.topic).filter(Boolean);
|
|
1622
|
+
const manifestObjectives = Array.from(goalRows).map(row => ({
|
|
1623
|
+
topic: row.dataset.topic,
|
|
1624
|
+
description: (row.querySelector('.topic-desc-edit')?.value || row.querySelector('.topic-desc-text')?.textContent || '').trim()
|
|
1625
|
+
})).filter(g => g.topic);
|
|
1626
|
+
|
|
1072
1627
|
const body = {
|
|
1073
1628
|
name: document.getElementById('tier-name').value,
|
|
1074
1629
|
description: document.getElementById('tier-description').value,
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1630
|
+
allowed_tools,
|
|
1631
|
+
topics,
|
|
1632
|
+
goals,
|
|
1633
|
+
manifest: {
|
|
1634
|
+
topics: manifestTopics,
|
|
1635
|
+
objectives: manifestObjectives
|
|
1636
|
+
}
|
|
1079
1637
|
};
|
|
1080
1638
|
await request(`/settings/tiers/${encodeURIComponent(tierId)}`, {
|
|
1081
1639
|
method: 'PUT',
|
|
@@ -1086,7 +1644,7 @@ function bindSettingsActions() {
|
|
|
1086
1644
|
});
|
|
1087
1645
|
|
|
1088
1646
|
document.getElementById('copy-tier-btn').addEventListener('click', async () => {
|
|
1089
|
-
const toTier = document.getElementById('tier-
|
|
1647
|
+
const toTier = document.getElementById('tier-select').value;
|
|
1090
1648
|
const fromTier = document.getElementById('copy-from-tier').value;
|
|
1091
1649
|
if (!toTier || !fromTier || toTier === fromTier) return;
|
|
1092
1650
|
await request(`/settings/tiers/${encodeURIComponent(toTier)}/copy-from/${encodeURIComponent(fromTier)}`, {
|
|
@@ -1134,12 +1692,23 @@ function bindSettingsActions() {
|
|
|
1134
1692
|
document.getElementById('tier-select').value = tierId;
|
|
1135
1693
|
renderTierEditor(tierId);
|
|
1136
1694
|
});
|
|
1695
|
+
|
|
1696
|
+
// A2A-41: toggle for three-column tier view
|
|
1697
|
+
document.getElementById('show-drag-columns')?.addEventListener('sl-change', () => {
|
|
1698
|
+
renderTierColumns();
|
|
1699
|
+
});
|
|
1700
|
+
|
|
1701
|
+
document.getElementById('preview-caller-btn')?.addEventListener('click', openCallerPreview);
|
|
1702
|
+
document.getElementById('preview-close-btn')?.addEventListener('click', () => {
|
|
1703
|
+
document.getElementById('preview-dialog').hide();
|
|
1704
|
+
});
|
|
1137
1705
|
}
|
|
1138
1706
|
|
|
1139
1707
|
async function loadSettings() {
|
|
1140
1708
|
const payload = await request('/settings');
|
|
1141
1709
|
state.settings = payload;
|
|
1142
1710
|
fillTierSelects();
|
|
1711
|
+
renderTierColumns();
|
|
1143
1712
|
document.getElementById('defaults-expiration').value = payload.defaults?.expiration || '7d';
|
|
1144
1713
|
document.getElementById('defaults-max-calls').value = payload.defaults?.maxCalls || 100;
|
|
1145
1714
|
}
|
|
@@ -1150,7 +1719,7 @@ function renderCallbookStatus() {
|
|
|
1150
1719
|
|
|
1151
1720
|
const s = state.dashboardStatus;
|
|
1152
1721
|
if (!s) {
|
|
1153
|
-
el.textContent = 'Loading
|
|
1722
|
+
el.textContent = 'Loading\u2026';
|
|
1154
1723
|
return;
|
|
1155
1724
|
}
|
|
1156
1725
|
|
|
@@ -1171,8 +1740,7 @@ function renderCallbookStatus() {
|
|
|
1171
1740
|
const extMetaText = extMeta.length ? ` <span class="mono">(${esc(extMeta.join(', '))})</span>` : '';
|
|
1172
1741
|
const extErrorText = ext && ext.error ? esc(ext.error) : '';
|
|
1173
1742
|
const extAttemptsHtml = extAttempts.length
|
|
1174
|
-
? `<details style="margin-top:0.5rem;">
|
|
1175
|
-
<summary>External IP probe</summary>
|
|
1743
|
+
? `<sl-details summary="External IP probe" style="margin-top:0.5rem;">
|
|
1176
1744
|
<div class="mono" style="margin-top:0.35rem;">
|
|
1177
1745
|
${extAttempts.map(a => {
|
|
1178
1746
|
const service = a && a.service ? String(a.service) : '-';
|
|
@@ -1182,7 +1750,7 @@ function renderCallbookStatus() {
|
|
|
1182
1750
|
return esc(`${service}: ${ok ? 'ok' + status : 'failed' + err}`);
|
|
1183
1751
|
}).join('<br>')}
|
|
1184
1752
|
</div>
|
|
1185
|
-
</details>`
|
|
1753
|
+
</sl-details>`
|
|
1186
1754
|
: '';
|
|
1187
1755
|
|
|
1188
1756
|
el.innerHTML = `
|
|
@@ -1204,18 +1772,18 @@ function renderAutoUpdateStatus() {
|
|
|
1204
1772
|
|
|
1205
1773
|
const au = state.autoUpdate;
|
|
1206
1774
|
if (!au) {
|
|
1207
|
-
el.textContent = 'Loading
|
|
1775
|
+
el.textContent = 'Loading\u2026';
|
|
1208
1776
|
if (toggleBtn) toggleBtn.disabled = true;
|
|
1209
1777
|
return;
|
|
1210
1778
|
}
|
|
1211
1779
|
|
|
1212
1780
|
const stateText = formatUpdaterState(au.state);
|
|
1213
|
-
const
|
|
1781
|
+
const variant = badgeVariant(au.state);
|
|
1214
1782
|
const enabled = Boolean(au.enabled);
|
|
1215
1783
|
const intervalSec = Number.isFinite(au.interval_ms) ? Math.floor(au.interval_ms / 1000) : null;
|
|
1216
1784
|
|
|
1217
1785
|
el.innerHTML = `
|
|
1218
|
-
<div><strong>Status:</strong> <
|
|
1786
|
+
<div><strong>Status:</strong> <sl-badge variant="${variant}">${esc(stateText)}</sl-badge></div>
|
|
1219
1787
|
<div><strong>Enabled:</strong> ${enabled ? 'yes' : 'no'}</div>
|
|
1220
1788
|
<div><strong>Current version:</strong> <span class="mono">${esc(au.current_version || '-')}</span></div>
|
|
1221
1789
|
<div><strong>Latest version:</strong> <span class="mono">${esc(au.latest_version || '-')}</span></div>
|
|
@@ -1274,13 +1842,13 @@ function renderCallbookDevices() {
|
|
|
1274
1842
|
<td>${esc(String(sessions))}</td>
|
|
1275
1843
|
<td>${revoked ? esc(fmtDate(dev.revoked_at)) : '-'}</td>
|
|
1276
1844
|
<td>
|
|
1277
|
-
<button data-revoke="${esc(dev.id)}" ${revoked ? 'disabled' : ''}>Revoke</button>
|
|
1845
|
+
<sl-button size="small" variant="danger" data-revoke="${esc(dev.id)}" ${revoked ? 'disabled' : ''}>Revoke</sl-button>
|
|
1278
1846
|
</td>
|
|
1279
1847
|
`;
|
|
1280
1848
|
tbody.appendChild(tr);
|
|
1281
1849
|
});
|
|
1282
1850
|
|
|
1283
|
-
tbody.querySelectorAll('
|
|
1851
|
+
tbody.querySelectorAll('[data-revoke]').forEach(btn => {
|
|
1284
1852
|
btn.addEventListener('click', async () => {
|
|
1285
1853
|
const deviceId = btn.dataset.revoke;
|
|
1286
1854
|
if (!deviceId) return;
|
|
@@ -1311,14 +1879,6 @@ function bindCallbookActions() {
|
|
|
1311
1879
|
const labelEl = document.getElementById('callbook-label');
|
|
1312
1880
|
const warningsEl = document.getElementById('callbook-warnings');
|
|
1313
1881
|
|
|
1314
|
-
document.getElementById('callbook-refresh')?.addEventListener('click', () => {
|
|
1315
|
-
Promise.all([loadDashboardStatus(true), loadCallbookDevices()]).catch(err => showNotice(err.message));
|
|
1316
|
-
});
|
|
1317
|
-
|
|
1318
|
-
document.getElementById('callbook-refresh-devices')?.addEventListener('click', () => {
|
|
1319
|
-
loadCallbookDevices().catch(err => showNotice(err.message));
|
|
1320
|
-
});
|
|
1321
|
-
|
|
1322
1882
|
document.getElementById('callbook-logout')?.addEventListener('click', async () => {
|
|
1323
1883
|
try {
|
|
1324
1884
|
await request('/callbook/logout', { method: 'POST' });
|
|
@@ -1362,10 +1922,6 @@ function bindCallbookActions() {
|
|
|
1362
1922
|
}
|
|
1363
1923
|
|
|
1364
1924
|
function bindAutoUpdateActions() {
|
|
1365
|
-
document.getElementById('auto-update-refresh')?.addEventListener('click', () => {
|
|
1366
|
-
loadAutoUpdateStatus().catch(err => showNotice(err.message));
|
|
1367
|
-
});
|
|
1368
|
-
|
|
1369
1925
|
document.getElementById('auto-update-check')?.addEventListener('click', async () => {
|
|
1370
1926
|
try {
|
|
1371
1927
|
await request('/update/check', { method: 'POST', body: JSON.stringify({}) });
|
|
@@ -1413,13 +1969,13 @@ function renderInvites() {
|
|
|
1413
1969
|
<td>${invite.tier || '-'}</td>
|
|
1414
1970
|
<td>${invite.calls_made || 0}${invite.max_calls ? `/${invite.max_calls}` : ''}</td>
|
|
1415
1971
|
<td>${fmtDate(invite.expires_at)}</td>
|
|
1416
|
-
<td>${invite.revoked ? 'revoked' : 'active'}</td>
|
|
1417
|
-
<td><button data-revoke="${invite.id}" ${invite.revoked ? 'disabled' : ''}>Revoke</button></td>
|
|
1972
|
+
<td><sl-badge variant="${invite.revoked ? 'danger' : 'success'}">${invite.revoked ? 'revoked' : 'active'}</sl-badge></td>
|
|
1973
|
+
<td><sl-button size="small" variant="danger" data-revoke="${invite.id}" ${invite.revoked ? 'disabled' : ''}>Revoke</sl-button></td>
|
|
1418
1974
|
`;
|
|
1419
1975
|
tbody.appendChild(tr);
|
|
1420
1976
|
});
|
|
1421
1977
|
|
|
1422
|
-
tbody.querySelectorAll('
|
|
1978
|
+
tbody.querySelectorAll('[data-revoke]').forEach(btn => {
|
|
1423
1979
|
btn.addEventListener('click', async () => {
|
|
1424
1980
|
const tokenId = btn.dataset.revoke;
|
|
1425
1981
|
await request(`/invites/${encodeURIComponent(tokenId)}/revoke`, { method: 'POST' });
|
|
@@ -1436,6 +1992,18 @@ async function loadInvites() {
|
|
|
1436
1992
|
}
|
|
1437
1993
|
|
|
1438
1994
|
function bindInviteActions() {
|
|
1995
|
+
const cancelBtn = document.getElementById('generate-invite-cancel');
|
|
1996
|
+
const inviteDetails = document.getElementById('generate-invite-details');
|
|
1997
|
+
const inviteMessageWrap = document.getElementById('invite-message-wrap');
|
|
1998
|
+
const inviteMessage = document.getElementById('invite-message');
|
|
1999
|
+
|
|
2000
|
+
// Cancel button collapses the sl-details
|
|
2001
|
+
if (cancelBtn && inviteDetails) {
|
|
2002
|
+
cancelBtn.addEventListener('click', () => {
|
|
2003
|
+
inviteDetails.open = false;
|
|
2004
|
+
});
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1439
2007
|
document.getElementById('invite-form').addEventListener('submit', async (e) => {
|
|
1440
2008
|
e.preventDefault();
|
|
1441
2009
|
const body = {
|
|
@@ -1450,7 +2018,13 @@ function bindInviteActions() {
|
|
|
1450
2018
|
method: 'POST',
|
|
1451
2019
|
body: JSON.stringify(body)
|
|
1452
2020
|
});
|
|
1453
|
-
|
|
2021
|
+
// Show the invite message textarea with the result
|
|
2022
|
+
if (inviteMessage) {
|
|
2023
|
+
inviteMessage.value = result.invite_message || result.invite_url;
|
|
2024
|
+
if (inviteMessageWrap) inviteMessageWrap.style.display = 'block';
|
|
2025
|
+
}
|
|
2026
|
+
// Collapse the details after successful creation
|
|
2027
|
+
if (inviteDetails) inviteDetails.open = false;
|
|
1454
2028
|
if (result.warnings && result.warnings.length) {
|
|
1455
2029
|
showNotice(result.warnings[0]);
|
|
1456
2030
|
} else {
|
|
@@ -1460,13 +2034,7 @@ function bindInviteActions() {
|
|
|
1460
2034
|
});
|
|
1461
2035
|
}
|
|
1462
2036
|
|
|
1463
|
-
function
|
|
1464
|
-
document.getElementById('refresh-contacts').addEventListener('click', () => loadContacts().catch(err => showNotice(err.message)));
|
|
1465
|
-
document.getElementById('refresh-calls').addEventListener('click', () => loadCalls().catch(err => showNotice(err.message)));
|
|
1466
|
-
document.getElementById('refresh-invites').addEventListener('click', () => loadInvites().catch(err => showNotice(err.message)));
|
|
1467
|
-
document.getElementById('refresh-logs').addEventListener('click', () => loadLogs().catch(err => showNotice(err.message)));
|
|
1468
|
-
document.getElementById('refresh-log-stats').addEventListener('click', () => loadLogStats().catch(err => showNotice(err.message)));
|
|
1469
|
-
|
|
2037
|
+
function bindLogFilterRefresh() {
|
|
1470
2038
|
// Auto-refresh logs as filters change (debounced).
|
|
1471
2039
|
let debounce = null;
|
|
1472
2040
|
const schedule = () => {
|
|
@@ -1485,19 +2053,64 @@ function bindRefreshButtons() {
|
|
|
1485
2053
|
].forEach(id => {
|
|
1486
2054
|
const el = document.getElementById(id);
|
|
1487
2055
|
if (!el) return;
|
|
2056
|
+
// Shoelace components fire sl-input and sl-change events
|
|
2057
|
+
el.addEventListener('sl-input', schedule);
|
|
2058
|
+
el.addEventListener('sl-change', schedule);
|
|
2059
|
+
// Also listen for native events as fallback
|
|
1488
2060
|
el.addEventListener('input', schedule);
|
|
1489
2061
|
el.addEventListener('change', schedule);
|
|
1490
2062
|
});
|
|
1491
2063
|
}
|
|
1492
2064
|
|
|
2065
|
+
// --- Smart tab polling ---
|
|
2066
|
+
|
|
2067
|
+
let pollTimer = null;
|
|
2068
|
+
|
|
2069
|
+
function getActiveTab() {
|
|
2070
|
+
const tabGroup = document.getElementById('main-tabs');
|
|
2071
|
+
if (!tabGroup) return 'contacts';
|
|
2072
|
+
// Shoelace tab group: find the active tab by checking which tab has the active attribute
|
|
2073
|
+
const activeTab = tabGroup.querySelector('sl-tab[active]');
|
|
2074
|
+
return activeTab ? activeTab.getAttribute('panel') : 'contacts';
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
const tabLoaders = {
|
|
2078
|
+
contacts: loadContacts,
|
|
2079
|
+
calls: loadCalls,
|
|
2080
|
+
logs: () => { loadLogs(); loadLogStats(); },
|
|
2081
|
+
permissions: () => {},
|
|
2082
|
+
invites: loadInvites,
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
function startPolling() {
|
|
2086
|
+
stopPolling();
|
|
2087
|
+
pollTimer = setInterval(() => {
|
|
2088
|
+
const loader = tabLoaders[getActiveTab()];
|
|
2089
|
+
if (loader) loader().catch(() => {});
|
|
2090
|
+
}, 5000);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
function stopPolling() {
|
|
2094
|
+
if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
|
|
2095
|
+
}
|
|
2096
|
+
|
|
2097
|
+
function onTabSwitch(tabName) {
|
|
2098
|
+
const loader = tabLoaders[tabName];
|
|
2099
|
+
if (loader) {
|
|
2100
|
+
try { loader().catch(() => {}); } catch (_) {}
|
|
2101
|
+
}
|
|
2102
|
+
startPolling(); // reset the 5s timer
|
|
2103
|
+
}
|
|
2104
|
+
|
|
1493
2105
|
async function bootstrap() {
|
|
1494
2106
|
bindTabs();
|
|
1495
2107
|
bindContactsActions();
|
|
1496
2108
|
bindSettingsActions();
|
|
2109
|
+
bindItemListDelegation();
|
|
1497
2110
|
bindCallbookActions();
|
|
1498
2111
|
bindAutoUpdateActions();
|
|
1499
2112
|
bindInviteActions();
|
|
1500
|
-
|
|
2113
|
+
bindLogFilterRefresh();
|
|
1501
2114
|
|
|
1502
2115
|
try {
|
|
1503
2116
|
await Promise.all([
|
|
@@ -1513,6 +2126,7 @@ async function bootstrap() {
|
|
|
1513
2126
|
]);
|
|
1514
2127
|
showNotice('Dashboard loaded');
|
|
1515
2128
|
connectRealtimeEvents();
|
|
2129
|
+
startPolling();
|
|
1516
2130
|
|
|
1517
2131
|
setInterval(() => {
|
|
1518
2132
|
loadAutoUpdateStatus().catch(() => {});
|