a2acalling 0.6.57 → 0.6.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,585 @@
1
+ # Dashboard Backlog Blitz — 5 Tickets, 3 Waves
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Execute all 5 dashboard backlog tickets (A2A-36, A2A-37, A2A-38, A2A-39, A2A-40) in parallel waves with adversarial builder/reviewer teams that don't step on each other's toes.
6
+
7
+ **Architecture:** All 5 tickets touch the same 3 dashboard files (`index.html`, `app.js`, `style.css`). We isolate work using git worktrees and sequence into 3 waves based on file-section overlap. Each wave merges to `main` before the next starts. Each team is a builder + reviewer pair that iterate until both agree the ticket is done.
8
+
9
+ **Tech Stack:** Vanilla JS, HTML, CSS (no framework). Shoelace web components (CDN) for Wave 3 only. Tauri/Rust for native app keyboard shortcuts.
10
+
11
+ ---
12
+
13
+ ## Wave Analysis — Why This Grouping
14
+
15
+ ### File Overlap Matrix
16
+
17
+ | Ticket | index.html sections | app.js functions | style.css | dashboard.js | lib.rs |
18
+ |--------|-------------------|-----------------|-----------|-------------|--------|
19
+ | **A2A-38** (Calls bug) | lines 51-70 (calls panel) | lines 620-658 (renderCalls, loadCallDetail) | lines 178-181 (.summary) | lines 1235-1259 (GET /calls/:id) | — |
20
+ | **A2A-37** (Contacts) | lines 24-49 (contacts panel) | lines 310-618 (renderContacts, bindContactsActions) | new styles for collapse/pin | — | — |
21
+ | **A2A-36** (Refresh) | button elements across ALL tabs | lines 1463-1491 (bindRefreshButtons) + new polling | lines 130-141 (button styles) | — | — |
22
+ | **A2A-39** (Tabs+Invites) | lines 15-21 (nav) + 219-253 (invites panel) | lines 1405-1461 (renderInvites, bindInviteActions) | — | — | lines 40-51, 84-108 |
23
+ | **A2A-40** (Shoelace) | ALL | ALL | ALL | — | — |
24
+
25
+ ### Dependency Chain
26
+
27
+ ```
28
+ A2A-38 ──┐
29
+ ├── merge to main ── A2A-39 ──┐
30
+ A2A-37 ──┘ ├── merge to main ── A2A-40
31
+ A2A-36 ──┘
32
+ ```
33
+
34
+ - **A2A-39 depends on A2A-37** — ticket says "Follow the exact same pattern being implemented for Add Contact"
35
+ - **A2A-36 is cross-cutting** — removes buttons from all tabs, needs stable tab structure
36
+ - **A2A-40 depends on ALL** — rewrites every UI element to Shoelace
37
+
38
+ ---
39
+
40
+ ## Wave 1 — Two Parallel Teams (Zero Overlap)
41
+
42
+ ### Team Alpha: A2A-38 — Calls Tab Summary Bug (Urgent)
43
+
44
+ **Branch:** `feature/a2a-38` (worktree: `/root/a2a-wt-38`)
45
+
46
+ **Problem:** `loadCallDetail()` renders the full message transcript in the summary section instead of the agent-generated summary.
47
+
48
+ **Files:**
49
+ - Modify: `src/dashboard/public/app.js` lines 644-658 (`loadCallDetail()`)
50
+ - Modify: `src/dashboard/public/index.html` lines 51-70 (calls panel — add collapsible transcript section)
51
+ - Modify: `src/dashboard/public/style.css` lines 178-181 (`.summary` styling)
52
+ - Investigate: `src/routes/dashboard.js` lines 1235-1259 (verify API returns `summary` field separately)
53
+
54
+ **Step 1: Investigate the API response**
55
+
56
+ Read `src/routes/dashboard.js` lines 1235-1259 to understand what fields the `/calls/:conversationId` endpoint returns. The response comes from `convStore.getConversationContext()`. Check if `summary` and `owner_summary` are separate fields from `recentMessages`.
57
+
58
+ Also check `src/lib/conversation-store.js` for the `getConversationContext()` method to see the full data shape.
59
+
60
+ **Step 2: Fix `loadCallDetail()` in app.js**
61
+
62
+ Current code (lines 644-658) fetches the call and renders everything as transcript. Replace with:
63
+
64
+ ```javascript
65
+ async function loadCallDetail(conversationId) {
66
+ const res = await fetch(`/api/a2a/dashboard/calls/${conversationId}?messages=40`);
67
+ const data = await res.json();
68
+ if (!data.success) return;
69
+ const call = data.call;
70
+ const el = document.getElementById('call-detail');
71
+
72
+ // Summary section — use agent-generated summary, not transcript
73
+ const summaryText = call.summary || call.owner_summary || 'Summary pending\u2026';
74
+
75
+ // Transcript — collapsible details element
76
+ const transcript = (call.recentMessages || [])
77
+ .map(m => `[${new Date(m.timestamp).toLocaleString()}] ${m.direction}: ${m.content}`)
78
+ .join('\n');
79
+
80
+ el.innerHTML = `
81
+ <h3>Call with ${call.contact?.name || 'Unknown'}</h3>
82
+ <p><strong>Status:</strong> ${call.status || 'unknown'}</p>
83
+ <h4>Summary</h4>
84
+ <pre class="summary">${escapeHtml(summaryText)}</pre>
85
+ <details>
86
+ <summary>Full Transcript (${(call.recentMessages || []).length} messages)</summary>
87
+ <pre class="mono">${escapeHtml(transcript)}</pre>
88
+ </details>
89
+ `;
90
+ el.style.display = 'block';
91
+ }
92
+ ```
93
+
94
+ Note: Check if `escapeHtml()` exists in the codebase. If not, add a minimal one at the top of app.js (or use the existing pattern for HTML escaping).
95
+
96
+ **Step 3: Verify summary field comes from API**
97
+
98
+ If the API doesn't return `summary` separately from messages, modify `src/routes/dashboard.js` lines 1235-1259 to ensure the response includes:
99
+ ```javascript
100
+ { success: true, call: { ...contextData, summary: contextData.summary, owner_summary: contextData.owner_summary, contact } }
101
+ ```
102
+
103
+ **Step 4: Test manually**
104
+
105
+ Start the server, navigate to Calls tab, click a call with a completed summary. Verify:
106
+ - Summary section shows the agent-generated summary text (not transcript)
107
+ - "Summary pending..." shows for active calls with no summary yet
108
+ - Full Transcript is in a collapsible `<details>` element below
109
+ - Clicking the details toggle reveals the full message history
110
+
111
+ **Step 5: Commit**
112
+
113
+ ```bash
114
+ git add src/dashboard/public/app.js src/dashboard/public/index.html src/routes/dashboard.js
115
+ git commit -m "fix(A2A-38): show agent summary instead of transcript in calls detail"
116
+ ```
117
+
118
+ ---
119
+
120
+ ### Team Beta: A2A-37 — Contacts Tab Restructure (High)
121
+
122
+ **Branch:** `feature/a2a-37` (worktree: `/root/a2a-wt-37`)
123
+
124
+ **Problem:** Add Contact form dominates the viewport, "My agents" section is buried, no pinning for frequently-called agents.
125
+
126
+ **Files:**
127
+ - Modify: `src/dashboard/public/index.html` lines 24-49 (contacts panel)
128
+ - Modify: `src/dashboard/public/app.js` lines 310-618 (`renderContacts()`, `bindContactsActions()`)
129
+ - Modify: `src/dashboard/public/style.css` (add collapsible card + pin styles)
130
+
131
+ **Step 1: Restructure contacts HTML in index.html**
132
+
133
+ Replace the contacts panel (lines 24-49). The Add Contact form should be hidden behind a toggle button:
134
+
135
+ ```html
136
+ <section id="tab-contacts" class="panel is-active">
137
+ <div class="section-header">
138
+ <h2>Contacts</h2>
139
+ </div>
140
+
141
+ <!-- Collapsed Add Contact button -->
142
+ <button id="toggle-add-contact" class="btn" style="margin-bottom:1rem;">+ Add Contact</button>
143
+
144
+ <!-- Collapsible Add Contact form (hidden by default) -->
145
+ <div id="add-contact-card" class="card" style="display:none; margin-bottom:1rem;">
146
+ <h3>Add Contact</h3>
147
+ <!-- Keep all existing form fields exactly as-is -->
148
+ <form id="add-contact-form">
149
+ <!-- ... existing 8 fields ... -->
150
+ </form>
151
+ </div>
152
+
153
+ <!-- Contact sections (rendered dynamically) -->
154
+ <div id="contact-sections"></div>
155
+ <div id="contact-detail" style="display:none;"></div>
156
+ </section>
157
+ ```
158
+
159
+ **Step 2: Add toggle logic in app.js**
160
+
161
+ In `bindContactsActions()`, add the toggle handler:
162
+
163
+ ```javascript
164
+ document.getElementById('toggle-add-contact')?.addEventListener('click', () => {
165
+ const card = document.getElementById('add-contact-card');
166
+ const btn = document.getElementById('toggle-add-contact');
167
+ const isVisible = card.style.display !== 'none';
168
+ card.style.display = isVisible ? 'none' : 'block';
169
+ btn.textContent = isVisible ? '+ Add Contact' : '− Cancel';
170
+ });
171
+ ```
172
+
173
+ After successful form submit (around line 510), collapse the form:
174
+ ```javascript
175
+ document.getElementById('add-contact-card').style.display = 'none';
176
+ document.getElementById('toggle-add-contact').textContent = '+ Add Contact';
177
+ ```
178
+
179
+ **Step 3: Reorder contact sections in `renderContacts()`**
180
+
181
+ Modify `renderContacts()` (lines 310-427) so the rendered order is:
182
+ 1. **My Agents** — always first
183
+ 2. **Last Called Agents** — with pinned agents at top
184
+ 3. **Grouped by Owner** — everything else
185
+
186
+ **Step 4: Add pin/unpin functionality**
187
+
188
+ Add pin state to localStorage:
189
+
190
+ ```javascript
191
+ function getPinnedContacts() {
192
+ try { return JSON.parse(localStorage.getItem('a2a-pinned-contacts') || '[]'); }
193
+ catch { return []; }
194
+ }
195
+
196
+ function togglePin(contactId) {
197
+ const pinned = getPinnedContacts();
198
+ const idx = pinned.indexOf(contactId);
199
+ if (idx >= 0) pinned.splice(idx, 1);
200
+ else pinned.push(contactId);
201
+ localStorage.setItem('a2a-pinned-contacts', JSON.stringify(pinned));
202
+ renderContacts(); // re-render to update pin state
203
+ }
204
+ ```
205
+
206
+ In the contact row HTML generator (lines 345-382), add a pin button:
207
+
208
+ ```javascript
209
+ const isPinned = getPinnedContacts().includes(c.id);
210
+ const pinBtn = `<button class="btn-link pin-btn" data-contact-id="${c.id}" title="${isPinned ? 'Unpin' : 'Pin'}">${isPinned ? '\u{1F4CC}' : '\u{1F4C5}'}</button>`;
211
+ ```
212
+
213
+ In the "Last Called" section, sort pinned contacts first:
214
+
215
+ ```javascript
216
+ lastCalled.sort((a, b) => {
217
+ const aPinned = pinnedSet.has(a.id);
218
+ const bPinned = pinnedSet.has(b.id);
219
+ if (aPinned && !bPinned) return -1;
220
+ if (!aPinned && bPinned) return 1;
221
+ return new Date(b.last_call_at) - new Date(a.last_call_at);
222
+ });
223
+ ```
224
+
225
+ **Step 5: Add styles in style.css**
226
+
227
+ ```css
228
+ /* Collapsible card */
229
+ #add-contact-card { padding: 1rem; }
230
+ #add-contact-card h3 { margin-top: 0; }
231
+
232
+ /* Pin button */
233
+ .pin-btn { cursor: pointer; font-size: 0.85rem; padding: 0.2rem; }
234
+ ```
235
+
236
+ **Step 6: Test manually**
237
+
238
+ - Contacts tab loads with form hidden, "+" button visible
239
+ - Click "+", form expands; click again, collapses
240
+ - Add a contact, form auto-collapses, list refreshes
241
+ - "My Agents" section appears first
242
+ - Pin icon appears on last-called contacts
243
+ - Click pin, contact moves to top of last-called section
244
+ - Reload page — pinned state persists
245
+
246
+ **Step 7: Commit**
247
+
248
+ ```bash
249
+ git add src/dashboard/public/index.html src/dashboard/public/app.js src/dashboard/public/style.css
250
+ git commit -m "feat(A2A-37): restructure contacts tab — collapsible form, section reorder, pinnable agents"
251
+ ```
252
+
253
+ ---
254
+
255
+ ## Wave 1 Merge
256
+
257
+ After both Team Alpha and Team Beta finish:
258
+
259
+ ```bash
260
+ # Merge A2A-38 first (smaller change, urgent bug)
261
+ git checkout main
262
+ git merge feature/a2a-38 --no-ff -m "feat(A2A-38): fix calls tab summary display"
263
+
264
+ # Merge A2A-37 (different sections, should merge cleanly)
265
+ git merge feature/a2a-37 --no-ff -m "feat(A2A-37): restructure contacts tab"
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Wave 2 — Two Parallel Teams (After Wave 1 Merge)
271
+
272
+ ### Team Gamma: A2A-39 — Tab Reorder + Invites Redesign (Medium)
273
+
274
+ **Branch:** `feature/a2a-39` (worktree: `/root/a2a-wt-39`, branched from post-Wave-1 main)
275
+
276
+ **Problem:** Logs tab is in wrong position; Invites form should be collapsible like Contacts.
277
+
278
+ **Files:**
279
+ - Modify: `src/dashboard/public/index.html` lines 15-21 (nav) + 219-253 (invites panel)
280
+ - Modify: `src/dashboard/public/app.js` lines 1405-1461 (invites rendering + actions)
281
+ - Modify: `native/macos/src-tauri/src/lib.rs` lines 40-51 (keyboard shortcuts)
282
+
283
+ **Step 1: Reorder tabs in index.html**
284
+
285
+ Change nav button order (lines 15-21) to: Contacts | Calls | Settings | Invites | Logs
286
+
287
+ Change `<section>` panel order to match (for readability).
288
+
289
+ **Step 2: Redesign invites form**
290
+
291
+ Apply the exact same collapsible pattern from A2A-37:
292
+
293
+ ```html
294
+ <!-- Replace always-visible form with toggle -->
295
+ <button id="toggle-gen-invite" class="btn" style="margin-bottom:1rem;">+ Generate Invite</button>
296
+
297
+ <div id="gen-invite-card" class="card" style="display:none; margin-bottom:1rem;">
298
+ <h3>Generate Invite</h3>
299
+ <form id="invite-form">
300
+ <!-- Keep existing fields: name, owner, tier, expires, max-calls, notify -->
301
+ </form>
302
+ </div>
303
+
304
+ <!-- Invite message output (shown after generation) -->
305
+ <div id="invite-msg-wrap" style="display:none; margin-bottom:1rem;">
306
+ <h4>Share this invite</h4>
307
+ <textarea id="invite-msg" rows="5" readonly></textarea>
308
+ <button id="copy-invite" class="btn">Copy</button>
309
+ </div>
310
+
311
+ <!-- Existing invites table stays always visible -->
312
+ <table id="invites-table">...</table>
313
+ ```
314
+
315
+ **Step 3: Add toggle logic in app.js**
316
+
317
+ In `bindInviteActions()`:
318
+
319
+ ```javascript
320
+ document.getElementById('toggle-gen-invite')?.addEventListener('click', () => {
321
+ const card = document.getElementById('gen-invite-card');
322
+ const btn = document.getElementById('toggle-gen-invite');
323
+ const isVisible = card.style.display !== 'none';
324
+ card.style.display = isVisible ? 'none' : 'block';
325
+ btn.textContent = isVisible ? '+ Generate Invite' : '− Cancel';
326
+ });
327
+ ```
328
+
329
+ After successful invite creation, collapse the form and show the invite message.
330
+
331
+ **Step 4: Update keyboard shortcuts in lib.rs**
332
+
333
+ Change the Tauri native app keyboard shortcut mappings (lines 40-51):
334
+
335
+ ```rust
336
+ // New order: Contacts(1) | Calls(2) | Settings(3) | Invites(4) | Logs(5)
337
+ let contacts = MenuItem::with_id(app, "tab-contacts", "Contacts", true, Some("CmdOrCtrl+1"))?;
338
+ let calls = MenuItem::with_id(app, "tab-calls", "Calls", true, Some("CmdOrCtrl+2"))?;
339
+ let settings = MenuItem::with_id(app, "tab-settings", "Settings", true, Some("CmdOrCtrl+3"))?;
340
+ let invites = MenuItem::with_id(app, "tab-invites", "Invites", true, Some("CmdOrCtrl+4"))?;
341
+ let logs = MenuItem::with_id(app, "tab-logs", "Logs", true, Some("CmdOrCtrl+5"))?;
342
+ ```
343
+
344
+ Also update the `Submenu::with_items` call to match the new order.
345
+
346
+ **Step 5: Test**
347
+
348
+ - Tab order is Contacts | Calls | Settings | Invites | Logs
349
+ - Cmd+1-5 map to the new order (in Tauri)
350
+ - Hash-based deep links still work
351
+ - Invites form is collapsed behind "+" button
352
+ - Generate invite, form collapses, invite message appears
353
+ - Existing invites table always visible
354
+
355
+ **Step 6: Commit**
356
+
357
+ ```bash
358
+ git add src/dashboard/public/index.html src/dashboard/public/app.js native/macos/src-tauri/src/lib.rs
359
+ git commit -m "feat(A2A-39): reorder tabs, collapsible invite form"
360
+ ```
361
+
362
+ ---
363
+
364
+ ### Team Delta: A2A-36 — Remove Refresh Buttons, Add Auto-Poll (High)
365
+
366
+ **Branch:** `feature/a2a-36` (worktree: `/root/a2a-wt-36`, branched from post-Wave-1 main)
367
+
368
+ **Problem:** Manual refresh buttons everywhere. Data should auto-update.
369
+
370
+ **Files:**
371
+ - Modify: `src/dashboard/public/index.html` (remove all refresh `<button>` elements)
372
+ - Modify: `src/dashboard/public/app.js` (remove `bindRefreshButtons()`, add smart polling)
373
+ - Modify: `src/dashboard/public/style.css` (remove refresh-specific styles if any)
374
+
375
+ **Step 1: Remove refresh buttons from index.html**
376
+
377
+ Remove all `<button>` elements with IDs: `refresh-contacts`, `refresh-calls`, `refresh-invites`, `refresh-logs`, `refresh-log-stats`, `callbook-refresh`, `callbook-refresh-devices`, `auto-update-refresh`.
378
+
379
+ Clean up any header rows that become empty after button removal.
380
+
381
+ **Step 2: Remove `bindRefreshButtons()` from app.js**
382
+
383
+ Delete the function (lines 1463-1491) and its call in `bootstrap()` (line 1496 approximately).
384
+
385
+ **Step 3: Add smart polling in app.js**
386
+
387
+ Add a polling system that only fetches data for the active tab:
388
+
389
+ ```javascript
390
+ // Smart tab polling
391
+ let pollInterval = null;
392
+
393
+ function getActiveTab() {
394
+ return document.querySelector('.tab.is-active')?.dataset.tab || 'contacts';
395
+ }
396
+
397
+ const tabLoaders = {
398
+ contacts: loadContacts,
399
+ calls: loadCalls,
400
+ logs: loadLogs,
401
+ settings: () => {}, // settings don't need polling
402
+ invites: loadInvites,
403
+ };
404
+
405
+ function startTabPolling() {
406
+ if (pollInterval) clearInterval(pollInterval);
407
+ pollInterval = setInterval(() => {
408
+ const tab = getActiveTab();
409
+ const loader = tabLoaders[tab];
410
+ if (loader) loader();
411
+ }, 5000); // 5-second interval
412
+ }
413
+
414
+ // On tab switch, immediately load fresh data + restart poll timer
415
+ function onTabSwitch(tabName) {
416
+ const loader = tabLoaders[tabName];
417
+ if (loader) loader();
418
+ startTabPolling(); // restart interval so next poll is 5s from now
419
+ }
420
+ ```
421
+
422
+ Integrate `onTabSwitch()` into the existing `activateTab()` function (lines 253-269) — call it at the end of the function after making the tab visible.
423
+
424
+ Start polling in `bootstrap()` after initial data load.
425
+
426
+ **Step 4: Keep SSE events as-is**
427
+
428
+ The existing `connectRealtimeEvents()` (lines 112-162) stays unchanged — it provides instant updates for calls, summaries, and contact status. The polling is a complement for data that doesn't have SSE events (logs, invites).
429
+
430
+ **Step 5: Preserve log filter auto-refresh**
431
+
432
+ The log filter debounced refresh logic (lines 1470-1490 in current `bindRefreshButtons()`) should be extracted and kept as a separate concern. Move it to a `bindLogFilterRefresh()` function.
433
+
434
+ **Step 6: Test**
435
+
436
+ - No refresh buttons visible on any tab
437
+ - Switch to Contacts tab — data loads immediately
438
+ - Wait 5 seconds — data refreshes automatically (check network tab)
439
+ - Switch to Calls — immediate load, then polls every 5s
440
+ - Log filters still trigger immediate refresh on change
441
+ - SSE events still update in real-time (test with a call)
442
+
443
+ **Step 7: Commit**
444
+
445
+ ```bash
446
+ git add src/dashboard/public/index.html src/dashboard/public/app.js src/dashboard/public/style.css
447
+ git commit -m "feat(A2A-36): remove refresh buttons, add smart auto-polling"
448
+ ```
449
+
450
+ ---
451
+
452
+ ## Wave 2 Merge
453
+
454
+ ```bash
455
+ # Merge A2A-39 first (structural change to tab order)
456
+ git checkout main
457
+ git merge feature/a2a-39 --no-ff -m "feat(A2A-39): reorder tabs and redesign invites"
458
+
459
+ # Merge A2A-36 (button removals — may need minor conflict resolution in index.html)
460
+ git merge feature/a2a-36 --no-ff -m "feat(A2A-36): auto-poll replaces refresh buttons"
461
+ ```
462
+
463
+ ---
464
+
465
+ ## Wave 3 — One Team (After Wave 2 Merge)
466
+
467
+ ### Team Epsilon: A2A-40 — Shoelace Migration (High)
468
+
469
+ **Branch:** `feature/a2a-40` (worktree: `/root/a2a-wt-40`, branched from post-Wave-2 main)
470
+
471
+ **Problem:** Dashboard uses hand-rolled HTML/CSS. Migrate to Shoelace web components for polished, consistent UI.
472
+
473
+ **Files:**
474
+ - Modify: `src/dashboard/public/index.html` (add CDN, replace all HTML elements)
475
+ - Modify: `src/dashboard/public/app.js` (update event listeners, rendering functions)
476
+ - Modify: `src/dashboard/public/style.css` (simplify to Shoelace theme overrides)
477
+
478
+ **Step 1: Add Shoelace CDN to index.html**
479
+
480
+ In `<head>`:
481
+ ```html
482
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.19/cdn/themes/light.css" />
483
+ <script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.19/cdn/shoelace-autoloader.js"></script>
484
+ ```
485
+
486
+ **Step 2: Migrate tab navigation**
487
+
488
+ Replace the `<nav>` buttons with:
489
+ ```html
490
+ <sl-tab-group id="main-tabs">
491
+ <sl-tab slot="nav" panel="contacts" active>Contacts</sl-tab>
492
+ <sl-tab slot="nav" panel="calls">Calls</sl-tab>
493
+ <sl-tab slot="nav" panel="settings">Settings</sl-tab>
494
+ <sl-tab slot="nav" panel="invites">Invites</sl-tab>
495
+ <sl-tab slot="nav" panel="logs">Logs</sl-tab>
496
+
497
+ <sl-tab-panel name="contacts">...</sl-tab-panel>
498
+ <sl-tab-panel name="calls">...</sl-tab-panel>
499
+ <sl-tab-panel name="settings">...</sl-tab-panel>
500
+ <sl-tab-panel name="invites">...</sl-tab-panel>
501
+ <sl-tab-panel name="logs">...</sl-tab-panel>
502
+ </sl-tab-group>
503
+ ```
504
+
505
+ **Step 3: Migrate form elements**
506
+
507
+ Replace across all tabs:
508
+ - `<input>` → `<sl-input>`
509
+ - `<textarea>` → `<sl-textarea>`
510
+ - `<select>` → `<sl-select>` + `<sl-option>`
511
+ - `<button class="btn">` → `<sl-button>`
512
+ - `<input type="checkbox">` → `<sl-checkbox>`
513
+
514
+ **Step 4: Migrate collapsible sections**
515
+
516
+ Replace `<details>` / toggle buttons with `<sl-details>`:
517
+ ```html
518
+ <sl-details summary="+ Add Contact">
519
+ <form id="add-contact-form">...</form>
520
+ </sl-details>
521
+ ```
522
+
523
+ **Step 5: Migrate status indicators**
524
+
525
+ - Status pills → `<sl-badge>` with `variant="success|warning|danger"`
526
+ - Toast notifications → `<sl-alert variant="primary" duration="3000" closable>`
527
+
528
+ **Step 6: Update app.js event listeners**
529
+
530
+ Replace tab switching to use Shoelace events:
531
+ ```javascript
532
+ document.getElementById('main-tabs').addEventListener('sl-tab-show', (e) => {
533
+ const tabName = e.detail.name;
534
+ window.location.hash = tabName;
535
+ onTabSwitch(tabName);
536
+ });
537
+ ```
538
+
539
+ Replace button clicks for `<sl-button>` (they still emit 'click' events, but form submission may need adjustment for `<sl-input>` — use `.value` property access).
540
+
541
+ **Step 7: Simplify style.css**
542
+
543
+ Remove all hand-rolled component styles (tab buttons, panel active states, button styles, form input styles). Keep only:
544
+ - Layout rules (flexbox, grid)
545
+ - Shoelace theme overrides (CSS custom properties)
546
+ - Table styles (Shoelace has no table component)
547
+ - App-specific layout (`.section-header`, margins)
548
+
549
+ **Step 8: Verify Tauri compatibility**
550
+
551
+ Shoelace web components use Shadow DOM which WebView2/WebKit should support. Test that:
552
+ - All components render in Tauri's WebView
553
+ - `sl-tab-show` events fire correctly
554
+ - Deep links still work
555
+ - Keyboard shortcuts still trigger tab switches
556
+
557
+ **Step 9: Test everything**
558
+
559
+ - All 5 tabs render with Shoelace components
560
+ - Forms submit correctly (add contact, generate invite, tier settings)
561
+ - Collapsible sections work (add contact, invite form, transcript)
562
+ - Auto-polling continues to work
563
+ - SSE real-time events still update the UI
564
+ - Status badges show correct colors
565
+ - Tables render with proper styling
566
+ - Native app (Tauri) renders correctly
567
+
568
+ **Step 10: Commit**
569
+
570
+ ```bash
571
+ git add src/dashboard/public/index.html src/dashboard/public/app.js src/dashboard/public/style.css
572
+ git commit -m "feat(A2A-40): migrate dashboard to Shoelace web components"
573
+ ```
574
+
575
+ ---
576
+
577
+ ## Execution Summary
578
+
579
+ | Wave | Teams | Tickets | Parallel? | Est. Overlap Risk |
580
+ |------|-------|---------|-----------|-------------------|
581
+ | 1 | Alpha + Beta | A2A-38, A2A-37 | Yes | Zero (different sections) |
582
+ | 2 | Gamma + Delta | A2A-39, A2A-36 | Yes | Low (nav vs button removal) |
583
+ | 3 | Epsilon | A2A-40 | Solo | None (starts from clean merge) |
584
+
585
+ **Total: 5 tickets, 5 adversarial teams (10 agents), 3 waves.**
@@ -40,14 +40,14 @@ fn build_menu(app: &tauri::AppHandle) -> tauri::Result<Menu<tauri::Wry>> {
40
40
  // View menu with tab shortcuts
41
41
  let contacts = MenuItem::with_id(app, "tab-contacts", "Contacts", true, Some("CmdOrCtrl+1"))?;
42
42
  let calls = MenuItem::with_id(app, "tab-calls", "Calls", true, Some("CmdOrCtrl+2"))?;
43
- let logs = MenuItem::with_id(app, "tab-logs", "Logs", true, Some("CmdOrCtrl+3"))?;
44
- let settings = MenuItem::with_id(app, "tab-settings", "Settings", true, Some("CmdOrCtrl+4"))?;
45
- let invites = MenuItem::with_id(app, "tab-invites", "Invites", true, Some("CmdOrCtrl+5"))?;
43
+ let settings = MenuItem::with_id(app, "tab-settings", "Settings", true, Some("CmdOrCtrl+3"))?;
44
+ let invites = MenuItem::with_id(app, "tab-invites", "Invites", true, Some("CmdOrCtrl+4"))?;
45
+ let logs = MenuItem::with_id(app, "tab-logs", "Logs", true, Some("CmdOrCtrl+5"))?;
46
46
  let sep2 = PredefinedMenuItem::separator(app)?;
47
47
  let refresh = MenuItem::with_id(app, "refresh", "Refresh", true, Some("CmdOrCtrl+R"))?;
48
48
 
49
49
  let view_menu = Submenu::with_items(app, "View", true, &[
50
- &contacts, &calls, &logs, &settings, &invites, &sep2, &refresh,
50
+ &contacts, &calls, &settings, &invites, &logs, &sep2, &refresh,
51
51
  ])?;
52
52
 
53
53
  // Edit menu (standard macOS)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.57",
3
+ "version": "0.6.58",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {