Diagnostic Panel

diagnostic-panelViewers

The developer view of a running plugin, fed by one `DiagnosticSession` snapshot: Overview, Host, Transport/Clock, MIDI, Audio, Parameters, Presets/State, Events, Stimulus, Performance and Package. Controlled and presentational — it owns no bridge and opens no feed, so the same component serves a drawer over a plugin face, a dev-runner pane and this docs page. Its one editorial rule is the reason it can be trusted: every value either comes from the snapshot or renders as an explicit Unreported cell. There is no `?? 0` in it anywhere, because a host that never reported its latency is not a host with zero latency, and a meter frame that never arrived is not silence — a panel that fills those gaps with plausible numbers is worse than no panel, since a developer will act on them. The same care shows up in the stereo verdict, which consults per-channel RMS before correlation: a digitally silent pair correlates at exactly 1.0 by the meter convention, so correlation alone would report dead air as perfect mono. Audio taps are read through `session.readScope` inside the draw loop rather than from the snapshot, so a 512-sample frame per tap never enters a value React diffs. Overview leads with a status strip — health, feed verdicts, build, host rate and block, latency, connection — whose one-word verdict overrides the host's own claim when a feed it promised has gone silent, because a dead feed is a broken host bridge however healthy the DSP reports itself; a dropped-frame count growing in proportion to callbacks is named as "the host is not draining" rather than printed as a large number nobody can interpret. Copy report puts a ten-line plain-text summary on the clipboard ahead of the full JSON, because a JSON report is unreadable in the chat window it actually gets pasted into. Nothing costs a frame while the panel is closed: the Audio tab's four viewers share one animation frame that is never scheduled unless that tab is visible and the document is not hidden, each tab is memoised over only the slice of the snapshot it reads, and tables past 200 rows render only the band you can see with spacer rows carrying the rest of the height.

DiagnosticsfailedMock Diagnostics · disconnectedv0.0.0-mock · 0000000 · built 2 hours ago
Healthfailed
Feeds0/11 ok
Buildv0.0.0-mock · 0000000 · built 2 hours ago
HostCodex Mock Host · 48 kHz · 512 smp
Latency0 smp (0 ms)
Linkdisconnected

Report

A ten-line summary for chat, then the full JSON report beneath it.

Runtime

Runtime healthUnreported
First errorUnreported
Build modedevelopment
Connectiondisconnected

Feed health

Per-feed delivery health, measured from arrival timestamps.
FeedExpectedObservedAgeDroppedVerdict
host30 Hz0 Hznever0silent
transport30 Hz0 Hznever0silent
midiIn30 Hz0 Hznever0silent
midiOut30 Hz0 Hznever0silent
meters30 Hz0 Hznever0silent
scope15 Hz0 Hznever0silent
lifecycle30 Hz0 Hz0 ms0starved
counters30 Hz0 Hznever0silent
params30 Hz0 Hznever0silent
presets30 Hz0 Hznever0silent
clock30 Hz0 Hznever0silent

Identity

Buildv0.0.0-mock · 0000000 · built 2 hours ago
ModuleMock Diagnostics
Idcodex.mock.diagnostics
VendorCodex Music
Version0.0.0-mock
Classeffect
Targetdev-runner
SDK0.0.0

Feeds

Subscribedhost, transport, midiIn, midiOut, meters, scope, lifecycle, counters, params, presets, clock
Declaredhost, transport, midiIn, midiOut, meters, scope, lifecycle, counters, params, presets, clock
Max feed rate30 Hz
Dropped0 feed / 0 midi / 0 lifecycle

Every tab on a scripted mock host — seeded, so these are the same frames on every visit. Values come from the snapshot or render as an explicit Unreported cell; nothing here is a default dressed up as a measurement. The Sampler tab appears only because this mock declares a bank table — it is capability-gated, so a synth never shows it. Overview leads with the status strip; Audio puts four viewers on one shared animation frame with a trigger and a single-shot freeze; Events filters by kind, severity and "since last preset load". Under 480 px the rail collapses to glyphs and keeps its labels for screen readers.

Install

Copies the source into your project. Anything it depends on comes with it.

$ npx shadcn add @syntho-ui/diagnostic-panel

Pulls in

use-diagnostic-sessioncontrol-thememidi-message-decodelevel-meteroscilloscopegoniometerspectrumpiano-keyboardstft

First time? Add the registry to your components.json once:

components.json
{
  "registries": {
    "@syntho-ui": "https://engine.syntho.app/r/{name}.json"
  }
}

Usage

Controlled or uncontrolled, whichever suits the surrounding state.

example.tsx
import { DiagnosticPanel } from "@/components/ui/diagnostic-panel"
import { createDiagnosticSession } from "@codex-music/audio-sdk/diagnostic-session"
import { createModuleFaceHost } from "@codex-music/audio-sdk/module-face-host"

const host = createModuleFaceHost()
const session = createDiagnosticSession(host)
host.ready()
session.open()             // nothing streams until this is called

// The session re-derives feed ages on every read — it has to, since a feed
// going silent announces itself by sending nothing — so wrap it before handing
// it to useSyncExternalStore, or React sees a new store value every render.
const readSnapshot = useMemo(() => createSnapshotStabilizer(session.snapshot), [session])
const snapshot = useSyncExternalStore(session.subscribe, readSnapshot, readSnapshot)

<DiagnosticPanel session={session} snapshot={snapshot} />

// Controlled tab, when the surrounding chrome owns the selection.
<DiagnosticPanel session={session} snapshot={snapshot} tab={tab} onTabChange={setTab} />

Props

PropTypeDefaultDescription
sessionDiagnosticSessionThe SDK session the panel reads. Used for stimulus commands, `buildLocalReport()` and `readScope` in the audio tab's draw loop; the panel never opens or closes it, so visibility stays the caller's decision.
snapshotDiagnosticSnapshotThe value to render. Read it with `useSyncExternalStore` over `createSnapshotStabilizer(session.snapshot)` rather than over `session.snapshot` directly: the session re-derives feed-health ages at read time, so its raw snapshot is not referentially stable enough for that hook. Any field left `undefined` is one this host never reported, and renders as an Unreported cell rather than a default.
tabDiagnosticTabIdControlled tab selection. Omit to let the panel own it. One of overview, host, transport, midi, audio, parameters, presets, events, stimulus, performance, package.
onTabChange(tab: DiagnosticTabId) => voidFires when the rail selection changes, in both controlled and uncontrolled modes.
headerActionsReactNodeRendered at the right of the header — the overlay puts its close button here.
livebooleanDefaults to true. Set false while the panel is mounted but not visible: the shared animation frame behind the Audio tab's viewers is then never scheduled at all, so a hidden panel costs nothing rather than merely idling.

Interaction

  • Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
  • Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
  • Double-click resets to defaultValue.
  • Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
  • Proper slider and switch roles with live aria-valuetext, so a screen reader announces the formatted value rather than the raw number.
  • The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.