fix(onboarding): midi-input multi-provider discovery + home-tour lifecycle (#568)

Addresses Codex review of #526/#528:
- midi-input discover(): one provider's enumerate() rejection no longer aborts
  the whole discovery — other providers (e.g. a native/desktop MIDI provider)
  are still queried; denial is only reported when NO provider enumerates.
- Home tour now waits for a 'v3:dashboard-rendered' event (dashboard.js emits
  it after the #v3-home innerHTML swap) before attaching Shepherd, instead of a
  single animation frame that could latch onto pre-render nodes the async
  dashboard render then replaces.
- "Play it now" onboarding now arms the tour (armPendingFirstRun) to run the
  first time the user returns to v3-home, instead of silently never showing it.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos
2026-06-22 14:06:28 +02:00
committed by GitHub
co-authored by Claude Opus 4.8
parent 820a18648a
commit 5d0229fc82
4 changed files with 70 additions and 9 deletions
+24 -2
View File
@@ -177,14 +177,31 @@
async function _discover() {
if (providers.size === 0) return _unavailable('No MIDI provider registered', _snapshot());
let found = 0;
let enumerated = 0;
let lastError = null;
for (const provider of providers.values()) {
if (!provider.handlers.enumerate) continue;
let list;
try { list = await provider.handlers.enumerate(); }
catch (e) {
// requestMIDIAccess rejection = permission denied / unsupported.
return _denied(_str(e && e.message, 'MIDI access denied'), _snapshot());
// One provider failing (e.g. browser Web-MIDI permission denied,
// or unsupported) must NOT hide other providers' devices (e.g. a
// native/desktop MIDI provider). Record it and keep going; denial
// is only reported below when NO provider enumerated successfully.
lastError = e;
// Drop this provider's now-unverifiable sources so revoked /
// unplugged devices don't linger as selectable and fail on a
// later open (it just failed to enumerate, so we can't trust its
// previous list).
for (const [key, s] of Array.from(sources.entries())) {
if (s.providerId === provider.id) {
_closeSessionInternal(key, 'enumerate-failed');
sources.delete(key);
}
}
continue;
}
enumerated += 1;
const fresh = new Set();
for (const raw of (Array.isArray(list) ? list : [])) {
const sourceId = _str(raw.sourceId || raw.id, '');
@@ -216,6 +233,11 @@
}
// Restore a previously-selected source if it reappeared.
if (selectedKey && !sources.has(selectedKey)) { /* keep the preference; it may return later */ }
// Only surface a denial when NO provider could enumerate — otherwise a
// single denied/unsupported provider would mask the working ones.
if (enumerated === 0 && lastError) {
return _denied(_str(lastError && lastError.message, 'MIDI access denied'), _snapshot());
}
_emit('sources-changed', { count: found });
_contributeDiagnostics();
return _handled(_snapshot({ discovered: found }));