@signalsandsorcery/plugin-sdk 2.3.1 → 2.25.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.mts 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 {
@@ -139,10 +227,24 @@ interface PluginHost {
139
227
  setTrackMute(trackId: string, muted: boolean): Promise<void>;
140
228
  /** Set track volume (linear 0.0 - 1.0). Only works on owned tracks. */
141
229
  setTrackVolume(trackId: string, volume: number): Promise<void>;
230
+ /**
231
+ * Set/replace a time-based volume automation curve (a fade envelope) on a track,
232
+ * or clear it with an empty array. Points are {time: seconds, db}; linear between
233
+ * points. Used by crossfade tracks to fade origin↔target across the looped
234
+ * transition. Optional — callers MUST null-check. @since SDK 2.25.0
235
+ */
236
+ setTrackVolumeAutomation?(trackId: string, points: Array<{
237
+ time: number;
238
+ db: number;
239
+ }>): Promise<void>;
142
240
  /** Set track pan (-1.0 left to 1.0 right). Only works on owned tracks. */
143
241
  setTrackPan(trackId: string, pan: number): Promise<void>;
144
242
  /** Set track solo state. Only works on owned tracks. */
145
243
  setTrackSolo(trackId: string, solo: boolean): Promise<void>;
244
+ /** Whether ANY track in the project is currently soloed (across all panels).
245
+ * Lets a panel dim its non-soloed rows (the engine silences them via the
246
+ * effective-mute model). Read-only; not ownership-scoped. */
247
+ isAnySoloActive(): Promise<boolean>;
146
248
  /** Rename a track. Only works on owned tracks. */
147
249
  setTrackName(trackId: string, name: string): Promise<void>;
148
250
  /**
@@ -160,9 +262,31 @@ interface PluginHost {
160
262
  */
161
263
  setTrackRole(trackId: string, role: string): Promise<void>;
162
264
  /** Shuffle preset: keep MIDI, apply a random preset from the same category. Only works on owned tracks. */
163
- shufflePreset(trackId: string): Promise<ShufflePresetResult>;
265
+ /**
266
+ * Shuffle preset: keep MIDI, apply a random preset from the same category.
267
+ * `excludeNames` (since SDK 1.5.0) filters preset names out of the random
268
+ * pool; the current preset is always implicitly excluded. Use this to
269
+ * implement a "no-repeat until full cycle" shuffle: the panel accumulates
270
+ * the history and resets when shufflePreset throws "no presets available".
271
+ */
272
+ shufflePreset(trackId: string, excludeNames?: readonly string[]): Promise<ShufflePresetResult>;
164
273
  /** Duplicate track: copy MIDI + role to a new track with a different preset. Only works on owned tracks. */
165
274
  duplicateTrack(trackId: string): Promise<PluginTrackHandle>;
275
+ /**
276
+ * Persist this plugin's track row order for the active scene. Pass the stable
277
+ * track dbIds ({@link PluginTrackHandle.dbId}) in the desired top-to-bottom
278
+ * order. Reload-safe — {@link getPluginTracks} returns tracks in this order
279
+ * across scene switches and project reopen.
280
+ *
281
+ * Per-panel and decoupled from the engine-synced global track order, so
282
+ * reordering one panel never disturbs other plugins' tracks. Tracks omitted
283
+ * from the list (e.g. newly added or duplicated) keep their natural order at
284
+ * the end. Pairs with the {@link useTrackReorder} hook, which drives the
285
+ * drag-and-drop UI and calls this on drop.
286
+ *
287
+ * @since SDK 2.16.0
288
+ */
289
+ reorderTracks(orderedTrackIds: readonly string[]): Promise<void>;
166
290
  /**
167
291
  * Return the canonical list of valid role tokens that the host's
168
292
  * classifier and UI understand. Plugins should use this list when
@@ -208,6 +332,18 @@ interface PluginHost {
208
332
  * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.
209
333
  */
210
334
  postProcessMidi(notes: PluginMidiNote[], options: PostProcessOptions): Promise<PluginMidiNote[]>;
335
+ /**
336
+ * Read a track's current MIDI notes for in-place editing (e.g. a piano
337
+ * roll). Returns the track's clips with beat-based notes; an empty `clips`
338
+ * array means the track has no MIDI. Reads LIVE engine state (NOT the DB),
339
+ * so it reflects unsaved generator output too and needs no project_id
340
+ * scoping — do not "fix" this into a DB query.
341
+ *
342
+ * Ownership-gated like {@link writeMidiClip}. Optional so a plugin built
343
+ * against this SDK still loads on an older host — callers MUST null-check.
344
+ * @since SDK 2.15.0
345
+ */
346
+ readMidiNotes?(trackId: string): Promise<ReadMidiResult>;
211
347
  /** Place an audio file on a track this plugin owns. */
212
348
  writeAudioClip(trackId: string, filePath: string, position?: number): Promise<void>;
213
349
  /**
@@ -237,6 +373,16 @@ interface PluginHost {
237
373
  setPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;
238
374
  /** Get current plugin state (base64-encoded). */
239
375
  getPluginState(trackId: string, pluginIndex: number): Promise<string>;
376
+ /**
377
+ * Set a plugin's RAW VST3/AU state — the plugin's own getStateInformation
378
+ * format, bypassing Tracktion's ValueTree wrapper. Use for third-party
379
+ * instruments (u-he Diva, Serum, …) whose patches the ValueTree round-trip
380
+ * does not faithfully preserve. Default Surge XT presets use setPluginState.
381
+ * @since SDK 2.15.0
382
+ */
383
+ setRawPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;
384
+ /** Get a plugin's RAW VST3/AU state (see setRawPluginState). @since SDK 2.15.0 */
385
+ getRawPluginState(trackId: string, pluginIndex: number): Promise<string>;
240
386
  /** List plugins currently loaded on a track. */
241
387
  getTrackPlugins(trackId: string): Promise<PluginSynthInfo[]>;
242
388
  /** Remove a plugin from a track. */
@@ -253,14 +399,154 @@ interface PluginHost {
253
399
  showInstrumentEditor(trackId: string): Promise<void>;
254
400
  /** Close the instrument plugin's editor window. */
255
401
  hideInstrumentEditor(trackId: string): Promise<void>;
402
+ /**
403
+ * Load the engine's built-in sampler on the track (if not already
404
+ * present) and configure it with a single one-shot sound. Every MIDI
405
+ * note triggers the loaded sample regardless of pitch — used by the
406
+ * drum-generator plugin where the LLM's emitted pitch is advisory.
407
+ *
408
+ * Idempotent: calling repeatedly on the same track swaps the loaded
409
+ * sample without stacking more sampler instances. The sampler counts
410
+ * as the track's instrument; mixing it with `setTrackInstrument` on
411
+ * the same track is undefined behaviour for now.
412
+ *
413
+ * @since SDK 1.2.0
414
+ */
415
+ setTrackDrumKit(trackId: string, kit: DrumKit): Promise<void>;
416
+ /**
417
+ * Load the engine's built-in sampler on the track (if not already
418
+ * present) and configure it with a pitched, polyphonic, multi-zone
419
+ * instrument. Each MIDI note triggers the zone whose [minKey,maxKey]
420
+ * range contains it; the zone is played back pitch-shifted relative
421
+ * to its rootKey. Polyphony is handled by the Tracktion sampler's
422
+ * voice allocator.
423
+ *
424
+ * Used by the instrument-generator plugin to load a pre-rendered
425
+ * pitched-sample manifest. Mutually exclusive with `setTrackDrumKit`
426
+ * on the same track (both occupy the sampler slot) and with
427
+ * `setTrackInstrument(pluginId)` (which loads a VST synth instead).
428
+ *
429
+ * Idempotent: calling repeatedly on the same track swaps the loaded
430
+ * zones without stacking sampler instances.
431
+ *
432
+ * @since SDK 1.3.0
433
+ */
434
+ setTrackInstrumentSampler(trackId: string, instrument: InstrumentSampler): Promise<void>;
435
+ /**
436
+ * List audio files (by default `.wav`) under `rootPath`. Returns
437
+ * absolute file paths. `recursive` defaults to false; pass `true` to
438
+ * walk subdirectories. The drum-generator plugin uses this to
439
+ * lazily discover available samples without round-tripping each
440
+ * folder through `getSamples`.
441
+ *
442
+ * Plugins MUST NOT use this to read paths outside their declared
443
+ * sample roots — the host may add path validation in a later release.
444
+ *
445
+ * @since SDK 1.2.0
446
+ */
447
+ listAudioFiles(rootPath: string, options?: ListAudioFilesOptions): Promise<string[]>;
448
+ /**
449
+ * Read a text file's contents from the host filesystem (UTF-8). Returns
450
+ * `null` on any read error (missing file, permission, etc.) — the
451
+ * caller does not need to wrap the call in try/catch.
452
+ *
453
+ * Intended for plugin sample-library metadata: instrument manifest
454
+ * JSON (`<instrument-id>/manifest.json`) and prompt-sibling text
455
+ * (`<id>.txt`). Plugins parse the returned string themselves so the
456
+ * host stays content-agnostic.
457
+ *
458
+ * Plugins MUST NOT use this to read paths outside their declared
459
+ * sample roots — the host may add path validation in a later release.
460
+ *
461
+ * @since SDK 1.4.0
462
+ */
463
+ readTextFile(absolutePath: string): Promise<string | null>;
256
464
  /** Get the FULL generation context for the active scene. */
257
465
  getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;
258
466
  /** Get lightweight musical context (no concurrent track MIDI data). */
259
467
  getMusicalContext(): Promise<MusicalContext>;
260
468
  /** Get the active scene ID. Null if no scene is active. */
261
469
  getActiveSceneId(): string | null;
470
+ /**
471
+ * Get the bound project's DB id. Null when no project is bound.
472
+ * Optional — older hosts and the renderer-side host proxy may omit it;
473
+ * callers MUST feature-check. Used e.g. to detect project switches for
474
+ * per-project conversation persistence.
475
+ * @since SDK 2.18.0
476
+ */
477
+ getProjectId?(): string | null;
262
478
  /** Get list of all scenes in the project. */
263
479
  getSceneList(): Promise<PluginSceneInfo[]>;
480
+ /**
481
+ * Enumerate importable track candidates from OTHER scenes, scoped to this
482
+ * plugin's track type (derived from the plugin id). Each candidate is
483
+ * annotated with `importable` + `disabledReason` — the host computes the
484
+ * harmonic/length/tempo gate so the UI only renders it. By default the active
485
+ * scene is excluded; pass `includeSameScene` to also surface the active
486
+ * scene's MIDI tracks owned by OTHER panels (the cross-panel re-sound source).
487
+ * Scenes with no candidate of this type are omitted.
488
+ *
489
+ * Optional so a plugin built against this SDK still loads on an older host —
490
+ * callers MUST null-check and hide the affordance when absent.
491
+ * @since SDK 2.13.0
492
+ */
493
+ listImportableTracks?(opts?: ListImportableTracksOptions): Promise<ImportCandidateScene[]>;
494
+ /**
495
+ * Import a source track (from another scene) into the active scene as a
496
+ * faithful, independent copy, delegating to the `import_track_from_scene`
497
+ * tool. Returns the new track's handle so the panel can append a row.
498
+ * Throws on a gate violation — call only for candidates with `importable`.
499
+ * Optional — callers MUST null-check (see `listImportableTracks`).
500
+ * @since SDK 2.13.0
501
+ */
502
+ importTrack?(opts: {
503
+ sourceSceneId: string;
504
+ sourceTrackId: string;
505
+ }): Promise<PluginTrackHandle>;
506
+ /**
507
+ * Read a source track's CURRENT sound — sample path (drums), sampler zones
508
+ * (instruments), or Surge preset state (synths) — so a panel can copy just
509
+ * the sound onto another track, IGNORING the contract gate that `importTrack`
510
+ * enforces ("different contract, same preset"). Read-only: applies nothing.
511
+ * The selector is the source track's DB row id (`ImportCandidateTrack.dbId`).
512
+ * Returns null when the track has no stored sound. Optional — callers MUST
513
+ * null-check (see `listImportableTracks`).
514
+ * @since SDK 2.14.0
515
+ */
516
+ getTrackSound?(sourceTrackDbId: string): Promise<TrackSoundSnapshot | null>;
517
+ /**
518
+ * Read a source track's persisted MIDI by its DB row id — the cross-panel
519
+ * READ half of "re-sound a part on a different instrument". Unlike
520
+ * `readMidiNotes` (engine-read, ownership-gated), this reads the DB and is
521
+ * NOT ownership-gated, so a panel can pull a part out of a track owned by a
522
+ * DIFFERENT panel in the same scene (the selector is
523
+ * `ImportCandidateTrack.dbId`, e.g. a `sameScene` candidate). Notes are
524
+ * beat-based, identical shape to `readMidiNotes`; the loop span comes from the
525
+ * source scene. Returns `{ clips: [] }` when the track has no MIDI. Optional —
526
+ * callers MUST null-check (see `listImportableTracks`).
527
+ * @since SDK 2.20.0
528
+ */
529
+ readImportableTrackMidi?(sourceTrackDbId: string): Promise<ReadMidiResult>;
530
+ /**
531
+ * List THIS panel's family tracks in a specific scene (by DB id), WITHOUT the
532
+ * import key/length/tempo gate that `listImportableTracks` applies. Powers the
533
+ * crossfade picker: the origin (from) and target (to) scenes of a transition
534
+ * deliberately differ in key, so gating would wrongly hide valid candidates.
535
+ * Project-scoped, read-only. Returns [] for an unknown/empty scene. Optional —
536
+ * callers MUST null-check (see `listImportableTracks`).
537
+ * @since SDK 2.22.0
538
+ */
539
+ listSceneFamilyTracks?(sceneDbId: string): Promise<SceneFamilyTrack[]>;
540
+ /**
541
+ * Read a specific scene's musical key (tonic + mode) by db id. Labels the
542
+ * SOURCE keys of a crossfade's origin/target patterns — the active-scene
543
+ * musical context only carries the transition scene's key. Optional — callers
544
+ * MUST null-check. @since SDK 2.24.0
545
+ */
546
+ getSceneKey?(sceneDbId: string): Promise<{
547
+ key: string;
548
+ mode: string;
549
+ } | null>;
264
550
  /** Subscribe to transport state changes. Returns unsubscribe function. */
265
551
  onTransportEvent(listener: TransportEventListener): UnsubscribeFn;
266
552
  /** Subscribe to deck boundary events. Returns unsubscribe function. */
@@ -269,8 +555,59 @@ interface PluginHost {
269
555
  onSceneChange(listener: SceneChangeListener): UnsubscribeFn;
270
556
  /** Get current transport state (one-shot). */
271
557
  getTransportState(): Promise<PluginTransportState>;
558
+ /**
559
+ * One-shot mono peak level for every track this plugin owns. Drives the
560
+ * cosmetic per-track strip meters; poll at ~30Hz while the transport is
561
+ * playing. The host scopes the result to this plugin's tracks and coalesces
562
+ * the underlying engine read, so a busy engine yields a STALE meter rather
563
+ * than a backlog (playback always wins over the GUI). Optional: guard with
564
+ * `typeof host.getTrackLevels === 'function'` for older hosts.
565
+ * @since SDK 2.21.0
566
+ */
567
+ getTrackLevels?(): Promise<PluginTrackLevel[]>;
272
568
  /** Generate text/JSON via the host's authenticated LLM service. */
273
569
  generateWithLLM(request: LLMGenerationRequest): Promise<LLMGenerationResult>;
570
+ /**
571
+ * Generate with native tool-use (function calling). Used by agentic plugins
572
+ * (chat panel, etc.) to drive an iterative loop where the model calls tools,
573
+ * observes results, and decides next steps — same loop class as Claude Code
574
+ * or VS Code agent mode.
575
+ *
576
+ * Shape mirrors Gemini's `generateContent` REST surface; the host forwards
577
+ * verbatim to the gateway's Gemini-native passthrough endpoint, which adds
578
+ * the central Google API key. Plugins never see provider credentials.
579
+ *
580
+ * Available since SDK 2.4.0.
581
+ */
582
+ generateWithLLMTools(request: LLMToolUseRequest): Promise<LLMToolUseResponse>;
583
+ /**
584
+ * Resolve absolute paths for spawning the bundled `sas` CLI as a subprocess.
585
+ * Used by agentic plugins that drive the CLI as their tool surface (chat
586
+ * panel, etc.). Returns `null` when called from a renderer-side host or
587
+ * when the CLI isn't accessible.
588
+ *
589
+ * Available since SDK 2.4.0.
590
+ */
591
+ getCliPaths(): {
592
+ appExe: string;
593
+ cliEntry: string;
594
+ } | null;
595
+ /**
596
+ * Resolve the absolute path to a bundled resource directory shipped with
597
+ * the app via `extraResources` (e.g. `'drum-samples'`,
598
+ * `'tracktion-presets'`). In dev, resolves to
599
+ * `<projectRoot>/resources/<name>`. In packaged builds, resolves to
600
+ * `<process.resourcesPath>/<name>`.
601
+ *
602
+ * Returns `null` if the host cannot resolve paths in this context
603
+ * (e.g. Electron mocked out in unit tests). Plugins MUST null-check and
604
+ * either degrade gracefully or fall back to a known dev path.
605
+ *
606
+ * Async by design: the renderer-side host proxy round-trips through IPC.
607
+ *
608
+ * @since SDK 2.7.0
609
+ */
610
+ getBundledResourcePath(name: string): Promise<string | null>;
274
611
  /** Check if LLM access is available (user authenticated + gateway reachable). */
275
612
  isLLMAvailable(): Promise<boolean>;
276
613
  /**
@@ -281,23 +618,59 @@ interface PluginHost {
281
618
  * `'scene'` to hide project-level tools they shouldn't call. When omitted,
282
619
  * every tool regardless of scope is returned.
283
620
  *
621
+ * `opts.includeDeferred` (since SDK 2.18.0) opts in to tools flagged with
622
+ * `deferLoading` (progressive disclosure). Default `false` mirrors
623
+ * `/api/v1/actions` — the curated core surface. Used by curation layers
624
+ * that promote specific deferred/project tools onto an agent's default
625
+ * declaration set.
626
+ *
284
627
  * @since SDK 1.2.0
285
628
  */
286
629
  listAppTools(opts?: {
287
630
  scope?: 'scene' | 'project';
631
+ includeDeferred?: boolean;
288
632
  }): Promise<PluginAppTool[]>;
289
633
  /**
290
634
  * Execute a host app tool by name. Delegates to the in-process
291
- * ToolRegistry — every mutation broadcasts to the UI automatically.
635
+ * ToolRegistry — every call (including this one) broadcasts to the
636
+ * UI's `mutations:tool-executed` channel so renderer state stays
637
+ * fresh whether the call mutates or is read-only. Read-only callers
638
+ * pay zero extra cost since the renderer debounces and skips
639
+ * redundant reloads.
292
640
  *
293
641
  * For scene-scoped tools tagged with `autoBindSceneId`, the host
294
642
  * overrides the caller's `sceneId` param with the currently-active
295
643
  * scene. That keeps a scene-bound caller from accidentally targeting
296
644
  * another scene.
297
645
  *
646
+ * `opts.provenance` (since SDK 2.18.0) stamps the originating actor onto
647
+ * every domain event this call emits — pass `'agent'` from autonomous
648
+ * agent loops so the UI orchestrator can gate auto-navigation, `'user'`
649
+ * when proxying a direct user gesture. Omitted = `'system'`.
650
+ *
298
651
  * @since SDK 1.2.0
299
652
  */
300
- executeAppTool(name: string, params: Record<string, unknown>): Promise<PluginAppToolResult>;
653
+ executeAppTool(name: string, params: Record<string, unknown>, opts?: {
654
+ provenance?: 'agent' | 'user';
655
+ }): Promise<PluginAppToolResult>;
656
+ /**
657
+ * Monotonic counter that increments on every state mutation
658
+ * (`broadcastMutation('tool-executed', ...)`). Use as a cache key for
659
+ * derived state that depends on the project: when the counter changes,
660
+ * something mutated; when it doesn't, your cache is still valid.
661
+ *
662
+ * Mostly aimed at performance-sensitive callers like ambient-context
663
+ * builders that want to skip re-querying state when nothing has
664
+ * changed. The counter is process-local — it resets on app restart
665
+ * and is not durable across sessions.
666
+ *
667
+ * Implementation detail: the counter is bumped by `mutation-broadcaster`
668
+ * before the broadcaster fires, so a synchronous `getMutationSeq()`
669
+ * call from inside a mutation listener will see the post-bump value.
670
+ *
671
+ * @since SDK 2.6.0
672
+ */
673
+ getMutationSeq(): number;
301
674
  /** Get available preset categories for a synth plugin. */
302
675
  getPresetCategories(pluginName: string): Promise<string[]>;
303
676
  /** Get a random preset from a category. */
@@ -310,6 +683,150 @@ interface PluginHost {
310
683
  getDataDirectory(): string;
311
684
  /** Persisted key-value settings store. */
312
685
  settings: PluginSettingsStore;
686
+ /**
687
+ * Return the absolute path to an installed sample pack's root directory,
688
+ * or `null` if the pack is missing OR its installed version doesn't match
689
+ * what the current app build expects.
690
+ *
691
+ * Plugins should treat `null` as "show the download CTA"; do NOT fall back
692
+ * to a hardcoded path. The host owns where samples live (currently
693
+ * `<userData>/samples/<installSubdir>/`).
694
+ *
695
+ * Stable packIds: `'sas-drum-pack'`, `'sas-instrument-pack'`. Both packs
696
+ * are downloaded on demand via the host's pack-download flow; see
697
+ * `host.isSamplePackCurrent` and the renderer-side `DownloadPackButton`.
698
+ *
699
+ * @since SDK 2.7.0
700
+ */
701
+ getSamplePackRoot(packId: string): Promise<string | null>;
702
+ /**
703
+ * True if the installed version of `packId` matches the version this app
704
+ * build expects. False if the pack is missing OR the installed version
705
+ * differs (older or newer).
706
+ *
707
+ * Plugins call this on activate to decide between rendering their normal
708
+ * UI vs the "Sample library not installed / Update available" CTA.
709
+ *
710
+ * @since SDK 2.7.0
711
+ */
712
+ isSamplePackCurrent(packId: string): Promise<boolean>;
713
+ /**
714
+ * Return the currently-installed version string for `packId` (e.g. `'1'`,
715
+ * `'2'`), or `null` if the pack is not installed at all. Reads the
716
+ * `_pack-version.json` marker inside the pack's install dir.
717
+ *
718
+ * Useful for distinguishing the "missing" CTA from the "stale, update
719
+ * available" CTA — plugins can call this when `isSamplePackCurrent`
720
+ * returns false to pick the right empty-state message.
721
+ *
722
+ * @since SDK 2.7.0
723
+ */
724
+ getSamplePackInstalledVersion(packId: string): Promise<string | null>;
725
+ /**
726
+ * Trigger a download + install of `packId` via the host's pack system (the
727
+ * same flow `getSamplePackRoot` / `isSamplePackCurrent` report on). Resolves
728
+ * when the install completes or fails. Plugins call this from a "download
729
+ * library" CTA instead of reaching into the app's IPC (`window.electronAPI`)
730
+ * directly.
731
+ *
732
+ * @since SDK 2.8.0
733
+ */
734
+ startSamplePackDownload(packId: string): Promise<{
735
+ success: boolean;
736
+ error?: string;
737
+ }>;
738
+ /**
739
+ * Subscribe to download/install progress for `packId`. Returns an unsubscribe
740
+ * fn. `status` mirrors the host's pack-download states (e.g. `'downloading' |
741
+ * 'extracting' | 'installing' | 'complete' | 'error'`); `progress` is 0-100.
742
+ *
743
+ * @since SDK 2.8.0
744
+ */
745
+ onSamplePackProgress(packId: string, listener: (progress: {
746
+ packId?: string;
747
+ status: string;
748
+ progress: number;
749
+ message?: string;
750
+ }) => void): UnsubscribeFn;
751
+ /**
752
+ * Return the canonical display metadata (`displayName`, `description`,
753
+ * `sizeBytes`) for `packId` from the host's pack registry — the SAME source
754
+ * the host uses to download + version-check the pack. A plugin's download CTA
755
+ * should prefer this over a hardcoded copy so the size/description stay in
756
+ * sync with whatever bundle the host actually ships (no per-version drift).
757
+ * Resolves `null` for an unknown packId.
758
+ *
759
+ * Optional so a plugin built against this SDK still runs on an older host:
760
+ * callers should fall back to their own static copy when it is absent or
761
+ * returns `null`.
762
+ *
763
+ * @since SDK 2.12.0
764
+ */
765
+ getSamplePackInfo?(packId: string): Promise<SamplePackPublicInfo | null>;
766
+ /**
767
+ * Per-pack roots of the USER's imported sample packs for `kind`. Each root
768
+ * is laid out exactly like the corresponding stock pack (drums:
769
+ * `<root>/<role>/<file>.wav` + `.txt` sidecars; instruments:
770
+ * `<root>/<category>/<id>/manifest.json`), so resolvers scan them as
771
+ * additional roots alongside `getSamplePackRoot`. `[]` when nothing is
772
+ * imported. User content lives under `<userData>/user-samples/` — strictly
773
+ * separate on disk; stock pack installs never touch it.
774
+ *
775
+ * Optional for older-host compat: feature-check
776
+ * (`host.getUserSampleRoots?.(...)`) and treat absence as `[]`.
777
+ *
778
+ * @since SDK 2.20.0
779
+ */
780
+ getUserSampleRoots?(kind: 'drums' | 'instruments'): Promise<string[]>;
781
+ /**
782
+ * Ask the host app to open its sample-import wizard targeting `kind`.
783
+ * Fire-and-forget; renderer-hosted plugins only (the wizard is an app-level
784
+ * modal — the main-process host no-ops). Library changes land as
785
+ * `onSamplePackProgress` events with packId `user:<kind>` and
786
+ * `status: 'complete'`, so subscribe to that to refresh.
787
+ *
788
+ * @since SDK 2.20.0
789
+ */
790
+ openSampleImportWizard?(kind: 'drums' | 'instruments'): void;
791
+ /**
792
+ * Start a deck playing the given scene/transition. Mirrors the workstation's
793
+ * transport play. `contentType` defaults to `'scene'`.
794
+ *
795
+ * @since SDK 2.9.0
796
+ */
797
+ deckPlay(deckId: string, contentId?: string, contentType?: 'scene' | 'transition'): Promise<{
798
+ success: boolean;
799
+ error?: string;
800
+ code?: string;
801
+ }>;
802
+ /**
803
+ * Stop a deck.
804
+ *
805
+ * @since SDK 2.9.0
806
+ */
807
+ deckStop(deckId: string): Promise<{
808
+ success: boolean;
809
+ error?: string;
810
+ }>;
811
+ /**
812
+ * Subscribe to per-deck state changes. Each event carries the `deckId`, the
813
+ * `property` that changed (e.g. `'playing'`), and its new `value`. Returns an
814
+ * unsubscribe fn.
815
+ *
816
+ * @since SDK 2.9.0
817
+ */
818
+ onDeckStateChanged(listener: (event: {
819
+ deckId: string;
820
+ property: string;
821
+ value: unknown;
822
+ }) => void): UnsubscribeFn;
823
+ /**
824
+ * Subscribe to the "all decks stopped" engine event (e.g. global transport
825
+ * stop). Returns an unsubscribe fn.
826
+ *
827
+ * @since SDK 2.9.0
828
+ */
829
+ onAllDecksStopped(listener: () => void): UnsubscribeFn;
313
830
  /** Get a value from scene-scoped plugin data. */
314
831
  getSceneData<T = unknown>(sceneId: string, key: string): Promise<T | null>;
315
832
  /** Set a value in scene-scoped plugin data. */
@@ -449,6 +966,18 @@ interface PluginHost {
449
966
  onComposeProgress(listener: ComposeProgressListener): UnsubscribeFn;
450
967
  /** Subscribe to engine ready events (fires when the engine finishes loading tracks after a scene change). */
451
968
  onEngineReady(listener: () => void): UnsubscribeFn;
969
+ /**
970
+ * Subscribe to external state mutations (CLI, MCP, or HTTP-API tool calls
971
+ * that bypass plugin-host methods). Fires after such a tool finishes,
972
+ * signalling that scene/track DB state may have changed underneath the
973
+ * plugin's local cache. Use it to refresh state that the plugin doesn't
974
+ * own — e.g. re-running adoptSceneTracks() so AI-created tracks become
975
+ * visible without requiring the user to switch scenes.
976
+ *
977
+ * Optional: only the renderer-side host implements this. Main-side
978
+ * plugins should subscribe to the typed domain-event bus instead.
979
+ */
980
+ onAfterAgentMutation?(listener: () => void): UnsubscribeFn;
452
981
  /** Audition a single note on a track (fire-and-forget preview). */
453
982
  auditionNote(trackId: string, pitch: number, velocity: number, durationMs: number): Promise<void>;
454
983
  /** Get presets for this plugin, optionally filtered by category. */
@@ -539,6 +1068,14 @@ interface PluginHost {
539
1068
  * @since SDK 2.2.0
540
1069
  */
541
1070
  getCurrentInputDevice(): Promise<AudioInputDevice | null>;
1071
+ /**
1072
+ * Subscribe to input-device changes (user picks a new mic in the
1073
+ * Audio Routing panel). Listeners should refetch via
1074
+ * `getCurrentInputDevice()`. Returns an unsubscribe fn.
1075
+ *
1076
+ * @since SDK 2.4.0
1077
+ */
1078
+ onInputDeviceChange(listener: () => void): UnsubscribeFn;
542
1079
  /**
543
1080
  * Get the platform's mic-to-output round-trip latency offset in
544
1081
  * samples. 0 = uncalibrated. Plugins recording audio apply this via
@@ -623,6 +1160,105 @@ interface PluginTrackHandle {
623
1160
  /** Custom instrument display name (null = Surge XT) */
624
1161
  instrumentName?: string | null;
625
1162
  }
1163
+ /**
1164
+ * One source track offered by `listImportableTracks`, already filtered to the
1165
+ * calling panel's type. The host computes the gate; the UI only renders it.
1166
+ * @since SDK 2.13.0
1167
+ */
1168
+ interface ImportCandidateTrack {
1169
+ /** Source track's engine track id (the selector passed back to importTrack). */
1170
+ trackId: string;
1171
+ /** Source track's DB row id (globally unique; good React key). */
1172
+ dbId: string;
1173
+ /** Display name shown in the modal row. */
1174
+ name: string;
1175
+ /** Musical role if set (drives the row icon). */
1176
+ role?: string;
1177
+ /** True when this track can be copied into the active scene as-is. */
1178
+ importable: boolean;
1179
+ /** Why the track is disabled (shown as a tooltip). Present iff `!importable`. */
1180
+ disabledReason?: string;
1181
+ }
1182
+ /**
1183
+ * One track in a specific scene, returned by `host.listSceneFamilyTracks`,
1184
+ * already narrowed to the calling panel's family. Unlike `ImportCandidateTrack`
1185
+ * it carries NO import gate — the crossfade picker lists every same-family track
1186
+ * in the origin/target scene regardless of key/length. @since SDK 2.22.0
1187
+ */
1188
+ interface SceneFamilyTrack {
1189
+ /** Track's DB row id — the selector for getTrackSound + crossfade metadata. */
1190
+ dbId: string;
1191
+ /** Display name shown in the picker. */
1192
+ name: string;
1193
+ /** Musical role if set — used to enforce same-role crossfade pairing. */
1194
+ role?: string;
1195
+ }
1196
+ /**
1197
+ * One OTHER scene and its candidate tracks (already type-filtered). Scenes with
1198
+ * zero candidates of the panel's type are omitted by the host.
1199
+ * @since SDK 2.13.0
1200
+ */
1201
+ interface ImportCandidateScene {
1202
+ /** Source scene's engine scene id. */
1203
+ sceneId: string;
1204
+ /** Source scene's display name. */
1205
+ sceneName: string;
1206
+ /** Candidate tracks of this panel's type (may include disabled ones). */
1207
+ tracks: ImportCandidateTrack[];
1208
+ /**
1209
+ * True for the synthetic "this scene — other panels" entry: the ACTIVE
1210
+ * scene's MIDI tracks owned by OTHER panels. Importing one re-sounds the part
1211
+ * on the calling panel's instrument (via `readImportableTrackMidi` +
1212
+ * `writeMidiClip`) rather than faithfully copying it. Absent/false for
1213
+ * ordinary cross-scene entries. @since SDK 2.20.0
1214
+ */
1215
+ sameScene?: boolean;
1216
+ }
1217
+ /**
1218
+ * A source track's current sound, as returned by `host.getTrackSound`. The
1219
+ * discriminant matches the panel that reads it: drums → 'sample', instruments →
1220
+ * 'instrument', synths → 'preset'. `label` is the human name for the History row.
1221
+ * @since SDK 2.14.0
1222
+ */
1223
+ /**
1224
+ * How a synth `state` blob is serialized. `valuetree` is Tracktion's wrapped
1225
+ * format (default Surge XT presets); `raw` is the plugin's own
1226
+ * getStateInformation format (third-party instruments). Absent ⇒ `valuetree`,
1227
+ * for backward compatibility with history recorded before SDK 2.15.0.
1228
+ * @since SDK 2.15.0
1229
+ */
1230
+ type SynthStateType = 'raw' | 'valuetree';
1231
+ type TrackSoundSnapshot = {
1232
+ kind: 'sample';
1233
+ samplePath: string;
1234
+ label: string;
1235
+ } | {
1236
+ kind: 'instrument';
1237
+ displayName: string;
1238
+ instrumentId: string | null;
1239
+ zones: InstrumentZone[];
1240
+ label: string;
1241
+ } | {
1242
+ kind: 'preset';
1243
+ state: string;
1244
+ label: string;
1245
+ stateType?: SynthStateType;
1246
+ };
1247
+ /** Options for `PluginHost.listImportableTracks`. @since SDK 2.13.0 */
1248
+ interface ListImportableTracksOptions {
1249
+ /**
1250
+ * Coarse content family. 'midi' = synth/drum/instrument, 'audio' = stems,
1251
+ * 'sample' = loops. Defaults are derived from the calling plugin id, so
1252
+ * panels normally pass nothing.
1253
+ */
1254
+ family?: 'midi' | 'audio' | 'sample';
1255
+ /**
1256
+ * When true, prepend the active scene's MIDI tracks owned by OTHER panels as a
1257
+ * `sameScene` entry (the cross-panel re-sound source). Off by default so the
1258
+ * plain cross-scene import is unchanged. MIDI panels only. @since SDK 2.20.0
1259
+ */
1260
+ includeSameScene?: boolean;
1261
+ }
626
1262
  interface PluginTrackInfo extends PluginTrackHandle {
627
1263
  /** Is track muted? */
628
1264
  muted: boolean;
@@ -691,6 +1327,29 @@ interface MidiWriteResult {
691
1327
  /** Actual bars covered */
692
1328
  bars: number;
693
1329
  }
1330
+ /**
1331
+ * One clip returned by {@link PluginHost.readMidiNotes}. `endTime - startTime`
1332
+ * (seconds) is the clip's loop span; round-trip it back into
1333
+ * {@link MidiClipData} on save so an edit never changes the clip length.
1334
+ * @since SDK 2.15.0
1335
+ */
1336
+ interface ReadMidiClip {
1337
+ /** Clip start in seconds (engine timeline). */
1338
+ startTime: number;
1339
+ /** Clip end in seconds. Loop span = endTime - startTime. */
1340
+ endTime: number;
1341
+ /** Beat-based notes, identical shape to {@link MidiClipData.notes}. */
1342
+ notes: PluginMidiNote[];
1343
+ }
1344
+ /**
1345
+ * Result of {@link PluginHost.readMidiNotes}: every clip on the track. Drum /
1346
+ * instrument / synth tracks are single-clip, so callers normally use
1347
+ * `clips[0]`; the array form mirrors the engine and is future-proof.
1348
+ * @since SDK 2.15.0
1349
+ */
1350
+ interface ReadMidiResult {
1351
+ clips: ReadMidiClip[];
1352
+ }
694
1353
  /**
695
1354
  * Options for {@link PluginHost.exportTracksAsMidiBundle}.
696
1355
  * @since SDK 1.1.0
@@ -805,6 +1464,15 @@ interface MusicalContext {
805
1464
  genre: string | null;
806
1465
  timeSignature: string;
807
1466
  chordProgression: PluginChordTiming[];
1467
+ /**
1468
+ * The scene's natural-language contract prompt (e.g. "dark psytrance,
1469
+ * driving 130 BPM, claustrophobic"). Null when the scene has no
1470
+ * contract set yet. Auto-prefixed to the LLM by `host.generateWithLLM`
1471
+ * so every per-track generation sees the scene-level intent without
1472
+ * each plugin having to plumb it through manually.
1473
+ * @since SDK 1.2.0
1474
+ */
1475
+ contractPrompt: string | null;
808
1476
  }
809
1477
  interface PluginChordTiming {
810
1478
  /** Chord symbol: 'Cm7', 'G', 'Fmaj7', etc. */
@@ -825,6 +1493,15 @@ interface PluginGenerationContext {
825
1493
  genre: string | null;
826
1494
  };
827
1495
  concurrentTracks: PluginConcurrentTrackInfo[];
1496
+ /**
1497
+ * Count of tracks the host had to drop entirely from `concurrentTracks`
1498
+ * because their notes pushed the running total past the cross-track
1499
+ * budget. Panels should disclose this to the LLM (e.g. "… N additional
1500
+ * tracks omitted to fit token budget") so the model knows it is
1501
+ * working with partial context.
1502
+ * @since SDK 1.2.0
1503
+ */
1504
+ truncatedTrackCount?: number;
828
1505
  }
829
1506
  interface PluginConcurrentTrackInfo {
830
1507
  trackId: string;
@@ -832,6 +1509,22 @@ interface PluginConcurrentTrackInfo {
832
1509
  presetCategory: string | null;
833
1510
  /** Notes organized by which chord they fall under */
834
1511
  notesByChord: PluginChordSegment[];
1512
+ /**
1513
+ * The user-typed prompt that produced this track's MIDI (from
1514
+ * `tracks.prompt`). Lets the LLM see *intent* alongside the notes —
1515
+ * "punchy 909 kick" carries more meaning than the kick MIDI alone.
1516
+ * @since SDK 1.2.0
1517
+ */
1518
+ prompt?: string;
1519
+ /**
1520
+ * True when the host capped this track's notes (per-track budget).
1521
+ * The `notesByChord` payload is a prefix of the real content; the
1522
+ * total dropped count is `originalNoteCount - sum(notesByChord.notes.length)`.
1523
+ * @since SDK 1.2.0
1524
+ */
1525
+ truncated?: boolean;
1526
+ /** The track's full note count before per-track truncation. */
1527
+ originalNoteCount?: number;
835
1528
  }
836
1529
  interface PluginChordSegment {
837
1530
  chord: string;
@@ -871,6 +1564,20 @@ interface PluginTransportState {
871
1564
  position: number;
872
1565
  timeSignature: string;
873
1566
  }
1567
+ /**
1568
+ * Mono peak level for a single track, as reported by `getTrackLevels()`.
1569
+ * Drives the cosmetic per-track strip meters. `peakDb` is the max of the
1570
+ * L/R channels, floored at -120 (the "no signal" sentinel).
1571
+ * @since SDK 2.21.0
1572
+ */
1573
+ interface PluginTrackLevel {
1574
+ /** Tracktion engine track id — matches `PluginTrackHandle.id`. */
1575
+ trackId: string;
1576
+ /** Mono peak in dBFS (max of L/R), floored at -120. */
1577
+ peakDb: number;
1578
+ /** Latched overload since the last poll. */
1579
+ clipped: boolean;
1580
+ }
874
1581
  interface PluginSceneInfo {
875
1582
  id: string;
876
1583
  name: string;
@@ -899,6 +1606,17 @@ interface PluginSceneContext {
899
1606
  hasTracks: boolean;
900
1607
  /** Whether bulk generation is currently in progress */
901
1608
  isBulkGenerating: boolean;
1609
+ /**
1610
+ * Scene kind. A 'transition' scene bridges two other scenes (the
1611
+ * transition-as-scene feature) and unlocks the crossfade-track UI in the
1612
+ * instrument panels; ordinary scenes are 'scene'. Absent on older hosts.
1613
+ * @since SDK 2.22.0
1614
+ */
1615
+ sceneType?: 'scene' | 'transition';
1616
+ /** For a transition scene, the DB id of the scene it bridges FROM (origin). Null otherwise. @since SDK 2.22.0 */
1617
+ transitionFromSceneId?: string | null;
1618
+ /** For a transition scene, the DB id of the scene it bridges TO (target). Null otherwise. @since SDK 2.22.0 */
1619
+ transitionToSceneId?: string | null;
902
1620
  }
903
1621
  /** Placeholder track state for the progressive bulk-add UX */
904
1622
  interface BulkAddPlaceholderTrack {
@@ -937,6 +1655,90 @@ interface LLMGenerationResult {
937
1655
  /** Model that generated the response */
938
1656
  model: string;
939
1657
  }
1658
+ /** A single part of a Gemini-style content block. */
1659
+ interface LLMPart {
1660
+ /** Plain text. Mutually exclusive with functionCall / functionResponse. */
1661
+ text?: string;
1662
+ /** A tool/function the model is asking the host to invoke. */
1663
+ functionCall?: {
1664
+ name: string;
1665
+ args: Record<string, unknown>;
1666
+ /**
1667
+ * Opaque signature returned by Gemini 3+ tool-use models. Must be echoed
1668
+ * verbatim when the assistant turn is replayed on a later iteration, or
1669
+ * the API rejects the request with a 400 ("Function call is missing a
1670
+ * thought_signature in functionCall parts."). Pre-Gemini-3 models leave
1671
+ * this undefined; preserving it round-trip is safe across families.
1672
+ */
1673
+ thoughtSignature?: string;
1674
+ };
1675
+ /** The result of a tool call, fed back into the loop on the next turn. */
1676
+ functionResponse?: {
1677
+ name: string;
1678
+ response: Record<string, unknown>;
1679
+ };
1680
+ }
1681
+ interface LLMContent {
1682
+ /** 'user' = user/tool-result; 'model' = assistant. */
1683
+ role: 'user' | 'model';
1684
+ parts: LLMPart[];
1685
+ }
1686
+ interface LLMFunctionDeclaration {
1687
+ name: string;
1688
+ description: string;
1689
+ /** JSON Schema. Use `type: 'object'` with `properties` for any tool. */
1690
+ parameters: {
1691
+ type: 'object';
1692
+ properties?: Record<string, unknown>;
1693
+ required?: string[];
1694
+ };
1695
+ }
1696
+ interface LLMTool {
1697
+ functionDeclarations: LLMFunctionDeclaration[];
1698
+ }
1699
+ interface LLMGenerationConfig {
1700
+ temperature?: number;
1701
+ topP?: number;
1702
+ topK?: number;
1703
+ maxOutputTokens?: number;
1704
+ }
1705
+ interface LLMSystemInstruction {
1706
+ parts: {
1707
+ text: string;
1708
+ }[];
1709
+ }
1710
+ interface LLMToolUseRequest {
1711
+ /** Gemini model id (e.g. 'gemini-2.5-flash'). */
1712
+ model: string;
1713
+ /** Conversation so far, including any tool-result turns. */
1714
+ contents: LLMContent[];
1715
+ /** System prompt as Gemini-native systemInstruction. */
1716
+ systemInstruction?: LLMSystemInstruction;
1717
+ /** Tool declarations the model may call. */
1718
+ tools?: LLMTool[];
1719
+ /** Optional tool-call mode override. */
1720
+ toolConfig?: {
1721
+ functionCallingConfig?: {
1722
+ mode?: 'AUTO' | 'ANY' | 'NONE';
1723
+ allowedFunctionNames?: string[];
1724
+ };
1725
+ };
1726
+ generationConfig?: LLMGenerationConfig;
1727
+ }
1728
+ interface LLMUsageMetadata {
1729
+ promptTokenCount: number;
1730
+ candidatesTokenCount: number;
1731
+ totalTokenCount: number;
1732
+ }
1733
+ interface LLMCandidate {
1734
+ content: LLMContent;
1735
+ finishReason?: string;
1736
+ index?: number;
1737
+ }
1738
+ interface LLMToolUseResponse {
1739
+ candidates: LLMCandidate[];
1740
+ usageMetadata?: LLMUsageMetadata;
1741
+ }
940
1742
  interface PluginPresetData {
941
1743
  name: string;
942
1744
  category: string;
@@ -948,6 +1750,23 @@ interface ShufflePresetResult {
948
1750
  presetName: string;
949
1751
  presetCategory: string;
950
1752
  }
1753
+ /**
1754
+ * One entry in a track's in-session "sound history" — the data behind the
1755
+ * TrackRow ↩ back-arrow and the drawer "History" tab (see `useSoundHistory`).
1756
+ *
1757
+ * `descriptor` is opaque to the SDK: each generator plugin defines its own shape
1758
+ * (a drum sample path string, an instrument `{ displayName, zones }`, a synth
1759
+ * `{ pluginIndex, stateBase64 }`) and is the value handed back to the plugin's
1760
+ * `applySound` callback to re-apply the sound.
1761
+ */
1762
+ interface SoundHistoryEntry {
1763
+ /** Human-readable label shown in the History list (filename, preset/instrument name). */
1764
+ label: string;
1765
+ /** Opaque, plugin-defined value used to re-apply this sound. */
1766
+ descriptor: unknown;
1767
+ /** User-starred. Favorited entries are never auto-evicted by the history cap. */
1768
+ favorite?: boolean;
1769
+ }
951
1770
  interface PluginSettingsSchema {
952
1771
  type: 'object';
953
1772
  properties: Record<string, SettingDefinition>;
@@ -1170,7 +1989,7 @@ interface PluginAudioTextureResult {
1170
1989
  */
1171
1990
  cuePoints: PluginCuePoints | null;
1172
1991
  /**
1173
- * Path to the un-trimmed (raw) Lyria output. Used by the audio-texture
1992
+ * Path to the un-trimmed (raw) Lyria output. Used by the stems
1174
1993
  * trim editor to draw the full waveform. Persist via
1175
1994
  * `host.setRawAudioFilePath`. Null when no raw file is available.
1176
1995
  */
@@ -1187,7 +2006,7 @@ interface PluginAudioTextureResult {
1187
2006
  * Cue-points sidecar surfaced by the audio-processor `trim` command —
1188
2007
  * sample positions for each detected beat inside the generated WAV.
1189
2008
  * Mirrors the canonical `CuePoints` shape from the assistant; duplicated
1190
- * here so external plugins don't reach into sas-assistant internals.
2009
+ * here so external plugins don't reach into sas-app internals.
1191
2010
  */
1192
2011
  interface PluginCuePoints {
1193
2012
  /** Schema version (currently 1). */
@@ -1266,6 +2085,14 @@ interface PluginAppTool {
1266
2085
  inputSchema: PluginAppToolInputSchema;
1267
2086
  /** `'scene'` = safe for scene-scoped callers. `'project'` = cross-scene. */
1268
2087
  scope?: 'scene' | 'project';
2088
+ /**
2089
+ * `true` = the operation cannot be undone via the host's checkpoint/undo
2090
+ * system (project delete, disk overwrite, external export, …). The host
2091
+ * gates such calls behind a user-approval flow when invoked with agent
2092
+ * provenance; agent UIs may also surface the flag (e.g. ⚠ in a tool list).
2093
+ * @since SDK 2.18.0
2094
+ */
2095
+ irreversible?: boolean;
1269
2096
  }
1270
2097
  /** Result shape returned by `PluginHost.executeAppTool`. */
1271
2098
  interface PluginAppToolResult {
@@ -1377,26 +2204,237 @@ interface FxPresetDataEntry {
1377
2204
  type FxPresetData = Partial<Record<FxCategory, FxPresetDataEntry>>;
1378
2205
 
1379
2206
  /**
1380
- * SDK TrackRow Reusable track row component for generator plugins.
2207
+ * TrackDrawerthe unified per-track drawer body.
1381
2208
  *
1382
- * Renders a complete track UI with prompt input, generation controls,
1383
- * shuffle/copy, volume/pan, mute/solo, FX drawer, and visual states
1384
- * (amber pulse for "needs generation", progress overlay, error indicator).
2209
+ * ONE drawer with a flat contextual tab strip. Which tabs appear is computed
2210
+ * from which callbacks the host panel provides:
2211
+ * - FX (onFxToggle) — the 6-category FX toggle bar
2212
+ * - Pick (onSelect) — instrument-plugin picker (+ native editor stage)
2213
+ * - History (onRestoreSound) — sounds this track has had (restore / favorite)
2214
+ * - Import (onImportSound) — copy a sound from a matching track in another scene
1385
2215
  *
1386
- * Layout matches TrackInput (main branch) for visual parity.
2216
+ * The active tab is CONTROLLED by the host (activeTab / onTabChange) so the
2217
+ * track row's FX button and ▾ button can open the SAME drawer to a chosen tab.
2218
+ * When only one tab is enabled (e.g. loops = FX only) the strip is hidden and
2219
+ * that single view renders directly.
1387
2220
  *
1388
- * Depends only on PluginHost types + existing shared renderer components.
2221
+ * (Was `InstrumentDrawer` renamed once it grew an FX tab + Import tab. A
2222
+ * `TrackDrawer as InstrumentDrawer` alias is exported from the barrel for
2223
+ * backwards compatibility.)
1389
2224
  */
1390
2225
 
1391
- interface SDKTrackRowProps {
1392
- /** Track identity */
1393
- track: {
1394
- id: string;
1395
- name: string;
1396
- role?: string;
1397
- };
1398
- /** Current prompt text (optional — omit when using contentSlot) */
1399
- prompt?: string;
2226
+ /** The contextual tabs a track drawer can show, in display order. */
2227
+ type DrawerTab = 'fx' | 'pick' | 'history' | 'import' | 'edit';
2228
+ interface TrackDrawerProps {
2229
+ /** Which tab is active (controlled by the host TrackRow). */
2230
+ activeTab: DrawerTab;
2231
+ /** Switch tabs (strip clicks). */
2232
+ onTabChange?: (tab: DrawerTab) => void;
2233
+ trackId: string;
2234
+ fxState: TrackFxDetailState;
2235
+ onFxToggle?: (category: FxCategory, enabled: boolean) => void;
2236
+ onFxPresetChange?: (category: FxCategory, presetIndex: number) => void;
2237
+ onFxDryWetChange?: (category: FxCategory, value: number) => void;
2238
+ /** Disable FX controls (e.g. while the track is generating). */
2239
+ fxDisabled?: boolean;
2240
+ /** Available instrument plugins from engine scan. */
2241
+ instruments?: InstrumentDescriptor[];
2242
+ /** Currently loaded instrument plugin ID (null = default Surge XT). */
2243
+ currentPluginId?: string | null;
2244
+ /** Whether the instrument scan is still in progress. */
2245
+ isLoading?: boolean;
2246
+ /** Called when user selects an instrument (presence enables the Pick tab). */
2247
+ onSelect?: (pluginId: string) => void;
2248
+ /** Re-scan plugins. */
2249
+ onRefresh?: () => void;
2250
+ /** Pick-tab sub-view: show the native plugin editor instead of the grid. */
2251
+ editorStage?: boolean;
2252
+ /** Called when user clicks "Open Plugin Editor". */
2253
+ onShowEditor?: () => void;
2254
+ /** Called when user goes back from the editor to the instrument grid. */
2255
+ onBackToInstruments?: () => void;
2256
+ /** Name of the selected instrument (shown in the editor header). */
2257
+ selectedInstrumentName?: string | null;
2258
+ soundHistory?: readonly SoundHistoryEntry[];
2259
+ soundHistoryCursor?: number;
2260
+ /** Restore a sound by index; presence enables the History tab. */
2261
+ onRestoreSound?: (index: number) => void;
2262
+ /** Toggle the favorite (⭐) flag on a history entry; omit to hide the star. */
2263
+ onToggleFavorite?: (index: number) => void;
2264
+ /** Open the sound-import picker; presence enables the Import tab. */
2265
+ onImportSound?: () => void;
2266
+ /** Button label, e.g. "Import Sample" (drums/instruments) or "Import Preset" (synths). */
2267
+ importSoundLabel?: string;
2268
+ /** Current MIDI notes for the piano-roll editor. */
2269
+ editNotes?: readonly PluginMidiNote[];
2270
+ /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */
2271
+ onNotesChange?: (notes: PluginMidiNote[]) => void;
2272
+ /** Scene length in bars (piano-roll grid width). Default 4. */
2273
+ editBars?: number;
2274
+ /** Scene BPM (piano-roll audition timing). Default 120. */
2275
+ editBpm?: number;
2276
+ /** Snap step in quarter notes for the piano roll (default 0.25). */
2277
+ editSnap?: number;
2278
+ /** Optional single-note preview when the user adds a note. */
2279
+ onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;
2280
+ }
2281
+ 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;
2282
+
2283
+ /**
2284
+ * useTrackLevels — drives the cosmetic per-track strip meters.
2285
+ *
2286
+ * The hard constraint for this feature is "playback ALWAYS wins over the GUI;
2287
+ * NO blocking threads." This hook is built around that:
2288
+ *
2289
+ * - It polls `host.getTrackLevels()` at ~30Hz with a recursive setTimeout that
2290
+ * only schedules the NEXT tick AFTER the previous await resolves. That is
2291
+ * automatic backpressure: a slow/stalled engine simply slows the meter, it
2292
+ * can never queue a backlog of requests. (The host + bridge also coalesce,
2293
+ * so a busy engine yields a STALE snapshot, never a pile-up.)
2294
+ * - It writes into a ref-held Map and notifies row subscribers, so the OWNING
2295
+ * panel never re-renders at 30Hz. Each row reads its own value via
2296
+ * `useTrackLevel` and re-renders only itself.
2297
+ * - It polls while the panel is mounted and the window is visible, and pauses
2298
+ * when the window is hidden. It deliberately does NOT gate on transport
2299
+ * "is playing": this app drives playback through decks / the clip launcher,
2300
+ * and the linear-transport play flag does not track that reliably. When
2301
+ * audio is stopped the engine simply returns floor levels, so the bars are
2302
+ * empty anyway — no need (and no reliable signal) to stop polling.
2303
+ *
2304
+ * Usage (panel):
2305
+ * const levels = useTrackLevels(host);
2306
+ * ...<TrackRow levels={levels} ... /> // row calls useTrackLevel(levels, id)
2307
+ */
2308
+
2309
+ /**
2310
+ * Stable handle returned by {@link useTrackLevels}. Rows read their own level
2311
+ * and subscribe to per-tick notifications through it; its identity is stable
2312
+ * across renders so a row's subscription is set up once.
2313
+ */
2314
+ interface TrackLevelsHandle {
2315
+ /** Current level for a track, or null when idle/absent (renders an empty bar). */
2316
+ getLevel(trackId: string): PluginTrackLevel | null;
2317
+ /** Subscribe to per-tick updates. Returns an unsubscribe function. */
2318
+ subscribe(listener: () => void): () => void;
2319
+ }
2320
+ /**
2321
+ * Poll every owned track's level while mounted + visible. Returns a stable
2322
+ * handle; the owning component does NOT re-render per tick. Pass `enabled =
2323
+ * false` to turn it off entirely (e.g. a panel that wants no meters). Safe to
2324
+ * call even when the host predates `getTrackLevels` (older SDK) — it stays idle.
2325
+ */
2326
+ declare function useTrackLevels(host: PluginHost | null | undefined, enabled?: boolean): TrackLevelsHandle;
2327
+ /**
2328
+ * Per-row selector. Subscribes to the shared scheduler and re-renders ONLY the
2329
+ * calling component when this track's level changes. Returns null when idle
2330
+ * (transport stopped, window hidden, or the track has no meter yet).
2331
+ */
2332
+ declare function useTrackLevel(handle: TrackLevelsHandle | null | undefined, trackId: string): PluginTrackLevel | null;
2333
+ /**
2334
+ * Per-row meter view-model: the current level plus a held peak for the meter UI.
2335
+ */
2336
+ interface TrackMeterView {
2337
+ /** Current mono peak in dBFS (floored at -120). */
2338
+ peakDb: number;
2339
+ /** Held peak in dBFS — stays at the recent maximum for ~PEAK_HOLD_MS, then falls. */
2340
+ peakHoldDb: number;
2341
+ /** Latched clip flag for the last poll window. */
2342
+ clipped: boolean;
2343
+ /** True when the track currently has a live meter row. */
2344
+ active: boolean;
2345
+ }
2346
+ /**
2347
+ * Per-row meter selector WITH PEAK-HOLD. Like {@link useTrackLevel} it subscribes
2348
+ * to the shared ~30Hz scheduler and re-renders only the calling component, but it
2349
+ * also tracks a held peak that stays at the recent maximum for ~PEAK_HOLD_MS then
2350
+ * decays — so the eye can register where the signal peaked while the bar itself
2351
+ * moves fast. No extra timers or rAF: the held value is recomputed on each
2352
+ * scheduler notify, using performance.now() for hold/decay timing.
2353
+ */
2354
+ declare function useTrackMeter(handle: TrackLevelsHandle | null | undefined, trackId: string): TrackMeterView;
2355
+ /**
2356
+ * Track the transport's play/stop state for a plugin. Seeds from
2357
+ * `getTransportState()` and follows `onTransportEvent`. Use its result as the
2358
+ * `active` arg to {@link useTrackLevels} so meters animate only during playback.
2359
+ */
2360
+ declare function useTransportPlaying(host: PluginHost | null | undefined): boolean;
2361
+
2362
+ /**
2363
+ * Props the reorder machinery hands to a single row. Spread `handleProps` on the
2364
+ * drag grip and `rowProps` on the row's outer element; `isDragging` /
2365
+ * `isDragTarget` drive the visual state.
2366
+ */
2367
+ interface TrackRowDragProps {
2368
+ handleProps: {
2369
+ draggable: true;
2370
+ onDragStart: (e: DragEvent<HTMLElement>) => void;
2371
+ onDragEnd: (e: DragEvent<HTMLElement>) => void;
2372
+ };
2373
+ rowProps: {
2374
+ onDragEnter: (e: DragEvent<HTMLElement>) => void;
2375
+ onDragOver: (e: DragEvent<HTMLElement>) => void;
2376
+ onDragLeave: (e: DragEvent<HTMLElement>) => void;
2377
+ onDrop: (e: DragEvent<HTMLElement>) => void;
2378
+ };
2379
+ /** This row is the one currently being dragged (dim it). */
2380
+ isDragging: boolean;
2381
+ /** This row is the current drop target (show an insertion accent). */
2382
+ isDragTarget: boolean;
2383
+ }
2384
+ /**
2385
+ * Pure helper: return a NEW array with the item at `from` moved to `to`.
2386
+ * Out-of-range or no-op moves return a shallow copy unchanged. Exported for
2387
+ * unit testing the index math without a DOM.
2388
+ */
2389
+ declare function moveItem<T>(arr: readonly T[], from: number, to: number): T[];
2390
+ interface UseTrackReorderOptions<T> {
2391
+ /** Host (only {@link PluginHost.reorderTracks} is used). */
2392
+ host: Pick<PluginHost, 'reorderTracks'>;
2393
+ /** The panel's current track array (also the render order). */
2394
+ items: T[];
2395
+ /** The panel's state setter for `items` (used for optimistic update + revert). */
2396
+ setItems: Dispatch<SetStateAction<T[]>>;
2397
+ /** Stable id for persistence — use the track's dbId, not its engine id. */
2398
+ getId: (item: T) => string;
2399
+ /** Called if persistence fails, after the optimistic update is reverted. */
2400
+ onError?: (err: unknown) => void;
2401
+ }
2402
+ interface UseTrackReorderResult {
2403
+ /** Build the drag props for the row at `index`; spread onto its TrackRow. */
2404
+ dragPropsFor: (index: number) => TrackRowDragProps;
2405
+ /** Index of the row being dragged, or null. */
2406
+ draggingIndex: number | null;
2407
+ /** Index of the current drop-target row, or null. */
2408
+ dragOverIndex: number | null;
2409
+ }
2410
+ /**
2411
+ * Drag-and-drop reordering for a panel's track list. Dropping a row onto another
2412
+ * row moves it into that row's position (everything between shifts); the top and
2413
+ * bottom are reachable by dropping on the first/last row.
2414
+ */
2415
+ declare function useTrackReorder<T>({ host, items, setItems, getId, onError, }: UseTrackReorderOptions<T>): UseTrackReorderResult;
2416
+
2417
+ /**
2418
+ * SDK TrackRow — Reusable track row component for generator plugins.
2419
+ *
2420
+ * Renders a complete track UI with prompt input, generation controls,
2421
+ * shuffle/copy, volume/pan, mute/solo, FX drawer, and visual states
2422
+ * (amber pulse for "needs generation", progress overlay, error indicator).
2423
+ *
2424
+ * Layout matches TrackInput (main branch) for visual parity.
2425
+ *
2426
+ * Depends only on PluginHost types + existing shared renderer components.
2427
+ */
2428
+
2429
+ interface SDKTrackRowProps {
2430
+ /** Track identity */
2431
+ track: {
2432
+ id: string;
2433
+ name: string;
2434
+ role?: string;
2435
+ };
2436
+ /** Current prompt text (optional — omit when using contentSlot) */
2437
+ prompt?: string;
1400
2438
  /** Playback state */
1401
2439
  runtimeState: {
1402
2440
  muted: boolean;
@@ -1404,10 +2442,19 @@ interface SDKTrackRowProps {
1404
2442
  volume: number;
1405
2443
  pan: number;
1406
2444
  };
2445
+ /** True when ANOTHER track is soloed, so this (non-soloed) track is currently
2446
+ * silenced. Renders the row dimmed while leaving its Mute button UNLIT — the
2447
+ * engine's effective-mute model silences it without touching user-mute. Purely
2448
+ * visual; does not change mute/solo state. */
2449
+ soloedOut?: boolean;
1407
2450
  /** FX category states */
1408
2451
  fxDetailState: TrackFxDetailState;
1409
- /** FX panel visibility */
1410
- fxDrawerOpen: boolean;
2452
+ /** Whether the unified track drawer is open. */
2453
+ drawerOpen: boolean;
2454
+ /** Which tab the drawer is showing. */
2455
+ drawerTab: DrawerTab;
2456
+ /** Switch the active drawer tab (tab-strip clicks). Omit for single-tab panels (e.g. loops = FX only). */
2457
+ onTabChange?: (tab: DrawerTab) => void;
1411
2458
  /** Generation in progress */
1412
2459
  isGenerating?: boolean;
1413
2460
  /** Auth state */
@@ -1428,8 +2475,9 @@ interface SDKTrackRowProps {
1428
2475
  onShuffle?: () => void;
1429
2476
  /** Duplicate track (optional — omit to hide Copy button) */
1430
2477
  onCopy?: () => void;
1431
- /** Delete track */
1432
- onDelete: () => void;
2478
+ /** Delete track. Optional — omit to hide the delete button (e.g. a composite
2479
+ * like CrossfadeTrackRow owns a single delete for the whole pair). */
2480
+ onDelete?: () => void;
1433
2481
  /** Custom content replacing the prompt input (e.g., sample info display) */
1434
2482
  contentSlot?: React.ReactNode;
1435
2483
  /** Toggle mute */
@@ -1456,10 +2504,8 @@ interface SDKTrackRowProps {
1456
2504
  instrumentName?: string | null;
1457
2505
  /** Whether the current instrument plugin is missing from the system */
1458
2506
  instrumentMissing?: boolean;
1459
- /** Whether the instrument drawer is open */
1460
- instrumentDrawerOpen?: boolean;
1461
- /** Toggle the instrument drawer */
1462
- onToggleInstrumentDrawer?: () => void;
2507
+ /** Open/close the drawer to a non-FX tab (the ▾ button). Omit to hide it. */
2508
+ onToggleDrawer?: () => void;
1463
2509
  /** Available instrument plugins for the drawer */
1464
2510
  availableInstruments?: InstrumentDescriptor[];
1465
2511
  /** Currently loaded instrument plugin ID */
@@ -1470,43 +2516,538 @@ interface SDKTrackRowProps {
1470
2516
  instrumentsLoading?: boolean;
1471
2517
  /** Re-scan for instruments */
1472
2518
  onRefreshInstruments?: () => void;
1473
- /** Which stage the instrument drawer is in */
1474
- instrumentDrawerStage?: 'instruments' | 'editor';
2519
+ /** Pick-tab sub-view: native plugin editor instead of the instrument grid. */
2520
+ editorStage?: boolean;
1475
2521
  /** Called when user clicks "Open Editor" */
1476
2522
  onShowEditor?: () => void;
1477
2523
  /** Called when user wants to go back from editor view */
1478
2524
  onBackToInstruments?: () => void;
2525
+ /** Ordered list of sounds this track has had this session. */
2526
+ soundHistory?: readonly SoundHistoryEntry[];
2527
+ /** Index into soundHistory of the currently-applied sound. */
2528
+ soundHistoryCursor?: number;
2529
+ /** Restore a sound from the History tab by index. */
2530
+ onRestoreSound?: (index: number) => void;
2531
+ /** Toggle the favorite (⭐) flag on a history entry. */
2532
+ onToggleFavorite?: (index: number) => void;
2533
+ /** Open the drawer's sound-import picker; omit to hide the button. */
2534
+ onImportSound?: () => void;
2535
+ /** Sound-import button label ("Import Sample" / "Import Preset"). */
2536
+ importSoundLabel?: string;
2537
+ /** Current MIDI notes for the piano-roll editor (the 'edit' tab). */
2538
+ editNotes?: readonly PluginMidiNote[];
2539
+ /** Persist edited notes; PRESENCE of this callback enables the Edit tab. */
2540
+ onNotesChange?: (notes: PluginMidiNote[]) => void;
2541
+ /** Scene length in bars (piano-roll grid width). */
2542
+ editBars?: number;
2543
+ /** Scene BPM (piano-roll audition timing). */
2544
+ editBpm?: number;
2545
+ /** Snap step in quarter notes for the piano roll (default 0.25). */
2546
+ editSnap?: number;
2547
+ /** Optional single-note preview when the user adds a note. */
2548
+ onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;
2549
+ /** Drag props from {@link useTrackReorder}. When present, renders the grip
2550
+ * handle and makes the row a drop target. Omit for non-reorderable lists. */
2551
+ drag?: TrackRowDragProps;
2552
+ /** Shared meter handle from `useTrackLevels(host, isPlaying)`. When present,
2553
+ * a thin peak meter welds to the bottom of the row. Omit to hide it. */
2554
+ levels?: TrackLevelsHandle;
2555
+ }
2556
+ 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;
2557
+
2558
+ /**
2559
+ * Crossfade-pair metadata — family-agnostic types + parsing shared by every
2560
+ * generator panel that supports transition crossfades (synth / drum / instrument).
2561
+ *
2562
+ * A crossfade pair is two normal tracks linked by a shared `groupId`, persisted
2563
+ * in scene plugin_data under `track:<dbId>:crossfade`. Both members play the
2564
+ * same MIDI; one wears the origin preset, the other the target preset. The panel
2565
+ * owns the family-specific create flow (how a preset/sample is copied) and the
2566
+ * render; this module owns only the shape + the scene-data → pairs parse so the
2567
+ * logic can't drift across the three panels.
2568
+ *
2569
+ * @since SDK 2.23.0
2570
+ */
2571
+ /** Which half of the pair a per-layer control / member targets. */
2572
+ type CrossfadeSlot = 'origin' | 'target';
2573
+ /**
2574
+ * Equal-power center gain (~-3 dB, 1/√2) applied to BOTH crossfade layers so a
2575
+ * centered, non-functional slider already sounds like a midpoint blend. The
2576
+ * per-layer volume sliders start here; a later phase's fader drives them.
2577
+ */
2578
+ declare const EQUAL_POWER_GAIN = 0.707;
2579
+ /**
2580
+ * Per-member crossfade metadata (one scene-data value per member track). The two
2581
+ * members (origin/target) of a pair share a `groupId`.
2582
+ */
2583
+ interface CrossfadeMeta {
2584
+ groupId: string;
2585
+ slot: CrossfadeSlot;
2586
+ /** DB id of the partner member track. */
2587
+ partnerDbId: string;
2588
+ /** DB id of the SOURCE track this layer's preset/sample was copied from. */
2589
+ sourceTrackDbId: string;
2590
+ /** DB id of the scene the source track lives in (the from/to scene). */
2591
+ sourceSceneId: string;
2592
+ /** Source track display name (shown in the caption). */
2593
+ sourceName: string;
2594
+ /** Copied preset/sample label (shown in the caption). */
2595
+ soundLabel: string;
2596
+ /** Crossfade position 0..1 (kept identical on both members). */
2597
+ sliderPos: number;
2598
+ }
2599
+ /** A complete crossfade pair (both members present), keyed by groupId. */
2600
+ interface CrossfadePairMeta {
2601
+ groupId: string;
2602
+ sliderPos: number;
2603
+ originDbId: string;
2604
+ targetDbId: string;
2605
+ originSourceName: string;
2606
+ originSoundLabel: string;
2607
+ targetSourceName: string;
2608
+ targetSoundLabel: string;
2609
+ }
2610
+ /** Narrow an unknown scene-data value to CrossfadeMeta (defensive — survives partial blobs). */
2611
+ declare function asCrossfadeMeta(val: unknown): CrossfadeMeta | null;
2612
+ /**
2613
+ * Scan all `track:<dbId>:crossfade` keys in a scene's plugin_data and assemble
2614
+ * COMPLETE pairs (both origin + target present). A half-broken group (partner
2615
+ * deleted underneath) is omitted, so its surviving member falls back to a normal
2616
+ * row instead of vanishing.
2617
+ */
2618
+ declare function parseCrossfadePairs(sceneData: Record<string, unknown>): CrossfadePairMeta[];
2619
+ /** One volume-automation point: a dB value at a time offset (seconds from clip start). */
2620
+ interface VolumeAutomationPoint {
2621
+ time: number;
2622
+ db: number;
2623
+ }
2624
+ /** Origin + target volume curves for one crossfade pair. */
2625
+ interface CrossfadeVolumeCurves {
2626
+ origin: VolumeAutomationPoint[];
2627
+ target: VolumeAutomationPoint[];
1479
2628
  }
1480
- 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;
2629
+ /**
2630
+ * Equal-power crossfade volume curves over a transition of `bars` at `bpm`.
2631
+ * The ORIGIN layer fades OUT and the TARGET fades IN; `sliderPos` (0..1) sets
2632
+ * WHERE in time the equal-power (-3 dB) crossover sits — 0 = hand off near the
2633
+ * start, 1 = hold the origin until near the end. Points span the clip window
2634
+ * [0, durationSeconds] so the engine re-reads them each loop (re-fade per loop).
2635
+ * `steps`+1 points with linear interpolation approximate the cos/sin curve.
2636
+ *
2637
+ * Returns dB point arrays for `host.setTrackVolumeAutomation` — origin on the top
2638
+ * layer, target on the bottom. @since SDK 2.25.0
2639
+ */
2640
+ declare function buildCrossfadeVolumeCurves(bars: number, bpm: number, sliderPos: number, steps?: number): CrossfadeVolumeCurves;
1481
2641
 
1482
2642
  /**
1483
- * InstrumentDrawerTwo-stage nested menu for instrument selection + editor access.
2643
+ * CrossfadeTrackRowa transition "crossfade track": two stacked TrackRows
2644
+ * (origin on top, target on bottom) joined by a horizontal crossfade slider.
1484
2645
  *
1485
- * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.
1486
- * Stage 2 (editor): Shows "Open Editor" button for the selected plugin's native GUI.
2646
+ * Both layers play the SAME generated MIDI; the top wears the ORIGIN scene
2647
+ * track's preset and the bottom wears the TARGET scene track's preset. The user
2648
+ * cannot regenerate, shuffle, or change the preset/sample on either layer —
2649
+ * those controls are simply not wired into the inner TrackRows (the SDK
2650
+ * TrackRow is "controlled by omission"). What remains: per-layer volume/pan,
2651
+ * GROUP mute/solo (both layers toggle together), and a single delete that
2652
+ * removes the whole pair.
2653
+ *
2654
+ * The slider represents WHERE the crossfade happens. In this phase it is
2655
+ * centered and non-functional (omit `onSliderChange` → it renders disabled); a
2656
+ * later phase wires it to fade origin→target across the bars.
2657
+ *
2658
+ * @since SDK 2.22.0
1487
2659
  */
1488
2660
 
1489
- interface InstrumentDrawerProps {
1490
- /** Available instrument plugins from engine scan */
1491
- instruments: InstrumentDescriptor[];
1492
- /** Currently loaded instrument plugin ID (null = default Surge XT) */
1493
- currentPluginId: string | null;
1494
- /** Whether the scan is still in progress */
1495
- isLoading: boolean;
1496
- /** Called when user selects an instrument */
1497
- onSelect: (pluginId: string) => void;
1498
- /** Called when user clicks refresh to re-scan plugins */
1499
- onRefresh: () => void;
1500
- /** Which stage the drawer is in */
1501
- stage?: 'instruments' | 'editor';
1502
- /** Called when user clicks "Open Editor" */
1503
- onShowEditor?: () => void;
1504
- /** Called when user wants to go back from editor view to instrument list */
1505
- onBackToInstruments?: () => void;
1506
- /** Name of the selected instrument (for display in editor header) */
1507
- selectedInstrumentName?: string | null;
2661
+ /** One layer (engine track) of a crossfade pair. */
2662
+ interface CrossfadeLayer {
2663
+ /** Engine track id of this layer's track (also the meter key). */
2664
+ trackId: string;
2665
+ /** Display name of this layer's (newly created) track. */
2666
+ name: string;
2667
+ /** Musical role (same for both layers — crossfades are same-role). */
2668
+ role?: string;
2669
+ /** Name of the SOURCE track this layer was cloned from (origin/target scene). */
2670
+ sourceName?: string;
2671
+ /** Human label of the copied preset/sound, shown in the caption. */
2672
+ soundLabel?: string;
2673
+ /** Playback state for this layer. */
2674
+ runtimeState: {
2675
+ muted: boolean;
2676
+ solo: boolean;
2677
+ volume: number;
2678
+ pan: number;
2679
+ };
2680
+ }
2681
+ interface CrossfadeTrackRowProps {
2682
+ /** Top layer — wears the origin (from) scene track's preset. */
2683
+ origin: CrossfadeLayer;
2684
+ /** Bottom layer — wears the target (to) scene track's preset. */
2685
+ target: CrossfadeLayer;
2686
+ /** Crossfade position 0..1 (0 = all origin, 1 = all target). Defaults centered. */
2687
+ sliderPos?: number;
2688
+ /** Toggle mute on BOTH layers together (group mute). */
2689
+ onMuteToggle: () => void;
2690
+ /** Toggle solo on BOTH layers together (group solo). */
2691
+ onSoloToggle: () => void;
2692
+ /** Change one layer's volume (per-layer). */
2693
+ onVolumeChange: (slot: CrossfadeSlot, volume: number) => void;
2694
+ /** Change one layer's pan (per-layer). */
2695
+ onPanChange: (slot: CrossfadeSlot, pan: number) => void;
2696
+ /** Delete the whole pair. */
2697
+ onDelete: () => void;
2698
+ /** Move the crossfade point. Omit to render the slider read-only (phase 1). */
2699
+ onSliderChange?: (pos: number) => void;
2700
+ /** Shared meter handle (welds a peak meter to each layer). */
2701
+ levels?: TrackLevelsHandle;
2702
+ /** Left-border accent. Defaults to transition purple. */
2703
+ accentColor?: string;
2704
+ }
2705
+ declare function CrossfadeTrackRow({ origin, target, sliderPos, onMuteToggle, onSoloToggle, onVolumeChange, onPanChange, onDelete, onSliderChange, levels, accentColor, }: CrossfadeTrackRowProps): React.ReactElement;
2706
+
2707
+ /**
2708
+ * Crossfade MIDI inpainting — builds the LLM user-prompt for a bridge that
2709
+ * MORPHS the ORIGIN part into the TARGET part.
2710
+ *
2711
+ * A normal scene generation composes a part standalone from the scene's chords.
2712
+ * A crossfade bridge is different: it is INPAINTING between two fixed endpoints.
2713
+ * The generated part must begin feeling continuous with the origin pattern and
2714
+ * end feeling continuous with the target pattern, transforming between them
2715
+ * across the transition's bars.
2716
+ *
2717
+ * The harmonic frame — Key / mode / BPM / bars / the transition chord
2718
+ * progression (with beat timing) / scene contract — is injected AUTOMATICALLY by
2719
+ * `host.generateWithLLM` (it prepends the active scene's "Musical Context" block
2720
+ * unless `skipContextPrefix` is set). So this prompt does NOT restate key/bpm/
2721
+ * chords — it adds only the two endpoint patterns + the morph instructions, and
2722
+ * references the harmonic frame as "given above".
2723
+ *
2724
+ * REPRESENTATION (researched for Gemini): ABC notation is the LLM-native format
2725
+ * for melodic generation, but it's weak for percussion, would need a separate
2726
+ * output parser (our output is JSON note-events, already proven with Gemini),
2727
+ * and an inpainting task wants input/output FORMAT SYMMETRY. So each endpoint is
2728
+ * given as the exact JSON note-events PLUS a pitch-named, bar-structured "gloss"
2729
+ * — the transferable wins from the research (pitch NAMES over raw MIDI numbers,
2730
+ * explicit bar/beat structure) layered on the precise, symmetric JSON. Drums
2731
+ * (uniform pitch) get a rhythmic gloss instead of pitch names.
2732
+ *
2733
+ * This changes only the LLM INPUT framing: the OUTPUT schema is unchanged, so the
2734
+ * calling panel keeps its system prompt + parser (and, for drums, its flatten step).
2735
+ *
2736
+ * @since SDK 2.24.0
2737
+ */
2738
+
2739
+ interface CrossfadeInpaintInput {
2740
+ /** Musical role of the bridge part (e.g. 'bass'). '' falls back to "melodic". */
2741
+ role: string;
2742
+ /** Transition length in bars (the morph timeline). */
2743
+ bars: number;
2744
+ /** Display name of the ORIGIN source track (the part the bridge begins from). */
2745
+ originName: string;
2746
+ /** Display name of the TARGET source track (the part the bridge arrives at). */
2747
+ targetName: string;
2748
+ /** ORIGIN source scene's key label (e.g. "G minor"). Null/omitted = unknown. */
2749
+ originKey?: string | null;
2750
+ /** TARGET source scene's key label. Null/omitted = unknown. */
2751
+ targetKey?: string | null;
2752
+ /** ORIGIN pattern notes (beat-based; from the FROM scene). May be empty. */
2753
+ originNotes: readonly PluginMidiNote[];
2754
+ /** TARGET pattern notes (beat-based; from the TO scene). May be empty. */
2755
+ targetNotes: readonly PluginMidiNote[];
2756
+ /** Drums: pitch is uniform (flattened), so gloss RHYTHM instead of pitch names. */
2757
+ percussive?: boolean;
1508
2758
  }
1509
- declare function InstrumentDrawer({ instruments, currentPluginId, isLoading, onSelect, onRefresh, stage, onShowEditor, onBackToInstruments, selectedInstrumentName, }: InstrumentDrawerProps): React.ReactElement;
2759
+ /**
2760
+ * Build the inpainting user-prompt. The result is the prompt BODY only — pass it
2761
+ * as `request.user` to `host.generateWithLLM` with the panel's normal system
2762
+ * prompt and `responseFormat: 'json'`; the harmonic context auto-prefixes.
2763
+ */
2764
+ declare function buildCrossfadeInpaintPrompt(input: CrossfadeInpaintInput): string;
2765
+
2766
+ /**
2767
+ * ImportTrackModal — "import a track from another scene" picker (SDK component).
2768
+ *
2769
+ * Shared by all five generator panels (drums / instruments / synths / loops /
2770
+ * stems). Self-fetching: given the scoped `host`, it calls
2771
+ * `host.listImportableTracks()` to enumerate candidates (already filtered to
2772
+ * the calling panel's type and gate-annotated by the host) and
2773
+ * `host.importTrack()` to perform the copy. The UI only renders `importable` +
2774
+ * `disabledReason` — it never computes the harmonic/length/tempo gate itself.
2775
+ *
2776
+ * Two-step picker: choose a source scene, then a track in it. Incompatible
2777
+ * tracks render disabled with a reason tooltip (never hidden), per product
2778
+ * decision.
2779
+ *
2780
+ * @since SDK 2.13.0
2781
+ */
2782
+
2783
+ interface ImportTrackModalProps {
2784
+ /** Scoped host — the modal calls listImportableTracks / importTrack itself. */
2785
+ host: PluginHost;
2786
+ /** Controls visibility (the panel owns open/closed from its header button). */
2787
+ open: boolean;
2788
+ /** Close handler (Escape, backdrop, Cancel, or after a successful import). */
2789
+ onClose: () => void;
2790
+ /** Fired after a successful import with the new track handle. */
2791
+ onImported: (handle: PluginTrackHandle) => void;
2792
+ /** Optional modal title (default names the whole-track import). */
2793
+ title?: string;
2794
+ /** data-testid prefix so each panel's modal is addressable in tests. */
2795
+ testIdPrefix?: string;
2796
+ /**
2797
+ * 'track' (default) imports a whole track via `importTrack`. 'sound' copies
2798
+ * ONLY the sound onto an existing track: every candidate is selectable (the
2799
+ * contract gate is ignored) and the chosen track is handed back via `onPick`
2800
+ * instead of being imported — the panel applies it via `host.getTrackSound`.
2801
+ */
2802
+ mode?: 'track' | 'sound';
2803
+ /** Sound-mode pick handler — required when `mode='sound'`. */
2804
+ onPick?: (sel: {
2805
+ sourceTrackDbId: string;
2806
+ trackName: string;
2807
+ sceneName: string;
2808
+ }) => void | Promise<void>;
2809
+ /**
2810
+ * Cross-panel port handler (track mode). When provided, the modal also lists
2811
+ * the ACTIVE scene's tracks owned by OTHER panels as a `sameScene` group —
2812
+ * shown first and selected by default — and routes a pick there to this
2813
+ * callback instead of `importTrack`. The panel re-sounds the part on its own
2814
+ * instrument (create track → copy MIDI → load native sound). @since SDK 2.20.0
2815
+ */
2816
+ onPortTrack?: (sel: {
2817
+ sourceTrackDbId: string;
2818
+ trackName: string;
2819
+ role?: string;
2820
+ }) => void | Promise<void>;
2821
+ }
2822
+ declare function ImportTrackModal({ host, open, onClose, onImported, title, testIdPrefix, mode, onPick, onPortTrack, }: ImportTrackModalProps): React.ReactElement | null;
2823
+
2824
+ /**
2825
+ * CrossfadeModal — "add a crossfade track" picker for a transition scene.
2826
+ *
2827
+ * Shown only inside a `scene_type='transition'` scene. The user picks an ORIGIN
2828
+ * track (from the transition's FROM scene) and a TARGET track (from its TO
2829
+ * scene). Crossfades are same-role: once an origin is chosen, the target
2830
+ * dropdown is filtered to the origin's role.
2831
+ *
2832
+ * Self-fetching: given the scoped `host`, it calls `host.listSceneFamilyTracks`
2833
+ * for both scenes (ungated — a transition deliberately bridges different keys).
2834
+ * It does NOT build the pair itself; it hands the two selections to `onCreate`,
2835
+ * which the panel implements (create two tracks, generate one shared MIDI clip,
2836
+ * copy each preset). `onCreate` should reject on failure so the modal can show
2837
+ * it and stay open.
2838
+ *
2839
+ * @since SDK 2.22.0
2840
+ */
2841
+
2842
+ /** A picked source track handed to `onCreate`. */
2843
+ interface CrossfadeSelection {
2844
+ /** Source track DB id (selector for getTrackSound + crossfade metadata). */
2845
+ dbId: string;
2846
+ /** Display name (for the row caption). */
2847
+ name: string;
2848
+ /** Musical role (same for both — enforced by the picker). */
2849
+ role?: string;
2850
+ }
2851
+ interface CrossfadeModalProps {
2852
+ /** Scoped host — the modal calls listSceneFamilyTracks itself. */
2853
+ host: PluginHost;
2854
+ /** Controls visibility (the panel owns open/closed from its header button). */
2855
+ open: boolean;
2856
+ /** DB id of the transition's FROM (origin) scene. */
2857
+ fromSceneId: string;
2858
+ /** DB id of the transition's TO (target) scene. */
2859
+ toSceneId: string;
2860
+ /** Display name for the origin scene heading (optional). */
2861
+ fromSceneName?: string;
2862
+ /** Display name for the target scene heading (optional). */
2863
+ toSceneName?: string;
2864
+ /** Close handler (Escape, backdrop, Cancel, or after a successful create). */
2865
+ onClose: () => void;
2866
+ /** Build the crossfade pair. Should reject on failure so the modal shows it. */
2867
+ onCreate: (origin: CrossfadeSelection, target: CrossfadeSelection) => Promise<void>;
2868
+ /** data-testid prefix. */
2869
+ testIdPrefix?: string;
2870
+ }
2871
+ declare function CrossfadeModal({ host, open, fromSceneId, toSceneId, fromSceneName, toSceneName, onClose, onCreate, testIdPrefix, }: CrossfadeModalProps): React.ReactElement | null;
2872
+
2873
+ /**
2874
+ * ConfirmDialog — styled in-app confirmation modal (SDK component).
2875
+ *
2876
+ * A small, reusable "are you sure?" dialog matching the app's dark theme
2877
+ * (mirrors ImportTrackModal chrome: sas-panel / sas-border / shadow-xl). It
2878
+ * guards destructive actions; the first consumer is track deletion, which was
2879
+ * one stray click away from losing a track's MIDI + sound.
2880
+ *
2881
+ * Controlled component — the caller owns `open` and the confirm/cancel
2882
+ * handlers. Escape and a backdrop click both cancel, and the Cancel button is
2883
+ * auto-focused on open so a reflexive Enter dismisses rather than deletes.
2884
+ *
2885
+ * @since SDK 2.17.0
2886
+ */
2887
+
2888
+ interface ConfirmDialogProps {
2889
+ /** Controls visibility (the caller owns open/closed). */
2890
+ open: boolean;
2891
+ /** Bold heading line. */
2892
+ title: string;
2893
+ /** Body copy — a string or richer node. */
2894
+ message: React.ReactNode;
2895
+ /** Confirm button label (default "Delete"). */
2896
+ confirmLabel?: string;
2897
+ /** Cancel button label (default "Cancel"). */
2898
+ cancelLabel?: string;
2899
+ /** When true (default), the confirm button reads as a destructive (red) action. */
2900
+ destructive?: boolean;
2901
+ /** Fired when the user confirms. */
2902
+ onConfirm: () => void;
2903
+ /** Fired on Cancel, Escape, or backdrop click. */
2904
+ onCancel: () => void;
2905
+ /** data-testid prefix so each dialog is addressable in tests. */
2906
+ testIdPrefix?: string;
2907
+ }
2908
+ declare function ConfirmDialog({ open, title, message, confirmLabel, cancelLabel, destructive, onConfirm, onCancel, testIdPrefix, }: ConfirmDialogProps): React.ReactElement | null;
2909
+
2910
+ /**
2911
+ * Modal — the SDK's one modal-stacking primitive (portal + z-tier + backdrop).
2912
+ *
2913
+ * Every SDK modal renders INSIDE a plugin's accordion section, whose animated
2914
+ * `overflow-hidden` + `transition-all` wrapper establishes a stacking context.
2915
+ * An inline `position: fixed` overlay is therefore scoped to that section and
2916
+ * can be painted UNDER a neighbouring panel (the "import modal invisible on a
2917
+ * later open" bug). This component solves that once: it portals the overlay to
2918
+ * <body> — out of every panel's stacking context — at a z-tier above all the
2919
+ * app's `z-50` dropdowns/banners but below the toast tier (`z-[9999]`), so
2920
+ * toasts still float over modals.
2921
+ *
2922
+ * Controlled: the caller owns `open` and `onClose`. The caller renders its own
2923
+ * dialog box as `children` (keep the box's `onClick={e => e.stopPropagation()}`
2924
+ * so inside-clicks don't dismiss). Escape and a backdrop click both close.
2925
+ *
2926
+ * @since SDK 2.21.0
2927
+ */
2928
+
2929
+ interface ModalProps {
2930
+ /** Controls visibility (the caller owns open/closed). */
2931
+ open: boolean;
2932
+ /** Close handler — fired on Escape and backdrop click. */
2933
+ onClose: () => void;
2934
+ /** The dialog box. Give it `onClick={e => e.stopPropagation()}`. */
2935
+ children: React.ReactNode;
2936
+ /** data-testid prefix; the backdrop is `${testIdPrefix}-overlay`. */
2937
+ testIdPrefix?: string;
2938
+ /** Close when the backdrop is clicked (default true). */
2939
+ closeOnBackdrop?: boolean;
2940
+ /** Close on Escape (default true). */
2941
+ closeOnEscape?: boolean;
2942
+ /** Focused when the modal opens (e.g. a Cancel button) so a reflexive Enter is safe. */
2943
+ initialFocusRef?: React.RefObject<HTMLElement>;
2944
+ }
2945
+ declare function Modal({ open, onClose, children, testIdPrefix, closeOnBackdrop, closeOnEscape, initialFocusRef, }: ModalProps): React.ReactElement | null;
2946
+
2947
+ /**
2948
+ * PianoRollEditor — a compact, DOM-based MIDI note editor for the track drawer.
2949
+ *
2950
+ * Controlled: `notes` in, `onChange(next)` out. Notes render as absolutely-
2951
+ * positioned divs over a beat/pitch grid (DOM, not canvas — so it themes with
2952
+ * sas-* tokens and is fully driveable by React Testing Library). Supports:
2953
+ * - add : click an empty grid cell
2954
+ * - delete : click an existing note (no drag)
2955
+ * - move : drag a note's body (snap-quantised)
2956
+ * - resize : drag a note's right-edge handle (snap-quantised, ≥ one step)
2957
+ * - octave : shift the whole clip ±12 (toolbar) — no velocity lane
2958
+ * / marquee yet.
2959
+ * On load the viewport auto-scrolls to vertically center the note cluster, so a
2960
+ * low melody isn't stranded off-screen at the bottom of the pitch range.
2961
+ *
2962
+ * Coordinate spaces:
2963
+ * pitch (0-127) ── row = hi - pitch ── top px = row * ROW_HEIGHT
2964
+ * beat (¼ notes) ─────────────────────── left px = beat * PX_PER_BEAT
2965
+ *
2966
+ * The pure helpers (`cellToPx` / `pxToCell` / `transposeNotes`) and layout
2967
+ * constants are exported so coordinate math can be unit-tested without a DOM.
2968
+ */
2969
+
2970
+ /** Horizontal pixels per quarter-note beat. */
2971
+ declare const PX_PER_BEAT = 24;
2972
+ /** Vertical pixels per semitone row. */
2973
+ declare const ROW_HEIGHT = 12;
2974
+ /** Left keyboard-gutter width (px). */
2975
+ declare const GUTTER_W = 28;
2976
+ /** Pointer travel (px) before a press on a note becomes a drag instead of a click. */
2977
+ declare const DRAG_DEAD_ZONE = 4;
2978
+ /** Width (px) of the right-edge grab handle that resizes a note's length. */
2979
+ declare const RESIZE_HANDLE_PX = 6;
2980
+ /** MIDI pitch → scientific note name (60 = C4). */
2981
+ declare function pitchToName(pitch: number): string;
2982
+ /**
2983
+ * Cell (pitch, startBeat) → top-left pixel offset within the grid.
2984
+ * `hi` is the highest (top) visible pitch.
2985
+ */
2986
+ declare function cellToPx(pitch: number, startBeat: number, hi: number): {
2987
+ left: number;
2988
+ top: number;
2989
+ };
2990
+ /**
2991
+ * Grid-local pixel → snapped cell. `hi` is the highest visible pitch; the beat
2992
+ * snaps to the nearest `snap` step and clamps to `[0, totalBeats - snap]`;
2993
+ * pitch clamps to `[0, 127]`.
2994
+ */
2995
+ declare function pxToCell(localX: number, localY: number, hi: number, snap: number, bars: number, beatsPerBar: number): {
2996
+ pitch: number;
2997
+ startBeat: number;
2998
+ };
2999
+ /**
3000
+ * New `durationBeats` for a note whose right edge is dragged to grid-local pixel
3001
+ * `localX`. The end snaps to the nearest `snap` step, is clamped to at least one
3002
+ * step past `startBeat`, and never extends beyond the grid's right edge
3003
+ * (`bars * beatsPerBar`). `startBeat` and `pitch` are untouched.
3004
+ */
3005
+ declare function resizeNoteDuration(startBeat: number, localX: number, snap: number, bars: number, beatsPerBar: number): number;
3006
+ /**
3007
+ * `scrollTop` that vertically centers the bulk of the notes in a `viewportH`-px
3008
+ * window. Targets the MEDIAN pitch (robust to a stray high/low outlier — keeps
3009
+ * "where the majority of notes are" framed) and clamps to the valid scroll
3010
+ * range. `hi` is the top visible pitch; `rowCount` the total rows in the grid.
3011
+ * Returns 0 when there are no notes.
3012
+ */
3013
+ declare function centerScrollTop(pitches: readonly number[], hi: number, rowCount: number, viewportH: number): number;
3014
+ /** Transpose every note by `semitones`, clamping pitch to [0,127] (never drops a note). */
3015
+ declare function transposeNotes(notes: readonly PluginMidiNote[], semitones: number): PluginMidiNote[];
3016
+ interface PianoRollEditorProps {
3017
+ /** Controlled note list (quarter-note beats). The editor never mutates this. */
3018
+ notes: readonly PluginMidiNote[];
3019
+ /** Emitted on every edit (add / delete / move / transpose) with the full next array. */
3020
+ onChange: (next: PluginMidiNote[]) => void;
3021
+ /** Scene length in bars → grid width = bars * beatsPerBar * PX_PER_BEAT. */
3022
+ bars: number;
3023
+ /** BPM — used only for audition timing in v1. */
3024
+ bpm: number;
3025
+ /** Beats per bar (time-signature numerator). Default 4. */
3026
+ beatsPerBar?: number;
3027
+ /** Snap step in quarter notes (1 = ¼ note, 0.25 = 1/16). Default 0.25. */
3028
+ snap?: number;
3029
+ /** Snap steps the toolbar selector offers. Default [1, 0.5, 0.25]. */
3030
+ snapOptions?: number[];
3031
+ /** Notified when the user changes snap (the editor still tracks it internally). */
3032
+ onSnapChange?: (snap: number) => void;
3033
+ /** Lowest pitch always visible. Default C2 (36). */
3034
+ minPitch?: number;
3035
+ /** Highest pitch always visible. Default C6 (84). */
3036
+ maxPitch?: number;
3037
+ /** Expand the visible window to include notes outside [minPitch,maxPitch]. Default true. */
3038
+ autoFit?: boolean;
3039
+ /** Optional single-note preview, fired when a note is added. */
3040
+ onAuditionNote?: (pitch: number, velocity: number, durationMs: number) => void;
3041
+ /** Velocity for newly-added notes. Default 100. */
3042
+ defaultVelocity?: number;
3043
+ /** Disable all interaction (e.g. while the track is generating). Default false. */
3044
+ disabled?: boolean;
3045
+ /** Extra className for the outer container. */
3046
+ className?: string;
3047
+ /** Test id for the outer container. Default "sdk-piano-roll". */
3048
+ testId?: string;
3049
+ }
3050
+ declare function PianoRollEditor({ notes, onChange, bars, bpm, beatsPerBar, snap, snapOptions, onSnapChange, minPitch, maxPitch, autoFit, onAuditionNote, defaultVelocity, disabled, className, testId, }: PianoRollEditorProps): React.ReactElement;
1510
3051
 
1511
3052
  /**
1512
3053
  * VolumeSlider Component
@@ -1621,6 +3162,323 @@ declare function calculateTimeBasedTarget(elapsedMs: number, estimatedDurationMs
1621
3162
  */
1622
3163
  declare function SorceryProgressBar({ isLoading, statusText, completeText, onComplete, heightClass, initialProgress, onProgressChange, estimatedDurationMs, }: SorceryProgressBarProps): React.ReactElement | null;
1623
3164
 
3165
+ /**
3166
+ * DownloadPackButton — versioned-pack download trigger (SDK component).
3167
+ *
3168
+ * Parameterized by `packId`; drives the download through the host
3169
+ * (`host.startSamplePackDownload` / `host.onSamplePackProgress`) so plugins
3170
+ * never reach into the app's IPC (`window.electronAPI`). Two display variants:
3171
+ * - 'compact' (default) — small uppercase button for panel headers
3172
+ * - 'large' — bigger CTA used inside SamplePackCTACard
3173
+ *
3174
+ * @since SDK 2.8.0 (moved from the app and refactored onto PluginHost).
3175
+ */
3176
+
3177
+ type DownloadPackButtonVariant = 'compact' | 'large';
3178
+ interface DownloadPackButtonProps {
3179
+ /** Host the plugin received; drives the download + progress. */
3180
+ host: PluginHost;
3181
+ packId: string;
3182
+ /** Pack display name, e.g. 'Drum Sample Library'. Used in tooltips/labels. */
3183
+ displayName: string;
3184
+ /** Bundle size in bytes (shown in the large-variant label). */
3185
+ sizeBytes?: number;
3186
+ variant?: DownloadPackButtonVariant;
3187
+ /** Called once after the install completes (status === 'complete'). */
3188
+ onDownloadComplete?: () => void;
3189
+ }
3190
+ declare const DownloadPackButton: React.FC<DownloadPackButtonProps>;
3191
+
3192
+ /**
3193
+ * SamplePackCTACard — empty-state card a generator panel renders when its
3194
+ * sample pack is missing OR a newer version is available. Wraps
3195
+ * DownloadPackButton in a centered card. The completion callback should
3196
+ * re-fetch pack status on the parent so the card unmounts and the normal panel
3197
+ * UI takes over.
3198
+ *
3199
+ * @since SDK 2.8.0 (moved from the app; download driven through PluginHost).
3200
+ */
3201
+
3202
+ type SamplePackCTACardStatus = 'missing' | 'stale' | 'checking';
3203
+ /** Minimal pack info the card needs. A PackConfig is structurally compatible. */
3204
+ interface SamplePackCardInfo {
3205
+ packId: string;
3206
+ displayName: string;
3207
+ description: string;
3208
+ sizeBytes?: number;
3209
+ }
3210
+ interface SamplePackCTACardProps {
3211
+ /** Host the plugin received; drives the download. */
3212
+ host: PluginHost;
3213
+ pack: SamplePackCardInfo;
3214
+ status: SamplePackCTACardStatus;
3215
+ onDownloadComplete?: () => void;
3216
+ }
3217
+ declare const SamplePackCTACard: React.FC<SamplePackCTACardProps>;
3218
+
3219
+ /**
3220
+ * WaveformView — small canvas waveform for an audio file on disk.
3221
+ *
3222
+ * Reads bytes via `host.getAudioFileBytes`, decodes via
3223
+ * `AudioContext.decodeAudioData`, computes peaks, and renders to a
3224
+ * canvas. Suitable for take rows, sample previews, or any place a
3225
+ * decorative ~40px waveform makes sense.
3226
+ *
3227
+ * The component is self-contained: it owns the AudioContext and the
3228
+ * peak buffer, decodes once per `filePath` change, and tears down on
3229
+ * unmount. Failures (file missing, decode error) render as a silent
3230
+ * blank canvas — the caller can decide how to surface errors.
3231
+ */
3232
+
3233
+ interface WaveformViewProps {
3234
+ host: PluginHost;
3235
+ filePath: string;
3236
+ /** Number of bins to compute. Default 256 — plenty for ~40px tall rows. */
3237
+ bins?: number;
3238
+ /** Tailwind / inline className for sizing. Default: w-full h-10. */
3239
+ className?: string;
3240
+ /** Override the bar fill style (e.g., to match a track color). */
3241
+ fillStyle?: string;
3242
+ /**
3243
+ * If set, the bin range spans `targetSamples` instead of the file's
3244
+ * actual length. Bins beyond the audio render as flat silence — used
3245
+ * to align a partial recording inside a full-loop-width canvas so
3246
+ * every take row has the same time scale.
3247
+ */
3248
+ targetSamples?: number;
3249
+ }
3250
+ declare const WaveformView: React.FC<WaveformViewProps>;
3251
+
3252
+ /**
3253
+ * Shared level-meter component.
3254
+ *
3255
+ * Renders a horizontal LED-style bar over -60dBFS → 0dBFS:
3256
+ * - A fixed left-to-right gradient (green → orange → red), so the color is
3257
+ * tied to POSITION: a quiet signal lights only the green left, a hot signal
3258
+ * reaches the red right. An "unlit" mask hides the gradient beyond the
3259
+ * current level.
3260
+ * - A deterministic segment grid (the "LED monitor" look) drawn as a pure-CSS
3261
+ * repeating overlay — constant DOM, no per-frame cost.
3262
+ * - An optional peak-hold marker (`peakHoldDb`) — a bright line at the recent
3263
+ * maximum that the caller holds/decays (see `useTrackMeter`).
3264
+ * - An optional CLIP badge the caller wires up.
3265
+ *
3266
+ * Pure presentational: takes the current dB + `active` flag (+ optional held
3267
+ * peak) and draws. The only production consumer is the per-track strip
3268
+ * (`TrackMeterStrip`, via `compact`). `compact` shrinks the bar and drops the
3269
+ * numeric dB readout.
3270
+ */
3271
+
3272
+ interface LevelMeterProps {
3273
+ /** Current peak level in dBFS. -120 means "no signal". */
3274
+ peakDb: number;
3275
+ /** True when the underlying audio callback is firing. False = floor. */
3276
+ active: boolean;
3277
+ /**
3278
+ * Held peak in dBFS for the peak-hold marker. Omit to draw no marker. The
3279
+ * marker is hidden when this is at/below the visible floor (-60).
3280
+ */
3281
+ peakHoldDb?: number;
3282
+ /** Latched clip flag. When true, render the CLIP badge. */
3283
+ clipped?: boolean;
3284
+ /** User-clickable handler to clear the latched clip indicator. */
3285
+ onClearClip?: () => void;
3286
+ /**
3287
+ * Thin strip mode for per-track meters: hides the numeric dB readout and
3288
+ * shrinks the bar. Keeps the (rare) CLIP badge.
3289
+ */
3290
+ compact?: boolean;
3291
+ /** Optional className overlaid on the wrapper for layout tweaks. */
3292
+ className?: string;
3293
+ /** Inline test id — make multiple instances distinguishable. */
3294
+ 'data-testid'?: string;
3295
+ }
3296
+ declare const LevelMeter: React.FC<LevelMeterProps>;
3297
+
3298
+ /**
3299
+ * TrackMeterStrip — the thin per-track peak meter welded to the bottom of a
3300
+ * track row. Cosmetic: gives a general sense of each track's level and adds
3301
+ * motion during playback.
3302
+ *
3303
+ * This is deliberately its OWN component so the per-row meter selector
3304
+ * (`useTrackMeter`) re-renders ONLY this strip at ~30Hz, never the heavy
3305
+ * TrackRow around it. Render it as a full-width sibling directly under a row
3306
+ * body; it welds on with a squared top edge (like the track drawer does).
3307
+ */
3308
+
3309
+ interface TrackMeterStripProps {
3310
+ /** Shared meter handle from `useTrackLevels(host, isPlaying)`. */
3311
+ levels: TrackLevelsHandle;
3312
+ /** Tracktion engine track id (matches `PluginTrackHandle.id`). */
3313
+ trackId: string;
3314
+ /** Round the bottom corners (false when a drawer welds on below). Default true. */
3315
+ roundBottom?: boolean;
3316
+ /** Optional className for layout tweaks on the wrapper. */
3317
+ className?: string;
3318
+ }
3319
+ declare const TrackMeterStrip: React.FC<TrackMeterStripProps>;
3320
+
3321
+ /**
3322
+ * ScrollingWaveform — live waveform during recording (Phase 8.10).
3323
+ *
3324
+ * Reads the platform's `peakDb` history and renders it as a horizontal
3325
+ * bar-graph that scrolls left as new samples arrive. Two halves: top
3326
+ * band shows positive amplitude, bottom band mirrors it (matches the
3327
+ * static waveform's min/max layout in `WaveformView`).
3328
+ *
3329
+ * The data source is a function the caller supplies — typically a ref
3330
+ * to the `inputLevelDb` value from `AudioRoutingContext` polled at
3331
+ * ~30Hz. The component samples that ref via requestAnimationFrame and
3332
+ * shifts a fixed-size float ring buffer one column per frame.
3333
+ *
3334
+ * Pure presentational + animation logic; no IPC. Stops animating
3335
+ * when `active` is false (engine isn't running the audio callback).
3336
+ */
3337
+
3338
+ interface ScrollingWaveformProps {
3339
+ /** Function returning the latest peak in dBFS. Called per RAF. */
3340
+ getPeakDb: () => number;
3341
+ /** True while the audio callback is running; false freezes the wave. */
3342
+ active: boolean;
3343
+ /** Number of horizontal columns in the ring buffer. */
3344
+ columns?: number;
3345
+ /** Optional className for sizing. */
3346
+ className?: string;
3347
+ /** Highlight color for the wave. */
3348
+ fillStyle?: string;
3349
+ }
3350
+ declare const ScrollingWaveform: React.FC<ScrollingWaveformProps>;
3351
+
3352
+ /**
3353
+ * OffsetScrubber — manual sample-offset slider for Lyria-generated audio.
3354
+ *
3355
+ * Renders a thin horizontal track with one tick per detected beat (tall
3356
+ * tick on the downbeat) and a draggable thumb. Drag distance maps to a
3357
+ * sample offset that is applied to the audio clip via
3358
+ * `host.setAudioOffsetSamples(trackId, n)`.
3359
+ *
3360
+ * Snap behavior:
3361
+ * - Default: snap to the nearest beat in `cuePoints.beats`.
3362
+ * - Hold Shift: bypass snap (free 1-sample resolution).
3363
+ * - Click on a tick mark: jump to that beat exactly.
3364
+ *
3365
+ * The visible range is one bar (= meter beats) on each side of bar 1.
3366
+ * For a 4-bar / 4/4 clip at 44100 Hz, one bar at 120 BPM is 88_200
3367
+ * samples — so the slider covers ±88_200 samples, ~2 s either way. That
3368
+ * matches the alignment errors we observe from Lyria detection misses
3369
+ * (typically <1 beat off).
3370
+ *
3371
+ * BPM mismatch chip: shown when `cuePoints.detected_bpm` is more than
3372
+ * 1 BPM away from the project BPM, since the beat ticks won't line up
3373
+ * with the project grid in that case.
3374
+ */
3375
+
3376
+ interface OffsetScrubberProps {
3377
+ /** Detected beat positions + sample rate. Slider is disabled when null. */
3378
+ cuePoints: PluginCuePoints | null;
3379
+ /** Current offset, in samples (signed). */
3380
+ offsetSamples: number;
3381
+ /** Project BPM — used to compute the visible range and the mismatch chip. */
3382
+ projectBpm: number;
3383
+ /** Beats per bar, defaults to 4. */
3384
+ meter?: number;
3385
+ /** Called on drag-end with the resolved offset (already snapped). */
3386
+ onChange: (offsetSamples: number) => void;
3387
+ /** Disable interaction (e.g., during generation / split). */
3388
+ disabled?: boolean;
3389
+ }
3390
+ declare function OffsetScrubber({ cuePoints, offsetSamples, projectBpm, meter, onChange, disabled, }: OffsetScrubberProps): React.ReactElement;
3391
+
3392
+ /**
3393
+ * Shared waveform peaks + canvas drawer.
3394
+ *
3395
+ * Originally inlined in `stems/TrimEditorDrawer.tsx`; lifted to
3396
+ * this module so the recorder plugin's per-take rows can render the
3397
+ * same compact min/max display without duplicating the math.
3398
+ *
3399
+ * Design:
3400
+ * - `computePeaks` reduces an AudioBuffer to `bins` min/max pairs (mono
3401
+ * average across channels). Output layout is interleaved
3402
+ * `[min0, max0, min1, max1, ...]` so the renderer reads pairs
3403
+ * sequentially without index arithmetic.
3404
+ * - `drawWaveform` paints one 1px vertical bar per canvas column,
3405
+ * dpr-aware so it stays crisp on retina displays.
3406
+ *
3407
+ * No host or React dependencies — pure functions are safe to use from
3408
+ * tests, web workers, or non-React renderers.
3409
+ */
3410
+ interface WaveformPeaks {
3411
+ /** Sample rate of the source file (used to convert sample → seconds). */
3412
+ sampleRate: number;
3413
+ /** Total length of the raw file in samples. */
3414
+ totalSamples: number;
3415
+ /** Min/max pairs per bin (length = bins × 2). */
3416
+ peaks: Float32Array;
3417
+ }
3418
+ /**
3419
+ * Reduce an AudioBuffer to `bins` min/max pairs. Mono averages across
3420
+ * channels. The output buffer is fixed-size (`bins * 2`) for fast canvas
3421
+ * traversal.
3422
+ *
3423
+ * `targetSamples` (optional) extends the bin range to a fixed sample
3424
+ * count larger than the buffer's actual length — bins falling beyond
3425
+ * the buffer get (0, 0) pairs, which renders as a flat tail. Used by
3426
+ * the recorder so a partial last chunk's waveform sits at the start of
3427
+ * a full-loop-width canvas instead of being stretched to fill.
3428
+ */
3429
+ declare function computePeaks(audioBuffer: AudioBuffer, bins: number, targetSamples?: number): WaveformPeaks;
3430
+ /**
3431
+ * Draw min/max peaks to the given canvas. Resizes the canvas backing
3432
+ * store to CSS pixels × devicePixelRatio so the result is crisp on
3433
+ * retina. Caller controls CSS sizing via the `<canvas>` element's
3434
+ * className.
3435
+ */
3436
+ declare function drawWaveform(canvas: HTMLCanvasElement, peaks: WaveformPeaks, options?: {
3437
+ fillStyle?: string;
3438
+ }): void;
3439
+
3440
+ /**
3441
+ * WAV peak analyzer (Phase 8.10).
3442
+ *
3443
+ * Reads a WAV file via the plugin host, decodes it via Web Audio,
3444
+ * scans every channel for the absolute maximum sample, and returns
3445
+ * peak dBFS + a clipped flag (true when the peak >= -1dBFS, matching
3446
+ * the engine's hard-limiter ceiling).
3447
+ *
3448
+ * Used by the recorder's take rows to surface "this take peaked at
3449
+ * -8dB" or "this take CLIPPED" without the user having to click play.
3450
+ */
3451
+
3452
+ interface PeakAnalysis {
3453
+ peakLinear: number;
3454
+ peakDb: number;
3455
+ clipped: boolean;
3456
+ }
3457
+ declare function analyzeWavPeak(host: PluginHost, filePath: string): Promise<PeakAnalysis>;
3458
+
3459
+ /**
3460
+ * Synthesize a PluginCuePoints object from raw BPM/sample-rate inputs.
3461
+ *
3462
+ * The OffsetScrubber consumes PluginCuePoints — a beat grid plus
3463
+ * per-beat sample positions, normally produced by Lyria's onset
3464
+ * detector. The recorder doesn't have detected cue points (live
3465
+ * recordings have no detection pass), but it always knows the project
3466
+ * BPM, the engine sample rate, and the loop length in bars. That's
3467
+ * enough to construct a synthetic grid where every beat sits on a
3468
+ * regular interval — which is exactly what the scrubber needs to
3469
+ * provide tick marks + snap behavior for nudging the take's offset.
3470
+ */
3471
+
3472
+ interface SynthesizeCuePointsOptions {
3473
+ bpm: number;
3474
+ sampleRate: number;
3475
+ /** Total bars in the clip (e.g. 4 for a 4-bar loop). */
3476
+ bars: number;
3477
+ /** Beats per bar. Defaults to 4 (4/4). */
3478
+ meter?: number;
3479
+ }
3480
+ declare function synthesizeCuePoints({ bpm, sampleRate, bars, meter, }: SynthesizeCuePointsOptions): PluginCuePoints;
3481
+
1624
3482
  /**
1625
3483
  * useSceneState — Scene-keyed state hook for plugin developers.
1626
3484
  *
@@ -1646,6 +3504,87 @@ type SetSceneState<T> = (value: T | ((prev: T) => T)) => void;
1646
3504
  type SetSceneStateForScene<T> = (sceneId: string, value: T | ((prev: T) => T)) => void;
1647
3505
  declare function useSceneState<T>(activeSceneId: string | null, initialValue: T): [T, SetSceneState<T>, SetSceneStateForScene<T>];
1648
3506
 
3507
+ /**
3508
+ * useAnySolo — reactively reports whether ANY track in the project is soloed.
3509
+ *
3510
+ * Solo is cross-panel: when the user solos a track in ANY panel, the engine's
3511
+ * effective-mute model silences every non-soloed track. A panel uses this flag
3512
+ * to DIM its own non-soloed rows without lighting their Mute buttons:
3513
+ *
3514
+ * ```tsx
3515
+ * const anySolo = useAnySolo(host);
3516
+ * // ...
3517
+ * <TrackRow soloedOut={anySolo && !track.runtimeState.solo} ... />
3518
+ * ```
3519
+ *
3520
+ * Refreshes on mount and on every track-state change. `onTrackStateChange`
3521
+ * fires for tracks in ALL panels (not just this plugin's), so a solo toggled in
3522
+ * another panel updates this flag too.
3523
+ */
3524
+
3525
+ declare function useAnySolo(host: Pick<PluginHost, 'isAnySoloActive' | 'onTrackStateChange'>): boolean;
3526
+
3527
+ /**
3528
+ * useSoundHistory — generic, per-track "what sounds has this track had?" stack.
3529
+ *
3530
+ * Powers the drawer "History" tab: restore any earlier sound, star favorites,
3531
+ * and (via the host plugin) persist across project reopen. The SDK is ignorant
3532
+ * of WHAT a sound is — each plugin records an opaque `descriptor` (a drum sample
3533
+ * path / an instrument `{ displayName, zones }` / a synth Surge state blob) plus
3534
+ * a human `label`, and supplies `applySound` to re-apply a chosen descriptor.
3535
+ *
3536
+ * Persistence is the plugin's job: pass `opts.onChange` (called after every
3537
+ * mutation with the new state) to save, and call `restore()` on load to seed.
3538
+ * Favorited entries are never auto-evicted by the cap.
3539
+ *
3540
+ * Robustness: `applySound` + `onChange` are read through refs, so the returned
3541
+ * object is referentially STABLE regardless of whether the caller memoizes them.
3542
+ * Plugins list this object in `loadTracks` deps — an unstable return previously
3543
+ * caused a render loop, so keep it stable.
3544
+ *
3545
+ * @since SDK 2.13.0
3546
+ */
3547
+
3548
+ /** A track's ordered sound history plus the index of the currently-applied sound. */
3549
+ interface TrackSoundHistory {
3550
+ entries: readonly SoundHistoryEntry[];
3551
+ /** Index into `entries` of the currently-applied sound; -1 when empty. */
3552
+ cursor: number;
3553
+ }
3554
+ interface UseSoundHistoryOptions {
3555
+ /** Max non-favorited entries kept per track (favorites are never evicted). Default 24. */
3556
+ max?: number;
3557
+ /**
3558
+ * Called after every mutation (record/undo/restoreTo/toggleFavorite/clear) with the
3559
+ * track's new state — use it to persist. NOT called by `restore()` (that's a load).
3560
+ */
3561
+ onChange?: (trackId: string, state: TrackSoundHistory) => void;
3562
+ }
3563
+ interface UseSoundHistoryResult {
3564
+ /** Remember a sound that was just applied (generation, scene-load, or shuffle). */
3565
+ record(trackId: string, descriptor: unknown, label: string): void;
3566
+ /** Re-apply the sound one step before the current one. Resolves true if it moved. */
3567
+ undo(trackId: string): Promise<boolean>;
3568
+ /** Re-apply a specific entry by index. Resolves true if it applied. */
3569
+ restoreTo(trackId: string, index: number): Promise<boolean>;
3570
+ /** The ordered history + cursor for a track (safe empty default). */
3571
+ list(trackId: string): TrackSoundHistory;
3572
+ /** Whether there is an earlier sound to step back to. */
3573
+ canUndo(trackId: string): boolean;
3574
+ /** Forget a track's history (e.g. on regenerate). Persists the cleared state. */
3575
+ clear(trackId: string): void;
3576
+ /** Forget ALL tracks' history in memory (e.g. before re-seeding on scene load). */
3577
+ reset(): void;
3578
+ /** Seed a track's full history (e.g. from persistence on load). Does NOT fire onChange. */
3579
+ restore(trackId: string, state: {
3580
+ entries?: readonly SoundHistoryEntry[];
3581
+ cursor?: number;
3582
+ } | null | undefined): void;
3583
+ /** Toggle the favorite flag on an entry (favorites survive cap eviction). */
3584
+ toggleFavorite(trackId: string, index: number): void;
3585
+ }
3586
+ declare function useSoundHistory(applySound: (trackId: string, descriptor: unknown) => Promise<void>, opts?: UseSoundHistoryOptions): UseSoundHistoryResult;
3587
+
1649
3588
  /**
1650
3589
  * Plugin SDK Version
1651
3590
  *
@@ -1654,7 +3593,7 @@ declare function useSceneState<T>(activeSceneId: string | null, initialValue: T)
1654
3593
  * Registry checks semver.gte(PLUGIN_SDK_VERSION, manifest.minHostVersion)
1655
3594
  * during activation and marks incompatible plugins accordingly.
1656
3595
  */
1657
- declare const PLUGIN_SDK_VERSION = "2.3.0";
3596
+ declare const PLUGIN_SDK_VERSION = "2.25.0";
1658
3597
 
1659
3598
  /**
1660
3599
  * FX Preset Definitions
@@ -1708,4 +3647,98 @@ declare function sliderToDb(slider: number): number;
1708
3647
  */
1709
3648
  declare function dbToSlider(db: number): number;
1710
3649
 
1711
- export { type AudioInputDevice, 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 PluginTrimWindow, type PluginUIProps, type PostProcessOptions, type RecordingChunkFinalizedEvent, type RecordingTargetInfo, 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 };
3650
+ /**
3651
+ * Format the cross-plugin concurrent-track context into a prose block
3652
+ * that's safe to drop straight into an LLM user-prompt. Both the synth
3653
+ * and drum builtin panels use this so the rendered prompt stays
3654
+ * consistent across generators — and so a single change here propagates
3655
+ * to every plugin that calls `host.getGenerationContext()`.
3656
+ *
3657
+ * Per-track payload follows the user's preferred shape (raw note JSON
3658
+ * grouped by chord) so the model sees velocity / start-beat /
3659
+ * duration / pitch verbatim and can reason about feel + harmony.
3660
+ *
3661
+ * Returns the empty string when there are no concurrent tracks — call
3662
+ * sites can `if (block) push(block)` rather than baking in a placeholder.
3663
+ */
3664
+
3665
+ declare function formatConcurrentTracks(ctx: PluginGenerationContext): string;
3666
+
3667
+ /**
3668
+ * Lightweight, dependency-free semantic matching for sample selection.
3669
+ *
3670
+ * Sample generators (drums, instruments) ship a short StableAudio text
3671
+ * prompt next to every sample ("tight 909-style kick one shot, hard click
3672
+ * transient, short punchy body, dry, no hi hats, no loop"). When the user
3673
+ * asks for "a 1950s style boom bap kick" we want to pick the sample whose
3674
+ * prompt is closest to that intent — instead of a uniform random draw —
3675
+ * while still preserving variety so a vague "give me a kick" doesn't return
3676
+ * the identical sample every time.
3677
+ *
3678
+ * Design notes:
3679
+ * - Pure functions, no I/O, no SDK-type dependencies → trivially unit
3680
+ * testable with an injected `rng`, and safe to call from either the
3681
+ * main or renderer process.
3682
+ * - Scoring is IDF-weighted query-coverage (a TF-IDF / BM25-lite). The
3683
+ * IDF is derived from the candidate pool itself, so it is STRUCTURAL —
3684
+ * no hand-maintained synonym tables. Rare, discriminating tokens in the
3685
+ * prompts ("909", "dusty", "tube") dominate; corpus-universal filler
3686
+ * ("one", "shot", "dry") washes out to ~zero IDF on its own.
3687
+ * - The near-universal negative clauses StableAudio prompts carry
3688
+ * ("no hi hats", "no loop", "no melody") are stripped before tokenizing;
3689
+ * they are pure noise for matching.
3690
+ * - Selection is softmax-weighted random among the top-k. Flat scores →
3691
+ * ~uniform (≈ the old random behavior); a clear winner → tight
3692
+ * convergence. The all-zero (no-signal) case is intentionally left to
3693
+ * the caller to fall back to its existing random path over the full
3694
+ * pool — see `scorePromptMatch`'s contract below.
3695
+ */
3696
+ /**
3697
+ * Tokenize a prompt or query into matchable lowercase tokens.
3698
+ *
3699
+ * 1. Drop comma-delimited negative clauses ("no hi hats", "no loop").
3700
+ * 2. Lowercase, split on any non-alphanumeric run.
3701
+ * 3. Drop stop-words and 1–2 digit numeric noise ("01", "02") while
3702
+ * keeping meaningful numerics ("808", "909", "1950").
3703
+ */
3704
+ declare function tokenizePrompt(text: string): string[];
3705
+ /**
3706
+ * Score each candidate prompt against the query, returning a parallel array
3707
+ * of scores in [0, 1] (1 = the candidate covers all of the query's
3708
+ * discriminating intent).
3709
+ *
3710
+ * Contract: a returned max of 0 means the query shares NO matchable token
3711
+ * with any candidate (no signal). Callers should treat that as "fall back to
3712
+ * the existing uniform-random pick over the full pool" so vague queries keep
3713
+ * today's variety rather than biasing toward an arbitrary top-k slice.
3714
+ */
3715
+ declare function scorePromptMatch(query: string, candidatePrompts: ReadonlyArray<string>): number[];
3716
+ /** One scored candidate. `key` (if present) is what `excludeKeys` matches on. */
3717
+ interface ScoredCandidate<T> {
3718
+ item: T;
3719
+ score: number;
3720
+ key?: string;
3721
+ }
3722
+ interface PickTopKOptions {
3723
+ /** Consider only the top-k by score (default 5). */
3724
+ k?: number;
3725
+ /**
3726
+ * Softmax temperature (default 0.3). Lower → sharper preference for the
3727
+ * top match; higher → flatter (more variety). Scores are in [0, 1].
3728
+ */
3729
+ temperature?: number;
3730
+ /** Candidate keys to exclude (e.g. shuffle history). */
3731
+ excludeKeys?: ReadonlySet<string>;
3732
+ /** Injectable RNG in [0, 1) for deterministic tests (default Math.random). */
3733
+ rng?: () => number;
3734
+ }
3735
+ /**
3736
+ * Pick one candidate via softmax-weighted random selection among the top-k
3737
+ * by score. Returns null only when the pool is empty after exclusion.
3738
+ *
3739
+ * Equal scores → equal weights → uniform pick among the top-k, so this
3740
+ * degrades gracefully toward random when the query gives no preference.
3741
+ */
3742
+ declare function pickTopKWeighted<T>(scored: ReadonlyArray<ScoredCandidate<T>>, options?: PickTopKOptions): T | null;
3743
+
3744
+ 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, type CrossfadeVolumeCurves, 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, type VolumeAutomationPoint, VolumeSlider, type WaveformPeaks, WaveformView, type WaveformViewProps, analyzeWavPeak, asCrossfadeMeta, buildCrossfadeInpaintPrompt, buildCrossfadeVolumeCurves, 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 };