a2acalling 0.6.58 → 0.6.60
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/package.json +1 -1
- package/scripts/install-skills.js +8 -1
- package/src/dashboard/public/app.js +520 -22
- package/src/dashboard/public/index.html +20 -8
- package/src/dashboard/public/style.css +219 -0
- 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/.a2a-manifest.json +0 -47
- package/.claude/a2a-skill-reference.md +0 -462
- 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
package/package.json
CHANGED
|
@@ -37,12 +37,19 @@ const SKILL_FILES = [
|
|
|
37
37
|
// Copied to .claude/ so Claude Code discovers it naturally without grepping
|
|
38
38
|
// node_modules. This is opt-in context: only loaded when the agent looks.
|
|
39
39
|
{ src: 'SKILL.md', dest: '.claude/a2a-skill-reference.md' },
|
|
40
|
-
// Claude Code slash commands
|
|
40
|
+
// Claude Code slash commands — core (A2A-28)
|
|
41
41
|
{ src: '.claude/commands/a2a-call.md', dest: '.claude/commands/a2a-call.md' },
|
|
42
42
|
{ src: '.claude/commands/a2a-invite.md', dest: '.claude/commands/a2a-invite.md' },
|
|
43
43
|
{ src: '.claude/commands/a2a-contacts.md', dest: '.claude/commands/a2a-contacts.md' },
|
|
44
44
|
{ src: '.claude/commands/a2a-status.md', dest: '.claude/commands/a2a-status.md' },
|
|
45
45
|
{ src: '.claude/commands/a2a-setup.md', dest: '.claude/commands/a2a-setup.md' },
|
|
46
|
+
// Claude Code slash commands — extended (A2A-43)
|
|
47
|
+
{ src: '.claude/commands/a2a-update.md', dest: '.claude/commands/a2a-update.md' },
|
|
48
|
+
{ src: '.claude/commands/a2a-uninstall.md', dest: '.claude/commands/a2a-uninstall.md' },
|
|
49
|
+
{ src: '.claude/commands/a2a-app.md', dest: '.claude/commands/a2a-app.md' },
|
|
50
|
+
{ src: '.claude/commands/a2a-conversations.md', dest: '.claude/commands/a2a-conversations.md' },
|
|
51
|
+
{ src: '.claude/commands/a2a-gui.md', dest: '.claude/commands/a2a-gui.md' },
|
|
52
|
+
{ src: '.claude/commands/a2a-skills.md', dest: '.claude/commands/a2a-skills.md' },
|
|
46
53
|
// Codex agent instructions
|
|
47
54
|
{ src: '.codex/AGENTS.md', dest: '.codex/AGENTS.md' }
|
|
48
55
|
];
|
|
@@ -261,7 +261,10 @@ function bindTabs() {
|
|
|
261
261
|
|
|
262
262
|
// Deep-link support: activate the tab matching the URL hash
|
|
263
263
|
const activateFromHash = () => {
|
|
264
|
-
|
|
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';
|
|
265
268
|
if (hash) {
|
|
266
269
|
// Use try/catch in case the tab group isn't fully ready
|
|
267
270
|
try { tabGroup.show(hash); } catch (err) {}
|
|
@@ -1095,6 +1098,198 @@ async function loadTrace(traceId) {
|
|
|
1095
1098
|
renderTraceDetail();
|
|
1096
1099
|
}
|
|
1097
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
|
+
|
|
1098
1293
|
function fillTierSelects() {
|
|
1099
1294
|
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
1100
1295
|
const tierSelect = document.getElementById('tier-select');
|
|
@@ -1102,21 +1297,23 @@ function fillTierSelects() {
|
|
|
1102
1297
|
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
1103
1298
|
const inviteTier = document.getElementById('invite-tier');
|
|
1104
1299
|
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
`<sl-option value="${esc(tier.id)}">${
|
|
1108
|
-
).join('');
|
|
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('');
|
|
1109
1304
|
|
|
1110
1305
|
tierSelect.innerHTML = optionsHtml;
|
|
1111
1306
|
copyFrom.innerHTML = optionsHtml;
|
|
1112
1307
|
inviteTier.innerHTML = optionsHtml;
|
|
1113
1308
|
newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
1114
1309
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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);
|
|
1120
1317
|
}
|
|
1121
1318
|
}
|
|
1122
1319
|
|
|
@@ -1124,13 +1321,273 @@ function renderTierEditor(tierId) {
|
|
|
1124
1321
|
const tier = (state.settings?.tiers || []).find(t => t.id === tierId);
|
|
1125
1322
|
if (!tier) return;
|
|
1126
1323
|
|
|
1127
|
-
document.getElementById('tier-id').value = tier.id;
|
|
1128
1324
|
document.getElementById('tier-name').value = tier.name || tier.id;
|
|
1129
1325
|
document.getElementById('tier-description').value = tier.description || '';
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
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();
|
|
1134
1591
|
}
|
|
1135
1592
|
|
|
1136
1593
|
function bindSettingsActions() {
|
|
@@ -1140,14 +1597,43 @@ function bindSettingsActions() {
|
|
|
1140
1597
|
|
|
1141
1598
|
document.getElementById('tier-form').addEventListener('submit', async (e) => {
|
|
1142
1599
|
e.preventDefault();
|
|
1143
|
-
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
|
+
|
|
1144
1627
|
const body = {
|
|
1145
1628
|
name: document.getElementById('tier-name').value,
|
|
1146
1629
|
description: document.getElementById('tier-description').value,
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1630
|
+
allowed_tools,
|
|
1631
|
+
topics,
|
|
1632
|
+
goals,
|
|
1633
|
+
manifest: {
|
|
1634
|
+
topics: manifestTopics,
|
|
1635
|
+
objectives: manifestObjectives
|
|
1636
|
+
}
|
|
1151
1637
|
};
|
|
1152
1638
|
await request(`/settings/tiers/${encodeURIComponent(tierId)}`, {
|
|
1153
1639
|
method: 'PUT',
|
|
@@ -1158,7 +1644,7 @@ function bindSettingsActions() {
|
|
|
1158
1644
|
});
|
|
1159
1645
|
|
|
1160
1646
|
document.getElementById('copy-tier-btn').addEventListener('click', async () => {
|
|
1161
|
-
const toTier = document.getElementById('tier-
|
|
1647
|
+
const toTier = document.getElementById('tier-select').value;
|
|
1162
1648
|
const fromTier = document.getElementById('copy-from-tier').value;
|
|
1163
1649
|
if (!toTier || !fromTier || toTier === fromTier) return;
|
|
1164
1650
|
await request(`/settings/tiers/${encodeURIComponent(toTier)}/copy-from/${encodeURIComponent(fromTier)}`, {
|
|
@@ -1206,12 +1692,23 @@ function bindSettingsActions() {
|
|
|
1206
1692
|
document.getElementById('tier-select').value = tierId;
|
|
1207
1693
|
renderTierEditor(tierId);
|
|
1208
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
|
+
});
|
|
1209
1705
|
}
|
|
1210
1706
|
|
|
1211
1707
|
async function loadSettings() {
|
|
1212
1708
|
const payload = await request('/settings');
|
|
1213
1709
|
state.settings = payload;
|
|
1214
1710
|
fillTierSelects();
|
|
1711
|
+
renderTierColumns();
|
|
1215
1712
|
document.getElementById('defaults-expiration').value = payload.defaults?.expiration || '7d';
|
|
1216
1713
|
document.getElementById('defaults-max-calls').value = payload.defaults?.maxCalls || 100;
|
|
1217
1714
|
}
|
|
@@ -1581,7 +2078,7 @@ const tabLoaders = {
|
|
|
1581
2078
|
contacts: loadContacts,
|
|
1582
2079
|
calls: loadCalls,
|
|
1583
2080
|
logs: () => { loadLogs(); loadLogStats(); },
|
|
1584
|
-
|
|
2081
|
+
permissions: () => {},
|
|
1585
2082
|
invites: loadInvites,
|
|
1586
2083
|
};
|
|
1587
2084
|
|
|
@@ -1609,6 +2106,7 @@ async function bootstrap() {
|
|
|
1609
2106
|
bindTabs();
|
|
1610
2107
|
bindContactsActions();
|
|
1611
2108
|
bindSettingsActions();
|
|
2109
|
+
bindItemListDelegation();
|
|
1612
2110
|
bindCallbookActions();
|
|
1613
2111
|
bindAutoUpdateActions();
|
|
1614
2112
|
bindInviteActions();
|