@signalsandsorcery/plugin-sdk 2.34.1 → 2.35.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.mjs CHANGED
@@ -4126,9 +4126,276 @@ function TransitionDesigner({
4126
4126
  ] });
4127
4127
  }
4128
4128
 
4129
- // src/components/DownloadPackButton.tsx
4130
- import { useCallback as useCallback9, useEffect as useEffect10, useState as useState12 } from "react";
4129
+ // src/components/PanelMasterStrip.tsx
4130
+ import { useMemo as useMemo6, useState as useState12 } from "react";
4131
4131
  import { jsx as jsx18, jsxs as jsxs14 } from "react/jsx-runtime";
4132
+ function PanelMasterStrip({
4133
+ bus,
4134
+ availableFx = [],
4135
+ fxLoading = false,
4136
+ soloedOut = false,
4137
+ disabled = false,
4138
+ fxPickerOpen,
4139
+ onToggleFxPicker,
4140
+ onRefreshFx,
4141
+ onVolumeChange,
4142
+ onMuteToggle,
4143
+ onSoloToggle,
4144
+ onAddFx,
4145
+ onRemoveFx,
4146
+ onToggleFxEnabled,
4147
+ onShowFxEditor
4148
+ }) {
4149
+ const [search, setSearch] = useState12("");
4150
+ const filtered = useMemo6(() => {
4151
+ const q = search.trim().toLowerCase();
4152
+ if (!q) return availableFx;
4153
+ return availableFx.filter(
4154
+ (fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
4155
+ );
4156
+ }, [availableFx, search]);
4157
+ return /* @__PURE__ */ jsxs14(
4158
+ "div",
4159
+ {
4160
+ "data-testid": "panel-master-strip",
4161
+ className: `flex flex-col gap-1 px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt/50 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
4162
+ children: [
4163
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
4164
+ /* @__PURE__ */ jsx18(
4165
+ "span",
4166
+ {
4167
+ className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
4168
+ title: "Panel mix bus \u2014 volume, mute/solo and FX applied to this panel's summed output",
4169
+ children: "BUS"
4170
+ }
4171
+ ),
4172
+ /* @__PURE__ */ jsx18("div", { className: "w-24", children: /* @__PURE__ */ jsx18(
4173
+ VolumeSlider,
4174
+ {
4175
+ value: dbToSlider(bus.volume),
4176
+ onChange: (sliderValue) => onVolumeChange(sliderToDb(sliderValue)),
4177
+ disabled
4178
+ }
4179
+ ) }),
4180
+ /* @__PURE__ */ jsx18(
4181
+ "button",
4182
+ {
4183
+ "data-testid": "bus-mute-button",
4184
+ onClick: onMuteToggle,
4185
+ disabled,
4186
+ className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${bus.muted ? "bg-red-600 text-white" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"} disabled:opacity-50`,
4187
+ title: bus.muted ? "Unmute panel bus" : "Mute panel bus (silences the whole panel)",
4188
+ children: "M"
4189
+ }
4190
+ ),
4191
+ /* @__PURE__ */ jsx18(
4192
+ "button",
4193
+ {
4194
+ "data-testid": "bus-solo-button",
4195
+ onClick: onSoloToggle,
4196
+ disabled,
4197
+ className: `px-1.5 py-0.5 text-xs font-bold rounded transition-colors ${bus.soloed ? "bg-amber-500 text-black" : "bg-sas-panel-alt text-sas-muted hover:bg-sas-border"} disabled:opacity-50`,
4198
+ title: bus.soloed ? "Unsolo panel bus" : "Solo this panel (silences other panels/tracks in scope)",
4199
+ children: "S"
4200
+ }
4201
+ ),
4202
+ /* @__PURE__ */ jsx18("div", { className: "flex items-center gap-1 flex-1 min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ jsxs14(
4203
+ "span",
4204
+ {
4205
+ "data-testid": `bus-fx-chip-${fx.index}`,
4206
+ className: `flex items-center gap-1 px-1.5 py-0.5 rounded-sm border text-[10px] whitespace-nowrap ${fx.enabled ? "border-sas-accent/60 text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted/50 bg-sas-panel"}`,
4207
+ title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
4208
+ children: [
4209
+ /* @__PURE__ */ jsx18(
4210
+ "button",
4211
+ {
4212
+ "data-testid": `bus-fx-toggle-${fx.index}`,
4213
+ onClick: () => onToggleFxEnabled(fx.index, !fx.enabled),
4214
+ disabled,
4215
+ className: "hover:opacity-70 disabled:opacity-50",
4216
+ title: fx.enabled ? `Bypass ${fx.name}` : `Enable ${fx.name}`,
4217
+ children: fx.enabled ? "\u25CF" : "\u25CB"
4218
+ }
4219
+ ),
4220
+ onShowFxEditor ? /* @__PURE__ */ jsx18(
4221
+ "button",
4222
+ {
4223
+ "data-testid": `bus-fx-edit-${fx.index}`,
4224
+ onClick: () => onShowFxEditor(fx.index),
4225
+ disabled,
4226
+ className: "max-w-[80px] truncate hover:underline disabled:opacity-50",
4227
+ title: `Open ${fx.name} editor`,
4228
+ children: fx.name
4229
+ }
4230
+ ) : /* @__PURE__ */ jsx18("span", { className: "max-w-[80px] truncate", children: fx.name }),
4231
+ /* @__PURE__ */ jsx18(
4232
+ "button",
4233
+ {
4234
+ "data-testid": `bus-fx-remove-${fx.index}`,
4235
+ onClick: () => onRemoveFx(fx.index),
4236
+ disabled,
4237
+ className: "text-sas-muted/60 hover:text-sas-danger disabled:opacity-50",
4238
+ title: `Remove ${fx.name} from the bus`,
4239
+ children: "\u2715"
4240
+ }
4241
+ )
4242
+ ]
4243
+ },
4244
+ `${fx.index}:${fx.pluginId}`
4245
+ )) }),
4246
+ /* @__PURE__ */ jsx18(
4247
+ "button",
4248
+ {
4249
+ "data-testid": "bus-fx-add-button",
4250
+ onClick: () => onToggleFxPicker(!fxPickerOpen),
4251
+ disabled,
4252
+ className: `px-1.5 py-0.5 rounded-sm border text-xs whitespace-nowrap transition-colors ${fxPickerOpen ? "border-sas-accent text-sas-accent bg-sas-accent/10" : "border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"} disabled:opacity-50`,
4253
+ title: "Add an FX plugin to the panel bus",
4254
+ children: "FX +"
4255
+ }
4256
+ )
4257
+ ] }),
4258
+ fxPickerOpen && /* @__PURE__ */ jsxs14("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
4259
+ /* @__PURE__ */ jsxs14("div", { className: "flex items-center gap-2", children: [
4260
+ /* @__PURE__ */ jsx18(
4261
+ "input",
4262
+ {
4263
+ type: "text",
4264
+ value: search,
4265
+ onChange: (e) => setSearch(e.target.value),
4266
+ placeholder: "Search FX...",
4267
+ className: "sas-input flex-1 px-2 py-1 text-xs"
4268
+ }
4269
+ ),
4270
+ onRefreshFx && /* @__PURE__ */ jsx18(
4271
+ "button",
4272
+ {
4273
+ onClick: () => onRefreshFx(),
4274
+ disabled: fxLoading,
4275
+ 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",
4276
+ title: "Re-scan plugins",
4277
+ children: fxLoading ? "..." : "Refresh"
4278
+ }
4279
+ )
4280
+ ] }),
4281
+ fxLoading && availableFx.length === 0 ? /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ jsxs14("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
4282
+ filtered.map((fx) => /* @__PURE__ */ jsxs14(
4283
+ "button",
4284
+ {
4285
+ "data-testid": `bus-fx-pick-${fx.pluginId}`,
4286
+ onClick: () => onAddFx(fx.pluginId),
4287
+ className: "flex flex-col items-start px-2 py-1.5 rounded-sm border text-left transition-colors border-sas-border bg-sas-panel-alt text-sas-muted hover:border-sas-accent hover:text-sas-accent",
4288
+ title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
4289
+ children: [
4290
+ /* @__PURE__ */ jsx18("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
4291
+ /* @__PURE__ */ jsx18("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
4292
+ ]
4293
+ },
4294
+ fx.pluginId
4295
+ )),
4296
+ filtered.length === 0 && /* @__PURE__ */ jsx18("div", { className: "col-span-3 text-xs text-sas-muted/60 text-center py-2", children: search.trim() ? "No matches" : "No FX plugins found" })
4297
+ ] })
4298
+ ] })
4299
+ ]
4300
+ }
4301
+ );
4302
+ }
4303
+
4304
+ // src/hooks/usePanelBus.ts
4305
+ import { useCallback as useCallback9, useEffect as useEffect10, useRef as useRef11, useState as useState13 } from "react";
4306
+ function usePanelBus(host, activeSceneId) {
4307
+ const supported = typeof host.getPanelBusState === "function";
4308
+ const [bus, setBus] = useState13(null);
4309
+ const [availableFx, setAvailableFx] = useState13([]);
4310
+ const [fxLoading, setFxLoading] = useState13(false);
4311
+ const [fxPickerOpen, setFxPickerOpen] = useState13(false);
4312
+ const fxLoadedRef = useRef11(false);
4313
+ const loadSeqRef = useRef11(0);
4314
+ const reload = useCallback9(async () => {
4315
+ if (!supported || !activeSceneId || !host.getPanelBusState) {
4316
+ setBus(null);
4317
+ return;
4318
+ }
4319
+ const seq = ++loadSeqRef.current;
4320
+ try {
4321
+ const state = await host.getPanelBusState(activeSceneId);
4322
+ if (loadSeqRef.current === seq) setBus(state);
4323
+ } catch {
4324
+ }
4325
+ }, [host, activeSceneId, supported]);
4326
+ useEffect10(() => {
4327
+ setBus(null);
4328
+ setFxPickerOpen(false);
4329
+ void reload();
4330
+ }, [reload]);
4331
+ const loadFxList = useCallback9(
4332
+ async (force) => {
4333
+ if (!supported || !host.getAvailableFx) return;
4334
+ if (fxLoadedRef.current && !force) return;
4335
+ setFxLoading(true);
4336
+ try {
4337
+ const list = await host.getAvailableFx();
4338
+ setAvailableFx(list);
4339
+ fxLoadedRef.current = true;
4340
+ } catch {
4341
+ } finally {
4342
+ setFxLoading(false);
4343
+ }
4344
+ },
4345
+ [host, supported]
4346
+ );
4347
+ const openPicker = useCallback9(
4348
+ (open) => {
4349
+ setFxPickerOpen(open);
4350
+ if (open) void loadFxList(false);
4351
+ },
4352
+ [loadFxList]
4353
+ );
4354
+ const mutate = useCallback9(
4355
+ (fn) => {
4356
+ if (!fn || !activeSceneId) return;
4357
+ void (async () => {
4358
+ try {
4359
+ await fn();
4360
+ } catch {
4361
+ }
4362
+ await reload();
4363
+ })();
4364
+ },
4365
+ [activeSceneId, reload]
4366
+ );
4367
+ return {
4368
+ supported,
4369
+ bus,
4370
+ availableFx,
4371
+ fxLoading,
4372
+ fxPickerOpen,
4373
+ setFxPickerOpen: openPicker,
4374
+ refreshFx: () => void loadFxList(true),
4375
+ reload,
4376
+ onVolumeChange: (volumeDb) => mutate(host.setPanelBusVolume && (() => host.setPanelBusVolume(activeSceneId, volumeDb))),
4377
+ onMuteToggle: () => mutate(
4378
+ host.setPanelBusMute && (() => host.setPanelBusMute(activeSceneId, !(bus?.muted ?? false)))
4379
+ ),
4380
+ onSoloToggle: () => mutate(
4381
+ host.setPanelBusSolo && (() => host.setPanelBusSolo(activeSceneId, !(bus?.soloed ?? false)))
4382
+ ),
4383
+ onAddFx: (pluginId) => mutate(host.loadPanelBusFx && (async () => {
4384
+ await host.loadPanelBusFx(activeSceneId, pluginId);
4385
+ })),
4386
+ onRemoveFx: (fxIndex) => mutate(host.removePanelBusFx && (() => host.removePanelBusFx(activeSceneId, fxIndex))),
4387
+ onToggleFxEnabled: (fxIndex, enabled) => mutate(
4388
+ host.setPanelBusFxEnabled && (() => host.setPanelBusFxEnabled(activeSceneId, fxIndex, enabled))
4389
+ ),
4390
+ onShowFxEditor: (fxIndex) => mutate(
4391
+ host.showPanelBusFxEditor && (() => host.showPanelBusFxEditor(activeSceneId, fxIndex))
4392
+ )
4393
+ };
4394
+ }
4395
+
4396
+ // src/components/DownloadPackButton.tsx
4397
+ import { useCallback as useCallback10, useEffect as useEffect11, useState as useState14 } from "react";
4398
+ import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
4132
4399
  function formatSize(bytes) {
4133
4400
  if (!bytes || bytes <= 0) return "";
4134
4401
  const gb = bytes / 1024 ** 3;
@@ -4144,10 +4411,10 @@ var DownloadPackButton = ({
4144
4411
  variant = "compact",
4145
4412
  onDownloadComplete
4146
4413
  }) => {
4147
- const [status, setStatus] = useState12("idle");
4148
- const [progress, setProgress] = useState12(0);
4149
- const [errorMessage, setErrorMessage] = useState12(null);
4150
- useEffect10(() => {
4414
+ const [status, setStatus] = useState14("idle");
4415
+ const [progress, setProgress] = useState14(0);
4416
+ const [errorMessage, setErrorMessage] = useState14(null);
4417
+ useEffect11(() => {
4151
4418
  const unsub = host.onSamplePackProgress(packId, (p) => {
4152
4419
  setStatus(p.status);
4153
4420
  setProgress(p.progress);
@@ -4162,7 +4429,7 @@ var DownloadPackButton = ({
4162
4429
  });
4163
4430
  return unsub;
4164
4431
  }, [host, packId, onDownloadComplete]);
4165
- const handleClick = useCallback9(async () => {
4432
+ const handleClick = useCallback10(async () => {
4166
4433
  if (status !== "idle" && status !== "error") return;
4167
4434
  try {
4168
4435
  setStatus("downloading");
@@ -4216,8 +4483,8 @@ var DownloadPackButton = ({
4216
4483
  } else {
4217
4484
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
4218
4485
  }
4219
- return /* @__PURE__ */ jsxs14("div", { children: [
4220
- /* @__PURE__ */ jsx18(
4486
+ return /* @__PURE__ */ jsxs15("div", { children: [
4487
+ /* @__PURE__ */ jsx19(
4221
4488
  "button",
4222
4489
  {
4223
4490
  "data-testid": `download-pack-button-${packId}`,
@@ -4228,12 +4495,12 @@ var DownloadPackButton = ({
4228
4495
  children: buttonLabel
4229
4496
  }
4230
4497
  ),
4231
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx18("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
4498
+ variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
4232
4499
  ] });
4233
4500
  };
4234
4501
 
4235
4502
  // src/components/SamplePackCTACard.tsx
4236
- import { jsx as jsx19, jsxs as jsxs15 } from "react/jsx-runtime";
4503
+ import { jsx as jsx20, jsxs as jsxs16 } from "react/jsx-runtime";
4237
4504
  var SamplePackCTACard = ({
4238
4505
  host,
4239
4506
  pack,
@@ -4241,7 +4508,7 @@ var SamplePackCTACard = ({
4241
4508
  onDownloadComplete
4242
4509
  }) => {
4243
4510
  if (status === "checking") {
4244
- return /* @__PURE__ */ jsx19(
4511
+ return /* @__PURE__ */ jsx20(
4245
4512
  "div",
4246
4513
  {
4247
4514
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -4252,16 +4519,16 @@ var SamplePackCTACard = ({
4252
4519
  }
4253
4520
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
4254
4521
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
4255
- return /* @__PURE__ */ jsxs15(
4522
+ return /* @__PURE__ */ jsxs16(
4256
4523
  "div",
4257
4524
  {
4258
4525
  "data-testid": `sample-pack-cta-${pack.packId}`,
4259
4526
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
4260
4527
  children: [
4261
- /* @__PURE__ */ jsx19("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4262
- /* @__PURE__ */ jsx19("div", { className: "text-base text-sas-text mb-1", children: headline }),
4263
- /* @__PURE__ */ jsx19("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4264
- /* @__PURE__ */ jsx19(
4528
+ /* @__PURE__ */ jsx20("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4529
+ /* @__PURE__ */ jsx20("div", { className: "text-base text-sas-text mb-1", children: headline }),
4530
+ /* @__PURE__ */ jsx20("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4531
+ /* @__PURE__ */ jsx20(
4265
4532
  DownloadPackButton,
4266
4533
  {
4267
4534
  host,
@@ -4278,7 +4545,7 @@ var SamplePackCTACard = ({
4278
4545
  };
4279
4546
 
4280
4547
  // src/components/WaveformView.tsx
4281
- import { useEffect as useEffect11, useRef as useRef11, useState as useState13 } from "react";
4548
+ import { useEffect as useEffect12, useRef as useRef12, useState as useState15 } from "react";
4282
4549
 
4283
4550
  // src/components/waveform.ts
4284
4551
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -4341,7 +4608,7 @@ function drawWaveform(canvas, peaks, options = {}) {
4341
4608
  }
4342
4609
 
4343
4610
  // src/components/WaveformView.tsx
4344
- import { jsx as jsx20 } from "react/jsx-runtime";
4611
+ import { jsx as jsx21 } from "react/jsx-runtime";
4345
4612
  var WaveformView = ({
4346
4613
  host,
4347
4614
  filePath,
@@ -4350,9 +4617,9 @@ var WaveformView = ({
4350
4617
  fillStyle,
4351
4618
  targetSamples
4352
4619
  }) => {
4353
- const canvasRef = useRef11(null);
4354
- const [peaks, setPeaks] = useState13(null);
4355
- useEffect11(() => {
4620
+ const canvasRef = useRef12(null);
4621
+ const [peaks, setPeaks] = useState15(null);
4622
+ useEffect12(() => {
4356
4623
  let cancelled = false;
4357
4624
  let audioContext = null;
4358
4625
  (async () => {
@@ -4378,7 +4645,7 @@ var WaveformView = ({
4378
4645
  cancelled = true;
4379
4646
  };
4380
4647
  }, [host, filePath, bins, targetSamples]);
4381
- useEffect11(() => {
4648
+ useEffect12(() => {
4382
4649
  if (!peaks) return;
4383
4650
  const canvas = canvasRef.current;
4384
4651
  if (!canvas) return;
@@ -4389,7 +4656,7 @@ var WaveformView = ({
4389
4656
  observer.observe(canvas);
4390
4657
  return () => observer.disconnect();
4391
4658
  }, [peaks, fillStyle]);
4392
- return /* @__PURE__ */ jsx20(
4659
+ return /* @__PURE__ */ jsx21(
4393
4660
  "canvas",
4394
4661
  {
4395
4662
  ref: canvasRef,
@@ -4400,8 +4667,8 @@ var WaveformView = ({
4400
4667
  };
4401
4668
 
4402
4669
  // src/components/ScrollingWaveform.tsx
4403
- import { useEffect as useEffect12, useRef as useRef12 } from "react";
4404
- import { jsx as jsx21 } from "react/jsx-runtime";
4670
+ import { useEffect as useEffect13, useRef as useRef13 } from "react";
4671
+ import { jsx as jsx22 } from "react/jsx-runtime";
4405
4672
  var ScrollingWaveform = ({
4406
4673
  getPeakDb,
4407
4674
  active,
@@ -4409,11 +4676,11 @@ var ScrollingWaveform = ({
4409
4676
  className,
4410
4677
  fillStyle
4411
4678
  }) => {
4412
- const canvasRef = useRef12(null);
4413
- const ringRef = useRef12(new Float32Array(columns));
4414
- const writeIdxRef = useRef12(0);
4415
- const rafRef = useRef12(null);
4416
- useEffect12(() => {
4679
+ const canvasRef = useRef13(null);
4680
+ const ringRef = useRef13(new Float32Array(columns));
4681
+ const writeIdxRef = useRef13(0);
4682
+ const rafRef = useRef13(null);
4683
+ useEffect13(() => {
4417
4684
  if (ringRef.current.length !== columns) {
4418
4685
  const next = new Float32Array(columns);
4419
4686
  const prev = ringRef.current;
@@ -4425,7 +4692,7 @@ var ScrollingWaveform = ({
4425
4692
  writeIdxRef.current = writeIdxRef.current % columns;
4426
4693
  }
4427
4694
  }, [columns]);
4428
- useEffect12(() => {
4695
+ useEffect13(() => {
4429
4696
  if (!active) {
4430
4697
  if (rafRef.current !== null) {
4431
4698
  cancelAnimationFrame(rafRef.current);
@@ -4477,7 +4744,7 @@ var ScrollingWaveform = ({
4477
4744
  }
4478
4745
  };
4479
4746
  }, [active, getPeakDb, fillStyle]);
4480
- return /* @__PURE__ */ jsx21(
4747
+ return /* @__PURE__ */ jsx22(
4481
4748
  "canvas",
4482
4749
  {
4483
4750
  ref: canvasRef,
@@ -4488,8 +4755,8 @@ var ScrollingWaveform = ({
4488
4755
  };
4489
4756
 
4490
4757
  // src/components/OffsetScrubber.tsx
4491
- import { useCallback as useCallback10, useEffect as useEffect13, useMemo as useMemo6, useRef as useRef13, useState as useState14 } from "react";
4492
- import { jsx as jsx22, jsxs as jsxs16 } from "react/jsx-runtime";
4758
+ import { useCallback as useCallback11, useEffect as useEffect14, useMemo as useMemo7, useRef as useRef14, useState as useState16 } from "react";
4759
+ import { jsx as jsx23, jsxs as jsxs17 } from "react/jsx-runtime";
4493
4760
  var SLIDER_HEIGHT_PX = 28;
4494
4761
  var TICK_HEIGHT_PX = 14;
4495
4762
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -4502,40 +4769,40 @@ function OffsetScrubber({
4502
4769
  onChange,
4503
4770
  disabled = false
4504
4771
  }) {
4505
- const trackRef = useRef13(null);
4506
- const [draftOffset, setDraftOffset] = useState14(offsetSamples);
4507
- const [isDragging, setIsDragging] = useState14(false);
4508
- useEffect13(() => {
4772
+ const trackRef = useRef14(null);
4773
+ const [draftOffset, setDraftOffset] = useState16(offsetSamples);
4774
+ const [isDragging, setIsDragging] = useState16(false);
4775
+ useEffect14(() => {
4509
4776
  if (!isDragging) setDraftOffset(offsetSamples);
4510
4777
  }, [offsetSamples, isDragging]);
4511
4778
  const sampleRate = cuePoints?.sample_rate ?? 44100;
4512
4779
  const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
4513
- const beatsForRange = useMemo6(() => {
4780
+ const beatsForRange = useMemo7(() => {
4514
4781
  return Math.round(60 / projectBpm * sampleRate);
4515
4782
  }, [projectBpm, sampleRate]);
4516
4783
  const rangeSamples = beatsForRange * meter;
4517
- const sampleToFraction = useCallback10(
4784
+ const sampleToFraction = useCallback11(
4518
4785
  (sample) => {
4519
4786
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
4520
4787
  return (clamped + rangeSamples) / (2 * rangeSamples);
4521
4788
  },
4522
4789
  [rangeSamples]
4523
4790
  );
4524
- const fractionToSample = useCallback10(
4791
+ const fractionToSample = useCallback11(
4525
4792
  (fraction) => {
4526
4793
  const clamped = Math.max(0, Math.min(1, fraction));
4527
4794
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
4528
4795
  },
4529
4796
  [rangeSamples]
4530
4797
  );
4531
- const snapTargets = useMemo6(() => {
4798
+ const snapTargets = useMemo7(() => {
4532
4799
  if (!cuePoints || cuePoints.beats.length === 0) return [];
4533
4800
  const downbeat = cuePoints.beats[0];
4534
4801
  const positives = cuePoints.beats.map((b) => b - downbeat);
4535
4802
  const negatives = positives.slice(1).map((p) => -p);
4536
4803
  return [...negatives, ...positives].sort((a, b) => a - b);
4537
4804
  }, [cuePoints]);
4538
- const snapToBeat = useCallback10(
4805
+ const snapToBeat = useCallback11(
4539
4806
  (sample) => {
4540
4807
  if (snapTargets.length === 0) return sample;
4541
4808
  let best = snapTargets[0];
@@ -4551,7 +4818,7 @@ function OffsetScrubber({
4551
4818
  },
4552
4819
  [snapTargets]
4553
4820
  );
4554
- const handlePointerDown = useCallback10(
4821
+ const handlePointerDown = useCallback11(
4555
4822
  (e) => {
4556
4823
  if (disabled || !cuePoints) return;
4557
4824
  e.preventDefault();
@@ -4585,7 +4852,7 @@ function OffsetScrubber({
4585
4852
  },
4586
4853
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
4587
4854
  );
4588
- const handleResetToZero = useCallback10(() => {
4855
+ const handleResetToZero = useCallback11(() => {
4589
4856
  if (disabled) return;
4590
4857
  setDraftOffset(0);
4591
4858
  onChange(0);
@@ -4593,7 +4860,7 @@ function OffsetScrubber({
4593
4860
  const thumbFraction = sampleToFraction(draftOffset);
4594
4861
  const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
4595
4862
  const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
4596
- const ticks = useMemo6(() => {
4863
+ const ticks = useMemo7(() => {
4597
4864
  if (!cuePoints) return [];
4598
4865
  const downbeat = cuePoints.beats[0] ?? 0;
4599
4866
  return cuePoints.beats.map((b, i) => {
@@ -4604,9 +4871,9 @@ function OffsetScrubber({
4604
4871
  });
4605
4872
  }, [cuePoints, sampleToFraction]);
4606
4873
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
4607
- return /* @__PURE__ */ jsxs16("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
4608
- /* @__PURE__ */ jsx22("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
4609
- /* @__PURE__ */ jsxs16(
4874
+ return /* @__PURE__ */ jsxs17("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
4875
+ /* @__PURE__ */ jsx23("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
4876
+ /* @__PURE__ */ jsxs17(
4610
4877
  "div",
4611
4878
  {
4612
4879
  ref: trackRef,
@@ -4622,7 +4889,7 @@ function OffsetScrubber({
4622
4889
  "aria-valuenow": draftOffset,
4623
4890
  "aria-disabled": isDisabled,
4624
4891
  children: [
4625
- /* @__PURE__ */ jsx22(
4892
+ /* @__PURE__ */ jsx23(
4626
4893
  "div",
4627
4894
  {
4628
4895
  "aria-hidden": "true",
@@ -4630,7 +4897,7 @@ function OffsetScrubber({
4630
4897
  style: { left: "50%" }
4631
4898
  }
4632
4899
  ),
4633
- ticks.map((t) => /* @__PURE__ */ jsx22(
4900
+ ticks.map((t) => /* @__PURE__ */ jsx23(
4634
4901
  "div",
4635
4902
  {
4636
4903
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -4645,7 +4912,7 @@ function OffsetScrubber({
4645
4912
  },
4646
4913
  t.i
4647
4914
  )),
4648
- /* @__PURE__ */ jsx22(
4915
+ /* @__PURE__ */ jsx23(
4649
4916
  "div",
4650
4917
  {
4651
4918
  "data-testid": "offset-scrubber-thumb",
@@ -4662,7 +4929,7 @@ function OffsetScrubber({
4662
4929
  ]
4663
4930
  }
4664
4931
  ),
4665
- /* @__PURE__ */ jsx22(
4932
+ /* @__PURE__ */ jsx23(
4666
4933
  "span",
4667
4934
  {
4668
4935
  "data-testid": "offset-scrubber-readout",
@@ -4670,7 +4937,7 @@ function OffsetScrubber({
4670
4937
  children: formatOffset(draftOffset, sampleRate)
4671
4938
  }
4672
4939
  ),
4673
- /* @__PURE__ */ jsx22(
4940
+ /* @__PURE__ */ jsx23(
4674
4941
  "button",
4675
4942
  {
4676
4943
  type: "button",
@@ -4682,7 +4949,7 @@ function OffsetScrubber({
4682
4949
  children: "\u2316"
4683
4950
  }
4684
4951
  ),
4685
- bpmMismatch && /* @__PURE__ */ jsx22(
4952
+ bpmMismatch && /* @__PURE__ */ jsx23(
4686
4953
  "span",
4687
4954
  {
4688
4955
  "data-testid": "offset-bpm-mismatch",
@@ -4753,14 +5020,17 @@ function synthesizeCuePoints({
4753
5020
  };
4754
5021
  }
4755
5022
 
5023
+ // src/panel-core/useGeneratorPanelCore.tsx
5024
+ import { useState as useState21, useEffect as useEffect17, useCallback as useCallback15, useRef as useRef18, useMemo as useMemo9 } from "react";
5025
+
4756
5026
  // src/hooks/useSceneState.ts
4757
- import { useState as useState15, useCallback as useCallback11, useRef as useRef14 } from "react";
5027
+ import { useState as useState17, useCallback as useCallback12, useRef as useRef15 } from "react";
4758
5028
  function useSceneState(activeSceneId, initialValue) {
4759
- const [stateMap, setStateMap] = useState15(() => /* @__PURE__ */ new Map());
4760
- const activeSceneIdRef = useRef14(activeSceneId);
5029
+ const [stateMap, setStateMap] = useState17(() => /* @__PURE__ */ new Map());
5030
+ const activeSceneIdRef = useRef15(activeSceneId);
4761
5031
  activeSceneIdRef.current = activeSceneId;
4762
5032
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
4763
- const setForCurrentScene = useCallback11((value) => {
5033
+ const setForCurrentScene = useCallback12((value) => {
4764
5034
  const sid = activeSceneIdRef.current;
4765
5035
  if (sid === null) return;
4766
5036
  setStateMap((prev) => {
@@ -4771,7 +5041,7 @@ function useSceneState(activeSceneId, initialValue) {
4771
5041
  return newMap;
4772
5042
  });
4773
5043
  }, [initialValue]);
4774
- const setForScene = useCallback11((sceneId, value) => {
5044
+ const setForScene = useCallback12((sceneId, value) => {
4775
5045
  setStateMap((prev) => {
4776
5046
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
4777
5047
  const next = typeof value === "function" ? value(current) : value;
@@ -4784,10 +5054,10 @@ function useSceneState(activeSceneId, initialValue) {
4784
5054
  }
4785
5055
 
4786
5056
  // src/hooks/useAnySolo.ts
4787
- import { useEffect as useEffect14, useState as useState16 } from "react";
5057
+ import { useEffect as useEffect15, useState as useState18 } from "react";
4788
5058
  function useAnySolo(host) {
4789
- const [anySolo, setAnySolo] = useState16(false);
4790
- useEffect14(() => {
5059
+ const [anySolo, setAnySolo] = useState18(false);
5060
+ useEffect15(() => {
4791
5061
  let active = true;
4792
5062
  const refresh = () => {
4793
5063
  host.isAnySoloActive().then((v) => {
@@ -4806,7 +5076,7 @@ function useAnySolo(host) {
4806
5076
  }
4807
5077
 
4808
5078
  // src/hooks/useSoundHistory.ts
4809
- import { useCallback as useCallback12, useMemo as useMemo7, useRef as useRef15, useState as useState17 } from "react";
5079
+ import { useCallback as useCallback13, useMemo as useMemo8, useRef as useRef16, useState as useState19 } from "react";
4810
5080
  var EMPTY = { entries: [], cursor: -1 };
4811
5081
  function sameDescriptor(a, b) {
4812
5082
  if (a === b) return true;
@@ -4818,14 +5088,14 @@ function sameDescriptor(a, b) {
4818
5088
  }
4819
5089
  function useSoundHistory(applySound, opts = {}) {
4820
5090
  const max = Math.max(2, opts.max ?? 24);
4821
- const applyRef = useRef15(applySound);
5091
+ const applyRef = useRef16(applySound);
4822
5092
  applyRef.current = applySound;
4823
- const onChangeRef = useRef15(opts.onChange);
5093
+ const onChangeRef = useRef16(opts.onChange);
4824
5094
  onChangeRef.current = opts.onChange;
4825
- const dataRef = useRef15({});
4826
- const [, setVersion] = useState17(0);
4827
- const bump = useCallback12(() => setVersion((v) => v + 1), []);
4828
- const commit = useCallback12(
5095
+ const dataRef = useRef16({});
5096
+ const [, setVersion] = useState19(0);
5097
+ const bump = useCallback13(() => setVersion((v) => v + 1), []);
5098
+ const commit = useCallback13(
4829
5099
  (trackId, next, notify) => {
4830
5100
  dataRef.current = { ...dataRef.current, [trackId]: next };
4831
5101
  bump();
@@ -4833,7 +5103,7 @@ function useSoundHistory(applySound, opts = {}) {
4833
5103
  },
4834
5104
  [bump]
4835
5105
  );
4836
- const record = useCallback12(
5106
+ const record = useCallback13(
4837
5107
  (trackId, descriptor, label) => {
4838
5108
  const h = dataRef.current[trackId];
4839
5109
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -4848,7 +5118,7 @@ function useSoundHistory(applySound, opts = {}) {
4848
5118
  },
4849
5119
  [max, commit]
4850
5120
  );
4851
- const restoreTo = useCallback12(
5121
+ const restoreTo = useCallback13(
4852
5122
  async (trackId, index) => {
4853
5123
  const h = dataRef.current[trackId];
4854
5124
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -4858,7 +5128,7 @@ function useSoundHistory(applySound, opts = {}) {
4858
5128
  },
4859
5129
  [commit]
4860
5130
  );
4861
- const undo = useCallback12(
5131
+ const undo = useCallback13(
4862
5132
  (trackId) => {
4863
5133
  const h = dataRef.current[trackId];
4864
5134
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -4866,7 +5136,7 @@ function useSoundHistory(applySound, opts = {}) {
4866
5136
  },
4867
5137
  [restoreTo]
4868
5138
  );
4869
- const toggleFavorite = useCallback12(
5139
+ const toggleFavorite = useCallback13(
4870
5140
  (trackId, index) => {
4871
5141
  const h = dataRef.current[trackId];
4872
5142
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -4875,7 +5145,7 @@ function useSoundHistory(applySound, opts = {}) {
4875
5145
  },
4876
5146
  [commit]
4877
5147
  );
4878
- const restore = useCallback12(
5148
+ const restore = useCallback13(
4879
5149
  (trackId, state) => {
4880
5150
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
4881
5151
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -4884,15 +5154,15 @@ function useSoundHistory(applySound, opts = {}) {
4884
5154
  },
4885
5155
  [commit]
4886
5156
  );
4887
- const list = useCallback12(
5157
+ const list = useCallback13(
4888
5158
  (trackId) => dataRef.current[trackId] ?? EMPTY,
4889
5159
  []
4890
5160
  );
4891
- const canUndo = useCallback12((trackId) => {
5161
+ const canUndo = useCallback13((trackId) => {
4892
5162
  const h = dataRef.current[trackId];
4893
5163
  return !!h && h.cursor > 0;
4894
5164
  }, []);
4895
- const clear = useCallback12(
5165
+ const clear = useCallback13(
4896
5166
  (trackId) => {
4897
5167
  if (dataRef.current[trackId]) {
4898
5168
  const next = { ...dataRef.current };
@@ -4904,18 +5174,2356 @@ function useSoundHistory(applySound, opts = {}) {
4904
5174
  },
4905
5175
  [bump]
4906
5176
  );
4907
- const reset = useCallback12(() => {
5177
+ const reset = useCallback13(() => {
4908
5178
  dataRef.current = {};
4909
5179
  bump();
4910
5180
  }, [bump]);
4911
- return useMemo7(
5181
+ return useMemo8(
4912
5182
  () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
4913
5183
  [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
4914
5184
  );
4915
5185
  }
4916
5186
 
5187
+ // src/panel-core/track-state.ts
5188
+ function newTrackState(handle, overrides = {}) {
5189
+ return {
5190
+ handle,
5191
+ prompt: "",
5192
+ role: "",
5193
+ runtimeState: { id: handle.id, muted: false, solo: false, volume: 0.75, pan: 0 },
5194
+ fxDetailState: { ...EMPTY_FX_DETAIL_STATE },
5195
+ drawerOpen: false,
5196
+ drawerTab: "fx",
5197
+ editorStage: false,
5198
+ isGenerating: false,
5199
+ error: null,
5200
+ hasMidi: false,
5201
+ generationProgress: 0,
5202
+ editNotes: [],
5203
+ editBars: 4,
5204
+ editBpm: 120,
5205
+ instrumentPluginId: handle.instrumentPluginId ?? null,
5206
+ instrumentName: handle.instrumentName ?? null,
5207
+ instrumentMissing: false,
5208
+ shuffleHistory: /* @__PURE__ */ new Set(),
5209
+ ...overrides
5210
+ };
5211
+ }
5212
+
5213
+ // src/panel-core/panel-helpers.ts
5214
+ function trackDataKey(dbId, suffix) {
5215
+ return `track:${dbId}:${suffix}`;
5216
+ }
5217
+ function pluginFxToToggleFx(sdkState) {
5218
+ const result = { ...EMPTY_FX_DETAIL_STATE };
5219
+ for (const category of ["eq", "compressor", "chorus", "phaser", "delay", "reverb"]) {
5220
+ const sdkCat = sdkState[category];
5221
+ if (sdkCat) {
5222
+ result[category] = {
5223
+ enabled: sdkCat.enabled,
5224
+ presetIndex: sdkCat.presetIndex,
5225
+ dryWet: sdkCat.dryWet
5226
+ };
5227
+ }
5228
+ }
5229
+ return result;
5230
+ }
5231
+ function parseLLMNoteResponse(content) {
5232
+ try {
5233
+ let jsonStr = content.trim();
5234
+ const fenceMatch = jsonStr.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
5235
+ if (fenceMatch) {
5236
+ jsonStr = fenceMatch[1].trim();
5237
+ }
5238
+ const parsed = JSON.parse(jsonStr);
5239
+ if (typeof parsed !== "object" || parsed === null || !("notes" in parsed)) {
5240
+ return null;
5241
+ }
5242
+ const obj = parsed;
5243
+ if (!Array.isArray(obj.notes)) {
5244
+ return null;
5245
+ }
5246
+ const validNotes = [];
5247
+ for (const raw of obj.notes) {
5248
+ if (typeof raw !== "object" || raw === null) continue;
5249
+ const note = raw;
5250
+ const pitch = typeof note.pitch === "number" ? note.pitch : NaN;
5251
+ const startBeat = typeof note.startBeat === "number" ? note.startBeat : NaN;
5252
+ const durationBeats = typeof note.durationBeats === "number" ? note.durationBeats : NaN;
5253
+ const velocity = typeof note.velocity === "number" ? note.velocity : NaN;
5254
+ if (!isNaN(pitch) && pitch >= 0 && pitch <= 127 && !isNaN(startBeat) && startBeat >= 0 && !isNaN(durationBeats) && durationBeats > 0 && !isNaN(velocity) && velocity >= 1 && velocity <= 127) {
5255
+ validNotes.push({
5256
+ pitch: Math.round(pitch),
5257
+ startBeat,
5258
+ durationBeats,
5259
+ velocity: Math.round(velocity)
5260
+ });
5261
+ }
5262
+ }
5263
+ const role = typeof obj.role === "string" ? obj.role : void 0;
5264
+ return { notes: validNotes, role };
5265
+ } catch {
5266
+ return null;
5267
+ }
5268
+ }
5269
+
5270
+ // src/panel-core/group-meta.ts
5271
+ function parseTrackGroups(sceneData, spec) {
5272
+ const pattern = new RegExp(`^track:(.+):${spec.metaKey}$`);
5273
+ const groups = /* @__PURE__ */ new Map();
5274
+ for (const [key, val] of Object.entries(sceneData)) {
5275
+ const match = pattern.exec(key);
5276
+ if (!match) continue;
5277
+ const meta = spec.asMeta(val);
5278
+ if (!meta) continue;
5279
+ const groupId = spec.groupIdOf(meta);
5280
+ const list = groups.get(groupId) ?? [];
5281
+ list.push({ dbId: match[1], meta });
5282
+ groups.set(groupId, list);
5283
+ }
5284
+ const out = [];
5285
+ for (const [groupId, members] of groups) {
5286
+ if (spec.sortMembers) members.sort(spec.sortMembers);
5287
+ out.push({ groupId, members });
5288
+ }
5289
+ return out;
5290
+ }
5291
+ function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
5292
+ const byDbId = /* @__PURE__ */ new Map();
5293
+ for (const t of tracks) byDbId.set(getDbId(t), t);
5294
+ const resolved = [];
5295
+ const memberDbIds = /* @__PURE__ */ new Set();
5296
+ const staleMemberDbIds = [];
5297
+ for (const parsed of parsedGroups) {
5298
+ const live = { groupId: parsed.groupId, members: [] };
5299
+ for (const member of parsed.members) {
5300
+ const track = byDbId.get(member.dbId);
5301
+ if (track) live.members.push({ dbId: member.dbId, meta: member.meta, track });
5302
+ else staleMemberDbIds.push(member.dbId);
5303
+ }
5304
+ if (live.members.length === 0) continue;
5305
+ const complete = opts.isComplete ? opts.isComplete(live, parsed) : live.members.length === parsed.members.length;
5306
+ if (!complete) continue;
5307
+ resolved.push(live);
5308
+ for (const m of live.members) memberDbIds.add(m.dbId);
5309
+ }
5310
+ return { resolved, memberDbIds, staleMemberDbIds };
5311
+ }
5312
+
5313
+ // src/panel-core/useTransitionOps.ts
5314
+ import { useCallback as useCallback14, useEffect as useEffect16, useRef as useRef17, useState as useState20 } from "react";
5315
+ function useTransitionOps({
5316
+ host,
5317
+ adapter,
5318
+ activeSceneId,
5319
+ isConnected,
5320
+ isAuthenticated,
5321
+ sceneContext,
5322
+ tracks,
5323
+ setTracks,
5324
+ loadTracks,
5325
+ setCrossfadePairsMeta,
5326
+ setFadesMeta,
5327
+ resolvedCrossfadePairs,
5328
+ resolvedFades
5329
+ }) {
5330
+ const { identity } = adapter;
5331
+ const appliedFadeAutomationRef = useRef17(/* @__PURE__ */ new Set());
5332
+ const applyCrossfadeAutomation = useCallback14(
5333
+ async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
5334
+ if (host.setTrackVolumeAutomation) {
5335
+ const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
5336
+ await host.setTrackVolumeAutomation(originTrackId, curves.origin).catch(() => {
5337
+ });
5338
+ await host.setTrackVolumeAutomation(targetTrackId, curves.target).catch(() => {
5339
+ });
5340
+ } else {
5341
+ await host.setTrackVolume(originTrackId, EQUAL_POWER_GAIN).catch(() => {
5342
+ });
5343
+ await host.setTrackVolume(targetTrackId, EQUAL_POWER_GAIN).catch(() => {
5344
+ });
5345
+ }
5346
+ },
5347
+ [host]
5348
+ );
5349
+ const applyFadeAutomation = useCallback14(
5350
+ async (trackId, direction, bars, bpm, sliderPos, gesture) => {
5351
+ if (!host.setTrackVolumeAutomation) return;
5352
+ const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
5353
+ await host.setTrackVolumeAutomation(trackId, points).catch(() => {
5354
+ });
5355
+ },
5356
+ [host]
5357
+ );
5358
+ const [isCreatingCrossfade, setIsCreatingCrossfade] = useState20(false);
5359
+ const handleCreateCrossfade = useCallback14(
5360
+ async (origin, target) => {
5361
+ const scene = activeSceneId;
5362
+ const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
5363
+ const toSceneId = sceneContext?.transitionToSceneId ?? "";
5364
+ if (!scene) throw new Error("No active scene.");
5365
+ if (!isConnected) throw new Error("Systems not connected.");
5366
+ if (!isAuthenticated) throw new Error("Please sign in to generate the bridge.");
5367
+ if (tracks.length + 2 > identity.maxTracks) {
5368
+ throw new Error("Not enough track slots for a crossfade.");
5369
+ }
5370
+ setIsCreatingCrossfade(true);
5371
+ const created = [];
5372
+ try {
5373
+ const role = target.role ?? origin.role ?? "";
5374
+ const mc = await host.getMusicalContext();
5375
+ const [originMidi, targetMidi, originKey, targetKey] = await Promise.all([
5376
+ host.readImportableTrackMidi ? host.readImportableTrackMidi(origin.dbId) : Promise.resolve({ clips: [] }),
5377
+ host.readImportableTrackMidi ? host.readImportableTrackMidi(target.dbId) : Promise.resolve({ clips: [] }),
5378
+ host.getSceneKey ? host.getSceneKey(fromSceneId) : Promise.resolve(null),
5379
+ host.getSceneKey ? host.getSceneKey(toSceneId) : Promise.resolve(null)
5380
+ ]);
5381
+ const userPrompt = buildCrossfadeInpaintPrompt({
5382
+ role,
5383
+ bars: mc.bars,
5384
+ originName: origin.name,
5385
+ targetName: target.name,
5386
+ originKey: originKey ? `${originKey.key} ${originKey.mode}` : null,
5387
+ targetKey: targetKey ? `${targetKey.key} ${targetKey.mode}` : null,
5388
+ originNotes: originMidi.clips[0]?.notes ?? [],
5389
+ targetNotes: targetMidi.clips[0]?.notes ?? []
5390
+ });
5391
+ const llm = await host.generateWithLLM({
5392
+ system: adapter.buildSystemPrompt(host.getValidRoles()),
5393
+ user: userPrompt,
5394
+ responseFormat: "json"
5395
+ });
5396
+ const parsed = adapter.parseNotesResponse(llm.content);
5397
+ if (!parsed || parsed.notes.length === 0) {
5398
+ throw new Error("The bridge generator returned no notes.");
5399
+ }
5400
+ const notes = await host.postProcessMidi(parsed.notes, {
5401
+ quantize: true,
5402
+ removeOverlaps: true
5403
+ });
5404
+ const clip = {
5405
+ startTime: 0,
5406
+ endTime: mc.bars * 4 * 60 / mc.bpm,
5407
+ tempo: mc.bpm,
5408
+ notes
5409
+ };
5410
+ const top = await host.createTrack({
5411
+ name: `${identity.trackNamePrefix}-${Date.now()}-xf-o`,
5412
+ ...adapter.createTrackOptions()
5413
+ });
5414
+ created.push(top);
5415
+ const bottom = await host.createTrack({
5416
+ name: `${identity.trackNamePrefix}-${Date.now()}-xf-t`,
5417
+ ...adapter.createTrackOptions()
5418
+ });
5419
+ created.push(bottom);
5420
+ if (role) {
5421
+ await host.setTrackRole(top.id, role).catch(() => {
5422
+ });
5423
+ await host.setTrackRole(bottom.id, role).catch(() => {
5424
+ });
5425
+ }
5426
+ await host.writeMidiClip(top.id, clip);
5427
+ await host.writeMidiClip(bottom.id, clip);
5428
+ const copySound = async (newTrackId, sourceDbId) => {
5429
+ if (!host.getTrackSound) return "default";
5430
+ const snap = await host.getTrackSound(sourceDbId);
5431
+ if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) return "default";
5432
+ return adapter.sound.copySnapshot(newTrackId, snap);
5433
+ };
5434
+ const originLabel = await copySound(top.id, origin.dbId);
5435
+ const targetLabel = await copySound(bottom.id, target.dbId);
5436
+ await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
5437
+ const groupId = top.dbId;
5438
+ const originMeta = {
5439
+ groupId,
5440
+ slot: "origin",
5441
+ partnerDbId: bottom.dbId,
5442
+ sourceTrackDbId: origin.dbId,
5443
+ sourceSceneId: fromSceneId,
5444
+ sourceName: origin.name,
5445
+ soundLabel: originLabel,
5446
+ sliderPos: 0.5
5447
+ };
5448
+ const targetMeta = {
5449
+ groupId,
5450
+ slot: "target",
5451
+ partnerDbId: top.dbId,
5452
+ sourceTrackDbId: target.dbId,
5453
+ sourceSceneId: toSceneId,
5454
+ sourceName: target.name,
5455
+ soundLabel: targetLabel,
5456
+ sliderPos: 0.5
5457
+ };
5458
+ await host.setSceneData(scene, `track:${top.dbId}:crossfade`, originMeta);
5459
+ await host.setSceneData(scene, `track:${bottom.dbId}:crossfade`, targetMeta);
5460
+ await loadTracks(true);
5461
+ host.showToast("success", "Crossfade created", `${origin.name} \u2192 ${target.name}`);
5462
+ } catch (err) {
5463
+ for (const h of [...created].reverse()) {
5464
+ try {
5465
+ await host.deleteTrack(h.id);
5466
+ } catch {
5467
+ }
5468
+ }
5469
+ throw err instanceof Error ? err : new Error(String(err));
5470
+ } finally {
5471
+ setIsCreatingCrossfade(false);
5472
+ }
5473
+ },
5474
+ [
5475
+ host,
5476
+ adapter,
5477
+ identity,
5478
+ activeSceneId,
5479
+ isConnected,
5480
+ isAuthenticated,
5481
+ tracks.length,
5482
+ sceneContext,
5483
+ applyCrossfadeAutomation,
5484
+ loadTracks
5485
+ ]
5486
+ );
5487
+ const [isCreatingFade, setIsCreatingFade] = useState20(false);
5488
+ const handleCreateFade = useCallback14(
5489
+ async (selection, direction, gesture) => {
5490
+ const scene = activeSceneId;
5491
+ const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
5492
+ const toSceneId = sceneContext?.transitionToSceneId ?? "";
5493
+ if (!scene) throw new Error("No active scene.");
5494
+ if (!isConnected) throw new Error("Systems not connected.");
5495
+ if (!isAuthenticated) throw new Error("Please sign in to generate the fade.");
5496
+ if (tracks.length + 1 > identity.maxTracks) {
5497
+ throw new Error("Not enough track slots for a fade.");
5498
+ }
5499
+ setIsCreatingFade(true);
5500
+ const created = [];
5501
+ try {
5502
+ const role = selection.role ?? "";
5503
+ const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
5504
+ const mc = await host.getMusicalContext();
5505
+ const [srcMidi, srcKey] = await Promise.all([
5506
+ host.readImportableTrackMidi ? host.readImportableTrackMidi(selection.dbId) : Promise.resolve({ clips: [] }),
5507
+ host.getSceneKey ? host.getSceneKey(sourceSceneId) : Promise.resolve(null)
5508
+ ]);
5509
+ const srcNotes = srcMidi.clips[0]?.notes ?? [];
5510
+ const keyStr = srcKey ? `${srcKey.key} ${srcKey.mode}` : null;
5511
+ const userPrompt = buildCrossfadeInpaintPrompt({
5512
+ role,
5513
+ bars: mc.bars,
5514
+ originName: direction === "out" ? selection.name : "silence",
5515
+ targetName: direction === "in" ? selection.name : "silence",
5516
+ originKey: direction === "out" ? keyStr : null,
5517
+ targetKey: direction === "in" ? keyStr : null,
5518
+ originNotes: direction === "out" ? srcNotes : [],
5519
+ targetNotes: direction === "in" ? srcNotes : []
5520
+ });
5521
+ const llm = await host.generateWithLLM({
5522
+ system: adapter.buildSystemPrompt(host.getValidRoles()),
5523
+ user: userPrompt,
5524
+ responseFormat: "json"
5525
+ });
5526
+ const parsed = adapter.parseNotesResponse(llm.content);
5527
+ if (!parsed || parsed.notes.length === 0) {
5528
+ throw new Error("The fade generator returned no notes.");
5529
+ }
5530
+ const notes = await host.postProcessMidi(parsed.notes, {
5531
+ quantize: true,
5532
+ removeOverlaps: true
5533
+ });
5534
+ const clip = {
5535
+ startTime: 0,
5536
+ endTime: mc.bars * 4 * 60 / mc.bpm,
5537
+ tempo: mc.bpm,
5538
+ notes
5539
+ };
5540
+ const track = await host.createTrack({
5541
+ name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}`,
5542
+ ...adapter.createTrackOptions()
5543
+ });
5544
+ created.push(track);
5545
+ if (role) await host.setTrackRole(track.id, role).catch(() => {
5546
+ });
5547
+ await host.writeMidiClip(track.id, clip);
5548
+ let soundLabel = "default";
5549
+ if (host.getTrackSound) {
5550
+ const snap = await host.getTrackSound(selection.dbId);
5551
+ if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
5552
+ soundLabel = await adapter.sound.copySnapshot(track.id, snap);
5553
+ }
5554
+ }
5555
+ await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
5556
+ appliedFadeAutomationRef.current.add(track.id);
5557
+ const meta = {
5558
+ direction,
5559
+ gesture,
5560
+ sourceTrackDbId: selection.dbId,
5561
+ sourceSceneId,
5562
+ sourceName: selection.name,
5563
+ soundLabel,
5564
+ sliderPos: 0.5
5565
+ };
5566
+ await host.setSceneData(scene, `track:${track.dbId}:fade`, meta);
5567
+ await loadTracks(true);
5568
+ host.showToast(
5569
+ "success",
5570
+ direction === "in" ? "Fade in created" : "Fade out created",
5571
+ selection.name
5572
+ );
5573
+ } catch (err) {
5574
+ for (const h of [...created].reverse()) {
5575
+ try {
5576
+ await host.deleteTrack(h.id);
5577
+ } catch {
5578
+ }
5579
+ }
5580
+ throw err instanceof Error ? err : new Error(String(err));
5581
+ } finally {
5582
+ setIsCreatingFade(false);
5583
+ }
5584
+ },
5585
+ [
5586
+ host,
5587
+ adapter,
5588
+ identity,
5589
+ activeSceneId,
5590
+ isConnected,
5591
+ isAuthenticated,
5592
+ tracks.length,
5593
+ sceneContext,
5594
+ applyFadeAutomation,
5595
+ loadTracks
5596
+ ]
5597
+ );
5598
+ const handleCrossfadeMute = useCallback14(
5599
+ (pair) => {
5600
+ const newMuted = !pair.origin.runtimeState.muted;
5601
+ for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
5602
+ setTracks(
5603
+ (prev) => prev.map(
5604
+ (t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
5605
+ )
5606
+ );
5607
+ host.setTrackMute(id, newMuted).catch(() => {
5608
+ });
5609
+ }
5610
+ },
5611
+ [host, setTracks]
5612
+ );
5613
+ const handleCrossfadeSolo = useCallback14(
5614
+ (pair) => {
5615
+ const newSolo = !pair.origin.runtimeState.solo;
5616
+ for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
5617
+ setTracks(
5618
+ (prev) => prev.map(
5619
+ (t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
5620
+ )
5621
+ );
5622
+ host.setTrackSolo(id, newSolo).catch(() => {
5623
+ });
5624
+ }
5625
+ },
5626
+ [host, setTracks]
5627
+ );
5628
+ const handleCrossfadeDelete = useCallback14(
5629
+ async (pair) => {
5630
+ try {
5631
+ for (const member of [pair.origin, pair.target]) {
5632
+ await host.deleteTrack(member.handle.id);
5633
+ if (activeSceneId) {
5634
+ await host.deleteSceneData(activeSceneId, `track:${member.handle.dbId}:crossfade`);
5635
+ }
5636
+ }
5637
+ setCrossfadePairsMeta((prev) => prev.filter((p) => p.groupId !== pair.groupId));
5638
+ setTracks(
5639
+ (prev) => prev.filter(
5640
+ (t) => t.handle.id !== pair.origin.handle.id && t.handle.id !== pair.target.handle.id
5641
+ )
5642
+ );
5643
+ host.showToast("success", "Crossfade removed");
5644
+ } catch (err) {
5645
+ host.showToast(
5646
+ "error",
5647
+ "Failed to delete crossfade",
5648
+ err instanceof Error ? err.message : String(err)
5649
+ );
5650
+ }
5651
+ },
5652
+ [host, activeSceneId, setCrossfadePairsMeta, setTracks]
5653
+ );
5654
+ const crossfadeSliderTimers = useRef17({});
5655
+ const handleCrossfadeSlider = useCallback14(
5656
+ (pair, pos) => {
5657
+ setCrossfadePairsMeta(
5658
+ (prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
5659
+ );
5660
+ if (crossfadeSliderTimers.current[pair.groupId]) {
5661
+ clearTimeout(crossfadeSliderTimers.current[pair.groupId]);
5662
+ }
5663
+ crossfadeSliderTimers.current[pair.groupId] = setTimeout(() => {
5664
+ void (async () => {
5665
+ const mc = await host.getMusicalContext();
5666
+ await applyCrossfadeAutomation(
5667
+ pair.origin.handle.id,
5668
+ pair.target.handle.id,
5669
+ mc.bars,
5670
+ mc.bpm,
5671
+ pos
5672
+ );
5673
+ if (activeSceneId) {
5674
+ const sceneData = await host.getAllSceneData(activeSceneId);
5675
+ for (const dbId of [pair.originDbId, pair.targetDbId]) {
5676
+ const meta = asCrossfadeMeta(sceneData[`track:${dbId}:crossfade`]);
5677
+ if (meta) {
5678
+ host.setSceneData(activeSceneId, `track:${dbId}:crossfade`, { ...meta, sliderPos: pos }).catch(() => {
5679
+ });
5680
+ }
5681
+ }
5682
+ }
5683
+ })();
5684
+ }, 200);
5685
+ },
5686
+ [host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
5687
+ );
5688
+ const handleFadeDelete = useCallback14(
5689
+ async (fade) => {
5690
+ try {
5691
+ await host.deleteTrack(fade.track.handle.id);
5692
+ if (activeSceneId) {
5693
+ await host.deleteSceneData(activeSceneId, `track:${fade.dbId}:fade`);
5694
+ }
5695
+ setFadesMeta((prev) => prev.filter((f) => f.dbId !== fade.dbId));
5696
+ setTracks((prev) => prev.filter((t) => t.handle.id !== fade.track.handle.id));
5697
+ host.showToast("success", "Fade removed");
5698
+ } catch (err) {
5699
+ host.showToast(
5700
+ "error",
5701
+ "Failed to delete fade",
5702
+ err instanceof Error ? err.message : String(err)
5703
+ );
5704
+ }
5705
+ },
5706
+ [host, activeSceneId, setFadesMeta, setTracks]
5707
+ );
5708
+ const fadeSliderTimers = useRef17({});
5709
+ const handleFadeSlider = useCallback14(
5710
+ (fade, pos) => {
5711
+ setFadesMeta(
5712
+ (prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
5713
+ );
5714
+ if (fadeSliderTimers.current[fade.dbId]) clearTimeout(fadeSliderTimers.current[fade.dbId]);
5715
+ fadeSliderTimers.current[fade.dbId] = setTimeout(() => {
5716
+ void (async () => {
5717
+ const mc = await host.getMusicalContext();
5718
+ await applyFadeAutomation(
5719
+ fade.track.handle.id,
5720
+ fade.meta.direction,
5721
+ mc.bars,
5722
+ mc.bpm,
5723
+ pos,
5724
+ fade.meta.gesture
5725
+ );
5726
+ if (activeSceneId) {
5727
+ const sceneData = await host.getAllSceneData(activeSceneId);
5728
+ const meta = asFadeMeta(sceneData[`track:${fade.dbId}:fade`]);
5729
+ if (meta) {
5730
+ host.setSceneData(activeSceneId, `track:${fade.dbId}:fade`, { ...meta, sliderPos: pos }).catch(() => {
5731
+ });
5732
+ }
5733
+ }
5734
+ })();
5735
+ }, 200);
5736
+ },
5737
+ [host, activeSceneId, applyFadeAutomation, setFadesMeta]
5738
+ );
5739
+ const lastResyncKeyRef = useRef17("");
5740
+ useEffect16(() => {
5741
+ if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
5742
+ return;
5743
+ }
5744
+ const resyncKey = [
5745
+ ...resolvedCrossfadePairs.map(
5746
+ (p) => `${p.origin.handle.dbId}<${p.originSourceDbId}|${p.target.handle.dbId}<${p.targetSourceDbId}`
5747
+ ),
5748
+ ...resolvedFades.map((f) => `${f.track.handle.dbId}<${f.meta.sourceTrackDbId}`)
5749
+ ].join(",");
5750
+ if (resyncKey === lastResyncKeyRef.current) return;
5751
+ lastResyncKeyRef.current = resyncKey;
5752
+ let cancelled = false;
5753
+ const reapplyIfDrifted = async (layerTrackId, layerDbId, sourceDbId) => {
5754
+ if (!host.getTrackSound || cancelled) return;
5755
+ const [sourceSnap, layerSnap] = await Promise.all([
5756
+ host.getTrackSound(sourceDbId),
5757
+ host.getTrackSound(layerDbId)
5758
+ ]);
5759
+ if (cancelled || !sourceSnap || sourceSnap.kind !== adapter.sound.acceptedSnapshotKind) {
5760
+ return;
5761
+ }
5762
+ if (soundIdentity(sourceSnap) === soundIdentity(layerSnap)) return;
5763
+ try {
5764
+ await adapter.sound.copySnapshot(layerTrackId, sourceSnap);
5765
+ } catch {
5766
+ }
5767
+ };
5768
+ void (async () => {
5769
+ for (const pair of resolvedCrossfadePairs) {
5770
+ await reapplyIfDrifted(pair.origin.handle.id, pair.origin.handle.dbId, pair.originSourceDbId);
5771
+ await reapplyIfDrifted(pair.target.handle.id, pair.target.handle.dbId, pair.targetSourceDbId);
5772
+ }
5773
+ for (const fade of resolvedFades) {
5774
+ await reapplyIfDrifted(fade.track.handle.id, fade.track.handle.dbId, fade.meta.sourceTrackDbId);
5775
+ }
5776
+ })();
5777
+ return () => {
5778
+ cancelled = true;
5779
+ };
5780
+ }, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
5781
+ useEffect16(() => {
5782
+ if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
5783
+ void (async () => {
5784
+ const mc = await host.getMusicalContext();
5785
+ for (const fade of resolvedFades) {
5786
+ const id = fade.track.handle.id;
5787
+ if (appliedFadeAutomationRef.current.has(id)) continue;
5788
+ appliedFadeAutomationRef.current.add(id);
5789
+ await applyFadeAutomation(
5790
+ id,
5791
+ fade.meta.direction,
5792
+ mc.bars,
5793
+ mc.bpm,
5794
+ fade.meta.sliderPos,
5795
+ fade.meta.gesture
5796
+ );
5797
+ }
5798
+ })();
5799
+ }, [resolvedFades, host, applyFadeAutomation]);
5800
+ return {
5801
+ isCreatingCrossfade,
5802
+ isCreatingFade,
5803
+ handleCreateCrossfade,
5804
+ handleCreateFade,
5805
+ handleCrossfadeMute,
5806
+ handleCrossfadeSolo,
5807
+ handleCrossfadeDelete,
5808
+ handleCrossfadeSlider,
5809
+ handleFadeDelete,
5810
+ handleFadeSlider
5811
+ };
5812
+ }
5813
+
5814
+ // src/panel-core/useGeneratorPanelCore.tsx
5815
+ import { jsx as jsx24, jsxs as jsxs18 } from "react/jsx-runtime";
5816
+ var EMPTY_PLACEHOLDERS = [];
5817
+ function useGeneratorPanelCore({
5818
+ ui,
5819
+ adapter
5820
+ }) {
5821
+ const {
5822
+ host,
5823
+ activeSceneId,
5824
+ isAuthenticated,
5825
+ isConnected,
5826
+ onHeaderContent,
5827
+ onLoading,
5828
+ sceneContext,
5829
+ onOpenContract,
5830
+ onExpandSelf,
5831
+ isExpanded
5832
+ } = ui;
5833
+ const { identity, features } = adapter;
5834
+ const logTag = identity.logTag;
5835
+ const adapterRef = useRef18(adapter);
5836
+ useEffect17(() => {
5837
+ if (adapterRef.current !== adapter) {
5838
+ adapterRef.current = adapter;
5839
+ console.warn(
5840
+ `[${logTag}] GeneratorPanelAdapter identity changed between renders \u2014 wrap it in useMemo(() => createAdapter(host), [host]) to avoid load loops.`
5841
+ );
5842
+ }
5843
+ }, [adapter, logTag]);
5844
+ const supportsMeters = typeof host.getTrackLevels === "function";
5845
+ const trackLevels = useTrackLevels(host, isExpanded);
5846
+ const [tracks, setTracks] = useState21([]);
5847
+ const [isLoadingTracks, setIsLoadingTracks] = useState21(false);
5848
+ const [importOpen, setImportOpen] = useState21(false);
5849
+ const [soundImportTarget, setSoundImportTarget] = useState21(null);
5850
+ const [designerView, setDesignerView] = useState21(false);
5851
+ const [transitionSourceTotal, setTransitionSourceTotal] = useState21(0);
5852
+ const [crossfadePairsMeta, setCrossfadePairsMeta] = useState21([]);
5853
+ const [fadesMeta, setFadesMeta] = useState21([]);
5854
+ const [genericGroupMetas, setGenericGroupMetas] = useState21({});
5855
+ const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
5856
+ const [placeholders, , setPlaceholdersForScene] = useSceneState(
5857
+ activeSceneId,
5858
+ EMPTY_PLACEHOLDERS
5859
+ );
5860
+ const saveTimeoutRefs = useRef18({});
5861
+ const editLoadStartedRef = useRef18(/* @__PURE__ */ new Set());
5862
+ const [availableInstruments, setAvailableInstruments] = useState21([]);
5863
+ const [instrumentsLoading, setInstrumentsLoading] = useState21(false);
5864
+ const engineToDbIdRef = useRef18(/* @__PURE__ */ new Map());
5865
+ const tracksLoadedForSceneRef = useRef18(null);
5866
+ const persistSoundHistory = useCallback15(
5867
+ (trackId, state) => {
5868
+ if (!activeSceneId) return;
5869
+ const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
5870
+ host.setSceneData(activeSceneId, trackDataKey(dbId, "soundHistory"), state).catch(() => {
5871
+ });
5872
+ },
5873
+ [host, activeSceneId]
5874
+ );
5875
+ const soundHistory = useSoundHistory(adapter.sound.applySound, {
5876
+ max: adapter.sound.historyMax,
5877
+ onChange: persistSoundHistory
5878
+ });
5879
+ const anySolo = useAnySolo(host);
5880
+ const reorder = useTrackReorder({
5881
+ host,
5882
+ items: tracks,
5883
+ setItems: setTracks,
5884
+ getId: (t) => t.handle.dbId
5885
+ });
5886
+ const loadTracks = useCallback15(
5887
+ async (incremental = false) => {
5888
+ const sceneAtStart = activeSceneId;
5889
+ if (!sceneAtStart) {
5890
+ setTracks([]);
5891
+ setCrossfadePairsMeta([]);
5892
+ setFadesMeta([]);
5893
+ setGenericGroupMetas({});
5894
+ tracksLoadedForSceneRef.current = null;
5895
+ setIsLoadingTracks(false);
5896
+ return;
5897
+ }
5898
+ if (!incremental && tracksLoadedForSceneRef.current !== sceneAtStart) {
5899
+ setTracks([]);
5900
+ }
5901
+ tracksLoadedForSceneRef.current = sceneAtStart;
5902
+ if (!incremental) soundHistory.reset();
5903
+ const isStale = () => tracksLoadedForSceneRef.current !== sceneAtStart;
5904
+ if (!incremental) setIsLoadingTracks(true);
5905
+ try {
5906
+ await host.adoptSceneTracks();
5907
+ if (isStale()) return;
5908
+ const handles = await host.getPluginTracks();
5909
+ if (isStale()) return;
5910
+ const sceneData = await host.getAllSceneData(sceneAtStart);
5911
+ if (isStale()) return;
5912
+ const idMap = /* @__PURE__ */ new Map();
5913
+ for (const h of handles) {
5914
+ idMap.set(h.id, h.dbId);
5915
+ }
5916
+ engineToDbIdRef.current = idMap;
5917
+ const trackStates = [];
5918
+ for (const handle of handles) {
5919
+ let runtimeState = {
5920
+ id: handle.id,
5921
+ muted: false,
5922
+ solo: false,
5923
+ volume: 0.75,
5924
+ pan: 0
5925
+ };
5926
+ let hasMidi = false;
5927
+ try {
5928
+ const info = await host.getTrackInfo(handle.id);
5929
+ runtimeState = {
5930
+ id: handle.id,
5931
+ muted: info.muted,
5932
+ solo: info.soloed,
5933
+ volume: info.volume,
5934
+ pan: info.pan
5935
+ };
5936
+ hasMidi = info.hasMidi;
5937
+ } catch {
5938
+ }
5939
+ let fxDetailState = newTrackState(handle).fxDetailState;
5940
+ try {
5941
+ const fxState = await host.getTrackFxState(handle.id);
5942
+ fxDetailState = pluginFxToToggleFx(fxState);
5943
+ } catch {
5944
+ }
5945
+ const promptKey = trackDataKey(handle.dbId, "prompt");
5946
+ let prompt = typeof sceneData[promptKey] === "string" ? sceneData[promptKey] : "";
5947
+ if (!prompt && handle.prompt) {
5948
+ prompt = handle.prompt;
5949
+ host.setSceneData(sceneAtStart, promptKey, prompt).catch(() => {
5950
+ });
5951
+ }
5952
+ if (!hasMidi && handle.role) {
5953
+ hasMidi = true;
5954
+ }
5955
+ let instrumentMissing = false;
5956
+ if (handle.instrumentPluginId) {
5957
+ try {
5958
+ const instrDescriptor = await host.getTrackInstrument(handle.id);
5959
+ if (instrDescriptor?.missing) {
5960
+ instrumentMissing = true;
5961
+ }
5962
+ } catch {
5963
+ }
5964
+ }
5965
+ trackStates.push(
5966
+ newTrackState(handle, {
5967
+ prompt,
5968
+ role: handle.role ?? "",
5969
+ runtimeState,
5970
+ fxDetailState,
5971
+ hasMidi,
5972
+ instrumentMissing
5973
+ })
5974
+ );
5975
+ }
5976
+ if (isStale()) return;
5977
+ setTracks((prev) => {
5978
+ const prevByDbId = new Map(prev.map((p) => [p.handle.dbId, p]));
5979
+ return trackStates.map((ts) => {
5980
+ const carry = prevByDbId.get(ts.handle.dbId);
5981
+ return carry ? { ...ts, editNotes: carry.editNotes, editBars: carry.editBars, editBpm: carry.editBpm } : ts;
5982
+ });
5983
+ });
5984
+ for (const ts of trackStates) {
5985
+ const persisted = sceneData[trackDataKey(ts.handle.dbId, "soundHistory")];
5986
+ if (persisted && typeof persisted === "object") {
5987
+ soundHistory.restore(ts.handle.id, persisted);
5988
+ }
5989
+ }
5990
+ if (!isStale()) {
5991
+ setCrossfadePairsMeta(parseCrossfadePairs(sceneData));
5992
+ setFadesMeta(parseFades(sceneData));
5993
+ if (adapter.groupExtensions && adapter.groupExtensions.length > 0) {
5994
+ const map = {};
5995
+ for (const ext of adapter.groupExtensions) {
5996
+ map[ext.metaKey] = parseTrackGroups(sceneData, ext);
5997
+ }
5998
+ setGenericGroupMetas(map);
5999
+ }
6000
+ }
6001
+ } catch (error) {
6002
+ console.error(`[${logTag}] Failed to load tracks:`, error);
6003
+ } finally {
6004
+ if (tracksLoadedForSceneRef.current === sceneAtStart) {
6005
+ setIsLoadingTracks(false);
6006
+ }
6007
+ }
6008
+ },
6009
+ [host, activeSceneId, soundHistory, adapter, logTag]
6010
+ );
6011
+ useEffect17(() => {
6012
+ loadTracks();
6013
+ }, [loadTracks]);
6014
+ useEffect17(() => {
6015
+ const map = /* @__PURE__ */ new Map();
6016
+ for (const t of tracks) {
6017
+ map.set(t.handle.id, t.handle.dbId);
6018
+ }
6019
+ engineToDbIdRef.current = map;
6020
+ }, [tracks]);
6021
+ const loadedCompletedIdsRef = useRef18(/* @__PURE__ */ new Set());
6022
+ useEffect17(() => {
6023
+ if (placeholders.length === 0) {
6024
+ loadedCompletedIdsRef.current.clear();
6025
+ return;
6026
+ }
6027
+ const newCompleted = placeholders.filter(
6028
+ (ph) => ph.status === "completed" && !loadedCompletedIdsRef.current.has(ph.id)
6029
+ );
6030
+ if (newCompleted.length > 0) {
6031
+ for (const ph of newCompleted) {
6032
+ loadedCompletedIdsRef.current.add(ph.id);
6033
+ }
6034
+ console.log(
6035
+ `[${logTag}] ${newCompleted.length} track(s) completed, reloading. IDs:`,
6036
+ newCompleted.map((ph) => ph.id)
6037
+ );
6038
+ loadTracks(true);
6039
+ }
6040
+ }, [placeholders, loadTracks, logTag]);
6041
+ const adoptAndLoad = useCallback15(() => {
6042
+ loadTracks(true);
6043
+ }, [loadTracks]);
6044
+ useEffect17(() => {
6045
+ const unsub = host.onEngineReady(() => {
6046
+ adoptAndLoad();
6047
+ });
6048
+ return unsub;
6049
+ }, [host, adoptAndLoad]);
6050
+ useEffect17(() => {
6051
+ if (typeof host.onAfterAgentMutation !== "function") return;
6052
+ let timer = null;
6053
+ const unsub = host.onAfterAgentMutation(() => {
6054
+ if (timer) clearTimeout(timer);
6055
+ timer = setTimeout(() => {
6056
+ timer = null;
6057
+ loadTracks(true);
6058
+ }, 500);
6059
+ });
6060
+ return () => {
6061
+ unsub?.();
6062
+ if (timer) clearTimeout(timer);
6063
+ };
6064
+ }, [host, loadTracks]);
6065
+ useEffect17(() => {
6066
+ const unsub = host.onTrackStateChange((trackId, state) => {
6067
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
6068
+ });
6069
+ return unsub;
6070
+ }, [host]);
6071
+ useEffect17(() => {
6072
+ if (!features.bulkComposePlaceholders) return;
6073
+ console.log(`[${logTag}] Subscribing to composeProgress`);
6074
+ const unsub = host.onComposeProgress((event) => {
6075
+ const targetScene = event.sceneId;
6076
+ if (!targetScene) return;
6077
+ console.log(
6078
+ `[${logTag}] composeProgress event:`,
6079
+ event.phase,
6080
+ "sceneId:",
6081
+ targetScene,
6082
+ "placeholders:",
6083
+ event.placeholders?.length ?? "none"
6084
+ );
6085
+ switch (event.phase) {
6086
+ case "planning":
6087
+ setIsComposingForScene(targetScene, true);
6088
+ setPlaceholdersForScene(targetScene, []);
6089
+ break;
6090
+ case "generating":
6091
+ setIsComposingForScene(targetScene, false);
6092
+ if (event.placeholders) {
6093
+ setPlaceholdersForScene(targetScene, event.placeholders);
6094
+ }
6095
+ break;
6096
+ case "complete":
6097
+ case "error":
6098
+ setIsComposingForScene(targetScene, false);
6099
+ setPlaceholdersForScene(targetScene, EMPTY_PLACEHOLDERS);
6100
+ break;
6101
+ }
6102
+ });
6103
+ return unsub;
6104
+ }, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
6105
+ useEffect17(() => {
6106
+ const refs = saveTimeoutRefs;
6107
+ return () => {
6108
+ for (const timeout of Object.values(refs.current)) {
6109
+ clearTimeout(timeout);
6110
+ }
6111
+ };
6112
+ }, []);
6113
+ const isAddingTrackRef = useRef18(false);
6114
+ const [isAddingTrack, setIsAddingTrack] = useState21(false);
6115
+ const handleAddTrack = useCallback15(async () => {
6116
+ if (isAddingTrackRef.current) return;
6117
+ if (!activeSceneId) {
6118
+ host.showToast("warning", "Select SCENE");
6119
+ return;
6120
+ }
6121
+ if (!isConnected) {
6122
+ host.showToast("warning", "Systems not connected");
6123
+ return;
6124
+ }
6125
+ if (!isAuthenticated) {
6126
+ host.showToast("warning", "Sign In Required", "Please sign in to add tracks");
6127
+ return;
6128
+ }
6129
+ if (tracks.length >= identity.maxTracks) return;
6130
+ isAddingTrackRef.current = true;
6131
+ setIsAddingTrack(true);
6132
+ try {
6133
+ const handle = await host.createTrack({
6134
+ name: `${identity.trackNamePrefix}-${Date.now()}`,
6135
+ ...adapter.createTrackOptions()
6136
+ });
6137
+ setTracks((prev) => [...prev, newTrackState(handle)]);
6138
+ onExpandSelf?.();
6139
+ setTimeout(() => {
6140
+ const inputs = document.querySelectorAll(
6141
+ `[data-testid="${identity.familyKey}-section"] [data-testid="sdk-prompt-input"]`
6142
+ );
6143
+ if (inputs.length > 0) {
6144
+ inputs[inputs.length - 1].focus();
6145
+ }
6146
+ }, 350);
6147
+ } catch (error) {
6148
+ const msg = error instanceof Error ? error.message : "Unknown error";
6149
+ host.showToast("error", "Failed to create track", msg);
6150
+ } finally {
6151
+ isAddingTrackRef.current = false;
6152
+ setIsAddingTrack(false);
6153
+ }
6154
+ }, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
6155
+ const handlePortTrack = useCallback15(
6156
+ async (sel) => {
6157
+ if (!activeSceneId) {
6158
+ host.showToast("warning", "Select SCENE");
6159
+ return;
6160
+ }
6161
+ if (!isConnected) {
6162
+ host.showToast("warning", "Systems not connected");
6163
+ return;
6164
+ }
6165
+ if (tracks.length >= identity.maxTracks) {
6166
+ host.showToast("warning", "Track limit reached");
6167
+ return;
6168
+ }
6169
+ if (!host.readImportableTrackMidi) return;
6170
+ let handle = null;
6171
+ try {
6172
+ handle = await host.createTrack({
6173
+ name: `${identity.trackNamePrefix}-${Date.now()}`,
6174
+ ...adapter.createTrackOptions()
6175
+ });
6176
+ if (sel.role) {
6177
+ try {
6178
+ await host.setTrackRole(handle.id, sel.role);
6179
+ } catch {
6180
+ }
6181
+ }
6182
+ const midi = await host.readImportableTrackMidi(sel.sourceTrackDbId);
6183
+ const notes = midi.clips[0]?.notes ?? [];
6184
+ if (notes.length > 0) {
6185
+ const mc = await host.getMusicalContext();
6186
+ await host.writeMidiClip(handle.id, {
6187
+ startTime: 0,
6188
+ endTime: mc.bars * 4 * 60 / mc.bpm,
6189
+ tempo: mc.bpm,
6190
+ notes
6191
+ });
6192
+ }
6193
+ await adapter.applyPortedTrackSound(handle, sel.role);
6194
+ host.showToast(
6195
+ "success",
6196
+ `Imported to ${identity.familyKey}`,
6197
+ notes.length ? `${sel.trackName} \u2192 ${identity.familyKey}` : `${sel.trackName} (no MIDI yet)`
6198
+ );
6199
+ await loadTracks(true);
6200
+ } catch (err) {
6201
+ if (handle) {
6202
+ try {
6203
+ await host.deleteTrack(handle.id);
6204
+ } catch {
6205
+ }
6206
+ }
6207
+ host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
6208
+ }
6209
+ },
6210
+ [host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
6211
+ );
6212
+ const handleSoundImportPick = useCallback15(
6213
+ async (sel) => {
6214
+ const target = soundImportTarget;
6215
+ if (!target || !host.getTrackSound) {
6216
+ setSoundImportTarget(null);
6217
+ return;
6218
+ }
6219
+ const noun = adapter.sound.importNoun;
6220
+ const nounTitle = noun.charAt(0).toUpperCase() + noun.slice(1);
6221
+ try {
6222
+ const snap = await host.getTrackSound(sel.sourceTrackDbId);
6223
+ if (!snap || snap.kind !== adapter.sound.acceptedSnapshotKind) {
6224
+ host.showToast(
6225
+ "error",
6226
+ `No ${noun} to import`,
6227
+ `${sel.trackName} has no ${identity.familyKey} ${noun}.`
6228
+ );
6229
+ return;
6230
+ }
6231
+ const descriptor = adapter.sound.descriptorFromSnapshot(snap);
6232
+ await adapter.sound.applySound(target.handle.id, descriptor);
6233
+ soundHistory.record(target.handle.id, descriptor, snap.label);
6234
+ host.showToast("success", `${nounTitle} imported`, `${snap.label} \u2192 ${target.handle.name}`);
6235
+ } catch (err) {
6236
+ host.showToast("error", "Import failed", err instanceof Error ? err.message : String(err));
6237
+ } finally {
6238
+ setSoundImportTarget(null);
6239
+ }
6240
+ },
6241
+ [soundImportTarget, host, adapter, identity.familyKey, soundHistory]
6242
+ );
6243
+ const [isExportingMidi, setIsExportingMidi] = useState21(false);
6244
+ const handleExportMidi = useCallback15(async () => {
6245
+ if (isExportingMidi) return;
6246
+ setIsExportingMidi(true);
6247
+ try {
6248
+ const result = await host.exportTracksAsMidiBundle({
6249
+ defaultName: identity.exportDefaultName ?? "midi-tracks"
6250
+ });
6251
+ if (result.success) {
6252
+ const filename = result.filePath.split("/").pop() || result.filePath;
6253
+ const skippedNote = result.skippedCount > 0 ? ` (${result.skippedCount} empty track${result.skippedCount === 1 ? "" : "s"} skipped)` : "";
6254
+ host.showToast(
6255
+ "success",
6256
+ "MIDI exported",
6257
+ `${result.trackCount} track${result.trackCount === 1 ? "" : "s"} \u2192 ${filename}${skippedNote}`
6258
+ );
6259
+ } else if (!("canceled" in result && result.canceled)) {
6260
+ const errMsg = "error" in result ? result.error : "Unknown error";
6261
+ host.showToast("error", "Export failed", errMsg);
6262
+ }
6263
+ } catch (error) {
6264
+ const msg = error instanceof Error ? error.message : String(error);
6265
+ host.showToast("error", "Export failed", msg);
6266
+ } finally {
6267
+ setIsExportingMidi(false);
6268
+ }
6269
+ }, [host, identity.exportDefaultName, isExportingMidi]);
6270
+ const isBulkActive = !!(isComposing || placeholders.length > 0);
6271
+ const needsContract = !sceneContext?.hasContract;
6272
+ const xfFromId = sceneContext?.transitionFromSceneId ?? null;
6273
+ const xfToId = sceneContext?.transitionToSceneId ?? null;
6274
+ const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
6275
+ useEffect17(() => {
6276
+ if (!canCrossfade) setDesignerView(false);
6277
+ }, [canCrossfade]);
6278
+ useEffect17(() => {
6279
+ if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
6280
+ setTransitionSourceTotal(0);
6281
+ return;
6282
+ }
6283
+ let cancelled = false;
6284
+ void Promise.all([host.listSceneFamilyTracks(xfFromId), host.listSceneFamilyTracks(xfToId)]).then(([a, b]) => {
6285
+ if (!cancelled) setTransitionSourceTotal(a.length + b.length);
6286
+ }).catch(() => {
6287
+ if (!cancelled) setTransitionSourceTotal(0);
6288
+ });
6289
+ return () => {
6290
+ cancelled = true;
6291
+ };
6292
+ }, [canCrossfade, xfFromId, xfToId, host]);
6293
+ const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
6294
+ useEffect17(() => {
6295
+ if (!onHeaderContent) return;
6296
+ const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
6297
+ onHeaderContent(
6298
+ /* @__PURE__ */ jsxs18("div", { className: "flex gap-1 items-center", children: [
6299
+ features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ jsx24(
6300
+ "button",
6301
+ {
6302
+ "data-testid": `import-from-scene-${identity.familyKey}-button`,
6303
+ onClick: (e) => {
6304
+ e.stopPropagation();
6305
+ onExpandSelf?.();
6306
+ setImportOpen(true);
6307
+ },
6308
+ disabled: !activeSceneId || needsContract,
6309
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${!activeSceneId || needsContract ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-muted hover:border-sas-accent hover:text-sas-accent"}`,
6310
+ children: identity.importTrackLabel ?? "Import Track"
6311
+ }
6312
+ ),
6313
+ (!canCrossfade || !designerView) && /* @__PURE__ */ jsx24(
6314
+ "button",
6315
+ {
6316
+ "data-testid": `add-${identity.familyKey}-track-button`,
6317
+ onClick: (e) => {
6318
+ e.stopPropagation();
6319
+ if (needsContract) {
6320
+ onOpenContract?.();
6321
+ return;
6322
+ }
6323
+ handleAddTrack();
6324
+ },
6325
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors ${addDisabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-accent/10 border-sas-accent/30 text-sas-accent hover:bg-sas-accent/20"}`,
6326
+ children: identity.addTrackLabel ?? "Add Track"
6327
+ }
6328
+ ),
6329
+ canCrossfade && /* @__PURE__ */ jsxs18(
6330
+ "button",
6331
+ {
6332
+ "data-testid": `${identity.familyKey}-view-toggle`,
6333
+ onClick: (e) => {
6334
+ e.stopPropagation();
6335
+ if (!designerView) {
6336
+ if (needsContract) {
6337
+ onOpenContract?.();
6338
+ return;
6339
+ }
6340
+ onExpandSelf?.();
6341
+ }
6342
+ setDesignerView((v) => !v);
6343
+ },
6344
+ disabled: !designerView && needsContract,
6345
+ title: designerView ? "Back to the track list" : "Open the transition designer",
6346
+ className: "relative overflow-hidden px-2 py-0.5 text-[10px] font-medium rounded-sm border border-sas-accent/40 text-sas-accent transition-colors hover:border-sas-accent disabled:opacity-50",
6347
+ children: [
6348
+ transitionSourceTotal > 0 && /* @__PURE__ */ jsx24(
6349
+ "span",
6350
+ {
6351
+ className: "absolute inset-y-0 left-0 bg-sas-accent/25",
6352
+ style: { width: `${Math.min(100, transitionDone / transitionSourceTotal * 100)}%` },
6353
+ "aria-hidden": true
6354
+ }
6355
+ ),
6356
+ /* @__PURE__ */ jsxs18("span", { className: "relative", children: [
6357
+ "\u21C4 ",
6358
+ designerView ? "Transition" : "Tracks",
6359
+ transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
6360
+ ] })
6361
+ ]
6362
+ }
6363
+ )
6364
+ ] })
6365
+ );
6366
+ return () => {
6367
+ onHeaderContent(null);
6368
+ };
6369
+ }, [
6370
+ onHeaderContent,
6371
+ needsContract,
6372
+ isConnected,
6373
+ activeSceneId,
6374
+ tracks.length,
6375
+ isAddingTrack,
6376
+ handleAddTrack,
6377
+ onOpenContract,
6378
+ host,
6379
+ canCrossfade,
6380
+ designerView,
6381
+ transitionDone,
6382
+ transitionSourceTotal,
6383
+ onExpandSelf,
6384
+ identity,
6385
+ features.importTracks
6386
+ ]);
6387
+ useEffect17(() => {
6388
+ if (!onLoading) return;
6389
+ const anyGenerating = tracks.some((t) => t.isGenerating);
6390
+ onLoading(isLoadingTracks || anyGenerating || isBulkActive);
6391
+ return () => {
6392
+ onLoading(false);
6393
+ };
6394
+ }, [onLoading, isLoadingTracks, tracks, isBulkActive]);
6395
+ const handleDeleteTrack = useCallback15(
6396
+ async (trackId) => {
6397
+ try {
6398
+ await host.deleteTrack(trackId);
6399
+ const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
6400
+ if (activeSceneId) {
6401
+ await host.deleteSceneData(activeSceneId, trackDataKey(dbId, "prompt"));
6402
+ }
6403
+ setTracks((prev) => prev.filter((t) => t.handle.id !== trackId));
6404
+ } catch (error) {
6405
+ const msg = error instanceof Error ? error.message : "Unknown error";
6406
+ host.showToast("error", "Failed to delete track", msg);
6407
+ }
6408
+ },
6409
+ [host, activeSceneId]
6410
+ );
6411
+ const handlePromptChange = useCallback15(
6412
+ (trackId, prompt) => {
6413
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
6414
+ const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
6415
+ if (saveTimeoutRefs.current[trackId]) {
6416
+ clearTimeout(saveTimeoutRefs.current[trackId]);
6417
+ }
6418
+ saveTimeoutRefs.current[trackId] = setTimeout(() => {
6419
+ if (activeSceneId) {
6420
+ host.setSceneData(activeSceneId, trackDataKey(dbId, "prompt"), prompt).catch(() => {
6421
+ });
6422
+ }
6423
+ }, 500);
6424
+ },
6425
+ [host, activeSceneId]
6426
+ );
6427
+ const resolvedGenericGroups = useMemo9(() => {
6428
+ const out = {};
6429
+ for (const ext of adapter.groupExtensions ?? []) {
6430
+ out[ext.metaKey] = resolveTrackGroups(
6431
+ genericGroupMetas[ext.metaKey] ?? [],
6432
+ tracks,
6433
+ (t) => t.handle.dbId,
6434
+ {
6435
+ isComplete: ext.isComplete
6436
+ }
6437
+ );
6438
+ }
6439
+ return out;
6440
+ }, [adapter, genericGroupMetas, tracks]);
6441
+ const genericGroupMemberDbIds = useMemo9(() => {
6442
+ const s = /* @__PURE__ */ new Set();
6443
+ for (const r of Object.values(resolvedGenericGroups)) {
6444
+ for (const dbId of r.memberDbIds) s.add(dbId);
6445
+ }
6446
+ return s;
6447
+ }, [resolvedGenericGroups]);
6448
+ const engineToDbId = useCallback15(
6449
+ (trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
6450
+ []
6451
+ );
6452
+ const updateTrack = useCallback15(
6453
+ (trackId, patch) => {
6454
+ setTracks(
6455
+ (prev) => prev.map(
6456
+ (t) => t.handle.id === trackId ? typeof patch === "function" ? patch(t) : { ...t, ...patch } : t
6457
+ )
6458
+ );
6459
+ },
6460
+ []
6461
+ );
6462
+ const markEditLoaded = useCallback15((trackId) => {
6463
+ editLoadStartedRef.current.add(trackId);
6464
+ }, []);
6465
+ const tracksRef = useRef18(tracks);
6466
+ useEffect17(() => {
6467
+ tracksRef.current = tracks;
6468
+ }, [tracks]);
6469
+ const resolvedGenericGroupsRef = useRef18(resolvedGenericGroups);
6470
+ useEffect17(() => {
6471
+ resolvedGenericGroupsRef.current = resolvedGenericGroups;
6472
+ }, [resolvedGenericGroups]);
6473
+ const makeServices = useCallback15(() => {
6474
+ return {
6475
+ host,
6476
+ activeSceneId,
6477
+ tracks: tracksRef.current,
6478
+ updateTrack,
6479
+ setTracks,
6480
+ reloadTracks: loadTracks,
6481
+ soundHistory,
6482
+ engineToDbId,
6483
+ trackDataKey,
6484
+ markEditLoaded,
6485
+ createFamilyTrack: (nameSuffix = "") => host.createTrack({
6486
+ name: `${identity.trackNamePrefix}-${Date.now()}${nameSuffix}`,
6487
+ ...adapter.createTrackOptions()
6488
+ }),
6489
+ resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
6490
+ };
6491
+ }, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
6492
+ const handleGenerate = useCallback15(
6493
+ async (trackId) => {
6494
+ const track = tracks.find((t) => t.handle.id === trackId);
6495
+ if (!track || !track.prompt.trim()) return;
6496
+ if (!isAuthenticated) {
6497
+ host.showToast("warning", "Sign In Required", "Please sign in to generate MIDI");
6498
+ return;
6499
+ }
6500
+ setTracks(
6501
+ (prev) => prev.map(
6502
+ (t) => t.handle.id === trackId ? { ...t, isGenerating: true, error: null, generationProgress: 0 } : t
6503
+ )
6504
+ );
6505
+ try {
6506
+ await adapter.generation.generate(track, makeServices());
6507
+ } catch (error) {
6508
+ const msg = error instanceof Error ? error.message : "Generation failed";
6509
+ setTracks(
6510
+ (prev) => prev.map(
6511
+ (t) => t.handle.id === trackId ? { ...t, isGenerating: false, error: msg, generationProgress: 0 } : t
6512
+ )
6513
+ );
6514
+ host.showToast("error", "Generation failed", msg);
6515
+ }
6516
+ },
6517
+ [host, adapter, tracks, isAuthenticated, makeServices]
6518
+ );
6519
+ const handleMuteToggle = useCallback15(
6520
+ (trackId) => {
6521
+ const track = tracks.find((t) => t.handle.id === trackId);
6522
+ if (!track) return;
6523
+ const newMuted = !track.runtimeState.muted;
6524
+ setTracks(
6525
+ (prev) => prev.map(
6526
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: newMuted } } : t
6527
+ )
6528
+ );
6529
+ host.setTrackMute(trackId, newMuted).catch(() => {
6530
+ setTracks(
6531
+ (prev) => prev.map(
6532
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, muted: !newMuted } } : t
6533
+ )
6534
+ );
6535
+ });
6536
+ },
6537
+ [host, tracks]
6538
+ );
6539
+ const handleSoloToggle = useCallback15(
6540
+ (trackId) => {
6541
+ const track = tracks.find((t) => t.handle.id === trackId);
6542
+ if (!track) return;
6543
+ const newSolo = !track.runtimeState.solo;
6544
+ setTracks(
6545
+ (prev) => prev.map(
6546
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: newSolo } } : t
6547
+ )
6548
+ );
6549
+ host.setTrackSolo(trackId, newSolo).catch(() => {
6550
+ setTracks(
6551
+ (prev) => prev.map(
6552
+ (t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, solo: !newSolo } } : t
6553
+ )
6554
+ );
6555
+ });
6556
+ },
6557
+ [host, tracks]
6558
+ );
6559
+ const handleVolumeChange = useCallback15(
6560
+ (trackId, volume) => {
6561
+ setTracks(
6562
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
6563
+ );
6564
+ host.setTrackVolume(trackId, volume).catch(() => {
6565
+ });
6566
+ },
6567
+ [host]
6568
+ );
6569
+ const handlePanChange = useCallback15(
6570
+ (trackId, pan) => {
6571
+ setTracks(
6572
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
6573
+ );
6574
+ host.setTrackPan(trackId, pan).catch(() => {
6575
+ });
6576
+ },
6577
+ [host]
6578
+ );
6579
+ const handleShuffle = useCallback15(
6580
+ async (trackId) => {
6581
+ const track = tracks.find((t) => t.handle.id === trackId);
6582
+ if (!track) return;
6583
+ if (soundHistory.list(trackId).entries.length === 0) {
6584
+ try {
6585
+ const cap = await adapter.sound.captureSoundDescriptor(trackId);
6586
+ if (cap) soundHistory.record(trackId, cap.descriptor, adapter.sound.previousSoundLabel);
6587
+ } catch {
6588
+ }
6589
+ }
6590
+ try {
6591
+ let result;
6592
+ let nextHistory;
6593
+ try {
6594
+ result = await adapter.shuffle.shuffle(track, Array.from(track.shuffleHistory));
6595
+ nextHistory = new Set(track.shuffleHistory);
6596
+ } catch (firstErr) {
6597
+ if (adapter.shuffle.isExhaustedError(firstErr)) {
6598
+ nextHistory = /* @__PURE__ */ new Set();
6599
+ result = await adapter.shuffle.shuffle(track, []);
6600
+ } else {
6601
+ throw firstErr;
6602
+ }
6603
+ }
6604
+ nextHistory.add(result.appliedName);
6605
+ setTracks(
6606
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, shuffleHistory: nextHistory } : t)
6607
+ );
6608
+ try {
6609
+ const cap = await adapter.sound.captureSoundDescriptor(trackId);
6610
+ if (cap) soundHistory.record(trackId, cap.descriptor, result.appliedName);
6611
+ } catch {
6612
+ }
6613
+ console.log(`[${logTag}] Sound shuffled: ${result.appliedName} (history ${nextHistory.size})`);
6614
+ } catch (error) {
6615
+ const msg = error instanceof Error ? error.message : "Shuffle failed";
6616
+ host.showToast("error", "Shuffle failed", msg);
6617
+ }
6618
+ },
6619
+ [host, adapter, tracks, soundHistory, logTag]
6620
+ );
6621
+ const handleCopy = useCallback15(
6622
+ async (trackId) => {
6623
+ try {
6624
+ const newHandle = await host.duplicateTrack(trackId);
6625
+ await loadTracks();
6626
+ host.showToast("success", "Track duplicated", newHandle.name);
6627
+ } catch (error) {
6628
+ const msg = error instanceof Error ? error.message : "Copy failed";
6629
+ host.showToast("error", "Copy failed", msg);
6630
+ }
6631
+ },
6632
+ [host, loadTracks]
6633
+ );
6634
+ const handleFxToggle = useCallback15(
6635
+ (trackId, category, enabled) => {
6636
+ setTracks(
6637
+ (prev) => prev.map(
6638
+ (t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], enabled } } } : t
6639
+ )
6640
+ );
6641
+ host.toggleTrackFx(trackId, category, enabled).catch(() => {
6642
+ setTracks(
6643
+ (prev) => prev.map(
6644
+ (t) => t.handle.id === trackId ? {
6645
+ ...t,
6646
+ fxDetailState: {
6647
+ ...t.fxDetailState,
6648
+ [category]: { ...t.fxDetailState[category], enabled: !enabled }
6649
+ }
6650
+ } : t
6651
+ )
6652
+ );
6653
+ });
6654
+ },
6655
+ [host]
6656
+ );
6657
+ const handleFxPresetChange = useCallback15(
6658
+ (trackId, category, presetIndex) => {
6659
+ setTracks(
6660
+ (prev) => prev.map(
6661
+ (t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], presetIndex } } } : t
6662
+ )
6663
+ );
6664
+ host.setTrackFxPreset(trackId, category, presetIndex).then((result) => {
6665
+ if (result.dryWet !== void 0) {
6666
+ setTracks(
6667
+ (prev) => prev.map(
6668
+ (t) => t.handle.id === trackId ? {
6669
+ ...t,
6670
+ fxDetailState: {
6671
+ ...t.fxDetailState,
6672
+ [category]: { ...t.fxDetailState[category], dryWet: result.dryWet }
6673
+ }
6674
+ } : t
6675
+ )
6676
+ );
6677
+ }
6678
+ }).catch(() => {
6679
+ });
6680
+ },
6681
+ [host]
6682
+ );
6683
+ const handleFxDryWetChange = useCallback15(
6684
+ (trackId, category, value) => {
6685
+ setTracks(
6686
+ (prev) => prev.map(
6687
+ (t) => t.handle.id === trackId ? { ...t, fxDetailState: { ...t.fxDetailState, [category]: { ...t.fxDetailState[category], dryWet: value } } } : t
6688
+ )
6689
+ );
6690
+ host.setTrackFxDryWet(trackId, category, value).catch(() => {
6691
+ });
6692
+ },
6693
+ [host]
6694
+ );
6695
+ const toggleFxDrawer = useCallback15(
6696
+ (trackId) => {
6697
+ setTracks(
6698
+ (prev) => prev.map((t) => {
6699
+ if (t.handle.id !== trackId) return t;
6700
+ const onFx = t.drawerOpen && t.drawerTab === "fx";
6701
+ return { ...t, drawerOpen: !onFx, drawerTab: "fx", editorStage: false };
6702
+ })
6703
+ );
6704
+ const track = tracks.find((t) => t.handle.id === trackId);
6705
+ const wasOnFx = !!track && track.drawerOpen && track.drawerTab === "fx";
6706
+ if (track && !wasOnFx) {
6707
+ host.getTrackFxState(trackId).then((fxState) => {
6708
+ setTracks(
6709
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
6710
+ );
6711
+ }).catch(() => {
6712
+ });
6713
+ }
6714
+ },
6715
+ [host, tracks]
6716
+ );
6717
+ const loadEditNotes = useCallback15(
6718
+ async (trackId) => {
6719
+ try {
6720
+ const mc = await host.getMusicalContext();
6721
+ let notes = [];
6722
+ if (typeof host.readMidiNotes === "function") {
6723
+ const result = await host.readMidiNotes(trackId);
6724
+ notes = result.clips[0]?.notes ?? [];
6725
+ }
6726
+ setTracks(
6727
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes, editBars: mc.bars, editBpm: mc.bpm } : t)
6728
+ );
6729
+ } catch (err) {
6730
+ console.warn(`[${logTag}] Failed to load MIDI for editing:`, err);
6731
+ }
6732
+ },
6733
+ [host, logTag]
6734
+ );
6735
+ const handleNotesChange = useCallback15(
6736
+ (trackId, notes) => {
6737
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
6738
+ const key = `edit:${trackId}`;
6739
+ if (saveTimeoutRefs.current[key]) {
6740
+ clearTimeout(saveTimeoutRefs.current[key]);
6741
+ }
6742
+ saveTimeoutRefs.current[key] = setTimeout(() => {
6743
+ void (async () => {
6744
+ try {
6745
+ if (notes.length === 0) {
6746
+ await host.clearMidi(trackId);
6747
+ } else {
6748
+ const mc = await host.getMusicalContext();
6749
+ await host.writeMidiClip(trackId, {
6750
+ startTime: 0,
6751
+ endTime: mc.bars * 4 * 60 / mc.bpm,
6752
+ tempo: mc.bpm,
6753
+ notes
6754
+ });
6755
+ }
6756
+ } catch (err) {
6757
+ const msg = err instanceof Error ? err.message : String(err);
6758
+ host.showToast("error", "Failed to save edit", msg);
6759
+ }
6760
+ })();
6761
+ }, 300);
6762
+ },
6763
+ [host]
6764
+ );
6765
+ const handleTabChange = useCallback15(
6766
+ (trackId, tab) => {
6767
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
6768
+ if (tab === "fx") {
6769
+ host.getTrackFxState(trackId).then((fxState) => {
6770
+ setTracks(
6771
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, fxDetailState: pluginFxToToggleFx(fxState) } : t)
6772
+ );
6773
+ }).catch(() => {
6774
+ });
6775
+ } else if (tab === "pick" && availableInstruments.length === 0 && !instrumentsLoading) {
6776
+ setInstrumentsLoading(true);
6777
+ host.getAvailableInstruments().then((instruments) => {
6778
+ setAvailableInstruments(instruments);
6779
+ }).catch(() => {
6780
+ }).finally(() => {
6781
+ setInstrumentsLoading(false);
6782
+ });
6783
+ } else if (tab === "edit" && !editLoadStartedRef.current.has(trackId)) {
6784
+ editLoadStartedRef.current.add(trackId);
6785
+ void loadEditNotes(trackId);
6786
+ }
6787
+ },
6788
+ [host, availableInstruments.length, instrumentsLoading, loadEditNotes]
6789
+ );
6790
+ const handleProgressChange = useCallback15((trackId, pct) => {
6791
+ setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
6792
+ }, []);
6793
+ const handleToggleDrawer = useCallback15((trackId) => {
6794
+ setTracks(
6795
+ (prev) => prev.map((t) => {
6796
+ if (t.handle.id !== trackId) return t;
6797
+ const onSound = t.drawerOpen && t.drawerTab !== "fx";
6798
+ return { ...t, drawerOpen: !onSound, drawerTab: "history", editorStage: false };
6799
+ })
6800
+ );
6801
+ }, []);
6802
+ const handleInstrumentSelect = useCallback15(
6803
+ async (trackId, pluginId) => {
6804
+ const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
6805
+ if (isDefaultInstrument) {
6806
+ setTracks(
6807
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: false, editorStage: false } : t)
6808
+ );
6809
+ try {
6810
+ await host.setTrackInstrument(trackId, pluginId);
6811
+ const descriptor = await host.getTrackInstrument(trackId);
6812
+ setTracks(
6813
+ (prev) => prev.map(
6814
+ (t) => t.handle.id === trackId ? {
6815
+ ...t,
6816
+ instrumentPluginId: descriptor?.pluginId ?? null,
6817
+ instrumentName: descriptor?.name ?? null,
6818
+ instrumentMissing: descriptor?.missing ?? false
6819
+ } : t
6820
+ )
6821
+ );
6822
+ } catch (err) {
6823
+ const msg = err instanceof Error ? err.message : "Failed to load instrument";
6824
+ host.showToast("error", "Instrument load failed", msg);
6825
+ }
6826
+ return;
6827
+ }
6828
+ setTracks(
6829
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerTab: "pick", editorStage: true } : t)
6830
+ );
6831
+ try {
6832
+ await host.setTrackInstrument(trackId, pluginId);
6833
+ const descriptor = await host.getTrackInstrument(trackId);
6834
+ setTracks(
6835
+ (prev) => prev.map(
6836
+ (t) => t.handle.id === trackId ? {
6837
+ ...t,
6838
+ instrumentPluginId: descriptor?.pluginId ?? null,
6839
+ instrumentName: descriptor?.name ?? null,
6840
+ instrumentMissing: descriptor?.missing ?? false
6841
+ } : t
6842
+ )
6843
+ );
6844
+ } catch (err) {
6845
+ const msg = err instanceof Error ? err.message : "Failed to load instrument";
6846
+ console.error(`[${logTag}] Failed to set instrument:`, err);
6847
+ host.showToast("error", "Instrument load failed", msg);
6848
+ setTracks(
6849
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
6850
+ );
6851
+ }
6852
+ },
6853
+ [host, identity.defaultInstrumentPluginId, logTag]
6854
+ );
6855
+ const handleShowEditor = useCallback15(
6856
+ async (trackId) => {
6857
+ try {
6858
+ await host.showInstrumentEditor(trackId);
6859
+ } catch (err) {
6860
+ const msg = err instanceof Error ? err.message : "Failed to open editor";
6861
+ host.showToast("error", "Editor failed", msg);
6862
+ }
6863
+ },
6864
+ [host]
6865
+ );
6866
+ const handleBackToInstruments = useCallback15((trackId) => {
6867
+ setTracks(
6868
+ (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
6869
+ );
6870
+ }, []);
6871
+ const handleRefreshInstruments = useCallback15(() => {
6872
+ setInstrumentsLoading(true);
6873
+ host.getAvailableInstruments().then((instruments) => {
6874
+ setAvailableInstruments(instruments);
6875
+ }).catch(() => {
6876
+ }).finally(() => {
6877
+ setInstrumentsLoading(false);
6878
+ });
6879
+ }, [host]);
6880
+ const onAuditionNote = useCallback15(
6881
+ (trackId, pitch, velocity, ms) => {
6882
+ void host.auditionNote(trackId, pitch, velocity, ms);
6883
+ },
6884
+ [host]
6885
+ );
6886
+ const { resolvedCrossfadePairs, crossfadeMemberDbIds } = useMemo9(() => {
6887
+ const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
6888
+ const pairs = [];
6889
+ const members = /* @__PURE__ */ new Set();
6890
+ for (const p of crossfadePairsMeta) {
6891
+ const origin = byDbId.get(p.originDbId);
6892
+ const target = byDbId.get(p.targetDbId);
6893
+ if (origin && target) {
6894
+ pairs.push({ ...p, origin, target });
6895
+ members.add(p.originDbId);
6896
+ members.add(p.targetDbId);
6897
+ }
6898
+ }
6899
+ return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
6900
+ }, [tracks, crossfadePairsMeta]);
6901
+ const { resolvedFades, fadeMemberDbIds } = useMemo9(() => {
6902
+ const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
6903
+ const list = [];
6904
+ const members = /* @__PURE__ */ new Set();
6905
+ for (const f of fadesMeta) {
6906
+ const track = byDbId.get(f.dbId);
6907
+ if (track) {
6908
+ list.push({ ...f, track });
6909
+ members.add(f.dbId);
6910
+ }
6911
+ }
6912
+ return { resolvedFades: list, fadeMemberDbIds: members };
6913
+ }, [tracks, fadesMeta]);
6914
+ const transition = useTransitionOps({
6915
+ host,
6916
+ adapter,
6917
+ activeSceneId,
6918
+ isConnected,
6919
+ isAuthenticated,
6920
+ sceneContext,
6921
+ tracks,
6922
+ setTracks,
6923
+ loadTracks,
6924
+ setCrossfadePairsMeta,
6925
+ setFadesMeta,
6926
+ resolvedCrossfadePairs,
6927
+ resolvedFades
6928
+ });
6929
+ const setGroupMute = useCallback15(
6930
+ (trackIds, muted) => {
6931
+ for (const id of trackIds) {
6932
+ setTracks(
6933
+ (prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, muted } } : t)
6934
+ );
6935
+ host.setTrackMute(id, muted).catch(() => {
6936
+ });
6937
+ }
6938
+ },
6939
+ [host]
6940
+ );
6941
+ const setGroupSolo = useCallback15(
6942
+ (trackIds, solo) => {
6943
+ for (const id of trackIds) {
6944
+ setTracks(
6945
+ (prev) => prev.map((t) => t.handle.id === id ? { ...t, runtimeState: { ...t.runtimeState, solo } } : t)
6946
+ );
6947
+ host.setTrackSolo(id, solo).catch(() => {
6948
+ });
6949
+ }
6950
+ },
6951
+ [host]
6952
+ );
6953
+ const deleteGroup = useCallback15(
6954
+ async (members, cleanupKeySuffixes) => {
6955
+ for (const member of members) {
6956
+ try {
6957
+ await host.deleteTrack(member.engineId);
6958
+ } catch {
6959
+ }
6960
+ if (activeSceneId) {
6961
+ for (const suffix of cleanupKeySuffixes) {
6962
+ await host.deleteSceneData(activeSceneId, trackDataKey(member.dbId, suffix)).catch(() => {
6963
+ });
6964
+ }
6965
+ }
6966
+ }
6967
+ const gone = new Set(members.map((m) => m.engineId));
6968
+ setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
6969
+ await loadTracks(true);
6970
+ },
6971
+ [host, activeSceneId, loadTracks]
6972
+ );
6973
+ const handlers = useMemo9(
6974
+ () => ({
6975
+ promptChange: handlePromptChange,
6976
+ generate: (trackId) => {
6977
+ void handleGenerate(trackId);
6978
+ },
6979
+ shuffle: (trackId) => {
6980
+ void handleShuffle(trackId);
6981
+ },
6982
+ copy: (trackId) => {
6983
+ void handleCopy(trackId);
6984
+ },
6985
+ delete: (trackId) => {
6986
+ void handleDeleteTrack(trackId);
6987
+ },
6988
+ muteToggle: handleMuteToggle,
6989
+ soloToggle: handleSoloToggle,
6990
+ volumeChange: handleVolumeChange,
6991
+ panChange: handlePanChange,
6992
+ tabChange: handleTabChange,
6993
+ toggleDrawer: handleToggleDrawer,
6994
+ toggleFxDrawer,
6995
+ notesChange: handleNotesChange,
6996
+ progressChange: handleProgressChange
6997
+ }),
6998
+ [
6999
+ handlePromptChange,
7000
+ handleGenerate,
7001
+ handleShuffle,
7002
+ handleCopy,
7003
+ handleDeleteTrack,
7004
+ handleMuteToggle,
7005
+ handleSoloToggle,
7006
+ handleVolumeChange,
7007
+ handlePanChange,
7008
+ handleTabChange,
7009
+ handleToggleDrawer,
7010
+ toggleFxDrawer,
7011
+ handleNotesChange,
7012
+ handleProgressChange
7013
+ ]
7014
+ );
7015
+ return {
7016
+ ui,
7017
+ adapter,
7018
+ tracks,
7019
+ setTracks,
7020
+ isLoadingTracks,
7021
+ loadTracks,
7022
+ engineToDbId,
7023
+ supportsMeters,
7024
+ trackLevels,
7025
+ anySolo,
7026
+ reorder,
7027
+ soundHistory,
7028
+ isComposing,
7029
+ placeholders,
7030
+ isAddingTrack,
7031
+ isExportingMidi,
7032
+ designerView,
7033
+ canCrossfade,
7034
+ needsContract,
7035
+ xfFromId,
7036
+ xfToId,
7037
+ importOpen,
7038
+ setImportOpen,
7039
+ soundImportTarget,
7040
+ setSoundImportTarget,
7041
+ handleSoundImportPick,
7042
+ handlePortTrack,
7043
+ transition,
7044
+ crossfadePairsMeta,
7045
+ fadesMeta,
7046
+ resolvedCrossfadePairs,
7047
+ crossfadeMemberDbIds,
7048
+ resolvedFades,
7049
+ fadeMemberDbIds,
7050
+ resolvedGenericGroups,
7051
+ genericGroupMemberDbIds,
7052
+ availableInstruments,
7053
+ instrumentsLoading,
7054
+ handlers,
7055
+ handleGenerate,
7056
+ handleShuffle,
7057
+ handleAddTrack,
7058
+ handleDeleteTrack,
7059
+ handleExportMidi,
7060
+ handlePromptChange,
7061
+ handleMuteToggle,
7062
+ handleSoloToggle,
7063
+ handleVolumeChange,
7064
+ handlePanChange,
7065
+ handleTabChange,
7066
+ handleToggleDrawer,
7067
+ toggleFxDrawer,
7068
+ handleNotesChange,
7069
+ handleProgressChange,
7070
+ handleCopy,
7071
+ handleFxToggle,
7072
+ handleFxPresetChange,
7073
+ handleFxDryWetChange,
7074
+ handleInstrumentSelect,
7075
+ handleShowEditor,
7076
+ handleBackToInstruments,
7077
+ handleRefreshInstruments,
7078
+ onAuditionNote,
7079
+ makeServices,
7080
+ setGroupMute,
7081
+ setGroupSolo,
7082
+ deleteGroup
7083
+ };
7084
+ }
7085
+
7086
+ // src/panel-core/GeneratorPanelShell.tsx
7087
+ import React21, { useCallback as useCallback16 } from "react";
7088
+ import { Fragment as Fragment6, jsx as jsx25, jsxs as jsxs19 } from "react/jsx-runtime";
7089
+ function GeneratorPanelShell({ core, slots }) {
7090
+ const {
7091
+ ui,
7092
+ adapter,
7093
+ tracks,
7094
+ isLoadingTracks,
7095
+ supportsMeters,
7096
+ trackLevels,
7097
+ anySolo,
7098
+ reorder,
7099
+ soundHistory,
7100
+ isComposing,
7101
+ placeholders,
7102
+ designerView,
7103
+ canCrossfade,
7104
+ xfFromId,
7105
+ xfToId,
7106
+ importOpen,
7107
+ setImportOpen,
7108
+ soundImportTarget,
7109
+ setSoundImportTarget,
7110
+ handleSoundImportPick,
7111
+ handlePortTrack,
7112
+ transition,
7113
+ crossfadePairsMeta,
7114
+ fadesMeta,
7115
+ resolvedCrossfadePairs,
7116
+ crossfadeMemberDbIds,
7117
+ resolvedFades,
7118
+ fadeMemberDbIds,
7119
+ resolvedGenericGroups,
7120
+ genericGroupMemberDbIds,
7121
+ availableInstruments,
7122
+ instrumentsLoading,
7123
+ handlers,
7124
+ isExportingMidi,
7125
+ handleExportMidi,
7126
+ handleFxToggle,
7127
+ handleFxPresetChange,
7128
+ handleFxDryWetChange,
7129
+ handleInstrumentSelect,
7130
+ handleShowEditor,
7131
+ handleBackToInstruments,
7132
+ handleRefreshInstruments,
7133
+ onAuditionNote,
7134
+ loadTracks,
7135
+ makeServices,
7136
+ setGroupMute,
7137
+ setGroupSolo,
7138
+ deleteGroup
7139
+ } = core;
7140
+ const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
7141
+ const panelBus = usePanelBus(host, activeSceneId);
7142
+ const { identity, features } = adapter;
7143
+ const buildRowProps = useCallback16(
7144
+ (track, drag) => {
7145
+ const id = track.handle.id;
7146
+ const pickerProps = features.instrumentPicker ? {
7147
+ instrumentName: track.instrumentName,
7148
+ instrumentMissing: track.instrumentMissing,
7149
+ onToggleDrawer: () => handlers.toggleDrawer(id),
7150
+ availableInstruments,
7151
+ currentInstrumentPluginId: track.instrumentPluginId,
7152
+ onInstrumentSelect: (pluginId) => handleInstrumentSelect(id, pluginId),
7153
+ instrumentsLoading,
7154
+ onRefreshInstruments: handleRefreshInstruments,
7155
+ editorStage: track.editorStage,
7156
+ onShowEditor: () => handleShowEditor(id),
7157
+ onBackToInstruments: () => handleBackToInstruments(id)
7158
+ } : {};
7159
+ const importSoundProps = features.importTracks ? {
7160
+ onImportSound: () => setSoundImportTarget(track),
7161
+ importSoundLabel: adapter.sound.importSoundLabel
7162
+ } : {};
7163
+ const props = {
7164
+ ...drag ? { drag } : {},
7165
+ track: { id, name: track.handle.name, role: track.role },
7166
+ levels: supportsMeters ? trackLevels : void 0,
7167
+ prompt: track.prompt,
7168
+ runtimeState: {
7169
+ muted: track.runtimeState.muted,
7170
+ solo: track.runtimeState.solo,
7171
+ volume: track.runtimeState.volume,
7172
+ pan: track.runtimeState.pan
7173
+ },
7174
+ soloedOut: anySolo && !track.runtimeState.solo,
7175
+ fxDetailState: track.fxDetailState,
7176
+ drawerOpen: track.drawerOpen,
7177
+ drawerTab: track.drawerTab,
7178
+ onTabChange: (tab) => handlers.tabChange(id, tab),
7179
+ isGenerating: track.isGenerating,
7180
+ isAuthenticated,
7181
+ error: track.error,
7182
+ hasMidi: track.hasMidi,
7183
+ generationProgress: track.generationProgress,
7184
+ estimatedGenerationMs: identity.estimatedGenerationMs,
7185
+ onPromptChange: (prompt) => handlers.promptChange(id, prompt),
7186
+ onGenerate: () => handlers.generate(id),
7187
+ onShuffle: () => handlers.shuffle(id),
7188
+ onCopy: () => handlers.copy(id),
7189
+ onDelete: () => handlers.delete(id),
7190
+ onMuteToggle: () => handlers.muteToggle(id),
7191
+ onSoloToggle: () => handlers.soloToggle(id),
7192
+ onVolumeChange: (vol) => handlers.volumeChange(id, vol),
7193
+ onPanChange: (pan) => handlers.panChange(id, pan),
7194
+ onFxToggle: (cat, enabled) => handleFxToggle(id, cat, enabled),
7195
+ onFxPresetChange: (cat, idx) => handleFxPresetChange(id, cat, idx),
7196
+ onFxDryWetChange: (cat, val) => handleFxDryWetChange(id, cat, val),
7197
+ onToggleFxDrawer: () => handlers.toggleFxDrawer(id),
7198
+ onProgressChange: (pct) => handlers.progressChange(id, pct),
7199
+ accentColor: identity.accentColor,
7200
+ ...pickerProps,
7201
+ soundHistory: soundHistory.list(id).entries,
7202
+ soundHistoryCursor: soundHistory.list(id).cursor,
7203
+ onRestoreSound: (i) => {
7204
+ void soundHistory.restoreTo(id, i);
7205
+ },
7206
+ onToggleFavorite: (i) => soundHistory.toggleFavorite(id, i),
7207
+ ...importSoundProps,
7208
+ editNotes: track.editNotes,
7209
+ onNotesChange: (notes) => handlers.notesChange(id, notes),
7210
+ editBars: track.editBars,
7211
+ editBpm: track.editBpm,
7212
+ editSnap: 0.25,
7213
+ onAuditionNote: (pitch, vel, ms) => onAuditionNote(id, pitch, vel, ms)
7214
+ };
7215
+ return adapter.mapTrackRowProps ? adapter.mapTrackRowProps(track, props) : props;
7216
+ },
7217
+ [
7218
+ features.instrumentPicker,
7219
+ features.importTracks,
7220
+ adapter,
7221
+ supportsMeters,
7222
+ trackLevels,
7223
+ anySolo,
7224
+ isAuthenticated,
7225
+ identity,
7226
+ handlers,
7227
+ availableInstruments,
7228
+ instrumentsLoading,
7229
+ handleInstrumentSelect,
7230
+ handleRefreshInstruments,
7231
+ handleShowEditor,
7232
+ handleBackToInstruments,
7233
+ setSoundImportTarget,
7234
+ soundHistory,
7235
+ handleFxToggle,
7236
+ handleFxPresetChange,
7237
+ handleFxDryWetChange,
7238
+ onAuditionNote
7239
+ ]
7240
+ );
7241
+ if (!activeSceneId) {
7242
+ return /* @__PURE__ */ jsx25(
7243
+ "div",
7244
+ {
7245
+ "data-testid": `no-scene-placeholder-${identity.familyKey}`,
7246
+ className: "flex items-center justify-center py-8",
7247
+ children: /* @__PURE__ */ jsx25(
7248
+ "button",
7249
+ {
7250
+ onClick: () => onSelectScene?.(),
7251
+ className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
7252
+ children: "Select a Scene"
7253
+ }
7254
+ )
7255
+ }
7256
+ );
7257
+ }
7258
+ if (!sceneContext?.hasContract) {
7259
+ return /* @__PURE__ */ jsx25(
7260
+ "div",
7261
+ {
7262
+ "data-testid": `no-contract-placeholder-${identity.familyKey}`,
7263
+ className: "flex items-center justify-center py-8",
7264
+ children: /* @__PURE__ */ jsx25(
7265
+ "button",
7266
+ {
7267
+ onClick: () => onOpenContract?.(),
7268
+ className: "text-sas-muted text-xs hover:text-sas-accent transition-colors underline underline-offset-2",
7269
+ children: "Generate a Contract"
7270
+ }
7271
+ )
7272
+ }
7273
+ );
7274
+ }
7275
+ if (features.bulkComposePlaceholders && isComposing) {
7276
+ return /* @__PURE__ */ jsx25("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ jsx25(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
7277
+ }
7278
+ const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
7279
+ if (activePlaceholders.length > 0) {
7280
+ const tracksByDbId = /* @__PURE__ */ new Map();
7281
+ for (const t of tracks) {
7282
+ tracksByDbId.set(t.handle.dbId, t);
7283
+ if (t.handle.id !== t.handle.dbId) {
7284
+ tracksByDbId.set(t.handle.id, t);
7285
+ }
7286
+ }
7287
+ return /* @__PURE__ */ jsx25("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
7288
+ const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
7289
+ if (loadedTrack) {
7290
+ return /* @__PURE__ */ jsx25(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
7291
+ }
7292
+ return /* @__PURE__ */ jsx25(
7293
+ "div",
7294
+ {
7295
+ "data-testid": "bulk-placeholder-track",
7296
+ className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
7297
+ style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
7298
+ children: /* @__PURE__ */ jsx25(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
7299
+ },
7300
+ ph.id
7301
+ );
7302
+ }) });
7303
+ }
7304
+ const groupCtx = {
7305
+ services: makeServices(),
7306
+ anySolo,
7307
+ supportsMeters,
7308
+ levels: supportsMeters ? trackLevels : void 0,
7309
+ handlers,
7310
+ renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ jsx25(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
7311
+ setGroupMute,
7312
+ setGroupSolo,
7313
+ deleteGroup
7314
+ };
7315
+ return /* @__PURE__ */ jsxs19("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
7316
+ features.importTracks && host.listImportableTracks && /* @__PURE__ */ jsx25(
7317
+ ImportTrackModal,
7318
+ {
7319
+ host,
7320
+ open: importOpen,
7321
+ onClose: () => setImportOpen(false),
7322
+ onImported: () => {
7323
+ void loadTracks(true);
7324
+ },
7325
+ onPortTrack: host.readImportableTrackMidi ? handlePortTrack : void 0,
7326
+ testIdPrefix: `${identity.familyKey}-import`
7327
+ }
7328
+ ),
7329
+ features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ jsx25(
7330
+ ImportTrackModal,
7331
+ {
7332
+ host,
7333
+ mode: "sound",
7334
+ open: !!soundImportTarget,
7335
+ title: adapter.sound.importSoundLabel,
7336
+ onClose: () => setSoundImportTarget(null),
7337
+ onImported: () => {
7338
+ },
7339
+ onPick: handleSoundImportPick,
7340
+ testIdPrefix: `${identity.familyKey}-sound-import`
7341
+ }
7342
+ ),
7343
+ slots?.modals,
7344
+ canCrossfade && xfFromId && xfToId && /* @__PURE__ */ jsx25("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ jsx25(
7345
+ TransitionDesigner,
7346
+ {
7347
+ host,
7348
+ fromSceneId: xfFromId,
7349
+ toSceneId: xfToId,
7350
+ transitionSceneId: activeSceneId ?? "",
7351
+ excludeSourceDbIds: [
7352
+ ...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
7353
+ ...fadesMeta.map((f) => f.meta.sourceTrackDbId)
7354
+ ],
7355
+ onCreateCrossfade: transition.handleCreateCrossfade,
7356
+ onCreateFade: transition.handleCreateFade,
7357
+ familyLabel: identity.familyLabel,
7358
+ testIdPrefix: `${identity.familyKey}-transition-designer`
7359
+ }
7360
+ ) }),
7361
+ !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ jsx25("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ jsxs19(Fragment6, { children: [
7362
+ panelBus.supported && panelBus.bus && /* @__PURE__ */ jsx25(
7363
+ PanelMasterStrip,
7364
+ {
7365
+ bus: panelBus.bus,
7366
+ availableFx: panelBus.availableFx,
7367
+ fxLoading: panelBus.fxLoading,
7368
+ soloedOut: anySolo && !panelBus.bus.soloed,
7369
+ fxPickerOpen: panelBus.fxPickerOpen,
7370
+ onToggleFxPicker: panelBus.setFxPickerOpen,
7371
+ onRefreshFx: panelBus.refreshFx,
7372
+ onVolumeChange: panelBus.onVolumeChange,
7373
+ onMuteToggle: panelBus.onMuteToggle,
7374
+ onSoloToggle: panelBus.onSoloToggle,
7375
+ onAddFx: panelBus.onAddFx,
7376
+ onRemoveFx: panelBus.onRemoveFx,
7377
+ onToggleFxEnabled: panelBus.onToggleFxEnabled,
7378
+ onShowFxEditor: panelBus.onShowFxEditor
7379
+ }
7380
+ ),
7381
+ slots?.beforeRows,
7382
+ resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ jsx25(
7383
+ CrossfadeTrackRow,
7384
+ {
7385
+ accentColor: identity.transitionAccentColor,
7386
+ levels: supportsMeters ? trackLevels : void 0,
7387
+ sliderPos: pair.sliderPos,
7388
+ origin: {
7389
+ trackId: pair.origin.handle.id,
7390
+ name: pair.origin.handle.name,
7391
+ role: pair.origin.role,
7392
+ sourceName: pair.originSourceName,
7393
+ soundLabel: pair.originSoundLabel,
7394
+ runtimeState: pair.origin.runtimeState
7395
+ },
7396
+ target: {
7397
+ trackId: pair.target.handle.id,
7398
+ name: pair.target.handle.name,
7399
+ role: pair.target.role,
7400
+ sourceName: pair.targetSourceName,
7401
+ soundLabel: pair.targetSoundLabel,
7402
+ runtimeState: pair.target.runtimeState
7403
+ },
7404
+ onMuteToggle: () => transition.handleCrossfadeMute(pair),
7405
+ onSoloToggle: () => transition.handleCrossfadeSolo(pair),
7406
+ onVolumeChange: (slot, vol) => handlers.volumeChange(
7407
+ slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
7408
+ vol
7409
+ ),
7410
+ onPanChange: (slot, pan) => handlers.panChange(
7411
+ slot === "origin" ? pair.origin.handle.id : pair.target.handle.id,
7412
+ pan
7413
+ ),
7414
+ onSliderChange: (pos) => transition.handleCrossfadeSlider(pair, pos),
7415
+ onDelete: () => transition.handleCrossfadeDelete(pair)
7416
+ },
7417
+ pair.groupId
7418
+ )),
7419
+ resolvedFades.map((fade) => /* @__PURE__ */ jsx25(
7420
+ FadeTrackRow,
7421
+ {
7422
+ accentColor: identity.transitionAccentColor,
7423
+ levels: supportsMeters ? trackLevels : void 0,
7424
+ direction: fade.meta.direction,
7425
+ gesture: fade.meta.gesture,
7426
+ sliderPos: fade.meta.sliderPos,
7427
+ layer: {
7428
+ trackId: fade.track.handle.id,
7429
+ name: fade.track.handle.name,
7430
+ role: fade.track.role,
7431
+ sourceName: fade.meta.sourceName,
7432
+ soundLabel: fade.meta.soundLabel,
7433
+ runtimeState: fade.track.runtimeState
7434
+ },
7435
+ onMuteToggle: () => handlers.muteToggle(fade.track.handle.id),
7436
+ onSoloToggle: () => handlers.soloToggle(fade.track.handle.id),
7437
+ onVolumeChange: (vol) => handlers.volumeChange(fade.track.handle.id, vol),
7438
+ onPanChange: (pan) => handlers.panChange(fade.track.handle.id, pan),
7439
+ onSliderChange: (pos) => transition.handleFadeSlider(fade, pos),
7440
+ onDelete: () => transition.handleFadeDelete(fade)
7441
+ },
7442
+ fade.dbId
7443
+ )),
7444
+ (adapter.groupExtensions ?? []).flatMap(
7445
+ (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ jsx25(React21.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
7446
+ ),
7447
+ tracks.map((track, index) => {
7448
+ if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
7449
+ return null;
7450
+ }
7451
+ return /* @__PURE__ */ jsx25(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
7452
+ }),
7453
+ slots?.afterRows
7454
+ ] })),
7455
+ features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
7456
+ const hasAnyMidi = tracks.some((t) => t.hasMidi);
7457
+ const exportDisabled = isExportingMidi || !hasAnyMidi;
7458
+ return /* @__PURE__ */ jsx25("div", { className: "pt-2", children: /* @__PURE__ */ jsx25(
7459
+ "button",
7460
+ {
7461
+ "data-testid": "export-midi-tracks-button",
7462
+ onClick: handleExportMidi,
7463
+ disabled: exportDisabled,
7464
+ title: isExportingMidi ? "Exporting..." : !hasAnyMidi ? "Generate MIDI on at least one track first" : "Export all tracks as a ZIP of .mid files",
7465
+ className: `w-full px-2 py-1.5 text-[10px] uppercase tracking-wide rounded-sm border transition-colors ${exportDisabled ? "text-sas-muted/40 border-transparent hover:border-sas-accent cursor-not-allowed" : "text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent"}`,
7466
+ children: isExportingMidi ? "Exporting..." : "Export Tracks"
7467
+ }
7468
+ ) });
7469
+ })()
7470
+ ] });
7471
+ }
7472
+
7473
+ // src/panel-core/surge-sound-adapter.ts
7474
+ async function getInstrument(host, trackId) {
7475
+ try {
7476
+ const plugins = await host.getTrackPlugins(trackId);
7477
+ const instrument = plugins.find(
7478
+ (p) => !p.name.includes("Volume") && !p.name.includes("Pan") && !p.name.includes("Level")
7479
+ );
7480
+ if (!instrument) return null;
7481
+ return { index: instrument.index, isRaw: !instrument.name.includes("Surge") };
7482
+ } catch {
7483
+ return null;
7484
+ }
7485
+ }
7486
+ function createSurgeSoundAdapter(host, overrides = {}) {
7487
+ const applySound = async (trackId, descriptor) => {
7488
+ const { state, stateType } = descriptor;
7489
+ const inst = await getInstrument(host, trackId);
7490
+ if (!inst) return;
7491
+ if (stateType === "raw") await host.setRawPluginState(trackId, inst.index, state);
7492
+ else await host.setPluginState(trackId, inst.index, state);
7493
+ };
7494
+ return {
7495
+ applySound,
7496
+ captureSoundDescriptor: async (trackId) => {
7497
+ const inst = await getInstrument(host, trackId);
7498
+ if (!inst) return null;
7499
+ const state = inst.isRaw ? await host.getRawPluginState(trackId, inst.index) : await host.getPluginState(trackId, inst.index);
7500
+ return { descriptor: { state, stateType: inst.isRaw ? "raw" : "valuetree" } };
7501
+ },
7502
+ copySnapshot: async (trackId, snap) => {
7503
+ if (snap.kind !== "preset") return "default";
7504
+ await applySound(trackId, { state: snap.state, stateType: snap.stateType });
7505
+ await host.persistTrackPresetState?.(trackId, {
7506
+ state: snap.state,
7507
+ stateType: snap.stateType ?? "valuetree",
7508
+ name: snap.label
7509
+ }).catch(() => {
7510
+ });
7511
+ return snap.label;
7512
+ },
7513
+ descriptorFromSnapshot: (snap) => {
7514
+ const preset = snap;
7515
+ return { state: preset.state, stateType: preset.stateType };
7516
+ },
7517
+ acceptedSnapshotKind: "preset",
7518
+ historyMax: overrides.historyMax ?? 12,
7519
+ importSoundLabel: overrides.importSoundLabel ?? "Import Preset",
7520
+ importNoun: "preset",
7521
+ previousSoundLabel: "Previous preset"
7522
+ };
7523
+ }
7524
+
4917
7525
  // src/constants/sdk-version.ts
4918
- var PLUGIN_SDK_VERSION = "2.34.0";
7526
+ var PLUGIN_SDK_VERSION = "2.37.0";
4919
7527
 
4920
7528
  // src/utils/format-concurrent-tracks.ts
4921
7529
  function formatConcurrentTracks(ctx) {
@@ -5081,6 +7689,7 @@ export {
5081
7689
  FadeTrackRow,
5082
7690
  FxToggleBar,
5083
7691
  GUTTER_W,
7692
+ GeneratorPanelShell,
5084
7693
  ImportTrackModal,
5085
7694
  TrackDrawer as InstrumentDrawer,
5086
7695
  LevelMeter,
@@ -5089,6 +7698,7 @@ export {
5089
7698
  PLUGIN_SDK_VERSION,
5090
7699
  PX_PER_BEAT,
5091
7700
  PanSlider,
7701
+ PanelMasterStrip,
5092
7702
  PianoRollEditor,
5093
7703
  PluginError,
5094
7704
  RESIZE_HANDLE_PX,
@@ -5118,6 +7728,7 @@ export {
5118
7728
  cellToPx,
5119
7729
  centerScrollTop,
5120
7730
  computePeaks,
7731
+ createSurgeSoundAdapter,
5121
7732
  dbIdsFromKeys,
5122
7733
  dbToSlider,
5123
7734
  defaultFadeGesture,
@@ -5125,16 +7736,21 @@ export {
5125
7736
  formatConcurrentTracks,
5126
7737
  hashString,
5127
7738
  moveItem,
7739
+ newTrackState,
5128
7740
  normalizeSlots,
5129
7741
  padPair,
5130
7742
  padSlots,
5131
7743
  parseCrossfadePairs,
5132
7744
  parseFades,
7745
+ parseLLMNoteResponse,
7746
+ parseTrackGroups,
5133
7747
  pickTopKWeighted,
5134
7748
  pitchToName,
7749
+ pluginFxToToggleFx,
5135
7750
  pxToCell,
5136
7751
  reconcileSlots,
5137
7752
  resizeNoteDuration,
7753
+ resolveTrackGroups,
5138
7754
  rowKey,
5139
7755
  rowType,
5140
7756
  scorePromptMatch,
@@ -5143,14 +7759,18 @@ export {
5143
7759
  soundIdentity,
5144
7760
  synthesizeCuePoints,
5145
7761
  tokenizePrompt,
7762
+ trackDataKey,
5146
7763
  transposeNotes,
5147
7764
  useAnySolo,
7765
+ useGeneratorPanelCore,
7766
+ usePanelBus,
5148
7767
  useSceneState,
5149
7768
  useSoundHistory,
5150
7769
  useTrackLevel,
5151
7770
  useTrackLevels,
5152
7771
  useTrackMeter,
5153
7772
  useTrackReorder,
7773
+ useTransitionOps,
5154
7774
  useTransportPlaying
5155
7775
  };
5156
7776
  //# sourceMappingURL=index.mjs.map