Diagnostic Panel
diagnostic-panelViewersThe 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.
Report
Runtime
Feed health
| Feed | Expected | Observed | Age | Dropped | Verdict |
|---|---|---|---|---|---|
| host | 30 Hz | 0 Hz | never | 0 | silent |
| transport | 30 Hz | 0 Hz | never | 0 | silent |
| midiIn | 30 Hz | 0 Hz | never | 0 | silent |
| midiOut | 30 Hz | 0 Hz | never | 0 | silent |
| meters | 30 Hz | 0 Hz | never | 0 | silent |
| scope | 15 Hz | 0 Hz | never | 0 | silent |
| lifecycle | 30 Hz | 0 Hz | 0 ms | 0 | starved |
| counters | 30 Hz | 0 Hz | never | 0 | silent |
| params | 30 Hz | 0 Hz | never | 0 | silent |
| presets | 30 Hz | 0 Hz | never | 0 | silent |
| clock | 30 Hz | 0 Hz | never | 0 | silent |
Identity
Feeds
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-panelPulls in
First time? Add the registry to your components.json once:
{
"registries": {
"@syntho-ui": "https://engine.syntho.app/r/{name}.json"
}
}Usage
Controlled or uncontrolled, whichever suits the surrounding state.
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
| Prop | Type | Default | Description |
|---|---|---|---|
| session | DiagnosticSession | — | The 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. |
| snapshot | DiagnosticSnapshot | — | The 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. |
| tab | DiagnosticTabId | — | Controlled 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) => void | — | Fires when the rail selection changes, in both controlled and uncontrolled modes. |
| headerActions | ReactNode | — | Rendered at the right of the header — the overlay puts its close button here. |
| live | boolean | — | Defaults 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
sliderandswitchroles with livearia-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.