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