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
|
@@ -0,0 +1,1273 @@
|
|
|
1
|
+
# A2A-41: Dashboard Permissions Tab Overhaul — Implementation Plan (Revision 1)
|
|
2
|
+
|
|
3
|
+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
4
|
+
|
|
5
|
+
**Goal:** Rename the Settings tab to Permissions, remove the dead disclosure field (frontend + backend), replace plain textareas with rich interactive UI (expandable topic/goal rows, tool checkboxes, three-column drag-and-drop, validation warnings, caller preview dialog).
|
|
6
|
+
|
|
7
|
+
**Architecture:** All changes are in the existing dashboard SPA (vanilla JS + Shoelace v2.19 CDN) and the Express backend routes. No new dependencies, no build step. Frontend changes in 3 files (index.html, app.js, style.css), backend changes in 5 files (config.js, tokens.js, a2a.js, dashboard.js, cli.js). The dashboard reads tier data from `GET /api/a2a/dashboard/settings` which already returns both flat config arrays AND manifest objects with descriptions — we leverage the manifest data for rich topic/goal display.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Vanilla JS, Shoelace v2.19 web components (CDN), HTML5 Drag and Drop API, Express 4
|
|
10
|
+
|
|
11
|
+
**Revision Notes (from reviewer cycles 1+2):**
|
|
12
|
+
- Split into single branch but builder monitors diff size per-commit and flags if approaching 500 lines
|
|
13
|
+
- Use event delegation for topic/goal list events (not per-row binding) to prevent listener leaks
|
|
14
|
+
- Use `Promise.all()` for drag-and-drop saves with rollback on failure
|
|
15
|
+
- Fix topic count warning to avoid double-counting (use `manifest || flat`, not `manifest + flat`)
|
|
16
|
+
- Use `topic` key (NOT `objective`) for manifest objectives in save handler — `parseTopicObjects()` only reads `entry.topic`
|
|
17
|
+
- Fix renderTierWarnings empty-array truthiness: use `.length > 0` gate instead of `||` (empty arrays are truthy in JS)
|
|
18
|
+
- Add responsive CSS for three-column layout under 720px
|
|
19
|
+
- Document that HTML5 DnD does not work on touch devices
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Reuse Map
|
|
24
|
+
|
|
25
|
+
These existing code elements MUST be used — do NOT recreate them:
|
|
26
|
+
|
|
27
|
+
| What | Where | Why |
|
|
28
|
+
|------|-------|-----|
|
|
29
|
+
| `esc()` | `app.js:203` | HTML escaping in template literals |
|
|
30
|
+
| `request(path, opts)` | `app.js:171` | All dashboard API calls |
|
|
31
|
+
| `showNotice(msg)` | `app.js:25` | User feedback messages |
|
|
32
|
+
| `toLines(arr)` / `fromLines(str)` | `app.js:183-190` | Array↔textarea conversion (keep for fallback) |
|
|
33
|
+
| `parseTopicObjects(values)` | `dashboard.js:145` | Backend already parses `{topic, description}` objects |
|
|
34
|
+
| `sanitizeString()` | `dashboard.js:91` | Input sanitization |
|
|
35
|
+
| `sanitizeStringArray()` | `dashboard.js:129` | Array sanitization |
|
|
36
|
+
| `loadManifest()` / `saveManifest()` | imported from `disclosure.js` in `dashboard.js:18` | Manifest persistence |
|
|
37
|
+
| `state.settings` | `app.js:2` | Client-side settings cache (tiers, manifest, defaults) |
|
|
38
|
+
| `fillTierSelects()` | `app.js:1098` | Populates all tier dropdowns |
|
|
39
|
+
| `renderTierEditor(tierId)` | `app.js:1123` | Renders the tier edit form |
|
|
40
|
+
| `bindSettingsActions()` | `app.js:1136` | Event binding for settings tab |
|
|
41
|
+
| `loadSettings()` | `app.js:1211` | Fetches settings from API |
|
|
42
|
+
|
|
43
|
+
## Anti-Patterns (Do NOT)
|
|
44
|
+
|
|
45
|
+
- Do NOT use `console.log` in app.js — use `showNotice()` for user feedback
|
|
46
|
+
- Do NOT add npm dependencies — Shoelace is CDN-loaded, drag-and-drop is native
|
|
47
|
+
- Do NOT modify `src/lib/disclosure.js` — the disclosure SYSTEM stays; only the per-tier `disclosure` field is removed
|
|
48
|
+
- Do NOT touch `src/server.js` — route mounting is automatic
|
|
49
|
+
- Do NOT break the `a2a create --disclosure` CLI flag in the onboarding flow (`bin/cli.js:680-697`) — that code patches tiers during quickstart and still uses `disclosure` as a tier config field. Since we're removing the field from `validateTierPatch()`, the `config.setTier()` calls will simply ignore `disclosure` gracefully
|
|
50
|
+
- Do NOT bind per-row event listeners — use event delegation on container elements (same pattern as contacts panel at `app.js:564`)
|
|
51
|
+
|
|
52
|
+
## File Change Map
|
|
53
|
+
|
|
54
|
+
| Action | File | What changes |
|
|
55
|
+
|--------|------|-------------|
|
|
56
|
+
| Modify | `src/dashboard/public/index.html` | Rename tab/panel `settings`→`permissions`, remove tier-id + disclosure inputs, replace textareas with container divs, add warnings div, preview button + dialog, tier-columns layout, toggle checkbox |
|
|
57
|
+
| Modify | `src/dashboard/public/app.js` | Rewrite `fillTierSelects()` (emojis, default public), rewrite `renderTierEditor()` (no disclosure/tier-id, call new render functions), add `renderTopicList()`, `renderGoalList()`, `renderToolCheckboxes()`, `renderTierColumns()`, `renderTierWarnings()`, `openCallerPreview()`, `getPreviewData()`, drag handlers via event delegation, update `bindSettingsActions()`, update `tabLoaders` key, update `bindTabs()` hash alias |
|
|
58
|
+
| Modify | `src/dashboard/public/style.css` | Add topic-row, drag-handle, tier-columns, tier-drop-zone, tools-checklist, tier-warnings, preview-dialog styles, responsive breakpoint |
|
|
59
|
+
| Modify | `src/lib/config.js` | Remove `disclosure` from 4 tier defaults (lines 194, 204, 214, 224), remove disclosure validation block (lines 109-118) |
|
|
60
|
+
| Modify | `src/lib/tokens.js` | Remove `disclosure` param from `create()` (line 197), remove from record (line 276), remove from `validate()` return (line 362) |
|
|
61
|
+
| Modify | `src/routes/a2a.js` | Remove `disclosure: validation.disclosure` (line 353) |
|
|
62
|
+
| Modify | `src/routes/dashboard.js` | Remove `disclosure` from GET /settings tier response (line 1279), remove from PUT handler (line 1313), remove from POST handler (line 1378) |
|
|
63
|
+
| Modify | `bin/cli.js` | Remove `disclosure` from `create()` call (line 880), remove disclosure console.log (line 925), add `permissions` alias in gui `--tab` (line 1690), remove `--disclosure` from help text (line 2990) |
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Task 1: Branch Setup + Backend Disclosure Removal
|
|
68
|
+
|
|
69
|
+
**Files:**
|
|
70
|
+
- Modify: `src/lib/config.js:109-118,194,204,214,224`
|
|
71
|
+
- Modify: `src/lib/tokens.js:197,276,362`
|
|
72
|
+
- Modify: `src/routes/a2a.js:353`
|
|
73
|
+
- Modify: `src/routes/dashboard.js:1279,1313,1378`
|
|
74
|
+
- Modify: `bin/cli.js:680-697,880,925,2990`
|
|
75
|
+
|
|
76
|
+
**Step 1: Create feature branch**
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
git fetch origin main
|
|
80
|
+
git checkout origin/main
|
|
81
|
+
git checkout -b feature/a2a-41
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Step 2: Remove disclosure from config.js tier defaults**
|
|
85
|
+
|
|
86
|
+
In `src/lib/config.js`, remove the `disclosure` key from all 4 tier objects in `DEFAULT_CONFIG.tiers` (lines 194, 204, 214, 224). Add a comment at the top of the tiers block:
|
|
87
|
+
|
|
88
|
+
```javascript
|
|
89
|
+
// A2A-41: disclosure field removed from tiers. It was intended to control
|
|
90
|
+
// HOW info is shared (freely/minimally/none) but was never consumed by
|
|
91
|
+
// prompt templates. The disclosure SYSTEM (disclosure.js, manifest) remains.
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Remove the disclosure validation block (lines 109-118) from `validateTierPatch()`. Replace with:
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
// A2A-41: disclosure field intentionally removed. If present in input,
|
|
98
|
+
// it is silently ignored for backward compatibility with older clients.
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Step 3: Remove disclosure from tokens.js**
|
|
102
|
+
|
|
103
|
+
In `src/lib/tokens.js`:
|
|
104
|
+
- Line 197: Remove `disclosure = 'minimal',` from destructuring in `create()`
|
|
105
|
+
- Line 276: Remove `disclosure,` from the record object
|
|
106
|
+
- Line 362: Remove `disclosure: record.disclosure,` from validate() return
|
|
107
|
+
|
|
108
|
+
Add comment near top of `create()`:
|
|
109
|
+
|
|
110
|
+
```javascript
|
|
111
|
+
// A2A-41: disclosure field removed from tokens. Was never used by
|
|
112
|
+
// prompt generation. Existing tokens with disclosure field are harmless
|
|
113
|
+
// — the field is simply not read.
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Step 4: Remove disclosure from a2a.js**
|
|
117
|
+
|
|
118
|
+
In `src/routes/a2a.js` line 353: Remove `disclosure: validation.disclosure,` from the `a2aContext` object.
|
|
119
|
+
|
|
120
|
+
**Step 5: Remove disclosure from dashboard.js**
|
|
121
|
+
|
|
122
|
+
In `src/routes/dashboard.js`:
|
|
123
|
+
- Line 1279: Remove `disclosure: configTier.disclosure || 'minimal',` from GET /settings response
|
|
124
|
+
- Line 1313: Remove `if (body.disclosure !== undefined) update.disclosure = sanitizeString(body.disclosure, 40) || 'minimal';` from PUT handler
|
|
125
|
+
- Line 1378: Remove `disclosure: sanitizeString(body.disclosure || 'minimal', 40),` from POST handler
|
|
126
|
+
|
|
127
|
+
**Step 6: Remove disclosure from CLI**
|
|
128
|
+
|
|
129
|
+
In `bin/cli.js`:
|
|
130
|
+
- Lines 685, 691, 697: Remove `disclosure: 'minimal'`, `disclosure: 'standard'`, `disclosure: 'full'` from the tier patches
|
|
131
|
+
- Line 880: Remove `disclosure: args.flags.disclosure || args.flags.d || 'minimal',` from `store.create()` call
|
|
132
|
+
- Line 925: Remove `console.log(\`Disclosure: ${record.disclosure}\`);`
|
|
133
|
+
- Line 2990: Remove `--disclosure, -d Disclosure level (public, minimal, none)` from help text
|
|
134
|
+
|
|
135
|
+
**Step 7: Run tests**
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm test
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Expected: 328 passing, 2 failing (pre-existing). No NEW failures.
|
|
142
|
+
|
|
143
|
+
**Step 8: Commit**
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
git add src/lib/config.js src/lib/tokens.js src/routes/a2a.js src/routes/dashboard.js bin/cli.js
|
|
147
|
+
git commit -m "feat(a2a-41): remove disclosure field from tiers, tokens, and routes"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Task 2: Rename Settings → Permissions + Tier Dropdown Polish
|
|
153
|
+
|
|
154
|
+
Combines the rename and dropdown polish into one task since they both touch the same files.
|
|
155
|
+
|
|
156
|
+
**Files:**
|
|
157
|
+
- Modify: `src/dashboard/public/index.html:21,67-68,76,79`
|
|
158
|
+
- Modify: `src/dashboard/public/app.js:252-278,1098-1134,1136-1161,1584`
|
|
159
|
+
- Modify: `bin/cli.js:1689-1691,3034`
|
|
160
|
+
|
|
161
|
+
**Step 1: Rename tab, panel, heading in index.html**
|
|
162
|
+
|
|
163
|
+
- Line 21: `<sl-tab slot="nav" panel="settings">Settings</sl-tab>` → `<sl-tab slot="nav" panel="permissions">Permissions</sl-tab>`
|
|
164
|
+
- Line 67: `<sl-tab-panel name="settings">` → `<sl-tab-panel name="permissions">`
|
|
165
|
+
- Line 68: `<h2>Tier Settings</h2>` → `<h2>Permission Tiers</h2>`
|
|
166
|
+
|
|
167
|
+
**Step 2: Remove tier-id and disclosure inputs from index.html**
|
|
168
|
+
|
|
169
|
+
- Delete line 76: `<sl-input id="tier-id" label="Tier ID" readonly></sl-input>`
|
|
170
|
+
- Delete line 79: `<sl-input id="tier-disclosure" label="Disclosure" placeholder="minimal"></sl-input>`
|
|
171
|
+
|
|
172
|
+
**Step 3: Update tabLoaders and hash alias in app.js**
|
|
173
|
+
|
|
174
|
+
Line 1584: Change `settings: () => {},` to `permissions: () => {},`
|
|
175
|
+
|
|
176
|
+
In `bindTabs()` (app.js ~line 263), update `activateFromHash()`:
|
|
177
|
+
|
|
178
|
+
```javascript
|
|
179
|
+
const activateFromHash = () => {
|
|
180
|
+
let hash = window.location.hash.slice(1);
|
|
181
|
+
// A2A-41: backward-compat alias — old bookmarks/links using #settings
|
|
182
|
+
// still work after rename to #permissions
|
|
183
|
+
if (hash === 'settings') hash = 'permissions';
|
|
184
|
+
if (hash) {
|
|
185
|
+
try { tabGroup.show(hash); } catch (err) {}
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Step 4: Rewrite fillTierSelects() with emojis and default-to-public**
|
|
191
|
+
|
|
192
|
+
Replace the existing function (lines 1098-1121) with:
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
// A2A-41: emoji map for visual tier differentiation. Standard tiers get
|
|
196
|
+
// recognizable icons; custom/user-created tiers get a wrench.
|
|
197
|
+
const TIER_EMOJIS = { public: '\u{1F310}', friends: '\u{1F46B}', family: '\u{1F468}\u200D\u{1F469}\u200D\u{1F467}\u200D\u{1F466}' };
|
|
198
|
+
|
|
199
|
+
function fillTierSelects() {
|
|
200
|
+
const tiers = (state.settings?.tiers || []).slice().sort((a, b) => a.id.localeCompare(b.id));
|
|
201
|
+
const tierSelect = document.getElementById('tier-select');
|
|
202
|
+
const copyFrom = document.getElementById('copy-from-tier');
|
|
203
|
+
const newTierCopy = document.getElementById('new-tier-copy-from');
|
|
204
|
+
const inviteTier = document.getElementById('invite-tier');
|
|
205
|
+
|
|
206
|
+
const optionsHtml = tiers.map(tier => {
|
|
207
|
+
const emoji = TIER_EMOJIS[tier.id] || '\u{1F527}';
|
|
208
|
+
return `<sl-option value="${esc(tier.id)}">${emoji} ${esc(tier.name || tier.id)}</sl-option>`;
|
|
209
|
+
}).join('');
|
|
210
|
+
|
|
211
|
+
tierSelect.innerHTML = optionsHtml;
|
|
212
|
+
copyFrom.innerHTML = optionsHtml;
|
|
213
|
+
inviteTier.innerHTML = optionsHtml;
|
|
214
|
+
newTierCopy.innerHTML = `<sl-option value="">None</sl-option>${optionsHtml}`;
|
|
215
|
+
|
|
216
|
+
// A2A-41: default to 'public' — it's the base tier and most commonly edited
|
|
217
|
+
const defaultTier = tiers.find(t => t.id === 'public') ? 'public' : tiers[0]?.id;
|
|
218
|
+
if (defaultTier) {
|
|
219
|
+
tierSelect.value = defaultTier;
|
|
220
|
+
copyFrom.value = defaultTier;
|
|
221
|
+
inviteTier.value = defaultTier;
|
|
222
|
+
renderTierEditor(defaultTier);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
**Step 5: Update renderTierEditor() — remove disclosure/tier-id lines**
|
|
228
|
+
|
|
229
|
+
Remove:
|
|
230
|
+
- `document.getElementById('tier-id').value = tier.id;` (line 1127)
|
|
231
|
+
- `document.getElementById('tier-disclosure').value = tier.disclosure || 'minimal';` (line 1130)
|
|
232
|
+
|
|
233
|
+
**Step 6: Update save handler — remove disclosure, use tier-select for ID**
|
|
234
|
+
|
|
235
|
+
In the tier-form submit handler (line 1143): Change `document.getElementById('tier-id').value` to `document.getElementById('tier-select').value`
|
|
236
|
+
|
|
237
|
+
Remove `disclosure: document.getElementById('tier-disclosure').value,` (line 1147)
|
|
238
|
+
|
|
239
|
+
Update copy-tier handler (line 1161): Change `document.getElementById('tier-id').value` to `document.getElementById('tier-select').value`
|
|
240
|
+
|
|
241
|
+
**Step 7: Update CLI gui --tab**
|
|
242
|
+
|
|
243
|
+
In `bin/cli.js` line 1689-1691:
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
const tab = (args.flags.tab || args.flags.t || '').trim().toLowerCase();
|
|
247
|
+
// A2A-41: 'settings' remains as backward-compat alias for 'permissions'
|
|
248
|
+
const tabAliases = { settings: 'permissions' };
|
|
249
|
+
const resolvedTab = tabAliases[tab] || tab;
|
|
250
|
+
const allowedTabs = new Set(['contacts', 'calls', 'logs', 'permissions', 'invites']);
|
|
251
|
+
const hash = allowedTabs.has(resolvedTab) ? `#${resolvedTab}` : '';
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Update help text (line 3034) to say `contacts|calls|logs|permissions|invites`
|
|
255
|
+
|
|
256
|
+
**Step 8: Run tests**
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
npm test
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Step 9: Commit**
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
git add src/dashboard/public/index.html src/dashboard/public/app.js bin/cli.js
|
|
266
|
+
git commit -m "feat(a2a-41): rename Settings to Permissions, tier emojis, default public, remove dead fields"
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## Task 3: CSS Styles + Rich Topic/Goal/Tool UI
|
|
272
|
+
|
|
273
|
+
**Files:**
|
|
274
|
+
- Modify: `src/dashboard/public/style.css`
|
|
275
|
+
- Modify: `src/dashboard/public/index.html:80-82`
|
|
276
|
+
- Modify: `src/dashboard/public/app.js` — add constants, render functions, event delegation, update save handler
|
|
277
|
+
|
|
278
|
+
**Step 1: Add all new CSS to style.css**
|
|
279
|
+
|
|
280
|
+
Insert before the existing `@media (max-width: 720px)` block at line 204:
|
|
281
|
+
|
|
282
|
+
```css
|
|
283
|
+
/* ── A2A-41: Topic/Goal list rows ──────────────────────────── */
|
|
284
|
+
.topic-row {
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: flex-start;
|
|
287
|
+
gap: 0.5rem;
|
|
288
|
+
padding: 0.5rem 0.6rem;
|
|
289
|
+
border: 1px solid var(--line);
|
|
290
|
+
border-radius: 6px;
|
|
291
|
+
margin-bottom: 0.4rem;
|
|
292
|
+
background: var(--panel);
|
|
293
|
+
cursor: grab;
|
|
294
|
+
transition: box-shadow 0.15s ease, border-color 0.15s ease;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.topic-row:hover {
|
|
298
|
+
border-color: var(--accent);
|
|
299
|
+
box-shadow: 0 1px 4px rgba(20, 102, 193, 0.1);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.topic-row.dragging {
|
|
303
|
+
opacity: 0.5;
|
|
304
|
+
border-style: dashed;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.topic-row.inherited {
|
|
308
|
+
opacity: 0.55;
|
|
309
|
+
background: #f8fafc;
|
|
310
|
+
cursor: default;
|
|
311
|
+
border-style: dashed;
|
|
312
|
+
border-color: #c5cfd9;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.topic-row.inherited .drag-handle,
|
|
316
|
+
.topic-row.inherited .topic-delete-btn {
|
|
317
|
+
display: none;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.drag-handle {
|
|
321
|
+
color: #9ca8b5;
|
|
322
|
+
cursor: grab;
|
|
323
|
+
user-select: none;
|
|
324
|
+
font-size: 1.1rem;
|
|
325
|
+
line-height: 1;
|
|
326
|
+
padding-top: 2px;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.topic-content {
|
|
330
|
+
flex: 1;
|
|
331
|
+
min-width: 0;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.topic-header {
|
|
335
|
+
display: flex;
|
|
336
|
+
align-items: center;
|
|
337
|
+
gap: 0.4rem;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
.topic-label {
|
|
341
|
+
flex: 1;
|
|
342
|
+
font-size: 0.9rem;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.topic-description {
|
|
346
|
+
margin-top: 0.35rem;
|
|
347
|
+
padding-left: 0.1rem;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.topic-desc-text {
|
|
351
|
+
font-size: 0.82rem;
|
|
352
|
+
color: #4b5d73;
|
|
353
|
+
margin: 0 0 0.3rem;
|
|
354
|
+
line-height: 1.4;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.inherited-badge {
|
|
358
|
+
font-size: 0.72rem;
|
|
359
|
+
color: #7a8da0;
|
|
360
|
+
font-style: italic;
|
|
361
|
+
margin-left: auto;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
.add-item-btn {
|
|
365
|
+
width: 100%;
|
|
366
|
+
margin-top: 0.3rem;
|
|
367
|
+
border: 1px dashed var(--line);
|
|
368
|
+
border-radius: 6px;
|
|
369
|
+
padding: 0.4rem;
|
|
370
|
+
background: transparent;
|
|
371
|
+
color: #7a8da0;
|
|
372
|
+
cursor: pointer;
|
|
373
|
+
font-size: 0.85rem;
|
|
374
|
+
transition: border-color 0.15s, color 0.15s;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
.add-item-btn:hover {
|
|
378
|
+
border-color: var(--accent);
|
|
379
|
+
color: var(--accent);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ── A2A-41: Tools checklist ───────────────────────────────── */
|
|
383
|
+
.tools-checklist {
|
|
384
|
+
display: flex;
|
|
385
|
+
flex-direction: column;
|
|
386
|
+
gap: 0.35rem;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.tools-checklist sl-checkbox {
|
|
390
|
+
margin-bottom: 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.tools-checklist sl-checkbox::part(label) {
|
|
394
|
+
font-size: 0.88rem;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.tool-desc {
|
|
398
|
+
color: #4b5d73;
|
|
399
|
+
font-weight: normal;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ── A2A-41: Three-column tier layout ──────────────────────── */
|
|
403
|
+
.tier-columns {
|
|
404
|
+
display: grid;
|
|
405
|
+
grid-template-columns: 1fr 1fr 1fr;
|
|
406
|
+
gap: 0.8rem;
|
|
407
|
+
margin-bottom: 1.2rem;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
.tier-column {
|
|
411
|
+
border: 1px solid var(--line);
|
|
412
|
+
border-radius: 8px;
|
|
413
|
+
padding: 0.6rem;
|
|
414
|
+
background: #fbfdff;
|
|
415
|
+
min-height: 120px;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.tier-column h4 {
|
|
419
|
+
margin: 0 0 0.5rem;
|
|
420
|
+
font-size: 0.9rem;
|
|
421
|
+
color: var(--ink);
|
|
422
|
+
padding-bottom: 0.4rem;
|
|
423
|
+
border-bottom: 1px solid var(--line);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
.tier-drop-zone {
|
|
427
|
+
min-height: 60px;
|
|
428
|
+
transition: background 0.15s ease;
|
|
429
|
+
border-radius: 4px;
|
|
430
|
+
padding: 0.2rem;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.tier-drop-zone.drag-over {
|
|
434
|
+
background: rgba(20, 102, 193, 0.06);
|
|
435
|
+
outline: 2px dashed var(--accent);
|
|
436
|
+
outline-offset: -2px;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/* ── A2A-41: Validation warnings ───────────────────────────── */
|
|
440
|
+
.tier-warnings {
|
|
441
|
+
margin-bottom: 0.8rem;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
.tier-warning {
|
|
445
|
+
display: flex;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: 0.5rem;
|
|
448
|
+
padding: 0.45rem 0.7rem;
|
|
449
|
+
border-radius: 6px;
|
|
450
|
+
font-size: 0.82rem;
|
|
451
|
+
margin-bottom: 0.3rem;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.tier-warning.warn {
|
|
455
|
+
background: #fef3c7;
|
|
456
|
+
border: 1px solid #f59e0b;
|
|
457
|
+
color: #92400e;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
.tier-warning.danger {
|
|
461
|
+
background: #fee2e2;
|
|
462
|
+
border: 1px solid #ef4444;
|
|
463
|
+
color: #991b1b;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.tier-warning.info {
|
|
467
|
+
background: #dbeafe;
|
|
468
|
+
border: 1px solid #3b82f6;
|
|
469
|
+
color: #1e40af;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/* ── A2A-41: Preview dialog ────────────────────────────────── */
|
|
473
|
+
#preview-content h4 {
|
|
474
|
+
font-size: 0.88rem;
|
|
475
|
+
margin: 0.8rem 0 0.3rem;
|
|
476
|
+
color: var(--ink);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
#preview-content h4:first-child {
|
|
480
|
+
margin-top: 0;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
#preview-content ul {
|
|
484
|
+
margin: 0;
|
|
485
|
+
padding-left: 1.2rem;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
#preview-content li {
|
|
489
|
+
font-size: 0.85rem;
|
|
490
|
+
margin-bottom: 0.25rem;
|
|
491
|
+
line-height: 1.4;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
#preview-content li strong {
|
|
495
|
+
color: var(--ink);
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Then update the existing `@media (max-width: 720px)` block to include responsive collapse:
|
|
500
|
+
|
|
501
|
+
```css
|
|
502
|
+
@media (max-width: 720px) {
|
|
503
|
+
sl-tab-group::part(nav) {
|
|
504
|
+
overflow-x: auto;
|
|
505
|
+
}
|
|
506
|
+
/* A2A-41: collapse three-column layout to single column on narrow screens */
|
|
507
|
+
.tier-columns {
|
|
508
|
+
grid-template-columns: 1fr;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
**Step 2: Replace textareas in index.html**
|
|
514
|
+
|
|
515
|
+
Replace the 3 textarea lines (80-82):
|
|
516
|
+
```html
|
|
517
|
+
<sl-textarea id="tier-tools" label="Allowed Tools (one per line)" rows="5" placeholder="Read Grep Glob"></sl-textarea>
|
|
518
|
+
<sl-textarea id="tier-topics" label="Topics (one per line)" rows="6"></sl-textarea>
|
|
519
|
+
<sl-textarea id="tier-goals" label="Goals (one per line)" rows="6"></sl-textarea>
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
With:
|
|
523
|
+
```html
|
|
524
|
+
<label>Allowed Tools</label>
|
|
525
|
+
<div id="tier-tools-list" class="tools-checklist"></div>
|
|
526
|
+
<label>Topics</label>
|
|
527
|
+
<div id="tier-topics-list"></div>
|
|
528
|
+
<label>Goals</label>
|
|
529
|
+
<div id="tier-goals-list"></div>
|
|
530
|
+
```
|
|
531
|
+
|
|
532
|
+
**Step 3: Add constants to app.js**
|
|
533
|
+
|
|
534
|
+
Near the top (after `state` declaration):
|
|
535
|
+
|
|
536
|
+
```javascript
|
|
537
|
+
// A2A-41: tool descriptions for the checkbox UI. These match the tools
|
|
538
|
+
// available in Claude Code that an agent owner might want to expose to callers.
|
|
539
|
+
const TOOL_DESCRIPTIONS = {
|
|
540
|
+
'Bash': 'Execute shell commands \u2014 full access, can run anything',
|
|
541
|
+
'Bash(readonly)': 'Execute read-only shell commands \u2014 no writes, no installs',
|
|
542
|
+
'Read': 'Read files from the workspace',
|
|
543
|
+
'Grep': 'Search file contents with regex patterns',
|
|
544
|
+
'Glob': 'Find files by name patterns',
|
|
545
|
+
'WebSearch': 'Search the web for information',
|
|
546
|
+
'WebFetch': 'Fetch and read web page content'
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// A2A-41: standard tier order for inheritance. Custom tiers are not in this list.
|
|
550
|
+
const TIER_ORDER = ['public', 'friends', 'family'];
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
**Step 4: Add renderToolCheckboxes()**
|
|
554
|
+
|
|
555
|
+
```javascript
|
|
556
|
+
// A2A-41: renders tool checkboxes instead of a textarea. Each tool gets
|
|
557
|
+
// a checkbox with its description. Checked state comes from tier.allowed_tools.
|
|
558
|
+
function renderToolCheckboxes(allowedTools) {
|
|
559
|
+
const container = document.getElementById('tier-tools-list');
|
|
560
|
+
container.innerHTML = Object.entries(TOOL_DESCRIPTIONS).map(([tool, desc]) => {
|
|
561
|
+
const checked = (allowedTools || []).includes(tool) ? 'checked' : '';
|
|
562
|
+
return `<sl-checkbox value="${esc(tool)}" ${checked}><strong>${esc(tool)}</strong> \u2014 <span class="tool-desc">${esc(desc)}</span></sl-checkbox>`;
|
|
563
|
+
}).join('');
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Step 5: Add renderTopicList() — uses event delegation**
|
|
568
|
+
|
|
569
|
+
```javascript
|
|
570
|
+
// A2A-41: renders topics as expandable card rows with descriptions.
|
|
571
|
+
// Data comes from tier.manifest.topics (array of {topic, description} objects).
|
|
572
|
+
// Falls back to tier.topics (flat string array) for topics without manifest data.
|
|
573
|
+
function renderTopicList(tier) {
|
|
574
|
+
const container = document.getElementById('tier-topics-list');
|
|
575
|
+
const manifestTopics = tier.manifest?.topics || [];
|
|
576
|
+
const flatTopics = tier.topics || [];
|
|
577
|
+
|
|
578
|
+
// A2A-41: prefer manifest data (has descriptions), fall back to flat array
|
|
579
|
+
const allTopics = manifestTopics.length > 0
|
|
580
|
+
? manifestTopics.map(t => ({ label: t.topic, desc: t.description || '' }))
|
|
581
|
+
: flatTopics.map(t => ({ label: t, desc: '' }));
|
|
582
|
+
|
|
583
|
+
const rowsHtml = allTopics.map(t => `
|
|
584
|
+
<div class="topic-row" data-topic="${esc(t.label)}" data-type="topic">
|
|
585
|
+
<span class="drag-handle">\u2807</span>
|
|
586
|
+
<div class="topic-content">
|
|
587
|
+
<div class="topic-header">
|
|
588
|
+
<strong class="topic-label">${esc(t.label)}</strong>
|
|
589
|
+
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
590
|
+
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
591
|
+
</div>
|
|
592
|
+
<div class="topic-description" style="display:none;">
|
|
593
|
+
<p class="topic-desc-text">${esc(t.desc) || '<em>No description</em>'}</p>
|
|
594
|
+
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(t.desc)}"></sl-input>
|
|
595
|
+
</div>
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
`).join('');
|
|
599
|
+
|
|
600
|
+
container.innerHTML = rowsHtml + `<button class="add-item-btn" data-type="topic">+ Add topic</button>`;
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
**Step 6: Add renderGoalList()**
|
|
605
|
+
|
|
606
|
+
```javascript
|
|
607
|
+
// A2A-41: renders goals as expandable card rows, identical pattern to topics.
|
|
608
|
+
// Data from tier.manifest.objectives (array of {objective, description}).
|
|
609
|
+
function renderGoalList(tier) {
|
|
610
|
+
const container = document.getElementById('tier-goals-list');
|
|
611
|
+
const manifestGoals = tier.manifest?.objectives || [];
|
|
612
|
+
const flatGoals = tier.goals || [];
|
|
613
|
+
|
|
614
|
+
const allGoals = manifestGoals.length > 0
|
|
615
|
+
? manifestGoals.map(g => ({ label: g.objective || g.topic, desc: g.description || '' }))
|
|
616
|
+
: flatGoals.map(g => ({ label: g, desc: '' }));
|
|
617
|
+
|
|
618
|
+
const rowsHtml = allGoals.map(g => `
|
|
619
|
+
<div class="topic-row" data-topic="${esc(g.label)}" data-type="goal">
|
|
620
|
+
<span class="drag-handle">\u2807</span>
|
|
621
|
+
<div class="topic-content">
|
|
622
|
+
<div class="topic-header">
|
|
623
|
+
<strong class="topic-label">${esc(g.label)}</strong>
|
|
624
|
+
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
625
|
+
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
626
|
+
</div>
|
|
627
|
+
<div class="topic-description" style="display:none;">
|
|
628
|
+
<p class="topic-desc-text">${esc(g.desc) || '<em>No description</em>'}</p>
|
|
629
|
+
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(g.desc)}"></sl-input>
|
|
630
|
+
</div>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
`).join('');
|
|
634
|
+
|
|
635
|
+
container.innerHTML = rowsHtml + `<button class="add-item-btn" data-type="goal">+ Add goal</button>`;
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
**Step 7: Add event delegation for topic/goal lists**
|
|
640
|
+
|
|
641
|
+
CRITICAL: Use event delegation on the containers, NOT per-row binding. This prevents the listener leak the reviewer flagged.
|
|
642
|
+
|
|
643
|
+
```javascript
|
|
644
|
+
// A2A-41: event delegation for topic and goal list interactions.
|
|
645
|
+
// Uses a single click handler on each container instead of per-row binding,
|
|
646
|
+
// preventing listener accumulation when topics are added dynamically.
|
|
647
|
+
function bindItemListDelegation() {
|
|
648
|
+
['tier-topics-list', 'tier-goals-list'].forEach(containerId => {
|
|
649
|
+
const container = document.getElementById(containerId);
|
|
650
|
+
if (!container) return;
|
|
651
|
+
|
|
652
|
+
container.addEventListener('click', (e) => {
|
|
653
|
+
// Expand/collapse
|
|
654
|
+
const expandBtn = e.target.closest('.topic-expand-btn');
|
|
655
|
+
if (expandBtn) {
|
|
656
|
+
const row = expandBtn.closest('.topic-row');
|
|
657
|
+
const desc = row.querySelector('.topic-description');
|
|
658
|
+
if (desc) {
|
|
659
|
+
const isHidden = desc.style.display === 'none';
|
|
660
|
+
desc.style.display = isHidden ? '' : 'none';
|
|
661
|
+
expandBtn.name = isHidden ? 'chevron-up' : 'chevron-down';
|
|
662
|
+
}
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Delete
|
|
667
|
+
const deleteBtn = e.target.closest('.topic-delete-btn');
|
|
668
|
+
if (deleteBtn) {
|
|
669
|
+
deleteBtn.closest('.topic-row').remove();
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// Add new item
|
|
674
|
+
const addBtn = e.target.closest('.add-item-btn');
|
|
675
|
+
if (addBtn) {
|
|
676
|
+
const type = addBtn.dataset.type;
|
|
677
|
+
const label = type === 'topic' ? 'Topic name' : 'Goal name';
|
|
678
|
+
const newRow = document.createElement('div');
|
|
679
|
+
newRow.className = 'topic-row';
|
|
680
|
+
newRow.dataset.type = type;
|
|
681
|
+
newRow.innerHTML = `
|
|
682
|
+
<span class="drag-handle">\u2807</span>
|
|
683
|
+
<div class="topic-content">
|
|
684
|
+
<sl-input class="new-item-label" size="small" placeholder="${label}" autofocus></sl-input>
|
|
685
|
+
<sl-input class="new-item-desc" size="small" placeholder="Description (optional)"></sl-input>
|
|
686
|
+
<div class="row" style="margin-top:0.3rem;">
|
|
687
|
+
<sl-button size="small" variant="primary" class="confirm-add-btn">Add</sl-button>
|
|
688
|
+
<sl-button size="small" class="cancel-add-btn">Cancel</sl-button>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
`;
|
|
692
|
+
container.insertBefore(newRow, addBtn);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Confirm add
|
|
697
|
+
const confirmBtn = e.target.closest('.confirm-add-btn');
|
|
698
|
+
if (confirmBtn) {
|
|
699
|
+
const row = confirmBtn.closest('.topic-row');
|
|
700
|
+
const nameInput = row.querySelector('.new-item-label');
|
|
701
|
+
const descInput = row.querySelector('.new-item-desc');
|
|
702
|
+
const name = nameInput.value.trim();
|
|
703
|
+
if (!name) { nameInput.focus(); return; }
|
|
704
|
+
|
|
705
|
+
row.dataset.topic = name;
|
|
706
|
+
row.innerHTML = `
|
|
707
|
+
<span class="drag-handle">\u2807</span>
|
|
708
|
+
<div class="topic-content">
|
|
709
|
+
<div class="topic-header">
|
|
710
|
+
<strong class="topic-label">${esc(name)}</strong>
|
|
711
|
+
<sl-icon-button name="chevron-down" class="topic-expand-btn" label="Expand"></sl-icon-button>
|
|
712
|
+
<sl-icon-button name="trash" class="topic-delete-btn" label="Delete"></sl-icon-button>
|
|
713
|
+
</div>
|
|
714
|
+
<div class="topic-description" style="display:none;">
|
|
715
|
+
<p class="topic-desc-text">${esc(descInput.value)}</p>
|
|
716
|
+
<sl-input class="topic-desc-edit" size="small" placeholder="Add description..." value="${esc(descInput.value)}"></sl-input>
|
|
717
|
+
</div>
|
|
718
|
+
</div>
|
|
719
|
+
`;
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Cancel add
|
|
724
|
+
const cancelBtn = e.target.closest('.cancel-add-btn');
|
|
725
|
+
if (cancelBtn) {
|
|
726
|
+
cancelBtn.closest('.topic-row').remove();
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// Description edit via sl-change (Shoelace event, delegated)
|
|
732
|
+
container.addEventListener('sl-change', (e) => {
|
|
733
|
+
const input = e.target.closest('.topic-desc-edit');
|
|
734
|
+
if (input) {
|
|
735
|
+
const row = input.closest('.topic-row');
|
|
736
|
+
const textEl = row.querySelector('.topic-desc-text');
|
|
737
|
+
if (textEl) textEl.textContent = input.value || '';
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
**Step 8: Update renderTierEditor()**
|
|
745
|
+
|
|
746
|
+
```javascript
|
|
747
|
+
function renderTierEditor(tierId) {
|
|
748
|
+
const tier = (state.settings?.tiers || []).find(t => t.id === tierId);
|
|
749
|
+
if (!tier) return;
|
|
750
|
+
|
|
751
|
+
document.getElementById('tier-name').value = tier.name || tier.id;
|
|
752
|
+
document.getElementById('tier-description').value = tier.description || '';
|
|
753
|
+
renderToolCheckboxes(tier.allowed_tools);
|
|
754
|
+
renderTopicList(tier);
|
|
755
|
+
renderGoalList(tier);
|
|
756
|
+
renderTierWarnings(tier);
|
|
757
|
+
renderTierColumns();
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**Step 9: Update save handler — collect from new UI**
|
|
762
|
+
|
|
763
|
+
Replace the tier-form submit handler body:
|
|
764
|
+
|
|
765
|
+
```javascript
|
|
766
|
+
document.getElementById('tier-form').addEventListener('submit', async (e) => {
|
|
767
|
+
e.preventDefault();
|
|
768
|
+
const tierId = document.getElementById('tier-select').value;
|
|
769
|
+
|
|
770
|
+
// A2A-41: collect tools from checkboxes
|
|
771
|
+
const toolCheckboxes = document.querySelectorAll('#tier-tools-list sl-checkbox');
|
|
772
|
+
const allowed_tools = Array.from(toolCheckboxes)
|
|
773
|
+
.filter(cb => cb.checked)
|
|
774
|
+
.map(cb => cb.value);
|
|
775
|
+
|
|
776
|
+
// A2A-41: collect topics from row elements
|
|
777
|
+
const topicRows = document.querySelectorAll('#tier-topics-list .topic-row[data-topic]');
|
|
778
|
+
const topics = Array.from(topicRows).map(row => row.dataset.topic).filter(Boolean);
|
|
779
|
+
const manifestTopics = Array.from(topicRows).map(row => ({
|
|
780
|
+
topic: row.dataset.topic,
|
|
781
|
+
description: (row.querySelector('.topic-desc-edit')?.value || row.querySelector('.topic-desc-text')?.textContent || '').trim()
|
|
782
|
+
})).filter(t => t.topic);
|
|
783
|
+
|
|
784
|
+
// A2A-41: collect goals from row elements. IMPORTANT: use 'topic' key (NOT
|
|
785
|
+
// 'objective') because parseTopicObjects() in dashboard.js:160 only reads
|
|
786
|
+
// entry.topic. The semantic distinction 'objective' vs 'topic' is UI-only;
|
|
787
|
+
// the storage layer uses {topic, description} uniformly for both.
|
|
788
|
+
const goalRows = document.querySelectorAll('#tier-goals-list .topic-row[data-topic]');
|
|
789
|
+
const goals = Array.from(goalRows).map(row => row.dataset.topic).filter(Boolean);
|
|
790
|
+
const manifestObjectives = Array.from(goalRows).map(row => ({
|
|
791
|
+
topic: row.dataset.topic,
|
|
792
|
+
description: (row.querySelector('.topic-desc-edit')?.value || row.querySelector('.topic-desc-text')?.textContent || '').trim()
|
|
793
|
+
})).filter(g => g.topic);
|
|
794
|
+
|
|
795
|
+
const body = {
|
|
796
|
+
name: document.getElementById('tier-name').value,
|
|
797
|
+
description: document.getElementById('tier-description').value,
|
|
798
|
+
allowed_tools,
|
|
799
|
+
topics,
|
|
800
|
+
goals,
|
|
801
|
+
manifest: {
|
|
802
|
+
topics: manifestTopics,
|
|
803
|
+
objectives: manifestObjectives
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
await request(`/settings/tiers/${encodeURIComponent(tierId)}`, {
|
|
807
|
+
method: 'PUT',
|
|
808
|
+
body: JSON.stringify(body)
|
|
809
|
+
});
|
|
810
|
+
showNotice(`Saved tier "${tierId}"`);
|
|
811
|
+
await loadSettings();
|
|
812
|
+
});
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
**Step 10: Wire event delegation in bootstrap**
|
|
816
|
+
|
|
817
|
+
In `bindSettingsActions()` (or in `bootstrap()` after `bindSettingsActions()`), add:
|
|
818
|
+
|
|
819
|
+
```javascript
|
|
820
|
+
bindItemListDelegation();
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
**Step 11: Run tests**
|
|
824
|
+
|
|
825
|
+
```bash
|
|
826
|
+
npm test
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
**Step 12: Commit**
|
|
830
|
+
|
|
831
|
+
```bash
|
|
832
|
+
git add src/dashboard/public/style.css src/dashboard/public/index.html src/dashboard/public/app.js
|
|
833
|
+
git commit -m "feat(a2a-41): rich topic/goal lists, tool checkboxes, and all CSS styles"
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
---
|
|
837
|
+
|
|
838
|
+
## Task 4: Three-Column Drag-and-Drop + Validation Warnings + Caller Preview
|
|
839
|
+
|
|
840
|
+
**Files:**
|
|
841
|
+
- Modify: `src/dashboard/public/index.html` — add tier-columns, toggle, warnings, preview button + dialog
|
|
842
|
+
- Modify: `src/dashboard/public/app.js` — add `renderTierColumns()`, `bindDragEvents()`, `renderTierWarnings()`, `getPreviewData()`, `openCallerPreview()`
|
|
843
|
+
|
|
844
|
+
**Step 1: Add HTML elements to index.html permissions panel**
|
|
845
|
+
|
|
846
|
+
After the tier dropdown `.row` div and before `<form id="tier-form">`, add:
|
|
847
|
+
|
|
848
|
+
```html
|
|
849
|
+
<div class="row">
|
|
850
|
+
<label for="tier-select">Tier</label>
|
|
851
|
+
<sl-select id="tier-select" size="small" style="min-width:200px;"></sl-select>
|
|
852
|
+
<sl-button id="new-tier-btn" size="small">New Tier</sl-button>
|
|
853
|
+
<sl-button id="preview-caller-btn" size="small" variant="neutral">\u{1F441} Preview as Caller</sl-button>
|
|
854
|
+
</div>
|
|
855
|
+
|
|
856
|
+
<div id="tier-warnings" class="tier-warnings"></div>
|
|
857
|
+
|
|
858
|
+
<sl-checkbox id="show-drag-columns">Show all tiers side-by-side</sl-checkbox>
|
|
859
|
+
<div id="tier-columns" class="tier-columns"></div>
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
Note: the `.row` div already exists — just add the preview button into it and add the new elements below it.
|
|
863
|
+
|
|
864
|
+
At the end of the permissions panel (before `</sl-tab-panel>`), add:
|
|
865
|
+
|
|
866
|
+
```html
|
|
867
|
+
<sl-dialog id="preview-dialog" label="Caller Preview" style="--width: 540px;">
|
|
868
|
+
<div id="preview-content"></div>
|
|
869
|
+
<sl-button slot="footer" variant="primary" id="preview-close-btn">Close</sl-button>
|
|
870
|
+
</sl-dialog>
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
**Step 2: Add renderTierColumns() to app.js**
|
|
874
|
+
|
|
875
|
+
```javascript
|
|
876
|
+
// A2A-41: renders the three-column drag zone showing all standard tiers
|
|
877
|
+
// side-by-side. Inherited topics shown as grayed-out non-draggable rows.
|
|
878
|
+
// Custom tiers are not shown here — they don't have a defined inheritance
|
|
879
|
+
// hierarchy. HTML5 drag-and-drop does NOT work on touch devices (mobile).
|
|
880
|
+
function renderTierColumns() {
|
|
881
|
+
const container = document.getElementById('tier-columns');
|
|
882
|
+
if (!container) return;
|
|
883
|
+
const tiers = state.settings?.tiers || [];
|
|
884
|
+
const toggle = document.getElementById('show-drag-columns');
|
|
885
|
+
container.style.display = toggle?.checked ? '' : 'none';
|
|
886
|
+
|
|
887
|
+
const html = TIER_ORDER.map(tierId => {
|
|
888
|
+
const tier = tiers.find(t => t.id === tierId);
|
|
889
|
+
if (!tier) return '';
|
|
890
|
+
|
|
891
|
+
const emoji = TIER_EMOJIS[tierId] || '\u{1F527}';
|
|
892
|
+
const tierIdx = TIER_ORDER.indexOf(tierId);
|
|
893
|
+
|
|
894
|
+
// Inherited topics from lower tiers
|
|
895
|
+
let inheritedRows = '';
|
|
896
|
+
for (let i = 0; i < tierIdx; i++) {
|
|
897
|
+
const lowerTier = tiers.find(t => t.id === TIER_ORDER[i]);
|
|
898
|
+
if (!lowerTier) continue;
|
|
899
|
+
const lowerTopics = lowerTier.manifest?.topics?.length
|
|
900
|
+
? lowerTier.manifest.topics
|
|
901
|
+
: (lowerTier.topics || []).map(t => ({ topic: t, description: '' }));
|
|
902
|
+
lowerTopics.forEach(t => {
|
|
903
|
+
inheritedRows += `
|
|
904
|
+
<div class="topic-row inherited" data-topic="${esc(t.topic)}" data-tier="${esc(TIER_ORDER[i])}">
|
|
905
|
+
<div class="topic-content">
|
|
906
|
+
<div class="topic-header">
|
|
907
|
+
<strong class="topic-label">${esc(t.topic)}</strong>
|
|
908
|
+
<span class="inherited-badge">from ${esc(TIER_ORDER[i])}</span>
|
|
909
|
+
</div>
|
|
910
|
+
</div>
|
|
911
|
+
</div>`;
|
|
912
|
+
});
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// Own topics — draggable
|
|
916
|
+
const ownTopics = tier.manifest?.topics?.length
|
|
917
|
+
? tier.manifest.topics
|
|
918
|
+
: (tier.topics || []).map(t => ({ topic: t, description: '' }));
|
|
919
|
+
const ownRows = ownTopics.map(t => `
|
|
920
|
+
<div class="topic-row" draggable="true" data-topic="${esc(t.topic)}" data-tier="${esc(tierId)}">
|
|
921
|
+
<span class="drag-handle">\u2807</span>
|
|
922
|
+
<div class="topic-content">
|
|
923
|
+
<div class="topic-header">
|
|
924
|
+
<strong class="topic-label">${esc(t.topic)}</strong>
|
|
925
|
+
</div>
|
|
926
|
+
</div>
|
|
927
|
+
</div>
|
|
928
|
+
`).join('');
|
|
929
|
+
|
|
930
|
+
return `
|
|
931
|
+
<div class="tier-column" data-tier="${esc(tierId)}">
|
|
932
|
+
<h4>${emoji} ${esc(tier.name || tierId)}</h4>
|
|
933
|
+
<div class="tier-drop-zone" data-tier="${esc(tierId)}">
|
|
934
|
+
${inheritedRows}${ownRows}
|
|
935
|
+
</div>
|
|
936
|
+
</div>`;
|
|
937
|
+
}).join('');
|
|
938
|
+
|
|
939
|
+
container.innerHTML = html;
|
|
940
|
+
bindDragEvents();
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
**Step 3: Add bindDragEvents() with Promise.all and error recovery**
|
|
945
|
+
|
|
946
|
+
```javascript
|
|
947
|
+
// A2A-41: HTML5 drag-and-drop handlers for moving topics between tier columns.
|
|
948
|
+
// On drop, both tiers are saved via Promise.all() to prevent data loss if one
|
|
949
|
+
// request fails. On error, state is reloaded from server to reset UI.
|
|
950
|
+
function bindDragEvents() {
|
|
951
|
+
const zones = document.querySelectorAll('.tier-drop-zone');
|
|
952
|
+
|
|
953
|
+
document.querySelectorAll('.tier-columns .topic-row[draggable="true"]').forEach(row => {
|
|
954
|
+
row.addEventListener('dragstart', (e) => {
|
|
955
|
+
e.dataTransfer.setData('application/json', JSON.stringify({
|
|
956
|
+
topic: row.dataset.topic,
|
|
957
|
+
sourceTier: row.dataset.tier
|
|
958
|
+
}));
|
|
959
|
+
row.classList.add('dragging');
|
|
960
|
+
});
|
|
961
|
+
row.addEventListener('dragend', () => row.classList.remove('dragging'));
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
zones.forEach(zone => {
|
|
965
|
+
zone.addEventListener('dragover', (e) => {
|
|
966
|
+
e.preventDefault();
|
|
967
|
+
zone.classList.add('drag-over');
|
|
968
|
+
});
|
|
969
|
+
zone.addEventListener('dragleave', () => zone.classList.remove('drag-over'));
|
|
970
|
+
zone.addEventListener('drop', async (e) => {
|
|
971
|
+
e.preventDefault();
|
|
972
|
+
zone.classList.remove('drag-over');
|
|
973
|
+
|
|
974
|
+
let data;
|
|
975
|
+
try { data = JSON.parse(e.dataTransfer.getData('application/json')); } catch { return; }
|
|
976
|
+
const { topic, sourceTier } = data;
|
|
977
|
+
const targetTier = zone.dataset.tier;
|
|
978
|
+
|
|
979
|
+
if (!topic || !sourceTier || !targetTier || sourceTier === targetTier) return;
|
|
980
|
+
|
|
981
|
+
const sourceTierData = (state.settings?.tiers || []).find(t => t.id === sourceTier);
|
|
982
|
+
const targetTierData = (state.settings?.tiers || []).find(t => t.id === targetTier);
|
|
983
|
+
if (!sourceTierData || !targetTierData) return;
|
|
984
|
+
|
|
985
|
+
const sourceTopics = (sourceTierData.topics || []).filter(t => t !== topic);
|
|
986
|
+
const sourceManifestTopics = (sourceTierData.manifest?.topics || []).filter(t => t.topic !== topic);
|
|
987
|
+
const movedManifest = (sourceTierData.manifest?.topics || []).find(t => t.topic === topic);
|
|
988
|
+
const targetTopics = [...(targetTierData.topics || []), topic];
|
|
989
|
+
const targetManifestTopics = [...(targetTierData.manifest?.topics || []), movedManifest || { topic, description: '' }];
|
|
990
|
+
|
|
991
|
+
// A2A-41: save both tiers atomically with Promise.all to prevent
|
|
992
|
+
// data loss if one request fails. On error, reload from server.
|
|
993
|
+
try {
|
|
994
|
+
await Promise.all([
|
|
995
|
+
request(`/settings/tiers/${encodeURIComponent(sourceTier)}`, {
|
|
996
|
+
method: 'PUT',
|
|
997
|
+
body: JSON.stringify({
|
|
998
|
+
topics: sourceTopics,
|
|
999
|
+
manifest: { topics: sourceManifestTopics, objectives: sourceTierData.manifest?.objectives || [] }
|
|
1000
|
+
})
|
|
1001
|
+
}),
|
|
1002
|
+
request(`/settings/tiers/${encodeURIComponent(targetTier)}`, {
|
|
1003
|
+
method: 'PUT',
|
|
1004
|
+
body: JSON.stringify({
|
|
1005
|
+
topics: targetTopics,
|
|
1006
|
+
manifest: { topics: targetManifestTopics, objectives: targetTierData.manifest?.objectives || [] }
|
|
1007
|
+
})
|
|
1008
|
+
})
|
|
1009
|
+
]);
|
|
1010
|
+
showNotice(`Moved "${topic}" from ${sourceTier} to ${targetTier}`);
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
showNotice(`Move failed: ${err.message}. Reloading...`);
|
|
1013
|
+
}
|
|
1014
|
+
await loadSettings();
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
```
|
|
1019
|
+
|
|
1020
|
+
**Step 4: Add renderTierWarnings() — fixed topic count logic**
|
|
1021
|
+
|
|
1022
|
+
```javascript
|
|
1023
|
+
// A2A-41: contextual validation warnings for the currently selected tier.
|
|
1024
|
+
// Warns about empty tiers, dangerous tool grants, and inverted tier sizes.
|
|
1025
|
+
function renderTierWarnings(tier) {
|
|
1026
|
+
const container = document.getElementById('tier-warnings');
|
|
1027
|
+
if (!container) return;
|
|
1028
|
+
const warnings = [];
|
|
1029
|
+
|
|
1030
|
+
// A2A-41: use manifest OR flat topics (not both) to avoid double-counting.
|
|
1031
|
+
// Manifest is preferred when non-empty; flat array is the fallback.
|
|
1032
|
+
// NOTE: can't use || for this because empty arrays are truthy in JS.
|
|
1033
|
+
const mTopics = tier.manifest?.topics;
|
|
1034
|
+
const topicCount = (mTopics && mTopics.length > 0 ? mTopics : (tier.topics || [])).length;
|
|
1035
|
+
if (topicCount === 0) {
|
|
1036
|
+
warnings.push({ level: 'warn', text: "This tier has no topics \u2014 callers won't have conversation context." });
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (tier.id === 'public' && (tier.allowed_tools || []).includes('Bash')) {
|
|
1040
|
+
warnings.push({ level: 'danger', text: 'Bash (full access) is granted to the public tier \u2014 any caller can execute commands.' });
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (tier.id === 'family') {
|
|
1044
|
+
const allTiers = state.settings?.tiers || [];
|
|
1045
|
+
const friends = allTiers.find(t => t.id === 'friends');
|
|
1046
|
+
if (friends) {
|
|
1047
|
+
const mFam = tier.manifest?.topics;
|
|
1048
|
+
const familyOwn = (mFam && mFam.length > 0 ? mFam : (tier.topics || [])).length;
|
|
1049
|
+
const mFri = friends.manifest?.topics;
|
|
1050
|
+
const friendsOwn = (mFri && mFri.length > 0 ? mFri : (friends.topics || [])).length;
|
|
1051
|
+
if (familyOwn < friendsOwn) {
|
|
1052
|
+
warnings.push({ level: 'info', text: 'Family tier has fewer topics than Friends \u2014 usually Family is the most open tier.' });
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
container.innerHTML = warnings.map(w =>
|
|
1058
|
+
`<div class="tier-warning ${w.level}">${esc(w.text)}</div>`
|
|
1059
|
+
).join('');
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
**Step 5: Add getPreviewData() and openCallerPreview()**
|
|
1064
|
+
|
|
1065
|
+
```javascript
|
|
1066
|
+
// A2A-41: merges topics/goals/tools from the selected tier and all lower tiers,
|
|
1067
|
+
// mirroring the backend's getTopicsForTier() inheritance. Used by the preview dialog.
|
|
1068
|
+
function getPreviewData(tierId) {
|
|
1069
|
+
const selectedIndex = TIER_ORDER.indexOf(tierId);
|
|
1070
|
+
const tiers = state.settings?.tiers || [];
|
|
1071
|
+
const merged = { topics: [], objectives: [], tools: new Set(), do_not_discuss: [], never_disclose: [] };
|
|
1072
|
+
|
|
1073
|
+
// A2A-41: for custom tiers not in TIER_ORDER, show only own data.
|
|
1074
|
+
// No inheritance is applied because custom tiers have no defined hierarchy.
|
|
1075
|
+
if (selectedIndex < 0) {
|
|
1076
|
+
const t = tiers.find(t => t.id === tierId);
|
|
1077
|
+
if (t) {
|
|
1078
|
+
(t.manifest?.topics || []).forEach(item => merged.topics.push({ ...item, source: tierId }));
|
|
1079
|
+
(t.manifest?.objectives || []).forEach(item => merged.objectives.push({ ...item, source: tierId }));
|
|
1080
|
+
(t.allowed_tools || []).forEach(tool => merged.tools.add(tool));
|
|
1081
|
+
}
|
|
1082
|
+
merged.never_disclose = state.settings?.manifest?.never_disclose || [];
|
|
1083
|
+
return merged;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
for (let i = 0; i <= selectedIndex; i++) {
|
|
1087
|
+
const t = tiers.find(t => t.id === TIER_ORDER[i]);
|
|
1088
|
+
if (!t) continue;
|
|
1089
|
+
(t.manifest?.topics || []).forEach(item => merged.topics.push({ ...item, source: TIER_ORDER[i] }));
|
|
1090
|
+
(t.manifest?.objectives || []).forEach(item => merged.objectives.push({ ...item, source: TIER_ORDER[i] }));
|
|
1091
|
+
(t.manifest?.do_not_discuss || []).forEach(item => {
|
|
1092
|
+
if (!merged.do_not_discuss.includes(item)) merged.do_not_discuss.push(item);
|
|
1093
|
+
});
|
|
1094
|
+
(t.allowed_tools || []).forEach(tool => merged.tools.add(tool));
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
merged.never_disclose = state.settings?.manifest?.never_disclose || [];
|
|
1098
|
+
return merged;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// A2A-41: opens the caller preview dialog showing the merged effective view
|
|
1102
|
+
// for the selected tier. Helps the agent owner understand what a caller sees.
|
|
1103
|
+
function openCallerPreview() {
|
|
1104
|
+
const tierId = document.getElementById('tier-select').value;
|
|
1105
|
+
const data = getPreviewData(tierId);
|
|
1106
|
+
const emoji = TIER_EMOJIS[tierId] || '\u{1F527}';
|
|
1107
|
+
const tierName = (state.settings?.tiers || []).find(t => t.id === tierId)?.name || tierId;
|
|
1108
|
+
|
|
1109
|
+
const dialog = document.getElementById('preview-dialog');
|
|
1110
|
+
dialog.label = `\u{1F441} Caller Preview \u2014 ${emoji} ${tierName}`;
|
|
1111
|
+
|
|
1112
|
+
const topicsList = data.topics.length > 0
|
|
1113
|
+
? data.topics.map(t => `<li><strong>${esc(t.topic)}</strong>${t.description ? ` \u2014 ${esc(t.description)}` : ''}</li>`).join('')
|
|
1114
|
+
: '<li><em>None configured</em></li>';
|
|
1115
|
+
|
|
1116
|
+
const goalsList = data.objectives.length > 0
|
|
1117
|
+
? data.objectives.map(g => `<li><strong>${esc(g.objective || g.topic)}</strong>${g.description ? ` \u2014 ${esc(g.description)}` : ''}</li>`).join('')
|
|
1118
|
+
: '<li><em>None configured</em></li>';
|
|
1119
|
+
|
|
1120
|
+
const toolsList = data.tools.size > 0
|
|
1121
|
+
? Array.from(data.tools).map(t => `<li><strong>${esc(t)}</strong>${TOOL_DESCRIPTIONS[t] ? ` \u2014 ${esc(TOOL_DESCRIPTIONS[t])}` : ''}</li>`).join('')
|
|
1122
|
+
: '<li><em>None configured</em></li>';
|
|
1123
|
+
|
|
1124
|
+
const dndList = data.do_not_discuss.length > 0
|
|
1125
|
+
? data.do_not_discuss.map(d => `<li>${esc(typeof d === 'string' ? d : d.topic || '')}</li>`).join('')
|
|
1126
|
+
: '<li><em>None configured</em></li>';
|
|
1127
|
+
|
|
1128
|
+
const neverList = data.never_disclose.length > 0
|
|
1129
|
+
? data.never_disclose.map(n => `<li>${esc(n)}</li>`).join('')
|
|
1130
|
+
: '<li><em>None configured</em></li>';
|
|
1131
|
+
|
|
1132
|
+
document.getElementById('preview-content').innerHTML = `
|
|
1133
|
+
<h4>Topics this caller can discuss:</h4>
|
|
1134
|
+
<ul>${topicsList}</ul>
|
|
1135
|
+
<h4>Goals:</h4>
|
|
1136
|
+
<ul>${goalsList}</ul>
|
|
1137
|
+
<h4>Tools available:</h4>
|
|
1138
|
+
<ul>${toolsList}</ul>
|
|
1139
|
+
<h4>Will not discuss:</h4>
|
|
1140
|
+
<ul>${dndList}</ul>
|
|
1141
|
+
<h4>Never disclosed (any tier):</h4>
|
|
1142
|
+
<ul>${neverList}</ul>
|
|
1143
|
+
`;
|
|
1144
|
+
|
|
1145
|
+
dialog.show();
|
|
1146
|
+
}
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
**Step 6: Wire toggle, preview, and initial render in bindSettingsActions()**
|
|
1150
|
+
|
|
1151
|
+
Add to `bindSettingsActions()`:
|
|
1152
|
+
|
|
1153
|
+
```javascript
|
|
1154
|
+
// A2A-41: toggle for three-column tier view
|
|
1155
|
+
document.getElementById('show-drag-columns')?.addEventListener('sl-change', () => {
|
|
1156
|
+
renderTierColumns();
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
document.getElementById('preview-caller-btn')?.addEventListener('click', openCallerPreview);
|
|
1160
|
+
document.getElementById('preview-close-btn')?.addEventListener('click', () => {
|
|
1161
|
+
document.getElementById('preview-dialog').hide();
|
|
1162
|
+
});
|
|
1163
|
+
```
|
|
1164
|
+
|
|
1165
|
+
Update `loadSettings()` to call `renderTierColumns()` after `fillTierSelects()`.
|
|
1166
|
+
|
|
1167
|
+
**Step 7: Run tests**
|
|
1168
|
+
|
|
1169
|
+
```bash
|
|
1170
|
+
npm test
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
**Step 8: Check diff size**
|
|
1174
|
+
|
|
1175
|
+
```bash
|
|
1176
|
+
git diff --stat origin/main..HEAD
|
|
1177
|
+
git diff origin/main..HEAD | wc -l
|
|
1178
|
+
```
|
|
1179
|
+
|
|
1180
|
+
If approaching 500 lines, note it but continue — the ticket is one coherent feature.
|
|
1181
|
+
|
|
1182
|
+
**Step 9: Commit**
|
|
1183
|
+
|
|
1184
|
+
```bash
|
|
1185
|
+
git add src/dashboard/public/index.html src/dashboard/public/app.js
|
|
1186
|
+
git commit -m "feat(a2a-41): tier columns, drag-and-drop, validation warnings, caller preview"
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
---
|
|
1190
|
+
|
|
1191
|
+
## Task 5: Final Verification + Push + PR
|
|
1192
|
+
|
|
1193
|
+
**Step 1: Run full test suite**
|
|
1194
|
+
|
|
1195
|
+
```bash
|
|
1196
|
+
npm test
|
|
1197
|
+
```
|
|
1198
|
+
|
|
1199
|
+
Expected: 328 passing, 2 failing (pre-existing only).
|
|
1200
|
+
|
|
1201
|
+
**Step 2: Push and open PR**
|
|
1202
|
+
|
|
1203
|
+
```bash
|
|
1204
|
+
git push origin feature/a2a-41
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
Open PR via `gh pr create` with the required template:
|
|
1208
|
+
|
|
1209
|
+
```markdown
|
|
1210
|
+
## Summary
|
|
1211
|
+
Overhaul the dashboard Settings tab: rename to Permissions, remove dead disclosure field from frontend and all backend paths, replace textareas with rich interactive topic/goal rows and tool checkboxes, add three-column drag-and-drop tier layout, validation warnings, and caller preview dialog.
|
|
1212
|
+
|
|
1213
|
+
## Ticket
|
|
1214
|
+
A2A-41
|
|
1215
|
+
|
|
1216
|
+
## Reused Code
|
|
1217
|
+
- `esc()` (app.js:203) for HTML escaping
|
|
1218
|
+
- `request()` (app.js:171) for all API calls
|
|
1219
|
+
- `showNotice()` (app.js:25) for user feedback
|
|
1220
|
+
- `parseTopicObjects()` (dashboard.js:145) for backend manifest parsing
|
|
1221
|
+
- `loadManifest()/saveManifest()` (disclosure.js) for manifest persistence
|
|
1222
|
+
- `sanitizeString()/sanitizeStringArray()` (dashboard.js) for input validation
|
|
1223
|
+
|
|
1224
|
+
## New Abstractions
|
|
1225
|
+
- `renderTopicList()`, `renderGoalList()` — render expandable card rows from manifest data
|
|
1226
|
+
- `renderToolCheckboxes()` — renders Shoelace checkboxes from TOOL_DESCRIPTIONS map
|
|
1227
|
+
- `renderTierColumns()` — three-column drag zone with inheritance display
|
|
1228
|
+
- `bindDragEvents()` — HTML5 DnD with Promise.all save and error recovery
|
|
1229
|
+
- `bindItemListDelegation()` — event delegation for topic/goal list interactions
|
|
1230
|
+
- `renderTierWarnings()` — contextual validation warnings
|
|
1231
|
+
- `getPreviewData()`, `openCallerPreview()` — merged effective view dialog
|
|
1232
|
+
|
|
1233
|
+
## Alternatives Considered
|
|
1234
|
+
- Per-row event binding: rejected due to listener accumulation on dynamic adds
|
|
1235
|
+
- Sequential drag-and-drop saves: rejected due to data loss risk on partial failure
|
|
1236
|
+
- Framework (React/Lit): rejected to maintain zero-build-step vanilla JS approach
|
|
1237
|
+
|
|
1238
|
+
## Test Coverage
|
|
1239
|
+
- Backend tests (328 passing) verify disclosure removal does not break token/config flows
|
|
1240
|
+
- Frontend changes are visual — verified via browser inspection
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
**Step 3: Update Linear**
|
|
1244
|
+
|
|
1245
|
+
Update ticket to "In Review".
|
|
1246
|
+
|
|
1247
|
+
---
|
|
1248
|
+
|
|
1249
|
+
## Acceptance Criteria Mapping
|
|
1250
|
+
|
|
1251
|
+
| Criterion | Task |
|
|
1252
|
+
|-----------|------|
|
|
1253
|
+
| Tab renamed Settings → Permissions | Task 2 |
|
|
1254
|
+
| Heading renamed | Task 2 |
|
|
1255
|
+
| Public tier pre-selected | Task 2 |
|
|
1256
|
+
| Tier dropdown emojis | Task 2 |
|
|
1257
|
+
| Tier ID field removed | Task 2 |
|
|
1258
|
+
| Disclosure field removed (frontend + backend) | Tasks 1, 2 |
|
|
1259
|
+
| Topics as expandable rows | Task 3 |
|
|
1260
|
+
| Goals as expandable rows | Task 3 |
|
|
1261
|
+
| Add topic/goal inline forms | Task 3 |
|
|
1262
|
+
| Tools as checkbox list | Task 3 |
|
|
1263
|
+
| Three-column tier layout | Task 4 |
|
|
1264
|
+
| Drag-and-drop topics between tiers | Task 4 |
|
|
1265
|
+
| Inherited items grayed out with badge | Task 4 |
|
|
1266
|
+
| Validation warnings | Task 4 |
|
|
1267
|
+
| Preview as Caller dialog | Task 4 |
|
|
1268
|
+
| All code has inline comments | All tasks |
|
|
1269
|
+
| Backward-compat --tab settings alias | Task 2 |
|
|
1270
|
+
|
|
1271
|
+
## Scope Exclusions
|
|
1272
|
+
|
|
1273
|
+
The ticket mentions "Native macOS app rebuilt and published to GitHub releases" — this is a release concern, NOT an implementation concern. The native app wraps the dashboard SPA, so dashboard changes are automatically visible in the app. Rebuilding and publishing the app is handled by the release process.
|