@signalsandsorcery/plugin-sdk 2.0.2 → 2.24.1

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/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import React, { ComponentType, ReactNode } from 'react';
1
+ import React, { ComponentType, ReactNode, DragEvent, Dispatch, SetStateAction } from 'react';
2
2
 
3
3
  /**
4
4
  * Plugin SDK Type Definitions
@@ -10,6 +10,67 @@ import React, { ComponentType, ReactNode } from 'react';
10
10
 
11
11
  /** What kind of Tracktion content this plugin creates */
12
12
  type GeneratorType = 'midi' | 'audio' | 'sample' | 'hybrid';
13
+ /**
14
+ * Drum-kit configuration for `host.setTrackDrumKit`. Prototype shape carries
15
+ * a single sample; future multi-slot kits will extend this with a `notes`
16
+ * map (`Record<midiNote, samplePath>`) for GM-style drum maps.
17
+ */
18
+ interface DrumKit {
19
+ /** Absolute path to the sample (WAV, AIFF, FLAC). Triggered on every note-on. */
20
+ samplePath: string;
21
+ }
22
+ /**
23
+ * One key-mapped sample zone in a pitched, polyphonic instrument.
24
+ * Used by `host.setTrackInstrumentSampler`.
25
+ *
26
+ * Zones in an InstrumentSampler MUST be disjoint and ordered low to
27
+ * high by rootKey — the engine rejects overlap because Tracktion would
28
+ * otherwise double-trigger every matching sound on each note-on.
29
+ */
30
+ interface InstrumentZone {
31
+ /** Absolute path to the zone's sample (WAV, FLAC, AIFF). */
32
+ samplePath: string;
33
+ /** MIDI note this sample sounds at unshifted (0-127). */
34
+ rootKey: number;
35
+ /** Inclusive low end of the key range that triggers this zone (0-127). */
36
+ minKey: number;
37
+ /** Inclusive high end of the key range that triggers this zone (0-127). */
38
+ maxKey: number;
39
+ /**
40
+ * If true, the sampler plays the sample for the duration the note is
41
+ * held and stops on note-off (good for sustaining pads, organs, etc.,
42
+ * whose source has been pre-trimmed to a steady-state region).
43
+ * If false, the sampler plays the sample through to its end ignoring
44
+ * note-off (good for plucks, mallets, percussion).
45
+ */
46
+ openEnded: boolean;
47
+ }
48
+ /**
49
+ * Pitched instrument configuration for `host.setTrackInstrumentSampler`.
50
+ * Parallel to `DrumKit` but multi-zone and pitch-aware. A manifest
51
+ * authored by the pitched-sample pipeline reduces to one of these.
52
+ *
53
+ * NOTE: This is distinct from `host.setTrackInstrument(trackId, pluginId)`
54
+ * which loads a VST3/AU synth plugin. `setTrackInstrumentSampler` loads
55
+ * the built-in Tracktion sampler with N pre-rendered zones.
56
+ */
57
+ interface InstrumentSampler {
58
+ /** Display name (e.g. "Bright Warm Pluck"). Used for diagnostics. */
59
+ name: string;
60
+ /** Disjoint zones, ordered low->high by rootKey. At least one required. */
61
+ zones: ReadonlyArray<InstrumentZone>;
62
+ }
63
+ /** Options for `host.listAudioFiles`. */
64
+ interface ListAudioFilesOptions {
65
+ /**
66
+ * File extensions to include (dot-prefixed, lowercase). Defaults to
67
+ * `['.wav']`. Other audio formats (`.aif`, `.flac`, `.mp3`) are passed
68
+ * through verbatim; the host does not transcode.
69
+ */
70
+ extensions?: string[];
71
+ /** Walk subdirectories. Defaults to `false`. */
72
+ recursive?: boolean;
73
+ }
13
74
  /** Describes an available instrument plugin (VST3/AU synth) on the system. */
