@signalsandsorcery/plugin-sdk 2.0.1 → 2.0.2

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.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/types/plugin-sdk.types.ts","../src/types/fx-toggle.types.ts","../src/components/TrackRow.tsx","../src/components/InstrumentDrawer.tsx","../src/components/VolumeSlider.tsx","../src/utils/volume-conversion.ts","../src/components/PanSlider.tsx","../src/constants/fx-presets.ts","../src/components/FxToggleBar.tsx","../src/components/SorceryProgressBar.tsx","../src/hooks/useSceneState.ts","../src/constants/sdk-version.ts"],"sourcesContent":["/**\n * @sas/plugin-sdk — Public API\n *\n * Everything an external plugin author needs to build a generator plugin\n * for Signals & Sorcery.\n */\n\n// ============================================================================\n// Types — Core plugin contract\n// ============================================================================\n\nexport type {\n GeneratorType,\n InstrumentDescriptor,\n GeneratorPlugin,\n PluginUIProps,\n PluginHost,\n ExportedPluginData,\n CreateTrackOptions,\n PluginTrackHandle,\n PluginTrackInfo,\n PluginSynthInfo,\n PluginTrackRuntimeState,\n TrackStateChangeListener,\n PluginFxCategoryDetailState,\n PluginTrackFxDetailState,\n MidiClipData,\n PluginMidiNote,\n MidiWriteResult,\n ExportMidiBundleOptions,\n ExportMidiBundleResult,\n PostProcessOptions,\n MusicalContext,\n PluginChordTiming,\n PluginGenerationContext,\n PluginConcurrentTrackInfo,\n PluginChordSegment,\n TransportEvent,\n DeckBoundaryEvent,\n PluginTransportState,\n PluginSceneInfo,\n PluginSceneContext,\n BulkAddPlaceholderTrack,\n TransportEventListener,\n DeckBoundaryListener,\n SceneChangeListener,\n UnsubscribeFn,\n LLMGenerationRequest,\n LLMGenerationResult,\n PluginPresetData,\n ShufflePresetResult,\n PluginSettingsSchema,\n SettingDefinition,\n PluginSettingsStore,\n // AI skill surface — lets plugins declare LLM-callable actions\n // registered as namespaced tools (plugin:<id>:<skill>). Required for\n // plugins that expose a `chat` or similar agent-delegation skill.\n PluginSkill,\n PluginSkillInputSchema,\n PluginErrorCode,\n PluginManifest,\n PluginCapabilities,\n PluginFileDialogOptions,\n PluginDownloadOptions,\n PluginHttpRequestOptions,\n PluginHttpResponse,\n PluginSampleFilter,\n PluginSampleInfo,\n PluginSampleImportResult,\n PluginSampleTrackInfo,\n PluginAudioTextureRequest,\n PluginAudioTextureResult,\n ComposeSceneOptions,\n ComposeSceneResult,\n ComposeProgressListener,\n ComposeProgressEvent,\n PluginPresetInfo,\n SavePluginPresetOptions,\n PluginAppTool,\n PluginAppToolInputSchema,\n PluginAppToolResult,\n PluginStatus,\n PluginRegistration,\n StemType,\n PluginStemSplitResult,\n PluginStemTrackInfo,\n} from './types/plugin-sdk.types';\n\nexport { PluginError } from './types/plugin-sdk.types';\n\n// ============================================================================\n// Types — FX toggle system\n// ============================================================================\n\nexport type {\n FxCategory,\n FxPreset,\n MixInterpolation,\n FxPresetConfig,\n FxCategoryDetailState,\n TrackFxDetailState,\n TrackFxState,\n FxPresetDataEntry,\n FxPresetData,\n} from './types/fx-toggle.types';\n\nexport {\n FX_CATEGORIES,\n FX_CHAIN_ORDER,\n FX_ENGINE_PLUGIN_NAMES,\n FX_DISPLAY_LABELS,\n EMPTY_FX_STATE,\n DEFAULT_FX_DRY_WET,\n DEFAULT_FX_CATEGORY_DETAIL,\n EMPTY_FX_DETAIL_STATE,\n} from './types/fx-toggle.types';\n\n// ============================================================================\n// Components\n// ============================================================================\n\nexport { TrackRow, type SDKTrackRowProps } from './components/TrackRow';\nexport { InstrumentDrawer, type InstrumentDrawerProps } from './components/InstrumentDrawer';\nexport { VolumeSlider } from './components/VolumeSlider';\nexport { PanSlider } from './components/PanSlider';\nexport { FxToggleBar, type FxToggleBarProps } from './components/FxToggleBar';\nexport { SorceryProgressBar, calculateTimeBasedTarget } from './components/SorceryProgressBar';\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport { useSceneState } from './hooks/useSceneState';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n// VALID_INSTRUMENT_ROLES (SDK 1.x) removed in 2.0.0 — external plugins now\n// call `host.getValidRoles()` on PluginHost at runtime. The canonical list\n// lives in the assistant (src/music-engine/constants/instrument-classification.ts)\n// and is exposed via that accessor.\nexport { PLUGIN_SDK_VERSION } from './constants/sdk-version';\nexport { FX_PRESET_CONFIGS } from './constants/fx-presets';\n\n// ============================================================================\n// Utils\n// ============================================================================\n\nexport { sliderToDb, dbToSlider, SLIDER_UNITY, DB_MAX, DB_MIN } from './utils/volume-conversion';\n","/**\n * Plugin SDK Type Definitions\n *\n * Complete type system for the generator plugin architecture.\n * Plugins implement GeneratorPlugin and interact with the host via PluginHost.\n * All plugin output flows through TracktionEngine (MIDI or audio clips).\n */\n\nimport type { ComponentType, ReactNode } from 'react';\n\n// ============================================================================\n// Core Plugin Interface\n// ============================================================================\n\n/** What kind of Tracktion content this plugin creates */\nexport type GeneratorType = 'midi' | 'audio' | 'sample' | 'hybrid';\n\n/** Describes an available instrument plugin (VST3/AU synth) on the system. */\nexport interface InstrumentDescriptor {\n /** Stable plugin identifier for loading (VST3 TUID or AU component ID) */\n pluginId: string;\n /** Display name */\n name: string;\n /** Plugin manufacturer */\n manufacturer: string;\n /** Plugin format */\n type: 'vst3' | 'au' | 'vst' | 'internal';\n /** Plugin category (from scan) */\n category: string;\n /** Whether this plugin is currently installed/available */\n missing?: boolean;\n}\n\n/** Every generator plugin must implement this interface. */\nexport interface GeneratorPlugin {\n /** Unique ID, npm-style scope: '@sas/synth-generator', '@user/my-plugin' */\n readonly id: string;\n /** Human-readable name shown in accordion header */\n readonly displayName: string;\n /** Semver version string */\n readonly version: string;\n /** Short description for settings/marketplace */\n readonly description: string;\n /** 24x24 icon — data URL, relative path from plugin dir, or undefined */\n readonly icon?: string;\n /** What kind of Tracktion content this plugin creates */\n readonly generatorType: GeneratorType;\n /** Minimum host SDK version this plugin requires */\n readonly minHostVersion?: string;\n\n /**\n * Called once when plugin is loaded. Receives the PluginHost API.\n * If this throws, plugin is marked as failed and not rendered.\n */\n activate(host: PluginHost): Promise<void>;\n\n /**\n * Called when plugin is being unloaded (disable, uninstall, app quit).\n * Must complete within 5 seconds or host force-kills.\n */\n deactivate(): Promise<void>;\n\n /**\n * Return the React component rendered inside the accordion section.\n * Component receives PluginUIProps from the host.\n */\n getUIComponent(): ComponentType<PluginUIProps>;\n\n /**\n * Return JSON Schema for plugin-specific settings.\n * Host auto-renders a settings form. Return null if no settings.\n */\n getSettingsSchema(): PluginSettingsSchema | null;\n\n /**\n * Optional: Called when the active scene changes.\n */\n onSceneChanged?(sceneId: string | null): Promise<void>;\n\n /**\n * Optional: Called when the generation context changes\n * (chords updated, tracks added/removed, BPM changed).\n */\n onContextChanged?(context: MusicalContext): void;\n\n /**\n * Optional: Declare LLM-callable skills this plugin provides.\n * Skills are registered as namespaced tools (plugin:<pluginId>:<skillId>)\n * and become available to AI agents for orchestration.\n *\n * Example: the chat-panel plugin declares a `chat` skill so external\n * agents (Claude Code, OpenClaw) can delegate scene-scoped natural\n * language work to the in-app agent via a single call.\n */\n getSkills?(): PluginSkill[];\n}\n\n// ============================================================================\n// Plugin Skills (AI Harness)\n// ============================================================================\n\n/** An LLM-callable action declared by a plugin. */\nexport interface PluginSkill {\n /** Unique skill id within this plugin (e.g., 'chat', 'generate_bassline') */\n id: string;\n /** Human-readable description — drives LLM tool selection */\n description: string;\n /** JSON Schema for the skill's input parameters */\n inputSchema: PluginSkillInputSchema;\n /** Whether this skill only reads state (no mutations). Default: false */\n isReadOnly?: boolean;\n}\n\n/** JSON Schema shape for skill input parameters. */\nexport interface PluginSkillInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n// ============================================================================\n// Plugin UI Props\n// ============================================================================\n\n/** Props passed to every plugin's React component by the host */\nexport interface PluginUIProps {\n /** The scoped PluginHost API instance for this plugin */\n host: PluginHost;\n /** Currently active scene ID (null if none selected) */\n activeSceneId: string | null;\n /** Whether the user is authenticated (for LLM access) */\n isAuthenticated: boolean;\n /** Whether all systems are connected (engine, gateway) */\n isConnected: boolean;\n /** Which workstation deck this is rendered in */\n deckId?: 'left' | 'right';\n /** Plugin calls this to set/clear header buttons. Pass null to clear. */\n onHeaderContent?: (content: ReactNode | null) => void;\n /** Plugin calls this to show/hide the loading spinner in the header. */\n onLoading?: (loading: boolean) => void;\n /** Scene-level context: contract state, chords, BPM, etc. Null if no scene. */\n sceneContext?: PluginSceneContext | null;\n /** Callback to open the scene selector (Scenes accordion section). */\n onSelectScene?: (() => void) | null;\n /** Callback to open the contract/chords section (for \"Generate a Contract\" CTA). */\n onOpenContract?: (() => void) | null;\n /** Callback to expand this plugin's own accordion section. */\n onExpandSelf?: (() => void) | null;\n}\n\n// ============================================================================\n// PluginHost API\n// ============================================================================\n\n/** Scoped API surface that plugins interact with. Plugins NEVER get direct TracktionEngine access. */\nexport interface PluginHost {\n // --- Track Management (ownership-scoped) ---\n\n /** Create a new track in the active scene. Host enforces ownership and scene routing. */\n createTrack(options: CreateTrackOptions): Promise<PluginTrackHandle>;\n\n /** Delete a track previously created by THIS plugin. */\n deleteTrack(trackId: string): Promise<void>;\n\n /** Get all tracks this plugin owns in the active scene. */\n getPluginTracks(): Promise<PluginTrackHandle[]>;\n\n /** Adopt unowned tracks in the active scene matching this plugin's generator type. */\n adoptSceneTracks(): Promise<PluginTrackHandle[]>;\n\n /** Get info about a specific owned track. */\n getTrackInfo(trackId: string): Promise<PluginTrackInfo>;\n\n /** Set track mute state. Only works on owned tracks. */\n setTrackMute(trackId: string, muted: boolean): Promise<void>;\n\n /** Set track volume (linear 0.0 - 1.0). Only works on owned tracks. */\n setTrackVolume(trackId: string, volume: number): Promise<void>;\n\n /** Set track pan (-1.0 left to 1.0 right). Only works on owned tracks. */\n setTrackPan(trackId: string, pan: number): Promise<void>;\n\n /** Set track solo state. Only works on owned tracks. */\n setTrackSolo(trackId: string, solo: boolean): Promise<void>;\n\n /** Rename a track. Only works on owned tracks. */\n setTrackName(trackId: string, name: string): Promise<void>;\n\n /**\n * Persist a track's musical role to the `tracks.role` column. Call this\n * after an LLM generation classifies the track (e.g. `'bass'`, `'lead'`,\n * `'pad'`, `'fx'`, `'kicks'`) so downstream features — especially the v1\n * transition generator's layer classifier — can see the role.\n *\n * Canonical values understood by the transition classifier include\n * `bass`, `drums`, `lead`, `chords`, `pad`, `arp`, `fx`, `kicks`,\n * `snares`, `hats`, `clap`, `perc`, `riser`, `impact`. Anything else is\n * stored verbatim but won't match the neutral-role set.\n *\n * Only works on owned tracks.\n */\n setTrackRole(trackId: string, role: string): Promise<void>;\n\n /** Shuffle preset: keep MIDI, apply a random preset from the same category. Only works on owned tracks. */\n shufflePreset(trackId: string): Promise<ShufflePresetResult>;\n\n /** Duplicate track: copy MIDI + role to a new track with a different preset. Only works on owned tracks. */\n duplicateTrack(trackId: string): Promise<PluginTrackHandle>;\n\n /**\n * Return the canonical list of valid role tokens that the host's\n * classifier and UI understand. Plugins should use this list when\n * building LLM prompts or validating role values before calling\n * {@link setTrackRole}.\n *\n * The assistant owns the canonical taxonomy — plugins MUST NOT ship\n * their own hardcoded list, which would drift from the host. Pair with\n * {@link setTrackRole} to persist a classified role.\n *\n * @since SDK 2.0.0\n */\n getValidRoles(): readonly string[];\n\n // --- FX Operations (ownership-scoped) ---\n\n /** Get detailed FX state for a track (enabled, preset, dry/wet per category). */\n getTrackFxState(trackId: string): Promise<PluginTrackFxDetailState>;\n\n /** Toggle an FX category on/off for a track. */\n toggleTrackFx(trackId: string, category: string, enabled: boolean): Promise<void>;\n\n /** Set FX preset for a track. Returns the new dry/wet value if applicable. */\n setTrackFxPreset(trackId: string, category: string, presetIndex: number): Promise<{ dryWet?: number }>;\n\n /** Set FX dry/wet level for a track. */\n setTrackFxDryWet(trackId: string, category: string, value: number): Promise<void>;\n\n // --- Real-time Track State ---\n\n /** Subscribe to real-time track state changes (mute, solo, volume, pan). Returns unsubscribe fn. */\n onTrackStateChange(listener: TrackStateChangeListener): UnsubscribeFn;\n\n // --- MIDI Operations ---\n\n /** Write MIDI notes to a track this plugin owns. Replaces existing MIDI. */\n writeMidiClip(trackId: string, clip: MidiClipData): Promise<MidiWriteResult>;\n\n /** Clear all MIDI from a track this plugin owns. */\n clearMidi(trackId: string): Promise<void>;\n\n /**\n * Export all tracks owned by this plugin in the active scene as a ZIP bundle\n * of Standard MIDI Files (one .mid per track, named after each track with\n * collision-avoidance suffixes). Prompts the user for a save location.\n *\n * Tracks with no MIDI data are skipped. Returns the path written, or\n * `{ canceled: true }` if the user dismissed the save dialog.\n *\n * @since SDK 1.1.0\n */\n exportTracksAsMidiBundle(\n options?: ExportMidiBundleOptions\n ): Promise<ExportMidiBundleResult>;\n\n /**\n * Run the host's MIDI post-processing pipeline on raw notes.\n * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.\n */\n postProcessMidi(notes: PluginMidiNote[], options: PostProcessOptions): Promise<PluginMidiNote[]>;\n\n // --- Audio Operations ---\n\n /** Place an audio file on a track this plugin owns. */\n writeAudioClip(trackId: string, filePath: string, position?: number): Promise<void>;\n\n /**\n * Render a single track to a temporary WAV file and return its path.\n * Only works on owned tracks. For MIDI/synth tracks the host mutes siblings\n * and renders the scene. For single-clip audio tracks the host MAY take a\n * copy-source fast path.\n * @since SDK 1.2.0\n */\n exportTrackAudio?(trackId: string): Promise<ExportTrackAudioResult>;\n\n /**\n * Run a chain of audio operations on an input WAV via the bundled\n * sas-audio-processor binary. Unsupported ops throw NOT_IMPLEMENTED.\n * @since SDK 1.2.0\n */\n processAudio?(\n inputPath: string,\n operations: AudioProcessingOp[]\n ): Promise<ProcessAudioResult>;\n\n /**\n * Replace a track's audio content. For audio tracks, clears clips and\n * adds the new audio. For MIDI/synth tracks, the original row is stashed\n * in plugin_data and a new audio_tracks row is inserted (MIDI is lost).\n * @since SDK 1.2.0\n */\n replaceTrackAudio?(trackId: string, audioPath: string): Promise<void>;\n\n // --- Plugin/Synth Operations ---\n\n /** Load a VST3/AU plugin onto a track this plugin owns. */\n loadSynthPlugin(trackId: string, pluginName: string): Promise<number>;\n\n /** Set plugin state (base64-encoded preset data). */\n setPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;\n\n /** Get current plugin state (base64-encoded). */\n getPluginState(trackId: string, pluginIndex: number): Promise<string>;\n\n /** List plugins currently loaded on a track. */\n getTrackPlugins(trackId: string): Promise<PluginSynthInfo[]>;\n\n /** Remove a plugin from a track. */\n removePlugin(trackId: string, pluginIndex: number): Promise<void>;\n\n /** Check if a specific VST/AU plugin is available on the system. */\n isPluginAvailable(pluginName: string): Promise<boolean>;\n\n // --- Instrument Plugin Selection ---\n\n /** Get available instrument plugins (VST3/AU synths) scanned by the engine. */\n getAvailableInstruments(): Promise<InstrumentDescriptor[]>;\n\n /** Get the instrument plugin currently loaded on a track. Null = default (Surge XT). */\n getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;\n\n /** Change the instrument plugin on a track. Preserves MIDI data. */\n setTrackInstrument(trackId: string, pluginId: string): Promise<void>;\n\n /** Open the instrument plugin's native editor GUI as a floating window. */\n showInstrumentEditor(trackId: string): Promise<void>;\n\n /** Close the instrument plugin's editor window. */\n hideInstrumentEditor(trackId: string): Promise<void>;\n\n // --- Scene Context (read-only) ---\n\n /** Get the FULL generation context for the active scene. */\n getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;\n\n /** Get lightweight musical context (no concurrent track MIDI data). */\n getMusicalContext(): Promise<MusicalContext>;\n\n /** Get the active scene ID. Null if no scene is active. */\n getActiveSceneId(): string | null;\n\n /** Get list of all scenes in the project. */\n getSceneList(): Promise<PluginSceneInfo[]>;\n\n // --- Transport & Playback Events ---\n\n /** Subscribe to transport state changes. Returns unsubscribe function. */\n onTransportEvent(listener: TransportEventListener): UnsubscribeFn;\n\n /** Subscribe to deck boundary events. Returns unsubscribe function. */\n onDeckBoundary(listener: DeckBoundaryListener): UnsubscribeFn;\n\n /** Subscribe to scene change events. Returns unsubscribe function. */\n onSceneChange(listener: SceneChangeListener): UnsubscribeFn;\n\n /** Get current transport state (one-shot). */\n getTransportState(): Promise<PluginTransportState>;\n\n // --- LLM Access (metered, authenticated) ---\n\n /** Generate text/JSON via the host's authenticated LLM service. */\n generateWithLLM(request: LLMGenerationRequest): Promise<LLMGenerationResult>;\n\n /** Check if LLM access is available (user authenticated + gateway reachable). */\n isLLMAvailable(): Promise<boolean>;\n\n // --- App Tool Bridge ---\n\n /**\n * List the host's registered app tools. Used by plugins (e.g. the chat\n * panel) that want to expose the same surface external AI agents have.\n *\n * `opts.scope` filters by scope tag — scene-scoped consumers pass\n * `'scene'` to hide project-level tools they shouldn't call. When omitted,\n * every tool regardless of scope is returned.\n *\n * @since SDK 1.2.0\n */\n listAppTools(opts?: { scope?: 'scene' | 'project' }): Promise<PluginAppTool[]>;\n\n /**\n * Execute a host app tool by name. Delegates to the in-process\n * ToolRegistry — every mutation broadcasts to the UI automatically.\n *\n * For scene-scoped tools tagged with `autoBindSceneId`, the host\n * overrides the caller's `sceneId` param with the currently-active\n * scene. That keeps a scene-bound caller from accidentally targeting\n * another scene.\n *\n * @since SDK 1.2.0\n */\n executeAppTool(\n name: string,\n params: Record<string, unknown>\n ): Promise<PluginAppToolResult>;\n\n // --- Preset System ---\n\n /** Get available preset categories for a synth plugin. */\n getPresetCategories(pluginName: string): Promise<string[]>;\n\n /** Get a random preset from a category. */\n getRandomPreset(category: string): Promise<PluginPresetData | null>;\n\n /** Get a specific preset by name from a category. */\n getPresetByName(category: string, name: string): Promise<PluginPresetData | null>;\n\n /** Use LLM to classify a text description into a preset category. */\n classifyPresetCategory(description: string): Promise<string>;\n\n // --- Storage & Settings ---\n\n /** Get absolute path to this plugin's isolated data directory. */\n getDataDirectory(): string;\n\n /** Persisted key-value settings store. */\n settings: PluginSettingsStore;\n\n // --- Scoped Data API ---\n\n /** Get a value from scene-scoped plugin data. */\n getSceneData<T = unknown>(sceneId: string, key: string): Promise<T | null>;\n\n /** Set a value in scene-scoped plugin data. */\n setSceneData(sceneId: string, key: string, value: unknown): Promise<void>;\n\n /** Get all key-value pairs for a scene. */\n getAllSceneData(sceneId: string): Promise<Record<string, unknown>>;\n\n /** Delete a key from scene-scoped plugin data. */\n deleteSceneData(sceneId: string, key: string): Promise<void>;\n\n /** Get the full project-scoped state object. */\n getProjectData<T = unknown>(key: string): Promise<T | null>;\n\n /** Set a project-scoped data value. */\n setProjectData(key: string, value: unknown): Promise<void>;\n\n // --- Notifications & Progress ---\n\n /** Show a toast notification to the user. */\n showToast(type: 'info' | 'success' | 'warning' | 'error', title: string, message?: string): void;\n\n /** Set progress indicator on a specific track. -1 to hide. */\n setProgress(trackId: string, progress: number): void;\n\n /** Set a global status message in the plugin's accordion header. */\n setStatusMessage(message: string | null): void;\n\n /** Request user confirmation via a modal dialog. */\n confirmAction(title: string, message: string): Promise<boolean>;\n\n // --- File System (Phase 2) ---\n\n /** Show a native file open dialog. Requires 'fileDialog' capability. */\n showOpenDialog(options: PluginFileDialogOptions): Promise<string[] | null>;\n\n /** Show a native file save dialog. Requires 'fileDialog' capability. */\n showSaveDialog(options: PluginFileDialogOptions): Promise<string | null>;\n\n /** Download a file to the plugin's data directory. */\n downloadFile(url: string, filename: string, options?: PluginDownloadOptions): Promise<string>;\n\n /** Copy a file into the plugin's data directory. */\n importFile(sourcePath: string, destFilename: string): Promise<string>;\n\n // --- Network (Phase 2, capability-gated) ---\n\n /** Make an HTTP request. Requires 'network' capability with allowedHosts. */\n httpRequest(options: PluginHttpRequestOptions): Promise<PluginHttpResponse>;\n\n // --- Secure Storage (Phase 2) ---\n\n /** Store a secret in the OS keychain (plugin-scoped). */\n storeSecret(key: string, value: string): Promise<void>;\n\n /** Retrieve a secret from the OS keychain (plugin-scoped). */\n getSecret(key: string): Promise<string | null>;\n\n /** Delete a secret from the OS keychain (plugin-scoped). */\n deleteSecret(key: string): Promise<void>;\n\n // --- Sample Library (Phase 2) ---\n\n /** Query the sample library with optional filters. */\n getSamples(filter?: PluginSampleFilter): Promise<PluginSampleInfo[]>;\n\n /** Get a single sample by ID. */\n getSampleById(id: string): Promise<PluginSampleInfo | null>;\n\n /** Import audio files into the sample library. */\n importSamples(filePaths: string[]): Promise<PluginSampleImportResult>;\n\n /** Create a sample track in the active scene. */\n createSampleTrack(sampleId: string, options?: { name?: string }): Promise<PluginTrackHandle>;\n\n /** Delete a sample track. */\n deleteSampleTrack(trackId: string): Promise<void>;\n\n /** Get all sample tracks in the active scene. Re-establishes ownership. */\n getPluginSampleTracks(): Promise<PluginSampleTrackInfo[]>;\n\n /** Time-stretch a sample to a target BPM. Returns the new sample info. */\n timeStretchSample(sampleId: string, targetBpm: number): Promise<PluginSampleInfo>;\n\n /**\n * Lightweight one-shot sample audition through the cue (headphone) output.\n *\n * Plays the file via a dedicated SimpleLoopPlayer instance in the audio\n * engine — no Tracktion track or clip is created, no BPM matching, no\n * sync. Calling previewSample again with a different file replaces the\n * current preview cleanly. Independent of loop-b: starting/stopping a\n * preview never affects the performance deck and vice versa.\n */\n previewSample(filePath: string): Promise<void>;\n\n /**\n * Stop any in-flight sample preview started by previewSample(). Safe to\n * call when no preview is active — never throws.\n */\n stopPreview(): Promise<void>;\n\n // --- Audio Generation (Phase 2) ---\n\n /** Invoke the host's audio texture generation pipeline. */\n generateAudioTexture(request: PluginAudioTextureRequest): Promise<PluginAudioTextureResult>;\n\n // --- Scene Composition ---\n\n /** Trigger bulk composition for the active scene (LLM plans arrangement, creates tracks, generates MIDI). */\n composeScene(options: ComposeSceneOptions): Promise<ComposeSceneResult>;\n\n /** Subscribe to composition progress events (planning, generating, complete, error). */\n onComposeProgress(listener: ComposeProgressListener): UnsubscribeFn;\n\n /** Subscribe to engine ready events (fires when the engine finishes loading tracks after a scene change). */\n onEngineReady(listener: () => void): UnsubscribeFn;\n\n // --- MIDI Extensions (Phase 2) ---\n\n /** Audition a single note on a track (fire-and-forget preview). */\n auditionNote(trackId: string, pitch: number, velocity: number, durationMs: number): Promise<void>;\n\n // --- Plugin Presets (Phase 2) ---\n\n /** Get presets for this plugin, optionally filtered by category. */\n getPluginPresets(category?: string): Promise<PluginPresetInfo[]>;\n\n /** Save a new preset for this plugin. */\n savePluginPreset(options: SavePluginPresetOptions): Promise<PluginPresetInfo>;\n\n /** Delete a plugin preset by ID. */\n deletePluginPreset(id: string): Promise<void>;\n\n // --- Performance / Logging (Phase 2) ---\n\n /** Log a performance metric. */\n logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;\n\n /** Start a timer. Returns a stop function that logs the duration. */\n startTimer(name: string): () => void;\n\n // --- Stem Splitting ---\n\n /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */\n splitStems(trackId: string): Promise<PluginStemSplitResult>;\n\n /** Check if the stem splitter binary is available. */\n isStemSplitterAvailable(): Promise<boolean>;\n}\n\n// ============================================================================\n// Stem Splitting Types\n// ============================================================================\n\n/** Stem type identifiers */\nexport type StemType = 'vocals' | 'drums' | 'bass' | 'other';\n\n/** Result of splitting an audio track into stems */\nexport interface PluginStemSplitResult {\n /** Created stem tracks with audio loaded (all auto-muted) */\n stems: PluginStemTrackInfo[];\n}\n\n/** Information about a single stem track created by stem splitting */\nexport interface PluginStemTrackInfo {\n /** The stem type (vocals, drums, bass, other) */\n stemType: StemType;\n /** Track handle for the new stem track */\n track: PluginTrackHandle;\n}\n\n// ============================================================================\n// Exported Plugin Data Types (for .sasproj portability)\n// ============================================================================\n\nexport interface ExportedPluginData {\n pluginId: string;\n scope: 'project' | 'scene' | 'global';\n scopeId: string | null;\n key: string;\n value: string; // JSON-serialized\n}\n\n// ============================================================================\n// Track Types\n// ============================================================================\n\nexport interface CreateTrackOptions {\n /** Display name for the track. Auto-generated if omitted. */\n name?: string;\n /** Musical role hint: 'bass', 'drums', 'lead', 'chords', 'pad', 'arp', 'fx' */\n role?: string;\n /** Load a synth plugin immediately (default: false) */\n loadSynth?: boolean;\n /** Which synth to load (default: 'Surge XT'). Ignored if loadSynth=false. */\n synthName?: string;\n /**\n * Stable plugin identifier for a custom instrument (VST3 TUID or AU component ID).\n * If provided with loadSynth=true, loads this plugin instead of synthName.\n * Null/undefined = use default (Surge XT).\n */\n instrumentPluginId?: string | null;\n /** Metadata stored in DB. Plugins can use this for plugin-specific data. */\n metadata?: Record<string, unknown>;\n}\n\nexport interface PluginTrackHandle {\n /** Tracktion engine track ID (stable, GUID-based) */\n id: string;\n /** Display name */\n name: string;\n /** Database row ID */\n dbId: string;\n /** Musical role (if set) */\n role?: string;\n /** Prompt from tracks table (fallback when plugin_data not yet populated) */\n prompt?: string;\n /** Custom instrument plugin ID (null = default Surge XT) */\n instrumentPluginId?: string | null;\n /** Custom instrument display name (null = Surge XT) */\n instrumentName?: string | null;\n}\n\nexport interface PluginTrackInfo extends PluginTrackHandle {\n /** Is track muted? */\n muted: boolean;\n /** Is track soloed? */\n soloed: boolean;\n /** Volume (linear 0-1) */\n volume: number;\n /** Pan (-1 to 1) */\n pan: number;\n /** Loaded plugins on this track */\n plugins: PluginSynthInfo[];\n /** Has MIDI clips? */\n hasMidi: boolean;\n /** Has audio clips? */\n hasAudio: boolean;\n}\n\nexport interface PluginSynthInfo {\n index: number;\n name: string;\n type: string; // 'VST3' | 'AudioUnit' | 'Internal'\n enabled: boolean;\n}\n\n// ============================================================================\n// Real-time Track State Types\n// ============================================================================\n\n/** Real-time runtime state of a track (pushed from engine) */\nexport interface PluginTrackRuntimeState {\n id: string;\n muted: boolean;\n solo: boolean;\n volume: number;\n pan: number;\n}\n\n/** Listener for real-time track state changes */\nexport type TrackStateChangeListener = (trackId: string, state: PluginTrackRuntimeState) => void;\n\n// ============================================================================\n// FX Detail Types (SDK-friendly re-export)\n// ============================================================================\n\n/** Per-category FX detail state */\nexport interface PluginFxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Full FX detail state for a track — one entry per FX category */\nexport type PluginTrackFxDetailState = Record<string, PluginFxCategoryDetailState>;\n\n// ============================================================================\n// MIDI Types\n// ============================================================================\n\nexport interface MidiClipData {\n /** Start time in seconds */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n /** BPM for beat<->time conversion */\n tempo: number;\n /** MIDI notes */\n notes: PluginMidiNote[];\n}\n\nexport interface PluginMidiNote {\n /** MIDI pitch 0-127 */\n pitch: number;\n /** Start position in quarter-note beats (0 = beginning of clip) */\n startBeat: number;\n /** Duration in quarter-note beats */\n durationBeats: number;\n /** Velocity 1-127 */\n velocity: number;\n /** MIDI channel 0-15 (default: 0) */\n channel?: number;\n}\n\nexport interface MidiWriteResult {\n /** Number of notes written */\n notesInserted: number;\n /** Actual bars covered */\n bars: number;\n}\n\n/**\n * Options for {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport interface ExportMidiBundleOptions {\n /** Default ZIP filename suggested in the save dialog (without extension). */\n defaultName?: string;\n}\n\n/**\n * Result of {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport type ExportMidiBundleResult =\n | { success: true; filePath: string; trackCount: number; skippedCount: number }\n | { success: false; canceled: true }\n | { success: false; canceled?: false; error: string };\n\n// ============================================================================\n// Audio Processing Bridge (SDK 1.2.0 — see ai-orchestration-design.md §16)\n// ============================================================================\n\n/** @since SDK 1.2.0 */\nexport interface ExportTrackAudioResult {\n path: string;\n bpm: number;\n durationMs: number;\n fromCopyFastPath?: boolean;\n}\n\n/** @since SDK 1.2.0 */\nexport interface ProcessAudioResult {\n outputPath: string;\n}\n\n/** @since SDK 1.2.0 */\nexport type AudioProcessingOp =\n | { tool: 'normalize' }\n | { tool: 'compress'; params?: { threshold?: number; ratio?: number } }\n | { tool: 'eq'; params?: { low_gain?: number; mid_gain?: number; high_gain?: number } }\n | { tool: 'reverb'; params?: { room_size?: number; dry_wet?: number } }\n | { tool: 'pitch-shift'; params: { semitones: number } }\n | { tool: 'time-stretch'; params: { target_bpm: number } }\n | { tool: 'filter'; params: { type: 'lowpass' | 'highpass'; cutoff: number } }\n | { tool: 'gain'; params: { db: number } }\n | { tool: 'limit' }\n | { tool: 'trim'; params?: { start?: number; end?: number } };\n\nexport interface PostProcessOptions {\n /** Snap notes to grid (default: true) */\n quantize?: boolean;\n /** Grid size: '1/4', '1/8', '1/16', '1/32', '1/8T', '1/16T' (default: '1/16') */\n quantizeGrid?: string;\n /** Quantize strength 0-100 (default: 75) */\n quantizeStrength?: number;\n /** Swing amount 0-100 (default: 0) */\n swing?: number;\n /** Humanize timing/velocity variation 0-100 (default: 0) */\n humanize?: number;\n /** Enforce diatonic scale (default: false). Uses scene key/mode. */\n enforceScale?: boolean;\n /** Clamp notes to pitch range [low, high] */\n clampRegister?: [number, number];\n /** Remove overlapping notes on same pitch/channel (default: true) */\n removeOverlaps?: boolean;\n}\n\n// ============================================================================\n// Context Types\n// ============================================================================\n\nexport interface MusicalContext {\n key: string; // 'C', 'D', 'Eb', 'F#', etc.\n mode: string; // 'major', 'minor', 'dorian', 'mixolydian', etc.\n bpm: number; // 20-960\n bars: number; // Scene length in bars\n genre: string | null; // 'Drum & Bass', 'Lo-fi Hip Hop', etc.\n timeSignature: string; // '4/4', '3/4', '6/8'\n chordProgression: PluginChordTiming[];\n}\n\nexport interface PluginChordTiming {\n /** Chord symbol: 'Cm7', 'G', 'Fmaj7', etc. */\n symbol: string;\n /** Start position in quarter notes */\n startQn: number;\n /** End position in quarter notes */\n endQn: number;\n}\n\n/** Full generation context — includes concurrent track MIDI data */\nexport interface PluginGenerationContext {\n chordProgression: {\n key: { tonic: string; mode: string };\n chordsWithTiming: PluginChordTiming[];\n genre: string | null;\n };\n concurrentTracks: PluginConcurrentTrackInfo[];\n}\n\nexport interface PluginConcurrentTrackInfo {\n trackId: string;\n role: string | undefined;\n presetCategory: string | null;\n /** Notes organized by which chord they fall under */\n notesByChord: PluginChordSegment[];\n}\n\nexport interface PluginChordSegment {\n chord: string;\n chordRangeQn: [number, number];\n notes: PluginMidiNote[];\n}\n\n// ============================================================================\n// Transport Types\n// ============================================================================\n\nexport interface TransportEvent {\n type: 'play' | 'stop' | 'pause' | 'bpmChange' | 'positionChange';\n bpm?: number;\n position?: number; // in seconds\n isPlaying?: boolean;\n}\n\nexport interface DeckBoundaryEvent {\n deckId: string; // 'loop-a', 'loop-b'\n bar: number; // Current bar number (1-based)\n beat: number; // Current beat within bar (1-based)\n loopCount: number; // How many loops completed\n}\n\nexport interface PluginTransportState {\n isPlaying: boolean;\n isPaused: boolean;\n bpm: number;\n position: number; // in seconds\n timeSignature: string;\n}\n\nexport interface PluginSceneInfo {\n id: string;\n name: string;\n isMuted: boolean;\n}\n\n/** Scene-level contract/context state passed to plugin UIs as a prop */\nexport interface PluginSceneContext {\n /** Whether a contract has been generated (genre or contractPrompt exists AND chords exist) */\n hasContract: boolean;\n /** Original user prompt text (e.g., \"dark psytrance\"). Null if none. */\n contractPrompt: string | null;\n /** Extracted genre. Null if none. */\n genre: string | null;\n /** Musical key. Null if no chord progression. */\n key: { tonic: string; mode: string } | null;\n /** Chord symbols (e.g., [\"Cm\", \"Fm\", \"G\"]). Empty if no chords. */\n chords: string[];\n /** BPM from project tempo */\n bpm: number;\n /** Scene length in bars */\n bars: number;\n /** Whether any synth tracks exist in this scene */\n hasTracks: boolean;\n /** Whether bulk generation is currently in progress */\n isBulkGenerating: boolean;\n}\n\n/** Placeholder track state for the progressive bulk-add UX */\nexport interface BulkAddPlaceholderTrack {\n id: string;\n planIndex: number;\n role: string;\n description: string;\n status: 'planned' | 'creating' | 'completed' | 'failed';\n error?: string;\n}\n\nexport type TransportEventListener = (event: TransportEvent) => void;\nexport type DeckBoundaryListener = (event: DeckBoundaryEvent) => void;\nexport type SceneChangeListener = (sceneId: string | null) => void;\nexport type UnsubscribeFn = () => void;\n\n// ============================================================================\n// LLM Types\n// ============================================================================\n\nexport interface LLMGenerationRequest {\n /** System prompt (instructions, role, output format) */\n system: string;\n /** User prompt (the actual request) */\n user: string;\n /** Max tokens for response (host may cap this) */\n maxTokens?: number;\n /** Expected response format hint */\n responseFormat?: 'text' | 'json';\n /**\n * If true, the host will NOT auto-prefix the user prompt with musical\n * context (key, BPM, chords, genre, etc.). Default: false (context IS\n * prefixed automatically).\n */\n skipContextPrefix?: boolean;\n}\n\nexport interface LLMGenerationResult {\n /** Raw response text */\n content: string;\n /** Tokens consumed */\n tokensUsed: number;\n /** Model that generated the response */\n model: string;\n}\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\nexport interface PluginPresetData {\n name: string;\n category: string;\n /** Base64-encoded plugin state — pass to setPluginState() */\n state: string;\n}\n\n/** Result of shufflePreset() — the new preset that was applied */\nexport interface ShufflePresetResult {\n presetName: string;\n presetCategory: string;\n}\n\n// ============================================================================\n// Settings Types\n// ============================================================================\n\nexport interface PluginSettingsSchema {\n type: 'object';\n properties: Record<string, SettingDefinition>;\n}\n\nexport interface SettingDefinition {\n type: 'string' | 'number' | 'boolean' | 'select';\n label: string;\n description?: string;\n default?: unknown;\n /** For 'select' type */\n options?: Array<{ label: string; value: string }>;\n /** For 'number' type */\n min?: number;\n max?: number;\n}\n\nexport interface PluginSettingsStore {\n get<T>(key: string, defaultValue: T): T;\n set(key: string, value: unknown): void;\n getAll(): Record<string, unknown>;\n /** Subscribe to settings changes. Returns unsubscribe fn. */\n onChange(listener: (key: string, value: unknown) => void): UnsubscribeFn;\n}\n\n// ============================================================================\n// Error Types\n// ============================================================================\n\nexport type PluginErrorCode =\n | 'NOT_OWNED' // Tried to modify a track not owned by this plugin\n | 'TRACK_NOT_FOUND' // Track ID doesn't exist in engine\n | 'TRACK_LIMIT_EXCEEDED' // Plugin has too many tracks\n | 'NO_ACTIVE_SCENE' // No scene selected\n | 'ENGINE_ERROR' // Tracktion engine call failed\n | 'INVALID_MIDI' // Malformed MIDI data\n | 'FILE_NOT_FOUND' // Audio file doesn't exist\n | 'INVALID_FORMAT' // Unsupported audio format\n | 'PLUGIN_NOT_FOUND' // VST/AU plugin not installed\n | 'LLM_BUDGET_EXCEEDED' // Over token limit\n | 'LLM_UNAVAILABLE' // Gateway unreachable\n | 'NOT_AUTHENTICATED' // User not logged in\n | 'TIMEOUT' // Operation timed out\n | 'CANCELLED' // User cancelled the operation\n | 'INCOMPATIBLE' // Plugin requires newer SDK version\n | 'CAPABILITY_DENIED' // Plugin lacks required capability\n | 'SECRET_NOT_FOUND'; // Secret key doesn't exist\n\nexport class PluginError extends Error {\n public readonly code: PluginErrorCode;\n public readonly details?: Record<string, unknown>;\n\n constructor(\n code: PluginErrorCode,\n message: string,\n details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'PluginError';\n this.code = code;\n this.details = details;\n }\n}\n\n// ============================================================================\n// Plugin Manifest (on-disk plugin.json)\n// ============================================================================\n\nexport interface PluginManifest {\n id: string;\n displayName: string;\n version: string;\n description: string;\n generatorType: GeneratorType;\n main: string; // e.g., 'dist/index.js'\n renderer?: string; // e.g., 'dist/ui.bundle.js' (UMD bundle for renderer)\n icon?: string; // e.g., 'assets/icon.svg'\n author?: string;\n license?: string;\n minHostVersion?: string;\n capabilities?: PluginCapabilities;\n settings?: Record<string, SettingDefinition>;\n builtIn?: boolean;\n repository?: string; // e.g., 'https://github.com/user/my-plugin'\n}\n\nexport interface PluginCapabilities {\n requiresLLM?: boolean;\n requiresSurgeXT?: boolean;\n requiresNetwork?: boolean;\n /** Allowed network hosts for httpRequest (e.g., ['api.splice.com']) */\n network?: { allowedHosts?: string[] };\n /** Plugin needs native file dialog access */\n fileDialog?: boolean;\n}\n\n// ============================================================================\n// Phase 2: File System Types\n// ============================================================================\n\nexport interface PluginFileDialogOptions {\n title?: string;\n defaultPath?: string;\n filters?: Array<{ name: string; extensions: string[] }>;\n /** For open dialog: allow selecting multiple files */\n multiSelections?: boolean;\n /** For open dialog: allow selecting directories */\n directories?: boolean;\n}\n\nexport interface PluginDownloadOptions {\n /** HTTP headers to include */\n headers?: Record<string, string>;\n /** Overwrite if file exists (default: false) */\n overwrite?: boolean;\n}\n\n// ============================================================================\n// Phase 2: Network Types\n// ============================================================================\n\nexport interface PluginHttpRequestOptions {\n url: string;\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n headers?: Record<string, string>;\n body?: string | Record<string, unknown>;\n /** Timeout in milliseconds (default: 30000) */\n timeoutMs?: number;\n}\n\nexport interface PluginHttpResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: string;\n}\n\n// ============================================================================\n// Phase 2: Sample Library Types\n// ============================================================================\n\nexport interface PluginSampleFilter {\n bpm?: number;\n key?: { tonic: string; mode?: string };\n category?: string;\n searchQuery?: string;\n}\n\nexport interface PluginSampleInfo {\n id: string;\n filename: string;\n filePath: string;\n category: string | null;\n bpm: number | null;\n keyTonic: string | null;\n keyMode: string | null;\n durationSeconds: number | null;\n fileSizeBytes: number | null;\n tags: string[] | null;\n}\n\nexport interface PluginSampleImportResult {\n imported: number;\n skipped: number;\n errors: string[];\n}\n\n/** Sample track with associated sample metadata (returned by getPluginSampleTracks) */\nexport interface PluginSampleTrackInfo {\n track: PluginTrackHandle;\n sample: PluginSampleInfo;\n volume: number;\n pan: number;\n}\n\n// ============================================================================\n// Phase 2: Audio Generation Types\n// ============================================================================\n\nexport interface PluginAudioTextureRequest {\n /** Text prompt describing the audio texture */\n prompt: string;\n /** Duration in seconds (default: scene length) */\n durationSeconds?: number;\n /** Target BPM (default: project BPM) */\n bpm?: number;\n}\n\nexport interface PluginAudioTextureResult {\n /** Path to the generated audio file */\n filePath: string;\n /** Duration of the generated audio in seconds */\n durationSeconds: number;\n}\n\n// ============================================================================\n// Scene Composition Types\n// ============================================================================\n\n/** Options for composing a full scene arrangement via LLM. */\nexport interface ComposeSceneOptions {\n /** The contract prompt / musical direction for the arrangement. */\n contractPrompt: string;\n /** Genre hint (e.g. 'techno', 'jazz'). Optional. */\n genre?: string | null;\n}\n\n/** Result from a scene composition. */\nexport interface ComposeSceneResult {\n /** Whether the composition completed successfully. */\n success: boolean;\n /** Number of tracks created. */\n tracksCreated: number;\n /** Error message if not successful. */\n error?: string;\n}\n\n/** Listener for composition progress events. */\nexport type ComposeProgressListener = (event: ComposeProgressEvent) => void;\n\n/** Progress event emitted during scene composition. */\nexport interface ComposeProgressEvent {\n /** Current phase: 'planning' (LLM deciding tracks), 'generating' (creating MIDI), 'complete', 'error'. */\n phase: 'planning' | 'generating' | 'complete' | 'error';\n /** Per-track placeholder state (available once planning is done). */\n placeholders?: BulkAddPlaceholderTrack[];\n /** Error message when phase is 'error'. */\n error?: string;\n /** Scene ID this compose event belongs to (for scene-keyed UI state). */\n sceneId?: string;\n}\n\n// ============================================================================\n// Phase 2: Plugin Preset Types\n// ============================================================================\n\nexport interface PluginPresetInfo {\n id: string;\n name: string;\n category: string | null;\n isBuiltIn: boolean;\n data: Record<string, unknown>;\n}\n\nexport interface SavePluginPresetOptions {\n name: string;\n category?: string;\n data: Record<string, unknown>;\n}\n\n// ============================================================================\n// App Tool Bridge (since SDK 1.2.0)\n// ============================================================================\n\n/** JSON Schema shape for a tool's input params. */\nexport interface PluginAppToolInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n/** Lightweight descriptor returned by `PluginHost.listAppTools`. */\nexport interface PluginAppTool {\n name: string;\n description: string;\n inputSchema: PluginAppToolInputSchema;\n /** `'scene'` = safe for scene-scoped callers. `'project'` = cross-scene. */\n scope?: 'scene' | 'project';\n}\n\n/** Result shape returned by `PluginHost.executeAppTool`. */\nexport interface PluginAppToolResult {\n success: boolean;\n action: string;\n message?: string;\n error?: string;\n /**\n * Tool-specific payload. Concrete shape depends on the tool — callers\n * should treat this as opaque unless they know the tool.\n */\n data?: unknown;\n}\n\n// ============================================================================\n// Plugin Registry Types (used by host internals)\n// ============================================================================\n\nexport type PluginStatus = 'pending' | 'active' | 'failed' | 'disabled' | 'incompatible';\n\nexport interface PluginRegistration {\n /** The loaded plugin instance */\n plugin: GeneratorPlugin;\n /** Current status */\n status: PluginStatus;\n /** Resolved manifest from disk */\n manifest: PluginManifest;\n /** The scoped PluginHost instance for this plugin */\n host: PluginHost | null;\n /** Sort order for accordion display */\n sortOrder: number;\n /** Whether the plugin is enabled */\n enabled: boolean;\n /** Error message if status is 'failed' */\n error?: string;\n}\n","/**\n * FX Toggle Types\n *\n * Types and constants for per-track FX toggle buttons.\n * Each track can enable/disable 6 FX categories independently.\n * The engine is the source of truth — no database persistence needed.\n */\n\n/** Available FX categories in signal chain order */\nexport type FxCategory = 'eq' | 'compressor' | 'chorus' | 'phaser' | 'delay' | 'reverb';\n\n/** All FX categories in signal chain order */\nexport const FX_CATEGORIES: readonly FxCategory[] = [\n 'eq',\n 'compressor',\n 'chorus',\n 'phaser',\n 'delay',\n 'reverb',\n] as const;\n\n/** Position in the signal chain (lower = earlier) */\nexport const FX_CHAIN_ORDER: Record<FxCategory, number> = {\n eq: 0,\n compressor: 1,\n chorus: 2,\n phaser: 3,\n delay: 4,\n reverb: 5,\n};\n\n/** Map from FxCategory to Tracktion Engine built-in plugin xmlTypeName */\nexport const FX_ENGINE_PLUGIN_NAMES: Record<FxCategory, string> = {\n eq: '4bandEq',\n compressor: 'compressor',\n chorus: 'chorus',\n phaser: 'phaser',\n delay: 'delay',\n reverb: 'reverb',\n};\n\n/** Display labels for UI buttons */\nexport const FX_DISPLAY_LABELS: Record<FxCategory, string> = {\n eq: 'EQ',\n compressor: 'Comp',\n chorus: 'Chorus',\n phaser: 'Phaser',\n delay: 'Delay',\n reverb: 'Reverb',\n};\n\n/** Per-track FX state: which categories are active */\nexport interface TrackFxState {\n eq: boolean;\n compressor: boolean;\n chorus: boolean;\n phaser: boolean;\n delay: boolean;\n reverb: boolean;\n}\n\n/** Default state: all FX disabled */\nexport const EMPTY_FX_STATE: TrackFxState = {\n eq: false,\n compressor: false,\n chorus: false,\n phaser: false,\n delay: false,\n reverb: false,\n};\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\n/** A single FX preset definition */\nexport interface FxPreset {\n /** Display name (e.g. \"Room\", \"Hall\") */\n name: string;\n /** Short label for button (e.g. \"RM\", \"HL\") */\n shortLabel: string;\n /** Map from automatable parameter name -> value (set via setPluginParameter) */\n params: Record<string, number>;\n /** CachedValue params set via XML state (getPluginState/setPluginState) */\n xmlStateParams?: Record<string, number>;\n /** BPM-relative delay time multiplier (1.0 = quarter note). When set, Delay Time is computed at apply time. */\n noteMultiplier?: number;\n /** Fixed delay time in ms (non-BPM-synced). Mutually exclusive with noteMultiplier. */\n fixedLengthMs?: number;\n}\n\n/** How dry/wet is applied to the plugin */\nexport type MixInterpolation = 'direct' | 'gain-scale' | 'ratio-scale';\n\n/** Preset configuration for an FX category */\nexport interface FxPresetConfig {\n /** Exactly 5 presets */\n presets: [FxPreset, FxPreset, FxPreset, FxPreset, FxPreset];\n /** Name of the native mix/wet parameter, or null if no native dry/wet */\n mixParamName: string | null;\n /** XML attribute name for dry/wet control (for plugins with no automatable mix param, e.g. chorus/phaser) */\n mixXmlAttr?: string;\n /** How to apply dry/wet (defaults to 'direct') */\n mixInterpolation: MixInterpolation;\n}\n\n/** Per-category detail state for a single FX on a track */\nexport interface FxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Extended FX state per track with preset and dry/wet info */\nexport type TrackFxDetailState = Record<FxCategory, FxCategoryDetailState>;\n\n/** Default dry/wet mix level (33% — musically useful for most effects) */\nexport const DEFAULT_FX_DRY_WET = 0.33;\n\n/** Default detail state for a single category */\nexport const DEFAULT_FX_CATEGORY_DETAIL: FxCategoryDetailState = {\n enabled: false,\n presetIndex: 0,\n dryWet: DEFAULT_FX_DRY_WET,\n};\n\n/** Default detail state: all FX disabled, preset 0, full wet */\nexport const EMPTY_FX_DETAIL_STATE: TrackFxDetailState = {\n eq: { ...DEFAULT_FX_CATEGORY_DETAIL },\n compressor: { ...DEFAULT_FX_CATEGORY_DETAIL },\n chorus: { ...DEFAULT_FX_CATEGORY_DETAIL },\n phaser: { ...DEFAULT_FX_CATEGORY_DETAIL },\n delay: { ...DEFAULT_FX_CATEGORY_DETAIL },\n reverb: { ...DEFAULT_FX_CATEGORY_DETAIL },\n};\n\n/** Persisted FX data for a single category (stored as JSON in database) */\nexport interface FxPresetDataEntry {\n presetIndex: number;\n dryWet: number;\n enabled: boolean;\n}\n\n/** Persisted FX data format (stored as JSON in database) */\nexport type FxPresetData = Partial<Record<FxCategory, FxPresetDataEntry>>;\n","/**\n * SDK TrackRow — Reusable track row component for generator plugins.\n *\n * Renders a complete track UI with prompt input, generation controls,\n * shuffle/copy, volume/pan, mute/solo, FX drawer, and visual states\n * (amber pulse for \"needs generation\", progress overlay, error indicator).\n *\n * Layout matches TrackInput (main branch) for visual parity.\n *\n * Depends only on PluginHost types + existing shared renderer components.\n */\n\nimport React from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { InstrumentDrawer } from './InstrumentDrawer';\nimport type { InstrumentDescriptor } from '../types/plugin-sdk.types';\nimport { VolumeSlider } from './VolumeSlider';\nimport { PanSlider } from './PanSlider';\nimport { FxToggleBar } from './FxToggleBar';\nimport { SorceryProgressBar } from './SorceryProgressBar';\nimport type { TrackFxDetailState, FxCategory } from '../types/fx-toggle.types';\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface SDKTrackRowProps {\n /** Track identity */\n track: { id: string; name: string; role?: string };\n /** Current prompt text (optional — omit when using contentSlot) */\n prompt?: string;\n /** Playback state */\n runtimeState: { muted: boolean; solo: boolean; volume: number; pan: number };\n /** FX category states */\n fxDetailState: TrackFxDetailState;\n /** FX panel visibility */\n fxDrawerOpen: boolean;\n /** Generation in progress */\n isGenerating?: boolean;\n /** Auth state */\n isAuthenticated?: boolean;\n /** Error from last generation */\n error?: string | null;\n /** Enables shuffle/copy buttons */\n hasMidi?: boolean;\n /** Progress % (for persistence across scene switches) */\n generationProgress?: number;\n /** For progress bar pacing */\n estimatedGenerationMs?: number;\n /** Prompt edit (optional — omit to hide prompt input) */\n onPromptChange?: (prompt: string) => void;\n /** \"Create\" button / Enter key (optional — omit to hide Create button) */\n onGenerate?: () => void;\n /** Shuffle preset (optional — omit to hide Shuffle button) */\n onShuffle?: () => void;\n /** Duplicate track (optional — omit to hide Copy button) */\n onCopy?: () => void;\n /** Delete track */\n onDelete: () => void;\n /** Custom content replacing the prompt input (e.g., sample info display) */\n contentSlot?: React.ReactNode;\n /** Toggle mute */\n onMuteToggle: () => void;\n /** Toggle solo */\n onSoloToggle: () => void;\n /** Volume slider */\n onVolumeChange: (vol: number) => void;\n /** Pan slider */\n onPanChange: (pan: number) => void;\n /** FX category toggle (optional — omit to hide FX button) */\n onFxToggle?: (cat: FxCategory, enabled: boolean) => void;\n /** FX preset select */\n onFxPresetChange?: (cat: FxCategory, idx: number) => void;\n /** FX dry/wet */\n onFxDryWetChange?: (cat: FxCategory, val: number) => void;\n /** Open/close FX (optional — omit to hide FX button) */\n onToggleFxDrawer?: () => void;\n /** Progress persistence callback */\n onProgressChange?: (pct: number) => void;\n /** Left border accent color */\n accentColor?: string;\n // --- Instrument Plugin Selection ---\n /** Current instrument display name (null/undefined = Surge XT default) */\n instrumentName?: string | null;\n /** Whether the current instrument plugin is missing from the system */\n instrumentMissing?: boolean;\n /** Whether the instrument drawer is open */\n instrumentDrawerOpen?: boolean;\n /** Toggle the instrument drawer */\n onToggleInstrumentDrawer?: () => void;\n /** Available instrument plugins for the drawer */\n availableInstruments?: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID */\n currentInstrumentPluginId?: string | null;\n /** Called when user selects an instrument from the drawer */\n onInstrumentSelect?: (pluginId: string) => void;\n /** Whether instrument scan is loading */\n instrumentsLoading?: boolean;\n /** Re-scan for instruments */\n onRefreshInstruments?: () => void;\n // --- Instrument Editor (Stage 2) ---\n /** Which stage the instrument drawer is in */\n instrumentDrawerStage?: 'instruments' | 'editor';\n /** Called when user clicks \"Open Editor\" */\n onShowEditor?: () => void;\n /** Called when user wants to go back from editor view */\n onBackToInstruments?: () => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function TrackRow({\n track,\n prompt,\n runtimeState,\n fxDetailState,\n fxDrawerOpen,\n isGenerating = false,\n isAuthenticated = false,\n error,\n hasMidi = false,\n generationProgress = 0,\n estimatedGenerationMs = 15000,\n onPromptChange,\n onGenerate,\n onShuffle,\n onCopy,\n onDelete,\n contentSlot,\n onMuteToggle,\n onSoloToggle,\n onVolumeChange,\n onPanChange,\n onFxToggle,\n onFxPresetChange,\n onFxDryWetChange,\n onToggleFxDrawer,\n onProgressChange,\n accentColor = '#A78BFA',\n instrumentName,\n instrumentMissing,\n instrumentDrawerOpen,\n onToggleInstrumentDrawer,\n availableInstruments,\n currentInstrumentPluginId,\n onInstrumentSelect,\n instrumentsLoading,\n onRefreshInstruments,\n instrumentDrawerStage,\n onShowEditor,\n onBackToInstruments,\n}: SDKTrackRowProps): React.ReactElement {\n const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;\n\n // \"Needs generation\" = has prompt, no MIDI yet, not currently generating\n const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);\n\n const hasFxActive = Object.values(fxDetailState).some(\n (d: { enabled: boolean }) => d.enabled\n );\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {\n if (e.key === 'Enter' && !e.shiftKey && onGenerate) {\n e.preventDefault();\n onGenerate();\n }\n };\n\n // Amber pulse class for \"needs generation\" state\n const borderColorStyle = needsGeneration\n ? undefined // handled by className animation\n : accentColor;\n\n const borderClass = needsGeneration\n ? 'border-amber-400 animate-pulse'\n : 'border-sas-border';\n\n return (\n <div data-testid=\"sdk-track-row-wrapper\" className=\"w-full\">\n <div\n data-testid=\"sdk-track-row\"\n className={`relative flex items-stretch gap-1 p-2 rounded-sm border w-full overflow-hidden ${borderClass} bg-sas-panel-alt`}\n style={{\n borderLeftColor: needsGeneration ? '#f59e0b' : borderColorStyle,\n borderLeftWidth: '3px',\n }}\n >\n {/* Generating progress overlay - stops before buttons (right-44) */}\n {isGenerating && (\n <div className=\"absolute left-0 top-0 bottom-0 right-44 z-20\">\n <SorceryProgressBar\n isLoading={true}\n statusText=\"CONJURING MIDI...\"\n heightClass=\"h-full\"\n initialProgress={generationProgress}\n onProgressChange={onProgressChange}\n estimatedDurationMs={estimatedGenerationMs}\n />\n </div>\n )}\n\n {/* Left: Content area (prompt input or custom content slot) with track name, volume, and pan underneath */}\n <div className=\"flex flex-col flex-1 min-w-0 relative z-10\">\n {contentSlot ? contentSlot : onPromptChange ? (\n <input\n type=\"text\"\n data-testid=\"sdk-prompt-input\"\n value={prompt ?? ''}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => onPromptChange(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Describe your part...\"\n disabled={isGenerating}\n className=\"sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n ) : null}\n {/* Track name, volume slider, and pan slider in horizontal row */}\n <div className=\"flex items-center gap-2 mt-1\">\n {track.name && (\n <span className=\"text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]\" title={track.name}>\n {track.name}\n </span>\n )}\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">vol:</span>\n <VolumeSlider\n value={currentVolume}\n onChange={onVolumeChange}\n disabled={isGenerating}\n className=\"flex-1 min-w-[40px]\"\n />\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">pan:</span>\n <PanSlider\n value={currentPan}\n onChange={onPanChange}\n disabled={isGenerating}\n className=\"w-10 flex-shrink-0\"\n />\n </div>\n </div>\n\n {/* Error indicator - shows when generation failed */}\n {error && (\n <div\n data-testid=\"sdk-error-indicator\"\n className=\"flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help\"\n title={error}\n >\n <div className=\"relative\">\n <AlertCircle\n className=\"w-5 h-5 text-red-500 animate-pulse\"\n strokeWidth={2.5}\n />\n {/* Tooltip - appears on hover */}\n <div className=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate\">\n {error}\n </div>\n </div>\n </div>\n )}\n\n {/* Right: Button grid (2 rows) - z-30 to stay above generating overlay */}\n <div className=\"flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center\">\n {/* Top row: [Create] [Copy] M x — Create/Copy only shown when handlers provided */}\n <div className=\"flex gap-1 items-center\">\n {onGenerate && (\n <button\n data-testid=\"sdk-generate-button\"\n onClick={onGenerate}\n disabled={!isAuthenticated || isGenerating || !prompt?.trim()}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !isAuthenticated || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : needsGeneration\n ? 'bg-amber-500/30 border-amber-500 text-amber-400 hover:bg-amber-500 hover:text-sas-bg animate-pulse'\n : prompt?.trim()\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n }`}\n title={!isAuthenticated ? 'Please log in' : isGenerating ? 'Generating...' : 'Generate MIDI'}\n >\n Create\n </button>\n )}\n {onCopy && (\n <button\n data-testid=\"sdk-copy-button\"\n onClick={onCopy}\n disabled={!hasMidi || isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={hasMidi ? 'Duplicate track with different preset' : 'Generate MIDI first'}\n >\n Copy\n </button>\n )}\n <button\n data-testid=\"sdk-mute-button\"\n onClick={onMuteToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isMuted\n ? 'bg-red-600 text-white'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isMuted ? 'Unmute track' : 'Mute track'}\n >\n M\n </button>\n <button\n data-testid=\"sdk-delete-button\"\n onClick={onDelete}\n className=\"text-sas-danger/70 hover:text-sas-danger px-1 py-0.5 transition-colors text-sm\"\n title=\"Delete track\"\n >\n x\n </button>\n </div>\n {/* Bottom row: [Shuffle] [FX] Solo [P] — Shuffle/FX only shown when handlers provided */}\n <div className=\"flex gap-1 items-center\">\n {onShuffle && (\n <button\n data-testid=\"sdk-shuffle-button\"\n onClick={onShuffle}\n disabled={!hasMidi || isGenerating || !!currentInstrumentPluginId}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating || !!currentInstrumentPluginId\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={\n currentInstrumentPluginId\n ? 'Shuffle only works with default Surge XT'\n : hasMidi\n ? 'Re-roll sound (keep MIDI)'\n : 'Generate MIDI first'\n }\n >\n Shuffle\n </button>\n )}\n {onToggleFxDrawer && (\n <button\n data-testid=\"sdk-fx-button\"\n onClick={onToggleFxDrawer}\n disabled={isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : fxDrawerOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : hasFxActive\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={fxDrawerOpen ? 'Hide FX controls' : 'Show FX controls'}\n >\n FX\n </button>\n )}\n <button\n data-testid=\"sdk-solo-button\"\n onClick={onSoloToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isSoloed\n ? 'bg-yellow-500 text-black'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isSoloed ? 'Unsolo track' : 'Solo track'}\n >\n S\n </button>\n {onToggleInstrumentDrawer && (\n <button\n data-testid=\"sdk-plugin-button\"\n onClick={onToggleInstrumentDrawer}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : instrumentDrawerOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : instrumentMissing\n ? 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/40'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={`Plugin: ${instrumentName ?? 'Surge XT'}${instrumentMissing ? ' (missing)' : ''}`}\n >\n P\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* FX Drawer */}\n {fxDrawerOpen && !instrumentDrawerOpen && (\n <div data-testid=\"sdk-fx-drawer\" className=\"border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[180px] overflow-y-auto\">\n <FxToggleBar\n trackId={track.id}\n fxState={fxDetailState}\n onToggle={(_trackId: string, category: FxCategory, enabled: boolean) => onFxToggle?.(category, enabled)}\n onPresetChange={(_trackId: string, category: FxCategory, presetIndex: number) => onFxPresetChange?.(category, presetIndex)}\n onDryWetChange={(_trackId: string, category: FxCategory, value: number) => onFxDryWetChange?.(category, value)}\n disabled={isGenerating}\n />\n </div>\n )}\n\n {/* Instrument Drawer */}\n {instrumentDrawerOpen && !fxDrawerOpen && availableInstruments && onInstrumentSelect && onRefreshInstruments && (\n <div data-testid=\"sdk-instrument-drawer\" className=\"border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2\">\n <InstrumentDrawer\n instruments={availableInstruments}\n currentPluginId={currentInstrumentPluginId ?? null}\n isLoading={instrumentsLoading ?? false}\n onSelect={onInstrumentSelect}\n onRefresh={onRefreshInstruments}\n stage={instrumentDrawerStage}\n onShowEditor={onShowEditor}\n onBackToInstruments={onBackToInstruments}\n selectedInstrumentName={instrumentName}\n />\n </div>\n )}\n </div>\n );\n}\n\nexport default TrackRow;\n","/**\n * InstrumentDrawer — Two-stage nested menu for instrument selection + editor access.\n *\n * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.\n * Stage 2 (editor): Shows \"Open Editor\" button for the selected plugin's native GUI.\n */\n\nimport React, { useState, useMemo } from 'react';\nimport type { InstrumentDescriptor } from '../types/plugin-sdk.types';\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface InstrumentDrawerProps {\n /** Available instrument plugins from engine scan */\n instruments: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID (null = default Surge XT) */\n currentPluginId: string | null;\n /** Whether the scan is still in progress */\n isLoading: boolean;\n /** Called when user selects an instrument */\n onSelect: (pluginId: string) => void;\n /** Called when user clicks refresh to re-scan plugins */\n onRefresh: () => void;\n // --- Editor access (Stage 2) ---\n /** Which stage the drawer is in */\n stage?: 'instruments' | 'editor';\n /** Called when user clicks \"Open Editor\" */\n onShowEditor?: () => void;\n /** Called when user wants to go back from editor view to instrument list */\n onBackToInstruments?: () => void;\n /** Name of the selected instrument (for display in editor header) */\n selectedInstrumentName?: string | null;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function InstrumentDrawer({\n instruments,\n currentPluginId,\n isLoading,\n onSelect,\n onRefresh,\n stage = 'instruments',\n onShowEditor,\n onBackToInstruments,\n selectedInstrumentName,\n}: InstrumentDrawerProps): React.ReactElement {\n const [search, setSearch] = useState('');\n\n /** Sentinel pluginId for the default Surge XT entry */\n const SURGE_XT_DEFAULT_ID = 'Surge XT';\n\n // Filter instruments by search query, with selected instrument always first\n const filtered = useMemo((): InstrumentDescriptor[] => {\n let all = instruments.filter(\n (i: InstrumentDescriptor) => i.name !== 'Surge XT'\n );\n if (search.trim()) {\n const q = search.toLowerCase();\n all = all.filter(\n (i: InstrumentDescriptor) =>\n i.name.toLowerCase().includes(q) ||\n i.manufacturer.toLowerCase().includes(q)\n );\n }\n // Move the currently selected instrument to the top\n if (currentPluginId) {\n const selectedIdx = all.findIndex((i: InstrumentDescriptor) => i.pluginId === currentPluginId);\n if (selectedIdx > 0) {\n const [selected] = all.splice(selectedIdx, 1);\n all.unshift(selected);\n }\n }\n return all;\n }, [instruments, search, currentPluginId]);\n\n // Is the default Surge XT selected?\n const isDefaultSelected = currentPluginId === null;\n\n // Determine which pluginId is \"selected\" among scanned instruments\n const isSelected = (pluginId: string): boolean => {\n return pluginId === currentPluginId;\n };\n\n // ---- Stage 2: Editor Access ----\n if (stage === 'editor') {\n return (\n <div className=\"flex flex-col gap-2\">\n {/* Back button + instrument name header */}\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => onBackToInstruments?.()}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors\"\n >\n &larr; Back\n </button>\n <span className=\"text-xs text-sas-muted font-medium truncate flex-1\">\n {selectedInstrumentName ?? 'Plugin'}\n </span>\n </div>\n\n {/* Open Editor button */}\n <button\n onClick={() => onShowEditor?.()}\n className=\"w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors\"\n >\n Open Plugin Editor\n </button>\n </div>\n );\n }\n\n // ---- Stage 1: Instrument List (default) ----\n return (\n <div className=\"flex flex-col gap-2\">\n {/* Search + Refresh row */}\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n value={search}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}\n placeholder=\"Search instruments...\"\n className=\"sas-input flex-1 px-2 py-1 text-xs\"\n />\n <button\n onClick={onRefresh}\n disabled={isLoading}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50\"\n title=\"Re-scan plugins\"\n >\n {isLoading ? '...' : 'Refresh'}\n </button>\n </div>\n\n {/* Instrument grid */}\n {isLoading && instruments.length === 0 ? (\n <div className=\"text-xs text-sas-muted/60 text-center py-3\">\n Scanning plugins...\n </div>\n ) : (\n <div className=\"grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto\">\n {/* Permanent \"Surge XT (Default)\" entry — always available */}\n <button\n key=\"__surge-xt-default__\"\n onClick={() => onSelect(SURGE_XT_DEFAULT_ID)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n isDefaultSelected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title=\"Surge XT — Default instrument\"\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {isDefaultSelected && '\\u2713 '}Surge XT\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">\n Default\n </span>\n </button>\n {/* Scanned instruments */}\n {filtered.map((inst: InstrumentDescriptor) => {\n const selected = isSelected(inst.pluginId);\n return (\n <button\n key={inst.pluginId}\n onClick={() => onSelect(inst.pluginId)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n selected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : inst.missing\n ? 'border-amber-500/50 bg-amber-500/10 text-amber-400 hover:border-amber-500'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={`${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? ' — MISSING' : ''}`}\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {selected && '\\u2713 '}{inst.name}\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">\n {inst.manufacturer || inst.type.toUpperCase()}\n </span>\n </button>\n );\n })}\n {filtered.length === 0 && (\n <div className=\"col-span-2 text-xs text-sas-muted/60 text-center py-2\">\n {search.trim() ? 'No matches' : 'No other plugins found'}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport default InstrumentDrawer;\n","/**\n * VolumeSlider Component\n *\n * Compact horizontal volume slider for track volume control.\n * Uses native HTML range input with custom styling.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\nimport { sliderToDb } from '../utils/volume-conversion';\n\ninterface VolumeSliderProps {\n /** Volume value from 0 to 1 */\n value: number;\n /** Called when volume changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Format slider value as dB for tooltip display\n */\nfunction formatDb(value: number): string {\n const db = sliderToDb(value);\n if (db <= -60) return '-∞ dB';\n const sign = db >= 0 ? '+' : '';\n return `${sign}${db.toFixed(1)} dB`;\n}\n\n/**\n * Debounce helper for volume changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n // Update callback ref when callback changes\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const VolumeSlider: React.FC<VolumeSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Volume: ${formatDb(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default VolumeSlider;\n","/**\n * Volume Conversion Utilities\n *\n * Converts between UI slider position (0-1) and engine dB values using a power\n * curve with +6 dB headroom. The curve places unity gain (0 dB) at slider 0.75,\n * giving the top 25% of the slider a meaningful 6 dB boost range instead of the\n * previous perceptual dead zone.\n *\n * Mapping:\n * slider 0.00 → -60 dB (silence)\n * slider 0.75 → 0 dB (unity gain)\n * slider 1.00 → +6 dB (max boost)\n */\n\n/** Slider position that maps to 0 dB (unity gain) */\nexport const SLIDER_UNITY = 0.75;\n\n/** Maximum dB value at slider = 1.0 */\nexport const DB_MAX = 6;\n\n/** Minimum dB value (silence floor) */\nexport const DB_MIN = -60;\n\n/**\n * Exponent derived so that slider=1.0 yields exactly DB_MAX dB.\n *\n * gain_at_1 = (1 / SLIDER_UNITY) ^ EXPONENT = 10^(DB_MAX/20)\n * EXPONENT = log(10^(DB_MAX/20)) / log(1/SLIDER_UNITY)\n */\nconst EXPONENT: number =\n Math.log(Math.pow(10, DB_MAX / 20)) / Math.log(1 / SLIDER_UNITY);\n\n/**\n * Convert a UI slider position (0-1) to engine dB.\n *\n * @param slider - Slider value in [0, 1]\n * @returns dB value in [DB_MIN, DB_MAX]\n */\nexport function sliderToDb(slider: number): number {\n if (slider <= 0) return DB_MIN;\n const gain = Math.pow(slider / SLIDER_UNITY, EXPONENT);\n const db = 20 * Math.log10(gain);\n return Math.max(DB_MIN, Math.min(DB_MAX, db));\n}\n\n/**\n * Convert an engine dB value back to a UI slider position (0-1).\n * Inverse of sliderToDb().\n *\n * @param db - Volume in dB\n * @returns Slider value in [0, 1]\n */\nexport function dbToSlider(db: number): number {\n if (db <= DB_MIN) return 0;\n if (db >= DB_MAX) return 1;\n const gain = Math.pow(10, db / 20);\n const slider = SLIDER_UNITY * Math.pow(gain, 1 / EXPONENT);\n return Math.min(1, Math.max(0, slider));\n}\n","/**\n * PanSlider Component\n *\n * Compact horizontal pan slider for track stereo positioning.\n * Range: -1 (left) to +1 (right), 0 = center.\n * No text label - tooltip only.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\n\ninterface PanSliderProps {\n /** Pan value from -1 (left) to 1 (right), 0 = center */\n value: number;\n /** Called when pan changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Convert pan value (-1 to 1) to display string\n */\nfunction toPanDisplay(value: number): string {\n if (Math.abs(value) < 0.02) {\n return 'Center';\n }\n const percent = Math.abs(Math.round(value * 100));\n return value < 0 ? `L${percent}` : `R${percent}`;\n}\n\n/**\n * Debounce helper for pan changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const PanSlider: React.FC<PanSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n // Double-click to reset to center\n const handleDoubleClick = useCallback(() => {\n setLocalValue(0);\n onChange(0);\n }, [onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Pan: ${toPanDisplay(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"-1\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n onDoubleClick={handleDoubleClick}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default PanSlider;\n","/**\n * FX Preset Definitions\n *\n * 5 presets per FX category (30 total).\n *\n * Parameter names must match the Tracktion Engine's AutomatableParameter names\n * for each built-in plugin exactly (case-sensitive).\n *\n * Chorus & Phaser have ZERO automatable parameters — all values are set via\n * XML state (CachedValues on the plugin ValueTree).\n *\n * Lives in shared/ so both main process (services) and renderer (UI) can import.\n */\n\nimport type { FxCategory, FxPresetConfig } from '../types/fx-toggle.types';\n\n// ============================================================================\n// EQ (4-Band Equaliser)\n// ============================================================================\n\nconst EQ_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'The Smiley',\n shortLabel: 'SM',\n params: {\n 'Low-shelf freq': 80, 'Low-shelf gain': 4, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 500, 'Mid gain 1': -3, 'Mid Q 1': 0.7,\n 'Mid freq 2': 2000, 'Mid gain 2': -2, 'Mid Q 2': 0.7,\n 'High-shelf freq': 12000, 'High-shelf gain': 4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Telephone',\n shortLabel: 'TP',\n params: {\n 'Low-shelf freq': 400, 'Low-shelf gain': -20, 'Low-shelf Q': 1.0,\n 'Mid freq 1': 1000, 'Mid gain 1': 5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 3000, 'Mid gain 2': -5, 'Mid Q 2': 1.0,\n 'High-shelf freq': 5000, 'High-shelf gain': -20, 'High-shelf Q': 1.0,\n },\n },\n {\n name: 'Warmth',\n shortLabel: 'WM',\n params: {\n 'Low-shelf freq': 120, 'Low-shelf gain': 3, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 400, 'Mid gain 1': 2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 4000, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 10000, 'High-shelf gain': -4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Vocal Air',\n shortLabel: 'VA',\n params: {\n 'Low-shelf freq': 100, 'Low-shelf gain': -6, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 300, 'Mid gain 1': -2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 1500, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 14000, 'High-shelf gain': 6, 'High-shelf Q': 0.4,\n },\n },\n {\n name: 'De-Box',\n shortLabel: 'DB',\n params: {\n 'Low-shelf freq': 60, 'Low-shelf gain': 0, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 350, 'Mid gain 1': -5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 800, 'Mid gain 2': -3, 'Mid Q 2': 2.0,\n 'High-shelf freq': 10000, 'High-shelf gain': 0, 'High-shelf Q': 0.5,\n },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'gain-scale',\n};\n\n// ============================================================================\n// Compressor\n// ============================================================================\n\nconst COMPRESSOR_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Leveler',\n shortLabel: 'VL',\n params: { 'Threshold': 0.251, 'Ratio': 0.5, 'Attack': 20.0, 'Release': 200.0, 'Output': 2.0 },\n },\n {\n name: 'Drum Smash',\n shortLabel: 'DS',\n params: { 'Threshold': 0.100, 'Ratio': 0.1, 'Attack': 0.5, 'Release': 100.0, 'Output': 8.0 },\n },\n {\n name: 'Bus Glue',\n shortLabel: 'BG',\n params: { 'Threshold': 0.316, 'Ratio': 0.666, 'Attack': 80.0, 'Release': 150.0, 'Output': 1.0 },\n },\n {\n name: 'Bass Anchor',\n shortLabel: 'BA',\n params: { 'Threshold': 0.177, 'Ratio': 0.25, 'Attack': 10.0, 'Release': 250.0, 'Output': 4.0 },\n },\n {\n name: 'Safety Net',\n shortLabel: 'SN',\n params: { 'Threshold': 0.891, 'Ratio': 0.0, 'Attack': 0.3, 'Release': 50.0, 'Output': 0.0 },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'ratio-scale',\n};\n\n// ============================================================================\n// Chorus\n// ============================================================================\n\nconst CHORUS_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Dimension',\n shortLabel: 'DM',\n params: {},\n xmlStateParams: { depthMs: 1.5, speedHz: 0.5, width: 1.0, mixProportion: 0.5 },\n },\n {\n name: '80s Crystal',\n shortLabel: '80',\n params: {},\n xmlStateParams: { depthMs: 4.0, speedHz: 2.5, width: 0.8, mixProportion: 0.4 },\n },\n {\n name: 'Sea Sick',\n shortLabel: 'SS',\n params: {},\n xmlStateParams: { depthMs: 7.0, speedHz: 0.8, width: 0.3, mixProportion: 1.0 },\n },\n {\n name: 'Pseudo-Leslie',\n shortLabel: 'PL',\n params: {},\n xmlStateParams: { depthMs: 2.0, speedHz: 6.0, width: 0.9, mixProportion: 0.7 },\n },\n {\n name: 'Thickener',\n shortLabel: 'TK',\n params: {},\n xmlStateParams: { depthMs: 1.0, speedHz: 0.2, width: 1.0, mixProportion: 0.3 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'mixProportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Phaser\n// ============================================================================\n\nconst PHASER_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Slow Burn',\n shortLabel: 'SB',\n params: {},\n xmlStateParams: { depth: 6.0, rate: 0.1, feedback: 0.3 },\n },\n {\n name: 'Funky Quack',\n shortLabel: 'FQ',\n params: {},\n xmlStateParams: { depth: 3.0, rate: 2.0, feedback: 0.8 },\n },\n {\n name: 'Jet Plane',\n shortLabel: 'JP',\n params: {},\n xmlStateParams: { depth: 8.0, rate: 0.2, feedback: 0.9 },\n },\n {\n name: 'Underwater',\n shortLabel: 'UW',\n params: {},\n xmlStateParams: { depth: 1.5, rate: 4.0, feedback: 0.1 },\n },\n {\n name: 'Static Notch',\n shortLabel: 'ST',\n params: {},\n xmlStateParams: { depth: 2.0, rate: 0.05, feedback: 0.6 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'depth',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Delay\n// ============================================================================\n\nconst DELAY_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Slap',\n shortLabel: 'VS',\n fixedLengthMs: 110,\n params: { 'Feedback': -20.0, 'Mix proportion': 0.25 },\n },\n {\n name: 'Grand Canyon',\n shortLabel: 'GC',\n noteMultiplier: 1.0,\n params: { 'Feedback': -4.0, 'Mix proportion': 0.45 },\n },\n {\n name: 'Wide Doubler',\n shortLabel: 'WD',\n fixedLengthMs: 25,\n params: { 'Feedback': -30.0, 'Mix proportion': 0.5 },\n },\n {\n name: 'Dub Echo',\n shortLabel: 'DE',\n noteMultiplier: 0.6,\n params: { 'Feedback': -1.5, 'Mix proportion': 0.4 },\n },\n {\n name: 'Rhythmic Wash',\n shortLabel: 'RW',\n noteMultiplier: 0.75,\n params: { 'Feedback': -8.0, 'Mix proportion': 0.2 },\n },\n ],\n mixParamName: 'Mix proportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Reverb\n// ============================================================================\n\nconst REVERB_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Drum Room',\n shortLabel: 'DR',\n params: { 'Room Size': 0.2, 'Damping': 0.2, 'Wet Level': 0.15, 'Dry Level': 0.5, 'Width': 0.8 },\n },\n {\n name: 'Vocal Hall',\n shortLabel: 'VH',\n params: { 'Room Size': 0.8, 'Damping': 0.6, 'Wet Level': 0.25, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n {\n name: 'Cathedral',\n shortLabel: 'CT',\n params: { 'Room Size': 1.0, 'Damping': 0.1, 'Wet Level': 0.333, 'Dry Level': 0.2, 'Width': 1.0 },\n },\n {\n name: 'Tile Bathroom',\n shortLabel: 'TB',\n params: { 'Room Size': 0.15, 'Damping': 0.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 0.5 },\n },\n {\n name: 'Vintage Plate',\n shortLabel: 'VP',\n params: { 'Room Size': 0.4, 'Damping': 1.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n ],\n mixParamName: 'Wet Level',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Export\n// ============================================================================\n\n/** All preset configs keyed by FX category */\nexport const FX_PRESET_CONFIGS: Record<FxCategory, FxPresetConfig> = {\n eq: EQ_PRESETS,\n compressor: COMPRESSOR_PRESETS,\n chorus: CHORUS_PRESETS,\n phaser: PHASER_PRESETS,\n delay: DELAY_PRESETS,\n reverb: REVERB_PRESETS,\n};\n","/**\n * FxToggleBar Component\n *\n * Per-track FX control panel with 6 rows (one per FX category).\n * Each row: [Category toggle] [Preset 1-5 buttons] [Dry/Wet slider]\n *\n * Signal chain order: EQ -> Compressor -> Chorus -> Phaser -> Delay -> Reverb\n */\n\nimport React from 'react';\nimport type { FxCategory, TrackFxDetailState, FxCategoryDetailState } from '../types/fx-toggle.types';\nimport { FX_CATEGORIES, FX_DISPLAY_LABELS } from '../types/fx-toggle.types';\nimport { FX_PRESET_CONFIGS } from '../constants/fx-presets';\n\n/** Per-category active colors */\nconst FX_COLORS: Record<FxCategory, string> = {\n eq: 'bg-blue-500',\n compressor: 'bg-orange-500',\n chorus: 'bg-teal-500',\n phaser: 'bg-purple-500',\n delay: 'bg-green-500',\n reverb: 'bg-cyan-500',\n};\n\nexport interface FxToggleBarProps {\n trackId: string;\n fxState: TrackFxDetailState;\n onToggle: (trackId: string, category: FxCategory, enabled: boolean) => void;\n onPresetChange: (trackId: string, category: FxCategory, presetIndex: number) => void;\n onDryWetChange: (trackId: string, category: FxCategory, value: number) => void;\n disabled?: boolean;\n}\n\nexport const FxToggleBar: React.FC<FxToggleBarProps> = ({\n trackId,\n fxState,\n onToggle,\n onPresetChange,\n onDryWetChange,\n disabled = false,\n}) => {\n return (\n <div className=\"flex flex-col gap-1\" data-testid=\"fx-toggle-bar\">\n {FX_CATEGORIES.map((category: FxCategory) => {\n const detail: FxCategoryDetailState = fxState[category];\n const isActive = detail.enabled;\n const label = FX_DISPLAY_LABELS[category];\n const activeColor = FX_COLORS[category];\n const config = FX_PRESET_CONFIGS[category];\n\n return (\n <div key={category} className=\"flex items-center gap-0.5\">\n {/* Category toggle button */}\n <button\n data-testid={`fx-toggle-${category}`}\n disabled={disabled}\n onClick={() => onToggle(trackId, category, !isActive)}\n className={`w-14 py-0.5 text-[10px] font-semibold rounded-sm transition-colors leading-none flex-shrink-0 text-center ${\n disabled\n ? 'bg-sas-panel text-sas-muted/30 cursor-not-allowed'\n : isActive\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/60 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={`${isActive ? 'Disable' : 'Enable'} ${category.toUpperCase()}`}\n >\n {label}\n </button>\n\n {/* Preset buttons 1-5 */}\n {config.presets.map((preset, idx: number) => (\n <button\n key={idx}\n data-testid={`fx-preset-${category}-${idx}`}\n disabled={disabled || !isActive}\n onClick={() => onPresetChange(trackId, category, idx)}\n className={`w-5 h-5 text-[9px] font-medium rounded-sm transition-colors leading-none flex-shrink-0 ${\n disabled || !isActive\n ? 'bg-sas-panel text-sas-muted/20 cursor-not-allowed'\n : detail.presetIndex === idx\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/50 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={preset.name}\n >\n {idx + 1}\n </button>\n ))}\n\n {/* Dry/Wet slider */}\n <input\n type=\"range\"\n data-testid={`fx-drywet-${category}`}\n min=\"0\"\n max=\"100\"\n value={Math.round(detail.dryWet * 100)}\n disabled={disabled || !isActive}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n onDryWetChange(trackId, category, Number(e.target.value) / 100)\n }\n className=\"flex-1 min-w-[30px] h-3 accent-sas-accent disabled:opacity-30 cursor-pointer disabled:cursor-not-allowed\"\n title={`Dry/Wet: ${Math.round(detail.dryWet * 100)}%`}\n />\n <span className=\"text-[8px] text-sas-muted/50 w-6 text-right flex-shrink-0\">\n {Math.round(detail.dryWet * 100)}%\n </span>\n </div>\n );\n })}\n </div>\n );\n};\n","/**\n * SorceryProgressBar Component\n *\n * A progress bar for long, uncertain wait times (10-30s). Supports two modes:\n *\n * 1. **Time-based mode** (when `estimatedDurationMs` is provided):\n * Uses elapsed time and an ease-out curve to pace progress realistically.\n * Reaches ~90% at the estimated completion time, then asymptotically\n * approaches 95% if the operation runs long.\n *\n * 2. **Phase-based mode** (legacy fallback, no `estimatedDurationMs`):\n * \"Zeno's Paradox\" style - progress moves quickly at first, then\n * asymptotically slows toward 95%.\n *\n * Visual style: Segmented \"retro CLI\" look with glowing teal accent,\n * diagonal stripes, and subtle pulse animation.\n */\n\nimport React, { useState, useEffect, useRef } from 'react';\n\n/**\n * Props for SorceryProgressBar component\n */\ninterface SorceryProgressBarProps {\n /** Whether loading is in progress */\n isLoading: boolean;\n /** Text shown during loading (default: \"CONJURING...\") */\n statusText?: string;\n /** Text shown on completion (default: \"COMPLETE\") */\n completeText?: string;\n /** Callback when loading completes */\n onComplete?: () => void;\n /** Height class override (default: \"h-10\") */\n heightClass?: string;\n /** Initial progress value (0-100) to resume from - persists across scene switches */\n initialProgress?: number;\n /** Callback when progress changes - use to persist progress in parent state */\n onProgressChange?: (progress: number) => void;\n /** Estimated total duration in ms - enables time-aware pacing */\n estimatedDurationMs?: number;\n}\n\n/**\n * Calculates target progress based on elapsed time and estimated duration.\n * Uses an ease-out power curve for natural-feeling progress:\n * - At 10% of estimated time: ~21% (feels responsive early)\n * - At 30% of estimated time: ~53% (good midpoint feel)\n * - At 50% of estimated time: ~74% (past halfway visually)\n * - At 80% of estimated time: ~88% (approaching completion)\n * - At 100% of estimated time: 90% (leaves room for overshoot)\n * - Beyond estimate: asymptotically approaches 95%\n */\nexport function calculateTimeBasedTarget(elapsedMs: number, estimatedDurationMs: number): number {\n const t = elapsedMs / estimatedDurationMs;\n if (t <= 0) return 0;\n\n if (t <= 1.0) {\n // Ease-out power curve reaching 90% at t=1.0\n return 90 * (1 - Math.pow(1 - t, 2.5));\n }\n\n // Beyond estimate: asymptotically approach 95%\n const overshootRatio = (elapsedMs - estimatedDurationMs) / estimatedDurationMs;\n return 90 + 5 * (1 - Math.exp(-overshootRatio * 3));\n}\n\n/**\n * Calculates the next progress value using \"Zeno's Paradox\" algorithm (legacy fallback).\n * - Phase 1 (0-20%): Rapid progress (5-15% per tick)\n * - Phase 2 (20-60%): Steady progress (2-7% per tick)\n * - Phase 3 (60-95%): Asymptotic slowdown\n * - Caps at 95% until actual completion\n */\nfunction calculateNextProgress(currentProgress: number): number {\n if (currentProgress < 20) {\n return currentProgress + Math.random() * 10 + 5;\n }\n if (currentProgress < 60) {\n return currentProgress + Math.random() * 5 + 2;\n }\n if (currentProgress < 95) {\n const remaining = 95 - currentProgress;\n const increment = remaining * (Math.random() * 0.2 + 0.1);\n return currentProgress + Math.max(increment, 0.1);\n }\n return 95;\n}\n\n/**\n * Calculates the next tick interval for phase-based mode (legacy fallback).\n */\nfunction calculateNextTickInterval(progress: number): number {\n if (progress < 30) {\n return Math.random() * 200 + 150; // 150-350ms\n }\n if (progress < 70) {\n return Math.random() * 300 + 200; // 200-500ms\n }\n return Math.random() * 600 + 400; // 400-1000ms\n}\n\n/** Tick interval for time-based mode (ms) */\nconst TIME_BASED_TICK_MIN = 200;\nconst TIME_BASED_TICK_RANGE = 100;\n\n/**\n * SorceryProgressBar - A mystical progress bar for uncertain wait times\n */\nexport function SorceryProgressBar({\n isLoading,\n statusText = 'CONJURING...',\n completeText = 'COMPLETE',\n onComplete,\n heightClass = 'h-10',\n initialProgress = 0,\n onProgressChange,\n estimatedDurationMs,\n}: SorceryProgressBarProps): React.ReactElement | null {\n const [progress, setProgress] = useState<number>(initialProgress);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Initialize to false so first render with isLoading=true triggers animation start\n const isLoadingRef = useRef<boolean>(false);\n const hasStartedRef = useRef<boolean>(false);\n const startTimeRef = useRef<number>(0);\n\n // Store callbacks in refs to avoid dependency issues\n const onProgressChangeRef = useRef(onProgressChange);\n const onCompleteRef = useRef(onComplete);\n onProgressChangeRef.current = onProgressChange;\n onCompleteRef.current = onComplete;\n\n // Store props in refs - only used when loading starts, not as dependencies\n const initialProgressRef = useRef(initialProgress);\n initialProgressRef.current = initialProgress;\n const estimatedDurationMsRef = useRef(estimatedDurationMs);\n estimatedDurationMsRef.current = estimatedDurationMs;\n\n // Effect to handle loading state changes - ONLY depends on isLoading\n useEffect(() => {\n const wasLoading = isLoadingRef.current;\n isLoadingRef.current = isLoading;\n\n if (isLoading && !wasLoading) {\n // Loading just started\n hasStartedRef.current = true;\n startTimeRef.current = Date.now();\n\n // Start fresh or resume from initial progress (read from ref)\n const startProgress = initialProgressRef.current > 0 ? initialProgressRef.current : 0;\n setProgress(startProgress);\n\n const duration = estimatedDurationMsRef.current;\n\n if (duration && duration > 0) {\n // Time-based mode: pace progress using elapsed time\n const tick = (): void => {\n setProgress((prev) => {\n const elapsed = Date.now() - startTimeRef.current;\n const target = calculateTimeBasedTarget(elapsed, duration);\n\n // Add subtle jitter for organic feel (±0.5%)\n const jitter = (Math.random() - 0.5) * 1.0;\n // Move toward target, ensure monotonically increasing, cap at 95%\n const next = Math.min(Math.max(target + jitter, prev + 0.05), 95);\n\n onProgressChangeRef.current?.(next);\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN + Math.random() * TIME_BASED_TICK_RANGE);\n return next;\n });\n };\n\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN);\n } else {\n // Phase-based mode (legacy fallback)\n const tick = (): void => {\n setProgress((prev) => {\n if (prev >= 95) {\n timerRef.current = setTimeout(tick, 1000);\n return 95;\n }\n\n const next = Math.min(calculateNextProgress(prev), 95);\n onProgressChangeRef.current?.(next);\n\n const interval = calculateNextTickInterval(next);\n timerRef.current = setTimeout(tick, interval);\n\n return next;\n });\n };\n\n const firstInterval = calculateNextTickInterval(startProgress);\n timerRef.current = setTimeout(tick, firstInterval);\n }\n } else if (!isLoading && wasLoading && hasStartedRef.current) {\n // Loading just finished - jump to 100%\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setProgress(100);\n onProgressChangeRef.current?.(100);\n onCompleteRef.current?.();\n hasStartedRef.current = false;\n }\n\n // Cleanup on unmount only\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n // ONLY depend on isLoading - other props are read from refs\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isLoading]);\n\n // Don't render if not loading and progress is 0\n if (!isLoading && progress === 0) {\n return null;\n }\n\n const displayProgress = Math.floor(progress);\n const isComplete = !isLoading && progress === 100;\n\n // Calculate transition duration based on progress phase\n const transitionDuration = progress < 50 ? '300ms' : progress < 80 ? '500ms' : '700ms';\n\n return (\n <div\n className={`relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`}\n >\n {/* Progress fill with stripes and glow */}\n <div\n className={`\n h-full\n bg-gradient-to-r from-sas-accent/70 to-sas-accent\n shadow-glow-soft\n sorcery-progress-fill\n animate-progress-stripes\n ${progress > 70 ? 'animate-progress-pulse' : ''}\n transition-all ease-out\n `}\n style={{\n width: `${progress}%`,\n transitionDuration,\n }}\n />\n\n {/* Text overlay */}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n {isLoading && progress < 100 ? (\n <span className=\"font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider\">\n {statusText} {displayProgress}%\n </span>\n ) : isComplete ? (\n <span className=\"font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider\">\n {completeText}\n </span>\n ) : null}\n </div>\n\n {/* Scanline overlay for retro CRT effect */}\n <div\n className=\"absolute inset-0 pointer-events-none opacity-10\"\n style={{\n backgroundImage: `repeating-linear-gradient(\n to bottom,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.3) 2px,\n rgba(0, 0, 0, 0.3) 4px\n )`,\n }}\n />\n </div>\n );\n}\n\nexport default SorceryProgressBar;\n","/**\n * useSceneState — Scene-keyed state hook for plugin developers.\n *\n * Works like `useState`, but maintains separate state per scene.\n * When the user switches scenes, the previous scene's state is preserved\n * and restored when they switch back.\n *\n * Returns `[value, setForCurrentScene, setForScene]`:\n * - `value` — state for the currently active scene\n * - `setForCurrentScene(v)` — updates state for whatever scene is active at call time\n * - `setForScene(sceneId, v)` — updates state for a specific scene (for async callbacks)\n *\n * Both setters support the functional updater pattern: `prev => next`.\n *\n * **Important:** For object/array `initialValue`, hoist to a module-level constant\n * to keep the setter callbacks referentially stable:\n * ```ts\n * const EMPTY: string[] = [];\n * const [items, setItems, setItemsForScene] = useSceneState(activeSceneId, EMPTY);\n * ```\n */\n\nimport { useState, useCallback, useRef } from 'react';\n\ntype SetSceneState<T> = (value: T | ((prev: T) => T)) => void;\ntype SetSceneStateForScene<T> = (sceneId: string, value: T | ((prev: T) => T)) => void;\n\nexport function useSceneState<T>(\n activeSceneId: string | null,\n initialValue: T\n): [T, SetSceneState<T>, SetSceneStateForScene<T>] {\n const [stateMap, setStateMap] = useState<Map<string, T>>(() => new Map());\n const activeSceneIdRef = useRef(activeSceneId);\n activeSceneIdRef.current = activeSceneId;\n\n const currentValue = activeSceneId !== null && stateMap.has(activeSceneId)\n ? stateMap.get(activeSceneId)!\n : initialValue;\n\n const setForCurrentScene = useCallback((value: T | ((prev: T) => T)): void => {\n const sid = activeSceneIdRef.current;\n if (sid === null) return;\n setStateMap(prev => {\n const current = prev.has(sid) ? prev.get(sid)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sid, next);\n return newMap;\n });\n }, [initialValue]);\n\n const setForScene = useCallback((sceneId: string, value: T | ((prev: T) => T)): void => {\n setStateMap(prev => {\n const current = prev.has(sceneId) ? prev.get(sceneId)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sceneId, next);\n return newMap;\n });\n }, [initialValue]);\n\n return [currentValue, setForCurrentScene, setForScene];\n}\n","/**\n * Plugin SDK Version\n *\n * Semver version of the plugin SDK contract.\n * Plugins declare minSdkVersion in their manifest.\n * Registry checks semver.gte(PLUGIN_SDK_VERSION, manifest.minHostVersion)\n * during activation and marks incompatible plugins accordingly.\n */\nexport const PLUGIN_SDK_VERSION = '2.0.0';\n"],"mappings":"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","names":["import_react","import_jsx_runtime","import_react","import_jsx_runtime","useDebouncedCallback","import_jsx_runtime","import_react","import_jsx_runtime","import_jsx_runtime","import_react"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/types/plugin-sdk.types.ts","../src/types/fx-toggle.types.ts","../src/components/TrackRow.tsx","../src/components/InstrumentDrawer.tsx","../src/components/VolumeSlider.tsx","../src/utils/volume-conversion.ts","../src/components/PanSlider.tsx","../src/constants/fx-presets.ts","../src/components/FxToggleBar.tsx","../src/components/SorceryProgressBar.tsx","../src/hooks/useSceneState.ts","../src/constants/sdk-version.ts"],"sourcesContent":["/**\n * @sas/plugin-sdk — Public API\n *\n * Everything an external plugin author needs to build a generator plugin\n * for Signals & Sorcery.\n */\n\n// ============================================================================\n// Types — Core plugin contract\n// ============================================================================\n\nexport type {\n GeneratorType,\n InstrumentDescriptor,\n GeneratorPlugin,\n PluginUIProps,\n PluginHost,\n ExportedPluginData,\n CreateTrackOptions,\n PluginTrackHandle,\n PluginTrackInfo,\n PluginSynthInfo,\n PluginTrackRuntimeState,\n TrackStateChangeListener,\n PluginFxCategoryDetailState,\n PluginTrackFxDetailState,\n MidiClipData,\n PluginMidiNote,\n MidiWriteResult,\n ExportMidiBundleOptions,\n ExportMidiBundleResult,\n PostProcessOptions,\n MusicalContext,\n PluginChordTiming,\n PluginGenerationContext,\n PluginConcurrentTrackInfo,\n PluginChordSegment,\n TransportEvent,\n DeckBoundaryEvent,\n PluginTransportState,\n PluginSceneInfo,\n PluginSceneContext,\n BulkAddPlaceholderTrack,\n TransportEventListener,\n DeckBoundaryListener,\n SceneChangeListener,\n UnsubscribeFn,\n LLMGenerationRequest,\n LLMGenerationResult,\n PluginPresetData,\n ShufflePresetResult,\n PluginSettingsSchema,\n SettingDefinition,\n PluginSettingsStore,\n // AI skill surface — lets plugins declare LLM-callable actions\n // registered as namespaced tools (plugin:<id>:<skill>). Required for\n // plugins that expose a `chat` or similar agent-delegation skill.\n PluginSkill,\n PluginSkillInputSchema,\n PluginErrorCode,\n PluginManifest,\n PluginCapabilities,\n PluginFileDialogOptions,\n PluginDownloadOptions,\n PluginHttpRequestOptions,\n PluginHttpResponse,\n PluginSampleFilter,\n PluginSampleInfo,\n PluginSampleImportResult,\n PluginSampleTrackInfo,\n PluginAudioTextureRequest,\n PluginAudioTextureResult,\n PluginCuePoints,\n ComposeSceneOptions,\n ComposeSceneResult,\n ComposeProgressListener,\n ComposeProgressEvent,\n PluginPresetInfo,\n SavePluginPresetOptions,\n PluginAppTool,\n PluginAppToolInputSchema,\n PluginAppToolResult,\n PluginStatus,\n PluginRegistration,\n StemType,\n PluginStemSplitResult,\n PluginStemTrackInfo,\n} from './types/plugin-sdk.types';\n\nexport { PluginError } from './types/plugin-sdk.types';\n\n// ============================================================================\n// Types — FX toggle system\n// ============================================================================\n\nexport type {\n FxCategory,\n FxPreset,\n MixInterpolation,\n FxPresetConfig,\n FxCategoryDetailState,\n TrackFxDetailState,\n TrackFxState,\n FxPresetDataEntry,\n FxPresetData,\n} from './types/fx-toggle.types';\n\nexport {\n FX_CATEGORIES,\n FX_CHAIN_ORDER,\n FX_ENGINE_PLUGIN_NAMES,\n FX_DISPLAY_LABELS,\n EMPTY_FX_STATE,\n DEFAULT_FX_DRY_WET,\n DEFAULT_FX_CATEGORY_DETAIL,\n EMPTY_FX_DETAIL_STATE,\n} from './types/fx-toggle.types';\n\n// ============================================================================\n// Components\n// ============================================================================\n\nexport { TrackRow, type SDKTrackRowProps } from './components/TrackRow';\nexport { InstrumentDrawer, type InstrumentDrawerProps } from './components/InstrumentDrawer';\nexport { VolumeSlider } from './components/VolumeSlider';\nexport { PanSlider } from './components/PanSlider';\nexport { FxToggleBar, type FxToggleBarProps } from './components/FxToggleBar';\nexport { SorceryProgressBar, calculateTimeBasedTarget } from './components/SorceryProgressBar';\n\n// ============================================================================\n// Hooks\n// ============================================================================\n\nexport { useSceneState } from './hooks/useSceneState';\n\n// ============================================================================\n// Constants\n// ============================================================================\n\n// VALID_INSTRUMENT_ROLES (SDK 1.x) removed in 2.0.0 — external plugins now\n// call `host.getValidRoles()` on PluginHost at runtime. The canonical list\n// lives in the assistant (src/music-engine/constants/instrument-classification.ts)\n// and is exposed via that accessor.\nexport { PLUGIN_SDK_VERSION } from './constants/sdk-version';\nexport { FX_PRESET_CONFIGS } from './constants/fx-presets';\n\n// ============================================================================\n// Utils\n// ============================================================================\n\nexport { sliderToDb, dbToSlider, SLIDER_UNITY, DB_MAX, DB_MIN } from './utils/volume-conversion';\n","/**\n * Plugin SDK Type Definitions\n *\n * Complete type system for the generator plugin architecture.\n * Plugins implement GeneratorPlugin and interact with the host via PluginHost.\n * All plugin output flows through TracktionEngine (MIDI or audio clips).\n */\n\nimport type { ComponentType, ReactNode } from 'react';\n\n// ============================================================================\n// Core Plugin Interface\n// ============================================================================\n\n/** What kind of Tracktion content this plugin creates */\nexport type GeneratorType = 'midi' | 'audio' | 'sample' | 'hybrid';\n\n/** Describes an available instrument plugin (VST3/AU synth) on the system. */\nexport interface InstrumentDescriptor {\n /** Stable plugin identifier for loading (VST3 TUID or AU component ID) */\n pluginId: string;\n /** Display name */\n name: string;\n /** Plugin manufacturer */\n manufacturer: string;\n /** Plugin format */\n type: 'vst3' | 'au' | 'vst' | 'internal';\n /** Plugin category (from scan) */\n category: string;\n /** Whether this plugin is currently installed/available */\n missing?: boolean;\n}\n\n/** Every generator plugin must implement this interface. */\nexport interface GeneratorPlugin {\n /** Unique ID, npm-style scope: '@sas/synth-generator', '@user/my-plugin' */\n readonly id: string;\n /** Human-readable name shown in accordion header */\n readonly displayName: string;\n /** Semver version string */\n readonly version: string;\n /** Short description for settings/marketplace */\n readonly description: string;\n /** 24x24 icon — data URL, relative path from plugin dir, or undefined */\n readonly icon?: string;\n /** What kind of Tracktion content this plugin creates */\n readonly generatorType: GeneratorType;\n /** Minimum host SDK version this plugin requires */\n readonly minHostVersion?: string;\n\n /**\n * Called once when plugin is loaded. Receives the PluginHost API.\n * If this throws, plugin is marked as failed and not rendered.\n */\n activate(host: PluginHost): Promise<void>;\n\n /**\n * Called when plugin is being unloaded (disable, uninstall, app quit).\n * Must complete within 5 seconds or host force-kills.\n */\n deactivate(): Promise<void>;\n\n /**\n * Return the React component rendered inside the accordion section.\n * Component receives PluginUIProps from the host.\n */\n getUIComponent(): ComponentType<PluginUIProps>;\n\n /**\n * Return JSON Schema for plugin-specific settings.\n * Host auto-renders a settings form. Return null if no settings.\n */\n getSettingsSchema(): PluginSettingsSchema | null;\n\n /**\n * Optional: Called when the active scene changes.\n */\n onSceneChanged?(sceneId: string | null): Promise<void>;\n\n /**\n * Optional: Called when the generation context changes\n * (chords updated, tracks added/removed, BPM changed).\n */\n onContextChanged?(context: MusicalContext): void;\n\n /**\n * Optional: Declare LLM-callable skills this plugin provides.\n * Skills are registered as namespaced tools (plugin:<pluginId>:<skillId>)\n * and become available to AI agents for orchestration.\n *\n * Example: the chat-panel plugin declares a `chat` skill so external\n * agents (Claude Code, OpenClaw) can delegate scene-scoped natural\n * language work to the in-app agent via a single call.\n */\n getSkills?(): PluginSkill[];\n}\n\n// ============================================================================\n// Plugin Skills (AI Harness)\n// ============================================================================\n\n/** An LLM-callable action declared by a plugin. */\nexport interface PluginSkill {\n /** Unique skill id within this plugin (e.g., 'chat', 'generate_bassline') */\n id: string;\n /** Human-readable description — drives LLM tool selection */\n description: string;\n /** JSON Schema for the skill's input parameters */\n inputSchema: PluginSkillInputSchema;\n /** Whether this skill only reads state (no mutations). Default: false */\n isReadOnly?: boolean;\n}\n\n/** JSON Schema shape for skill input parameters. */\nexport interface PluginSkillInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n// ============================================================================\n// Plugin UI Props\n// ============================================================================\n\n/** Props passed to every plugin's React component by the host */\nexport interface PluginUIProps {\n /** The scoped PluginHost API instance for this plugin */\n host: PluginHost;\n /** Currently active scene ID (null if none selected) */\n activeSceneId: string | null;\n /** Whether the user is authenticated (for LLM access) */\n isAuthenticated: boolean;\n /** Whether all systems are connected (engine, gateway) */\n isConnected: boolean;\n /** Which workstation deck this is rendered in */\n deckId?: 'left' | 'right';\n /** Plugin calls this to set/clear header buttons. Pass null to clear. */\n onHeaderContent?: (content: ReactNode | null) => void;\n /** Plugin calls this to show/hide the loading spinner in the header. */\n onLoading?: (loading: boolean) => void;\n /** Scene-level context: contract state, chords, BPM, etc. Null if no scene. */\n sceneContext?: PluginSceneContext | null;\n /** Callback to open the scene selector (Scenes accordion section). */\n onSelectScene?: (() => void) | null;\n /** Callback to open the contract/chords section (for \"Generate a Contract\" CTA). */\n onOpenContract?: (() => void) | null;\n /** Callback to expand this plugin's own accordion section. */\n onExpandSelf?: (() => void) | null;\n}\n\n// ============================================================================\n// PluginHost API\n// ============================================================================\n\n/** Scoped API surface that plugins interact with. Plugins NEVER get direct TracktionEngine access. */\nexport interface PluginHost {\n // --- Track Management (ownership-scoped) ---\n\n /** Create a new track in the active scene. Host enforces ownership and scene routing. */\n createTrack(options: CreateTrackOptions): Promise<PluginTrackHandle>;\n\n /** Delete a track previously created by THIS plugin. */\n deleteTrack(trackId: string): Promise<void>;\n\n /** Get all tracks this plugin owns in the active scene. */\n getPluginTracks(): Promise<PluginTrackHandle[]>;\n\n /** Adopt unowned tracks in the active scene matching this plugin's generator type. */\n adoptSceneTracks(): Promise<PluginTrackHandle[]>;\n\n /** Get info about a specific owned track. */\n getTrackInfo(trackId: string): Promise<PluginTrackInfo>;\n\n /** Set track mute state. Only works on owned tracks. */\n setTrackMute(trackId: string, muted: boolean): Promise<void>;\n\n /** Set track volume (linear 0.0 - 1.0). Only works on owned tracks. */\n setTrackVolume(trackId: string, volume: number): Promise<void>;\n\n /** Set track pan (-1.0 left to 1.0 right). Only works on owned tracks. */\n setTrackPan(trackId: string, pan: number): Promise<void>;\n\n /** Set track solo state. Only works on owned tracks. */\n setTrackSolo(trackId: string, solo: boolean): Promise<void>;\n\n /** Rename a track. Only works on owned tracks. */\n setTrackName(trackId: string, name: string): Promise<void>;\n\n /**\n * Persist a track's musical role to the `tracks.role` column. Call this\n * after an LLM generation classifies the track (e.g. `'bass'`, `'lead'`,\n * `'pad'`, `'fx'`, `'kicks'`) so downstream features — especially the v1\n * transition generator's layer classifier — can see the role.\n *\n * Canonical values understood by the transition classifier include\n * `bass`, `drums`, `lead`, `chords`, `pad`, `arp`, `fx`, `kicks`,\n * `snares`, `hats`, `clap`, `perc`, `riser`, `impact`. Anything else is\n * stored verbatim but won't match the neutral-role set.\n *\n * Only works on owned tracks.\n */\n setTrackRole(trackId: string, role: string): Promise<void>;\n\n /** Shuffle preset: keep MIDI, apply a random preset from the same category. Only works on owned tracks. */\n shufflePreset(trackId: string): Promise<ShufflePresetResult>;\n\n /** Duplicate track: copy MIDI + role to a new track with a different preset. Only works on owned tracks. */\n duplicateTrack(trackId: string): Promise<PluginTrackHandle>;\n\n /**\n * Return the canonical list of valid role tokens that the host's\n * classifier and UI understand. Plugins should use this list when\n * building LLM prompts or validating role values before calling\n * {@link setTrackRole}.\n *\n * The assistant owns the canonical taxonomy — plugins MUST NOT ship\n * their own hardcoded list, which would drift from the host. Pair with\n * {@link setTrackRole} to persist a classified role.\n *\n * @since SDK 2.0.0\n */\n getValidRoles(): readonly string[];\n\n // --- FX Operations (ownership-scoped) ---\n\n /** Get detailed FX state for a track (enabled, preset, dry/wet per category). */\n getTrackFxState(trackId: string): Promise<PluginTrackFxDetailState>;\n\n /** Toggle an FX category on/off for a track. */\n toggleTrackFx(trackId: string, category: string, enabled: boolean): Promise<void>;\n\n /** Set FX preset for a track. Returns the new dry/wet value if applicable. */\n setTrackFxPreset(trackId: string, category: string, presetIndex: number): Promise<{ dryWet?: number }>;\n\n /** Set FX dry/wet level for a track. */\n setTrackFxDryWet(trackId: string, category: string, value: number): Promise<void>;\n\n // --- Real-time Track State ---\n\n /** Subscribe to real-time track state changes (mute, solo, volume, pan). Returns unsubscribe fn. */\n onTrackStateChange(listener: TrackStateChangeListener): UnsubscribeFn;\n\n // --- MIDI Operations ---\n\n /** Write MIDI notes to a track this plugin owns. Replaces existing MIDI. */\n writeMidiClip(trackId: string, clip: MidiClipData): Promise<MidiWriteResult>;\n\n /** Clear all MIDI from a track this plugin owns. */\n clearMidi(trackId: string): Promise<void>;\n\n /**\n * Export all tracks owned by this plugin in the active scene as a ZIP bundle\n * of Standard MIDI Files (one .mid per track, named after each track with\n * collision-avoidance suffixes). Prompts the user for a save location.\n *\n * Tracks with no MIDI data are skipped. Returns the path written, or\n * `{ canceled: true }` if the user dismissed the save dialog.\n *\n * @since SDK 1.1.0\n */\n exportTracksAsMidiBundle(\n options?: ExportMidiBundleOptions\n ): Promise<ExportMidiBundleResult>;\n\n /**\n * Run the host's MIDI post-processing pipeline on raw notes.\n * Wraps MidiProcessor: quantize -> swing -> scale -> register -> overlaps -> humanize.\n */\n postProcessMidi(notes: PluginMidiNote[], options: PostProcessOptions): Promise<PluginMidiNote[]>;\n\n // --- Audio Operations ---\n\n /** Place an audio file on a track this plugin owns. */\n writeAudioClip(trackId: string, filePath: string, position?: number): Promise<void>;\n\n /**\n * Render a single track to a temporary WAV file and return its path.\n * Only works on owned tracks. For MIDI/synth tracks the host mutes siblings\n * and renders the scene. For single-clip audio tracks the host MAY take a\n * copy-source fast path.\n * @since SDK 1.2.0\n */\n exportTrackAudio?(trackId: string): Promise<ExportTrackAudioResult>;\n\n /**\n * Run a chain of audio operations on an input WAV via the bundled\n * sas-audio-processor binary. Unsupported ops throw NOT_IMPLEMENTED.\n * @since SDK 1.2.0\n */\n processAudio?(\n inputPath: string,\n operations: AudioProcessingOp[]\n ): Promise<ProcessAudioResult>;\n\n /**\n * Replace a track's audio content. For audio tracks, clears clips and\n * adds the new audio. For MIDI/synth tracks, the original row is stashed\n * in plugin_data and a new audio_tracks row is inserted (MIDI is lost).\n * @since SDK 1.2.0\n */\n replaceTrackAudio?(trackId: string, audioPath: string): Promise<void>;\n\n // --- Plugin/Synth Operations ---\n\n /** Load a VST3/AU plugin onto a track this plugin owns. */\n loadSynthPlugin(trackId: string, pluginName: string): Promise<number>;\n\n /** Set plugin state (base64-encoded preset data). */\n setPluginState(trackId: string, pluginIndex: number, stateBase64: string): Promise<void>;\n\n /** Get current plugin state (base64-encoded). */\n getPluginState(trackId: string, pluginIndex: number): Promise<string>;\n\n /** List plugins currently loaded on a track. */\n getTrackPlugins(trackId: string): Promise<PluginSynthInfo[]>;\n\n /** Remove a plugin from a track. */\n removePlugin(trackId: string, pluginIndex: number): Promise<void>;\n\n /** Check if a specific VST/AU plugin is available on the system. */\n isPluginAvailable(pluginName: string): Promise<boolean>;\n\n // --- Instrument Plugin Selection ---\n\n /** Get available instrument plugins (VST3/AU synths) scanned by the engine. */\n getAvailableInstruments(): Promise<InstrumentDescriptor[]>;\n\n /** Get the instrument plugin currently loaded on a track. Null = default (Surge XT). */\n getTrackInstrument(trackId: string): Promise<InstrumentDescriptor | null>;\n\n /** Change the instrument plugin on a track. Preserves MIDI data. */\n setTrackInstrument(trackId: string, pluginId: string): Promise<void>;\n\n /** Open the instrument plugin's native editor GUI as a floating window. */\n showInstrumentEditor(trackId: string): Promise<void>;\n\n /** Close the instrument plugin's editor window. */\n hideInstrumentEditor(trackId: string): Promise<void>;\n\n // --- Scene Context (read-only) ---\n\n /** Get the FULL generation context for the active scene. */\n getGenerationContext(excludeTrackId?: string): Promise<PluginGenerationContext>;\n\n /** Get lightweight musical context (no concurrent track MIDI data). */\n getMusicalContext(): Promise<MusicalContext>;\n\n /** Get the active scene ID. Null if no scene is active. */\n getActiveSceneId(): string | null;\n\n /** Get list of all scenes in the project. */\n getSceneList(): Promise<PluginSceneInfo[]>;\n\n // --- Transport & Playback Events ---\n\n /** Subscribe to transport state changes. Returns unsubscribe function. */\n onTransportEvent(listener: TransportEventListener): UnsubscribeFn;\n\n /** Subscribe to deck boundary events. Returns unsubscribe function. */\n onDeckBoundary(listener: DeckBoundaryListener): UnsubscribeFn;\n\n /** Subscribe to scene change events. Returns unsubscribe function. */\n onSceneChange(listener: SceneChangeListener): UnsubscribeFn;\n\n /** Get current transport state (one-shot). */\n getTransportState(): Promise<PluginTransportState>;\n\n // --- LLM Access (metered, authenticated) ---\n\n /** Generate text/JSON via the host's authenticated LLM service. */\n generateWithLLM(request: LLMGenerationRequest): Promise<LLMGenerationResult>;\n\n /** Check if LLM access is available (user authenticated + gateway reachable). */\n isLLMAvailable(): Promise<boolean>;\n\n // --- App Tool Bridge ---\n\n /**\n * List the host's registered app tools. Used by plugins (e.g. the chat\n * panel) that want to expose the same surface external AI agents have.\n *\n * `opts.scope` filters by scope tag — scene-scoped consumers pass\n * `'scene'` to hide project-level tools they shouldn't call. When omitted,\n * every tool regardless of scope is returned.\n *\n * @since SDK 1.2.0\n */\n listAppTools(opts?: { scope?: 'scene' | 'project' }): Promise<PluginAppTool[]>;\n\n /**\n * Execute a host app tool by name. Delegates to the in-process\n * ToolRegistry — every mutation broadcasts to the UI automatically.\n *\n * For scene-scoped tools tagged with `autoBindSceneId`, the host\n * overrides the caller's `sceneId` param with the currently-active\n * scene. That keeps a scene-bound caller from accidentally targeting\n * another scene.\n *\n * @since SDK 1.2.0\n */\n executeAppTool(\n name: string,\n params: Record<string, unknown>\n ): Promise<PluginAppToolResult>;\n\n // --- Preset System ---\n\n /** Get available preset categories for a synth plugin. */\n getPresetCategories(pluginName: string): Promise<string[]>;\n\n /** Get a random preset from a category. */\n getRandomPreset(category: string): Promise<PluginPresetData | null>;\n\n /** Get a specific preset by name from a category. */\n getPresetByName(category: string, name: string): Promise<PluginPresetData | null>;\n\n /** Use LLM to classify a text description into a preset category. */\n classifyPresetCategory(description: string): Promise<string>;\n\n // --- Storage & Settings ---\n\n /** Get absolute path to this plugin's isolated data directory. */\n getDataDirectory(): string;\n\n /** Persisted key-value settings store. */\n settings: PluginSettingsStore;\n\n // --- Scoped Data API ---\n\n /** Get a value from scene-scoped plugin data. */\n getSceneData<T = unknown>(sceneId: string, key: string): Promise<T | null>;\n\n /** Set a value in scene-scoped plugin data. */\n setSceneData(sceneId: string, key: string, value: unknown): Promise<void>;\n\n /** Get all key-value pairs for a scene. */\n getAllSceneData(sceneId: string): Promise<Record<string, unknown>>;\n\n /** Delete a key from scene-scoped plugin data. */\n deleteSceneData(sceneId: string, key: string): Promise<void>;\n\n /** Get the full project-scoped state object. */\n getProjectData<T = unknown>(key: string): Promise<T | null>;\n\n /** Set a project-scoped data value. */\n setProjectData(key: string, value: unknown): Promise<void>;\n\n // --- Notifications & Progress ---\n\n /** Show a toast notification to the user. */\n showToast(type: 'info' | 'success' | 'warning' | 'error', title: string, message?: string): void;\n\n /** Set progress indicator on a specific track. -1 to hide. */\n setProgress(trackId: string, progress: number): void;\n\n /** Set a global status message in the plugin's accordion header. */\n setStatusMessage(message: string | null): void;\n\n /** Request user confirmation via a modal dialog. */\n confirmAction(title: string, message: string): Promise<boolean>;\n\n // --- File System (Phase 2) ---\n\n /** Show a native file open dialog. Requires 'fileDialog' capability. */\n showOpenDialog(options: PluginFileDialogOptions): Promise<string[] | null>;\n\n /** Show a native file save dialog. Requires 'fileDialog' capability. */\n showSaveDialog(options: PluginFileDialogOptions): Promise<string | null>;\n\n /** Download a file to the plugin's data directory. */\n downloadFile(url: string, filename: string, options?: PluginDownloadOptions): Promise<string>;\n\n /** Copy a file into the plugin's data directory. */\n importFile(sourcePath: string, destFilename: string): Promise<string>;\n\n // --- Network (Phase 2, capability-gated) ---\n\n /** Make an HTTP request. Requires 'network' capability with allowedHosts. */\n httpRequest(options: PluginHttpRequestOptions): Promise<PluginHttpResponse>;\n\n // --- Secure Storage (Phase 2) ---\n\n /** Store a secret in the OS keychain (plugin-scoped). */\n storeSecret(key: string, value: string): Promise<void>;\n\n /** Retrieve a secret from the OS keychain (plugin-scoped). */\n getSecret(key: string): Promise<string | null>;\n\n /** Delete a secret from the OS keychain (plugin-scoped). */\n deleteSecret(key: string): Promise<void>;\n\n // --- Sample Library (Phase 2) ---\n\n /** Query the sample library with optional filters. */\n getSamples(filter?: PluginSampleFilter): Promise<PluginSampleInfo[]>;\n\n /** Get a single sample by ID. */\n getSampleById(id: string): Promise<PluginSampleInfo | null>;\n\n /** Import audio files into the sample library. */\n importSamples(filePaths: string[]): Promise<PluginSampleImportResult>;\n\n /** Create a sample track in the active scene. */\n createSampleTrack(sampleId: string, options?: { name?: string }): Promise<PluginTrackHandle>;\n\n /** Delete a sample track. */\n deleteSampleTrack(trackId: string): Promise<void>;\n\n /** Get all sample tracks in the active scene. Re-establishes ownership. */\n getPluginSampleTracks(): Promise<PluginSampleTrackInfo[]>;\n\n /** Time-stretch a sample to a target BPM. Returns the new sample info. */\n timeStretchSample(sampleId: string, targetBpm: number): Promise<PluginSampleInfo>;\n\n /**\n * Fit a sample to the active scene's `(bpm, length_bars)`. Composes:\n * 1. Time-stretch to scene BPM (no-op if already matching).\n * 2. Chop / loop-stitch / passthrough so the resulting clip's duration\n * equals exactly `length_bars × 4 × (60 / bpm)` seconds.\n *\n * Required because the deck loops the clip at the scene's bar boundary —\n * a 4-bar sample dropped into a 2-bar scene used to over-run; a 4-bar\n * sample dropped into an 8-bar scene used to leave 4 bars of silence.\n *\n * The fitted sample is cached in the library by content hash, so\n * subsequent calls for the same `(sample, bpm, bars)` return instantly.\n */\n fitSampleToScene(sampleId: string): Promise<PluginSampleInfo>;\n\n /**\n * Lightweight one-shot sample audition through the cue (headphone) output.\n *\n * Plays the file via a dedicated SimpleLoopPlayer instance in the audio\n * engine — no Tracktion track or clip is created, no BPM matching, no\n * sync. Calling previewSample again with a different file replaces the\n * current preview cleanly. Independent of loop-b: starting/stopping a\n * preview never affects the performance deck and vice versa.\n */\n previewSample(filePath: string): Promise<void>;\n\n /**\n * Stop any in-flight sample preview started by previewSample(). Safe to\n * call when no preview is active — never throws.\n */\n stopPreview(): Promise<void>;\n\n // --- Audio Generation (Phase 2) ---\n\n /** Invoke the host's audio texture generation pipeline. */\n generateAudioTexture(request: PluginAudioTextureRequest): Promise<PluginAudioTextureResult>;\n\n // --- Audio Cue Points + Offset (Migration 060) ---\n\n /**\n * Persist cue points (detected beat positions) for an audio track.\n * Called once after `writeAudioClip` to remember the trim metadata so the\n * UI can later draw beat ticks and snap-to-beat the manual offset.\n *\n * Pass `null` to clear cue points. Throws OWNERSHIP_VIOLATION if the\n * track wasn't created by this plugin.\n */\n setCuePoints(trackId: string, cues: PluginCuePoints | null): Promise<void>;\n\n /** Read cue points previously written by `setCuePoints`. Returns null when none stored. */\n getCuePoints(trackId: string): Promise<PluginCuePoints | null>;\n\n /**\n * Set the manual sample-offset applied to the track's audio clip during\n * playback. Positive shifts later, negative shifts earlier with head\n * silence. Throws OWNERSHIP_VIOLATION if not owned by this plugin.\n */\n setAudioOffsetSamples(trackId: string, offsetSamples: number): Promise<void>;\n\n /** Read the current manual offset (0 if never set). */\n getAudioOffsetSamples(trackId: string): Promise<number>;\n\n // --- Scene Composition ---\n\n /** Trigger bulk composition for the active scene (LLM plans arrangement, creates tracks, generates MIDI). */\n composeScene(options: ComposeSceneOptions): Promise<ComposeSceneResult>;\n\n /** Subscribe to composition progress events (planning, generating, complete, error). */\n onComposeProgress(listener: ComposeProgressListener): UnsubscribeFn;\n\n /** Subscribe to engine ready events (fires when the engine finishes loading tracks after a scene change). */\n onEngineReady(listener: () => void): UnsubscribeFn;\n\n // --- MIDI Extensions (Phase 2) ---\n\n /** Audition a single note on a track (fire-and-forget preview). */\n auditionNote(trackId: string, pitch: number, velocity: number, durationMs: number): Promise<void>;\n\n // --- Plugin Presets (Phase 2) ---\n\n /** Get presets for this plugin, optionally filtered by category. */\n getPluginPresets(category?: string): Promise<PluginPresetInfo[]>;\n\n /** Save a new preset for this plugin. */\n savePluginPreset(options: SavePluginPresetOptions): Promise<PluginPresetInfo>;\n\n /** Delete a plugin preset by ID. */\n deletePluginPreset(id: string): Promise<void>;\n\n // --- Performance / Logging (Phase 2) ---\n\n /** Log a performance metric. */\n logMetric(name: string, durationMs: number, metadata?: Record<string, unknown>): void;\n\n /** Start a timer. Returns a stop function that logs the duration. */\n startTimer(name: string): () => void;\n\n // --- Stem Splitting ---\n\n /** Split an audio track into stems (vocals, drums, bass, other). Creates new muted tracks. */\n splitStems(trackId: string): Promise<PluginStemSplitResult>;\n\n /** Check if the stem splitter binary is available. */\n isStemSplitterAvailable(): Promise<boolean>;\n}\n\n// ============================================================================\n// Stem Splitting Types\n// ============================================================================\n\n/** Stem type identifiers */\nexport type StemType = 'vocals' | 'drums' | 'bass' | 'other';\n\n/** Result of splitting an audio track into stems */\nexport interface PluginStemSplitResult {\n /** Created stem tracks with audio loaded (all auto-muted) */\n stems: PluginStemTrackInfo[];\n}\n\n/** Information about a single stem track created by stem splitting */\nexport interface PluginStemTrackInfo {\n /** The stem type (vocals, drums, bass, other) */\n stemType: StemType;\n /** Track handle for the new stem track */\n track: PluginTrackHandle;\n}\n\n// ============================================================================\n// Exported Plugin Data Types (for .sasproj portability)\n// ============================================================================\n\nexport interface ExportedPluginData {\n pluginId: string;\n scope: 'project' | 'scene' | 'global';\n scopeId: string | null;\n key: string;\n value: string; // JSON-serialized\n}\n\n// ============================================================================\n// Track Types\n// ============================================================================\n\nexport interface CreateTrackOptions {\n /** Display name for the track. Auto-generated if omitted. */\n name?: string;\n /** Musical role hint: 'bass', 'drums', 'lead', 'chords', 'pad', 'arp', 'fx' */\n role?: string;\n /** Load a synth plugin immediately (default: false) */\n loadSynth?: boolean;\n /** Which synth to load (default: 'Surge XT'). Ignored if loadSynth=false. */\n synthName?: string;\n /**\n * Stable plugin identifier for a custom instrument (VST3 TUID or AU component ID).\n * If provided with loadSynth=true, loads this plugin instead of synthName.\n * Null/undefined = use default (Surge XT).\n */\n instrumentPluginId?: string | null;\n /** Metadata stored in DB. Plugins can use this for plugin-specific data. */\n metadata?: Record<string, unknown>;\n}\n\nexport interface PluginTrackHandle {\n /** Tracktion engine track ID (stable, GUID-based) */\n id: string;\n /** Display name */\n name: string;\n /** Database row ID */\n dbId: string;\n /** Musical role (if set) */\n role?: string;\n /** Prompt from tracks table (fallback when plugin_data not yet populated) */\n prompt?: string;\n /** Custom instrument plugin ID (null = default Surge XT) */\n instrumentPluginId?: string | null;\n /** Custom instrument display name (null = Surge XT) */\n instrumentName?: string | null;\n}\n\nexport interface PluginTrackInfo extends PluginTrackHandle {\n /** Is track muted? */\n muted: boolean;\n /** Is track soloed? */\n soloed: boolean;\n /** Volume (linear 0-1) */\n volume: number;\n /** Pan (-1 to 1) */\n pan: number;\n /** Loaded plugins on this track */\n plugins: PluginSynthInfo[];\n /** Has MIDI clips? */\n hasMidi: boolean;\n /** Has audio clips? */\n hasAudio: boolean;\n}\n\nexport interface PluginSynthInfo {\n index: number;\n name: string;\n type: string; // 'VST3' | 'AudioUnit' | 'Internal'\n enabled: boolean;\n}\n\n// ============================================================================\n// Real-time Track State Types\n// ============================================================================\n\n/** Real-time runtime state of a track (pushed from engine) */\nexport interface PluginTrackRuntimeState {\n id: string;\n muted: boolean;\n solo: boolean;\n volume: number;\n pan: number;\n}\n\n/** Listener for real-time track state changes */\nexport type TrackStateChangeListener = (trackId: string, state: PluginTrackRuntimeState) => void;\n\n// ============================================================================\n// FX Detail Types (SDK-friendly re-export)\n// ============================================================================\n\n/** Per-category FX detail state */\nexport interface PluginFxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Full FX detail state for a track — one entry per FX category */\nexport type PluginTrackFxDetailState = Record<string, PluginFxCategoryDetailState>;\n\n// ============================================================================\n// MIDI Types\n// ============================================================================\n\nexport interface MidiClipData {\n /** Start time in seconds */\n startTime: number;\n /** End time in seconds */\n endTime: number;\n /** BPM for beat<->time conversion */\n tempo: number;\n /** MIDI notes */\n notes: PluginMidiNote[];\n}\n\nexport interface PluginMidiNote {\n /** MIDI pitch 0-127 */\n pitch: number;\n /** Start position in quarter-note beats (0 = beginning of clip) */\n startBeat: number;\n /** Duration in quarter-note beats */\n durationBeats: number;\n /** Velocity 1-127 */\n velocity: number;\n /** MIDI channel 0-15 (default: 0) */\n channel?: number;\n}\n\nexport interface MidiWriteResult {\n /** Number of notes written */\n notesInserted: number;\n /** Actual bars covered */\n bars: number;\n}\n\n/**\n * Options for {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport interface ExportMidiBundleOptions {\n /** Default ZIP filename suggested in the save dialog (without extension). */\n defaultName?: string;\n}\n\n/**\n * Result of {@link PluginHost.exportTracksAsMidiBundle}.\n * @since SDK 1.1.0\n */\nexport type ExportMidiBundleResult =\n | { success: true; filePath: string; trackCount: number; skippedCount: number }\n | { success: false; canceled: true }\n | { success: false; canceled?: false; error: string };\n\n// ============================================================================\n// Audio Processing Bridge (SDK 1.2.0 — see ai-orchestration-design.md §16)\n// ============================================================================\n\n/** @since SDK 1.2.0 */\nexport interface ExportTrackAudioResult {\n path: string;\n bpm: number;\n durationMs: number;\n fromCopyFastPath?: boolean;\n}\n\n/** @since SDK 1.2.0 */\nexport interface ProcessAudioResult {\n outputPath: string;\n}\n\n/** @since SDK 1.2.0 */\nexport type AudioProcessingOp =\n | { tool: 'normalize' }\n | { tool: 'compress'; params?: { threshold?: number; ratio?: number } }\n | { tool: 'eq'; params?: { low_gain?: number; mid_gain?: number; high_gain?: number } }\n | { tool: 'reverb'; params?: { room_size?: number; dry_wet?: number } }\n | { tool: 'pitch-shift'; params: { semitones: number } }\n | { tool: 'time-stretch'; params: { target_bpm: number } }\n | { tool: 'filter'; params: { type: 'lowpass' | 'highpass'; cutoff: number } }\n | { tool: 'gain'; params: { db: number } }\n | { tool: 'limit' }\n | { tool: 'trim'; params?: { start?: number; end?: number } };\n\nexport interface PostProcessOptions {\n /** Snap notes to grid (default: true) */\n quantize?: boolean;\n /** Grid size: '1/4', '1/8', '1/16', '1/32', '1/8T', '1/16T' (default: '1/16') */\n quantizeGrid?: string;\n /** Quantize strength 0-100 (default: 75) */\n quantizeStrength?: number;\n /** Swing amount 0-100 (default: 0) */\n swing?: number;\n /** Humanize timing/velocity variation 0-100 (default: 0) */\n humanize?: number;\n /** Enforce diatonic scale (default: false). Uses scene key/mode. */\n enforceScale?: boolean;\n /** Clamp notes to pitch range [low, high] */\n clampRegister?: [number, number];\n /** Remove overlapping notes on same pitch/channel (default: true) */\n removeOverlaps?: boolean;\n}\n\n// ============================================================================\n// Context Types\n// ============================================================================\n\nexport interface MusicalContext {\n key: string; // 'C', 'D', 'Eb', 'F#', etc.\n mode: string; // 'major', 'minor', 'dorian', 'mixolydian', etc.\n bpm: number; // 20-960\n bars: number; // Scene length in bars\n genre: string | null; // 'Drum & Bass', 'Lo-fi Hip Hop', etc.\n timeSignature: string; // '4/4', '3/4', '6/8'\n chordProgression: PluginChordTiming[];\n}\n\nexport interface PluginChordTiming {\n /** Chord symbol: 'Cm7', 'G', 'Fmaj7', etc. */\n symbol: string;\n /** Start position in quarter notes */\n startQn: number;\n /** End position in quarter notes */\n endQn: number;\n}\n\n/** Full generation context — includes concurrent track MIDI data */\nexport interface PluginGenerationContext {\n chordProgression: {\n key: { tonic: string; mode: string };\n chordsWithTiming: PluginChordTiming[];\n genre: string | null;\n };\n concurrentTracks: PluginConcurrentTrackInfo[];\n}\n\nexport interface PluginConcurrentTrackInfo {\n trackId: string;\n role: string | undefined;\n presetCategory: string | null;\n /** Notes organized by which chord they fall under */\n notesByChord: PluginChordSegment[];\n}\n\nexport interface PluginChordSegment {\n chord: string;\n chordRangeQn: [number, number];\n notes: PluginMidiNote[];\n}\n\n// ============================================================================\n// Transport Types\n// ============================================================================\n\nexport interface TransportEvent {\n type: 'play' | 'stop' | 'pause' | 'bpmChange' | 'positionChange';\n bpm?: number;\n position?: number; // in seconds\n isPlaying?: boolean;\n}\n\nexport interface DeckBoundaryEvent {\n deckId: string; // 'loop-a', 'loop-b'\n bar: number; // Current bar number (1-based)\n beat: number; // Current beat within bar (1-based)\n loopCount: number; // How many loops completed\n}\n\nexport interface PluginTransportState {\n isPlaying: boolean;\n isPaused: boolean;\n bpm: number;\n position: number; // in seconds\n timeSignature: string;\n}\n\nexport interface PluginSceneInfo {\n id: string;\n name: string;\n isMuted: boolean;\n}\n\n/** Scene-level contract/context state passed to plugin UIs as a prop */\nexport interface PluginSceneContext {\n /** Whether a contract has been generated (genre or contractPrompt exists AND chords exist) */\n hasContract: boolean;\n /** Original user prompt text (e.g., \"dark psytrance\"). Null if none. */\n contractPrompt: string | null;\n /** Extracted genre. Null if none. */\n genre: string | null;\n /** Musical key. Null if no chord progression. */\n key: { tonic: string; mode: string } | null;\n /** Chord symbols (e.g., [\"Cm\", \"Fm\", \"G\"]). Empty if no chords. */\n chords: string[];\n /** BPM from project tempo */\n bpm: number;\n /** Scene length in bars */\n bars: number;\n /** Whether any synth tracks exist in this scene */\n hasTracks: boolean;\n /** Whether bulk generation is currently in progress */\n isBulkGenerating: boolean;\n}\n\n/** Placeholder track state for the progressive bulk-add UX */\nexport interface BulkAddPlaceholderTrack {\n id: string;\n planIndex: number;\n role: string;\n description: string;\n status: 'planned' | 'creating' | 'completed' | 'failed';\n error?: string;\n}\n\nexport type TransportEventListener = (event: TransportEvent) => void;\nexport type DeckBoundaryListener = (event: DeckBoundaryEvent) => void;\nexport type SceneChangeListener = (sceneId: string | null) => void;\nexport type UnsubscribeFn = () => void;\n\n// ============================================================================\n// LLM Types\n// ============================================================================\n\nexport interface LLMGenerationRequest {\n /** System prompt (instructions, role, output format) */\n system: string;\n /** User prompt (the actual request) */\n user: string;\n /** Max tokens for response (host may cap this) */\n maxTokens?: number;\n /** Expected response format hint */\n responseFormat?: 'text' | 'json';\n /**\n * If true, the host will NOT auto-prefix the user prompt with musical\n * context (key, BPM, chords, genre, etc.). Default: false (context IS\n * prefixed automatically).\n */\n skipContextPrefix?: boolean;\n}\n\nexport interface LLMGenerationResult {\n /** Raw response text */\n content: string;\n /** Tokens consumed */\n tokensUsed: number;\n /** Model that generated the response */\n model: string;\n}\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\nexport interface PluginPresetData {\n name: string;\n category: string;\n /** Base64-encoded plugin state — pass to setPluginState() */\n state: string;\n}\n\n/** Result of shufflePreset() — the new preset that was applied */\nexport interface ShufflePresetResult {\n presetName: string;\n presetCategory: string;\n}\n\n// ============================================================================\n// Settings Types\n// ============================================================================\n\nexport interface PluginSettingsSchema {\n type: 'object';\n properties: Record<string, SettingDefinition>;\n}\n\nexport interface SettingDefinition {\n type: 'string' | 'number' | 'boolean' | 'select';\n label: string;\n description?: string;\n default?: unknown;\n /** For 'select' type */\n options?: Array<{ label: string; value: string }>;\n /** For 'number' type */\n min?: number;\n max?: number;\n}\n\nexport interface PluginSettingsStore {\n get<T>(key: string, defaultValue: T): T;\n set(key: string, value: unknown): void;\n getAll(): Record<string, unknown>;\n /** Subscribe to settings changes. Returns unsubscribe fn. */\n onChange(listener: (key: string, value: unknown) => void): UnsubscribeFn;\n}\n\n// ============================================================================\n// Error Types\n// ============================================================================\n\nexport type PluginErrorCode =\n | 'NOT_OWNED' // Tried to modify a track not owned by this plugin\n | 'TRACK_NOT_FOUND' // Track ID doesn't exist in engine\n | 'TRACK_LIMIT_EXCEEDED' // Plugin has too many tracks\n | 'NO_ACTIVE_SCENE' // No scene selected\n | 'ENGINE_ERROR' // Tracktion engine call failed\n | 'INVALID_MIDI' // Malformed MIDI data\n | 'FILE_NOT_FOUND' // Audio file doesn't exist\n | 'INVALID_FORMAT' // Unsupported audio format\n | 'PLUGIN_NOT_FOUND' // VST/AU plugin not installed\n | 'LLM_BUDGET_EXCEEDED' // Over token limit\n | 'LLM_UNAVAILABLE' // Gateway unreachable\n | 'NOT_AUTHENTICATED' // User not logged in\n | 'TIMEOUT' // Operation timed out\n | 'CANCELLED' // User cancelled the operation\n | 'INCOMPATIBLE' // Plugin requires newer SDK version\n | 'CAPABILITY_DENIED' // Plugin lacks required capability\n | 'SECRET_NOT_FOUND' // Secret key doesn't exist\n | 'VALIDATION_ERROR'; // Inputs failed schema/format validation\n\nexport class PluginError extends Error {\n public readonly code: PluginErrorCode;\n public readonly details?: Record<string, unknown>;\n\n constructor(\n code: PluginErrorCode,\n message: string,\n details?: Record<string, unknown>\n ) {\n super(message);\n this.name = 'PluginError';\n this.code = code;\n this.details = details;\n }\n}\n\n// ============================================================================\n// Plugin Manifest (on-disk plugin.json)\n// ============================================================================\n\nexport interface PluginManifest {\n id: string;\n displayName: string;\n version: string;\n description: string;\n generatorType: GeneratorType;\n main: string; // e.g., 'dist/index.js'\n renderer?: string; // e.g., 'dist/ui.bundle.js' (UMD bundle for renderer)\n icon?: string; // e.g., 'assets/icon.svg'\n author?: string;\n license?: string;\n minHostVersion?: string;\n capabilities?: PluginCapabilities;\n settings?: Record<string, SettingDefinition>;\n builtIn?: boolean;\n repository?: string; // e.g., 'https://github.com/user/my-plugin'\n}\n\nexport interface PluginCapabilities {\n requiresLLM?: boolean;\n requiresSurgeXT?: boolean;\n requiresNetwork?: boolean;\n /** Allowed network hosts for httpRequest (e.g., ['api.splice.com']) */\n network?: { allowedHosts?: string[] };\n /** Plugin needs native file dialog access */\n fileDialog?: boolean;\n}\n\n// ============================================================================\n// Phase 2: File System Types\n// ============================================================================\n\nexport interface PluginFileDialogOptions {\n title?: string;\n defaultPath?: string;\n filters?: Array<{ name: string; extensions: string[] }>;\n /** For open dialog: allow selecting multiple files */\n multiSelections?: boolean;\n /** For open dialog: allow selecting directories */\n directories?: boolean;\n}\n\nexport interface PluginDownloadOptions {\n /** HTTP headers to include */\n headers?: Record<string, string>;\n /** Overwrite if file exists (default: false) */\n overwrite?: boolean;\n}\n\n// ============================================================================\n// Phase 2: Network Types\n// ============================================================================\n\nexport interface PluginHttpRequestOptions {\n url: string;\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';\n headers?: Record<string, string>;\n body?: string | Record<string, unknown>;\n /** Timeout in milliseconds (default: 30000) */\n timeoutMs?: number;\n}\n\nexport interface PluginHttpResponse {\n status: number;\n statusText: string;\n headers: Record<string, string>;\n body: string;\n}\n\n// ============================================================================\n// Phase 2: Sample Library Types\n// ============================================================================\n\nexport interface PluginSampleFilter {\n bpm?: number;\n key?: { tonic: string; mode?: string };\n category?: string;\n searchQuery?: string;\n}\n\nexport interface PluginSampleInfo {\n id: string;\n filename: string;\n filePath: string;\n category: string | null;\n bpm: number | null;\n keyTonic: string | null;\n keyMode: string | null;\n durationSeconds: number | null;\n fileSizeBytes: number | null;\n tags: string[] | null;\n}\n\nexport interface PluginSampleImportResult {\n imported: number;\n skipped: number;\n errors: string[];\n}\n\n/** Sample track with associated sample metadata (returned by getPluginSampleTracks) */\nexport interface PluginSampleTrackInfo {\n track: PluginTrackHandle;\n sample: PluginSampleInfo;\n volume: number;\n pan: number;\n}\n\n// ============================================================================\n// Phase 2: Audio Generation Types\n// ============================================================================\n\nexport interface PluginAudioTextureRequest {\n /** Text prompt describing the audio texture */\n prompt: string;\n /** Duration in seconds (default: scene length) */\n durationSeconds?: number;\n /** Target BPM (default: project BPM) */\n bpm?: number;\n}\n\nexport interface PluginAudioTextureResult {\n /** Path to the generated audio file */\n filePath: string;\n /** Duration of the generated audio in seconds */\n durationSeconds: number;\n /**\n * Beat positions inside the generated audio file plus the detected BPM.\n * Sample positions are relative to the file at `filePath`. Null when the\n * audio-processor did not surface detection data (older binary, fallback\n * path, or processing failed). Persist via `host.setCuePoints` after the\n * clip is written so the OffsetScrubber UI can read them later.\n */\n cuePoints: PluginCuePoints | null;\n}\n\n/**\n * Cue-points sidecar surfaced by the audio-processor `trim` command —\n * sample positions for each detected beat inside the generated WAV.\n * Mirrors the canonical `CuePoints` shape from the assistant; duplicated\n * here so external plugins don't reach into sas-assistant internals.\n */\nexport interface PluginCuePoints {\n /** Schema version (currently 1). */\n schema: 1;\n /** Sample rate the beat positions are expressed in. */\n sample_rate: number;\n /** Detected BPM (may differ from project BPM). Null when detection failed. */\n detected_bpm: number | null;\n /** Sample position of bar 1 / beat 1 inside the clip. */\n downbeat_sample: number;\n /** Monotone-increasing array of beat positions in samples. */\n beats: number[];\n /** ISO-8601 timestamp of when detection ran. */\n detected_at: string;\n}\n\n// ============================================================================\n// Scene Composition Types\n// ============================================================================\n\n/** Options for composing a full scene arrangement via LLM. */\nexport interface ComposeSceneOptions {\n /** The contract prompt / musical direction for the arrangement. */\n contractPrompt: string;\n /** Genre hint (e.g. 'techno', 'jazz'). Optional. */\n genre?: string | null;\n}\n\n/** Result from a scene composition. */\nexport interface ComposeSceneResult {\n /** Whether the composition completed successfully. */\n success: boolean;\n /** Number of tracks created. */\n tracksCreated: number;\n /** Error message if not successful. */\n error?: string;\n}\n\n/** Listener for composition progress events. */\nexport type ComposeProgressListener = (event: ComposeProgressEvent) => void;\n\n/** Progress event emitted during scene composition. */\nexport interface ComposeProgressEvent {\n /** Current phase: 'planning' (LLM deciding tracks), 'generating' (creating MIDI), 'complete', 'error'. */\n phase: 'planning' | 'generating' | 'complete' | 'error';\n /** Per-track placeholder state (available once planning is done). */\n placeholders?: BulkAddPlaceholderTrack[];\n /** Error message when phase is 'error'. */\n error?: string;\n /** Scene ID this compose event belongs to (for scene-keyed UI state). */\n sceneId?: string;\n}\n\n// ============================================================================\n// Phase 2: Plugin Preset Types\n// ============================================================================\n\nexport interface PluginPresetInfo {\n id: string;\n name: string;\n category: string | null;\n isBuiltIn: boolean;\n data: Record<string, unknown>;\n}\n\nexport interface SavePluginPresetOptions {\n name: string;\n category?: string;\n data: Record<string, unknown>;\n}\n\n// ============================================================================\n// App Tool Bridge (since SDK 1.2.0)\n// ============================================================================\n\n/** JSON Schema shape for a tool's input params. */\nexport interface PluginAppToolInputSchema {\n type: 'object';\n properties?: Record<string, unknown>;\n required?: string[];\n}\n\n/** Lightweight descriptor returned by `PluginHost.listAppTools`. */\nexport interface PluginAppTool {\n name: string;\n description: string;\n inputSchema: PluginAppToolInputSchema;\n /** `'scene'` = safe for scene-scoped callers. `'project'` = cross-scene. */\n scope?: 'scene' | 'project';\n}\n\n/** Result shape returned by `PluginHost.executeAppTool`. */\nexport interface PluginAppToolResult {\n success: boolean;\n action: string;\n message?: string;\n error?: string;\n /**\n * Tool-specific payload. Concrete shape depends on the tool — callers\n * should treat this as opaque unless they know the tool.\n */\n data?: unknown;\n}\n\n// ============================================================================\n// Plugin Registry Types (used by host internals)\n// ============================================================================\n\nexport type PluginStatus = 'pending' | 'active' | 'failed' | 'disabled' | 'incompatible';\n\nexport interface PluginRegistration {\n /** The loaded plugin instance */\n plugin: GeneratorPlugin;\n /** Current status */\n status: PluginStatus;\n /** Resolved manifest from disk */\n manifest: PluginManifest;\n /** The scoped PluginHost instance for this plugin */\n host: PluginHost | null;\n /** Sort order for accordion display */\n sortOrder: number;\n /** Whether the plugin is enabled */\n enabled: boolean;\n /** Error message if status is 'failed' */\n error?: string;\n}\n","/**\n * FX Toggle Types\n *\n * Types and constants for per-track FX toggle buttons.\n * Each track can enable/disable 6 FX categories independently.\n * The engine is the source of truth — no database persistence needed.\n */\n\n/** Available FX categories in signal chain order */\nexport type FxCategory = 'eq' | 'compressor' | 'chorus' | 'phaser' | 'delay' | 'reverb';\n\n/** All FX categories in signal chain order */\nexport const FX_CATEGORIES: readonly FxCategory[] = [\n 'eq',\n 'compressor',\n 'chorus',\n 'phaser',\n 'delay',\n 'reverb',\n] as const;\n\n/** Position in the signal chain (lower = earlier) */\nexport const FX_CHAIN_ORDER: Record<FxCategory, number> = {\n eq: 0,\n compressor: 1,\n chorus: 2,\n phaser: 3,\n delay: 4,\n reverb: 5,\n};\n\n/** Map from FxCategory to Tracktion Engine built-in plugin xmlTypeName */\nexport const FX_ENGINE_PLUGIN_NAMES: Record<FxCategory, string> = {\n eq: '4bandEq',\n compressor: 'compressor',\n chorus: 'chorus',\n phaser: 'phaser',\n delay: 'delay',\n reverb: 'reverb',\n};\n\n/** Display labels for UI buttons */\nexport const FX_DISPLAY_LABELS: Record<FxCategory, string> = {\n eq: 'EQ',\n compressor: 'Comp',\n chorus: 'Chorus',\n phaser: 'Phaser',\n delay: 'Delay',\n reverb: 'Reverb',\n};\n\n/** Per-track FX state: which categories are active */\nexport interface TrackFxState {\n eq: boolean;\n compressor: boolean;\n chorus: boolean;\n phaser: boolean;\n delay: boolean;\n reverb: boolean;\n}\n\n/** Default state: all FX disabled */\nexport const EMPTY_FX_STATE: TrackFxState = {\n eq: false,\n compressor: false,\n chorus: false,\n phaser: false,\n delay: false,\n reverb: false,\n};\n\n// ============================================================================\n// Preset Types\n// ============================================================================\n\n/** A single FX preset definition */\nexport interface FxPreset {\n /** Display name (e.g. \"Room\", \"Hall\") */\n name: string;\n /** Short label for button (e.g. \"RM\", \"HL\") */\n shortLabel: string;\n /** Map from automatable parameter name -> value (set via setPluginParameter) */\n params: Record<string, number>;\n /** CachedValue params set via XML state (getPluginState/setPluginState) */\n xmlStateParams?: Record<string, number>;\n /** BPM-relative delay time multiplier (1.0 = quarter note). When set, Delay Time is computed at apply time. */\n noteMultiplier?: number;\n /** Fixed delay time in ms (non-BPM-synced). Mutually exclusive with noteMultiplier. */\n fixedLengthMs?: number;\n}\n\n/** How dry/wet is applied to the plugin */\nexport type MixInterpolation = 'direct' | 'gain-scale' | 'ratio-scale';\n\n/** Preset configuration for an FX category */\nexport interface FxPresetConfig {\n /** Exactly 5 presets */\n presets: [FxPreset, FxPreset, FxPreset, FxPreset, FxPreset];\n /** Name of the native mix/wet parameter, or null if no native dry/wet */\n mixParamName: string | null;\n /** XML attribute name for dry/wet control (for plugins with no automatable mix param, e.g. chorus/phaser) */\n mixXmlAttr?: string;\n /** How to apply dry/wet (defaults to 'direct') */\n mixInterpolation: MixInterpolation;\n}\n\n/** Per-category detail state for a single FX on a track */\nexport interface FxCategoryDetailState {\n enabled: boolean;\n presetIndex: number; // 0-4\n dryWet: number; // 0.0-1.0\n}\n\n/** Extended FX state per track with preset and dry/wet info */\nexport type TrackFxDetailState = Record<FxCategory, FxCategoryDetailState>;\n\n/** Default dry/wet mix level (33% — musically useful for most effects) */\nexport const DEFAULT_FX_DRY_WET = 0.33;\n\n/** Default detail state for a single category */\nexport const DEFAULT_FX_CATEGORY_DETAIL: FxCategoryDetailState = {\n enabled: false,\n presetIndex: 0,\n dryWet: DEFAULT_FX_DRY_WET,\n};\n\n/** Default detail state: all FX disabled, preset 0, full wet */\nexport const EMPTY_FX_DETAIL_STATE: TrackFxDetailState = {\n eq: { ...DEFAULT_FX_CATEGORY_DETAIL },\n compressor: { ...DEFAULT_FX_CATEGORY_DETAIL },\n chorus: { ...DEFAULT_FX_CATEGORY_DETAIL },\n phaser: { ...DEFAULT_FX_CATEGORY_DETAIL },\n delay: { ...DEFAULT_FX_CATEGORY_DETAIL },\n reverb: { ...DEFAULT_FX_CATEGORY_DETAIL },\n};\n\n/** Persisted FX data for a single category (stored as JSON in database) */\nexport interface FxPresetDataEntry {\n presetIndex: number;\n dryWet: number;\n enabled: boolean;\n}\n\n/** Persisted FX data format (stored as JSON in database) */\nexport type FxPresetData = Partial<Record<FxCategory, FxPresetDataEntry>>;\n","/**\n * SDK TrackRow — Reusable track row component for generator plugins.\n *\n * Renders a complete track UI with prompt input, generation controls,\n * shuffle/copy, volume/pan, mute/solo, FX drawer, and visual states\n * (amber pulse for \"needs generation\", progress overlay, error indicator).\n *\n * Layout matches TrackInput (main branch) for visual parity.\n *\n * Depends only on PluginHost types + existing shared renderer components.\n */\n\nimport React from 'react';\nimport { AlertCircle } from 'lucide-react';\nimport { InstrumentDrawer } from './InstrumentDrawer';\nimport type { InstrumentDescriptor } from '../types/plugin-sdk.types';\nimport { VolumeSlider } from './VolumeSlider';\nimport { PanSlider } from './PanSlider';\nimport { FxToggleBar } from './FxToggleBar';\nimport { SorceryProgressBar } from './SorceryProgressBar';\nimport type { TrackFxDetailState, FxCategory } from '../types/fx-toggle.types';\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface SDKTrackRowProps {\n /** Track identity */\n track: { id: string; name: string; role?: string };\n /** Current prompt text (optional — omit when using contentSlot) */\n prompt?: string;\n /** Playback state */\n runtimeState: { muted: boolean; solo: boolean; volume: number; pan: number };\n /** FX category states */\n fxDetailState: TrackFxDetailState;\n /** FX panel visibility */\n fxDrawerOpen: boolean;\n /** Generation in progress */\n isGenerating?: boolean;\n /** Auth state */\n isAuthenticated?: boolean;\n /** Error from last generation */\n error?: string | null;\n /** Enables shuffle/copy buttons */\n hasMidi?: boolean;\n /** Progress % (for persistence across scene switches) */\n generationProgress?: number;\n /** For progress bar pacing */\n estimatedGenerationMs?: number;\n /** Prompt edit (optional — omit to hide prompt input) */\n onPromptChange?: (prompt: string) => void;\n /** \"Create\" button / Enter key (optional — omit to hide Create button) */\n onGenerate?: () => void;\n /** Shuffle preset (optional — omit to hide Shuffle button) */\n onShuffle?: () => void;\n /** Duplicate track (optional — omit to hide Copy button) */\n onCopy?: () => void;\n /** Delete track */\n onDelete: () => void;\n /** Custom content replacing the prompt input (e.g., sample info display) */\n contentSlot?: React.ReactNode;\n /** Toggle mute */\n onMuteToggle: () => void;\n /** Toggle solo */\n onSoloToggle: () => void;\n /** Volume slider */\n onVolumeChange: (vol: number) => void;\n /** Pan slider */\n onPanChange: (pan: number) => void;\n /** FX category toggle (optional — omit to hide FX button) */\n onFxToggle?: (cat: FxCategory, enabled: boolean) => void;\n /** FX preset select */\n onFxPresetChange?: (cat: FxCategory, idx: number) => void;\n /** FX dry/wet */\n onFxDryWetChange?: (cat: FxCategory, val: number) => void;\n /** Open/close FX (optional — omit to hide FX button) */\n onToggleFxDrawer?: () => void;\n /** Progress persistence callback */\n onProgressChange?: (pct: number) => void;\n /** Left border accent color */\n accentColor?: string;\n // --- Instrument Plugin Selection ---\n /** Current instrument display name (null/undefined = Surge XT default) */\n instrumentName?: string | null;\n /** Whether the current instrument plugin is missing from the system */\n instrumentMissing?: boolean;\n /** Whether the instrument drawer is open */\n instrumentDrawerOpen?: boolean;\n /** Toggle the instrument drawer */\n onToggleInstrumentDrawer?: () => void;\n /** Available instrument plugins for the drawer */\n availableInstruments?: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID */\n currentInstrumentPluginId?: string | null;\n /** Called when user selects an instrument from the drawer */\n onInstrumentSelect?: (pluginId: string) => void;\n /** Whether instrument scan is loading */\n instrumentsLoading?: boolean;\n /** Re-scan for instruments */\n onRefreshInstruments?: () => void;\n // --- Instrument Editor (Stage 2) ---\n /** Which stage the instrument drawer is in */\n instrumentDrawerStage?: 'instruments' | 'editor';\n /** Called when user clicks \"Open Editor\" */\n onShowEditor?: () => void;\n /** Called when user wants to go back from editor view */\n onBackToInstruments?: () => void;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function TrackRow({\n track,\n prompt,\n runtimeState,\n fxDetailState,\n fxDrawerOpen,\n isGenerating = false,\n isAuthenticated = false,\n error,\n hasMidi = false,\n generationProgress = 0,\n estimatedGenerationMs = 15000,\n onPromptChange,\n onGenerate,\n onShuffle,\n onCopy,\n onDelete,\n contentSlot,\n onMuteToggle,\n onSoloToggle,\n onVolumeChange,\n onPanChange,\n onFxToggle,\n onFxPresetChange,\n onFxDryWetChange,\n onToggleFxDrawer,\n onProgressChange,\n accentColor = '#A78BFA',\n instrumentName,\n instrumentMissing,\n instrumentDrawerOpen,\n onToggleInstrumentDrawer,\n availableInstruments,\n currentInstrumentPluginId,\n onInstrumentSelect,\n instrumentsLoading,\n onRefreshInstruments,\n instrumentDrawerStage,\n onShowEditor,\n onBackToInstruments,\n}: SDKTrackRowProps): React.ReactElement {\n const { muted: isMuted, solo: isSoloed, volume: currentVolume, pan: currentPan } = runtimeState;\n\n // \"Needs generation\" = has prompt, no MIDI yet, not currently generating\n const needsGeneration = !!(prompt?.trim() && !hasMidi && !isGenerating);\n\n const hasFxActive = Object.values(fxDetailState).some(\n (d: { enabled: boolean }) => d.enabled\n );\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>): void => {\n if (e.key === 'Enter' && !e.shiftKey && onGenerate) {\n e.preventDefault();\n onGenerate();\n }\n };\n\n // Amber pulse class for \"needs generation\" state\n const borderColorStyle = needsGeneration\n ? undefined // handled by className animation\n : accentColor;\n\n const borderClass = needsGeneration\n ? 'border-amber-400 animate-pulse'\n : 'border-sas-border';\n\n return (\n <div data-testid=\"sdk-track-row-wrapper\" className=\"w-full\">\n <div\n data-testid=\"sdk-track-row\"\n className={`relative flex items-stretch gap-1 p-2 rounded-sm border w-full overflow-hidden ${borderClass} bg-sas-panel-alt`}\n style={{\n borderLeftColor: needsGeneration ? '#f59e0b' : borderColorStyle,\n borderLeftWidth: '3px',\n }}\n >\n {/* Generating progress overlay - stops before buttons (right-44) */}\n {isGenerating && (\n <div className=\"absolute left-0 top-0 bottom-0 right-44 z-20\">\n <SorceryProgressBar\n isLoading={true}\n statusText=\"CONJURING MIDI...\"\n heightClass=\"h-full\"\n initialProgress={generationProgress}\n onProgressChange={onProgressChange}\n estimatedDurationMs={estimatedGenerationMs}\n />\n </div>\n )}\n\n {/* Left: Content area (prompt input or custom content slot) with track name, volume, and pan underneath */}\n <div className=\"flex flex-col flex-1 min-w-0 relative z-10\">\n {contentSlot ? contentSlot : onPromptChange ? (\n <input\n type=\"text\"\n data-testid=\"sdk-prompt-input\"\n value={prompt ?? ''}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => onPromptChange(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Describe your part...\"\n disabled={isGenerating}\n className=\"sas-input w-full px-2 py-1 text-xs disabled:opacity-50 disabled:cursor-not-allowed\"\n />\n ) : null}\n {/* Track name, volume slider, and pan slider in horizontal row */}\n <div className=\"flex items-center gap-2 mt-1\">\n {track.name && (\n <span className=\"text-[10px] text-sas-muted/60 truncate pl-2 flex-shrink-0 max-w-[80px]\" title={track.name}>\n {track.name}\n </span>\n )}\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">vol:</span>\n <VolumeSlider\n value={currentVolume}\n onChange={onVolumeChange}\n disabled={isGenerating}\n className=\"flex-1 min-w-[40px]\"\n />\n <span className=\"text-[9px] text-sas-muted/50 flex-shrink-0\">pan:</span>\n <PanSlider\n value={currentPan}\n onChange={onPanChange}\n disabled={isGenerating}\n className=\"w-10 flex-shrink-0\"\n />\n </div>\n </div>\n\n {/* Error indicator - shows when generation failed */}\n {error && (\n <div\n data-testid=\"sdk-error-indicator\"\n className=\"flex-shrink-0 relative z-10 self-stretch flex items-center px-1 group cursor-help\"\n title={error}\n >\n <div className=\"relative\">\n <AlertCircle\n className=\"w-5 h-5 text-red-500 animate-pulse\"\n strokeWidth={2.5}\n />\n {/* Tooltip - appears on hover */}\n <div className=\"absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-red-900/95 text-red-100 text-xs rounded shadow-lg whitespace-nowrap opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none z-50 max-w-[200px] truncate\">\n {error}\n </div>\n </div>\n </div>\n )}\n\n {/* Right: Button grid (2 rows) - z-30 to stay above generating overlay */}\n <div className=\"flex flex-col gap-0.5 flex-shrink-0 relative z-30 justify-center\">\n {/* Top row: [Create] [Copy] M x — Create/Copy only shown when handlers provided */}\n <div className=\"flex gap-1 items-center\">\n {onGenerate && (\n <button\n data-testid=\"sdk-generate-button\"\n onClick={onGenerate}\n disabled={!isAuthenticated || isGenerating || !prompt?.trim()}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !isAuthenticated || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : needsGeneration\n ? 'bg-amber-500/30 border-amber-500 text-amber-400 hover:bg-amber-500 hover:text-sas-bg animate-pulse'\n : prompt?.trim()\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n }`}\n title={!isAuthenticated ? 'Please log in' : isGenerating ? 'Generating...' : 'Generate MIDI'}\n >\n Create\n </button>\n )}\n {onCopy && (\n <button\n data-testid=\"sdk-copy-button\"\n onClick={onCopy}\n disabled={!hasMidi || isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={hasMidi ? 'Duplicate track with different preset' : 'Generate MIDI first'}\n >\n Copy\n </button>\n )}\n <button\n data-testid=\"sdk-mute-button\"\n onClick={onMuteToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isMuted\n ? 'bg-red-600 text-white'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isMuted ? 'Unmute track' : 'Mute track'}\n >\n M\n </button>\n <button\n data-testid=\"sdk-delete-button\"\n onClick={onDelete}\n className=\"text-sas-danger/70 hover:text-sas-danger px-1 py-0.5 transition-colors text-sm\"\n title=\"Delete track\"\n >\n x\n </button>\n </div>\n {/* Bottom row: [Shuffle] [FX] Solo [P] — Shuffle/FX only shown when handlers provided */}\n <div className=\"flex gap-1 items-center\">\n {onShuffle && (\n <button\n data-testid=\"sdk-shuffle-button\"\n onClick={onShuffle}\n disabled={!hasMidi || isGenerating || !!currentInstrumentPluginId}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n !hasMidi || isGenerating || !!currentInstrumentPluginId\n ? 'bg-sas-panel border-sas-border text-sas-muted/30 cursor-not-allowed'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={\n currentInstrumentPluginId\n ? 'Shuffle only works with default Surge XT'\n : hasMidi\n ? 'Re-roll sound (keep MIDI)'\n : 'Generate MIDI first'\n }\n >\n Shuffle\n </button>\n )}\n {onToggleFxDrawer && (\n <button\n data-testid=\"sdk-fx-button\"\n onClick={onToggleFxDrawer}\n disabled={isGenerating}\n className={`w-14 py-0.5 rounded-sm text-xs font-medium transition-colors border ${\n isGenerating\n ? 'bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed'\n : fxDrawerOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : hasFxActive\n ? 'bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg'\n : 'bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={fxDrawerOpen ? 'Hide FX controls' : 'Show FX controls'}\n >\n FX\n </button>\n )}\n <button\n data-testid=\"sdk-solo-button\"\n onClick={onSoloToggle}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : isSoloed\n ? 'bg-yellow-500 text-black'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={isSoloed ? 'Unsolo track' : 'Solo track'}\n >\n S\n </button>\n {onToggleInstrumentDrawer && (\n <button\n data-testid=\"sdk-plugin-button\"\n onClick={onToggleInstrumentDrawer}\n disabled={isGenerating}\n className={`px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${\n isGenerating\n ? 'bg-sas-panel text-sas-muted/50 cursor-not-allowed'\n : instrumentDrawerOpen\n ? 'bg-sas-accent border-sas-accent text-sas-bg'\n : instrumentMissing\n ? 'bg-amber-500/20 text-amber-400 hover:bg-amber-500/40'\n : 'bg-sas-panel-alt text-sas-muted hover:bg-sas-border'\n }`}\n title={`Plugin: ${instrumentName ?? 'Surge XT'}${instrumentMissing ? ' (missing)' : ''}`}\n >\n P\n </button>\n )}\n </div>\n </div>\n </div>\n\n {/* FX Drawer */}\n {fxDrawerOpen && !instrumentDrawerOpen && (\n <div data-testid=\"sdk-fx-drawer\" className=\"border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2 max-h-[180px] overflow-y-auto\">\n <FxToggleBar\n trackId={track.id}\n fxState={fxDetailState}\n onToggle={(_trackId: string, category: FxCategory, enabled: boolean) => onFxToggle?.(category, enabled)}\n onPresetChange={(_trackId: string, category: FxCategory, presetIndex: number) => onFxPresetChange?.(category, presetIndex)}\n onDryWetChange={(_trackId: string, category: FxCategory, value: number) => onFxDryWetChange?.(category, value)}\n disabled={isGenerating}\n />\n </div>\n )}\n\n {/* Instrument Drawer */}\n {instrumentDrawerOpen && !fxDrawerOpen && availableInstruments && onInstrumentSelect && onRefreshInstruments && (\n <div data-testid=\"sdk-instrument-drawer\" className=\"border border-t-0 border-sas-border bg-sas-bg rounded-b-sm px-3 py-2\">\n <InstrumentDrawer\n instruments={availableInstruments}\n currentPluginId={currentInstrumentPluginId ?? null}\n isLoading={instrumentsLoading ?? false}\n onSelect={onInstrumentSelect}\n onRefresh={onRefreshInstruments}\n stage={instrumentDrawerStage}\n onShowEditor={onShowEditor}\n onBackToInstruments={onBackToInstruments}\n selectedInstrumentName={instrumentName}\n />\n </div>\n )}\n </div>\n );\n}\n\nexport default TrackRow;\n","/**\n * InstrumentDrawer — Two-stage nested menu for instrument selection + editor access.\n *\n * Stage 1 (instruments): Searchable grid of available VST3/AU instrument plugins.\n * Stage 2 (editor): Shows \"Open Editor\" button for the selected plugin's native GUI.\n */\n\nimport React, { useState, useMemo } from 'react';\nimport type { InstrumentDescriptor } from '../types/plugin-sdk.types';\n\n// ============================================================================\n// Props\n// ============================================================================\n\nexport interface InstrumentDrawerProps {\n /** Available instrument plugins from engine scan */\n instruments: InstrumentDescriptor[];\n /** Currently loaded instrument plugin ID (null = default Surge XT) */\n currentPluginId: string | null;\n /** Whether the scan is still in progress */\n isLoading: boolean;\n /** Called when user selects an instrument */\n onSelect: (pluginId: string) => void;\n /** Called when user clicks refresh to re-scan plugins */\n onRefresh: () => void;\n // --- Editor access (Stage 2) ---\n /** Which stage the drawer is in */\n stage?: 'instruments' | 'editor';\n /** Called when user clicks \"Open Editor\" */\n onShowEditor?: () => void;\n /** Called when user wants to go back from editor view to instrument list */\n onBackToInstruments?: () => void;\n /** Name of the selected instrument (for display in editor header) */\n selectedInstrumentName?: string | null;\n}\n\n// ============================================================================\n// Component\n// ============================================================================\n\nexport function InstrumentDrawer({\n instruments,\n currentPluginId,\n isLoading,\n onSelect,\n onRefresh,\n stage = 'instruments',\n onShowEditor,\n onBackToInstruments,\n selectedInstrumentName,\n}: InstrumentDrawerProps): React.ReactElement {\n const [search, setSearch] = useState('');\n\n /** Sentinel pluginId for the default Surge XT entry */\n const SURGE_XT_DEFAULT_ID = 'Surge XT';\n\n // Filter instruments by search query, with selected instrument always first\n const filtered = useMemo((): InstrumentDescriptor[] => {\n let all = instruments.filter(\n (i: InstrumentDescriptor) => i.name !== 'Surge XT'\n );\n if (search.trim()) {\n const q = search.toLowerCase();\n all = all.filter(\n (i: InstrumentDescriptor) =>\n i.name.toLowerCase().includes(q) ||\n i.manufacturer.toLowerCase().includes(q)\n );\n }\n // Move the currently selected instrument to the top\n if (currentPluginId) {\n const selectedIdx = all.findIndex((i: InstrumentDescriptor) => i.pluginId === currentPluginId);\n if (selectedIdx > 0) {\n const [selected] = all.splice(selectedIdx, 1);\n all.unshift(selected);\n }\n }\n return all;\n }, [instruments, search, currentPluginId]);\n\n // Is the default Surge XT selected?\n const isDefaultSelected = currentPluginId === null;\n\n // Determine which pluginId is \"selected\" among scanned instruments\n const isSelected = (pluginId: string): boolean => {\n return pluginId === currentPluginId;\n };\n\n // ---- Stage 2: Editor Access ----\n if (stage === 'editor') {\n return (\n <div className=\"flex flex-col gap-2\">\n {/* Back button + instrument name header */}\n <div className=\"flex items-center gap-2\">\n <button\n onClick={() => onBackToInstruments?.()}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors\"\n >\n &larr; Back\n </button>\n <span className=\"text-xs text-sas-muted font-medium truncate flex-1\">\n {selectedInstrumentName ?? 'Plugin'}\n </span>\n </div>\n\n {/* Open Editor button */}\n <button\n onClick={() => onShowEditor?.()}\n className=\"w-full py-2 text-xs font-medium rounded-sm border border-sas-accent bg-sas-accent/20 text-sas-accent hover:bg-sas-accent/40 transition-colors\"\n >\n Open Plugin Editor\n </button>\n </div>\n );\n }\n\n // ---- Stage 1: Instrument List (default) ----\n return (\n <div className=\"flex flex-col gap-2\">\n {/* Search + Refresh row */}\n <div className=\"flex items-center gap-2\">\n <input\n type=\"text\"\n value={search}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}\n placeholder=\"Search instruments...\"\n className=\"sas-input flex-1 px-2 py-1 text-xs\"\n />\n <button\n onClick={onRefresh}\n disabled={isLoading}\n className=\"px-2 py-1 text-xs rounded-sm border border-sas-border text-sas-muted hover:text-sas-accent hover:border-sas-accent transition-colors disabled:opacity-50\"\n title=\"Re-scan plugins\"\n >\n {isLoading ? '...' : 'Refresh'}\n </button>\n </div>\n\n {/* Instrument grid */}\n {isLoading && instruments.length === 0 ? (\n <div className=\"text-xs text-sas-muted/60 text-center py-3\">\n Scanning plugins...\n </div>\n ) : (\n <div className=\"grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto\">\n {/* Permanent \"Surge XT (Default)\" entry — always available */}\n <button\n key=\"__surge-xt-default__\"\n onClick={() => onSelect(SURGE_XT_DEFAULT_ID)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n isDefaultSelected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title=\"Surge XT — Default instrument\"\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {isDefaultSelected && '\\u2713 '}Surge XT\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">\n Default\n </span>\n </button>\n {/* Scanned instruments */}\n {filtered.map((inst: InstrumentDescriptor) => {\n const selected = isSelected(inst.pluginId);\n return (\n <button\n key={inst.pluginId}\n onClick={() => onSelect(inst.pluginId)}\n className={`flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors ${\n selected\n ? 'border-sas-accent bg-sas-accent/20 text-sas-accent'\n : inst.missing\n ? 'border-amber-500/50 bg-amber-500/10 text-amber-400 hover:border-amber-500'\n : 'border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent'\n }`}\n title={`${inst.name} by ${inst.manufacturer} (${inst.type.toUpperCase()})${inst.missing ? ' — MISSING' : ''}`}\n >\n <span className=\"text-xs font-medium truncate w-full\">\n {selected && '\\u2713 '}{inst.name}\n </span>\n <span className=\"text-[9px] text-sas-muted/50 truncate w-full\">\n {inst.manufacturer || inst.type.toUpperCase()}\n </span>\n </button>\n );\n })}\n {filtered.length === 0 && (\n <div className=\"col-span-2 text-xs text-sas-muted/60 text-center py-2\">\n {search.trim() ? 'No matches' : 'No other plugins found'}\n </div>\n )}\n </div>\n )}\n </div>\n );\n}\n\nexport default InstrumentDrawer;\n","/**\n * VolumeSlider Component\n *\n * Compact horizontal volume slider for track volume control.\n * Uses native HTML range input with custom styling.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\nimport { sliderToDb } from '../utils/volume-conversion';\n\ninterface VolumeSliderProps {\n /** Volume value from 0 to 1 */\n value: number;\n /** Called when volume changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Format slider value as dB for tooltip display\n */\nfunction formatDb(value: number): string {\n const db = sliderToDb(value);\n if (db <= -60) return '-∞ dB';\n const sign = db >= 0 ? '+' : '';\n return `${sign}${db.toFixed(1)} dB`;\n}\n\n/**\n * Debounce helper for volume changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n // Update callback ref when callback changes\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n // Cleanup on unmount\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const VolumeSlider: React.FC<VolumeSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Volume: ${formatDb(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"0\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default VolumeSlider;\n","/**\n * Volume Conversion Utilities\n *\n * Converts between UI slider position (0-1) and engine dB values using a power\n * curve with +6 dB headroom. The curve places unity gain (0 dB) at slider 0.75,\n * giving the top 25% of the slider a meaningful 6 dB boost range instead of the\n * previous perceptual dead zone.\n *\n * Mapping:\n * slider 0.00 → -60 dB (silence)\n * slider 0.75 → 0 dB (unity gain)\n * slider 1.00 → +6 dB (max boost)\n */\n\n/** Slider position that maps to 0 dB (unity gain) */\nexport const SLIDER_UNITY = 0.75;\n\n/** Maximum dB value at slider = 1.0 */\nexport const DB_MAX = 6;\n\n/** Minimum dB value (silence floor) */\nexport const DB_MIN = -60;\n\n/**\n * Exponent derived so that slider=1.0 yields exactly DB_MAX dB.\n *\n * gain_at_1 = (1 / SLIDER_UNITY) ^ EXPONENT = 10^(DB_MAX/20)\n * EXPONENT = log(10^(DB_MAX/20)) / log(1/SLIDER_UNITY)\n */\nconst EXPONENT: number =\n Math.log(Math.pow(10, DB_MAX / 20)) / Math.log(1 / SLIDER_UNITY);\n\n/**\n * Convert a UI slider position (0-1) to engine dB.\n *\n * @param slider - Slider value in [0, 1]\n * @returns dB value in [DB_MIN, DB_MAX]\n */\nexport function sliderToDb(slider: number): number {\n if (slider <= 0) return DB_MIN;\n const gain = Math.pow(slider / SLIDER_UNITY, EXPONENT);\n const db = 20 * Math.log10(gain);\n return Math.max(DB_MIN, Math.min(DB_MAX, db));\n}\n\n/**\n * Convert an engine dB value back to a UI slider position (0-1).\n * Inverse of sliderToDb().\n *\n * @param db - Volume in dB\n * @returns Slider value in [0, 1]\n */\nexport function dbToSlider(db: number): number {\n if (db <= DB_MIN) return 0;\n if (db >= DB_MAX) return 1;\n const gain = Math.pow(10, db / 20);\n const slider = SLIDER_UNITY * Math.pow(gain, 1 / EXPONENT);\n return Math.min(1, Math.max(0, slider));\n}\n","/**\n * PanSlider Component\n *\n * Compact horizontal pan slider for track stereo positioning.\n * Range: -1 (left) to +1 (right), 0 = center.\n * No text label - tooltip only.\n */\n\nimport React, { useCallback, useState, useRef, useEffect } from 'react';\n\ninterface PanSliderProps {\n /** Pan value from -1 (left) to 1 (right), 0 = center */\n value: number;\n /** Called when pan changes (debounced) */\n onChange: (value: number) => void;\n /** Disable the slider */\n disabled?: boolean;\n /** Additional CSS classes */\n className?: string;\n}\n\n/**\n * Convert pan value (-1 to 1) to display string\n */\nfunction toPanDisplay(value: number): string {\n if (Math.abs(value) < 0.02) {\n return 'Center';\n }\n const percent = Math.abs(Math.round(value * 100));\n return value < 0 ? `L${percent}` : `R${percent}`;\n}\n\n/**\n * Debounce helper for pan changes\n */\nfunction useDebouncedCallback<T extends (...args: never[]) => void>(\n callback: T,\n delay: number\n): T {\n const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n const callbackRef = useRef(callback);\n\n useEffect(() => {\n callbackRef.current = callback;\n }, [callback]);\n\n const debouncedCallback = useCallback(\n (...args: Parameters<T>) => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n timeoutRef.current = setTimeout(() => {\n callbackRef.current(...args);\n }, delay);\n },\n [delay]\n ) as T;\n\n useEffect(() => {\n return () => {\n if (timeoutRef.current) {\n clearTimeout(timeoutRef.current);\n }\n };\n }, []);\n\n return debouncedCallback;\n}\n\nexport const PanSlider: React.FC<PanSliderProps> = ({\n value,\n onChange,\n disabled = false,\n className = '',\n}) => {\n // Local state for immediate visual feedback\n const [localValue, setLocalValue] = useState(value);\n const [isDragging, setIsDragging] = useState(false);\n\n // Sync local value with prop when not dragging\n useEffect(() => {\n if (!isDragging) {\n setLocalValue(value);\n }\n }, [value, isDragging]);\n\n // Debounced onChange to prevent IPC spam\n const debouncedOnChange = useDebouncedCallback(onChange, 50);\n\n const handleChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = parseFloat(e.target.value);\n setLocalValue(newValue);\n debouncedOnChange(newValue);\n },\n [debouncedOnChange]\n );\n\n const handleMouseDown = useCallback(() => {\n setIsDragging(true);\n }, []);\n\n const handleMouseUp = useCallback(() => {\n setIsDragging(false);\n // Send final value immediately on release\n onChange(localValue);\n }, [localValue, onChange]);\n\n // Double-click to reset to center\n const handleDoubleClick = useCallback(() => {\n setLocalValue(0);\n onChange(0);\n }, [onChange]);\n\n return (\n <div\n className={`flex items-center ${className}`}\n title={`Pan: ${toPanDisplay(localValue)}`}\n >\n <input\n type=\"range\"\n min=\"-1\"\n max=\"1\"\n step=\"0.01\"\n value={localValue}\n onChange={handleChange}\n onMouseDown={handleMouseDown}\n onMouseUp={handleMouseUp}\n onTouchStart={handleMouseDown}\n onTouchEnd={handleMouseUp}\n onDoubleClick={handleDoubleClick}\n disabled={disabled}\n className={`\n w-full h-1.5 rounded-full appearance-none cursor-pointer\n bg-gray-700\n disabled:opacity-50 disabled:cursor-not-allowed\n [&::-webkit-slider-thumb]:appearance-none\n [&::-webkit-slider-thumb]:w-3\n [&::-webkit-slider-thumb]:h-3\n [&::-webkit-slider-thumb]:rounded-full\n [&::-webkit-slider-thumb]:bg-sas-accent\n [&::-webkit-slider-thumb]:cursor-pointer\n [&::-webkit-slider-thumb]:transition-transform\n [&::-webkit-slider-thumb]:hover:scale-110\n [&::-moz-range-thumb]:w-3\n [&::-moz-range-thumb]:h-3\n [&::-moz-range-thumb]:rounded-full\n [&::-moz-range-thumb]:bg-sas-accent\n [&::-moz-range-thumb]:border-0\n [&::-moz-range-thumb]:cursor-pointer\n `}\n />\n </div>\n );\n};\n\nexport default PanSlider;\n","/**\n * FX Preset Definitions\n *\n * 5 presets per FX category (30 total).\n *\n * Parameter names must match the Tracktion Engine's AutomatableParameter names\n * for each built-in plugin exactly (case-sensitive).\n *\n * Chorus & Phaser have ZERO automatable parameters — all values are set via\n * XML state (CachedValues on the plugin ValueTree).\n *\n * Lives in shared/ so both main process (services) and renderer (UI) can import.\n */\n\nimport type { FxCategory, FxPresetConfig } from '../types/fx-toggle.types';\n\n// ============================================================================\n// EQ (4-Band Equaliser)\n// ============================================================================\n\nconst EQ_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'The Smiley',\n shortLabel: 'SM',\n params: {\n 'Low-shelf freq': 80, 'Low-shelf gain': 4, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 500, 'Mid gain 1': -3, 'Mid Q 1': 0.7,\n 'Mid freq 2': 2000, 'Mid gain 2': -2, 'Mid Q 2': 0.7,\n 'High-shelf freq': 12000, 'High-shelf gain': 4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Telephone',\n shortLabel: 'TP',\n params: {\n 'Low-shelf freq': 400, 'Low-shelf gain': -20, 'Low-shelf Q': 1.0,\n 'Mid freq 1': 1000, 'Mid gain 1': 5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 3000, 'Mid gain 2': -5, 'Mid Q 2': 1.0,\n 'High-shelf freq': 5000, 'High-shelf gain': -20, 'High-shelf Q': 1.0,\n },\n },\n {\n name: 'Warmth',\n shortLabel: 'WM',\n params: {\n 'Low-shelf freq': 120, 'Low-shelf gain': 3, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 400, 'Mid gain 1': 2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 4000, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 10000, 'High-shelf gain': -4, 'High-shelf Q': 0.5,\n },\n },\n {\n name: 'Vocal Air',\n shortLabel: 'VA',\n params: {\n 'Low-shelf freq': 100, 'Low-shelf gain': -6, 'Low-shelf Q': 0.7,\n 'Mid freq 1': 300, 'Mid gain 1': -2, 'Mid Q 1': 1.0,\n 'Mid freq 2': 1500, 'Mid gain 2': 0, 'Mid Q 2': 0.5,\n 'High-shelf freq': 14000, 'High-shelf gain': 6, 'High-shelf Q': 0.4,\n },\n },\n {\n name: 'De-Box',\n shortLabel: 'DB',\n params: {\n 'Low-shelf freq': 60, 'Low-shelf gain': 0, 'Low-shelf Q': 0.5,\n 'Mid freq 1': 350, 'Mid gain 1': -5, 'Mid Q 1': 2.0,\n 'Mid freq 2': 800, 'Mid gain 2': -3, 'Mid Q 2': 2.0,\n 'High-shelf freq': 10000, 'High-shelf gain': 0, 'High-shelf Q': 0.5,\n },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'gain-scale',\n};\n\n// ============================================================================\n// Compressor\n// ============================================================================\n\nconst COMPRESSOR_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Leveler',\n shortLabel: 'VL',\n params: { 'Threshold': 0.251, 'Ratio': 0.5, 'Attack': 20.0, 'Release': 200.0, 'Output': 2.0 },\n },\n {\n name: 'Drum Smash',\n shortLabel: 'DS',\n params: { 'Threshold': 0.100, 'Ratio': 0.1, 'Attack': 0.5, 'Release': 100.0, 'Output': 8.0 },\n },\n {\n name: 'Bus Glue',\n shortLabel: 'BG',\n params: { 'Threshold': 0.316, 'Ratio': 0.666, 'Attack': 80.0, 'Release': 150.0, 'Output': 1.0 },\n },\n {\n name: 'Bass Anchor',\n shortLabel: 'BA',\n params: { 'Threshold': 0.177, 'Ratio': 0.25, 'Attack': 10.0, 'Release': 250.0, 'Output': 4.0 },\n },\n {\n name: 'Safety Net',\n shortLabel: 'SN',\n params: { 'Threshold': 0.891, 'Ratio': 0.0, 'Attack': 0.3, 'Release': 50.0, 'Output': 0.0 },\n },\n ],\n mixParamName: null,\n mixInterpolation: 'ratio-scale',\n};\n\n// ============================================================================\n// Chorus\n// ============================================================================\n\nconst CHORUS_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Dimension',\n shortLabel: 'DM',\n params: {},\n xmlStateParams: { depthMs: 1.5, speedHz: 0.5, width: 1.0, mixProportion: 0.5 },\n },\n {\n name: '80s Crystal',\n shortLabel: '80',\n params: {},\n xmlStateParams: { depthMs: 4.0, speedHz: 2.5, width: 0.8, mixProportion: 0.4 },\n },\n {\n name: 'Sea Sick',\n shortLabel: 'SS',\n params: {},\n xmlStateParams: { depthMs: 7.0, speedHz: 0.8, width: 0.3, mixProportion: 1.0 },\n },\n {\n name: 'Pseudo-Leslie',\n shortLabel: 'PL',\n params: {},\n xmlStateParams: { depthMs: 2.0, speedHz: 6.0, width: 0.9, mixProportion: 0.7 },\n },\n {\n name: 'Thickener',\n shortLabel: 'TK',\n params: {},\n xmlStateParams: { depthMs: 1.0, speedHz: 0.2, width: 1.0, mixProportion: 0.3 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'mixProportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Phaser\n// ============================================================================\n\nconst PHASER_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Slow Burn',\n shortLabel: 'SB',\n params: {},\n xmlStateParams: { depth: 6.0, rate: 0.1, feedback: 0.3 },\n },\n {\n name: 'Funky Quack',\n shortLabel: 'FQ',\n params: {},\n xmlStateParams: { depth: 3.0, rate: 2.0, feedback: 0.8 },\n },\n {\n name: 'Jet Plane',\n shortLabel: 'JP',\n params: {},\n xmlStateParams: { depth: 8.0, rate: 0.2, feedback: 0.9 },\n },\n {\n name: 'Underwater',\n shortLabel: 'UW',\n params: {},\n xmlStateParams: { depth: 1.5, rate: 4.0, feedback: 0.1 },\n },\n {\n name: 'Static Notch',\n shortLabel: 'ST',\n params: {},\n xmlStateParams: { depth: 2.0, rate: 0.05, feedback: 0.6 },\n },\n ],\n mixParamName: null,\n mixXmlAttr: 'depth',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Delay\n// ============================================================================\n\nconst DELAY_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Vocal Slap',\n shortLabel: 'VS',\n fixedLengthMs: 110,\n params: { 'Feedback': -20.0, 'Mix proportion': 0.25 },\n },\n {\n name: 'Grand Canyon',\n shortLabel: 'GC',\n noteMultiplier: 1.0,\n params: { 'Feedback': -4.0, 'Mix proportion': 0.45 },\n },\n {\n name: 'Wide Doubler',\n shortLabel: 'WD',\n fixedLengthMs: 25,\n params: { 'Feedback': -30.0, 'Mix proportion': 0.5 },\n },\n {\n name: 'Dub Echo',\n shortLabel: 'DE',\n noteMultiplier: 0.6,\n params: { 'Feedback': -1.5, 'Mix proportion': 0.4 },\n },\n {\n name: 'Rhythmic Wash',\n shortLabel: 'RW',\n noteMultiplier: 0.75,\n params: { 'Feedback': -8.0, 'Mix proportion': 0.2 },\n },\n ],\n mixParamName: 'Mix proportion',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Reverb\n// ============================================================================\n\nconst REVERB_PRESETS: FxPresetConfig = {\n presets: [\n {\n name: 'Drum Room',\n shortLabel: 'DR',\n params: { 'Room Size': 0.2, 'Damping': 0.2, 'Wet Level': 0.15, 'Dry Level': 0.5, 'Width': 0.8 },\n },\n {\n name: 'Vocal Hall',\n shortLabel: 'VH',\n params: { 'Room Size': 0.8, 'Damping': 0.6, 'Wet Level': 0.25, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n {\n name: 'Cathedral',\n shortLabel: 'CT',\n params: { 'Room Size': 1.0, 'Damping': 0.1, 'Wet Level': 0.333, 'Dry Level': 0.2, 'Width': 1.0 },\n },\n {\n name: 'Tile Bathroom',\n shortLabel: 'TB',\n params: { 'Room Size': 0.15, 'Damping': 0.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 0.5 },\n },\n {\n name: 'Vintage Plate',\n shortLabel: 'VP',\n params: { 'Room Size': 0.4, 'Damping': 1.0, 'Wet Level': 0.2, 'Dry Level': 0.5, 'Width': 1.0 },\n },\n ],\n mixParamName: 'Wet Level',\n mixInterpolation: 'direct',\n};\n\n// ============================================================================\n// Export\n// ============================================================================\n\n/** All preset configs keyed by FX category */\nexport const FX_PRESET_CONFIGS: Record<FxCategory, FxPresetConfig> = {\n eq: EQ_PRESETS,\n compressor: COMPRESSOR_PRESETS,\n chorus: CHORUS_PRESETS,\n phaser: PHASER_PRESETS,\n delay: DELAY_PRESETS,\n reverb: REVERB_PRESETS,\n};\n","/**\n * FxToggleBar Component\n *\n * Per-track FX control panel with 6 rows (one per FX category).\n * Each row: [Category toggle] [Preset 1-5 buttons] [Dry/Wet slider]\n *\n * Signal chain order: EQ -> Compressor -> Chorus -> Phaser -> Delay -> Reverb\n */\n\nimport React from 'react';\nimport type { FxCategory, TrackFxDetailState, FxCategoryDetailState } from '../types/fx-toggle.types';\nimport { FX_CATEGORIES, FX_DISPLAY_LABELS } from '../types/fx-toggle.types';\nimport { FX_PRESET_CONFIGS } from '../constants/fx-presets';\n\n/** Per-category active colors */\nconst FX_COLORS: Record<FxCategory, string> = {\n eq: 'bg-blue-500',\n compressor: 'bg-orange-500',\n chorus: 'bg-teal-500',\n phaser: 'bg-purple-500',\n delay: 'bg-green-500',\n reverb: 'bg-cyan-500',\n};\n\nexport interface FxToggleBarProps {\n trackId: string;\n fxState: TrackFxDetailState;\n onToggle: (trackId: string, category: FxCategory, enabled: boolean) => void;\n onPresetChange: (trackId: string, category: FxCategory, presetIndex: number) => void;\n onDryWetChange: (trackId: string, category: FxCategory, value: number) => void;\n disabled?: boolean;\n}\n\nexport const FxToggleBar: React.FC<FxToggleBarProps> = ({\n trackId,\n fxState,\n onToggle,\n onPresetChange,\n onDryWetChange,\n disabled = false,\n}) => {\n return (\n <div className=\"flex flex-col gap-1\" data-testid=\"fx-toggle-bar\">\n {FX_CATEGORIES.map((category: FxCategory) => {\n const detail: FxCategoryDetailState = fxState[category];\n const isActive = detail.enabled;\n const label = FX_DISPLAY_LABELS[category];\n const activeColor = FX_COLORS[category];\n const config = FX_PRESET_CONFIGS[category];\n\n return (\n <div key={category} className=\"flex items-center gap-0.5\">\n {/* Category toggle button */}\n <button\n data-testid={`fx-toggle-${category}`}\n disabled={disabled}\n onClick={() => onToggle(trackId, category, !isActive)}\n className={`w-14 py-0.5 text-[10px] font-semibold rounded-sm transition-colors leading-none flex-shrink-0 text-center ${\n disabled\n ? 'bg-sas-panel text-sas-muted/30 cursor-not-allowed'\n : isActive\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/60 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={`${isActive ? 'Disable' : 'Enable'} ${category.toUpperCase()}`}\n >\n {label}\n </button>\n\n {/* Preset buttons 1-5 */}\n {config.presets.map((preset, idx: number) => (\n <button\n key={idx}\n data-testid={`fx-preset-${category}-${idx}`}\n disabled={disabled || !isActive}\n onClick={() => onPresetChange(trackId, category, idx)}\n className={`w-5 h-5 text-[9px] font-medium rounded-sm transition-colors leading-none flex-shrink-0 ${\n disabled || !isActive\n ? 'bg-sas-panel text-sas-muted/20 cursor-not-allowed'\n : detail.presetIndex === idx\n ? `${activeColor} text-white`\n : 'bg-sas-panel-alt text-sas-muted/50 hover:bg-sas-border hover:text-sas-muted'\n }`}\n title={preset.name}\n >\n {idx + 1}\n </button>\n ))}\n\n {/* Dry/Wet slider */}\n <input\n type=\"range\"\n data-testid={`fx-drywet-${category}`}\n min=\"0\"\n max=\"100\"\n value={Math.round(detail.dryWet * 100)}\n disabled={disabled || !isActive}\n onChange={(e: React.ChangeEvent<HTMLInputElement>) =>\n onDryWetChange(trackId, category, Number(e.target.value) / 100)\n }\n className=\"flex-1 min-w-[30px] h-3 accent-sas-accent disabled:opacity-30 cursor-pointer disabled:cursor-not-allowed\"\n title={`Dry/Wet: ${Math.round(detail.dryWet * 100)}%`}\n />\n <span className=\"text-[8px] text-sas-muted/50 w-6 text-right flex-shrink-0\">\n {Math.round(detail.dryWet * 100)}%\n </span>\n </div>\n );\n })}\n </div>\n );\n};\n","/**\n * SorceryProgressBar Component\n *\n * A progress bar for long, uncertain wait times (10-30s). Supports two modes:\n *\n * 1. **Time-based mode** (when `estimatedDurationMs` is provided):\n * Uses elapsed time and an ease-out curve to pace progress realistically.\n * Reaches ~90% at the estimated completion time, then asymptotically\n * approaches 95% if the operation runs long.\n *\n * 2. **Phase-based mode** (legacy fallback, no `estimatedDurationMs`):\n * \"Zeno's Paradox\" style - progress moves quickly at first, then\n * asymptotically slows toward 95%.\n *\n * Visual style: Segmented \"retro CLI\" look with glowing teal accent,\n * diagonal stripes, and subtle pulse animation.\n */\n\nimport React, { useState, useEffect, useRef } from 'react';\n\n/**\n * Props for SorceryProgressBar component\n */\ninterface SorceryProgressBarProps {\n /** Whether loading is in progress */\n isLoading: boolean;\n /** Text shown during loading (default: \"CONJURING...\") */\n statusText?: string;\n /** Text shown on completion (default: \"COMPLETE\") */\n completeText?: string;\n /** Callback when loading completes */\n onComplete?: () => void;\n /** Height class override (default: \"h-10\") */\n heightClass?: string;\n /** Initial progress value (0-100) to resume from - persists across scene switches */\n initialProgress?: number;\n /** Callback when progress changes - use to persist progress in parent state */\n onProgressChange?: (progress: number) => void;\n /** Estimated total duration in ms - enables time-aware pacing */\n estimatedDurationMs?: number;\n}\n\n/**\n * Calculates target progress based on elapsed time and estimated duration.\n * Uses an ease-out power curve for natural-feeling progress:\n * - At 10% of estimated time: ~21% (feels responsive early)\n * - At 30% of estimated time: ~53% (good midpoint feel)\n * - At 50% of estimated time: ~74% (past halfway visually)\n * - At 80% of estimated time: ~88% (approaching completion)\n * - At 100% of estimated time: 90% (leaves room for overshoot)\n * - Beyond estimate: asymptotically approaches 95%\n */\nexport function calculateTimeBasedTarget(elapsedMs: number, estimatedDurationMs: number): number {\n const t = elapsedMs / estimatedDurationMs;\n if (t <= 0) return 0;\n\n if (t <= 1.0) {\n // Ease-out power curve reaching 90% at t=1.0\n return 90 * (1 - Math.pow(1 - t, 2.5));\n }\n\n // Beyond estimate: asymptotically approach 95%\n const overshootRatio = (elapsedMs - estimatedDurationMs) / estimatedDurationMs;\n return 90 + 5 * (1 - Math.exp(-overshootRatio * 3));\n}\n\n/**\n * Calculates the next progress value using \"Zeno's Paradox\" algorithm (legacy fallback).\n * - Phase 1 (0-20%): Rapid progress (5-15% per tick)\n * - Phase 2 (20-60%): Steady progress (2-7% per tick)\n * - Phase 3 (60-95%): Asymptotic slowdown\n * - Caps at 95% until actual completion\n */\nfunction calculateNextProgress(currentProgress: number): number {\n if (currentProgress < 20) {\n return currentProgress + Math.random() * 10 + 5;\n }\n if (currentProgress < 60) {\n return currentProgress + Math.random() * 5 + 2;\n }\n if (currentProgress < 95) {\n const remaining = 95 - currentProgress;\n const increment = remaining * (Math.random() * 0.2 + 0.1);\n return currentProgress + Math.max(increment, 0.1);\n }\n return 95;\n}\n\n/**\n * Calculates the next tick interval for phase-based mode (legacy fallback).\n */\nfunction calculateNextTickInterval(progress: number): number {\n if (progress < 30) {\n return Math.random() * 200 + 150; // 150-350ms\n }\n if (progress < 70) {\n return Math.random() * 300 + 200; // 200-500ms\n }\n return Math.random() * 600 + 400; // 400-1000ms\n}\n\n/** Tick interval for time-based mode (ms) */\nconst TIME_BASED_TICK_MIN = 200;\nconst TIME_BASED_TICK_RANGE = 100;\n\n/**\n * SorceryProgressBar - A mystical progress bar for uncertain wait times\n */\nexport function SorceryProgressBar({\n isLoading,\n statusText = 'CONJURING...',\n completeText = 'COMPLETE',\n onComplete,\n heightClass = 'h-10',\n initialProgress = 0,\n onProgressChange,\n estimatedDurationMs,\n}: SorceryProgressBarProps): React.ReactElement | null {\n const [progress, setProgress] = useState<number>(initialProgress);\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Initialize to false so first render with isLoading=true triggers animation start\n const isLoadingRef = useRef<boolean>(false);\n const hasStartedRef = useRef<boolean>(false);\n const startTimeRef = useRef<number>(0);\n\n // Store callbacks in refs to avoid dependency issues\n const onProgressChangeRef = useRef(onProgressChange);\n const onCompleteRef = useRef(onComplete);\n onProgressChangeRef.current = onProgressChange;\n onCompleteRef.current = onComplete;\n\n // Store props in refs - only used when loading starts, not as dependencies\n const initialProgressRef = useRef(initialProgress);\n initialProgressRef.current = initialProgress;\n const estimatedDurationMsRef = useRef(estimatedDurationMs);\n estimatedDurationMsRef.current = estimatedDurationMs;\n\n // Effect to handle loading state changes - ONLY depends on isLoading\n useEffect(() => {\n const wasLoading = isLoadingRef.current;\n isLoadingRef.current = isLoading;\n\n if (isLoading && !wasLoading) {\n // Loading just started\n hasStartedRef.current = true;\n startTimeRef.current = Date.now();\n\n // Start fresh or resume from initial progress (read from ref)\n const startProgress = initialProgressRef.current > 0 ? initialProgressRef.current : 0;\n setProgress(startProgress);\n\n const duration = estimatedDurationMsRef.current;\n\n if (duration && duration > 0) {\n // Time-based mode: pace progress using elapsed time\n const tick = (): void => {\n setProgress((prev) => {\n const elapsed = Date.now() - startTimeRef.current;\n const target = calculateTimeBasedTarget(elapsed, duration);\n\n // Add subtle jitter for organic feel (±0.5%)\n const jitter = (Math.random() - 0.5) * 1.0;\n // Move toward target, ensure monotonically increasing, cap at 95%\n const next = Math.min(Math.max(target + jitter, prev + 0.05), 95);\n\n onProgressChangeRef.current?.(next);\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN + Math.random() * TIME_BASED_TICK_RANGE);\n return next;\n });\n };\n\n timerRef.current = setTimeout(tick, TIME_BASED_TICK_MIN);\n } else {\n // Phase-based mode (legacy fallback)\n const tick = (): void => {\n setProgress((prev) => {\n if (prev >= 95) {\n timerRef.current = setTimeout(tick, 1000);\n return 95;\n }\n\n const next = Math.min(calculateNextProgress(prev), 95);\n onProgressChangeRef.current?.(next);\n\n const interval = calculateNextTickInterval(next);\n timerRef.current = setTimeout(tick, interval);\n\n return next;\n });\n };\n\n const firstInterval = calculateNextTickInterval(startProgress);\n timerRef.current = setTimeout(tick, firstInterval);\n }\n } else if (!isLoading && wasLoading && hasStartedRef.current) {\n // Loading just finished - jump to 100%\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n setProgress(100);\n onProgressChangeRef.current?.(100);\n onCompleteRef.current?.();\n hasStartedRef.current = false;\n }\n\n // Cleanup on unmount only\n return () => {\n if (timerRef.current) {\n clearTimeout(timerRef.current);\n timerRef.current = null;\n }\n };\n // ONLY depend on isLoading - other props are read from refs\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isLoading]);\n\n // Don't render if not loading and progress is 0\n if (!isLoading && progress === 0) {\n return null;\n }\n\n const displayProgress = Math.floor(progress);\n const isComplete = !isLoading && progress === 100;\n\n // Calculate transition duration based on progress phase\n const transitionDuration = progress < 50 ? '300ms' : progress < 80 ? '500ms' : '700ms';\n\n return (\n <div\n className={`relative w-full ${heightClass} bg-sas-panel-alt border border-sas-border rounded-sm overflow-hidden shadow-inner`}\n >\n {/* Progress fill with stripes and glow */}\n <div\n className={`\n h-full\n bg-gradient-to-r from-sas-accent/70 to-sas-accent\n shadow-glow-soft\n sorcery-progress-fill\n animate-progress-stripes\n ${progress > 70 ? 'animate-progress-pulse' : ''}\n transition-all ease-out\n `}\n style={{\n width: `${progress}%`,\n transitionDuration,\n }}\n />\n\n {/* Text overlay */}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n {isLoading && progress < 100 ? (\n <span className=\"font-mono text-xs text-sas-accent font-bold drop-shadow-md tracking-wider\">\n {statusText} {displayProgress}%\n </span>\n ) : isComplete ? (\n <span className=\"font-mono text-xs text-sas-text font-bold drop-shadow-md tracking-wider\">\n {completeText}\n </span>\n ) : null}\n </div>\n\n {/* Scanline overlay for retro CRT effect */}\n <div\n className=\"absolute inset-0 pointer-events-none opacity-10\"\n style={{\n backgroundImage: `repeating-linear-gradient(\n to bottom,\n transparent,\n transparent 2px,\n rgba(0, 0, 0, 0.3) 2px,\n rgba(0, 0, 0, 0.3) 4px\n )`,\n }}\n />\n </div>\n );\n}\n\nexport default SorceryProgressBar;\n","/**\n * useSceneState — Scene-keyed state hook for plugin developers.\n *\n * Works like `useState`, but maintains separate state per scene.\n * When the user switches scenes, the previous scene's state is preserved\n * and restored when they switch back.\n *\n * Returns `[value, setForCurrentScene, setForScene]`:\n * - `value` — state for the currently active scene\n * - `setForCurrentScene(v)` — updates state for whatever scene is active at call time\n * - `setForScene(sceneId, v)` — updates state for a specific scene (for async callbacks)\n *\n * Both setters support the functional updater pattern: `prev => next`.\n *\n * **Important:** For object/array `initialValue`, hoist to a module-level constant\n * to keep the setter callbacks referentially stable:\n * ```ts\n * const EMPTY: string[] = [];\n * const [items, setItems, setItemsForScene] = useSceneState(activeSceneId, EMPTY);\n * ```\n */\n\nimport { useState, useCallback, useRef } from 'react';\n\ntype SetSceneState<T> = (value: T | ((prev: T) => T)) => void;\ntype SetSceneStateForScene<T> = (sceneId: string, value: T | ((prev: T) => T)) => void;\n\nexport function useSceneState<T>(\n activeSceneId: string | null,\n initialValue: T\n): [T, SetSceneState<T>, SetSceneStateForScene<T>] {\n const [stateMap, setStateMap] = useState<Map<string, T>>(() => new Map());\n const activeSceneIdRef = useRef(activeSceneId);\n activeSceneIdRef.current = activeSceneId;\n\n const currentValue = activeSceneId !== null && stateMap.has(activeSceneId)\n ? stateMap.get(activeSceneId)!\n : initialValue;\n\n const setForCurrentScene = useCallback((value: T | ((prev: T) => T)): void => {\n const sid = activeSceneIdRef.current;\n if (sid === null) return;\n setStateMap(prev => {\n const current = prev.has(sid) ? prev.get(sid)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sid, next);\n return newMap;\n });\n }, [initialValue]);\n\n const setForScene = useCallback((sceneId: string, value: T | ((prev: T) => T)): void => {\n setStateMap(prev => {\n const current = prev.has(sceneId) ? prev.get(sceneId)! : initialValue;\n const next = typeof value === 'function' ? (value as (prev: T) => T)(current) : value;\n const newMap = new Map(prev);\n newMap.set(sceneId, next);\n return newMap;\n });\n }, [initialValue]);\n\n return [currentValue, setForCurrentScene, setForScene];\n}\n","/**\n * Plugin SDK Version\n *\n * Semver version of the plugin SDK contract.\n * Plugins declare minSdkVersion in their manifest.\n * Registry checks semver.gte(PLUGIN_SDK_VERSION, manifest.minHostVersion)\n * during activation and marks incompatible plugins accordingly.\n */\nexport const PLUGIN_SDK_VERSION = '2.0.0';\n"],"mappings":"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","names":["import_react","import_jsx_runtime","import_react","import_jsx_runtime","useDebouncedCallback","import_jsx_runtime","import_react","import_jsx_runtime","import_jsx_runtime","import_react"]}