14
75
  interface InstrumentDescriptor {
15
76
  /** Stable plugin identifier for loading (VST3 TUID or AU component ID) */
@@ -122,6 +183,33 @@ interface PluginUIProps {
122
183
  onOpenContract?: (() => void) | null;
123
184
  /** Callback to expand this plugin's own accordion section. */
124
185
  onExpandSelf?: (() => void) | null;
186
+ /**
187
+ * Whether the host's accordion section for this plugin is currently expanded.
188
+ * Plugin UIs can watch transitions to take focus, refresh data, etc. The host
189
+ * keeps the plugin mounted across collapse/expand to preserve state, so this
190
+ * prop (not mount/unmount) is the signal that the user is actively viewing.
191
+ */
192
+ isExpanded?: boolean;
193
+ }
194
+ /**
195
+ * Canonical display metadata for a distributable sample pack, sourced from the
196
+ * HOST's pack registry (the same source it uses to download + version-check the
197
+ * bundle). Returned by `host.getSamplePackInfo` so a plugin's download CTA can
198
+ * show the live name / description / size instead of a hardcoded copy that
199
+ * drifts when a new pack version ships. Structurally compatible with
200
+ * `SamplePackCardInfo` (the CTA card prop).
201
+ *
202
+ * @since SDK 2.12.0
203
+ */
204
+ interface SamplePackPublicInfo {
205
+ /** Stable pack identifier, e.g. `'sas-instrument-pack'`. */
206
+ packId: string;
207
+ /** Human-readable pack name for the CTA headline. */
208
+ displayName: string;
209
+ /** One-line description of the pack's contents. */
210
+ description: string;
211
+ /** Size in bytes of the default download variant. */
212
+ sizeBytes: number;
125
213
  }
126
214
  /** Scoped API surface that plugins interact with. Plugins NEVER get direct TracktionEngine access. */
127
215
  interface PluginHost {
@@ -143,6 +231,10 @@ interface PluginHost {
143
231
  setTrackPan(trackId: string, pan: number): Promise<void>;
144
232
  /** Set track solo state. Only works on owned tracks. */
145
233
  setTrackSolo(trackId: string, solo: boolean): Promise<void>;
234
+ /** Whether ANY track in the project is currently soloed (across all panels).
235
+ * Lets a panel dim its non-soloed rows (the engine silences them via the
236
+ * effective-mute model). Read-only; not ownership-scoped. */
237
+ isAnySoloActive(): Promise<boolean>;
146
238
  /** Rename a track. Only works on owned tracks. */
147
239
  setTrackName(trackId: string, name: string): Promise<void>;
148
240
  /**
@@ -160,9 +252,31 @@ interface PluginHost {
160
252
  */
161
253
  setTrackRole(trackId: string, role: string): Promise<void>;
162
254
  /** Shuffle preset: keep MIDI, apply a random preset from the same category. Only works on owned tracks. */
163
- shufflePreset(trackId: string): Promise<ShufflePresetResult>;
255
+ /**
256
+ * Shuffle preset: keep MIDI, apply a random preset from the same category.
257
+ * `excludeNames` (since SDK 1.5.0) filters preset names out of the random
258
+ * pool; the current preset is always implicitly excluded. Use this to
259
+ * implement a "no-repeat until full cycle" shuffle: the panel accumulates
260
+ * the history and resets when shufflePreset throws "no presets available".
261
+ */
262
+ shufflePreset(trackId: string, excludeNames?: readonly string[]): Promise<ShufflePresetResult>;
164
263
  /** Duplicate track: copy MIDI + role to a new track with a different preset. Only works on owned tracks. */
165
264
  duplicateTrack(trackId: string): Promise<PluginTrackHandle>;
265
+ /**
266
+ * Persist this plugin's track row order for the active scene. Pass the stable
267
+ * track dbIds ({@link PluginTrackHandle.dbId}) in the desired top-to-bottom
268
+ * order. Reload-safe — {@link getPluginTracks} returns tracks in this order
269
+ * across scene switches and project reopen.
270
+ *
271
+ * Per-panel and decoupled from the engine-synced global track order, so
272
+ * reordering one panel never disturbs other plugins' tracks. Tracks omitted
273
+ * from the list (e.g. newly added or duplicated) keep their natural order at
274
+ * the end. Pairs with the {@link useTrackReorder} hook, which drives the
275
+ * drag-and-drop UI and calls this on drop.
276
+ *
277
+ * @since SDK 2.16.0
278
+ */
279
+ reorderTracks(orderedTrackIds: readonly string[]): Promise<void>;
166
280
  /**
167
281
  * Return the canonical list of valid role tokens that the host's
168
282
  * classifier and UI understand. Plugins should use this list when
@@ -208,6 +322,18 @@ interface PluginHost {
208
322
  * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.
209
323
  */
210
324
  postProcessMidi(notes: PluginMidiNote[], options: PostProcessOptions): Promise<PluginMidiNote[]>;
325
+ /**
326
+ * Read a track's current MIDI notes for in-place editing (e.g. a piano
327
+ * roll). Returns the track's clips with beat-based notes; an empty `clips`
328
+ * array means the track has no MIDI. Reads LIVE engine state (NOT the DB),
329
+ * so it reflects unsaved generator output too and needs no project_id
330
+ * scoping — do not "fix" this into a DB query.
331
+ *
332
+ * Ownership-gated like {@link writeMidiClip}. Optional so a plugin built
333
+ * against this SDK still loads on an older host — callers MUST null-check.
334
+ * @since SDK 2.15.0
335
+ */
336
+ readMidiNotes?(trackId: string): Promise<ReadMidiResult>;
211
337
  /** Place an audio file on a track this plugin owns. */
212
338
  writeAudioClip(trackId: string, filePath: string, position?: number): Promise<void>;
213
339
  /**
@@ -237,6 +363,16 @@ interface PluginHost {
237
363
  setPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;
238
364
  /** Get current plugin state (base64-encoded). */
239
365
  getPluginState(trackId: string, pluginIndex: number): Promise<string>;
366
+ /**
367
+ * Set a plugin's RAW VST3/AU state — the plugin's own getStateInformation
368
+ * format, bypassing Tracktion's ValueTree wrapper. Use for third-party
369
+ * instruments (u-he Diva, Serum, …) whose patches the ValueTree round-trip
370
+ * does not faithfully preserve. Default Surge XT presets use setPluginState.
371
+ * @since SDK 2.15.0
372
+ */
373
+ setRawPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;
374
+ /** Get a plugin's RAW VST3/AU state (see setRawPluginState). @since SDK 2.15.0 */
375
+ getRawPluginState(trackId: string, pluginIndex: number): Promise<string>;
240
376
  /** List plugins currently loaded on a track. */
241
377
  getTrackPlugins(trackId: string): Promise<PluginSynthInfo[]>;
242
378
  /** Remove a plugin from a track. */
@@ -253,14 +389,154 @@ interface PluginHost {
253
389
  showInstrumentEditor(trackId: string): Promise<void>;
254
390
  /** Close the instrument plugin's editor window. */
255
391
  hideInstrumentEditor(trackId: string): Promise<void>;
392
+ /**
393
+ * Load the engine's built-in sampler on the track (if not already
394
+ * present) and configure it with a single one-shot sound. Every MIDI
395
+ * note triggers the loaded sample regardless of pitch — used by the
396
+ * drum-generator plugin where the LLM's emitted pitch is advisory.
397
+ *
398
+ * Idempotent: calling repeatedly on the same track swaps the loaded
399
+ * sample without stacking more sampler instances. The sampler counts
400
+ * as the track's instrument; mixing it with `setTrackInstrument` on
401
+ * the same track is undefined behaviour for now.
402
+ *
403
+ * @since SDK 1.2.0
404
+ */
405
+ setTrackDrumKit(trackId: string, kit: DrumKit): Promise<void>;
406
+ /**
407
+ * Load the engine's built-in sampler on the track (if not already
408
+ * present) and configure it with a pitched, polyphonic, multi-zone
409
+ * instrument. Each MIDI note triggers the zone whose [minKey,maxKey]
410
+ * range contains it; the zone is played back pitch-shifted relative
411
+ * to its rootKey. Polyphony is handled by the Tracktion sampler's
412
+ * voice allocator.
413
+ *
414
+ * Used by the instrument-generator plugin to load a pre-rendered
415
+ * pitched-sample manifest. Mutually exclusive with `setTrackDrumKit`
416
+ * on the same track (both occupy the sampler slot) and with
417
+ * `setTrackInstrument(pluginId)` (which loads a VST synth instead).
418
+ *
419
+ * Idempotent: calling repeatedly on the same track swaps the loaded
420
+ * zones without stacking sampler instances.
421
+ *
422
+ * @since SDK 1.3.0
423
+ */
424
+ setTrackInstrumentSampler(trackId: string, instrument: InstrumentSampler): Promise<void>;
425
+ /**
426
+ * List audio files (by default `.wav`) under `rootPath`. Returns
427
+ * absolute file paths. `recursive` defaults to false; pass `true` to
428
+ * walk subdirectories. The drum-generator plugin uses this to
429
+ * lazily discover available samples without round-tripping each
430
+ * folder through `getSamples`.
431
+ *
432
+ * Plugins MUST NOT use this to read paths outside their declared
433
+ * sample roots — the host may add path validation in a later release.
434
+ *
435
+ * @since SDK 1.2.0
436
+ */
437
+ listAudioFiles(rootPath: string, options?: ListAudioFilesOptions): Promise<string[]>;
438
+ /**
439
+ * Read a text file's contents from the host filesystem (UTF-8). Returns
440
+ * `null` on any read error (missing file, permission, etc.) — the
441
+ * caller does not need to wrap the call in try/catch.
442
+ *
443
+ * Intended for plugin sample-library metadata: instrument manifest
444
+ * JSON (`<instrument-id>/manifest.json`) and prompt-sibling text
445
+ * (`<id>.txt`). Plugins parse the returned string themselves so the
446
+ * host stays content-agnostic.
447
+ *
448
+ * Plugins MUST NOT use this to read paths outside their declared
449
+ * sample roots — the host may add path validation in a later release.
450
+ *
451
+ * @since SDK 1.4.0
452
+ */
453
+ readTextFile(absolutePath: string): Promise<string | null>;
256
454
  /** Get the FULL generation context for the active scene. */
257
455
  getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;
258
456
  /** Get lightweight musical context (no concurrent track MIDI data). */
259
457
  getMusicalContext(): Promise<MusicalContext>;
260
458
  /** Get the active scene ID. Null if no scene is active. */
261
459
  getActiveSceneId(): string | null;
460
+ /**
461
+ * Get the bound project's DB id. Null when no project is bound.
462
+ * Optional — older hosts and the renderer-side host proxy may omit it;
463
+ * callers MUST feature-check. Used e.g. to detect project switches for
464
+ * per-project conversation persistence.
465
+ * @since SDK 2.18.0
466
+ */
467
+ getProjectId?(): string | null;
262
468
  /** Get list of all scenes in the project. */
263
469
  getSceneList(): Promise<PluginSceneInfo[]>;
470
+ /**
471
+ * Enumerate importable track candidates from OTHER scenes, scoped to this
472
+ * plugin's track type (derived from the plugin id). Each candidate is
473
+ * annotated with `importable` + `disabledReason` — the host computes the
474
+ * harmonic/length/tempo gate so the UI only renders it. By default the active
475
+ * scene is excluded; pass `includeSameScene` to also surface the active
476
+ * scene's MIDI tracks owned by OTHER panels (the cross-panel re-sound source).
477
+ * Scenes with no candidate of this type are omitted.
478
+ *
479
+ * Optional so a plugin built against this SDK still loads on an older host —
480
+ * callers MUST null-check and hide the affordance when absent.
481
+ * @since SDK 2.13.0
482
+ */
483
+ listImportableTracks?(opts?: ListImportableTracksOptions): Promise<ImportCandidateScene[]>;
484
+ /**
485
+ * Import a source track (from another scene) into the active scene as a
486
+ * faithful, independent copy, delegating to the `import_track_from_scene`
487
+ * tool. Returns the new track's handle so the panel can append a row.
488
+ * Throws on a gate violation — call only for candidates with `importable`.
489
+ * Optional — callers MUST null-check (see `listImportableTracks`).
490
+ * @since SDK 2.13.0
491
+ */
492
+ importTrack?(opts: {
493
+ sourceSceneId: string;
494
+ sourceTrackId: string;
495
+ }): Promise<PluginTrackHandle>;
496
+ /**
497
+ * Read a source track's CURRENT sound — sample path (drums), sampler zones
498
+ * (instruments), or Surge preset state (synths) — so a panel can copy just
499
+ * the sound onto another track, IGNORING the contract gate that `importTrack`
500
+ * enforces ("different contract, same preset"). Read-only: applies nothing.
501
+ * The selector is the source track's DB row id (`ImportCandidateTrack.dbId`).
502
+ * Returns null when the track has no stored sound. Optional — callers MUST
503
+ * null-check (see `listImportableTracks`).
504
+ * @since SDK 2.14.0
505
+ */
506
+ getTrackSound?(sourceTrackDbId: string): Promise<TrackSoundSnapshot | null>;
507
+ /**
508
+ * Read a source track's persisted MIDI by its DB row id — the cross-panel
509
+ * READ half of "re-sound a part on a different instrument". Unlike
510
+ * `readMidiNotes` (engine-read, ownership-gated), this reads the DB and is
511
+ * NOT ownership-gated, so a panel can pull a part out of a track owned by a
512
+ * DIFFERENT panel in the same scene (the selector is
513
+ * `ImportCandidateTrack.dbId`, e.g. a `sameScene` candidate). Notes are
514
+ * beat-based, identical shape to `readMidiNotes`; the loop span comes from the
515
+ * source scene. Returns `{ clips: [] }` when the track has no MIDI. Optional —
516
+ * callers MUST null-check (see `listImportableTracks`).
517
+ * @since SDK 2.20.0
518
+ */
519
+ readImportableTrackMidi?(sourceTrackDbId: string): Promise<ReadMidiResult>;
520
+ /**
521
+ * List THIS panel's family tracks in a specific scene (by DB id), WITHOUT the
522
+ * import key/length/tempo gate that `listImportableTracks` applies. Powers the
523
+ * crossfade picker: the origin (from) and target (to) scenes of a transition
524
+ * deliberately differ in key, so gating would wrongly hide valid candidates.
525
+ * Project-scoped, read-only. Returns [] for an unknown/empty scene. Optional —
526
+ * callers MUST null-check (see `listImportableTracks`).
527
+ * @since SDK 2.22.0
528
+ */
529
+ listSceneFamilyTracks?(sceneDbId: string): Promise<SceneFamilyTrack[]>;
530
+ /**
531
+ * Read a specific scene's musical key (tonic + mode) by db id. Labels the
532
+ * SOURCE keys of a crossfade's origin/target patterns — the active-scene
533
+ * musical context only carries the transition scene's key. Optional — callers
534
+ * MUST null-check. @since SDK 2.24.0
535
+ */
536
+ getSceneKey?(sceneDbId: string): Promise<{
537
+ key: string;
538
+ mode: string;
539
+ } | null>;
264
540
  /** Subscribe to transport state changes. Returns unsubscribe function. */
265
541
  onTransportEvent(listener: TransportEventListener): UnsubscribeFn;
266
542
  /** Subscribe to deck boundary events. Returns unsubscribe function. */
@@ -269,8 +545,59 @@ interface PluginHost {
269
545
  onSceneChange(listener: SceneChangeListener): UnsubscribeFn;
270
546
  /** Get current transport state (one-shot). */
271
547
  getTransportState(): Promise<PluginTransportState>;
548
+ /**
549
+ * One-shot mono peak level for every track this plugin owns. Drives the
550
+ * cosmetic per-track strip meters; poll at ~30Hz while the transport is
551
+ * playing. The host scopes the result to this plugin's tracks and coalesces
552
+ * the underlying engine read, so a busy engine yields a STALE meter rather
553
+ * than a backlog (playback always wins over the GUI). Optional: guard with
554
+ * `typeof host.getTrackLevels === 'function'` for older hosts.
555
+ * @since SDK 2.21.0
556
+ */
557
+ getTrackLevels?(): Promise<PluginTrackLevel[]>;
272
558
  /** Generate text/JSON via the host's authenticated LLM service. */
273
559
  generateWithLLM(request: LLMGenerationRequest): Promise<LLMGenerationResult>;
560
+ /**
561
+ * Generate with native tool-use (function calling). Used by agentic plugins
562
+ * (chat panel, etc.) to drive an iterative loop where the model calls tools,
563
+ * observes results, and decides next steps — same loop class as Claude Code
564
+ * or VS Code agent mode.
565
+ *
566
+ * Shape mirrors Gemini's `generateContent` REST surface; the host forwards
567
+ * verbatim to the gateway's Gemini-native passthrough endpoint, which adds
568
+ * the central Google API key. Plugins never see provider credentials.
569
+ *
570
+ * Available since SDK 2.4.0.
571
+ */
572
+ generateWithLLMTools(request: LLMToolUseRequest): Promise<LLMToolUseResponse>;
573
+ /**
574
+ * Resolve absolute paths for spawning the bundled `sas` CLI as a subprocess.
575
+ * Used by agentic plugins that drive the CLI as their tool surface (chat
576
+ * panel, etc.). Returns `null` when called from a renderer-side host or
577
+ * when the CLI isn't accessible.
578
+ *
579
+ * Available since SDK 2.4.0.
580
+ */
581
+ getCliPaths(): {
582
+ appExe: string;
583
+ cliEntry: string;
584
+ } | null;
585
+ /**
586
+ * Resolve the absolute path to a bundled resource directory shipped with
587
+ * the app via `extraResources` (e.g. `'drum-samples'`,
588
+ * `'tracktion-presets'`). In dev, resolves to
589
+ * `<projectRoot>/resources/<name>`. In packaged builds, resolves to
590
+ * `<process.resourcesPath>/<name>`.
591
+ *
592
+ * Returns `null` if the host cannot resolve paths in this context
593
+ * (e.g. Electron mocked out in unit tests). Plugins MUST null-check and
594
+ * either degrade gracefully or fall back to a known dev path.
595
+ *
596
+ * Async by design: the renderer-side host proxy round-trips through IPC.
597
+ *
598
+ * @since SDK 2.7.0
599
+ */
600
+ getBundledResourcePath(name: string): Promise<string | null>;
274
601
  /** Check if LLM access is available (user authenticated + gateway reachable). */
275
602
  isLLMAvailable(): Promise<boolean>;
276
603
  /**
@@ -281,23 +608,59 @@ interface PluginHost {
281
608
  * `'scene'` to hide project-level tools they shouldn't call. When omitted,
282
609
  * every tool regardless of scope is returned.
283
610
  *
611
+ * `opts.includeDeferred` (since SDK 2.18.0) opts in to tools flagged with
612
+ * `deferLoading` (progressive disclosure). Default `false` mirrors
613
+ * `/api/v1/actions` — the curated core surface. Used by curation layers
614
+ * that promote specific deferred/project tools onto an agent's default
615
+ * declaration set.
616
+ *
284
617
  * @since SDK 1.2.0
285
618
  */
286
619
  listAppTools(opts?: {
287
620
  scope?: 'scene' | 'project';
621
+ includeDeferred?: boolean;
288
622
  }): Promise<PluginAppTool[]>;
289
623
  /**
290
624
  * Execute a host app tool by name. Delegates to the in-process
291
- * ToolRegistry — every mutation broadcasts to the UI automatically.
625
+ * ToolRegistry — every call (including this one) broadcasts to the
626
+ * UI's `mutations:tool-executed` channel so renderer state stays
627
+ * fresh whether the call mutates or is read-only. Read-only callers
628
+ * pay zero extra cost since the renderer debounces and skips
629
+ * redundant reloads.
292
630
  *
293
631
  * For scene-scoped tools tagged with `autoBindSceneId`, the host
294
632
  * overrides the caller's `sceneId` param with the currently-active
295
633
  * scene. That keeps a scene-bound caller from accidentally targeting
296
634
  * another scene.
297
635
  *
636
+ * `opts.provenance` (since SDK 2.18.0) stamps the originating actor onto
637
+ * every domain event this call emits — pass `'agent'` from autonomous
638
+ * agent loops so the UI orchestrator can gate auto-navigation, `'user'`
639
+ * when proxying a direct user gesture. Omitted = `'system'`.
640
+ *
298
641
  * @since SDK 1.2.0
299
642
  */
300
- executeAppTool(name: string, params: Record<string, unknown>): Promise<PluginAppToolResult>;
643
+ executeAppTool(name: string, params: Record<string, unknown>, opts?: {
644
+ provenance?: 'agent' | 'user';
645
+ }): Promise<PluginAppToolResult>;
646
+ /**
647
+ * Monotonic counter that increments on every state mutation
648
+ * (`broadcastMutation('tool-executed', ...)`). Use as a cache key for
649
+ * derived state that depends on the project: when the counter changes,
650
+ * something mutated; when it doesn't, your cache is still valid.
651
+ *
652
+ * Mostly aimed at performance-sensitive callers like ambient-context
653
+ * builders that want to skip re-querying state when nothing has
654
+ * changed. The counter is process-local — it resets on app restart
655
+ * and is not durable across sessions.
656
+ *
657
+ * Implementation detail: the counter is bumped by `mutation-broadcaster`
658
+ * before the broadcaster fires, so a synchronous `getMutationSeq()`
659
+ * call from inside a mutation listener will see the post-bump value.
660
+ *
661
+ * @since SDK 2.6.0
662
+ */
663
+ getMutationSeq(): number;
301
664
  /** Get available preset categories for a synth plugin. */
302
665
  getPresetCategories(pluginName: string): Promise<string[]>;
303
666
  /** Get a random preset from a category. */
@@ -310,6 +673,150 @@ interface PluginHost {
310
673
  getDataDirectory(): string;
311
674
  /** Persisted key-value settings store. */
312
675
  settings: PluginSettingsStore;
676
+ /**
677
+ * Return the absolute path to an installed sample pack's root directory,
678
+ * or `null` if the pack is missing OR its installed version doesn't match
679
+ * what the current app build expects.
680
+ *
681
+ * Plugins should treat `null` as "show the download CTA"; do NOT fall back
682
+ * to a hardcoded path. The host owns where samples live (currently
683
+ * `<userData>/samples/<installSubdir>/`).
684
+ *
685
+ * Stable packIds: `'sas-drum-pack'`, `'sas-instrument-pack'`. Both packs
686
+ * are downloaded on demand via the host's pack-download flow; see
687
+ * `host.isSamplePackCurrent` and the renderer-side `DownloadPackButton`.
688
+ *
689
+ * @since SDK 2.7.0
690
+ */
691
+ getSamplePackRoot(packId: string): Promise<string | null>;
692
+ /**
693
+ * True if the installed version of `packId` matches the version this app
694
+ * build expects. False if the pack is missing OR the installed version
695
+ * differs (older or newer).
696
+ *
697
+ * Plugins call this on activate to decide between rendering their normal
698
+ * UI vs the "Sample library not installed / Update available" CTA.
699
+ *
700
+ * @since SDK 2.7.0
701
+ */
702
+ isSamplePackCurrent(packId: string): Promise<boolean>;
703
+ /**
704
+ * Return the currently-installed version string for `packId` (e.g. `'1'`,
705
+ * `'2'`), or `null` if the pack is not installed at all. Reads the
706
+ * `_pack-version.json` marker inside the pack's install dir.
707
+ *
708
+ * Useful for distinguishing the "missing" CTA from the "stale, update
709
+ * available" CTA — plugins can call this when `isSamplePackCurrent`
710
+ * returns false to pick the right empty-state message.
711
+ *
712
+ * @since SDK 2.7.0
713
+ */
714
+ getSamplePackInstalledVersion(packId: string): Promise<string | null>;
715
+ /**
716
+ * Trigger a download + install of `packId` via the host's pack system (the
717
+ * same flow `getSamplePackRoot` / `isSamplePackCurrent` report on). Resolves
718
+ * when the install completes or fails. Plugins call this from a "download
719
+ * library" CTA instead of reaching into the app's IPC (`window.electronAPI`)
720
+ * directly.
721
+ *
722
+ * @since SDK 2.8.0
723
+ */
724
+ startSamplePackDownload(packId: string): Promise<{
725
+ success: boolean;
726
+ error?: string;
727
+ }>;
728
+ /**
729
+ * Subscribe to download/install progress for `packId`. Returns an unsubscribe
730
+ * fn. `status` mirrors the host's pack-download states (e.g. `'downloading' |
731
+ * 'extracting' | 'installing' | 'complete' | 'error'`); `progress` is 0-100.
732
+ *
733
+ * @since SDK 2.8.0
734
+ */
735
+ onSamplePackProgress(packId: string, listener: (progress: {
736
+ packId?: string;
737
+ status: string;
738
+ progress: number;
739
+ message?: string;
740
+ }) => void): UnsubscribeFn;
741
+ /**
742
+ * Return the canonical display metadata (`displayName`, `description`,
743
+ * `sizeBytes`) for `packId` from the host's pack registry — the SAME source
744
+ * the host uses to download + version-check the pack. A plugin's download CTA
745
+ * should prefer this over a hardcoded copy so the size/description stay in
746
+ * sync with whatever bundle the host actually ships (no per-version drift).
747
+ * Resolves `null` for an unknown packId.
748
+ *
749
+ * Optional so a plugin built against this SDK still runs on an older host:
750
+ * callers should fall back to their own static copy when it is absent or
751
+ * returns `null`.
752
+ *
753
+ * @since SDK 2.12.0
754
+ */
755
+ getSamplePackInfo?(packId: string): Promise<SamplePackPublicInfo | null>;
756
+ /**
757
+ * Per-pack roots of the USER's imported sample packs for `kind`. Each root
758
+ * is laid out exactly like the corresponding stock pack (drums:
759
+ * `<root>/<role>/<file>.wav` + `.txt` sidecars; instruments:
760
+ * `<root>/<category>/<id>/manifest.json`), so resolvers scan them as
761
+ * additional roots alongside `getSamplePackRoot`. `[]` when nothing is
762
+ * imported. User content lives under `<userData>/user-samples/` — strictly
763
+ * separate on disk; stock pack installs never touch it.
764
+ *
765
+ * Optional for older-host compat: feature-check
766
+ * (`host.getUserSampleRoots?.(...)`) and treat absence as `[]`.
767
+ *
768
+ * @since SDK 2.20.0
769
+ */
770
+ getUserSampleRoots?(kind: 'drums' | 'instruments'): Promise<string[]>;
771
+ /**
772
+ * Ask the host app to open its sample-import wizard targeting `kind`.
773
+ * Fire-and-forget; renderer-hosted plugins only (the wizard is an app-level
774
+ * modal — the main-process host no-ops). Library changes land as
775
+ * `onSamplePackProgress` events with packId `user:<kind>` and
776
+ * `status: 'complete'`, so subscribe to that to refresh.
777
+ *
778
+ * @since SDK 2.20.0
779
+ */
780
+ openSampleImportWizard?(kind: 'drums' | 'instruments'): void;
781
+ /**
782
+ * Start a deck playing the given scene/transition. Mirrors the workstation's
783
+ * transport play. `contentType` defaults to `'scene'`.
784
+ *
785
+ * @since SDK 2.9.0
786
+ */
787
+ deckPlay(deckId: string, contentId?: string, contentType?: 'scene' | 'transition'): Promise<{
788
+ success: boolean;
789
+ error?: string;
790
+ code?: string;
791
+ }>;
792
+ /**
793
+ * Stop a deck.
794
+ *
795
+ * @since SDK 2.9.0
796
+ */
797
+ deckStop(deckId: string): Promise<{
798
+ success: boolean;
799
+ error?: string;
800
+ }>;
801
+ /**
802
+ * Subscribe to per-deck state changes. Each event carries the `deckId`, the
803
+ * `property` that changed (e.g. `'playing'`), and its new `value`. Returns an
804
+ * unsubscribe fn.
805
+ *
806
+ * @since SDK 2.9.0
807
+ */
808
+ onDeckStateChanged(listener: (event: {
809
+ deckId: string;
810
+ property: string;
811
+ value: unknown;
812
+ }) => void): UnsubscribeFn;
813
+ /**
814
+ * Subscribe to the "all decks stopped" engine event (e.g. global transport
815
+ * stop). Returns an unsubscribe fn.
816
+ *
817
+ * @since SDK 2.9.0
818
+ */
819
+ onAllDecksStopped(listener: () => void): UnsubscribeFn;
313
820
  /** Get a value from scene-scoped plugin data. */
314
821
  getSceneData<T = unknown>(sceneId: string, key: string): Promise<T | null>;
315
822
  /** Set a value in scene-scoped plugin data. */
@@ -412,12 +919,55 @@ interface PluginHost {
412
919
  setAudioOffsetSamples(trackId: string, offsetSamples: number): Promise<void>;
413
920
  /** Read the current manual offset (0 if never set). */
414
921
  getAudioOffsetSamples(trackId: string): Promise<number>;
922
+ /**
923
+ * Read raw bytes of an audio file written by the host. The path may be
924
+ * `~app/`-relative or project-relative — the host resolves it using the
925
+ * same logic as `writeAudioClip`. Throws FILE_NOT_FOUND if the path
926
+ * can't be resolved or doesn't exist on disk.
927
+ */
928
+ getAudioFileBytes(filePath: string): Promise<ArrayBuffer>;
929
+ /** Persist the original (raw, un-trimmed) audio file path for a track. */
930
+ setRawAudioFilePath(trackId: string, filePath: string | null): Promise<void>;
931
+ /** Read the raw audio file path persisted via `setRawAudioFilePath`. */
932
+ getRawAudioFilePath(trackId: string): Promise<string | null>;
933
+ /**
934
+ * Persist the cue-points detected in the raw (un-trimmed) audio file.
935
+ * Sample positions are in input-file coordinates.
936
+ */
937
+ setRawCuePoints(trackId: string, cues: PluginCuePoints | null): Promise<void>;
938
+ /** Read raw-domain cue points persisted via `setRawCuePoints`. */
939
+ getRawCuePoints(trackId: string): Promise<PluginCuePoints | null>;
940
+ /** Persist the current trim window inside the raw audio file. */
941
+ setTrimWindow(trackId: string, window: PluginTrimWindow | null): Promise<void>;
942
+ /** Read the current trim window persisted via `setTrimWindow`. */
943
+ getTrimWindow(trackId: string): Promise<PluginTrimWindow | null>;
944
+ /**
945
+ * Re-trim the raw audio file at the given sample offset and replace the
946
+ * track's audio clip with the new slice. Persists updated trimmed-domain
947
+ * cue points and the new trim window.
948
+ */
949
+ commitTrimWindow(trackId: string, startSample: number, durationSamples: number): Promise<{
950
+ filePath: string;
951
+ cuePoints: PluginCuePoints | null;
952
+ }>;
415
953
  /** Trigger bulk composition for the active scene (LLM plans arrangement, creates tracks, generates MIDI). */
416
954
  composeScene(options: ComposeSceneOptions): Promise<ComposeSceneResult>;
417
955
  /** Subscribe to composition progress events (planning, generating, complete, error). */
418
956
  onComposeProgress(listener: ComposeProgressListener): UnsubscribeFn;
419
957
  /** Subscribe to engine ready events (fires when the engine finishes loading tracks after a scene change). */
420
958
  onEngineReady(listener: () => void): UnsubscribeFn;
959
+ /**
960
+ * Subscribe to external state mutations (CLI, MCP, or HTTP-API tool calls
961
+ * that bypass plugin-host methods). Fires after such a tool finishes,
962
+ * signalling that scene/track DB state may have changed underneath the
963
+ * plugin's local cache. Use it to refresh state that the plugin doesn't
964
+ * own — e.g. re-running adoptSceneTracks() so AI-created tracks become
965
+ * visible without requiring the user to switch scenes.
966
+ *
967
+ * Optional: only the renderer-side host implements this. Main-side
968
+ * plugins should subscribe to the typed domain-event bus instead.
969
+ */
970
+ onAfterAgentMutation?(listener: () => void): UnsubscribeFn;
421
971
  /** Audition a single note on a track (fire-and-forget preview). */
422
972
  auditionNote(trackId: string, pitch: number, velocity: number, durationMs: number): Promise<void>;
423
973
  /** Get presets for this plugin, optionally filtered by category. */
@@ -434,6 +984,116 @@ interface PluginHost {
434
984
  splitStems(trackId: string): Promise<PluginStemSplitResult>;
435
985
  /** Check if the stem splitter binary is available. */
436
986
  isStemSplitterAvailable(): Promise<boolean>;
987
+ /**
988
+ * Enumerate audio input devices visible to the engine. Empty list means
989
+ * no input device is available (or the OS denied permission). Requires
990
+ * `audioCapture` capability.
991
+ * @since SDK 2.1.0
992
+ */
993
+ getAudioInputDevices(): Promise<AudioInputDevice[]>;
994
+ /**
995
+ * Snapshot of engine state needed to start a recording session. Reads
996
+ * the engine sample rate, the active scene id, the transition-render
997
+ * lock state, and current BPM/bars. Requires `audioCapture`.
998
+ * @since SDK 2.1.0
999
+ */
1000
+ getRecordingTargetInfo(): Promise<RecordingTargetInfo>;
1001
+ /**
1002
+ * Begin a recording session. Engine writes integer-PCM WAV chunks to
1003
+ * disk; one chunk per call to `markRecordingChunkBoundary`. Each
1004
+ * finalized chunk fires a `RecordingChunkFinalizedEvent` to
1005
+ * subscribers of `onRecordingChunkFinalized`. Throws
1006
+ * AUDIO_CAPTURE_DENIED on permission failure or if no device is
1007
+ * available.
1008
+ *
1009
+ * Pass `deviceId` to override the platform-configured input (rare —
1010
+ * only useful for tests or workflows that need a specific device).
1011
+ * Omit it to use the platform's selected input from
1012
+ * `AudioRoutingConfig.inputDeviceId` — this is the recommended path
1013
+ * for plugins post-SDK-2.2.0.
1014
+ *
1015
+ * @since SDK 2.1.0 (deviceId required) — 2.2.0 made it optional.
1016
+ */
1017
+ startTrackRecording(deviceId?: string): Promise<void>;
1018
+ /**
1019
+ * Mark the boundary between two recording chunks. The engine closes the
1020
+ * currently-open WAV writer and opens a new one; the closed file fires
1021
+ * a `RecordingChunkFinalizedEvent` once flush completes. No-op if no
1022
+ * recording session is active.
1023
+ *
1024
+ * Pass `boundaryHostTimeNs` from `DeckBoundaryEvent.boundaryHostTimeNs`
1025
+ * for sample-perfect take alignment (Path 2). The engine then splits
1026
+ * the chunk at the EXACT recorder-sample that corresponds to that
1027
+ * host-time, eliminating the ~5–50 ms of jitter introduced by the
1028
+ * legacy "split wherever the writer is" path. Required for any
1029
+ * workflow that overlays multiple takes (vocalist comping, layered
1030
+ * dubs); optional for single-take captures.
1031
+ *
1032
+ * @since SDK 2.1.0 — 2.4.0 added optional boundaryHostTimeNs.
1033
+ */
1034
+ markRecordingChunkBoundary(boundaryHostTimeNs?: number): Promise<void>;
1035
+ /**
1036
+ * Stop the active recording session. The final chunk is closed and
1037
+ * finalized; its `RecordingChunkFinalizedEvent` fires before this
1038
+ * promise resolves. Returns the path of the final chunk (also delivered
1039
+ * via the event for symmetry).
1040
+ * @since SDK 2.1.0
1041
+ */
1042
+ stopTrackRecording(): Promise<{
1043
+ finalChunkPath: string;
1044
+ durationMs: number;
1045
+ }>;
1046
+ /**
1047
+ * Subscribe to chunk-finalized events for this plugin's active recording
1048
+ * session. Auto-unsubscribed on `deactivate`. Returns unsubscribe fn.
1049
+ * @since SDK 2.1.0
1050
+ */
1051
+ onRecordingChunkFinalized(listener: (event: RecordingChunkFinalizedEvent) => void): UnsubscribeFn;
1052
+ /**
1053
+ * Get the platform-configured audio input device, or null when no
1054
+ * device is set. Read-only; configured via the assistant's
1055
+ * AudioRoutingPanel. Plugins use this to display the current input
1056
+ * to the user without exposing their own picker.
1057
+ *
1058
+ * @since SDK 2.2.0
1059
+ */
1060
+ getCurrentInputDevice(): Promise<AudioInputDevice | null>;
1061
+ /**
1062
+ * Subscribe to input-device changes (user picks a new mic in the
1063
+ * Audio Routing panel). Listeners should refetch via
1064
+ * `getCurrentInputDevice()`. Returns an unsubscribe fn.
1065
+ *
1066
+ * @since SDK 2.4.0
1067
+ */
1068
+ onInputDeviceChange(listener: () => void): UnsubscribeFn;
1069
+ /**
1070
+ * Get the platform's mic-to-output round-trip latency offset in
1071
+ * samples. 0 = uncalibrated. Plugins recording audio apply this via
1072
+ * `setAudioOffsetSamples` so takes line up with the source loop.
1073
+ *
1074
+ * @since SDK 2.2.0
1075
+ */
1076
+ getRecordingLatencyOffsetSamples(): Promise<number>;
1077
+ /**
1078
+ * Snapshot of the input level for the most recent audio block.
1079
+ * Renderer polls at ~30Hz to drive a level meter / scrolling
1080
+ * waveform without an event-channel subscription.
1081
+ *
1082
+ * @since SDK 2.3.0
1083
+ */
1084
+ getRecordingInputLevel(): Promise<{
1085
+ peakDb: number;
1086
+ peakLinear: number;
1087
+ clipped: boolean;
1088
+ active: boolean;
1089
+ }>;
1090
+ /**
1091
+ * Reset the latched clip indicator. Safe regardless of whether
1092
+ * monitoring or recording is active.
1093
+ *
1094
+ * @since SDK 2.3.0
1095
+ */
1096
+ clearRecordingInputClipIndicator(): Promise<void>;
437
1097
  }
438
1098
  /** Stem type identifiers */
439
1099
  type StemType = 'vocals' | 'drums' | 'bass' | 'other';
@@ -490,6 +1150,105 @@ interface PluginTrackHandle {
490
1150
  /** Custom instrument display name (null = Surge XT) */
491
1151
  instrumentName?: string | null;
492
1152
  }
1153
+ /**
1154
+ * One source track offered by `listImportableTracks`, already filtered to the
1155
+ * calling panel's type. The host computes the gate; the UI only renders it.
1156
+ * @since SDK 2.13.0
1157
+ */
1158
+ interface ImportCandidateTrack {
1159
+ /** Source track's engine track id (the selector passed back to importTrack). */
1160
+ trackId: string;
1161
+ /** Source track's DB row id (globally unique; good React key). */
1162
+ dbId: string;
1163
+ /** Display name shown in the modal row. */
1164
+ name: string;
1165
+ /** Musical role if set (drives the row icon). */
1166
+ role?: string;
1167
+ /** True when this track can be copied into the active scene as-is. */
1168
+ importable: boolean;
1169
+ /** Why the track is disabled (shown as a tooltip). Present iff `!importable`. */
1170
+ disabledReason?: string;
1171
+ }
1172
+ /**
1173
+ * One track in a specific scene, returned by `host.listSceneFamilyTracks`,
1174
+ * already narrowed to the calling panel's family. Unlike `ImportCandidateTrack`
1175
+ * it carries NO import gate — the crossfade picker lists every same-family track
1176
+ * in the origin/target scene regardless of key/length. @since SDK 2.22.0
1177
+ */
1178
+ interface SceneFamilyTrack {
1179
+ /** Track's DB row id — the selector for getTrackSound + crossfade metadata. */
1180
+ dbId: string;
1181
+ /** Display name shown in the picker. */
1182
+ name: string;
1183
+ /** Musical role if set — used to enforce same-role crossfade pairing. */
1184
+ role?: string;
1185
+ }
1186
+ /**
1187
+ * One OTHER scene and its candidate tracks (already type-filtered). Scenes with
1188
+ * zero candidates of the panel's type are omitted by the host.
1189
+ * @since SDK 2.13.0
1190
+ */
1191
+ interface ImportCandidateScene {
1192
+ /** Source scene's engine scene id. */
1193
+ sceneId: string;
1194
+ /** Source scene's display name. */
1195
+ sceneName: string;
1196
+ /** Candidate tracks of this panel's type (may include disabled ones). */
1197
+ tracks: ImportCandidateTrack[];
1198
+ /**
1199
+ * True for the synthetic "this scene — other panels" entry: the ACTIVE
1200
+ * scene's MIDI tracks owned by OTHER panels. Importing one re-sounds the part
1201
+ * on the calling panel's instrument (via `readImportableTrackMidi` +
1202
+ * `writeMidiClip`) rather than faithfully copying it. Absent/false for
1203
+ * ordinary cross-scene entries. @since SDK 2.20.0
1204
+ */
1205
+ sameScene?: boolean;
1206
+ }
1207
+ /**
1208
+ * A source track's current sound, as returned by `host.getTrackSound`. The
1209
+ * discriminant matches the panel that reads it: drums → 'sample', instruments →
1210
+ * 'instrument', synths → 'preset'. `label` is the human name for the History row.
1211
+ * @since SDK 2.14.0
1212
+ */
1213
+ /**
1214
+ * How a synth `state` blob is serialized. `valuetree` is Tracktion's wrapped
1215
+ * format (default Surge XT presets); `raw` is the plugin's own
1216
+ * getStateInformation format (third-party instruments). Absent ⇒ `valuetree`,
1217
+ * for backward compatibility with history recorded before SDK 2.15.0.
1218
+ * @since SDK 2.15.0
1219
+ */
1220
+ type SynthStateType = 'raw' | 'valuetree';
1221
+ type TrackSoundSnapshot = {
1222
+ kind: 'sample';
1223
+ samplePath: string;
1224
+ label: string;
1225
+ } | {
1226
+ kind: 'instrument';
1227
+ displayName: string;
1228
+ instrumentId: string | null;
1229
+ zones: InstrumentZone[];
1230
+ label: string;
1231
+ } | {
1232
+ kind: 'preset';
1233
+ state: string;
1234
+ label: string;
1235
+ stateType?: SynthStateType;
1236
+ };
1237
+ /** Options for `PluginHost.listImportableTracks`. @since SDK 2.13.0 */
1238
+ interface ListImportableTracksOptions {
1239
+ /**
1240
+ * Coarse content family. 'midi' = synth/drum/instrument, 'audio' = stems,
1241
+ * 'sample' = loops. Defaults are derived from the calling plugin id, so
1242
+ * panels normally pass nothing.
1243
+ */
1244
+ family?: 'midi' | 'audio' | 'sample';
1245
+ /**
1246
+ * When true, prepend the active scene's MIDI tracks owned by OTHER panels as a
1247
+ * `sameScene` entry (the cross-panel re-sound source). Off by default so the
1248
+ * plain cross-scene import is unchanged. MIDI panels only. @since SDK 2.20.0
1249
+ */
1250
+ includeSameScene?: boolean;
1251
+ }
493
1252
  interface PluginTrackInfo extends PluginTrackHandle {
494
1253
  /** Is track muted? */
495
1254
  muted: boolean;
@@ -558,6 +1317,29 @@ interface MidiWriteResult {
558
1317
  /** Actual bars covered */
559
1318
  bars: number;
560
1319
  }
1320
+ /**
1321
+ * One clip returned by {@link PluginHost.readMidiNotes}. `endTime - startTime`
1322
+ * (seconds) is the clip's loop span; round-trip it back into
1323
+ * {@link MidiClipData} on save so an edit never changes the clip length.
1324
+ * @since SDK 2.15.0
1325
+ */
1326
+ interface ReadMidiClip {
1327
+ /** Clip start in seconds (engine timeline). */
1328
+ startTime: number;
1329
+ /** Clip end in seconds. Loop span = endTime - startTime. */
1330
+ endTime: number;
1331
+ /** Beat-based notes, identical shape to {@link MidiClipData.notes}. */
1332
+ notes: PluginMidiNote[];
1333
+ }
1334
+ /**
1335
+ * Result of {@link PluginHost.readMidiNotes}: every clip on the track. Drum /
1336
+ * instrument / synth tracks are single-clip, so callers normally use
1337
+ * `clips[0]`; the array form mirrors the engine and is future-proof.
1338
+ * @since SDK 2.15.0
1339
+ */
1340
+ interface ReadMidiResult {
1341
+ clips: ReadMidiClip[];
1342
+ }
561
1343
  /**
562
1344
  * Options for {@link PluginHost.exportTracksAsMidiBundle}.
563
1345
  * @since SDK 1.1.0
@@ -672,6 +1454,15 @@ interface MusicalContext {
672
1454
  genre: string | null;
673
1455
  timeSignature: string;
674
1456
  chordProgression: PluginChordTiming[];
1457
+ /**
1458
+ * The scene's natural-language contract prompt (e.g. "dark psytrance,
1459
+ * driving 130 BPM, claustrophobic"). Null when the scene has no
1460
+ * contract set yet. Auto-prefixed to the LLM by `host.generateWithLLM`
1461
+ * so every per-track generation sees the scene-level intent without
1462
+ * each plugin having to plumb it through manually.
1463
+ * @since SDK 1.2.0
1464
+ */
1465
+ contractPrompt: string | null;
675
1466
  }
676
1467
  interface PluginChordTiming {
677
1468
  /** Chord symbol: 'Cm7', 'G', 'Fmaj7', etc. */
@@ -692,6 +1483,15 @@ interface PluginGenerationContext {
692
1483
  genre: string | null;
693
1484
  };
694
1485
  concurrentTracks: PluginConcurrentTrackInfo[];
1486
+ /**
1487
+ * Count of tracks the host had to drop entirely from `concurrentTracks`
1488
+ * because their notes pushed the running total past the cross-track
1489
+ * budget. Panels should disclose this to the LLM (e.g. "… N additional
1490
+ * tracks omitted to fit token budget") so the model knows it is
1491
+ * working with partial context.
1492
+ * @since SDK 1.2.0
1493
+ */
1494
+ truncatedTrackCount?: number;
695
1495
  }
696
1496
  interface PluginConcurrentTrackInfo {
697
1497
  trackId: string;
@@ -699,6 +1499,22 @@ interface PluginConcurrentTrackInfo {
699
1499
  presetCategory: string | null;
700
1500
  /** Notes organized by which chord they fall under */
701
1501
  notesByChord: PluginChordSegment[];
1502
+ /**
1503
+ * The user-typed prompt that produced this track's MIDI (from
1504
+ * `tracks.prompt`). Lets the LLM see *intent* alongside the notes —
1505
+ * "punchy 909 kick" carries more meaning than the kick MIDI alone.
1506
+ * @since SDK 1.2.0
1507
+ */
1508
+ prompt?: string;
1509
+ /**
1510
+ * True when the host capped this track's notes (per-track budget).
1511
+ * The `notesByChord` payload is a prefix of the real content; the
1512
+ * total dropped count is `originalNoteCount - sum(notesByChord.notes.length)`.
1513
+ * @since SDK 1.2.0
1514
+ */
1515
+ truncated?: boolean;
1516
+ /** The track's full note count before per-track truncation. */
1517
+ originalNoteCount?: number;
702
1518
  }
703
1519
  interface PluginChordSegment {
704
1520
  chord: string;
@@ -716,6 +1532,20 @@ interface DeckBoundaryEvent {
716
1532
  bar: number;
717
1533
  beat: number;
718
1534
  loopCount: number;
1535
+ /**
1536
+ * Stream-time sample index at which the loop wrap was detected in the
1537
+ * audio thread (engine's AudioBoundaryProbe). Undefined when the
1538
+ * audio-thread anchor was unavailable. @since SDK 2.4.0
1539
+ */
1540
+ boundaryAudioSamplePosition?: number;
1541
+ /**
1542
+ * Monotonic host-time (nanoseconds) at the audio block in which the
1543
+ * loop wrap was detected. Same clock as
1544
+ * `juce::AudioIODeviceCallbackContext::hostTimeNs`. Pair with
1545
+ * `markRecordingChunkBoundary(boundaryHostTimeNs)` for sample-perfect
1546
+ * take alignment. @since SDK 2.4.0
1547
+ */
1548
+ boundaryHostTimeNs?: number;
719
1549
  }
720
1550
  interface PluginTransportState {
721
1551
  isPlaying: boolean;
@@ -724,6 +1554,20 @@ interface PluginTransportState {
724
1554
  position: number;
725
1555
  timeSignature: string;
726
1556
  }
1557
+ /**
1558
+ * Mono peak level for a single track, as reported by `getTrackLevels()`.
1559
+ * Drives the cosmetic per-track strip meters. `peakDb` is the max of the
1560
+ * L/R channels, floored at -120 (the "no signal" sentinel).
1561
+ * @since SDK 2.21.0
1562
+ */
1563
+ interface PluginTrackLevel {
1564
+ /** Tracktion engine track id — matches `PluginTrackHandle.id`. */
1565
+ trackId: string;
1566
+ /** Mono peak in dBFS (max of L/R), floored at -120. */
1567
+ peakDb: number;
1568
+ /** Latched overload since the last poll. */
1569
+ clipped: boolean;
1570
+ }
727
1571
  interface PluginSceneInfo {
728
1572
  id: string;
729
1573
  name: string;
@@ -752,6 +1596,17 @@ interface PluginSceneContext {
752
1596
  hasTracks: boolean;
753
1597
  /** Whether bulk generation is currently in progress */
754
1598
  isBulkGenerating: boolean;
1599
+ /**
1600
+ * Scene kind. A 'transition' scene bridges two other scenes (the
1601
+ * transition-as-scene feature) and unlocks the crossfade-track UI in the
1602
+ * instrument panels; ordinary scenes are 'scene'. Absent on older hosts.
1603
+ * @since SDK 2.22.0
1604
+ */
1605
+ sceneType?: 'scene' | 'transition';
1606
+ /** For a transition scene, the DB id of the scene it bridges FROM (origin). Null otherwise. @since SDK 2.22.0 */
1607
+ transitionFromSceneId?: string | null;
1608
+ /** For a transition scene, the DB id of the scene it bridges TO (target). Null otherwise. @since SDK 2.22.0 */
1609
+ transitionToSceneId?: string | null;
755
1610
  }
756
1611
  /** Placeholder track state for the progressive bulk-add UX */
757
1612
  interface BulkAddPlaceholderTrack {
@@ -790,17 +1645,118 @@ interface LLMGenerationResult {
790
1645
  /** Model that generated the response */
791
1646
  model: string;
792
1647
  }
793
- interface PluginPresetData {
794
- name: string;
795
- category: string;
796
- /** Base64-encoded plugin state — pass to setPluginState() */
797
- state: string;
1648
+ /** A single part of a Gemini-style content block. */
1649
+ interface LLMPart {
1650
+ /** Plain text. Mutually exclusive with functionCall / functionResponse. */
1651
+ text?: string;
1652
+ /** A tool/function the model is asking the host to invoke. */
1653
+ functionCall?: {
1654
+ name: string;
1655
+ args: Record<string, unknown>;
1656
+ /**
1657
+ * Opaque signature returned by Gemini 3+ tool-use models. Must be echoed
1658
+ * verbatim when the assistant turn is replayed on a later iteration, or
1659
+ * the API rejects the request with a 400 ("Function call is missing a
1660
+ * thought_signature in functionCall parts."). Pre-Gemini-3 models leave
1661
+ * this undefined; preserving it round-trip is safe across families.
1662
+ */
1663
+ thoughtSignature?: string;
1664
+ };
1665
+ /** The result of a tool call, fed back into the loop on the next turn. */
1666
+ functionResponse?: {
1667
+ name: string;
1668
+ response: Record<string, unknown>;
1669
+ };
1670
+ }
1671
+ interface LLMContent {
1672
+ /** 'user' = user/tool-result; 'model' = assistant. */
1673
+ role: 'user' | 'model';
1674
+ parts: LLMPart[];
1675
+ }
1676
+ interface LLMFunctionDeclaration {
1677
+ name: string;
1678
+ description: string;
1679
+ /** JSON Schema. Use `type: 'object'` with `properties` for any tool. */
1680
+ parameters: {
1681
+ type: 'object';
1682
+ properties?: Record<string, unknown>;
1683
+ required?: string[];
1684
+ };
1685
+ }
1686
+ interface LLMTool {
1687
+ functionDeclarations: LLMFunctionDeclaration[];
1688
+ }
1689
+ interface LLMGenerationConfig {
1690
+ temperature?: number;
1691
+ topP?: number;
1692
+ topK?: number;
1693
+ maxOutputTokens?: number;
1694
+ }
1695
+ interface LLMSystemInstruction {
1696
+ parts: {
1697
+ text: string;
1698
+ }[];
1699
+ }
1700
+ interface LLMToolUseRequest {
1701
+ /** Gemini model id (e.g. 'gemini-2.5-flash'). */
1702
+ model: string;
1703
+ /** Conversation so far, including any tool-result turns. */
1704
+ contents: LLMContent[];
1705
+ /** System prompt as Gemini-native systemInstruction. */
1706
+ systemInstruction?: LLMSystemInstruction;
1707
+ /** Tool declarations the model may call. */
1708
+ tools?: LLMTool[];
1709
+ /** Optional tool-call mode override. */
1710
+ toolConfig?: {
1711
+ functionCallingConfig?: {
1712
+ mode?: 'AUTO' | 'ANY' | 'NONE';
1713
+ allowedFunctionNames?: string[];
1714
+ };
1715
+ };
1716
+ generationConfig?: LLMGenerationConfig;
1717
+ }
1718
+ interface LLMUsageMetadata {
1719
+ promptTokenCount: number;
1720
+ candidatesTokenCount: number;
1721
+ totalTokenCount: number;
1722
+ }
1723
+ interface LLMCandidate {
1724
+ content: LLMContent;
1725
+ finishReason?: string;
1726
+ index?: number;
1727
+ }
1728
+ interface LLMToolUseResponse {
1729
+ candidates: LLMCandidate[];
1730
+ usageMetadata?: LLMUsageMetadata;
1731
+ }
1732
+ interface PluginPresetData {
1733
+ name: string;
1734
+ category: string;
1735
+ /** Base64-encoded plugin state — pass to setPluginState() */
1736
+ state: string;
798
1737
  }
799
1738
  /** Result of shufflePreset() — the new preset that was applied */
800
1739
  interface ShufflePresetResult {
801
1740
  presetName: string;
802
1741
  presetCategory: string;
803
1742
  }
1743
+ /**
1744
+ * One entry in a track's in-session "sound history" — the data behind the
1745
+ * TrackRow ↩ back-arrow and the drawer "History" tab (see `useSoundHistory`).
1746
+ *
1747
+ * `descriptor` is opaque to the SDK: each generator plugin defines its own shape
1748
+ * (a drum sample path string, an instrument `{ displayName, zones }`, a synth
1749
+ * `{ pluginIndex, stateBase64 }`) and is the value handed back to the plugin's
1750
+ * `applySound` callback to re-apply the sound.
1751
+ */
1752
+ interface SoundHistoryEntry {
1753
+ /** Human-readable label shown in the History list (filename, preset/instrument name). */
1754
+ label: string;
1755
+ /** Opaque, plugin-defined value used to re-apply this sound. */
1756
+ descriptor: unknown;
1757
+ /** User-starred. Favorited entries are never auto-evicted by the history cap. */
1758
+ favorite?: boolean;
1759
+ }
804
1760
  interface PluginSettingsSchema {
805
1761
  type: 'object';
806
1762
  properties: Record<string, SettingDefinition>;
@@ -826,7 +1782,7 @@ interface PluginSettingsStore {
826
1782
  /** Subscribe to settings changes. Returns unsubscribe fn. */
827
1783
  onChange(listener: (key: string, value: unknown) => void): UnsubscribeFn;
828
1784
  }
829
- type PluginErrorCode = 'NOT_OWNED' | 'TRACK_NOT_FOUND' | 'TRACK_LIMIT_EXCEEDED' | 'NO_ACTIVE_SCENE' | 'ENGINE_ERROR' | 'INVALID_MIDI' | 'FILE_NOT_FOUND' | 'INVALID_FORMAT' | 'PLUGIN_NOT_FOUND' | 'LLM_BUDGET_EXCEEDED' | 'LLM_UNAVAILABLE' | 'NOT_AUTHENTICATED' | 'TIMEOUT' | 'CANCELLED' | 'INCOMPATIBLE' | 'CAPABILITY_DENIED' | 'SECRET_NOT_FOUND' | 'VALIDATION_ERROR';
1785
+ type PluginErrorCode = 'NOT_OWNED' | 'TRACK_NOT_FOUND' | 'TRACK_LIMIT_EXCEEDED' | 'NO_ACTIVE_SCENE' | 'ENGINE_ERROR' | 'INVALID_MIDI' | 'FILE_NOT_FOUND' | 'INVALID_FORMAT' | 'PLUGIN_NOT_FOUND' | 'LLM_BUDGET_EXCEEDED' | 'LLM_UNAVAILABLE' | 'NOT_AUTHENTICATED' | 'TIMEOUT' | 'CANCELLED' | 'INCOMPATIBLE' | 'CAPABILITY_DENIED' | 'SECRET_NOT_FOUND' | 'VALIDATION_ERROR' | 'AUDIO_CAPTURE_DENIED';
830
1786
  declare class PluginError extends Error {
831
1787
  readonly code: PluginErrorCode;
832
1788
  readonly details?: Record<string, unknown>;
@@ -859,6 +1815,82 @@ interface PluginCapabilities {
859
1815
  };
860
1816
  /** Plugin needs native file dialog access */
861
1817
  fileDialog?: boolean;
1818
+ /**
1819
+ * Plugin needs microphone / line-in capture. Gates the recording host
1820
+ * methods (getAudioInputDevices, startTrackRecording, etc).
1821
+ * @since SDK 2.1.0
1822
+ */
1823
+ audioCapture?: boolean;
1824
+ }
1825
+ /**
1826
+ * Audio input device exposed by the audio engine. The `deviceId` is the
1827
+ * stable identifier returned by JUCE's AudioDeviceManager and accepted as
1828
+ * the device argument to `startTrackRecording`.
1829
+ * @since SDK 2.1.0
1830
+ */
1831
+ interface AudioInputDevice {
1832
+ /** Stable device identifier — passed back to startTrackRecording. */
1833
+ deviceId: string;
1834
+ /** Human-readable device name (e.g., "MacBook Pro Microphone", "USB Mic"). */
1835
+ label: string;
1836
+ /** True if this is the system default input device. */
1837
+ isDefault: boolean;
1838
+ /** Number of input channels the device supports (1 = mono, 2 = stereo). */
1839
+ channelCount: number;
1840
+ }
1841
+ /**
1842
+ * Engine state snapshot that an audio-recording plugin needs before
1843
+ * starting a session.
1844
+ * @since SDK 2.1.0
1845
+ */
1846
+ interface RecordingTargetInfo {
1847
+ /** Engine device sample rate, e.g. 44100 or 48000. */
1848
+ engineSampleRate: number;
1849
+ /** Active scene id, or null when no scene is selected. */
1850
+ sceneId: string | null;
1851
+ /** True when a transition render lock is held — recorder must refuse. */
1852
+ isRenderLocked: boolean;
1853
+ /** Current project BPM. */
1854
+ bpm: number;
1855
+ /** Active scene length in bars (4/4 assumed), or null when no scene. */
1856
+ bars: number | null;
1857
+ /**
1858
+ * Sample-perfect-recording compatibility (Path 2 gate). When false,
1859
+ * the recorder must refuse to start a session and surface
1860
+ * `recordingCompatibilityReason` to the user — input + output
1861
+ * devices cannot be sample-aligned.
1862
+ * @since SDK 2.4.0
1863
+ */
1864
+ canRecordSamplePerfect?: boolean;
1865
+ recordingCompatibilityReason?: string;
1866
+ }
1867
+ /**
1868
+ * Event payload fired when the engine finalizes a recording chunk WAV
1869
+ * file (either at a boundary mark or at session stop).
1870
+ * @since SDK 2.1.0
1871
+ */
1872
+ interface RecordingChunkFinalizedEvent {
1873
+ /** Absolute path to the finalized WAV file on disk. */
1874
+ filePath: string;
1875
+ /** Zero-based chunk index within the active session. */
1876
+ chunkIndex: number;
1877
+ /** Duration of this chunk in milliseconds. */
1878
+ durationMs: number;
1879
+ /** WAV sample rate. */
1880
+ sampleRate: number;
1881
+ /** WAV channel count. */
1882
+ channels: number;
1883
+ /**
1884
+ * Sample-perfect-recording metadata (Path 2). When the chunk was
1885
+ * closed via a host-time-anchored `markRecordingChunkBoundary` call,
1886
+ * carries recorder-local sample positions plus the host-time at
1887
+ * which the boundary fired. Undefined / -1 means the boundary
1888
+ * lacked a host-time anchor (legacy or stop-driven finalize).
1889
+ * @since SDK 2.4.0
1890
+ */
1891
+ recorderSampleStart?: number;
1892
+ recorderSampleEnd?: number;
1893
+ boundaryHostTimeNs?: number;
862
1894
  }
863
1895
  interface PluginFileDialogOptions {
864
1896
  title?: string;
@@ -946,12 +1978,25 @@ interface PluginAudioTextureResult {
946
1978
  * clip is written so the OffsetScrubber UI can read them later.
947
1979
  */
948
1980
  cuePoints: PluginCuePoints | null;
1981
+ /**
1982
+ * Path to the un-trimmed (raw) Lyria output. Used by the stems
1983
+ * trim editor to draw the full waveform. Persist via
1984
+ * `host.setRawAudioFilePath`. Null when no raw file is available.
1985
+ */
1986
+ rawFilePath?: string | null;
1987
+ /** Same beats as `cuePoints` in raw-file sample coordinates. */
1988
+ rawCuePoints?: PluginCuePoints | null;
1989
+ /**
1990
+ * Auto-detected start of the trim window inside the raw file (sample
1991
+ * offset). Null when detection was skipped.
1992
+ */
1993
+ inputStartSample?: number | null;
949
1994
  }
950
1995
  /**
951
1996
  * Cue-points sidecar surfaced by the audio-processor `trim` command —
952
1997
  * sample positions for each detected beat inside the generated WAV.
953
1998
  * Mirrors the canonical `CuePoints` shape from the assistant; duplicated
954
- * here so external plugins don't reach into sas-assistant internals.
1999
+ * here so external plugins don't reach into sas-app internals.
955
2000
  */
956
2001
  interface PluginCuePoints {
957
2002
  /** Schema version (currently 1). */
@@ -967,6 +2012,15 @@ interface PluginCuePoints {
967
2012
  /** ISO-8601 timestamp of when detection ran. */
968
2013
  detected_at: string;
969
2014
  }
2015
+ /**
2016
+ * A trim window inside a raw (un-trimmed) audio file. `start_sample` is
2017
+ * the offset from the start of the raw file; `duration_samples` is the
2018
+ * length of the trimmed slice. Both are in raw-file sample coordinates.
2019
+ */
2020
+ interface PluginTrimWindow {
2021
+ start_sample: number;
2022
+ duration_samples: number;
2023
+ }
970
2024
  /** Options for composing a full scene arrangement via LLM. */
971
2025
  interface ComposeSceneOptions {
972
2026
  /** The contract prompt / musical direction for the arrangement. */
@@ -1021,6 +2075,14 @@ interface PluginAppTool {
1021
2075
  inputSchema: PluginAppToolInputSchema;
1022
2076
  /** `'scene'` = safe for scene-scoped callers. `'project'` = cross-scene. */
1023
2077
  scope?: 'scene' | 'project';
2078
+ /**
2079
+ * `true` = the operation cannot be undone via the host's checkpoint/undo
2080
+ * system (project delete, disk overwrite, external export, …). The host
2081
+ * gates such calls behind a user-approval flow when invoked with agent
2082
+ * provenance; agent UIs may also surface the flag (e.g. ⚠ in a tool list).
2083
+ * @since SDK 2.18.0
2084
+ */
2085
+ irreversible?: boolean;
1024
2086
  }
1025
2087
  /** Result shape returned by `PluginHost.executeAppTool`. */
1026
2088
  interface PluginAppToolResult {
@@ -1131,6 +2193,217 @@ interface FxPresetDataEntry {
1131
2193
  /** Persisted FX data format (stored as JSON in database) */
1132
2194
  type FxPresetData = Partial<Record<FxCategory, FxPresetDataEntry>>;
1133
2195
 
2196
+ /**
2197
+ * TrackDrawer — the unified per-track drawer body.
2198
+ *
2199
+ * ONE drawer with a flat contextual tab strip. Which tabs appear is computed
2200
+ * from which callbacks the host panel provides:
2201
+ * - FX (onFxToggle) — the 6-category FX toggle bar
2202
+ * - Pick (onSelect) — instrument-plugin picker (+ native editor stage)
2203
+ * - History (onRestoreSound) — sounds this track has had (restore / favorite)
2204
+ * - Import (onImportSound) — copy a sound from a matching track in another scene
2205
+ *
2206
+ * The active tab is CONTROLLED by the host (activeTab / onTabChange) so the
2207
+ * track row's FX button and ▾ button can open the SAME drawer to a chosen tab.
2208
+ * When only one tab is enabled (e.g. loops = FX only) the strip is hidden and
2209
+ * that single view renders directly.
2210
+ *
2211
+ * (Was `InstrumentDrawer` — renamed once it grew an FX tab + Import tab. A
2212
+ * `TrackDrawer as InstrumentDrawer` alias is exported from the barrel for
2213
+ * backwards compatibility.)
2214
+ */
2215
+
2216
+ /** The contextual tabs a track drawer can show, in display order. */
2217
+ type DrawerTab = 'fx' | 'pick' | 'history' | 'import' | 'edit';
2218
+ interface TrackDrawerProps {
2219
+ /** Which tab is active (controlled by the host TrackRow). */
2220
+ activeTab: DrawerTab;
2221
+ /** Switch tabs (strip clicks). */
2222
+ onTabChange?: (tab: DrawerTab) => void;
2223
+ trackId: string;
2224
+ fxState: TrackFxDetailState;
2225
+ onFxToggle?: (category: FxCategory, enabled: boolean) => void;
2226
+ onFxPresetChange?: (category: FxCategory, presetIndex: number) => void;
2227
+ onFxDryWetChange?: (category: FxCategory, value: number) => void;
2228
+ /** Disable FX controls (e.g. while the track is generating). */
2229
+ fxDisabled?: boolean;
2230
+ /** Available instrument plugins from engine scan. */
2231
+ instruments?: InstrumentDescriptor[];
2232
+ /** Currently loaded instrument plugin ID (null = default Surge XT). */
2233
+ currentPluginId?: string | null;
2234
+ /** Whether the instrument scan is still in progress. */
2235
+ isLoading?: boolean;
2236
+ /** Called when user selects an instrument (presence enables the Pick tab). */
2237
+ onSelect?: (pluginId: string) => void;
2238
+ /** Re-scan plugins. */
2239
+ onRefresh?: () => void;
2240
+ /** Pick-tab sub-view: show the native plugin editor instead of the grid. */
2241
+ editorStage?: boolean;
2242
+ /** Called when user clicks "Open Plugin Editor". */
2243
+ onShowEditor?: () => void;
2244
+ /** Called when user goes back from the editor to the instrument grid. */
2245
+ onBackToInstruments?: () => void;
2246
+ /** Name of the selected instrument (shown in the editor header). */
2247
+ selectedInstrumentName?: string | null;
2248
+ soundHistory?: readonly SoundHistoryEntry[];
2249
+ soundHistoryCursor?: number;
2250
+ /** Restore a sound by index; presence enables the History tab. */
2251
+ onRestoreSound?: (index: number) => void;
2252
+ /** Toggle the favorite (⭐) flag on a history entry; omit to hide the star. */
2253
+ onToggleFavorite?: (index: number) => void;
2254
+ /** Open the sound-import picker; presence enables the Import tab. */
2255
+ onImportSound?: () => void;
2256
+ /** Button label, e.g. "Import Sample" (drums/instruments) or "Import Preset" (synths). */
2257
+ importSoundLabel?: string;
2258
+ /** Current MIDI notes for the piano-roll editor. */
2259
+ editNotes?: readonly PluginMidiNote[];
2260
+ /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */
2261
+ onNotesChange?: (notes: PluginMidiNote[]) => void;
2262
+ /** Scene length in bars (piano-roll grid width). Default 4. */
2263
+ editBars?: number;
2264
+ /** Scene BPM (piano-roll audition timing). Default 120. */
2265
+ editBpm?: number;
2266
+ /** Snap step in quarter notes for the piano roll (default 0.25). */
2267
+ editSnap?: number;
2268
+ /** Optional single-note preview when the user adds a note. */
2269
+ onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;
2270
+ }
2271
+ declare function TrackDrawer({ activeTab, onTabChange, trackId, fxState, onFxToggle, onFxPresetChange, onFxDryWetChange, fxDisabled, instruments, currentPluginId, isLoading, onSelect, onRefresh, editorStage, onShowEditor, onBackToInstruments, selectedInstrumentName, soundHistory, soundHistoryCursor, onRestoreSound, onToggleFavorite, onImportSound, importSoundLabel, editNotes, onNotesChange, editBars, editBpm, editSnap, onAuditionNote, }: TrackDrawerProps): React.ReactElement;
2272
+
2273
+ /**
2274
+ * useTrackLevels — drives the cosmetic per-track strip meters.
2275
+ *
2276
+ * The hard constraint for this feature is "playback ALWAYS wins over the GUI;
2277
+ * NO blocking threads." This hook is built around that:
2278
+ *
2279
+ * - It polls `host.getTrackLevels()` at ~30Hz with a recursive setTimeout that
2280
+ * only schedules the NEXT tick AFTER the previous await resolves. That is
2281
+ * automatic backpressure: a slow/stalled engine simply slows the meter, it
2282
+ * can never queue a backlog of requests. (The host + bridge also coalesce,
2283
+ * so a busy engine yields a STALE snapshot, never a pile-up.)
2284
+ * - It writes into a ref-held Map and notifies row subscribers, so the OWNING
2285
+ * panel never re-renders at 30Hz. Each row reads its own value via
2286
+ * `useTrackLevel` and re-renders only itself.
2287
+ * - It polls while the panel is mounted and the window is visible, and pauses
2288
+ * when the window is hidden. It deliberately does NOT gate on transport
2289
+ * "is playing": this app drives playback through decks / the clip launcher,
2290
+ * and the linear-transport play flag does not track that reliably. When
2291
+ * audio is stopped the engine simply returns floor levels, so the bars are
2292
+ * empty anyway — no need (and no reliable signal) to stop polling.
2293
+ *
2294
+ * Usage (panel):
2295
+ * const levels = useTrackLevels(host);
2296
+ * ...<TrackRow levels={levels} ... /> // row calls useTrackLevel(levels, id)
2297
+ */
2298
+
2299
+ /**
2300
+ * Stable handle returned by {@link useTrackLevels}. Rows read their own level
2301
+ * and subscribe to per-tick notifications through it; its identity is stable
2302
+ * across renders so a row's subscription is set up once.
2303
+ */
2304
+ interface TrackLevelsHandle {
2305
+ /** Current level for a track, or null when idle/absent (renders an empty bar). */
2306
+ getLevel(trackId: string): PluginTrackLevel | null;
2307
+ /** Subscribe to per-tick updates. Returns an unsubscribe function. */
2308
+ subscribe(listener: () => void): () => void;
2309
+ }
2310
+ /**
2311
+ * Poll every owned track's level while mounted + visible. Returns a stable
2312
+ * handle; the owning component does NOT re-render per tick. Pass `enabled =
2313
+ * false` to turn it off entirely (e.g. a panel that wants no meters). Safe to
2314
+ * call even when the host predates `getTrackLevels` (older SDK) — it stays idle.
2315
+ */
2316
+ declare function useTrackLevels(host: PluginHost | null | undefined, enabled?: boolean): TrackLevelsHandle;
2317
+ /**
2318
+ * Per-row selector. Subscribes to the shared scheduler and re-renders ONLY the
2319
+ * calling component when this track's level changes. Returns null when idle
2320
+ * (transport stopped, window hidden, or the track has no meter yet).
2321
+ */
2322
+ declare function useTrackLevel(handle: TrackLevelsHandle | null | undefined, trackId: string): PluginTrackLevel | null;
2323
+ /**
2324
+ * Per-row meter view-model: the current level plus a held peak for the meter UI.
2325
+ */
2326
+ interface TrackMeterView {
2327
+ /** Current mono peak in dBFS (floored at -120). */
2328
+ peakDb: number;
2329
+ /** Held peak in dBFS — stays at the recent maximum for ~PEAK_HOLD_MS, then falls. */
2330
+ peakHoldDb: number;
2331
+ /** Latched clip flag for the last poll window. */
2332
+ clipped: boolean;
2333
+ /** True when the track currently has a live meter row. */
2334
+ active: boolean;
2335
+ }
2336
+ /**
2337
+ * Per-row meter selector WITH PEAK-HOLD. Like {@link useTrackLevel} it subscribes
2338
+ * to the shared ~30Hz scheduler and re-renders only the calling component, but it
2339
+ * also tracks a held peak that stays at the recent maximum for ~PEAK_HOLD_MS then
2340
+ * decays — so the eye can register where the signal peaked while the bar itself
2341
+ * moves fast. No extra timers or rAF: the held value is recomputed on each
2342
+ * scheduler notify, using performance.now() for hold/decay timing.
2343
+ */
2344
+ declare function useTrackMeter(handle: TrackLevelsHandle | null | undefined, trackId: string): TrackMeterView;
2345
+ /**
2346
+ * Track the transport's play/stop state for a plugin. Seeds from
2347
+ * `getTransportState()` and follows `onTransportEvent`. Use its result as the
2348
+ * `active` arg to {@link useTrackLevels} so meters animate only during playback.
2349
+ */
2350
+ declare function useTransportPlaying(host: PluginHost | null | undefined): boolean;
2351
+
2352
+ /**
2353
+ * Props the reorder machinery hands to a single row. Spread `handleProps` on the
2354
+ * drag grip and `rowProps` on the row's outer element; `isDragging` /
2355
+ * `isDragTarget` drive the visual state.
2356
+ */
2357
+ interface TrackRowDragProps {
2358
+ handleProps: {
2359
+ draggable: true;
2360
+ onDragStart: (e: DragEvent<HTMLElement>) => void;
2361
+ onDragEnd: (e: DragEvent<HTMLElement>) => void;
2362
+ };
2363
+ rowProps: {
2364
+ onDragEnter: (e: DragEvent<HTMLElement>) => void;
2365
+ onDragOver: (e: DragEvent<HTMLElement>) => void;
2366
+ onDragLeave: (e: DragEvent<HTMLElement>) => void;
2367
+ onDrop: (e: DragEvent<HTMLElement>) => void;
2368
+ };
2369
+ /** This row is the one currently being dragged (dim it). */
2370
+ isDragging: boolean;
2371
+ /** This row is the current drop target (show an insertion accent). */
2372
+ isDragTarget: boolean;
2373
+ }
2374
+ /**
2375
+ * Pure helper: return a NEW array with the item at `from` moved to `to`.
2376
+ * Out-of-range or no-op moves return a shallow copy unchanged. Exported for
2377
+ * unit testing the index math without a DOM.
2378
+ */
2379
+ declare function moveItem<T>(arr: readonly T[], from: number, to: number): T[];
2380
+ interface UseTrackReorderOptions<T> {
2381
+ /** Host (only {@link PluginHost.reorderTracks} is used). */
2382
+ host: Pick<PluginHost, 'reorderTracks'>;
2383
+ /** The panel's current track array (also the render order). */
2384
+ items: T[];
2385
+ /** The panel's state setter for `items` (used for optimistic update + revert). */
2386
+ setItems: Dispatch<SetStateAction<T[]>>;
2387
+ /** Stable id for persistence — use the track's dbId, not its engine id. */
2388
+ getId: (item: T) => string;
2389
+ /** Called if persistence fails, after the optimistic update is reverted. */
2390
+ onError?: (err: unknown) => void;
2391
+ }
2392
+ interface UseTrackReorderResult {
2393
+ /** Build the drag props for the row at `index`; spread onto its TrackRow. */
2394
+ dragPropsFor: (index: number) => TrackRowDragProps;
2395
+ /** Index of the row being dragged, or null. */
2396
+ draggingIndex: number | null;
2397
+ /** Index of the current drop-target row, or null. */
2398
+ dragOverIndex: number | null;
2399
+ }
2400
+ /**
2401
+ * Drag-and-drop reordering for a panel's track list. Dropping a row onto another
2402
+ * row moves it into that row's position (everything between shifts); the top and
2403
+ * bottom are reachable by dropping on the first/last row.
2404
+ */
2405
+ declare function useTrackReorder<T>({ host, items, setItems, getId, onError, }: UseTrackReorderOptions<T>): UseTrackReorderResult;
2406
+
1134
2407
  /**
1135
2408
  * SDK TrackRow — Reusable track row component for generator plugins.
1136
2409
  *
@@ -1159,10 +2432,19 @@ interface SDKTrackRowProps {
1159
2432
  volume: number;
1160
2433
  pan: number;
1161
2434
  };
2435
+ /** True when ANOTHER track is soloed, so this (non-soloed) track is currently
2436
+ * silenced. Renders the row dimmed while leaving its Mute button UNLIT — the
2437
+ * engine's effective-mute model silences it without touching user-mute. Purely
2438
+ * visual; does not change mute/solo state. */
2439
+ soloedOut?: boolean;
1162
2440
  /** FX category states */
1163
2441
  fxDetailState: TrackFxDetailState;
1164
- /** FX panel visibility */
1165
- fxDrawerOpen: boolean;
2442
+ /** Whether the unified track drawer is open. */
2443
+ drawerOpen: boolean;
2444
+ /** Which tab the drawer is showing. */
2445
+ drawerTab: DrawerTab;
2446
+ /** Switch the active drawer tab (tab-strip clicks). Omit for single-tab panels (e.g. loops = FX only). */
2447
+ onTabChange?: (tab: DrawerTab) => void;
1166
2448
  /** Generation in progress */
1167
2449
  isGenerating?: boolean;
1168
2450
  /** Auth state */
@@ -1183,8 +2465,9 @@ interface SDKTrackRowProps {
1183
2465
  onShuffle?: () => void;
1184
2466
  /** Duplicate track (optional — omit to hide Copy button) */
1185
2467
  onCopy?: () => void;
1186
- /** Delete track */
1187
- onDelete: () => void;
2468
+ /** Delete track. Optional — omit to hide the delete button (e.g. a composite
2469
+ * like CrossfadeTrackRow owns a single delete for the whole pair). */
2470
+ onDelete?: () => void;
1188
2471
  /** Custom content replacing the prompt input (e.g., sample info display) */
1189
2472
  contentSlot?: React.ReactNode;
1190
2473
  /** Toggle mute */
@@ -1211,10 +2494,8 @@ interface SDKTrackRowProps {
1211
2494
  instrumentName?: string | null;
1212
2495
  /** Whether the current instrument plugin is missing from the system */
1213
2496
  instrumentMissing?: boolean;
1214
- /** Whether the instrument drawer is open */
1215
- instrumentDrawerOpen?: boolean;
1216
- /** Toggle the instrument drawer */
1217
- onToggleInstrumentDrawer?: () => void;
2497
+ /** Open/close the drawer to a non-FX tab (the ▾ button). Omit to hide it. */
2498
+ onToggleDrawer?: () => void;
1218
2499
  /** Available instrument plugins for the drawer */
1219
2500
  availableInstruments?: InstrumentDescriptor[];
1220
2501
  /** Currently loaded instrument plugin ID */
@@ -1225,43 +2506,516 @@ interface SDKTrackRowProps {
1225
2506
  instrumentsLoading?: boolean;
1226
2507
  /** Re-scan for instruments */
1227
2508
  onRefreshInstruments?: () => void;
1228
- /** Which stage the instrument drawer is in */
1229
- instrumentDrawerStage?: 'instruments' | 'editor';
2509
+ /** Pick-tab sub-view: native plugin editor instead of the instrument grid. */
2510
+ editorStage?: boolean;
1230
2511
  /** Called when user clicks "Open Editor" */
1231
2512
  onShowEditor?: () => void;
1232
2513
  /** Called when user wants to go back from editor view */
1233
2514
  onBackToInstruments?: () => void;
2515
+ /** Ordered list of sounds this track has had this session. */
2516
+ soundHistory?: readonly SoundHistoryEntry[];
2517
+ /** Index into soundHistory of the currently-applied sound. */
2518
+ soundHistoryCursor?: number;
2519
+ /** Restore a sound from the History tab by index. */
2520
+ onRestoreSound?: (index: number) => void;
2521
+ /** Toggle the favorite (⭐) flag on a history entry. */
2522
+ onToggleFavorite?: (index: number) => void;
2523
+ /** Open the drawer's sound-import picker; omit to hide the button. */
2524
+ onImportSound?: () => void;
2525
+ /** Sound-import button label ("Import Sample" / "Import Preset"). */
2526
+ importSoundLabel?: string;
2527
+ /** Current MIDI notes for the piano-roll editor (the 'edit' tab). */
2528
+ editNotes?: readonly PluginMidiNote[];
2529
+ /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */
2530
+ onNotesChange?: (notes: PluginMidiNote[]) => void;
2531
+ /** Scene length in bars (piano-roll grid width). */
2532
+ editBars?: number;
2533
+ /** Scene BPM (piano-roll audition timing). */
2534
+ editBpm?: number;
2535
+ /** Snap step in quarter notes for the piano roll (default 0.25). */
2536
+ editSnap?: number;
2537
+ /** Optional single-note preview when the user adds a note. */
2538
+ onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;
2539
+ /** Drag props from {@link useTrackReorder}. When present, renders the grip
2540
+ * handle and makes the row a drop target. Omit for non-reorderable lists. */
2541
+ drag?: TrackRowDragProps;
2542
+ /** Shared meter handle from `useTrackLevels(host, isPlaying)`. When present,
2543
+ * a thin peak meter welds to the bottom of the row. Omit to hide it. */
2544
+ levels?: TrackLevelsHandle;
2545
+ }
2546
+ declare function TrackRow({ track, prompt, runtimeState, soloedOut, fxDetailState, drawerOpen, drawerTab, onTabChange, isGenerating, isAuthenticated, error, hasMidi, generationProgress, estimatedGenerationMs, onPromptChange, onGenerate, onShuffle, onCopy, onDelete, contentSlot, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onFxToggle, onFxPresetChange, onFxDryWetChange, onToggleFxDrawer, onProgressChange, accentColor, instrumentName, instrumentMissing, onToggleDrawer, availableInstruments, currentInstrumentPluginId, onInstrumentSelect, instrumentsLoading, onRefreshInstruments, editorStage, onShowEditor, onBackToInstruments, soundHistory, soundHistoryCursor, onRestoreSound, onToggleFavorite, onImportSound, importSoundLabel, editNotes, onNotesChange, editBars, editBpm, editSnap, onAuditionNote, drag, levels, }: SDKTrackRowProps): React.ReactElement;
2547
+
2548
+ /**
2549
+ * Crossfade-pair metadata — family-agnostic types + parsing shared by every
2550
+ * generator panel that supports transition crossfades (synth / drum / instrument).
2551
+ *
2552
+ * A crossfade pair is two normal tracks linked by a shared `groupId`, persisted
2553
+ * in scene plugin_data under `track:<dbId>:crossfade`. Both members play the
2554
+ * same MIDI; one wears the origin preset, the other the target preset. The panel
2555
+ * owns the family-specific create flow (how a preset/sample is copied) and the
2556
+ * render; this module owns only the shape + the scene-data → pairs parse so the
2557
+ * logic can't drift across the three panels.
2558
+ *
2559
+ * @since SDK 2.23.0
2560
+ */
2561
+ /** Which half of the pair a per-layer control / member targets. */
2562
+ type CrossfadeSlot = 'origin' | 'target';
2563
+ /**
2564
+ * Equal-power center gain (~-3 dB, 1/√2) applied to BOTH crossfade layers so a
2565
+ * centered, non-functional slider already sounds like a midpoint blend. The
2566
+ * per-layer volume sliders start here; a later phase's fader drives them.
2567
+ */
2568
+ declare const EQUAL_POWER_GAIN = 0.707;
2569
+ /**
2570
+ * Per-member crossfade metadata (one scene-data value per member track). The two
2571
+ * members (origin/target) of a pair share a `groupId`.
2572
+ */
2573
+ interface CrossfadeMeta {
2574
+ groupId: string;
2575
+ slot: CrossfadeSlot;
2576
+ /** DB id of the partner member track. */
2577
+ partnerDbId: string;
2578
+ /** DB id of the SOURCE track this layer's preset/sample was copied from. */
2579
+ sourceTrackDbId: string;
2580
+ /** DB id of the scene the source track lives in (the from/to scene). */
2581
+ sourceSceneId: string;
2582
+ /** Source track display name (shown in the caption). */
2583
+ sourceName: string;
2584
+ /** Copied preset/sample label (shown in the caption). */
2585
+ soundLabel: string;
2586
+ /** Crossfade position 0..1 (kept identical on both members). */
2587
+ sliderPos: number;
2588
+ }
2589
+ /** A complete crossfade pair (both members present), keyed by groupId. */
2590
+ interface CrossfadePairMeta {
2591
+ groupId: string;
2592
+ sliderPos: number;
2593
+ originDbId: string;
2594
+ targetDbId: string;
2595
+ originSourceName: string;
2596
+ originSoundLabel: string;
2597
+ targetSourceName: string;
2598
+ targetSoundLabel: string;
2599
+ }
2600
+ /** Narrow an unknown scene-data value to CrossfadeMeta (defensive — survives partial blobs). */
2601
+ declare function asCrossfadeMeta(val: unknown): CrossfadeMeta | null;
2602
+ /**
2603
+ * Scan all `track:<dbId>:crossfade` keys in a scene's plugin_data and assemble
2604
+ * COMPLETE pairs (both origin + target present). A half-broken group (partner
2605
+ * deleted underneath) is omitted, so its surviving member falls back to a normal
2606
+ * row instead of vanishing.
2607
+ */
2608
+ declare function parseCrossfadePairs(sceneData: Record<string, unknown>): CrossfadePairMeta[];
2609
+
2610
+ /**
2611
+ * CrossfadeTrackRow — a transition "crossfade track": two stacked TrackRows
2612
+ * (origin on top, target on bottom) joined by a horizontal crossfade slider.
2613
+ *
2614
+ * Both layers play the SAME generated MIDI; the top wears the ORIGIN scene
2615
+ * track's preset and the bottom wears the TARGET scene track's preset. The user
2616
+ * cannot regenerate, shuffle, or change the preset/sample on either layer —
2617
+ * those controls are simply not wired into the inner TrackRows (the SDK
2618
+ * TrackRow is "controlled by omission"). What remains: per-layer volume/pan,
2619
+ * GROUP mute/solo (both layers toggle together), and a single delete that
2620
+ * removes the whole pair.
2621
+ *
2622
+ * The slider represents WHERE the crossfade happens. In this phase it is
2623
+ * centered and non-functional (omit `onSliderChange` → it renders disabled); a
2624
+ * later phase wires it to fade origin→target across the bars.
2625
+ *
2626
+ * @since SDK 2.22.0
2627
+ */
2628
+
2629
+ /** One layer (engine track) of a crossfade pair. */
2630
+ interface CrossfadeLayer {
2631
+ /** Engine track id of this layer's track (also the meter key). */
2632
+ trackId: string;
2633
+ /** Display name of this layer's (newly created) track. */
2634
+ name: string;
2635
+ /** Musical role (same for both layers — crossfades are same-role). */
2636
+ role?: string;
2637
+ /** Name of the SOURCE track this layer was cloned from (origin/target scene). */
2638
+ sourceName?: string;
2639
+ /** Human label of the copied preset/sound, shown in the caption. */
2640
+ soundLabel?: string;
2641
+ /** Playback state for this layer. */
2642
+ runtimeState: {
2643
+ muted: boolean;
2644
+ solo: boolean;
2645
+ volume: number;
2646
+ pan: number;
2647
+ };
2648
+ }
2649
+ interface CrossfadeTrackRowProps {
2650
+ /** Top layer — wears the origin (from) scene track's preset. */
2651
+ origin: CrossfadeLayer;
2652
+ /** Bottom layer — wears the target (to) scene track's preset. */
2653
+ target: CrossfadeLayer;
2654
+ /** Crossfade position 0..1 (0 = all origin, 1 = all target). Defaults centered. */
2655
+ sliderPos?: number;
2656
+ /** Toggle mute on BOTH layers together (group mute). */
2657
+ onMuteToggle: () => void;
2658
+ /** Toggle solo on BOTH layers together (group solo). */
2659
+ onSoloToggle: () => void;
2660
+ /** Change one layer's volume (per-layer). */
2661
+ onVolumeChange: (slot: CrossfadeSlot, volume: number) => void;
2662
+ /** Change one layer's pan (per-layer). */
2663
+ onPanChange: (slot: CrossfadeSlot, pan: number) => void;
2664
+ /** Delete the whole pair. */
2665
+ onDelete: () => void;
2666
+ /** Move the crossfade point. Omit to render the slider read-only (phase 1). */
2667
+ onSliderChange?: (pos: number) => void;
2668
+ /** Shared meter handle (welds a peak meter to each layer). */
2669
+ levels?: TrackLevelsHandle;
2670
+ /** Left-border accent. Defaults to transition purple. */
2671
+ accentColor?: string;
1234
2672
  }
1235
- declare function TrackRow({ track, prompt, runtimeState, fxDetailState, fxDrawerOpen, isGenerating, isAuthenticated, error, hasMidi, generationProgress, estimatedGenerationMs, onPromptChange, onGenerate, onShuffle, onCopy, onDelete, contentSlot, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onFxToggle, onFxPresetChange, onFxDryWetChange, onToggleFxDrawer, onProgressChange, accentColor, instrumentName, instrumentMissing, instrumentDrawerOpen, onToggleInstrumentDrawer, availableInstruments, currentInstrumentPluginId, onInstrumentSelect, instrumentsLoading, onRefreshInstruments, instrumentDrawerStage, onShowEditor, onBackToInstruments, }: SDKTrackRowProps): React.ReactElement;
2673
+ declare function CrossfadeTrackRow({ origin, target, sliderPos, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onDelete, onSliderChange, levels, accentColor, }: CrossfadeTrackRowProps): React.ReactElement;
1236
2674
 
1237
2675
  /**
1238
- * InstrumentDrawerTwo-stage nested menu for instrument selection + editor access.
2676
+ * Crossfade MIDI inpainting builds the LLM user-prompt for a bridge that
2677
+ * MORPHS the ORIGIN part into the TARGET part.
2678
+ *
2679
+ * A normal scene generation composes a part standalone from the scene's chords.
2680
+ * A crossfade bridge is different: it is INPAINTING between two fixed endpoints.
2681
+ * The generated part must begin feeling continuous with the origin pattern and
2682
+ * end feeling continuous with the target pattern, transforming between them
2683
+ * across the transition's bars.
2684
+ *
2685
+ * The harmonic frame — Key / mode / BPM / bars / the transition chord
2686
+ * progression (with beat timing) / scene contract — is injected AUTOMATICALLY by
2687
+ * `host.generateWithLLM` (it prepends the active scene's "Musical Context" block
2688
+ * unless `skipContextPrefix` is set). So this prompt does NOT restate key/bpm/
2689
+ * chords — it adds only the two endpoint patterns + the morph instructions, and
2690
+ * references the harmonic frame as "given above".
2691
+ *
2692
+ * REPRESENTATION (researched for Gemini): ABC notation is the LLM-native format
2693
+ * for melodic generation, but it's weak for percussion, would need a separate
2694
+ * output parser (our output is JSON note-events, already proven with Gemini),
2695
+ * and an inpainting task wants input/output FORMAT SYMMETRY. So each endpoint is
2696
+ * given as the exact JSON note-events PLUS a pitch-named, bar-structured "gloss"
2697
+ * — the transferable wins from the research (pitch NAMES over raw MIDI numbers,
2698
+ * explicit bar/beat structure) layered on the precise, symmetric JSON. Drums
2699
+ * (uniform pitch) get a rhythmic gloss instead of pitch names.
2700
+ *
2701
+ * This changes only the LLM INPUT framing: the OUTPUT schema is unchanged, so the
2702
+ * calling panel keeps its system prompt + parser (and, for drums, its flatten step).
1239
2703
  *
1240
- * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.
1241
- * Stage 2 (editor): Shows "Open Editor" button for the selected plugin's native GUI.
2704
+ * @since SDK 2.24.0
1242
2705
  */
1243
2706
 
1244
- interface InstrumentDrawerProps {
1245
- /** Available instrument plugins from engine scan */
1246
- instruments: InstrumentDescriptor[];
1247
- /** Currently loaded instrument plugin ID (null = default Surge XT) */
1248
- currentPluginId: string | null;
1249
- /** Whether the scan is still in progress */
1250
- isLoading: boolean;
1251
- /** Called when user selects an instrument */
1252
- onSelect: (pluginId: string) => void;
1253
- /** Called when user clicks refresh to re-scan plugins */
1254
- onRefresh: () => void;
1255
- /** Which stage the drawer is in */
1256
- stage?: 'instruments' | 'editor';
1257
- /** Called when user clicks "Open Editor" */
1258
- onShowEditor?: () => void;
1259
- /** Called when user wants to go back from editor view to instrument list */
1260
- onBackToInstruments?: () => void;
1261
- /** Name of the selected instrument (for display in editor header) */
1262
- selectedInstrumentName?: string | null;
2707
+ interface CrossfadeInpaintInput {
2708
+ /** Musical role of the bridge part (e.g. 'bass'). '' falls back to "melodic". */
2709
+ role: string;
2710
+ /** Transition length in bars (the morph timeline). */
2711
+ bars: number;
2712
+ /** Display name of the ORIGIN source track (the part the bridge begins from). */
2713
+ originName: string;
2714
+ /** Display name of the TARGET source track (the part the bridge arrives at). */
2715
+ targetName: string;
2716
+ /** ORIGIN source scene's key label (e.g. "G minor"). Null/omitted = unknown. */
2717
+ originKey?: string | null;
2718
+ /** TARGET source scene's key label. Null/omitted = unknown. */
2719
+ targetKey?: string | null;
2720
+ /** ORIGIN pattern notes (beat-based; from the FROM scene). May be empty. */
2721
+ originNotes: readonly PluginMidiNote[];
2722
+ /** TARGET pattern notes (beat-based; from the TO scene). May be empty. */
2723
+ targetNotes: readonly PluginMidiNote[];
2724
+ /** Drums: pitch is uniform (flattened), so gloss RHYTHM instead of pitch names. */
2725
+ percussive?: boolean;
2726
+ }
2727
+ /**
2728
+ * Build the inpainting user-prompt. The result is the prompt BODY only — pass it
2729
+ * as `request.user` to `host.generateWithLLM` with the panel's normal system
2730
+ * prompt and `responseFormat: 'json'`; the harmonic context auto-prefixes.
2731
+ */
2732
+ declare function buildCrossfadeInpaintPrompt(input: CrossfadeInpaintInput): string;
2733
+
2734
+ /**
2735
+ * ImportTrackModal — "import a track from another scene" picker (SDK component).
2736
+ *
2737
+ * Shared by all five generator panels (drums / instruments / synths / loops /
2738
+ * stems). Self-fetching: given the scoped `host`, it calls
2739
+ * `host.listImportableTracks()` to enumerate candidates (already filtered to
2740
+ * the calling panel's type and gate-annotated by the host) and
2741
+ * `host.importTrack()` to perform the copy. The UI only renders `importable` +
2742
+ * `disabledReason` — it never computes the harmonic/length/tempo gate itself.
2743
+ *
2744
+ * Two-step picker: choose a source scene, then a track in it. Incompatible
2745
+ * tracks render disabled with a reason tooltip (never hidden), per product
2746
+ * decision.
2747
+ *
2748
+ * @since SDK 2.13.0
2749
+ */
2750
+
2751
+ interface ImportTrackModalProps {
2752
+ /** Scoped host — the modal calls listImportableTracks / importTrack itself. */
2753
+ host: PluginHost;
2754
+ /** Controls visibility (the panel owns open/closed from its header button). */
2755
+ open: boolean;
2756
+ /** Close handler (Escape, backdrop, Cancel, or after a successful import). */
2757
+ onClose: () => void;
2758
+ /** Fired after a successful import with the new track handle. */
2759
+ onImported: (handle: PluginTrackHandle) => void;
2760
+ /** Optional modal title (default names the whole-track import). */
2761
+ title?: string;
2762
+ /** data-testid prefix so each panel's modal is addressable in tests. */
2763
+ testIdPrefix?: string;
2764
+ /**
2765
+ * 'track' (default) imports a whole track via `importTrack`. 'sound' copies
2766
+ * ONLY the sound onto an existing track: every candidate is selectable (the
2767
+ * contract gate is ignored) and the chosen track is handed back via `onPick`
2768
+ * instead of being imported — the panel applies it via `host.getTrackSound`.
2769
+ */
2770
+ mode?: 'track' | 'sound';
2771
+ /** Sound-mode pick handler — required when `mode='sound'`. */
2772
+ onPick?: (sel: {
2773
+ sourceTrackDbId: string;
2774
+ trackName: string;
2775
+ sceneName: string;
2776
+ }) => void | Promise<void>;
2777
+ /**
2778
+ * Cross-panel port handler (track mode). When provided, the modal also lists
2779
+ * the ACTIVE scene's tracks owned by OTHER panels as a `sameScene` group —
2780
+ * shown first and selected by default — and routes a pick there to this
2781
+ * callback instead of `importTrack`. The panel re-sounds the part on its own
2782
+ * instrument (create track → copy MIDI → load native sound). @since SDK 2.20.0
2783
+ */
2784
+ onPortTrack?: (sel: {
2785
+ sourceTrackDbId: string;
2786
+ trackName: string;
2787
+ role?: string;
2788
+ }) => void | Promise<void>;
2789
+ }
2790
+ declare function ImportTrackModal({ host, open, onClose, onImported, title, testIdPrefix, mode, onPick, onPortTrack, }: ImportTrackModalProps): React.ReactElement | null;
2791
+
2792
+ /**
2793
+ * CrossfadeModal — "add a crossfade track" picker for a transition scene.
2794
+ *
2795
+ * Shown only inside a `scene_type='transition'` scene. The user picks an ORIGIN
2796
+ * track (from the transition's FROM scene) and a TARGET track (from its TO
2797
+ * scene). Crossfades are same-role: once an origin is chosen, the target
2798
+ * dropdown is filtered to the origin's role.
2799
+ *
2800
+ * Self-fetching: given the scoped `host`, it calls `host.listSceneFamilyTracks`
2801
+ * for both scenes (ungated — a transition deliberately bridges different keys).
2802
+ * It does NOT build the pair itself; it hands the two selections to `onCreate`,
2803
+ * which the panel implements (create two tracks, generate one shared MIDI clip,
2804
+ * copy each preset). `onCreate` should reject on failure so the modal can show
2805
+ * it and stay open.
2806
+ *
2807
+ * @since SDK 2.22.0
2808
+ */
2809
+
2810
+ /** A picked source track handed to `onCreate`. */
2811
+ interface CrossfadeSelection {
2812
+ /** Source track DB id (selector for getTrackSound + crossfade metadata). */
2813
+ dbId: string;
2814
+ /** Display name (for the row caption). */
2815
+ name: string;
2816
+ /** Musical role (same for both — enforced by the picker). */
2817
+ role?: string;
1263
2818
  }
1264
- declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, stage, onShowEditor, onBackToInstruments, selectedInstrumentName, }: InstrumentDrawerProps): React.ReactElement;
2819
+ interface CrossfadeModalProps {
2820
+ /** Scoped host — the modal calls listSceneFamilyTracks itself. */
2821
+ host: PluginHost;
2822
+ /** Controls visibility (the panel owns open/closed from its header button). */
2823
+ open: boolean;
2824
+ /** DB id of the transition's FROM (origin) scene. */
2825
+ fromSceneId: string;
2826
+ /** DB id of the transition's TO (target) scene. */
2827
+ toSceneId: string;
2828
+ /** Display name for the origin scene heading (optional). */
2829
+ fromSceneName?: string;
2830
+ /** Display name for the target scene heading (optional). */
2831
+ toSceneName?: string;
2832
+ /** Close handler (Escape, backdrop, Cancel, or after a successful create). */
2833
+ onClose: () => void;
2834
+ /** Build the crossfade pair. Should reject on failure so the modal shows it. */
2835
+ onCreate: (origin: CrossfadeSelection, target: CrossfadeSelection) => Promise<void>;
2836
+ /** data-testid prefix. */
2837
+ testIdPrefix?: string;
2838
+ }
2839
+ declare function CrossfadeModal({ host, open, fromSceneId, toSceneId, fromSceneName, toSceneName, onClose, onCreate, testIdPrefix, }: CrossfadeModalProps): React.ReactElement | null;
2840
+
2841
+ /**
2842
+ * ConfirmDialog — styled in-app confirmation modal (SDK component).
2843
+ *
2844
+ * A small, reusable "are you sure?" dialog matching the app's dark theme
2845
+ * (mirrors ImportTrackModal chrome: sas-panel / sas-border / shadow-xl). It
2846
+ * guards destructive actions; the first consumer is track deletion, which was
2847
+ * one stray click away from losing a track's MIDI + sound.
2848
+ *
2849
+ * Controlled component — the caller owns `open` and the confirm/cancel
2850
+ * handlers. Escape and a backdrop click both cancel, and the Cancel button is
2851
+ * auto-focused on open so a reflexive Enter dismisses rather than deletes.
2852
+ *
2853
+ * @since SDK 2.17.0
2854
+ */
2855
+
2856
+ interface ConfirmDialogProps {
2857
+ /** Controls visibility (the caller owns open/closed). */
2858
+ open: boolean;
2859
+ /** Bold heading line. */
2860
+ title: string;
2861
+ /** Body copy — a string or richer node. */
2862
+ message: React.ReactNode;
2863
+ /** Confirm button label (default "Delete"). */
2864
+ confirmLabel?: string;
2865
+ /** Cancel button label (default "Cancel"). */
2866
+ cancelLabel?: string;
2867
+ /** When true (default), the confirm button reads as a destructive (red) action. */
2868
+ destructive?: boolean;
2869
+ /** Fired when the user confirms. */
2870
+ onConfirm: () => void;
2871
+ /** Fired on Cancel, Escape, or backdrop click. */
2872
+ onCancel: () => void;
2873
+ /** data-testid prefix so each dialog is addressable in tests. */
2874
+ testIdPrefix?: string;
2875
+ }
2876
+ declare function ConfirmDialog({ open, title, message, confirmLabel, cancelLabel, destructive, onConfirm, onCancel, testIdPrefix, }: ConfirmDialogProps): React.ReactElement | null;
2877
+
2878
+ /**
2879
+ * Modal — the SDK's one modal-stacking primitive (portal + z-tier + backdrop).
2880
+ *
2881
+ * Every SDK modal renders INSIDE a plugin's accordion section, whose animated
2882
+ * `overflow-hidden` + `transition-all` wrapper establishes a stacking context.
2883
+ * An inline `position: fixed` overlay is therefore scoped to that section and
2884
+ * can be painted UNDER a neighbouring panel (the "import modal invisible on a
2885
+ * later open" bug). This component solves that once: it portals the overlay to
2886
+ * <body> — out of every panel's stacking context — at a z-tier above all the
2887
+ * app's `z-50` dropdowns/banners but below the toast tier (`z-[9999]`), so
2888
+ * toasts still float over modals.
2889
+ *
2890
+ * Controlled: the caller owns `open` and `onClose`. The caller renders its own
2891
+ * dialog box as `children` (keep the box's `onClick={e => e.stopPropagation()}`
2892
+ * so inside-clicks don't dismiss). Escape and a backdrop click both close.
2893
+ *
2894
+ * @since SDK 2.21.0
2895
+ */
2896
+
2897
+ interface ModalProps {
2898
+ /** Controls visibility (the caller owns open/closed). */
2899
+ open: boolean;
2900
+ /** Close handler — fired on Escape and backdrop click. */
2901
+ onClose: () => void;
2902
+ /** The dialog box. Give it `onClick={e => e.stopPropagation()}`. */
2903
+ children: React.ReactNode;
2904
+ /** data-testid prefix; the backdrop is `${testIdPrefix}-overlay`. */
2905
+ testIdPrefix?: string;
2906
+ /** Close when the backdrop is clicked (default true). */
2907
+ closeOnBackdrop?: boolean;
2908
+ /** Close on Escape (default true). */
2909
+ closeOnEscape?: boolean;
2910
+ /** Focused when the modal opens (e.g. a Cancel button) so a reflexive Enter is safe. */
2911
+ initialFocusRef?: React.RefObject<HTMLElement>;
2912
+ }
2913
+ declare function Modal({ open, onClose, children, testIdPrefix, closeOnBackdrop, closeOnEscape, initialFocusRef, }: ModalProps): React.ReactElement | null;
2914
+
2915
+ /**
2916
+ * PianoRollEditor — a compact, DOM-based MIDI note editor for the track drawer.
2917
+ *
2918
+ * Controlled: `notes` in, `onChange(next)` out. Notes render as absolutely-
2919
+ * positioned divs over a beat/pitch grid (DOM, not canvas — so it themes with
2920
+ * sas-* tokens and is fully driveable by React Testing Library). Supports:
2921
+ * - add : click an empty grid cell
2922
+ * - delete : click an existing note (no drag)
2923
+ * - move : drag a note's body (snap-quantised)
2924
+ * - resize : drag a note's right-edge handle (snap-quantised, ≥ one step)
2925
+ * - octave : shift the whole clip ±12 (toolbar) — no velocity lane
2926
+ * / marquee yet.
2927
+ * On load the viewport auto-scrolls to vertically center the note cluster, so a
2928
+ * low melody isn't stranded off-screen at the bottom of the pitch range.
2929
+ *
2930
+ * Coordinate spaces:
2931
+ * pitch (0-127) ── row = hi - pitch ── top px = row * ROW_HEIGHT
2932
+ * beat (¼ notes) ─────────────────────── left px = beat * PX_PER_BEAT
2933
+ *
2934
+ * The pure helpers (`cellToPx` / `pxToCell` / `transposeNotes`) and layout
2935
+ * constants are exported so coordinate math can be unit-tested without a DOM.
2936
+ */
2937
+
2938
+ /** Horizontal pixels per quarter-note beat. */
2939
+ declare const PX_PER_BEAT = 24;
2940
+ /** Vertical pixels per semitone row. */
2941
+ declare const ROW_HEIGHT = 12;
2942
+ /** Left keyboard-gutter width (px). */
2943
+ declare const GUTTER_W = 28;
2944
+ /** Pointer travel (px) before a press on a note becomes a drag instead of a click. */
2945
+ declare const DRAG_DEAD_ZONE = 4;
2946
+ /** Width (px) of the right-edge grab handle that resizes a note's length. */
2947
+ declare const RESIZE_HANDLE_PX = 6;
2948
+ /** MIDI pitch → scientific note name (60 = C4). */
2949
+ declare function pitchToName(pitch: number): string;
2950
+ /**
2951
+ * Cell (pitch, startBeat) → top-left pixel offset within the grid.
2952
+ * `hi` is the highest (top) visible pitch.
2953
+ */
2954
+ declare function cellToPx(pitch: number, startBeat: number, hi: number): {
2955
+ left: number;
2956
+ top: number;
2957
+ };
2958
+ /**
2959
+ * Grid-local pixel → snapped cell. `hi` is the highest visible pitch; the beat
2960
+ * snaps to the nearest `snap` step and clamps to `[0, totalBeats - snap]`;
2961
+ * pitch clamps to `[0, 127]`.
2962
+ */
2963
+ declare function pxToCell(localX: number, localY: number, hi: number, snap: number, bars: number, beatsPerBar: number): {
2964
+ pitch: number;
2965
+ startBeat: number;
2966
+ };
2967
+ /**
2968
+ * New `durationBeats` for a note whose right edge is dragged to grid-local pixel
2969
+ * `localX`. The end snaps to the nearest `snap` step, is clamped to at least one
2970
+ * step past `startBeat`, and never extends beyond the grid's right edge
2971
+ * (`bars * beatsPerBar`). `startBeat` and `pitch` are untouched.
2972
+ */
2973
+ declare function resizeNoteDuration(startBeat: number, localX: number, snap: number, bars: number, beatsPerBar: number): number;
2974
+ /**
2975
+ * `scrollTop` that vertically centers the bulk of the notes in a `viewportH`-px
2976
+ * window. Targets the MEDIAN pitch (robust to a stray high/low outlier — keeps
2977
+ * "where the majority of notes are" framed) and clamps to the valid scroll
2978
+ * range. `hi` is the top visible pitch; `rowCount` the total rows in the grid.
2979
+ * Returns 0 when there are no notes.
2980
+ */
2981
+ declare function centerScrollTop(pitches: readonly number[], hi: number, rowCount: number, viewportH: number): number;
2982
+ /** Transpose every note by `semitones`, clamping pitch to [0,127] (never drops a note). */
2983
+ declare function transposeNotes(notes: readonly PluginMidiNote[], semitones: number): PluginMidiNote[];
2984
+ interface PianoRollEditorProps {
2985
+ /** Controlled note list (quarter-note beats). The editor never mutates this. */
2986
+ notes: readonly PluginMidiNote[];
2987
+ /** Emitted on every edit (add / delete / move / transpose) with the full next array. */
2988
+ onChange: (next: PluginMidiNote[]) => void;
2989
+ /** Scene length in bars → grid width = bars * beatsPerBar * PX_PER_BEAT. */
2990
+ bars: number;
2991
+ /** BPM — used only for audition timing in v1. */
2992
+ bpm: number;
2993
+ /** Beats per bar (time-signature numerator). Default 4. */
2994
+ beatsPerBar?: number;
2995
+ /** Snap step in quarter notes (1 = ¼ note, 0.25 = 1/16). Default 0.25. */
2996
+ snap?: number;
2997
+ /** Snap steps the toolbar selector offers. Default [1, 0.5, 0.25]. */
2998
+ snapOptions?: number[];
2999
+ /** Notified when the user changes snap (the editor still tracks it internally). */
3000
+ onSnapChange?: (snap: number) => void;
3001
+ /** Lowest pitch always visible. Default C2 (36). */
3002
+ minPitch?: number;
3003
+ /** Highest pitch always visible. Default C6 (84). */
3004
+ maxPitch?: number;
3005
+ /** Expand the visible window to include notes outside [minPitch,maxPitch]. Default true. */
3006
+ autoFit?: boolean;
3007
+ /** Optional single-note preview, fired when a note is added. */
3008
+ onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;
3009
+ /** Velocity for newly-added notes. Default 100. */
3010
+ defaultVelocity?: number;
3011
+ /** Disable all interaction (e.g. while the track is generating). Default false. */
3012
+ disabled?: boolean;
3013
+ /** Extra className for the outer container. */
3014
+ className?: string;
3015
+ /** Test id for the outer container. Default "sdk-piano-roll". */
3016
+ testId?: string;
3017
+ }
3018
+ declare function PianoRollEditor({ notes, onChange, bars, bpm, beatsPerBar, snap, snapOptions, onSnapChange, minPitch, maxPitch, autoFit, onAuditionNote, defaultVelocity, disabled, className, testId, }: PianoRollEditorProps): React.ReactElement;
1265
3019
 
1266
3020
  /**
1267
3021
  * VolumeSlider Component
@@ -1376,6 +3130,323 @@ declare function calculateTimeBasedTarget(elapsedMs: number, estimatedDurationMs
1376
3130
  */
1377
3131
  declare function SorceryProgressBar({ isLoading, statusText, completeText, onComplete, heightClass, initialProgress, onProgressChange, estimatedDurationMs, }: SorceryProgressBarProps): React.ReactElement | null;
1378
3132
 
3133
+ /**
3134
+ * DownloadPackButton — versioned-pack download trigger (SDK component).
3135
+ *
3136
+ * Parameterized by `packId`; drives the download through the host
3137
+ * (`host.startSamplePackDownload` / `host.onSamplePackProgress`) so plugins
3138
+ * never reach into the app's IPC (`window.electronAPI`). Two display variants:
3139
+ * - 'compact' (default) — small uppercase button for panel headers
3140
+ * - 'large' — bigger CTA used inside SamplePackCTACard
3141
+ *
3142
+ * @since SDK 2.8.0 (moved from the app and refactored onto PluginHost).
3143
+ */
3144
+
3145
+ type DownloadPackButtonVariant = 'compact' | 'large';
3146
+ interface DownloadPackButtonProps {
3147
+ /** Host the plugin received; drives the download + progress. */
3148
+ host: PluginHost;
3149
+ packId: string;
3150
+ /** Pack display name, e.g. 'Drum Sample Library'. Used in tooltips/labels. */
3151
+ displayName: string;
3152
+ /** Bundle size in bytes (shown in the large-variant label). */
3153
+ sizeBytes?: number;
3154
+ variant?: DownloadPackButtonVariant;
3155
+ /** Called once after the install completes (status === 'complete'). */
3156
+ onDownloadComplete?: () => void;
3157
+ }
3158
+ declare const DownloadPackButton: React.FC<DownloadPackButtonProps>;
3159
+
3160
+ /**
3161
+ * SamplePackCTACard — empty-state card a generator panel renders when its
3162
+ * sample pack is missing OR a newer version is available. Wraps
3163
+ * DownloadPackButton in a centered card. The completion callback should
3164
+ * re-fetch pack status on the parent so the card unmounts and the normal panel
3165
+ * UI takes over.
3166
+ *
3167
+ * @since SDK 2.8.0 (moved from the app; download driven through PluginHost).
3168
+ */
3169
+
3170
+ type SamplePackCTACardStatus = 'missing' | 'stale' | 'checking';
3171
+ /** Minimal pack info the card needs. A PackConfig is structurally compatible. */
3172
+ interface SamplePackCardInfo {
3173
+ packId: string;
3174
+ displayName: string;
3175
+ description: string;
3176
+ sizeBytes?: number;
3177
+ }
3178
+ interface SamplePackCTACardProps {
3179
+ /** Host the plugin received; drives the download. */
3180
+ host: PluginHost;
3181
+ pack: SamplePackCardInfo;
3182
+ status: SamplePackCTACardStatus;
3183
+ onDownloadComplete?: () => void;
3184
+ }
3185
+ declare const SamplePackCTACard: React.FC<SamplePackCTACardProps>;
3186
+
3187
+ /**
3188
+ * WaveformView — small canvas waveform for an audio file on disk.
3189
+ *
3190
+ * Reads bytes via `host.getAudioFileBytes`, decodes via
3191
+ * `AudioContext.decodeAudioData`, computes peaks, and renders to a
3192
+ * canvas. Suitable for take rows, sample previews, or any place a
3193
+ * decorative ~40px waveform makes sense.
3194
+ *
3195
+ * The component is self-contained: it owns the AudioContext and the
3196
+ * peak buffer, decodes once per `filePath` change, and tears down on
3197
+ * unmount. Failures (file missing, decode error) render as a silent
3198
+ * blank canvas — the caller can decide how to surface errors.
3199
+ */
3200
+
3201
+ interface WaveformViewProps {
3202
+ host: PluginHost;
3203
+ filePath: string;
3204
+ /** Number of bins to compute. Default 256 — plenty for ~40px tall rows. */
3205
+ bins?: number;
3206
+ /** Tailwind / inline className for sizing. Default: w-full h-10. */
3207
+ className?: string;
3208
+ /** Override the bar fill style (e.g., to match a track color). */
3209
+ fillStyle?: string;
3210
+ /**
3211
+ * If set, the bin range spans `targetSamples` instead of the file's
3212
+ * actual length. Bins beyond the audio render as flat silence — used
3213
+ * to align a partial recording inside a full-loop-width canvas so
3214
+ * every take row has the same time scale.
3215
+ */
3216
+ targetSamples?: number;
3217
+ }
3218
+ declare const WaveformView: React.FC<WaveformViewProps>;
3219
+
3220
+ /**
3221
+ * Shared level-meter component.
3222
+ *
3223
+ * Renders a horizontal LED-style bar over -60dBFS → 0dBFS:
3224
+ * - A fixed left-to-right gradient (green → orange → red), so the color is
3225
+ * tied to POSITION: a quiet signal lights only the green left, a hot signal
3226
+ * reaches the red right. An "unlit" mask hides the gradient beyond the
3227
+ * current level.
3228
+ * - A deterministic segment grid (the "LED monitor" look) drawn as a pure-CSS
3229
+ * repeating overlay — constant DOM, no per-frame cost.
3230
+ * - An optional peak-hold marker (`peakHoldDb`) — a bright line at the recent
3231
+ * maximum that the caller holds/decays (see `useTrackMeter`).
3232
+ * - An optional CLIP badge the caller wires up.
3233
+ *
3234
+ * Pure presentational: takes the current dB + `active` flag (+ optional held
3235
+ * peak) and draws. The only production consumer is the per-track strip
3236
+ * (`TrackMeterStrip`, via `compact`). `compact` shrinks the bar and drops the
3237
+ * numeric dB readout.
3238
+ */
3239
+
3240
+ interface LevelMeterProps {
3241
+ /** Current peak level in dBFS. -120 means "no signal". */
3242
+ peakDb: number;
3243
+ /** True when the underlying audio callback is firing. False = floor. */
3244
+ active: boolean;
3245
+ /**
3246
+ * Held peak in dBFS for the peak-hold marker. Omit to draw no marker. The
3247
+ * marker is hidden when this is at/below the visible floor (-60).
3248
+ */
3249
+ peakHoldDb?: number;
3250
+ /** Latched clip flag. When true, render the CLIP badge. */
3251
+ clipped?: boolean;
3252
+ /** User-clickable handler to clear the latched clip indicator. */
3253
+ onClearClip?: () => void;
3254
+ /**
3255
+ * Thin strip mode for per-track meters: hides the numeric dB readout and
3256
+ * shrinks the bar. Keeps the (rare) CLIP badge.
3257
+ */
3258
+ compact?: boolean;
3259
+ /** Optional className overlaid on the wrapper for layout tweaks. */
3260
+ className?: string;
3261
+ /** Inline test id — make multiple instances distinguishable. */
3262
+ 'data-testid'?: string;
3263
+ }
3264
+ declare const LevelMeter: React.FC<LevelMeterProps>;
3265
+
3266
+ /**
3267
+ * TrackMeterStrip — the thin per-track peak meter welded to the bottom of a
3268
+ * track row. Cosmetic: gives a general sense of each track's level and adds
3269
+ * motion during playback.
3270
+ *
3271
+ * This is deliberately its OWN component so the per-row meter selector
3272
+ * (`useTrackMeter`) re-renders ONLY this strip at ~30Hz, never the heavy
3273
+ * TrackRow around it. Render it as a full-width sibling directly under a row
3274
+ * body; it welds on with a squared top edge (like the track drawer does).
3275
+ */
3276
+
3277
+ interface TrackMeterStripProps {
3278
+ /** Shared meter handle from `useTrackLevels(host, isPlaying)`. */
3279
+ levels: TrackLevelsHandle;
3280
+ /** Tracktion engine track id (matches `PluginTrackHandle.id`). */
3281
+ trackId: string;
3282
+ /** Round the bottom corners (false when a drawer welds on below). Default true. */
3283
+ roundBottom?: boolean;
3284
+ /** Optional className for layout tweaks on the wrapper. */
3285
+ className?: string;
3286
+ }
3287
+ declare const TrackMeterStrip: React.FC<TrackMeterStripProps>;
3288
+
3289
+ /**
3290
+ * ScrollingWaveform — live waveform during recording (Phase 8.10).
3291
+ *
3292
+ * Reads the platform's `peakDb` history and renders it as a horizontal
3293
+ * bar-graph that scrolls left as new samples arrive. Two halves: top
3294
+ * band shows positive amplitude, bottom band mirrors it (matches the
3295
+ * static waveform's min/max layout in `WaveformView`).
3296
+ *
3297
+ * The data source is a function the caller supplies — typically a ref
3298
+ * to the `inputLevelDb` value from `AudioRoutingContext` polled at
3299
+ * ~30Hz. The component samples that ref via requestAnimationFrame and
3300
+ * shifts a fixed-size float ring buffer one column per frame.
3301
+ *
3302
+ * Pure presentational + animation logic; no IPC. Stops animating
3303
+ * when `active` is false (engine isn't running the audio callback).
3304
+ */
3305
+
3306
+ interface ScrollingWaveformProps {
3307
+ /** Function returning the latest peak in dBFS. Called per RAF. */
3308
+ getPeakDb: () => number;
3309
+ /** True while the audio callback is running; false freezes the wave. */
3310
+ active: boolean;
3311
+ /** Number of horizontal columns in the ring buffer. */
3312
+ columns?: number;
3313
+ /** Optional className for sizing. */
3314
+ className?: string;
3315
+ /** Highlight color for the wave. */
3316
+ fillStyle?: string;
3317
+ }
3318
+ declare const ScrollingWaveform: React.FC<ScrollingWaveformProps>;
3319
+
3320
+ /**
3321
+ * OffsetScrubber — manual sample-offset slider for Lyria-generated audio.
3322
+ *
3323
+ * Renders a thin horizontal track with one tick per detected beat (tall
3324
+ * tick on the downbeat) and a draggable thumb. Drag distance maps to a
3325
+ * sample offset that is applied to the audio clip via
3326
+ * `host.setAudioOffsetSamples(trackId, n)`.
3327
+ *
3328
+ * Snap behavior:
3329
+ * - Default: snap to the nearest beat in `cuePoints.beats`.
3330
+ * - Hold Shift: bypass snap (free 1-sample resolution).
3331
+ * - Click on a tick mark: jump to that beat exactly.
3332
+ *
3333
+ * The visible range is one bar (= meter beats) on each side of bar 1.
3334
+ * For a 4-bar / 4/4 clip at 44100 Hz, one bar at 120 BPM is 88_200
3335
+ * samples — so the slider covers ±88_200 samples, ~2 s either way. That
3336
+ * matches the alignment errors we observe from Lyria detection misses
3337
+ * (typically <1 beat off).
3338
+ *
3339
+ * BPM mismatch chip: shown when `cuePoints.detected_bpm` is more than
3340
+ * 1 BPM away from the project BPM, since the beat ticks won't line up
3341
+ * with the project grid in that case.
3342
+ */
3343
+
3344
+ interface OffsetScrubberProps {
3345
+ /** Detected beat positions + sample rate. Slider is disabled when null. */
3346
+ cuePoints: PluginCuePoints | null;
3347
+ /** Current offset, in samples (signed). */
3348
+ offsetSamples: number;
3349
+ /** Project BPM — used to compute the visible range and the mismatch chip. */
3350
+ projectBpm: number;
3351
+ /** Beats per bar, defaults to 4. */
3352
+ meter?: number;
3353
+ /** Called on drag-end with the resolved offset (already snapped). */
3354
+ onChange: (offsetSamples: number) => void;
3355
+ /** Disable interaction (e.g., during generation / split). */
3356
+ disabled?: boolean;
3357
+ }
3358
+ declare function OffsetScrubber({ cuePoints, offsetSamples, projectBpm, meter, onChange, disabled, }: OffsetScrubberProps): React.ReactElement;
3359
+
3360
+ /**
3361
+ * Shared waveform peaks + canvas drawer.
3362
+ *
3363
+ * Originally inlined in `stems/TrimEditorDrawer.tsx`; lifted to
3364
+ * this module so the recorder plugin's per-take rows can render the
3365
+ * same compact min/max display without duplicating the math.
3366
+ *
3367
+ * Design:
3368
+ * - `computePeaks` reduces an AudioBuffer to `bins` min/max pairs (mono
3369
+ * average across channels). Output layout is interleaved
3370
+ * `[min0, max0, min1, max1, ...]` so the renderer reads pairs
3371
+ * sequentially without index arithmetic.
3372
+ * - `drawWaveform` paints one 1px vertical bar per canvas column,
3373
+ * dpr-aware so it stays crisp on retina displays.
3374
+ *
3375
+ * No host or React dependencies — pure functions are safe to use from
3376
+ * tests, web workers, or non-React renderers.
3377
+ */
3378
+ interface WaveformPeaks {
3379
+ /** Sample rate of the source file (used to convert sample → seconds). */
3380
+ sampleRate: number;
3381
+ /** Total length of the raw file in samples. */
3382
+ totalSamples: number;
3383
+ /** Min/max pairs per bin (length = bins × 2). */
3384
+ peaks: Float32Array;
3385
+ }
3386
+ /**
3387
+ * Reduce an AudioBuffer to `bins` min/max pairs. Mono averages across
3388
+ * channels. The output buffer is fixed-size (`bins * 2`) for fast canvas
3389
+ * traversal.
3390
+ *
3391
+ * `targetSamples` (optional) extends the bin range to a fixed sample
3392
+ * count larger than the buffer's actual length — bins falling beyond
3393
+ * the buffer get (0, 0) pairs, which renders as a flat tail. Used by
3394
+ * the recorder so a partial last chunk's waveform sits at the start of
3395
+ * a full-loop-width canvas instead of being stretched to fill.
3396
+ */
3397
+ declare function computePeaks(audioBuffer: AudioBuffer, bins: number, targetSamples?: number): WaveformPeaks;
3398
+ /**
3399
+ * Draw min/max peaks to the given canvas. Resizes the canvas backing
3400
+ * store to CSS pixels × devicePixelRatio so the result is crisp on
3401
+ * retina. Caller controls CSS sizing via the `<canvas>` element's
3402
+ * className.
3403
+ */
3404
+ declare function drawWaveform(canvas: HTMLCanvasElement, peaks: WaveformPeaks, options?: {
3405
+ fillStyle?: string;
3406
+ }): void;
3407
+
3408
+ /**
3409
+ * WAV peak analyzer (Phase 8.10).
3410
+ *
3411
+ * Reads a WAV file via the plugin host, decodes it via Web Audio,
3412
+ * scans every channel for the absolute maximum sample, and returns
3413
+ * peak dBFS + a clipped flag (true when the peak >= -1dBFS, matching
3414
+ * the engine's hard-limiter ceiling).
3415
+ *
3416
+ * Used by the recorder's take rows to surface "this take peaked at
3417
+ * -8dB" or "this take CLIPPED" without the user having to click play.
3418
+ */
3419
+
3420
+ interface PeakAnalysis {
3421
+ peakLinear: number;
3422
+ peakDb: number;
3423
+ clipped: boolean;
3424
+ }
3425
+ declare function analyzeWavPeak(host: PluginHost, filePath: string): Promise<PeakAnalysis>;
3426
+
3427
+ /**
3428
+ * Synthesize a PluginCuePoints object from raw BPM/sample-rate inputs.
3429
+ *
3430
+ * The OffsetScrubber consumes PluginCuePoints — a beat grid plus
3431
+ * per-beat sample positions, normally produced by Lyria's onset
3432
+ * detector. The recorder doesn't have detected cue points (live
3433
+ * recordings have no detection pass), but it always knows the project
3434
+ * BPM, the engine sample rate, and the loop length in bars. That's
3435
+ * enough to construct a synthetic grid where every beat sits on a
3436
+ * regular interval — which is exactly what the scrubber needs to
3437
+ * provide tick marks + snap behavior for nudging the take's offset.
3438
+ */
3439
+
3440
+ interface SynthesizeCuePointsOptions {
3441
+ bpm: number;
3442
+ sampleRate: number;
3443
+ /** Total bars in the clip (e.g. 4 for a 4-bar loop). */
3444
+ bars: number;
3445
+ /** Beats per bar. Defaults to 4 (4/4). */
3446
+ meter?: number;
3447
+ }
3448
+ declare function synthesizeCuePoints({ bpm, sampleRate, bars, meter, }: SynthesizeCuePointsOptions): PluginCuePoints;
3449
+
1379
3450
  /**
1380
3451
  * useSceneState — Scene-keyed state hook for plugin developers.
1381
3452
  *
@@ -1401,6 +3472,87 @@ type SetSceneState<T> = (value: T | ((prev: T) => T)) => void;
1401
3472
  type SetSceneStateForScene<T> = (sceneId: string, value: T | ((prev: T) => T)) => void;
1402
3473
  declare function useSceneState<T>(activeSceneId: string | null, initialValue: T): [T, SetSceneState<T>, SetSceneStateForScene<T>];
1403
3474
 
3475
+ /**
3476
+ * useAnySolo — reactively reports whether ANY track in the project is soloed.
3477
+ *
3478
+ * Solo is cross-panel: when the user solos a track in ANY panel, the engine's
3479
+ * effective-mute model silences every non-soloed track. A panel uses this flag
3480
+ * to DIM its own non-soloed rows without lighting their Mute buttons:
3481
+ *
3482
+ * ```tsx
3483
+ * const anySolo = useAnySolo(host);
3484
+ * // ...
3485
+ * <TrackRow soloedOut={anySolo && !track.runtimeState.solo} ... />
3486
+ * ```
3487
+ *
3488
+ * Refreshes on mount and on every track-state change. `onTrackStateChange`
3489
+ * fires for tracks in ALL panels (not just this plugin's), so a solo toggled in
3490
+ * another panel updates this flag too.
3491
+ */
3492
+
3493
+ declare function useAnySolo(host: Pick<PluginHost, 'isAnySoloActive' | 'onTrackStateChange'>): boolean;
3494
+
3495
+ /**
3496
+ * useSoundHistory — generic, per-track "what sounds has this track had?" stack.
3497
+ *
3498
+ * Powers the drawer "History" tab: restore any earlier sound, star favorites,
3499
+ * and (via the host plugin) persist across project reopen. The SDK is ignorant
3500
+ * of WHAT a sound is — each plugin records an opaque `descriptor` (a drum sample
3501
+ * path / an instrument `{ displayName, zones }` / a synth Surge state blob) plus
3502
+ * a human `label`, and supplies `applySound` to re-apply a chosen descriptor.
3503
+ *
3504
+ * Persistence is the plugin's job: pass `opts.onChange` (called after every
3505
+ * mutation with the new state) to save, and call `restore()` on load to seed.
3506
+ * Favorited entries are never auto-evicted by the cap.
3507
+ *
3508
+ * Robustness: `applySound` + `onChange` are read through refs, so the returned
3509
+ * object is referentially STABLE regardless of whether the caller memoizes them.
3510
+ * Plugins list this object in `loadTracks` deps — an unstable return previously
3511
+ * caused a render loop, so keep it stable.
3512
+ *
3513
+ * @since SDK 2.13.0
3514
+ */
3515
+
3516
+ /** A track's ordered sound history plus the index of the currently-applied sound. */
3517
+ interface TrackSoundHistory {
3518
+ entries: readonly SoundHistoryEntry[];
3519
+ /** Index into `entries` of the currently-applied sound; -1 when empty. */
3520
+ cursor: number;
3521
+ }
3522
+ interface UseSoundHistoryOptions {
3523
+ /** Max non-favorited entries kept per track (favorites are never evicted). Default 24. */
3524
+ max?: number;
3525
+ /**
3526
+ * Called after every mutation (record/undo/restoreTo/toggleFavorite/clear) with the
3527
+ * track's new state — use it to persist. NOT called by `restore()` (that's a load).
3528
+ */
3529
+ onChange?: (trackId: string, state: TrackSoundHistory) => void;
3530
+ }
3531
+ interface UseSoundHistoryResult {
3532
+ /** Remember a sound that was just applied (generation, scene-load, or shuffle). */
3533
+ record(trackId: string, descriptor: unknown, label: string): void;
3534
+ /** Re-apply the sound one step before the current one. Resolves true if it moved. */
3535
+ undo(trackId: string): Promise<boolean>;
3536
+ /** Re-apply a specific entry by index. Resolves true if it applied. */
3537
+ restoreTo(trackId: string, index: number): Promise<boolean>;
3538
+ /** The ordered history + cursor for a track (safe empty default). */
3539
+ list(trackId: string): TrackSoundHistory;
3540
+ /** Whether there is an earlier sound to step back to. */
3541
+ canUndo(trackId: string): boolean;
3542
+ /** Forget a track's history (e.g. on regenerate). Persists the cleared state. */
3543
+ clear(trackId: string): void;
3544
+ /** Forget ALL tracks' history in memory (e.g. before re-seeding on scene load). */
3545
+ reset(): void;
3546
+ /** Seed a track's full history (e.g. from persistence on load). Does NOT fire onChange. */
3547
+ restore(trackId: string, state: {
3548
+ entries?: readonly SoundHistoryEntry[];
3549
+ cursor?: number;
3550
+ } | null | undefined): void;
3551
+ /** Toggle the favorite flag on an entry (favorites survive cap eviction). */
3552
+ toggleFavorite(trackId: string, index: number): void;
3553
+ }
3554
+ declare function useSoundHistory(applySound: (trackId: string, descriptor: unknown) => Promise<void>, opts?: UseSoundHistoryOptions): UseSoundHistoryResult;
3555
+
1404
3556
  /**
1405
3557
  * Plugin SDK Version
1406
3558
  *
@@ -1409,7 +3561,7 @@ declare function useSceneState<T>(activeSceneId: string | null, initialValue: T)
1409
3561
  * Registry checks semver.gte(PLUGIN_SDK_VERSION, manifest.minHostVersion)
1410
3562
  * during activation and marks incompatible plugins accordingly.
1411
3563
  */
1412
- declare const PLUGIN_SDK_VERSION = "2.0.0";
3564
+ declare const PLUGIN_SDK_VERSION = "2.24.0";
1413
3565
 
1414
3566
  /**
1415
3567
  * FX Preset Definitions
@@ -1463,4 +3615,98 @@ declare function sliderToDb(slider: number): number;
1463
3615
  */
1464
3616
  declare function dbToSlider(db: number): number;
1465
3617
 
1466
- export { type BulkAddPlaceholderTrack, type ComposeProgressEvent, type ComposeProgressListener, type ComposeSceneOptions, type ComposeSceneResult, type CreateTrackOptions, DB_MAX, DB_MIN, DEFAULT_FX_CATEGORY_DETAIL, DEFAULT_FX_DRY_WET, type DeckBoundaryEvent, type DeckBoundaryListener, EMPTY_FX_DETAIL_STATE, EMPTY_FX_STATE, type ExportMidiBundleOptions, type ExportMidiBundleResult, type ExportedPluginData, FX_CATEGORIES, FX_CHAIN_ORDER, FX_DISPLAY_LABELS, FX_ENGINE_PLUGIN_NAMES, FX_PRESET_CONFIGS, type FxCategory, type FxCategoryDetailState, type FxPreset, type FxPresetConfig, type FxPresetData, type FxPresetDataEntry, FxToggleBar, type FxToggleBarProps, type GeneratorPlugin, type GeneratorType, type InstrumentDescriptor, InstrumentDrawer, type InstrumentDrawerProps, type LLMGenerationRequest, type LLMGenerationResult, type MidiClipData, type MidiWriteResult, type MixInterpolation, type MusicalContext, PLUGIN_SDK_VERSION, PanSlider, type PluginAppTool, type PluginAppToolInputSchema, type PluginAppToolResult, type PluginAudioTextureRequest, type PluginAudioTextureResult, type PluginCapabilities, type PluginChordSegment, type PluginChordTiming, type PluginConcurrentTrackInfo, type PluginCuePoints, type PluginDownloadOptions, PluginError, type PluginErrorCode, type PluginFileDialogOptions, type PluginFxCategoryDetailState, type PluginGenerationContext, type PluginHost, type PluginHttpRequestOptions, type PluginHttpResponse, type PluginManifest, type PluginMidiNote, type PluginPresetData, type PluginPresetInfo, type PluginRegistration, type PluginSampleFilter, type PluginSampleImportResult, type PluginSampleInfo, type PluginSampleTrackInfo, type PluginSceneContext, type PluginSceneInfo, type PluginSettingsSchema, type PluginSettingsStore, type PluginSkill, type PluginSkillInputSchema, type PluginStatus, type PluginStemSplitResult, type PluginStemTrackInfo, type PluginSynthInfo, type PluginTrackFxDetailState, type PluginTrackHandle, type PluginTrackInfo, type PluginTrackRuntimeState, type PluginTransportState, type PluginUIProps, type PostProcessOptions, type SDKTrackRowProps, SLIDER_UNITY, type SavePluginPresetOptions, type SceneChangeListener, type SettingDefinition, type ShufflePresetResult, SorceryProgressBar, type StemType, type TrackFxDetailState, type TrackFxState, TrackRow, type TrackStateChangeListener, type TransportEvent, type TransportEventListener, type UnsubscribeFn, VolumeSlider, calculateTimeBasedTarget, dbToSlider, sliderToDb, useSceneState };
3618
+ /**
3619
+ * Format the cross-plugin concurrent-track context into a prose block
3620
+ * that's safe to drop straight into an LLM user-prompt. Both the synth
3621
+ * and drum builtin panels use this so the rendered prompt stays
3622
+ * consistent across generators — and so a single change here propagates
3623
+ * to every plugin that calls `host.getGenerationContext()`.
3624
+ *
3625
+ * Per-track payload follows the user's preferred shape (raw note JSON
3626
+ * grouped by chord) so the model sees velocity / start-beat /
3627
+ * duration / pitch verbatim and can reason about feel + harmony.
3628
+ *
3629
+ * Returns the empty string when there are no concurrent tracks — call
3630
+ * sites can `if (block) push(block)` rather than baking in a placeholder.
3631
+ */
3632
+
3633
+ declare function formatConcurrentTracks(ctx: PluginGenerationContext): string;
3634
+
3635
+ /**
3636
+ * Lightweight, dependency-free semantic matching for sample selection.
3637
+ *
3638
+ * Sample generators (drums, instruments) ship a short StableAudio text
3639
+ * prompt next to every sample ("tight 909-style kick one shot, hard click
3640
+ * transient, short punchy body, dry, no hi hats, no loop"). When the user
3641
+ * asks for "a 1950s style boom bap kick" we want to pick the sample whose
3642
+ * prompt is closest to that intent — instead of a uniform random draw —
3643
+ * while still preserving variety so a vague "give me a kick" doesn't return
3644
+ * the identical sample every time.
3645
+ *
3646
+ * Design notes:
3647
+ * - Pure functions, no I/O, no SDK-type dependencies → trivially unit
3648
+ * testable with an injected `rng`, and safe to call from either the
3649
+ * main or renderer process.
3650
+ * - Scoring is IDF-weighted query-coverage (a TF-IDF / BM25-lite). The
3651
+ * IDF is derived from the candidate pool itself, so it is STRUCTURAL —
3652
+ * no hand-maintained synonym tables. Rare, discriminating tokens in the
3653
+ * prompts ("909", "dusty", "tube") dominate; corpus-universal filler
3654
+ * ("one", "shot", "dry") washes out to ~zero IDF on its own.
3655
+ * - The near-universal negative clauses StableAudio prompts carry
3656
+ * ("no hi hats", "no loop", "no melody") are stripped before tokenizing;
3657
+ * they are pure noise for matching.
3658
+ * - Selection is softmax-weighted random among the top-k. Flat scores →
3659
+ * ~uniform (≈ the old random behavior); a clear winner → tight
3660
+ * convergence. The all-zero (no-signal) case is intentionally left to
3661
+ * the caller to fall back to its existing random path over the full
3662
+ * pool — see `scorePromptMatch`'s contract below.
3663
+ */
3664
+ /**
3665
+ * Tokenize a prompt or query into matchable lowercase tokens.
3666
+ *
3667
+ * 1. Drop comma-delimited negative clauses ("no hi hats", "no loop").
3668
+ * 2. Lowercase, split on any non-alphanumeric run.
3669
+ * 3. Drop stop-words and 1–2 digit numeric noise ("01", "02") while
3670
+ * keeping meaningful numerics ("808", "909", "1950").
3671
+ */
3672
+ declare function tokenizePrompt(text: string): string[];
3673
+ /**
3674
+ * Score each candidate prompt against the query, returning a parallel array
3675
+ * of scores in [0, 1] (1 = the candidate covers all of the query's
3676
+ * discriminating intent).
3677
+ *
3678
+ * Contract: a returned max of 0 means the query shares NO matchable token
3679
+ * with any candidate (no signal). Callers should treat that as "fall back to
3680
+ * the existing uniform-random pick over the full pool" so vague queries keep
3681
+ * today's variety rather than biasing toward an arbitrary top-k slice.
3682
+ */
3683
+ declare function scorePromptMatch(query: string, candidatePrompts: ReadonlyArray<string>): number[];
3684
+ /** One scored candidate. `key` (if present) is what `excludeKeys` matches on. */
3685
+ interface ScoredCandidate<T> {
3686
+ item: T;
3687
+ score: number;
3688
+ key?: string;
3689
+ }
3690
+ interface PickTopKOptions {
3691
+ /** Consider only the top-k by score (default 5). */
3692
+ k?: number;
3693
+ /**
3694
+ * Softmax temperature (default 0.3). Lower → sharper preference for the
3695
+ * top match; higher → flatter (more variety). Scores are in [0, 1].
3696
+ */
3697
+ temperature?: number;
3698
+ /** Candidate keys to exclude (e.g. shuffle history). */
3699
+ excludeKeys?: ReadonlySet<string>;
3700
+ /** Injectable RNG in [0, 1) for deterministic tests (default Math.random). */
3701
+ rng?: () => number;
3702
+ }
3703
+ /**
3704
+ * Pick one candidate via softmax-weighted random selection among the top-k
3705
+ * by score. Returns null only when the pool is empty after exclusion.
3706
+ *
3707
+ * Equal scores → equal weights → uniform pick among the top-k, so this
3708
+ * degrades gracefully toward random when the query gives no preference.
3709
+ */
3710
+ declare function pickTopKWeighted<T>(scored: ReadonlyArray<ScoredCandidate<T>>, options?: PickTopKOptions): T | null;
3711
+
3712
+ export { type AudioInputDevice, type BulkAddPlaceholderTrack, type ComposeProgressEvent, type ComposeProgressListener, type ComposeSceneOptions, type ComposeSceneResult, ConfirmDialog, type ConfirmDialogProps, type CreateTrackOptions, type CrossfadeInpaintInput, type CrossfadeLayer, type CrossfadeMeta, CrossfadeModal, type CrossfadeModalProps, type CrossfadePairMeta, type CrossfadeSelection, type CrossfadeSlot, CrossfadeTrackRow, type CrossfadeTrackRowProps, DB_MAX, DB_MIN, DEFAULT_FX_CATEGORY_DETAIL, DEFAULT_FX_DRY_WET, DRAG_DEAD_ZONE, type DeckBoundaryEvent, type DeckBoundaryListener, DownloadPackButton, type DownloadPackButtonProps, type DownloadPackButtonVariant, type DrawerTab, type DrumKit, EMPTY_FX_DETAIL_STATE, EMPTY_FX_STATE, EQUAL_POWER_GAIN, type ExportMidiBundleOptions, type ExportMidiBundleResult, type ExportedPluginData, FX_CATEGORIES, FX_CHAIN_ORDER, FX_DISPLAY_LABELS, FX_ENGINE_PLUGIN_NAMES, FX_PRESET_CONFIGS, type FxCategory, type FxCategoryDetailState, type FxPreset, type FxPresetConfig, type FxPresetData, type FxPresetDataEntry, FxToggleBar, type FxToggleBarProps, GUTTER_W, type GeneratorPlugin, type GeneratorType, type ImportCandidateScene, type ImportCandidateTrack, ImportTrackModal, type ImportTrackModalProps, type InstrumentDescriptor, TrackDrawer as InstrumentDrawer, type TrackDrawerProps as InstrumentDrawerProps, type InstrumentSampler, type InstrumentZone, type LLMCandidate, type LLMContent, type LLMFunctionDeclaration, type LLMGenerationConfig, type LLMGenerationRequest, type LLMGenerationResult, type LLMPart, type LLMSystemInstruction, type LLMTool, type LLMToolUseRequest, type LLMToolUseResponse, type LLMUsageMetadata, LevelMeter, type LevelMeterProps, type ListAudioFilesOptions, type ListImportableTracksOptions, type MidiClipData, type MidiWriteResult, type MixInterpolation, Modal, type ModalProps, type MusicalContext, OffsetScrubber, type OffsetScrubberProps, PLUGIN_SDK_VERSION, PX_PER_BEAT, PanSlider, type PeakAnalysis, PianoRollEditor, type PianoRollEditorProps, type PickTopKOptions, type PluginAppTool, type PluginAppToolInputSchema, type PluginAppToolResult, type PluginAudioTextureRequest, type PluginAudioTextureResult, type PluginCapabilities, type PluginChordSegment, type PluginChordTiming, type PluginConcurrentTrackInfo, type PluginCuePoints, type PluginDownloadOptions, PluginError, type PluginErrorCode, type PluginFileDialogOptions, type PluginFxCategoryDetailState, type PluginGenerationContext, type PluginHost, type PluginHttpRequestOptions, type PluginHttpResponse, type PluginManifest, type PluginMidiNote, type PluginPresetData, type PluginPresetInfo, type PluginRegistration, type PluginSampleFilter, type PluginSampleImportResult, type PluginSampleInfo, type PluginSampleTrackInfo, type PluginSceneContext, type PluginSceneInfo, type PluginSettingsSchema, type PluginSettingsStore, type PluginSkill, type PluginSkillInputSchema, type PluginStatus, type PluginStemSplitResult, type PluginStemTrackInfo, type PluginSynthInfo, type PluginTrackFxDetailState, type PluginTrackHandle, type PluginTrackInfo, type PluginTrackLevel, type PluginTrackRuntimeState, type PluginTransportState, type PluginTrimWindow, type PluginUIProps, type PostProcessOptions, RESIZE_HANDLE_PX, ROW_HEIGHT, type ReadMidiClip, type ReadMidiResult, type RecordingChunkFinalizedEvent, type RecordingTargetInfo, type SDKTrackRowProps, SLIDER_UNITY, SamplePackCTACard, type SamplePackCTACardProps, type SamplePackCTACardStatus, type SamplePackCardInfo, type SavePluginPresetOptions, type SceneChangeListener, type SceneFamilyTrack, type ScoredCandidate, ScrollingWaveform, type ScrollingWaveformProps, type SettingDefinition, type ShufflePresetResult, SorceryProgressBar, type SoundHistoryEntry, type StemType, type SynthesizeCuePointsOptions, TrackDrawer, type TrackDrawerProps, type TrackFxDetailState, type TrackFxState, type TrackLevelsHandle, TrackMeterStrip, type TrackMeterStripProps, type TrackMeterView, TrackRow, type TrackRowDragProps, type TrackSoundHistory, type TrackSoundSnapshot, type TrackStateChangeListener, type TransportEvent, type TransportEventListener, type UnsubscribeFn, type UseSoundHistoryOptions, type UseSoundHistoryResult, type UseTrackReorderOptions, type UseTrackReorderResult, VolumeSlider, type WaveformPeaks, WaveformView, type WaveformViewProps, analyzeWavPeak, asCrossfadeMeta, buildCrossfadeInpaintPrompt, calculateTimeBasedTarget, cellToPx, centerScrollTop, computePeaks, dbToSlider, drawWaveform, formatConcurrentTracks, moveItem, parseCrossfadePairs, pickTopKWeighted, pitchToName, pxToCell, resizeNoteDuration, scorePromptMatch, sliderToDb, synthesizeCuePoints, tokenizePrompt, transposeNotes, useAnySolo, useSceneState, useSoundHistory, useTrackLevel, useTrackLevels, useTrackMeter, useTrackReorder, useTransportPlaying };