@signalsandsorcery/plugin-sdk 2.35.4 → 2.35.6

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
@@ -43,6 +43,9 @@ __export(index_exports, {
43
43
  DownloadPackButton: () => DownloadPackButton,
44
44
  EMPTY_FX_DETAIL_STATE: () => EMPTY_FX_DETAIL_STATE,
45
45
  EMPTY_FX_STATE: () => EMPTY_FX_STATE,
46
+ ENSEMBLE_MAX_VOICES: () => ENSEMBLE_MAX_VOICES,
47
+ ENSEMBLE_MIN_VOICES: () => ENSEMBLE_MIN_VOICES,
48
+ ENSEMBLE_STYLES: () => ENSEMBLE_STYLES,
46
49
  EQUAL_POWER_GAIN: () => EQUAL_POWER_GAIN,
47
50
  FX_CATEGORIES: () => FX_CATEGORIES,
48
51
  FX_CHAIN_ORDER: () => FX_CHAIN_ORDER,
@@ -54,9 +57,11 @@ __export(index_exports, {
54
57
  FxToggleBar: () => FxToggleBar,
55
58
  GUTTER_W: () => GUTTER_W,
56
59
  GeneratorPanelShell: () => GeneratorPanelShell,
60
+ GroupFadeTrackRow: () => GroupFadeTrackRow,
57
61
  ImportTrackModal: () => ImportTrackModal,
58
62
  InstrumentDrawer: () => TrackDrawer,
59
63
  LevelMeter: () => LevelMeter,
64
+ MIN_NOTE_DURATION_BEATS: () => MIN_NOTE_DURATION_BEATS,
60
65
  Modal: () => Modal,
61
66
  OffsetScrubber: () => OffsetScrubber,
62
67
  PLUGIN_SDK_VERSION: () => PLUGIN_SDK_VERSION,
@@ -68,6 +73,8 @@ __export(index_exports, {
68
73
  RESIZE_HANDLE_PX: () => RESIZE_HANDLE_PX,
69
74
  ROW_HEIGHT: () => ROW_HEIGHT,
70
75
  SLIDER_UNITY: () => SLIDER_UNITY,
76
+ STYLE_RULES: () => STYLE_RULES,
77
+ SUBMIT_ENSEMBLE_TOOL_NAME: () => SUBMIT_ENSEMBLE_TOOL_NAME,
71
78
  SamplePackCTACard: () => SamplePackCTACard,
72
79
  ScrollingWaveform: () => ScrollingWaveform,
73
80
  SorceryProgressBar: () => SorceryProgressBar,
@@ -80,6 +87,7 @@ __export(index_exports, {
80
87
  TransitionDesigner: () => TransitionDesigner,
81
88
  VolumeSlider: () => VolumeSlider,
82
89
  WaveformView: () => WaveformView,
90
+ analyzeEnsemble: () => analyzeEnsemble,
83
91
  analyzeWavPeak: () => analyzeWavPeak,
84
92
  asAudioEffect: () => asAudioEffect,
85
93
  asCrossfadeMeta: () => asCrossfadeMeta,
@@ -87,8 +95,11 @@ __export(index_exports, {
87
95
  asTransitionDesignerDraft: () => asTransitionDesignerDraft,
88
96
  buildCrossfadeInpaintPrompt: () => buildCrossfadeInpaintPrompt,
89
97
  buildCrossfadeVolumeCurves: () => buildCrossfadeVolumeCurves,
98
+ buildEnsembleSystemPrompt: () => buildEnsembleSystemPrompt,
90
99
  buildFadeVolumeCurve: () => buildFadeVolumeCurve,
91
100
  buildRowSlots: () => buildRowSlots,
101
+ buildSubmitEnsembleParameters: () => buildSubmitEnsembleParameters,
102
+ buildViolationRetrySuffix: () => buildViolationRetrySuffix,
92
103
  calculateTimeBasedTarget: () => calculateTimeBasedTarget,
93
104
  cellToPx: () => cellToPx,
94
105
  centerScrollTop: () => centerScrollTop,
@@ -97,15 +108,21 @@ __export(index_exports, {
97
108
  dbIdsFromKeys: () => dbIdsFromKeys,
98
109
  dbToSlider: () => dbToSlider,
99
110
  defaultFadeGesture: () => defaultFadeGesture,
111
+ defaultVoiceSpecs: () => defaultVoiceSpecs,
112
+ describeViolations: () => describeViolations,
100
113
  drawWaveform: () => drawWaveform,
114
+ enforceVoice: () => enforceVoice,
115
+ foldPitchToRegister: () => foldPitchToRegister,
101
116
  formatConcurrentTracks: () => formatConcurrentTracks,
102
117
  hashString: () => hashString,
103
118
  moveItem: () => moveItem,
119
+ nearestPitchWithPc: () => nearestPitchWithPc,
104
120
  newTrackState: () => newTrackState,
105
121
  normalizeSlots: () => normalizeSlots,
106
122
  padPair: () => padPair,
107
123
  padSlots: () => padSlots,
108
124
  parseCrossfadePairs: () => parseCrossfadePairs,
125
+ parseEnsembleArgs: () => parseEnsembleArgs,
109
126
  parseFades: () => parseFades,
110
127
  parseLLMNoteResponse: () => parseLLMNoteResponse,
111
128
  parseTrackGroups: () => parseTrackGroups,
@@ -122,6 +139,7 @@ __export(index_exports, {
122
139
  sliderToDb: () => sliderToDb,
123
140
  slotsEqual: () => slotsEqual,
124
141
  soundIdentity: () => soundIdentity,
142
+ splitFadeEntries: () => splitFadeEntries,
125
143
  synthesizeCuePoints: () => synthesizeCuePoints,
126
144
  tokenizePrompt: () => tokenizePrompt,
127
145
  trackDataKey: () => trackDataKey,
@@ -2991,7 +3009,10 @@ function asFadeMeta(val) {
2991
3009
  sourceSceneId: typeof m.sourceSceneId === "string" ? m.sourceSceneId : "",
2992
3010
  sourceName: typeof m.sourceName === "string" ? m.sourceName : "",
2993
3011
  soundLabel: typeof m.soundLabel === "string" ? m.soundLabel : "",
2994
- sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5
3012
+ sliderPos: typeof m.sliderPos === "number" ? m.sliderPos : 0.5,
3013
+ groupId: typeof m.groupId === "string" && m.groupId ? m.groupId : void 0,
3014
+ memberIndex: typeof m.memberIndex === "number" ? m.memberIndex : void 0,
3015
+ memberLabel: typeof m.memberLabel === "string" && m.memberLabel ? m.memberLabel : void 0
2995
3016
  };
2996
3017
  }
2997
3018
  function parseFades(sceneData) {
@@ -3005,6 +3026,33 @@ function parseFades(sceneData) {
3005
3026
  }
3006
3027
  return out;
3007
3028
  }
3029
+ function splitFadeEntries(entries) {
3030
+ const singles = [];
3031
+ const byGroup = /* @__PURE__ */ new Map();
3032
+ for (const entry of entries) {
3033
+ const gid = entry.meta.groupId;
3034
+ if (!gid) {
3035
+ singles.push(entry);
3036
+ continue;
3037
+ }
3038
+ const list = byGroup.get(gid) ?? [];
3039
+ list.push(entry);
3040
+ byGroup.set(gid, list);
3041
+ }
3042
+ const groups = [];
3043
+ for (const [groupId, members] of byGroup) {
3044
+ members.sort((a, b) => (a.meta.memberIndex ?? 0) - (b.meta.memberIndex ?? 0));
3045
+ const head = members[0].meta;
3046
+ groups.push({
3047
+ groupId,
3048
+ direction: head.direction,
3049
+ gesture: head.gesture,
3050
+ sliderPos: head.sliderPos,
3051
+ members
3052
+ });
3053
+ }
3054
+ return { singles, groups };
3055
+ }
3008
3056
  function buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture, steps = 32) {
3009
3057
  const durationSeconds = bars * 4 * 60 / Math.max(1, bpm);
3010
3058
  if (gesture === "build") {
@@ -3189,9 +3237,191 @@ function FadeTrackRow({
3189
3237
  );
3190
3238
  }
3191
3239
 
3192
- // src/components/FadeModal.tsx
3193
- var import_react14 = require("react");
3240
+ // src/components/GroupFadeTrackRow.tsx
3241
+ var import_react14 = __toESM(require("react"));
3194
3242
  var import_jsx_runtime15 = require("react/jsx-runtime");
3243
+ function MemberCaption({
3244
+ member,
3245
+ direction
3246
+ }) {
3247
+ const tag = direction === "in" ? "Fade in" : "Fade out";
3248
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-1.5 min-w-0 px-2 py-0.5", children: [
3249
+ member.memberLabel && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[9px] font-bold uppercase tracking-wide text-sas-accent flex-shrink-0", children: member.memberLabel }),
3250
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[11px] text-sas-text truncate", title: member.sourceName ?? member.name, children: member.sourceName ?? member.name }),
3251
+ member.soundLabel && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-[9px] text-sas-muted/60 truncate flex-shrink-0", title: member.soundLabel, children: [
3252
+ "\xB7 ",
3253
+ member.soundLabel
3254
+ ] }),
3255
+ !member.memberLabel && /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("span", { className: "text-[9px] text-sas-muted/50 flex-shrink-0", children: [
3256
+ "\xB7 ",
3257
+ tag
3258
+ ] })
3259
+ ] });
3260
+ }
3261
+ function GroupFadeTrackRow({
3262
+ groupLabel,
3263
+ direction,
3264
+ gesture,
3265
+ sliderPos = 0.5,
3266
+ members,
3267
+ onMemberMuteToggle,
3268
+ onMemberSoloToggle,
3269
+ onMemberVolumeChange,
3270
+ onMemberPanChange,
3271
+ onMuteAll,
3272
+ onSoloAll,
3273
+ onDelete,
3274
+ onSliderChange,
3275
+ levels,
3276
+ accentColor = "#9333EA"
3277
+ }) {
3278
+ const [confirmDelete, setConfirmDelete] = import_react14.default.useState(false);
3279
+ const allMuted = members.length > 0 && members.every((m) => m.runtimeState.muted);
3280
+ const allSolo = members.length > 0 && members.every((m) => m.runtimeState.solo);
3281
+ const badge = direction === "in" ? "\u2197 Fade in" : "\u2198 Fade out";
3282
+ const leftLabel = direction === "in" ? "(silent)" : groupLabel;
3283
+ const rightLabel = direction === "in" ? groupLabel : "(silent)";
3284
+ return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3285
+ "div",
3286
+ {
3287
+ "data-testid": "group-fade-track-row",
3288
+ className: "w-full rounded-sm border border-sas-border bg-sas-panel/40 overflow-hidden",
3289
+ style: { borderLeftColor: accentColor, borderLeftWidth: "3px" },
3290
+ children: [
3291
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center justify-between px-2 py-1 bg-sas-panel-alt/60 gap-2", children: [
3292
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 min-w-0", children: [
3293
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3294
+ "span",
3295
+ {
3296
+ "data-testid": "group-fade-direction-badge",
3297
+ className: "text-[10px] font-bold uppercase tracking-wide flex-shrink-0",
3298
+ style: { color: accentColor },
3299
+ children: badge
3300
+ }
3301
+ ),
3302
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3303
+ "span",
3304
+ {
3305
+ "data-testid": "group-fade-label",
3306
+ className: "text-[11px] text-sas-text truncate",
3307
+ title: `${groupLabel} \xB7 ${gesture}`,
3308
+ children: groupLabel
3309
+ }
3310
+ )
3311
+ ] }),
3312
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-1 flex-shrink-0", children: [
3313
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3314
+ "button",
3315
+ {
3316
+ "data-testid": "group-fade-mute-button",
3317
+ onClick: onMuteAll,
3318
+ className: `px-1.5 py-0.5 text-[9px] font-bold rounded-sm border transition-colors ${allMuted ? "bg-sas-danger/20 border-sas-danger text-sas-danger" : "border-sas-border text-sas-muted hover:text-sas-text"}`,
3319
+ title: allMuted ? "Unmute all members" : "Mute all members",
3320
+ children: "M"
3321
+ }
3322
+ ),
3323
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3324
+ "button",
3325
+ {
3326
+ "data-testid": "group-fade-solo-button",
3327
+ onClick: onSoloAll,
3328
+ className: `px-1.5 py-0.5 text-[9px] font-bold rounded-sm border transition-colors ${allSolo ? "bg-sas-accent/20 border-sas-accent text-sas-accent" : "border-sas-border text-sas-muted hover:text-sas-text"}`,
3329
+ title: allSolo ? "Unsolo all members" : "Solo all members",
3330
+ children: "S"
3331
+ }
3332
+ ),
3333
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3334
+ "button",
3335
+ {
3336
+ "data-testid": "group-fade-delete-button",
3337
+ onClick: () => setConfirmDelete(true),
3338
+ className: "text-sas-danger/70 hover:text-sas-danger px-1 transition-colors text-sm",
3339
+ title: "Delete group fade",
3340
+ "aria-label": "Delete group fade",
3341
+ children: "x"
3342
+ }
3343
+ )
3344
+ ] })
3345
+ ] }),
3346
+ members.map((member) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3347
+ TrackRow,
3348
+ {
3349
+ track: { id: member.trackId, name: "", role: member.role },
3350
+ runtimeState: member.runtimeState,
3351
+ fxDetailState: EMPTY_FX_DETAIL_STATE,
3352
+ drawerOpen: false,
3353
+ drawerTab: "fx",
3354
+ levels,
3355
+ accentColor,
3356
+ contentSlot: /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(MemberCaption, { member, direction }),
3357
+ onMuteToggle: () => onMemberMuteToggle(member.trackId),
3358
+ onSoloToggle: () => onMemberSoloToggle(member.trackId),
3359
+ onVolumeChange: (vol) => onMemberVolumeChange(member.trackId, vol),
3360
+ onPanChange: (pan) => onMemberPanChange(member.trackId, pan)
3361
+ },
3362
+ member.trackId
3363
+ )),
3364
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex items-center gap-2 px-3 py-1.5", "data-testid": "group-fade-slider-row", children: [
3365
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3366
+ "span",
3367
+ {
3368
+ className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] text-right flex-shrink-0",
3369
+ title: leftLabel,
3370
+ children: leftLabel
3371
+ }
3372
+ ),
3373
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3374
+ "input",
3375
+ {
3376
+ type: "range",
3377
+ "data-testid": "group-fade-slider",
3378
+ min: 0,
3379
+ max: 1,
3380
+ step: 0.01,
3381
+ value: sliderPos,
3382
+ disabled: !onSliderChange,
3383
+ onChange: onSliderChange ? (e) => onSliderChange(Number(e.target.value)) : void 0,
3384
+ style: { accentColor },
3385
+ className: "flex-1 disabled:opacity-60 disabled:cursor-not-allowed",
3386
+ "aria-label": "Group fade position"
3387
+ }
3388
+ ),
3389
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3390
+ "span",
3391
+ {
3392
+ className: "text-[9px] text-sas-muted/60 truncate max-w-[70px] flex-shrink-0",
3393
+ title: rightLabel,
3394
+ children: rightLabel
3395
+ }
3396
+ )
3397
+ ] }),
3398
+ /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3399
+ ConfirmDialog,
3400
+ {
3401
+ open: confirmDelete,
3402
+ title: "Delete group fade?",
3403
+ message: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
3404
+ "All ",
3405
+ members.length,
3406
+ " member tracks of this fade will be permanently removed from this scene. This cannot be undone."
3407
+ ] }),
3408
+ confirmLabel: "Delete",
3409
+ onConfirm: () => {
3410
+ setConfirmDelete(false);
3411
+ onDelete();
3412
+ },
3413
+ onCancel: () => setConfirmDelete(false),
3414
+ testIdPrefix: "group-fade-delete-confirm"
3415
+ }
3416
+ )
3417
+ ]
3418
+ }
3419
+ );
3420
+ }
3421
+
3422
+ // src/components/FadeModal.tsx
3423
+ var import_react15 = require("react");
3424
+ var import_jsx_runtime16 = require("react/jsx-runtime");
3195
3425
  function shortId(dbId) {
3196
3426
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3197
3427
  }
@@ -3234,7 +3464,7 @@ function OrphanRow({
3234
3464
  }) {
3235
3465
  const primary = track.prompt?.trim() || track.name;
3236
3466
  const meta = [track.role, shortId(track.dbId), gesture].filter(Boolean).join(" \xB7 ");
3237
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3467
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3238
3468
  "button",
3239
3469
  {
3240
3470
  type: "button",
@@ -3246,8 +3476,8 @@ function OrphanRow({
3246
3476
  disabled,
3247
3477
  className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
3248
3478
  children: [
3249
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3250
- meta && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3479
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3480
+ meta && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3251
3481
  ]
3252
3482
  }
3253
3483
  );
@@ -3264,14 +3494,14 @@ function FadeModal({
3264
3494
  onCreate,
3265
3495
  testIdPrefix = "fade-modal"
3266
3496
  }) {
3267
- const [load, setLoad] = (0, import_react14.useState)({ status: "loading" });
3268
- const [selectedDbId, setSelectedDbId] = (0, import_react14.useState)("");
3269
- const [isCreating, setIsCreating] = (0, import_react14.useState)(false);
3270
- const [error, setError] = (0, import_react14.useState)(null);
3271
- const [fromName, setFromName] = (0, import_react14.useState)(null);
3272
- const [toName, setToName] = (0, import_react14.useState)(null);
3273
- const cancelRef = (0, import_react14.useRef)(null);
3274
- const refresh = (0, import_react14.useCallback)(async () => {
3497
+ const [load, setLoad] = (0, import_react15.useState)({ status: "loading" });
3498
+ const [selectedDbId, setSelectedDbId] = (0, import_react15.useState)("");
3499
+ const [isCreating, setIsCreating] = (0, import_react15.useState)(false);
3500
+ const [error, setError] = (0, import_react15.useState)(null);
3501
+ const [fromName, setFromName] = (0, import_react15.useState)(null);
3502
+ const [toName, setToName] = (0, import_react15.useState)(null);
3503
+ const cancelRef = (0, import_react15.useRef)(null);
3504
+ const refresh = (0, import_react15.useCallback)(async () => {
3275
3505
  if (!host.listSceneFamilyTracks) {
3276
3506
  setLoad({ status: "error", message: "This host does not support fades." });
3277
3507
  return;
@@ -3291,7 +3521,7 @@ function FadeModal({
3291
3521
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
3292
3522
  }
3293
3523
  }, [host, fromSceneId, toSceneId]);
3294
- (0, import_react14.useEffect)(() => {
3524
+ (0, import_react15.useEffect)(() => {
3295
3525
  if (open) {
3296
3526
  setError(null);
3297
3527
  setIsCreating(false);
@@ -3299,29 +3529,29 @@ function FadeModal({
3299
3529
  void refresh();
3300
3530
  }
3301
3531
  }, [open, refresh]);
3302
- const excludeSet = (0, import_react14.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3303
- const { fadeOut, fadeIn } = (0, import_react14.useMemo)(
3532
+ const excludeSet = (0, import_react15.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3533
+ const { fadeOut, fadeIn } = (0, import_react15.useMemo)(
3304
3534
  () => load.status === "ready" ? computeOrphans(load.from, load.to, excludeSet) : { fadeOut: [], fadeIn: [] },
3305
3535
  [load, excludeSet]
3306
3536
  );
3307
- const allOrphans = (0, import_react14.useMemo)(
3537
+ const allOrphans = (0, import_react15.useMemo)(
3308
3538
  () => [
3309
3539
  ...fadeOut.map((t) => ({ track: t, direction: "out" })),
3310
3540
  ...fadeIn.map((t) => ({ track: t, direction: "in" }))
3311
3541
  ],
3312
3542
  [fadeOut, fadeIn]
3313
3543
  );
3314
- (0, import_react14.useEffect)(() => {
3544
+ (0, import_react15.useEffect)(() => {
3315
3545
  if (!allOrphans.some((o) => o.track.dbId === selectedDbId)) {
3316
3546
  setSelectedDbId(allOrphans[0]?.track.dbId ?? "");
3317
3547
  }
3318
3548
  }, [allOrphans, selectedDbId]);
3319
3549
  const selected = allOrphans.find((o) => o.track.dbId === selectedDbId) ?? null;
3320
3550
  const canCreate = !isCreating && !!selected;
3321
- const handleClose = (0, import_react14.useCallback)(() => {
3551
+ const handleClose = (0, import_react15.useCallback)(() => {
3322
3552
  if (!isCreating) onClose();
3323
3553
  }, [isCreating, onClose]);
3324
- const handleCreate = (0, import_react14.useCallback)(async () => {
3554
+ const handleCreate = (0, import_react15.useCallback)(async () => {
3325
3555
  if (!selected) return;
3326
3556
  setIsCreating(true);
3327
3557
  setError(null);
@@ -3342,16 +3572,16 @@ function FadeModal({
3342
3572
  if (!open) return null;
3343
3573
  const renderSection = (heading, list, section) => {
3344
3574
  if (list.length === 0) return null;
3345
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "block", children: [
3346
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3347
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3575
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "block", children: [
3576
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: heading }),
3577
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3348
3578
  "div",
3349
3579
  {
3350
3580
  role: "radiogroup",
3351
3581
  "aria-label": heading,
3352
3582
  "data-testid": `${testIdPrefix}-${section === "out" ? "fade-out" : "fade-in"}-list`,
3353
3583
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3354
- children: list.map((t) => /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3584
+ children: list.map((t) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3355
3585
  OrphanRow,
3356
3586
  {
3357
3587
  track: t,
@@ -3367,32 +3597,32 @@ function FadeModal({
3367
3597
  )
3368
3598
  ] });
3369
3599
  };
3370
- return /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(
3600
+ return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3371
3601
  "div",
3372
3602
  {
3373
3603
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3374
3604
  onClick: (e) => e.stopPropagation(),
3375
3605
  "data-testid": `${testIdPrefix}-box`,
3376
3606
  children: [
3377
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3378
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3607
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add fade" }),
3608
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3379
3609
  "Tracks with no counterpart between",
3380
3610
  " ",
3381
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3611
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3382
3612
  " and",
3383
3613
  " ",
3384
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3614
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3385
3615
  " can gracefully fade out (leaving) or fade in (entering) across this transition."
3386
3616
  ] }),
3387
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3388
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3389
- load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)(import_jsx_runtime15.Fragment, { children: [
3617
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3618
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3619
+ load.status === "ready" && (allOrphans.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", "data-testid": `${testIdPrefix}-empty`, children: "Every track has a counterpart in the other scene \u2014 nothing to fade. Use \u201C+ Crossfade\u201D to bridge matching tracks." }) : /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(import_jsx_runtime16.Fragment, { children: [
3390
3620
  renderSection(`Fade out${fromLabel ? ` (from ${fromLabel})` : ""}`, fadeOut, "out"),
3391
3621
  renderSection(`Fade in${toLabel ? ` (to ${toLabel})` : ""}`, fadeIn, "in")
3392
3622
  ] })),
3393
- error && /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3394
- /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
3395
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3623
+ error && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3624
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
3625
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3396
3626
  "button",
3397
3627
  {
3398
3628
  ref: cancelRef,
@@ -3403,7 +3633,7 @@ function FadeModal({
3403
3633
  children: "Cancel"
3404
3634
  }
3405
3635
  ),
3406
- /* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
3636
+ /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3407
3637
  "button",
3408
3638
  {
3409
3639
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3420,8 +3650,8 @@ function FadeModal({
3420
3650
  }
3421
3651
 
3422
3652
  // src/components/ImportTrackModal.tsx
3423
- var import_react15 = require("react");
3424
- var import_jsx_runtime16 = require("react/jsx-runtime");
3653
+ var import_react16 = require("react");
3654
+ var import_jsx_runtime17 = require("react/jsx-runtime");
3425
3655
  function ImportTrackModal({
3426
3656
  host,
3427
3657
  open,
@@ -3433,10 +3663,10 @@ function ImportTrackModal({
3433
3663
  onPick,
3434
3664
  onPortTrack
3435
3665
  }) {
3436
- const [load, setLoad] = (0, import_react15.useState)({ status: "loading" });
3437
- const [selectedSceneId, setSelectedSceneId] = (0, import_react15.useState)(null);
3438
- const [importingTrackId, setImportingTrackId] = (0, import_react15.useState)(null);
3439
- const refresh = (0, import_react15.useCallback)(async () => {
3666
+ const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
3667
+ const [selectedSceneId, setSelectedSceneId] = (0, import_react16.useState)(null);
3668
+ const [importingTrackId, setImportingTrackId] = (0, import_react16.useState)(null);
3669
+ const refresh = (0, import_react16.useCallback)(async () => {
3440
3670
  if (!host.listImportableTracks) {
3441
3671
  setLoad({ status: "error", message: "This host does not support importing tracks." });
3442
3672
  return;
@@ -3452,14 +3682,14 @@ function ImportTrackModal({
3452
3682
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load scenes." });
3453
3683
  }
3454
3684
  }, [host, mode, onPortTrack]);
3455
- (0, import_react15.useEffect)(() => {
3685
+ (0, import_react16.useEffect)(() => {
3456
3686
  if (open) {
3457
3687
  setSelectedSceneId(null);
3458
3688
  setImportingTrackId(null);
3459
3689
  void refresh();
3460
3690
  }
3461
3691
  }, [open, refresh]);
3462
- const handleImport = (0, import_react15.useCallback)(
3692
+ const handleImport = (0, import_react16.useCallback)(
3463
3693
  async (track, sourceSceneId, sceneName, isSameScene) => {
3464
3694
  if (isSameScene && onPortTrack) {
3465
3695
  if (!track.importable) return;
@@ -3500,16 +3730,16 @@ function ImportTrackModal({
3500
3730
  if (!open) return null;
3501
3731
  const scenes = load.status === "ready" ? load.scenes : [];
3502
3732
  const selectedScene = scenes.find((s) => s.sceneId === selectedSceneId) ?? null;
3503
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3733
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Modal, { open, onClose, testIdPrefix, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3504
3734
  "div",
3505
3735
  {
3506
3736
  className: "w-[420px] max-h-[70vh] overflow-hidden flex flex-col rounded-md border border-sas-border bg-sas-panel shadow-xl",
3507
3737
  onClick: (e) => e.stopPropagation(),
3508
3738
  "data-testid": `${testIdPrefix}-modal`,
3509
3739
  children: [
3510
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3511
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "flex items-center gap-2", children: [
3512
- selectedScene && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3740
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center justify-between px-3 py-2 border-b border-sas-border", children: [
3741
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-2", children: [
3742
+ selectedScene && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3513
3743
  "button",
3514
3744
  {
3515
3745
  className: "text-sas-muted hover:text-sas-accent text-xs",
@@ -3518,9 +3748,9 @@ function ImportTrackModal({
3518
3748
  children: "\u2190"
3519
3749
  }
3520
3750
  ),
3521
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3751
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sm font-medium text-sas-text", children: selectedScene ? selectedScene.sceneName : title })
3522
3752
  ] }),
3523
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)(
3753
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3524
3754
  "button",
3525
3755
  {
3526
3756
  className: "text-sas-muted hover:text-sas-accent text-sm",
@@ -3530,30 +3760,30 @@ function ImportTrackModal({
3530
3760
  }
3531
3761
  )
3532
3762
  ] }),
3533
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("div", { className: "overflow-y-auto p-2 flex-1", children: [
3534
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3535
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3536
- load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
3537
- load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3763
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "overflow-y-auto p-2 flex-1", children: [
3764
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-loading`, children: "Loading scenes\u2026" }),
3765
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "py-8 text-center text-xs text-red-400", "data-testid": `${testIdPrefix}-error`, children: load.message }),
3766
+ load.status === "ready" && scenes.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "py-8 text-center text-xs text-sas-muted", "data-testid": `${testIdPrefix}-empty`, children: mode === "sound" ? "No other scenes have a sound to import." : "No other scenes have a compatible track to import." }),
3767
+ load.status === "ready" && scenes.length > 0 && !selectedScene && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-scene-list`, children: scenes.map((scene) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3538
3768
  "button",
3539
3769
  {
3540
3770
  className: "w-full flex items-center justify-between px-2 py-1.5 rounded-sm border border-sas-border bg-sas-panel-alt text-left text-xs text-sas-text hover:border-sas-accent hover:text-sas-accent transition-colors",
3541
3771
  onClick: () => setSelectedSceneId(scene.sceneId),
3542
3772
  "data-testid": `${testIdPrefix}-scene`,
3543
3773
  children: [
3544
- /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "truncate", children: scene.sceneName }),
3545
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-sas-muted", children: [
3774
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "truncate", children: scene.sceneName }),
3775
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-sas-muted", children: [
3546
3776
  scene.tracks.length,
3547
3777
  " \u2192"
3548
3778
  ] })
3549
3779
  ]
3550
3780
  }
3551
3781
  ) }, scene.sceneId)) }),
3552
- selectedScene && /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3782
+ selectedScene && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("ul", { className: "flex flex-col gap-1", "data-testid": `${testIdPrefix}-track-list`, children: selectedScene.tracks.map((track) => {
3553
3783
  const busy = importingTrackId === track.trackId;
3554
3784
  const gated = mode === "track" && !track.importable;
3555
3785
  const disabled = gated || busy;
3556
- return /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)(
3786
+ return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("li", { children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3557
3787
  "button",
3558
3788
  {
3559
3789
  className: `w-full flex items-center justify-between px-2 py-1.5 rounded-sm border text-left text-xs transition-colors ${disabled ? "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed" : "bg-sas-panel-alt border-sas-border text-sas-text hover:border-sas-accent hover:text-sas-accent"}`,
@@ -3563,14 +3793,14 @@ function ImportTrackModal({
3563
3793
  "data-testid": `${testIdPrefix}-track`,
3564
3794
  "data-importable": mode === "sound" || track.importable ? "true" : "false",
3565
3795
  children: [
3566
- /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "truncate", children: [
3796
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "truncate", children: [
3567
3797
  track.name,
3568
- track.role ? /* @__PURE__ */ (0, import_jsx_runtime16.jsxs)("span", { className: "text-sas-muted", children: [
3798
+ track.role ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-sas-muted", children: [
3569
3799
  " \xB7 ",
3570
3800
  track.role
3571
3801
  ] }) : null
3572
3802
  ] }),
3573
- busy ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ (0, import_jsx_runtime16.jsx)("span", { className: "text-sas-muted", children: "\u2298" }) : null
3803
+ busy ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted", children: "\u2026" }) : gated ? /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-muted", children: "\u2298" }) : null
3574
3804
  ]
3575
3805
  }
3576
3806
  ) }, track.dbId);
@@ -3582,8 +3812,8 @@ function ImportTrackModal({
3582
3812
  }
3583
3813
 
3584
3814
  // src/components/CrossfadeModal.tsx
3585
- var import_react16 = require("react");
3586
- var import_jsx_runtime17 = require("react/jsx-runtime");
3815
+ var import_react17 = require("react");
3816
+ var import_jsx_runtime18 = require("react/jsx-runtime");
3587
3817
  function shortId2(dbId) {
3588
3818
  return dbId.length > 8 ? dbId.slice(0, 8) : dbId;
3589
3819
  }
@@ -3596,7 +3826,7 @@ function CandidateRow({
3596
3826
  }) {
3597
3827
  const primary = track.prompt?.trim() || track.name;
3598
3828
  const meta = [track.role, shortId2(track.dbId)].filter(Boolean).join(" \xB7 ");
3599
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3829
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3600
3830
  "button",
3601
3831
  {
3602
3832
  type: "button",
@@ -3608,8 +3838,8 @@ function CandidateRow({
3608
3838
  disabled,
3609
3839
  className: `w-full text-left px-2 py-1.5 rounded-sm border transition-colors disabled:opacity-50 ${selected ? "bg-sas-accent/15 border-sas-accent" : "bg-sas-panel border-sas-border hover:border-sas-accent/50"}`,
3610
3840
  children: [
3611
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3612
- meta && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3841
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-text truncate", title: primary, children: primary }),
3842
+ meta && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", title: track.dbId, children: meta })
3613
3843
  ]
3614
3844
  }
3615
3845
  );
@@ -3626,15 +3856,15 @@ function CrossfadeModal({
3626
3856
  onCreate,
3627
3857
  testIdPrefix = "crossfade-modal"
3628
3858
  }) {
3629
- const [load, setLoad] = (0, import_react16.useState)({ status: "loading" });
3630
- const [originDbId, setOriginDbId] = (0, import_react16.useState)("");
3631
- const [targetDbId, setTargetDbId] = (0, import_react16.useState)("");
3632
- const [isCreating, setIsCreating] = (0, import_react16.useState)(false);
3633
- const [error, setError] = (0, import_react16.useState)(null);
3634
- const [fromName, setFromName] = (0, import_react16.useState)(null);
3635
- const [toName, setToName] = (0, import_react16.useState)(null);
3636
- const cancelRef = (0, import_react16.useRef)(null);
3637
- const refresh = (0, import_react16.useCallback)(async () => {
3859
+ const [load, setLoad] = (0, import_react17.useState)({ status: "loading" });
3860
+ const [originDbId, setOriginDbId] = (0, import_react17.useState)("");
3861
+ const [targetDbId, setTargetDbId] = (0, import_react17.useState)("");
3862
+ const [isCreating, setIsCreating] = (0, import_react17.useState)(false);
3863
+ const [error, setError] = (0, import_react17.useState)(null);
3864
+ const [fromName, setFromName] = (0, import_react17.useState)(null);
3865
+ const [toName, setToName] = (0, import_react17.useState)(null);
3866
+ const cancelRef = (0, import_react17.useRef)(null);
3867
+ const refresh = (0, import_react17.useCallback)(async () => {
3638
3868
  if (!host.listSceneFamilyTracks) {
3639
3869
  setLoad({ status: "error", message: "This host does not support crossfade tracks." });
3640
3870
  return;
@@ -3654,7 +3884,7 @@ function CrossfadeModal({
3654
3884
  setLoad({ status: "error", message: err instanceof Error ? err.message : "Failed to load tracks." });
3655
3885
  }
3656
3886
  }, [host, fromSceneId, toSceneId]);
3657
- (0, import_react16.useEffect)(() => {
3887
+ (0, import_react17.useEffect)(() => {
3658
3888
  if (open) {
3659
3889
  setError(null);
3660
3890
  setIsCreating(false);
@@ -3663,21 +3893,21 @@ function CrossfadeModal({
3663
3893
  void refresh();
3664
3894
  }
3665
3895
  }, [open, refresh]);
3666
- const excludeSet = (0, import_react16.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3667
- const originCandidates = (0, import_react16.useMemo)(
3896
+ const excludeSet = (0, import_react17.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
3897
+ const originCandidates = (0, import_react17.useMemo)(
3668
3898
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
3669
3899
  [load, excludeSet]
3670
3900
  );
3671
- const targetCandidates = (0, import_react16.useMemo)(
3901
+ const targetCandidates = (0, import_react17.useMemo)(
3672
3902
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
3673
3903
  [load, excludeSet]
3674
3904
  );
3675
- (0, import_react16.useEffect)(() => {
3905
+ (0, import_react17.useEffect)(() => {
3676
3906
  if (!originCandidates.some((t) => t.dbId === originDbId)) {
3677
3907
  setOriginDbId(originCandidates[0]?.dbId ?? "");
3678
3908
  }
3679
3909
  }, [originCandidates, originDbId]);
3680
- (0, import_react16.useEffect)(() => {
3910
+ (0, import_react17.useEffect)(() => {
3681
3911
  if (!targetCandidates.some((t) => t.dbId === targetDbId)) {
3682
3912
  setTargetDbId(targetCandidates[0]?.dbId ?? "");
3683
3913
  }
@@ -3685,10 +3915,10 @@ function CrossfadeModal({
3685
3915
  const originTrack = originCandidates.find((t) => t.dbId === originDbId) ?? null;
3686
3916
  const targetTrack = targetCandidates.find((t) => t.dbId === targetDbId) ?? null;
3687
3917
  const canCreate = !isCreating && !!originTrack && !!targetTrack;
3688
- const handleClose = (0, import_react16.useCallback)(() => {
3918
+ const handleClose = (0, import_react17.useCallback)(() => {
3689
3919
  if (!isCreating) onClose();
3690
3920
  }, [isCreating, onClose]);
3691
- const handleCreate = (0, import_react16.useCallback)(async () => {
3921
+ const handleCreate = (0, import_react17.useCallback)(async () => {
3692
3922
  if (!originTrack || !targetTrack) return;
3693
3923
  setIsCreating(true);
3694
3924
  setError(null);
@@ -3706,26 +3936,26 @@ function CrossfadeModal({
3706
3936
  const fromLabel = fromName ?? fromSceneName ?? null;
3707
3937
  const toLabel = toName ?? toSceneName ?? null;
3708
3938
  if (!open) return null;
3709
- return /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3939
+ return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(Modal, { open, onClose: handleClose, testIdPrefix, initialFocusRef: cancelRef, children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3710
3940
  "div",
3711
3941
  {
3712
3942
  className: "bg-sas-panel border border-sas-border rounded-md shadow-xl w-[420px] max-w-[92vw] p-4 space-y-3",
3713
3943
  onClick: (e) => e.stopPropagation(),
3714
3944
  "data-testid": `${testIdPrefix}-box`,
3715
3945
  children: [
3716
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3717
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3946
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("h3", { className: "text-sm font-bold text-sas-text", children: "Add crossfade" }),
3947
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { className: "text-[11px] text-sas-muted leading-relaxed", children: [
3718
3948
  "Bridge a track from",
3719
3949
  " ",
3720
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3950
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: fromLabel ?? "the origin scene" }),
3721
3951
  " into one from",
3722
3952
  " ",
3723
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3953
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: toLabel ?? "the target scene" }),
3724
3954
  ". Both layers share one generated part; each keeps its own preset."
3725
3955
  ] }),
3726
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3727
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3728
- load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
3956
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-muted py-4 text-center", children: "Loading tracks\u2026" }),
3957
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger py-4 text-center", children: load.message }),
3958
+ load.status === "ready" && (originCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
3729
3959
  "div",
3730
3960
  {
3731
3961
  className: "text-xs text-sas-muted py-4 text-center",
@@ -3736,20 +3966,20 @@ function CrossfadeModal({
3736
3966
  ". Add one (or free one from another crossfade) first."
3737
3967
  ]
3738
3968
  }
3739
- ) : /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(import_jsx_runtime17.Fragment, { children: [
3740
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "block", children: [
3741
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3969
+ ) : /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(import_jsx_runtime18.Fragment, { children: [
3970
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "block", children: [
3971
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3742
3972
  "Origin ",
3743
3973
  fromLabel ? `(${fromLabel})` : "(top)"
3744
3974
  ] }),
3745
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3975
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3746
3976
  "div",
3747
3977
  {
3748
3978
  role: "radiogroup",
3749
3979
  "aria-label": "Origin track",
3750
3980
  "data-testid": `${testIdPrefix}-origin-list`,
3751
3981
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3752
- children: originCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
3982
+ children: originCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3753
3983
  CandidateRow,
3754
3984
  {
3755
3985
  track: t,
@@ -3763,23 +3993,23 @@ function CrossfadeModal({
3763
3993
  }
3764
3994
  )
3765
3995
  ] }),
3766
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "block", children: [
3767
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3996
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "block", children: [
3997
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
3768
3998
  "Target ",
3769
3999
  toLabel ? `(${toLabel})` : "(bottom)"
3770
4000
  ] }),
3771
- targetCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
4001
+ targetCandidates.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "text-xs text-sas-danger mt-0.5", "data-testid": `${testIdPrefix}-empty-target`, children: [
3772
4002
  "No available tracks in ",
3773
4003
  toLabel ?? "the target scene",
3774
4004
  " to crossfade into."
3775
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4005
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3776
4006
  "div",
3777
4007
  {
3778
4008
  role: "radiogroup",
3779
4009
  "aria-label": "Target track",
3780
4010
  "data-testid": `${testIdPrefix}-target-list`,
3781
4011
  className: "mt-1 space-y-1 max-h-40 overflow-y-auto pr-0.5",
3782
- children: targetCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4012
+ children: targetCandidates.map((t) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3783
4013
  CandidateRow,
3784
4014
  {
3785
4015
  track: t,
@@ -3794,9 +4024,9 @@ function CrossfadeModal({
3794
4024
  )
3795
4025
  ] })
3796
4026
  ] })),
3797
- error && /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
3798
- /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
3799
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4027
+ error && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger", "data-testid": `${testIdPrefix}-error`, children: error }),
4028
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex justify-end gap-2 pt-1", children: [
4029
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3800
4030
  "button",
3801
4031
  {
3802
4032
  ref: cancelRef,
@@ -3807,7 +4037,7 @@ function CrossfadeModal({
3807
4037
  children: "Cancel"
3808
4038
  }
3809
4039
  ),
3810
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
4040
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
3811
4041
  "button",
3812
4042
  {
3813
4043
  "data-testid": `${testIdPrefix}-confirm`,
@@ -3824,10 +4054,10 @@ function CrossfadeModal({
3824
4054
  }
3825
4055
 
3826
4056
  // src/components/TransitionDesigner.tsx
3827
- var import_react18 = require("react");
4057
+ var import_react19 = require("react");
3828
4058
 
3829
4059
  // src/hooks/useTrackReorder.ts
3830
- var import_react17 = require("react");
4060
+ var import_react18 = require("react");
3831
4061
  function moveItem(arr, from, to) {
3832
4062
  const next = arr.slice();
3833
4063
  if (from === to || from < 0 || to < 0 || from >= next.length || to >= next.length) {
@@ -3844,12 +4074,12 @@ function useTrackReorder({
3844
4074
  getId,
3845
4075
  onError
3846
4076
  }) {
3847
- const [draggingIndex, setDraggingIndex] = (0, import_react17.useState)(null);
3848
- const [dragOverIndex, setDragOverIndex] = (0, import_react17.useState)(null);
3849
- const fromRef = (0, import_react17.useRef)(null);
3850
- const itemsRef = (0, import_react17.useRef)(items);
4077
+ const [draggingIndex, setDraggingIndex] = (0, import_react18.useState)(null);
4078
+ const [dragOverIndex, setDragOverIndex] = (0, import_react18.useState)(null);
4079
+ const fromRef = (0, import_react18.useRef)(null);
4080
+ const itemsRef = (0, import_react18.useRef)(items);
3851
4081
  itemsRef.current = items;
3852
- const dragPropsFor = (0, import_react17.useCallback)(
4082
+ const dragPropsFor = (0, import_react18.useCallback)(
3853
4083
  (index) => ({
3854
4084
  handleProps: {
3855
4085
  draggable: true,
@@ -4031,7 +4261,7 @@ function dbIdsFromKeys(keys) {
4031
4261
  }
4032
4262
 
4033
4263
  // src/components/TransitionDesigner.tsx
4034
- var import_jsx_runtime18 = require("react/jsx-runtime");
4264
+ var import_jsx_runtime19 = require("react/jsx-runtime");
4035
4265
  var CROSSFADE_ESTIMATE_MS = 15e3;
4036
4266
  var FADE_ESTIMATE_MS = 11e3;
4037
4267
  var CREATE_ALL_CONCURRENCY = 5;
@@ -4052,56 +4282,67 @@ function TransitionDesigner({
4052
4282
  onCreateCrossfade,
4053
4283
  onCreateFade,
4054
4284
  onCreateAudioTransition,
4285
+ mapColumnSubjects,
4286
+ fadeEstimateMs,
4055
4287
  familyLabel,
4056
4288
  testIdPrefix = "transition-designer"
4057
4289
  }) {
4058
- const [load, setLoad] = (0, import_react18.useState)({ status: "loading" });
4059
- const [fromName, setFromName] = (0, import_react18.useState)(null);
4060
- const [toName, setToName] = (0, import_react18.useState)(null);
4061
- const [originSlots, setOriginSlots] = (0, import_react18.useState)([]);
4062
- const [targetSlots, setTargetSlots] = (0, import_react18.useState)([]);
4063
- const [creatingKeys, setCreatingKeys] = (0, import_react18.useState)(() => /* @__PURE__ */ new Set());
4064
- const [rowErrors, setRowErrors] = (0, import_react18.useState)({});
4065
- const [rowEffects, setRowEffects] = (0, import_react18.useState)({});
4066
- const rowEffectsRef = (0, import_react18.useRef)(rowEffects);
4290
+ const fadeOnly = !onCreateCrossfade;
4291
+ const [load, setLoad] = (0, import_react19.useState)({ status: "loading" });
4292
+ const [fromName, setFromName] = (0, import_react19.useState)(null);
4293
+ const [toName, setToName] = (0, import_react19.useState)(null);
4294
+ const [originSlots, setOriginSlots] = (0, import_react19.useState)([]);
4295
+ const [targetSlots, setTargetSlots] = (0, import_react19.useState)([]);
4296
+ const [creatingKeys, setCreatingKeys] = (0, import_react19.useState)(() => /* @__PURE__ */ new Set());
4297
+ const [rowErrors, setRowErrors] = (0, import_react19.useState)({});
4298
+ const [rowEffects, setRowEffects] = (0, import_react19.useState)({});
4299
+ const rowEffectsRef = (0, import_react19.useRef)(rowEffects);
4067
4300
  rowEffectsRef.current = rowEffects;
4068
4301
  const audioEffectsEnabled = !!onCreateAudioTransition;
4069
- const excludeRef = (0, import_react18.useRef)(excludeSourceDbIds);
4302
+ const excludeRef = (0, import_react19.useRef)(excludeSourceDbIds);
4070
4303
  excludeRef.current = excludeSourceDbIds;
4071
- const originSlotsRef = (0, import_react18.useRef)(originSlots);
4304
+ const originSlotsRef = (0, import_react19.useRef)(originSlots);
4072
4305
  originSlotsRef.current = originSlots;
4073
- const targetSlotsRef = (0, import_react18.useRef)(targetSlots);
4306
+ const targetSlotsRef = (0, import_react19.useRef)(targetSlots);
4074
4307
  targetSlotsRef.current = targetSlots;
4075
- const creatingKeysRef = (0, import_react18.useRef)(creatingKeys);
4308
+ const creatingKeysRef = (0, import_react19.useRef)(creatingKeys);
4076
4309
  creatingKeysRef.current = creatingKeys;
4077
- const dragRef = (0, import_react18.useRef)(null);
4078
- const [dragging, setDragging] = (0, import_react18.useState)(null);
4079
- const [dragOver, setDragOver] = (0, import_react18.useState)(null);
4080
- const excludeSet = (0, import_react18.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
4081
- const originPool = (0, import_react18.useMemo)(
4310
+ const dragRef = (0, import_react19.useRef)(null);
4311
+ const [dragging, setDragging] = (0, import_react19.useState)(null);
4312
+ const [dragOver, setDragOver] = (0, import_react19.useState)(null);
4313
+ const excludeSet = (0, import_react19.useMemo)(() => new Set(excludeSourceDbIds ?? []), [excludeSourceDbIds]);
4314
+ const originPool = (0, import_react19.useMemo)(
4082
4315
  () => load.status === "ready" ? load.origin.filter((t) => !excludeSet.has(t.dbId)) : [],
4083
4316
  [load, excludeSet]
4084
4317
  );
4085
- const targetPool = (0, import_react18.useMemo)(
4318
+ const targetPool = (0, import_react19.useMemo)(
4086
4319
  () => load.status === "ready" ? load.target.filter((t) => !excludeSet.has(t.dbId)) : [],
4087
4320
  [load, excludeSet]
4088
4321
  );
4089
- const originById = (0, import_react18.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
4090
- const targetById = (0, import_react18.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
4091
- const originByIdRef = (0, import_react18.useRef)(originById);
4322
+ const originById = (0, import_react19.useMemo)(() => new Map(originPool.map((t) => [t.dbId, t])), [originPool]);
4323
+ const targetById = (0, import_react19.useMemo)(() => new Map(targetPool.map((t) => [t.dbId, t])), [targetPool]);
4324
+ const originByIdRef = (0, import_react19.useRef)(originById);
4092
4325
  originByIdRef.current = originById;
4093
- const targetByIdRef = (0, import_react18.useRef)(targetById);
4326
+ const targetByIdRef = (0, import_react19.useRef)(targetById);
4094
4327
  targetByIdRef.current = targetById;
4095
- const refresh = (0, import_react18.useCallback)(async () => {
4328
+ const refresh = (0, import_react19.useCallback)(async () => {
4096
4329
  if (!host.listSceneFamilyTracks) {
4097
4330
  setLoad({ status: "error", message: "This host does not support transition tracks." });
4098
4331
  return;
4099
4332
  }
4100
4333
  setLoad({ status: "loading" });
4101
4334
  try {
4102
- const [origin, target, fName, tName, draftRaw] = await Promise.all([
4335
+ let [origin, target] = await Promise.all([
4103
4336
  host.listSceneFamilyTracks(fromSceneId),
4104
- host.listSceneFamilyTracks(toSceneId),
4337
+ host.listSceneFamilyTracks(toSceneId)
4338
+ ]);
4339
+ if (mapColumnSubjects) {
4340
+ [origin, target] = await Promise.all([
4341
+ mapColumnSubjects(fromSceneId, origin),
4342
+ mapColumnSubjects(toSceneId, target)
4343
+ ]);
4344
+ }
4345
+ const [fName, tName, draftRaw] = await Promise.all([
4105
4346
  host.getSceneName ? host.getSceneName(fromSceneId) : Promise.resolve(null),
4106
4347
  host.getSceneName ? host.getSceneName(toSceneId) : Promise.resolve(null),
4107
4348
  host.getSceneData ? host.getSceneData(transitionSceneId, TRANSITION_DESIGNER_DRAFT_KEY) : Promise.resolve(null)
@@ -4126,11 +4367,11 @@ function TransitionDesigner({
4126
4367
  message: err instanceof Error ? err.message : "Failed to load tracks."
4127
4368
  });
4128
4369
  }
4129
- }, [host, fromSceneId, toSceneId, transitionSceneId]);
4130
- (0, import_react18.useEffect)(() => {
4370
+ }, [host, fromSceneId, toSceneId, transitionSceneId, mapColumnSubjects]);
4371
+ (0, import_react19.useEffect)(() => {
4131
4372
  void refresh();
4132
4373
  }, [refresh]);
4133
- (0, import_react18.useEffect)(() => {
4374
+ (0, import_react19.useEffect)(() => {
4134
4375
  if (load.status !== "ready") return;
4135
4376
  const [po, pt] = padPair(
4136
4377
  reconcileSlots(originSlotsRef.current, originPool.map((t) => t.dbId)),
@@ -4139,7 +4380,7 @@ function TransitionDesigner({
4139
4380
  if (!slotsEqual(po, originSlotsRef.current)) setOriginSlots(po);
4140
4381
  if (!slotsEqual(pt, targetSlotsRef.current)) setTargetSlots(pt);
4141
4382
  }, [originPool, targetPool, load.status]);
4142
- const mutate = (0, import_react18.useCallback)(
4383
+ const mutate = (0, import_react19.useCallback)(
4143
4384
  (nextOrigin, nextTarget) => {
4144
4385
  const norm = normalizeSlots(nextOrigin, nextTarget);
4145
4386
  const [po, pt] = padPair(norm.originOrder, norm.targetOrder);
@@ -4152,7 +4393,7 @@ function TransitionDesigner({
4152
4393
  },
4153
4394
  [host, transitionSceneId]
4154
4395
  );
4155
- const setRowEffect = (0, import_react18.useCallback)(
4396
+ const setRowEffect = (0, import_react19.useCallback)(
4156
4397
  (sourceDbId, effect) => {
4157
4398
  setRowEffects((prev) => {
4158
4399
  const next = { ...prev, [sourceDbId]: effect };
@@ -4166,7 +4407,7 @@ function TransitionDesigner({
4166
4407
  },
4167
4408
  [host, transitionSceneId]
4168
4409
  );
4169
- const insertGapAbove = (0, import_react18.useCallback)(
4410
+ const insertGapAbove = (0, import_react19.useCallback)(
4170
4411
  (col, index) => {
4171
4412
  const slots = col === "origin" ? originSlots : targetSlots;
4172
4413
  const next = [...slots.slice(0, index), null, ...slots.slice(index)];
@@ -4175,7 +4416,7 @@ function TransitionDesigner({
4175
4416
  },
4176
4417
  [originSlots, targetSlots, mutate]
4177
4418
  );
4178
- const removeGap = (0, import_react18.useCallback)(
4419
+ const removeGap = (0, import_react19.useCallback)(
4179
4420
  (col, index) => {
4180
4421
  const slots = col === "origin" ? originSlots : targetSlots;
4181
4422
  const next = slots.filter((_, i) => i !== index);
@@ -4184,7 +4425,7 @@ function TransitionDesigner({
4184
4425
  },
4185
4426
  [originSlots, targetSlots, mutate]
4186
4427
  );
4187
- const handleDrop = (0, import_react18.useCallback)(
4428
+ const handleDrop = (0, import_react19.useCallback)(
4188
4429
  (col, to) => {
4189
4430
  const from = dragRef.current;
4190
4431
  dragRef.current = null;
@@ -4196,16 +4437,34 @@ function TransitionDesigner({
4196
4437
  },
4197
4438
  [originSlots, targetSlots, mutate]
4198
4439
  );
4199
- const rows = (0, import_react18.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
4200
- const creatingDbIds = (0, import_react18.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
4201
- const eligibleCount = (0, import_react18.useMemo)(
4202
- () => rows.filter((r) => {
4440
+ const rows = (0, import_react19.useMemo)(() => buildRowSlots(originSlots, targetSlots), [originSlots, targetSlots]);
4441
+ const fadeOnlyRows = (0, import_react19.useMemo)(
4442
+ () => fadeOnly ? [
4443
+ ...originPool.map((t) => ({
4444
+ originId: t.dbId,
4445
+ targetId: null,
4446
+ type: "fade-out"
4447
+ })),
4448
+ ...targetPool.map((t) => ({
4449
+ originId: null,
4450
+ targetId: t.dbId,
4451
+ type: "fade-in"
4452
+ }))
4453
+ ] : [],
4454
+ [fadeOnly, originPool, targetPool]
4455
+ );
4456
+ const fadeOnlyRowsRef = (0, import_react19.useRef)(fadeOnlyRows);
4457
+ fadeOnlyRowsRef.current = fadeOnlyRows;
4458
+ const effectiveRows = fadeOnly ? fadeOnlyRows : rows;
4459
+ const creatingDbIds = (0, import_react19.useMemo)(() => dbIdsFromKeys(creatingKeys), [creatingKeys]);
4460
+ const eligibleCount = (0, import_react19.useMemo)(
4461
+ () => effectiveRows.filter((r) => {
4203
4462
  const k = rowKey(r);
4204
4463
  return k !== null && !creatingKeys.has(k);
4205
4464
  }).length,
4206
- [rows, creatingKeys]
4465
+ [effectiveRows, creatingKeys]
4207
4466
  );
4208
- const createRow = (0, import_react18.useCallback)(
4467
+ const createRow = (0, import_react19.useCallback)(
4209
4468
  async (row) => {
4210
4469
  const key = rowKey(row);
4211
4470
  if (!key || !row.type || creatingKeysRef.current.has(key)) return;
@@ -4221,6 +4480,7 @@ function TransitionDesigner({
4221
4480
  const o = row.originId ? originByIdRef.current.get(row.originId) : void 0;
4222
4481
  const t = row.targetId ? targetByIdRef.current.get(row.targetId) : void 0;
4223
4482
  if (!o || !t) throw new Error("Track is no longer available \u2014 refresh and retry.");
4483
+ if (!onCreateCrossfade) throw new Error("This panel does not support crossfades.");
4224
4484
  await onCreateCrossfade(
4225
4485
  { dbId: o.dbId, name: o.name, role: o.role },
4226
4486
  { dbId: t.dbId, name: t.name, role: t.role }
@@ -4259,8 +4519,9 @@ function TransitionDesigner({
4259
4519
  },
4260
4520
  [onCreateCrossfade, onCreateFade, onCreateAudioTransition]
4261
4521
  );
4262
- const createAll = (0, import_react18.useCallback)(async () => {
4263
- const eligible = buildRowSlots(originSlotsRef.current, targetSlotsRef.current).filter((r) => {
4522
+ const createAll = (0, import_react19.useCallback)(async () => {
4523
+ const source = fadeOnly ? fadeOnlyRowsRef.current : buildRowSlots(originSlotsRef.current, targetSlotsRef.current);
4524
+ const eligible = source.filter((r) => {
4264
4525
  const k = rowKey(r);
4265
4526
  return k !== null && !creatingKeysRef.current.has(k);
4266
4527
  });
@@ -4276,7 +4537,7 @@ function TransitionDesigner({
4276
4537
  await Promise.all(
4277
4538
  Array.from({ length: Math.min(CREATE_ALL_CONCURRENCY, eligible.length) }, () => worker())
4278
4539
  );
4279
- }, [createRow]);
4540
+ }, [createRow, fadeOnly]);
4280
4541
  const fromLabel = fromName ?? "origin";
4281
4542
  const toLabel = toName ?? "target";
4282
4543
  const cellDragProps = (col, index, locked) => ({
@@ -4327,15 +4588,15 @@ function TransitionDesigner({
4327
4588
  const base = "group relative rounded-sm border px-2 py-1.5 text-left transition-colors select-none";
4328
4589
  const tone = isDragTarget ? "border-sas-accent bg-sas-accent/10" : "border-sas-border bg-sas-panel";
4329
4590
  if (slotId === null) {
4330
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4591
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4331
4592
  "div",
4332
4593
  {
4333
4594
  ...cellDragProps(col, index, false),
4334
4595
  "data-testid": `${testIdPrefix}-${col}-gap-${index}`,
4335
4596
  className: `${base} ${tone} border-dashed flex items-center justify-between ${isDragging ? "opacity-40" : "opacity-70"}`,
4336
4597
  children: [
4337
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4338
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4598
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: "\u2014 gap \u2014" }),
4599
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4339
4600
  "button",
4340
4601
  {
4341
4602
  type: "button",
@@ -4352,7 +4613,7 @@ function TransitionDesigner({
4352
4613
  }
4353
4614
  const primary = track ? track.prompt?.trim() || track.name : slotId;
4354
4615
  const meta = track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing";
4355
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4616
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4356
4617
  "div",
4357
4618
  {
4358
4619
  ...cellDragProps(col, index, locked),
@@ -4360,13 +4621,13 @@ function TransitionDesigner({
4360
4621
  "data-value": slotId,
4361
4622
  className: `${base} ${tone} ${isDragging ? "opacity-40" : ""} ${locked ? "opacity-60" : "cursor-grab active:cursor-grabbing"}`,
4362
4623
  title: track ? track.dbId : "Track no longer available",
4363
- children: /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-start gap-1", children: [
4364
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4365
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "min-w-0 flex-1", children: [
4366
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
4367
- meta && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4624
+ children: /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-start gap-1", children: [
4625
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-muted/60 text-xs leading-tight pt-0.5", "aria-hidden": true, children: "\u283F" }),
4626
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "min-w-0 flex-1", children: [
4627
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-text truncate", children: primary }),
4628
+ meta && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: meta })
4368
4629
  ] }),
4369
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4630
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4370
4631
  "button",
4371
4632
  {
4372
4633
  type: "button",
@@ -4382,22 +4643,159 @@ function TransitionDesigner({
4382
4643
  }
4383
4644
  );
4384
4645
  };
4385
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4386
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4387
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4388
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: fromLabel }),
4646
+ const renderFadeOnlyRow = (row, i) => {
4647
+ const slotId = row.originId ?? row.targetId;
4648
+ const byId = row.type === "fade-out" ? originById : targetById;
4649
+ const track = byId.get(slotId);
4650
+ const key = rowKey(row);
4651
+ const isCreatingThis = key !== null && creatingKeys.has(key);
4652
+ const errMsg = key !== null ? rowErrors[key] : void 0;
4653
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4654
+ "div",
4655
+ {
4656
+ "data-testid": `${testIdPrefix}-row-${i}`,
4657
+ className: "grid grid-cols-[1fr_auto] gap-2 items-center",
4658
+ children: [
4659
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4660
+ "div",
4661
+ {
4662
+ "data-testid": `${testIdPrefix}-subject-${slotId}`,
4663
+ className: `rounded-sm border border-sas-border bg-sas-panel px-2 py-1.5 ${isCreatingThis ? "opacity-60" : ""}`,
4664
+ title: track ? track.dbId : "Track no longer available",
4665
+ children: [
4666
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-text truncate", children: track ? track.prompt?.trim() || track.name : slotId }),
4667
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-[10px] text-sas-muted truncate mt-0.5", children: track ? [track.role, shortId3(track.dbId)].filter(Boolean).join(" \xB7 ") : "missing" })
4668
+ ]
4669
+ }
4670
+ ),
4671
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4672
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4673
+ "span",
4674
+ {
4675
+ "data-testid": `${testIdPrefix}-type-${i}`,
4676
+ className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-border text-sas-muted",
4677
+ children: row.type ? TYPE_LABEL[row.type] : "\u2014"
4678
+ }
4679
+ ),
4680
+ isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4681
+ SorceryProgressBar,
4682
+ {
4683
+ isLoading: true,
4684
+ heightClass: "h-5",
4685
+ statusText: "CREATING",
4686
+ estimatedDurationMs: fadeEstimateMs ?? FADE_ESTIMATE_MS
4687
+ }
4688
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4689
+ "button",
4690
+ {
4691
+ type: "button",
4692
+ "data-testid": `${testIdPrefix}-create-${i}`,
4693
+ onClick: () => createRow(row),
4694
+ className: "w-full px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg",
4695
+ children: "Create"
4696
+ }
4697
+ ),
4698
+ errMsg && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4699
+ "span",
4700
+ {
4701
+ "data-testid": `${testIdPrefix}-row-error-${i}`,
4702
+ className: "text-[10px] text-sas-danger text-center leading-tight",
4703
+ children: errMsg
4704
+ }
4705
+ )
4706
+ ] })
4707
+ ]
4708
+ },
4709
+ slotId
4710
+ );
4711
+ };
4712
+ if (fadeOnly) {
4713
+ const outRows = fadeOnlyRows.filter((r) => r.type === "fade-out");
4714
+ const inRows = fadeOnlyRows.filter((r) => r.type === "fade-in");
4715
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4716
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4717
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4718
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: fromLabel }),
4719
+ " \u2192",
4720
+ " ",
4721
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: toLabel }),
4722
+ familyLabel ? ` \xB7 ${familyLabel}` : "",
4723
+ " \xB7 each entry fades on its own \u2014 origin fades out, target fades in."
4724
+ ] }),
4725
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
4726
+ creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4727
+ "span",
4728
+ {
4729
+ className: "text-[10px] text-sas-accent whitespace-nowrap",
4730
+ "data-testid": `${testIdPrefix}-creating-count`,
4731
+ children: [
4732
+ creatingKeys.size,
4733
+ " creating\u2026"
4734
+ ]
4735
+ }
4736
+ ),
4737
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4738
+ "button",
4739
+ {
4740
+ type: "button",
4741
+ "data-testid": `${testIdPrefix}-create-all`,
4742
+ onClick: createAll,
4743
+ disabled: eligibleCount === 0,
4744
+ title: "Create every fade at once (runs several concurrently)",
4745
+ className: `px-2 py-0.5 text-[10px] font-medium rounded-sm border transition-colors whitespace-nowrap ${eligibleCount > 0 ? "bg-sas-accent/20 border-sas-accent text-sas-accent hover:bg-sas-accent hover:text-sas-bg" : "bg-sas-panel border-sas-border text-sas-muted/50 cursor-not-allowed"}`,
4746
+ children: [
4747
+ "Create all",
4748
+ eligibleCount > 0 ? ` (${eligibleCount})` : ""
4749
+ ]
4750
+ }
4751
+ )
4752
+ ] })
4753
+ ] }),
4754
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4755
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4756
+ load.status === "ready" && (fadeOnlyRows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4757
+ "Nothing to fade in this panel for either scene. Add tracks to ",
4758
+ fromLabel,
4759
+ " or ",
4760
+ toLabel,
4761
+ " ",
4762
+ "first (or free one by deleting an existing fade)."
4763
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-3", children: [
4764
+ outRows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-out-section`, children: [
4765
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4766
+ "Origin (",
4767
+ fromLabel,
4768
+ ") \u2014 fades out"
4769
+ ] }),
4770
+ outRows.map((row, i) => renderFadeOnlyRow(row, i))
4771
+ ] }),
4772
+ inRows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-fade-in-section`, children: [
4773
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted", children: [
4774
+ "Target (",
4775
+ toLabel,
4776
+ ") \u2014 fades in"
4777
+ ] }),
4778
+ inRows.map((row, j) => renderFadeOnlyRow(row, outRows.length + j))
4779
+ ] })
4780
+ ] }))
4781
+ ] });
4782
+ }
4783
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "space-y-2", "data-testid": `${testIdPrefix}-box`, children: [
4784
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center justify-between gap-3 pb-1 border-b border-sas-border", children: [
4785
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("p", { className: "text-[11px] text-sas-muted leading-snug min-w-0", children: [
4786
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: fromLabel }),
4389
4787
  " \u2192",
4390
4788
  " ",
4391
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-sas-text", children: toLabel }),
4789
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-sas-text", children: toLabel }),
4392
4790
  familyLabel ? ` \xB7 ${familyLabel}` : "",
4393
4791
  " \xB7 line up a track on each side to crossfade; leave one blank (or insert a gap) to fade."
4394
4792
  ] }),
4395
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
4396
- creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4793
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2 shrink-0", children: [
4794
+ creatingKeys.size > 0 && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] text-sas-accent whitespace-nowrap", "data-testid": `${testIdPrefix}-creating-count`, children: [
4397
4795
  creatingKeys.size,
4398
4796
  " creating\u2026"
4399
4797
  ] }),
4400
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4798
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4401
4799
  "button",
4402
4800
  {
4403
4801
  type: "button",
@@ -4414,49 +4812,49 @@ function TransitionDesigner({
4414
4812
  )
4415
4813
  ] })
4416
4814
  ] }),
4417
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4418
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4815
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "grid grid-cols-[1fr_auto_1fr] gap-2", children: [
4816
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate", children: [
4419
4817
  "Origin (",
4420
4818
  fromLabel,
4421
4819
  ")"
4422
4820
  ] }),
4423
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4424
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4821
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted text-center px-2", children: "Transition" }),
4822
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("span", { className: "text-[10px] uppercase tracking-wide text-sas-muted truncate text-right", children: [
4425
4823
  "Target (",
4426
4824
  toLabel,
4427
4825
  ")"
4428
4826
  ] })
4429
4827
  ] }),
4430
- load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4431
- load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4432
- load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4828
+ load.status === "loading" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted py-6 text-center", children: "Loading tracks\u2026" }),
4829
+ load.status === "error" && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-danger py-6 text-center", "data-testid": `${testIdPrefix}-error`, children: load.message }),
4830
+ load.status === "ready" && (rows.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "text-xs text-sas-muted py-6 text-center", "data-testid": `${testIdPrefix}-empty`, children: [
4433
4831
  "No tracks to arrange in this panel for either scene. Add tracks to ",
4434
4832
  fromLabel,
4435
4833
  " or ",
4436
4834
  toLabel,
4437
4835
  " ",
4438
4836
  "first (or free one by deleting an existing crossfade/fade)."
4439
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
4837
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "space-y-2", children: rows.map((row, i) => {
4440
4838
  const key = rowKey(row);
4441
4839
  const isCreatingThis = key !== null && creatingKeys.has(key);
4442
4840
  const errMsg = key !== null ? rowErrors[key] : void 0;
4443
- return /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)(
4841
+ return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4444
4842
  "div",
4445
4843
  {
4446
4844
  "data-testid": `${testIdPrefix}-row-${i}`,
4447
4845
  className: "grid grid-cols-[1fr_auto_1fr] gap-2 items-center",
4448
4846
  children: [
4449
4847
  renderCell("origin", i, row.originId),
4450
- /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4451
- !row.type ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4848
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "w-[160px] flex flex-col items-center gap-1", children: [
4849
+ !row.type ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[10px] text-sas-muted/50", children: "\u2014" }) : row.type === "crossfade" ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4452
4850
  "span",
4453
4851
  {
4454
4852
  "data-testid": `${testIdPrefix}-type-${i}`,
4455
4853
  className: "text-[10px] font-medium px-1.5 py-0.5 rounded-sm border border-sas-accent/50 text-sas-accent",
4456
4854
  children: TYPE_LABEL[row.type]
4457
4855
  }
4458
- ) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime18.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4459
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4856
+ ) : audioEffectsEnabled ? /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-1", "data-testid": `${testIdPrefix}-type-${i}`, children: [
4857
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4460
4858
  "select",
4461
4859
  {
4462
4860
  "data-testid": `${testIdPrefix}-effect-${i}`,
@@ -4466,11 +4864,11 @@ function TransitionDesigner({
4466
4864
  if (id) setRowEffect(id, e.target.value);
4467
4865
  },
4468
4866
  className: "text-[10px] bg-sas-panel border border-sas-border rounded-sm px-1 py-0.5 text-sas-text",
4469
- children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4867
+ children: AUDIO_EFFECTS.map((eff) => /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("option", { value: eff, children: AUDIO_EFFECT_LABEL[eff] }, eff))
4470
4868
  }
4471
4869
  ),
4472
- /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4473
- ] }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4870
+ /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[9px] text-sas-muted", children: row.type === "fade-out" ? "out" : "in" })
4871
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4474
4872
  "span",
4475
4873
  {
4476
4874
  "data-testid": `${testIdPrefix}-type-${i}`,
@@ -4478,7 +4876,7 @@ function TransitionDesigner({
4478
4876
  children: TYPE_LABEL[row.type]
4479
4877
  }
4480
4878
  ),
4481
- isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4879
+ isCreatingThis ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "w-full", children: /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4482
4880
  SorceryProgressBar,
4483
4881
  {
4484
4882
  isLoading: true,
@@ -4486,7 +4884,7 @@ function TransitionDesigner({
4486
4884
  statusText: "CREATING",
4487
4885
  estimatedDurationMs: row.type === "crossfade" ? CROSSFADE_ESTIMATE_MS : FADE_ESTIMATE_MS
4488
4886
  }
4489
- ) }) : /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4887
+ ) }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4490
4888
  "button",
4491
4889
  {
4492
4890
  type: "button",
@@ -4497,7 +4895,7 @@ function TransitionDesigner({
4497
4895
  children: "Create"
4498
4896
  }
4499
4897
  ),
4500
- errMsg && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)(
4898
+ errMsg && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4501
4899
  "span",
4502
4900
  {
4503
4901
  "data-testid": `${testIdPrefix}-row-error-${i}`,
@@ -4516,8 +4914,8 @@ function TransitionDesigner({
4516
4914
  }
4517
4915
 
4518
4916
  // src/components/PanelMasterStrip.tsx
4519
- var import_react19 = require("react");
4520
- var import_jsx_runtime19 = require("react/jsx-runtime");
4917
+ var import_react20 = require("react");
4918
+ var import_jsx_runtime20 = require("react/jsx-runtime");
4521
4919
  function PanelMasterStrip({
4522
4920
  bus,
4523
4921
  levels = null,
@@ -4536,22 +4934,22 @@ function PanelMasterStrip({
4536
4934
  onToggleFxEnabled,
4537
4935
  onShowFxEditor
4538
4936
  }) {
4539
- const [search, setSearch] = (0, import_react19.useState)("");
4540
- const filtered = (0, import_react19.useMemo)(() => {
4937
+ const [search, setSearch] = (0, import_react20.useState)("");
4938
+ const filtered = (0, import_react20.useMemo)(() => {
4541
4939
  const q = search.trim().toLowerCase();
4542
4940
  if (!q) return availableFx;
4543
4941
  return availableFx.filter(
4544
4942
  (fx) => fx.name.toLowerCase().includes(q) || fx.manufacturer.toLowerCase().includes(q)
4545
4943
  );
4546
4944
  }, [availableFx, search]);
4547
- return /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
4945
+ return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
4548
4946
  "div",
4549
4947
  {
4550
4948
  "data-testid": "panel-master-strip",
4551
4949
  className: `flex flex-col gap-1 px-2 py-1.5 rounded-sm border border-sas-border border-l-2 border-l-sas-accent/50 bg-sas-accent/5 transition-opacity ${soloedOut ? "opacity-40" : ""}`,
4552
4950
  children: [
4553
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
4554
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4951
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-2", children: [
4952
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4555
4953
  "span",
4556
4954
  {
4557
4955
  className: "text-[9px] font-bold tracking-widest text-sas-muted/70 select-none",
@@ -4559,8 +4957,8 @@ function PanelMasterStrip({
4559
4957
  children: "BUS"
4560
4958
  }
4561
4959
  ),
4562
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4563
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4960
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex-1 min-w-[8rem] flex flex-col gap-0.5", children: [
4961
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4564
4962
  VolumeSlider,
4565
4963
  {
4566
4964
  value: dbToSlider(bus.volume),
@@ -4568,8 +4966,8 @@ function PanelMasterStrip({
4568
4966
  disabled
4569
4967
  }
4570
4968
  ),
4571
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4572
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4969
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex flex-col gap-px", "data-testid": "bus-meter", children: [
4970
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4573
4971
  LevelMeter,
4574
4972
  {
4575
4973
  peakDb: levels?.leftDb ?? -120,
@@ -4579,7 +4977,7 @@ function PanelMasterStrip({
4579
4977
  "data-testid": "bus-meter-left"
4580
4978
  }
4581
4979
  ),
4582
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4980
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4583
4981
  LevelMeter,
4584
4982
  {
4585
4983
  peakDb: levels?.rightDb ?? -120,
@@ -4590,7 +4988,7 @@ function PanelMasterStrip({
4590
4988
  )
4591
4989
  ] })
4592
4990
  ] }),
4593
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
4991
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4594
4992
  "button",
4595
4993
  {
4596
4994
  "data-testid": "bus-mute-button",
@@ -4601,7 +4999,7 @@ function PanelMasterStrip({
4601
4999
  children: "M"
4602
5000
  }
4603
5001
  ),
4604
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5002
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4605
5003
  "button",
4606
5004
  {
4607
5005
  "data-testid": "bus-solo-button",
@@ -4612,14 +5010,14 @@ function PanelMasterStrip({
4612
5010
  children: "S"
4613
5011
  }
4614
5012
  ),
4615
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5013
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "flex items-center gap-1 max-w-[45%] min-w-0 overflow-x-auto", children: bus.fx.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
4616
5014
  "span",
4617
5015
  {
4618
5016
  "data-testid": `bus-fx-chip-${fx.index}`,
4619
5017
  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"}`,
4620
5018
  title: `${fx.name}${fx.enabled ? "" : " (bypassed)"}`,
4621
5019
  children: [
4622
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5020
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4623
5021
  "button",
4624
5022
  {
4625
5023
  "data-testid": `bus-fx-toggle-${fx.index}`,
@@ -4630,7 +5028,7 @@ function PanelMasterStrip({
4630
5028
  children: fx.enabled ? "\u25CF" : "\u25CB"
4631
5029
  }
4632
5030
  ),
4633
- onShowFxEditor ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5031
+ onShowFxEditor ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4634
5032
  "button",
4635
5033
  {
4636
5034
  "data-testid": `bus-fx-edit-${fx.index}`,
@@ -4640,8 +5038,8 @@ function PanelMasterStrip({
4640
5038
  title: `Open ${fx.name} editor`,
4641
5039
  children: fx.name
4642
5040
  }
4643
- ) : /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "max-w-[80px] truncate", children: fx.name }),
4644
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5041
+ ) : /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "max-w-[80px] truncate", children: fx.name }),
5042
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4645
5043
  "button",
4646
5044
  {
4647
5045
  "data-testid": `bus-fx-remove-${fx.index}`,
@@ -4656,7 +5054,7 @@ function PanelMasterStrip({
4656
5054
  },
4657
5055
  `${fx.index}:${fx.pluginId}`
4658
5056
  )) }),
4659
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5057
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4660
5058
  "button",
4661
5059
  {
4662
5060
  "data-testid": "bus-fx-add-button",
@@ -4668,9 +5066,9 @@ function PanelMasterStrip({
4668
5066
  }
4669
5067
  )
4670
5068
  ] }),
4671
- fxPickerOpen && /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
4672
- /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "flex items-center gap-2", children: [
4673
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5069
+ fxPickerOpen && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { "data-testid": "bus-fx-picker", className: "flex flex-col gap-2 pt-1", children: [
5070
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-2", children: [
5071
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4674
5072
  "input",
4675
5073
  {
4676
5074
  type: "text",
@@ -4680,7 +5078,7 @@ function PanelMasterStrip({
4680
5078
  className: "sas-input flex-1 px-2 py-1 text-xs"
4681
5079
  }
4682
5080
  ),
4683
- onRefreshFx && /* @__PURE__ */ (0, import_jsx_runtime19.jsx)(
5081
+ onRefreshFx && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
4684
5082
  "button",
4685
5083
  {
4686
5084
  onClick: () => onRefreshFx(),
@@ -4691,8 +5089,8 @@ function PanelMasterStrip({
4691
5089
  }
4692
5090
  )
4693
5091
  ] }),
4694
- fxLoading && availableFx.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
4695
- filtered.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime19.jsxs)(
5092
+ fxLoading && availableFx.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-xs text-sas-muted/60 text-center py-3", children: "Scanning plugins..." }) : /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "grid grid-cols-3 gap-1 max-h-[140px] overflow-y-auto", children: [
5093
+ filtered.map((fx) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
4696
5094
  "button",
4697
5095
  {
4698
5096
  "data-testid": `bus-fx-pick-${fx.pluginId}`,
@@ -4700,13 +5098,13 @@ function PanelMasterStrip({
4700
5098
  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",
4701
5099
  title: `${fx.name} by ${fx.manufacturer} (${fx.type.toUpperCase()})`,
4702
5100
  children: [
4703
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
4704
- /* @__PURE__ */ (0, import_jsx_runtime19.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
5101
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-xs font-medium truncate w-full", children: fx.name }),
5102
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("span", { className: "text-[9px] text-sas-muted/50 truncate w-full", children: fx.manufacturer || fx.type.toUpperCase() })
4705
5103
  ]
4706
5104
  },
4707
5105
  fx.pluginId
4708
5106
  )),
4709
- filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime19.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" })
5107
+ filtered.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime20.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" })
4710
5108
  ] })
4711
5109
  ] })
4712
5110
  ]
@@ -4715,18 +5113,18 @@ function PanelMasterStrip({
4715
5113
  }
4716
5114
 
4717
5115
  // src/hooks/usePanelBus.ts
4718
- var import_react20 = require("react");
5116
+ var import_react21 = require("react");
4719
5117
  var LEVELS_POLL_MS = 66;
4720
5118
  function usePanelBus(host, activeSceneId) {
4721
5119
  const supported = typeof host.getPanelBusState === "function";
4722
- const [bus, setBus] = (0, import_react20.useState)(null);
4723
- const [levels, setLevels] = (0, import_react20.useState)(null);
4724
- const [availableFx, setAvailableFx] = (0, import_react20.useState)([]);
4725
- const [fxLoading, setFxLoading] = (0, import_react20.useState)(false);
4726
- const [fxPickerOpen, setFxPickerOpen] = (0, import_react20.useState)(false);
4727
- const fxLoadedRef = (0, import_react20.useRef)(false);
4728
- const loadSeqRef = (0, import_react20.useRef)(0);
4729
- const reload = (0, import_react20.useCallback)(async () => {
5120
+ const [bus, setBus] = (0, import_react21.useState)(null);
5121
+ const [levels, setLevels] = (0, import_react21.useState)(null);
5122
+ const [availableFx, setAvailableFx] = (0, import_react21.useState)([]);
5123
+ const [fxLoading, setFxLoading] = (0, import_react21.useState)(false);
5124
+ const [fxPickerOpen, setFxPickerOpen] = (0, import_react21.useState)(false);
5125
+ const fxLoadedRef = (0, import_react21.useRef)(false);
5126
+ const loadSeqRef = (0, import_react21.useRef)(0);
5127
+ const reload = (0, import_react21.useCallback)(async () => {
4730
5128
  if (!supported || !activeSceneId || !host.getPanelBusState) {
4731
5129
  setBus(null);
4732
5130
  return;
@@ -4738,12 +5136,12 @@ function usePanelBus(host, activeSceneId) {
4738
5136
  } catch {
4739
5137
  }
4740
5138
  }, [host, activeSceneId, supported]);
4741
- (0, import_react20.useEffect)(() => {
5139
+ (0, import_react21.useEffect)(() => {
4742
5140
  setBus(null);
4743
5141
  setFxPickerOpen(false);
4744
5142
  void reload();
4745
5143
  }, [reload]);
4746
- (0, import_react20.useEffect)(() => {
5144
+ (0, import_react21.useEffect)(() => {
4747
5145
  if (!supported || !activeSceneId || !bus?.engaged || !host.getPanelBusLevels) {
4748
5146
  setLevels(null);
4749
5147
  return;
@@ -4765,7 +5163,7 @@ function usePanelBus(host, activeSceneId) {
4765
5163
  clearInterval(id);
4766
5164
  };
4767
5165
  }, [supported, activeSceneId, bus?.engaged, host]);
4768
- const loadFxList = (0, import_react20.useCallback)(
5166
+ const loadFxList = (0, import_react21.useCallback)(
4769
5167
  async (opts) => {
4770
5168
  if (!supported || !host.getAvailableFx) return;
4771
5169
  if (fxLoadedRef.current && !opts.force && !opts.rescan) return;
@@ -4781,14 +5179,14 @@ function usePanelBus(host, activeSceneId) {
4781
5179
  },
4782
5180
  [host, supported]
4783
5181
  );
4784
- const openPicker = (0, import_react20.useCallback)(
5182
+ const openPicker = (0, import_react21.useCallback)(
4785
5183
  (open) => {
4786
5184
  setFxPickerOpen(open);
4787
5185
  if (open) void loadFxList({});
4788
5186
  },
4789
5187
  [loadFxList]
4790
5188
  );
4791
- const mutate = (0, import_react20.useCallback)(
5189
+ const mutate = (0, import_react21.useCallback)(
4792
5190
  (fn) => {
4793
5191
  if (!fn || !activeSceneId) return;
4794
5192
  void (async () => {
@@ -4832,8 +5230,8 @@ function usePanelBus(host, activeSceneId) {
4832
5230
  }
4833
5231
 
4834
5232
  // src/components/DownloadPackButton.tsx
4835
- var import_react21 = require("react");
4836
- var import_jsx_runtime20 = require("react/jsx-runtime");
5233
+ var import_react22 = require("react");
5234
+ var import_jsx_runtime21 = require("react/jsx-runtime");
4837
5235
  function formatSize(bytes) {
4838
5236
  if (!bytes || bytes <= 0) return "";
4839
5237
  const gb = bytes / 1024 ** 3;
@@ -4849,10 +5247,10 @@ var DownloadPackButton = ({
4849
5247
  variant = "compact",
4850
5248
  onDownloadComplete
4851
5249
  }) => {
4852
- const [status, setStatus] = (0, import_react21.useState)("idle");
4853
- const [progress, setProgress] = (0, import_react21.useState)(0);
4854
- const [errorMessage, setErrorMessage] = (0, import_react21.useState)(null);
4855
- (0, import_react21.useEffect)(() => {
5250
+ const [status, setStatus] = (0, import_react22.useState)("idle");
5251
+ const [progress, setProgress] = (0, import_react22.useState)(0);
5252
+ const [errorMessage, setErrorMessage] = (0, import_react22.useState)(null);
5253
+ (0, import_react22.useEffect)(() => {
4856
5254
  const unsub = host.onSamplePackProgress(packId, (p) => {
4857
5255
  setStatus(p.status);
4858
5256
  setProgress(p.progress);
@@ -4867,7 +5265,7 @@ var DownloadPackButton = ({
4867
5265
  });
4868
5266
  return unsub;
4869
5267
  }, [host, packId, onDownloadComplete]);
4870
- const handleClick = (0, import_react21.useCallback)(async () => {
5268
+ const handleClick = (0, import_react22.useCallback)(async () => {
4871
5269
  if (status !== "idle" && status !== "error") return;
4872
5270
  try {
4873
5271
  setStatus("downloading");
@@ -4921,8 +5319,8 @@ var DownloadPackButton = ({
4921
5319
  } else {
4922
5320
  className = `${baseClasses} text-sas-muted hover:text-sas-accent border-sas-border hover:border-sas-accent`;
4923
5321
  }
4924
- return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { children: [
4925
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
5322
+ return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)("div", { children: [
5323
+ /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
4926
5324
  "button",
4927
5325
  {
4928
5326
  "data-testid": `download-pack-button-${packId}`,
@@ -4933,12 +5331,12 @@ var DownloadPackButton = ({
4933
5331
  children: buttonLabel
4934
5332
  }
4935
5333
  ),
4936
- variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
5334
+ variant === "large" && status === "error" && errorMessage && /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-xs text-sas-danger mt-2", "data-testid": `download-pack-error-${packId}`, children: errorMessage })
4937
5335
  ] });
4938
5336
  };
4939
5337
 
4940
5338
  // src/components/SamplePackCTACard.tsx
4941
- var import_jsx_runtime21 = require("react/jsx-runtime");
5339
+ var import_jsx_runtime22 = require("react/jsx-runtime");
4942
5340
  var SamplePackCTACard = ({
4943
5341
  host,
4944
5342
  pack,
@@ -4946,7 +5344,7 @@ var SamplePackCTACard = ({
4946
5344
  onDownloadComplete
4947
5345
  }) => {
4948
5346
  if (status === "checking") {
4949
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5347
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4950
5348
  "div",
4951
5349
  {
4952
5350
  "data-testid": `sample-pack-cta-checking-${pack.packId}`,
@@ -4957,16 +5355,16 @@ var SamplePackCTACard = ({
4957
5355
  }
4958
5356
  const headline = status === "stale" ? `${pack.displayName} update available` : `${pack.displayName} not installed`;
4959
5357
  const sublabel = status === "stale" ? `A newer version is available for download.` : pack.description;
4960
- return /* @__PURE__ */ (0, import_jsx_runtime21.jsxs)(
5358
+ return /* @__PURE__ */ (0, import_jsx_runtime22.jsxs)(
4961
5359
  "div",
4962
5360
  {
4963
5361
  "data-testid": `sample-pack-cta-${pack.packId}`,
4964
5362
  className: "flex flex-col items-center justify-center py-12 px-6 text-center",
4965
5363
  children: [
4966
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
4967
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
4968
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
4969
- /* @__PURE__ */ (0, import_jsx_runtime21.jsx)(
5364
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "text-sm uppercase tracking-wide text-sas-muted mb-2", children: status === "stale" ? "Update available" : "Sample library not installed" }),
5365
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "text-base text-sas-text mb-1", children: headline }),
5366
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)("div", { className: "text-xs text-sas-muted mb-6 max-w-md", children: sublabel }),
5367
+ /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
4970
5368
  DownloadPackButton,
4971
5369
  {
4972
5370
  host,
@@ -4983,7 +5381,7 @@ var SamplePackCTACard = ({
4983
5381
  };
4984
5382
 
4985
5383
  // src/components/WaveformView.tsx
4986
- var import_react22 = require("react");
5384
+ var import_react23 = require("react");
4987
5385
 
4988
5386
  // src/components/waveform.ts
4989
5387
  function computePeaks(audioBuffer, bins, targetSamples) {
@@ -5046,7 +5444,7 @@ function drawWaveform(canvas, peaks, options = {}) {
5046
5444
  }
5047
5445
 
5048
5446
  // src/components/WaveformView.tsx
5049
- var import_jsx_runtime22 = require("react/jsx-runtime");
5447
+ var import_jsx_runtime23 = require("react/jsx-runtime");
5050
5448
  var WaveformView = ({
5051
5449
  host,
5052
5450
  filePath,
@@ -5055,9 +5453,9 @@ var WaveformView = ({
5055
5453
  fillStyle,
5056
5454
  targetSamples
5057
5455
  }) => {
5058
- const canvasRef = (0, import_react22.useRef)(null);
5059
- const [peaks, setPeaks] = (0, import_react22.useState)(null);
5060
- (0, import_react22.useEffect)(() => {
5456
+ const canvasRef = (0, import_react23.useRef)(null);
5457
+ const [peaks, setPeaks] = (0, import_react23.useState)(null);
5458
+ (0, import_react23.useEffect)(() => {
5061
5459
  let cancelled = false;
5062
5460
  let audioContext = null;
5063
5461
  (async () => {
@@ -5083,7 +5481,7 @@ var WaveformView = ({
5083
5481
  cancelled = true;
5084
5482
  };
5085
5483
  }, [host, filePath, bins, targetSamples]);
5086
- (0, import_react22.useEffect)(() => {
5484
+ (0, import_react23.useEffect)(() => {
5087
5485
  if (!peaks) return;
5088
5486
  const canvas = canvasRef.current;
5089
5487
  if (!canvas) return;
@@ -5094,7 +5492,7 @@ var WaveformView = ({
5094
5492
  observer.observe(canvas);
5095
5493
  return () => observer.disconnect();
5096
5494
  }, [peaks, fillStyle]);
5097
- return /* @__PURE__ */ (0, import_jsx_runtime22.jsx)(
5495
+ return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5098
5496
  "canvas",
5099
5497
  {
5100
5498
  ref: canvasRef,
@@ -5105,8 +5503,8 @@ var WaveformView = ({
5105
5503
  };
5106
5504
 
5107
5505
  // src/components/ScrollingWaveform.tsx
5108
- var import_react23 = require("react");
5109
- var import_jsx_runtime23 = require("react/jsx-runtime");
5506
+ var import_react24 = require("react");
5507
+ var import_jsx_runtime24 = require("react/jsx-runtime");
5110
5508
  var ScrollingWaveform = ({
5111
5509
  getPeakDb,
5112
5510
  active,
@@ -5114,11 +5512,11 @@ var ScrollingWaveform = ({
5114
5512
  className,
5115
5513
  fillStyle
5116
5514
  }) => {
5117
- const canvasRef = (0, import_react23.useRef)(null);
5118
- const ringRef = (0, import_react23.useRef)(new Float32Array(columns));
5119
- const writeIdxRef = (0, import_react23.useRef)(0);
5120
- const rafRef = (0, import_react23.useRef)(null);
5121
- (0, import_react23.useEffect)(() => {
5515
+ const canvasRef = (0, import_react24.useRef)(null);
5516
+ const ringRef = (0, import_react24.useRef)(new Float32Array(columns));
5517
+ const writeIdxRef = (0, import_react24.useRef)(0);
5518
+ const rafRef = (0, import_react24.useRef)(null);
5519
+ (0, import_react24.useEffect)(() => {
5122
5520
  if (ringRef.current.length !== columns) {
5123
5521
  const next = new Float32Array(columns);
5124
5522
  const prev = ringRef.current;
@@ -5130,7 +5528,7 @@ var ScrollingWaveform = ({
5130
5528
  writeIdxRef.current = writeIdxRef.current % columns;
5131
5529
  }
5132
5530
  }, [columns]);
5133
- (0, import_react23.useEffect)(() => {
5531
+ (0, import_react24.useEffect)(() => {
5134
5532
  if (!active) {
5135
5533
  if (rafRef.current !== null) {
5136
5534
  cancelAnimationFrame(rafRef.current);
@@ -5182,7 +5580,7 @@ var ScrollingWaveform = ({
5182
5580
  }
5183
5581
  };
5184
5582
  }, [active, getPeakDb, fillStyle]);
5185
- return /* @__PURE__ */ (0, import_jsx_runtime23.jsx)(
5583
+ return /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5186
5584
  "canvas",
5187
5585
  {
5188
5586
  ref: canvasRef,
@@ -5193,8 +5591,8 @@ var ScrollingWaveform = ({
5193
5591
  };
5194
5592
 
5195
5593
  // src/components/OffsetScrubber.tsx
5196
- var import_react24 = require("react");
5197
- var import_jsx_runtime24 = require("react/jsx-runtime");
5594
+ var import_react25 = require("react");
5595
+ var import_jsx_runtime25 = require("react/jsx-runtime");
5198
5596
  var SLIDER_HEIGHT_PX = 28;
5199
5597
  var TICK_HEIGHT_PX = 14;
5200
5598
  var DOWNBEAT_TICK_HEIGHT_PX = 22;
@@ -5207,40 +5605,40 @@ function OffsetScrubber({
5207
5605
  onChange,
5208
5606
  disabled = false
5209
5607
  }) {
5210
- const trackRef = (0, import_react24.useRef)(null);
5211
- const [draftOffset, setDraftOffset] = (0, import_react24.useState)(offsetSamples);
5212
- const [isDragging, setIsDragging] = (0, import_react24.useState)(false);
5213
- (0, import_react24.useEffect)(() => {
5608
+ const trackRef = (0, import_react25.useRef)(null);
5609
+ const [draftOffset, setDraftOffset] = (0, import_react25.useState)(offsetSamples);
5610
+ const [isDragging, setIsDragging] = (0, import_react25.useState)(false);
5611
+ (0, import_react25.useEffect)(() => {
5214
5612
  if (!isDragging) setDraftOffset(offsetSamples);
5215
5613
  }, [offsetSamples, isDragging]);
5216
5614
  const sampleRate = cuePoints?.sample_rate ?? 44100;
5217
5615
  const detectedBpm = cuePoints?.detected_bpm ?? projectBpm;
5218
- const beatsForRange = (0, import_react24.useMemo)(() => {
5616
+ const beatsForRange = (0, import_react25.useMemo)(() => {
5219
5617
  return Math.round(60 / projectBpm * sampleRate);
5220
5618
  }, [projectBpm, sampleRate]);
5221
5619
  const rangeSamples = beatsForRange * meter;
5222
- const sampleToFraction = (0, import_react24.useCallback)(
5620
+ const sampleToFraction = (0, import_react25.useCallback)(
5223
5621
  (sample) => {
5224
5622
  const clamped = Math.max(-rangeSamples, Math.min(rangeSamples, sample));
5225
5623
  return (clamped + rangeSamples) / (2 * rangeSamples);
5226
5624
  },
5227
5625
  [rangeSamples]
5228
5626
  );
5229
- const fractionToSample = (0, import_react24.useCallback)(
5627
+ const fractionToSample = (0, import_react25.useCallback)(
5230
5628
  (fraction) => {
5231
5629
  const clamped = Math.max(0, Math.min(1, fraction));
5232
5630
  return Math.round(clamped * 2 * rangeSamples - rangeSamples);
5233
5631
  },
5234
5632
  [rangeSamples]
5235
5633
  );
5236
- const snapTargets = (0, import_react24.useMemo)(() => {
5634
+ const snapTargets = (0, import_react25.useMemo)(() => {
5237
5635
  if (!cuePoints || cuePoints.beats.length === 0) return [];
5238
5636
  const downbeat = cuePoints.beats[0];
5239
5637
  const positives = cuePoints.beats.map((b) => b - downbeat);
5240
5638
  const negatives = positives.slice(1).map((p) => -p);
5241
5639
  return [...negatives, ...positives].sort((a, b) => a - b);
5242
5640
  }, [cuePoints]);
5243
- const snapToBeat = (0, import_react24.useCallback)(
5641
+ const snapToBeat = (0, import_react25.useCallback)(
5244
5642
  (sample) => {
5245
5643
  if (snapTargets.length === 0) return sample;
5246
5644
  let best = snapTargets[0];
@@ -5256,7 +5654,7 @@ function OffsetScrubber({
5256
5654
  },
5257
5655
  [snapTargets]
5258
5656
  );
5259
- const handlePointerDown = (0, import_react24.useCallback)(
5657
+ const handlePointerDown = (0, import_react25.useCallback)(
5260
5658
  (e) => {
5261
5659
  if (disabled || !cuePoints) return;
5262
5660
  e.preventDefault();
@@ -5290,7 +5688,7 @@ function OffsetScrubber({
5290
5688
  },
5291
5689
  [disabled, cuePoints, fractionToSample, onChange, snapToBeat]
5292
5690
  );
5293
- const handleResetToZero = (0, import_react24.useCallback)(() => {
5691
+ const handleResetToZero = (0, import_react25.useCallback)(() => {
5294
5692
  if (disabled) return;
5295
5693
  setDraftOffset(0);
5296
5694
  onChange(0);
@@ -5298,7 +5696,7 @@ function OffsetScrubber({
5298
5696
  const thumbFraction = sampleToFraction(draftOffset);
5299
5697
  const thumbLeftPct = `${(thumbFraction * 100).toFixed(2)}%`;
5300
5698
  const bpmMismatch = cuePoints?.detected_bpm != null && Math.abs(cuePoints.detected_bpm - projectBpm) > 1;
5301
- const ticks = (0, import_react24.useMemo)(() => {
5699
+ const ticks = (0, import_react25.useMemo)(() => {
5302
5700
  if (!cuePoints) return [];
5303
5701
  const downbeat = cuePoints.beats[0] ?? 0;
5304
5702
  return cuePoints.beats.map((b, i) => {
@@ -5309,9 +5707,9 @@ function OffsetScrubber({
5309
5707
  });
5310
5708
  }, [cuePoints, sampleToFraction]);
5311
5709
  const isDisabled = disabled || !cuePoints || cuePoints.beats.length === 0;
5312
- return /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5313
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5314
- /* @__PURE__ */ (0, import_jsx_runtime24.jsxs)(
5710
+ return /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { "data-testid": "offset-scrubber", className: "flex items-center gap-2 w-full", children: [
5711
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)("span", { className: "text-[9px] text-sas-muted/60 uppercase tracking-wide flex-shrink-0", children: "Align" }),
5712
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
5315
5713
  "div",
5316
5714
  {
5317
5715
  ref: trackRef,
@@ -5327,7 +5725,7 @@ function OffsetScrubber({
5327
5725
  "aria-valuenow": draftOffset,
5328
5726
  "aria-disabled": isDisabled,
5329
5727
  children: [
5330
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5728
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5331
5729
  "div",
5332
5730
  {
5333
5731
  "aria-hidden": "true",
@@ -5335,7 +5733,7 @@ function OffsetScrubber({
5335
5733
  style: { left: "50%" }
5336
5734
  }
5337
5735
  ),
5338
- ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5736
+ ticks.map((t) => /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5339
5737
  "div",
5340
5738
  {
5341
5739
  "data-testid": t.isDownbeat ? "offset-tick-downbeat" : "offset-tick",
@@ -5350,7 +5748,7 @@ function OffsetScrubber({
5350
5748
  },
5351
5749
  t.i
5352
5750
  )),
5353
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5751
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5354
5752
  "div",
5355
5753
  {
5356
5754
  "data-testid": "offset-scrubber-thumb",
@@ -5367,7 +5765,7 @@ function OffsetScrubber({
5367
5765
  ]
5368
5766
  }
5369
5767
  ),
5370
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5768
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5371
5769
  "span",
5372
5770
  {
5373
5771
  "data-testid": "offset-scrubber-readout",
@@ -5375,7 +5773,7 @@ function OffsetScrubber({
5375
5773
  children: formatOffset(draftOffset, sampleRate)
5376
5774
  }
5377
5775
  ),
5378
- /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5776
+ /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5379
5777
  "button",
5380
5778
  {
5381
5779
  type: "button",
@@ -5387,7 +5785,7 @@ function OffsetScrubber({
5387
5785
  children: "\u2316"
5388
5786
  }
5389
5787
  ),
5390
- bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime24.jsx)(
5788
+ bpmMismatch && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
5391
5789
  "span",
5392
5790
  {
5393
5791
  "data-testid": "offset-bpm-mismatch",
@@ -5459,16 +5857,16 @@ function synthesizeCuePoints({
5459
5857
  }
5460
5858
 
5461
5859
  // src/panel-core/useGeneratorPanelCore.tsx
5462
- var import_react29 = require("react");
5860
+ var import_react30 = require("react");
5463
5861
 
5464
5862
  // src/hooks/useSceneState.ts
5465
- var import_react25 = require("react");
5863
+ var import_react26 = require("react");
5466
5864
  function useSceneState(activeSceneId, initialValue) {
5467
- const [stateMap, setStateMap] = (0, import_react25.useState)(() => /* @__PURE__ */ new Map());
5468
- const activeSceneIdRef = (0, import_react25.useRef)(activeSceneId);
5865
+ const [stateMap, setStateMap] = (0, import_react26.useState)(() => /* @__PURE__ */ new Map());
5866
+ const activeSceneIdRef = (0, import_react26.useRef)(activeSceneId);
5469
5867
  activeSceneIdRef.current = activeSceneId;
5470
5868
  const currentValue = activeSceneId !== null && stateMap.has(activeSceneId) ? stateMap.get(activeSceneId) : initialValue;
5471
- const setForCurrentScene = (0, import_react25.useCallback)((value) => {
5869
+ const setForCurrentScene = (0, import_react26.useCallback)((value) => {
5472
5870
  const sid = activeSceneIdRef.current;
5473
5871
  if (sid === null) return;
5474
5872
  setStateMap((prev) => {
@@ -5479,7 +5877,7 @@ function useSceneState(activeSceneId, initialValue) {
5479
5877
  return newMap;
5480
5878
  });
5481
5879
  }, [initialValue]);
5482
- const setForScene = (0, import_react25.useCallback)((sceneId, value) => {
5880
+ const setForScene = (0, import_react26.useCallback)((sceneId, value) => {
5483
5881
  setStateMap((prev) => {
5484
5882
  const current = prev.has(sceneId) ? prev.get(sceneId) : initialValue;
5485
5883
  const next = typeof value === "function" ? value(current) : value;
@@ -5492,10 +5890,10 @@ function useSceneState(activeSceneId, initialValue) {
5492
5890
  }
5493
5891
 
5494
5892
  // src/hooks/useAnySolo.ts
5495
- var import_react26 = require("react");
5893
+ var import_react27 = require("react");
5496
5894
  function useAnySolo(host) {
5497
- const [anySolo, setAnySolo] = (0, import_react26.useState)(false);
5498
- (0, import_react26.useEffect)(() => {
5895
+ const [anySolo, setAnySolo] = (0, import_react27.useState)(false);
5896
+ (0, import_react27.useEffect)(() => {
5499
5897
  let active = true;
5500
5898
  const refresh = () => {
5501
5899
  host.isAnySoloActive().then((v) => {
@@ -5514,7 +5912,7 @@ function useAnySolo(host) {
5514
5912
  }
5515
5913
 
5516
5914
  // src/hooks/useSoundHistory.ts
5517
- var import_react27 = require("react");
5915
+ var import_react28 = require("react");
5518
5916
  var EMPTY = { entries: [], cursor: -1 };
5519
5917
  function sameDescriptor(a, b) {
5520
5918
  if (a === b) return true;
@@ -5526,14 +5924,14 @@ function sameDescriptor(a, b) {
5526
5924
  }
5527
5925
  function useSoundHistory(applySound, opts = {}) {
5528
5926
  const max = Math.max(2, opts.max ?? 24);
5529
- const applyRef = (0, import_react27.useRef)(applySound);
5927
+ const applyRef = (0, import_react28.useRef)(applySound);
5530
5928
  applyRef.current = applySound;
5531
- const onChangeRef = (0, import_react27.useRef)(opts.onChange);
5929
+ const onChangeRef = (0, import_react28.useRef)(opts.onChange);
5532
5930
  onChangeRef.current = opts.onChange;
5533
- const dataRef = (0, import_react27.useRef)({});
5534
- const [, setVersion] = (0, import_react27.useState)(0);
5535
- const bump = (0, import_react27.useCallback)(() => setVersion((v) => v + 1), []);
5536
- const commit = (0, import_react27.useCallback)(
5931
+ const dataRef = (0, import_react28.useRef)({});
5932
+ const [, setVersion] = (0, import_react28.useState)(0);
5933
+ const bump = (0, import_react28.useCallback)(() => setVersion((v) => v + 1), []);
5934
+ const commit = (0, import_react28.useCallback)(
5537
5935
  (trackId, next, notify) => {
5538
5936
  dataRef.current = { ...dataRef.current, [trackId]: next };
5539
5937
  bump();
@@ -5541,7 +5939,7 @@ function useSoundHistory(applySound, opts = {}) {
5541
5939
  },
5542
5940
  [bump]
5543
5941
  );
5544
- const record = (0, import_react27.useCallback)(
5942
+ const record = (0, import_react28.useCallback)(
5545
5943
  (trackId, descriptor, label) => {
5546
5944
  const h = dataRef.current[trackId];
5547
5945
  const current = h && h.cursor >= 0 ? h.entries[h.cursor] : void 0;
@@ -5556,7 +5954,7 @@ function useSoundHistory(applySound, opts = {}) {
5556
5954
  },
5557
5955
  [max, commit]
5558
5956
  );
5559
- const restoreTo = (0, import_react27.useCallback)(
5957
+ const restoreTo = (0, import_react28.useCallback)(
5560
5958
  async (trackId, index) => {
5561
5959
  const h = dataRef.current[trackId];
5562
5960
  if (!h || index < 0 || index >= h.entries.length || index === h.cursor) return false;
@@ -5566,7 +5964,7 @@ function useSoundHistory(applySound, opts = {}) {
5566
5964
  },
5567
5965
  [commit]
5568
5966
  );
5569
- const undo = (0, import_react27.useCallback)(
5967
+ const undo = (0, import_react28.useCallback)(
5570
5968
  (trackId) => {
5571
5969
  const h = dataRef.current[trackId];
5572
5970
  if (!h || h.cursor <= 0) return Promise.resolve(false);
@@ -5574,7 +5972,7 @@ function useSoundHistory(applySound, opts = {}) {
5574
5972
  },
5575
5973
  [restoreTo]
5576
5974
  );
5577
- const toggleFavorite = (0, import_react27.useCallback)(
5975
+ const toggleFavorite = (0, import_react28.useCallback)(
5578
5976
  (trackId, index) => {
5579
5977
  const h = dataRef.current[trackId];
5580
5978
  if (!h || index < 0 || index >= h.entries.length) return;
@@ -5583,7 +5981,7 @@ function useSoundHistory(applySound, opts = {}) {
5583
5981
  },
5584
5982
  [commit]
5585
5983
  );
5586
- const restore = (0, import_react27.useCallback)(
5984
+ const restore = (0, import_react28.useCallback)(
5587
5985
  (trackId, state) => {
5588
5986
  const entries = Array.isArray(state?.entries) ? [...state.entries] : [];
5589
5987
  const raw = typeof state?.cursor === "number" ? state.cursor : entries.length - 1;
@@ -5592,15 +5990,15 @@ function useSoundHistory(applySound, opts = {}) {
5592
5990
  },
5593
5991
  [commit]
5594
5992
  );
5595
- const list = (0, import_react27.useCallback)(
5993
+ const list = (0, import_react28.useCallback)(
5596
5994
  (trackId) => dataRef.current[trackId] ?? EMPTY,
5597
5995
  []
5598
5996
  );
5599
- const canUndo = (0, import_react27.useCallback)((trackId) => {
5997
+ const canUndo = (0, import_react28.useCallback)((trackId) => {
5600
5998
  const h = dataRef.current[trackId];
5601
5999
  return !!h && h.cursor > 0;
5602
6000
  }, []);
5603
- const clear = (0, import_react27.useCallback)(
6001
+ const clear = (0, import_react28.useCallback)(
5604
6002
  (trackId) => {
5605
6003
  if (dataRef.current[trackId]) {
5606
6004
  const next = { ...dataRef.current };
@@ -5612,11 +6010,11 @@ function useSoundHistory(applySound, opts = {}) {
5612
6010
  },
5613
6011
  [bump]
5614
6012
  );
5615
- const reset = (0, import_react27.useCallback)(() => {
6013
+ const reset = (0, import_react28.useCallback)(() => {
5616
6014
  dataRef.current = {};
5617
6015
  bump();
5618
6016
  }, [bump]);
5619
- return (0, import_react27.useMemo)(
6017
+ return (0, import_react28.useMemo)(
5620
6018
  () => ({ record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite }),
5621
6019
  [record, undo, restoreTo, list, canUndo, clear, reset, restore, toggleFavorite]
5622
6020
  );
@@ -5749,7 +6147,7 @@ function resolveTrackGroups(parsedGroups, tracks, getDbId, opts = {}) {
5749
6147
  }
5750
6148
 
5751
6149
  // src/panel-core/useTransitionOps.ts
5752
- var import_react28 = require("react");
6150
+ var import_react29 = require("react");
5753
6151
  function useTransitionOps({
5754
6152
  host,
5755
6153
  adapter,
@@ -5766,8 +6164,8 @@ function useTransitionOps({
5766
6164
  resolvedFades
5767
6165
  }) {
5768
6166
  const { identity } = adapter;
5769
- const appliedFadeAutomationRef = (0, import_react28.useRef)(/* @__PURE__ */ new Set());
5770
- const applyCrossfadeAutomation = (0, import_react28.useCallback)(
6167
+ const appliedFadeAutomationRef = (0, import_react29.useRef)(/* @__PURE__ */ new Set());
6168
+ const applyCrossfadeAutomation = (0, import_react29.useCallback)(
5771
6169
  async (originTrackId, targetTrackId, bars, bpm, sliderPos) => {
5772
6170
  if (host.setTrackVolumeAutomation) {
5773
6171
  const curves = buildCrossfadeVolumeCurves(bars, bpm, sliderPos);
@@ -5784,7 +6182,7 @@ function useTransitionOps({
5784
6182
  },
5785
6183
  [host]
5786
6184
  );
5787
- const applyFadeAutomation = (0, import_react28.useCallback)(
6185
+ const applyFadeAutomation = (0, import_react29.useCallback)(
5788
6186
  async (trackId, direction, bars, bpm, sliderPos, gesture) => {
5789
6187
  if (!host.setTrackVolumeAutomation) return;
5790
6188
  const points = buildFadeVolumeCurve(bars, bpm, direction, sliderPos, gesture);
@@ -5793,8 +6191,25 @@ function useTransitionOps({
5793
6191
  },
5794
6192
  [host]
5795
6193
  );
5796
- const [isCreatingCrossfade, setIsCreatingCrossfade] = (0, import_react28.useState)(false);
5797
- const handleCreateCrossfade = (0, import_react28.useCallback)(
6194
+ const copyTrackFx = (0, import_react29.useCallback)(
6195
+ async (newTrackId, sourceDbId) => {
6196
+ if (typeof host.copyTrackFxFrom !== "function") return;
6197
+ try {
6198
+ const res = await host.copyTrackFxFrom(newTrackId, sourceDbId);
6199
+ if (res.externalMissing.length > 0) {
6200
+ host.showToast(
6201
+ "warning",
6202
+ "Some FX not copied",
6203
+ `Missing plugin(s): ${res.externalMissing.join(", ")}`
6204
+ );
6205
+ }
6206
+ } catch {
6207
+ }
6208
+ },
6209
+ [host]
6210
+ );
6211
+ const [isCreatingCrossfade, setIsCreatingCrossfade] = (0, import_react29.useState)(false);
6212
+ const handleCreateCrossfade = (0, import_react29.useCallback)(
5798
6213
  async (origin, target) => {
5799
6214
  const scene = activeSceneId;
5800
6215
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -5871,6 +6286,8 @@ function useTransitionOps({
5871
6286
  };
5872
6287
  const originLabel = await copySound(top.id, origin.dbId);
5873
6288
  const targetLabel = await copySound(bottom.id, target.dbId);
6289
+ await copyTrackFx(top.id, origin.dbId);
6290
+ await copyTrackFx(bottom.id, target.dbId);
5874
6291
  await applyCrossfadeAutomation(top.id, bottom.id, mc.bars, mc.bpm, 0.5);
5875
6292
  const groupId = top.dbId;
5876
6293
  const originMeta = {
@@ -5922,8 +6339,9 @@ function useTransitionOps({
5922
6339
  loadTracks
5923
6340
  ]
5924
6341
  );
5925
- const [isCreatingFade, setIsCreatingFade] = (0, import_react28.useState)(false);
5926
- const handleCreateFade = (0, import_react28.useCallback)(
6342
+ const [isCreatingGroupFade, setIsCreatingGroupFade] = (0, import_react29.useState)(false);
6343
+ const [isCreatingFade, setIsCreatingFade] = (0, import_react29.useState)(false);
6344
+ const handleCreateFade = (0, import_react29.useCallback)(
5927
6345
  async (selection, direction, gesture) => {
5928
6346
  const scene = activeSceneId;
5929
6347
  const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
@@ -5990,6 +6408,7 @@ function useTransitionOps({
5990
6408
  soundLabel = await adapter.sound.copySnapshot(track.id, snap);
5991
6409
  }
5992
6410
  }
6411
+ await copyTrackFx(track.id, selection.dbId);
5993
6412
  await applyFadeAutomation(track.id, direction, mc.bars, mc.bpm, 0.5, gesture);
5994
6413
  appliedFadeAutomationRef.current.add(track.id);
5995
6414
  const meta = {
@@ -6033,7 +6452,7 @@ function useTransitionOps({
6033
6452
  loadTracks
6034
6453
  ]
6035
6454
  );
6036
- const handleCrossfadeMute = (0, import_react28.useCallback)(
6455
+ const handleCrossfadeMute = (0, import_react29.useCallback)(
6037
6456
  (pair) => {
6038
6457
  const newMuted = !pair.origin.runtimeState.muted;
6039
6458
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -6048,7 +6467,7 @@ function useTransitionOps({
6048
6467
  },
6049
6468
  [host, setTracks]
6050
6469
  );
6051
- const handleCrossfadeSolo = (0, import_react28.useCallback)(
6470
+ const handleCrossfadeSolo = (0, import_react29.useCallback)(
6052
6471
  (pair) => {
6053
6472
  const newSolo = !pair.origin.runtimeState.solo;
6054
6473
  for (const id of [pair.origin.handle.id, pair.target.handle.id]) {
@@ -6063,7 +6482,7 @@ function useTransitionOps({
6063
6482
  },
6064
6483
  [host, setTracks]
6065
6484
  );
6066
- const handleCrossfadeDelete = (0, import_react28.useCallback)(
6485
+ const handleCrossfadeDelete = (0, import_react29.useCallback)(
6067
6486
  async (pair) => {
6068
6487
  try {
6069
6488
  for (const member of [pair.origin, pair.target]) {
@@ -6089,8 +6508,8 @@ function useTransitionOps({
6089
6508
  },
6090
6509
  [host, activeSceneId, setCrossfadePairsMeta, setTracks]
6091
6510
  );
6092
- const crossfadeSliderTimers = (0, import_react28.useRef)({});
6093
- const handleCrossfadeSlider = (0, import_react28.useCallback)(
6511
+ const crossfadeSliderTimers = (0, import_react29.useRef)({});
6512
+ const handleCrossfadeSlider = (0, import_react29.useCallback)(
6094
6513
  (pair, pos) => {
6095
6514
  setCrossfadePairsMeta(
6096
6515
  (prev) => prev.map((p) => p.groupId === pair.groupId ? { ...p, sliderPos: pos } : p)
@@ -6123,7 +6542,7 @@ function useTransitionOps({
6123
6542
  },
6124
6543
  [host, activeSceneId, applyCrossfadeAutomation, setCrossfadePairsMeta]
6125
6544
  );
6126
- const handleFadeDelete = (0, import_react28.useCallback)(
6545
+ const handleFadeDelete = (0, import_react29.useCallback)(
6127
6546
  async (fade) => {
6128
6547
  try {
6129
6548
  await host.deleteTrack(fade.track.handle.id);
@@ -6143,8 +6562,8 @@ function useTransitionOps({
6143
6562
  },
6144
6563
  [host, activeSceneId, setFadesMeta, setTracks]
6145
6564
  );
6146
- const fadeSliderTimers = (0, import_react28.useRef)({});
6147
- const handleFadeSlider = (0, import_react28.useCallback)(
6565
+ const fadeSliderTimers = (0, import_react29.useRef)({});
6566
+ const handleFadeSlider = (0, import_react29.useCallback)(
6148
6567
  (fade, pos) => {
6149
6568
  setFadesMeta(
6150
6569
  (prev) => prev.map((f) => f.dbId === fade.dbId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f)
@@ -6174,8 +6593,190 @@ function useTransitionOps({
6174
6593
  },
6175
6594
  [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6176
6595
  );
6177
- const lastResyncKeyRef = (0, import_react28.useRef)("");
6178
- (0, import_react28.useEffect)(() => {
6596
+ const handleCreateVerbatimGroupFade = (0, import_react29.useCallback)(
6597
+ async (subject, direction) => {
6598
+ const groupAdapter = adapter.transitionGroup;
6599
+ if (!groupAdapter) throw new Error("This panel does not support group fades.");
6600
+ const scene = activeSceneId;
6601
+ const fromSceneId = sceneContext?.transitionFromSceneId ?? "";
6602
+ const toSceneId = sceneContext?.transitionToSceneId ?? "";
6603
+ if (!scene) throw new Error("No active scene.");
6604
+ if (!isConnected) throw new Error("Systems not connected.");
6605
+ const sourceSceneId = direction === "out" ? fromSceneId : toSceneId;
6606
+ const members = await groupAdapter.expandSubject(sourceSceneId, subject.dbId);
6607
+ if (members.length === 0) {
6608
+ throw new Error("Nothing to fade \u2014 the source group has no members.");
6609
+ }
6610
+ if (tracks.length + members.length > identity.maxTracks) {
6611
+ throw new Error(`Not enough track slots for this fade (needs ${members.length}).`);
6612
+ }
6613
+ setIsCreatingGroupFade(true);
6614
+ const created = [];
6615
+ try {
6616
+ const mc = await host.getMusicalContext();
6617
+ const sliderPos = groupAdapter.defaultSliderPos?.(direction) ?? 0.5;
6618
+ const gesture = "volume";
6619
+ const clipEnd = mc.bars * 4 * 60 / mc.bpm;
6620
+ const maxBeats = mc.bars * 4;
6621
+ for (const member of members) {
6622
+ const handle = await host.createTrack({
6623
+ name: `${identity.trackNamePrefix}-${Date.now()}-fade-${direction}-v${member.memberIndex}`,
6624
+ ...adapter.createTrackOptions()
6625
+ });
6626
+ created.push({ handle, member, soundLabel: "default" });
6627
+ const role = member.role ?? subject.role ?? "";
6628
+ if (role) await host.setTrackRole(handle.id, role).catch(() => {
6629
+ });
6630
+ const srcMidi = host.readImportableTrackMidi ? await host.readImportableTrackMidi(member.dbId) : { clips: [] };
6631
+ const srcNotes = srcMidi.clips[0]?.notes ?? [];
6632
+ const notes = srcNotes.filter((n) => n.startBeat < maxBeats).map((n) => ({
6633
+ ...n,
6634
+ durationBeats: Math.min(n.durationBeats, maxBeats - n.startBeat)
6635
+ }));
6636
+ if (notes.length > 0) {
6637
+ const clip = { startTime: 0, endTime: clipEnd, tempo: mc.bpm, notes };
6638
+ await host.writeMidiClip(handle.id, clip);
6639
+ }
6640
+ if (host.getTrackSound) {
6641
+ const snap = await host.getTrackSound(member.dbId);
6642
+ if (snap && snap.kind === adapter.sound.acceptedSnapshotKind) {
6643
+ created[created.length - 1].soundLabel = await adapter.sound.copySnapshot(
6644
+ handle.id,
6645
+ snap
6646
+ );
6647
+ }
6648
+ }
6649
+ await copyTrackFx(handle.id, member.dbId);
6650
+ await applyFadeAutomation(handle.id, direction, mc.bars, mc.bpm, sliderPos, gesture);
6651
+ appliedFadeAutomationRef.current.add(handle.id);
6652
+ }
6653
+ const groupId = created[0].handle.dbId;
6654
+ for (const { handle, member, soundLabel } of created) {
6655
+ const meta = {
6656
+ direction,
6657
+ gesture,
6658
+ sourceTrackDbId: member.dbId,
6659
+ sourceSceneId,
6660
+ sourceName: member.name,
6661
+ soundLabel,
6662
+ sliderPos,
6663
+ groupId,
6664
+ memberIndex: member.memberIndex,
6665
+ memberLabel: member.memberLabel
6666
+ };
6667
+ await host.setSceneData(scene, `track:${handle.dbId}:fade`, meta);
6668
+ }
6669
+ await groupAdapter.writeGroupMetas(
6670
+ scene,
6671
+ created.map(({ handle, member }) => ({ newDbId: handle.dbId, member })),
6672
+ groupId
6673
+ );
6674
+ await loadTracks(true);
6675
+ host.showToast(
6676
+ "success",
6677
+ direction === "in" ? "Fade in created" : "Fade out created",
6678
+ subject.name
6679
+ );
6680
+ } catch (err) {
6681
+ for (const { handle } of [...created].reverse()) {
6682
+ try {
6683
+ await host.deleteTrack(handle.id);
6684
+ } catch {
6685
+ }
6686
+ await host.deleteSceneData(scene, `track:${handle.dbId}:fade`).catch(() => {
6687
+ });
6688
+ for (const suffix of groupAdapter.cleanupKeySuffixes) {
6689
+ await host.deleteSceneData(scene, `track:${handle.dbId}:${suffix}`).catch(() => {
6690
+ });
6691
+ }
6692
+ }
6693
+ throw err instanceof Error ? err : new Error(String(err));
6694
+ } finally {
6695
+ setIsCreatingGroupFade(false);
6696
+ }
6697
+ },
6698
+ [
6699
+ host,
6700
+ adapter,
6701
+ identity,
6702
+ activeSceneId,
6703
+ isConnected,
6704
+ tracks.length,
6705
+ sceneContext,
6706
+ applyFadeAutomation,
6707
+ copyTrackFx,
6708
+ loadTracks
6709
+ ]
6710
+ );
6711
+ const groupFadeSliderTimers = (0, import_react29.useRef)({});
6712
+ const handleGroupFadeSlider = (0, import_react29.useCallback)(
6713
+ (group, pos) => {
6714
+ setFadesMeta(
6715
+ (prev) => prev.map(
6716
+ (f) => f.meta.groupId === group.groupId ? { ...f, meta: { ...f.meta, sliderPos: pos } } : f
6717
+ )
6718
+ );
6719
+ if (groupFadeSliderTimers.current[group.groupId]) {
6720
+ clearTimeout(groupFadeSliderTimers.current[group.groupId]);
6721
+ }
6722
+ groupFadeSliderTimers.current[group.groupId] = setTimeout(() => {
6723
+ void (async () => {
6724
+ const mc = await host.getMusicalContext();
6725
+ const sceneData = activeSceneId ? await host.getAllSceneData(activeSceneId) : null;
6726
+ for (const member of group.members) {
6727
+ await applyFadeAutomation(
6728
+ member.track.handle.id,
6729
+ group.direction,
6730
+ mc.bars,
6731
+ mc.bpm,
6732
+ pos,
6733
+ group.gesture
6734
+ );
6735
+ if (activeSceneId && sceneData) {
6736
+ const meta = asFadeMeta(sceneData[`track:${member.dbId}:fade`]);
6737
+ if (meta) {
6738
+ host.setSceneData(activeSceneId, `track:${member.dbId}:fade`, {
6739
+ ...meta,
6740
+ sliderPos: pos
6741
+ }).catch(() => {
6742
+ });
6743
+ }
6744
+ }
6745
+ }
6746
+ })();
6747
+ }, 200);
6748
+ },
6749
+ [host, activeSceneId, applyFadeAutomation, setFadesMeta]
6750
+ );
6751
+ const handleGroupFadeDelete = (0, import_react29.useCallback)(
6752
+ async (group) => {
6753
+ const suffixes = ["fade", ...adapter.transitionGroup?.cleanupKeySuffixes ?? []];
6754
+ try {
6755
+ for (const member of group.members) {
6756
+ await host.deleteTrack(member.track.handle.id);
6757
+ if (activeSceneId) {
6758
+ for (const suffix of suffixes) {
6759
+ await host.deleteSceneData(activeSceneId, `track:${member.dbId}:${suffix}`).catch(() => {
6760
+ });
6761
+ }
6762
+ }
6763
+ }
6764
+ setFadesMeta((prev) => prev.filter((f) => f.meta.groupId !== group.groupId));
6765
+ const gone = new Set(group.members.map((m) => m.track.handle.id));
6766
+ setTracks((prev) => prev.filter((t) => !gone.has(t.handle.id)));
6767
+ host.showToast("success", "Fade removed");
6768
+ } catch (err) {
6769
+ host.showToast(
6770
+ "error",
6771
+ "Failed to delete fade",
6772
+ err instanceof Error ? err.message : String(err)
6773
+ );
6774
+ }
6775
+ },
6776
+ [host, adapter, activeSceneId, setFadesMeta, setTracks]
6777
+ );
6778
+ const lastResyncKeyRef = (0, import_react29.useRef)("");
6779
+ (0, import_react29.useEffect)(() => {
6179
6780
  if (!host.getTrackSound || resolvedCrossfadePairs.length === 0 && resolvedFades.length === 0) {
6180
6781
  return;
6181
6782
  }
@@ -6216,7 +6817,7 @@ function useTransitionOps({
6216
6817
  cancelled = true;
6217
6818
  };
6218
6819
  }, [resolvedCrossfadePairs, resolvedFades, host, adapter]);
6219
- (0, import_react28.useEffect)(() => {
6820
+ (0, import_react29.useEffect)(() => {
6220
6821
  if (!host.setTrackVolumeAutomation || resolvedFades.length === 0) return;
6221
6822
  void (async () => {
6222
6823
  const mc = await host.getMusicalContext();
@@ -6245,12 +6846,16 @@ function useTransitionOps({
6245
6846
  handleCrossfadeDelete,
6246
6847
  handleCrossfadeSlider,
6247
6848
  handleFadeDelete,
6248
- handleFadeSlider
6849
+ handleFadeSlider,
6850
+ isCreatingGroupFade,
6851
+ handleCreateVerbatimGroupFade,
6852
+ handleGroupFadeSlider,
6853
+ handleGroupFadeDelete
6249
6854
  };
6250
6855
  }
6251
6856
 
6252
6857
  // src/panel-core/useGeneratorPanelCore.tsx
6253
- var import_jsx_runtime25 = require("react/jsx-runtime");
6858
+ var import_jsx_runtime26 = require("react/jsx-runtime");
6254
6859
  var EMPTY_PLACEHOLDERS = [];
6255
6860
  function useGeneratorPanelCore({
6256
6861
  ui,
@@ -6270,8 +6875,8 @@ function useGeneratorPanelCore({
6270
6875
  } = ui;
6271
6876
  const { identity, features } = adapter;
6272
6877
  const logTag = identity.logTag;
6273
- const adapterRef = (0, import_react29.useRef)(adapter);
6274
- (0, import_react29.useEffect)(() => {
6878
+ const adapterRef = (0, import_react30.useRef)(adapter);
6879
+ (0, import_react30.useEffect)(() => {
6275
6880
  if (adapterRef.current !== adapter) {
6276
6881
  adapterRef.current = adapter;
6277
6882
  console.warn(
@@ -6281,27 +6886,27 @@ function useGeneratorPanelCore({
6281
6886
  }, [adapter, logTag]);
6282
6887
  const supportsMeters = typeof host.getTrackLevels === "function";
6283
6888
  const trackLevels = useTrackLevels(host, isExpanded);
6284
- const [tracks, setTracks] = (0, import_react29.useState)([]);
6285
- const [isLoadingTracks, setIsLoadingTracks] = (0, import_react29.useState)(false);
6286
- const [importOpen, setImportOpen] = (0, import_react29.useState)(false);
6287
- const [soundImportTarget, setSoundImportTarget] = (0, import_react29.useState)(null);
6288
- const [designerView, setDesignerView] = (0, import_react29.useState)(false);
6289
- const [transitionSourceTotal, setTransitionSourceTotal] = (0, import_react29.useState)(0);
6290
- const [crossfadePairsMeta, setCrossfadePairsMeta] = (0, import_react29.useState)([]);
6291
- const [fadesMeta, setFadesMeta] = (0, import_react29.useState)([]);
6292
- const [genericGroupMetas, setGenericGroupMetas] = (0, import_react29.useState)({});
6889
+ const [tracks, setTracks] = (0, import_react30.useState)([]);
6890
+ const [isLoadingTracks, setIsLoadingTracks] = (0, import_react30.useState)(false);
6891
+ const [importOpen, setImportOpen] = (0, import_react30.useState)(false);
6892
+ const [soundImportTarget, setSoundImportTarget] = (0, import_react30.useState)(null);
6893
+ const [designerView, setDesignerView] = (0, import_react30.useState)(false);
6894
+ const [transitionSourceTotal, setTransitionSourceTotal] = (0, import_react30.useState)(0);
6895
+ const [crossfadePairsMeta, setCrossfadePairsMeta] = (0, import_react30.useState)([]);
6896
+ const [fadesMeta, setFadesMeta] = (0, import_react30.useState)([]);
6897
+ const [genericGroupMetas, setGenericGroupMetas] = (0, import_react30.useState)({});
6293
6898
  const [isComposing, , setIsComposingForScene] = useSceneState(activeSceneId, false);
6294
6899
  const [placeholders, , setPlaceholdersForScene] = useSceneState(
6295
6900
  activeSceneId,
6296
6901
  EMPTY_PLACEHOLDERS
6297
6902
  );
6298
- const saveTimeoutRefs = (0, import_react29.useRef)({});
6299
- const editLoadStartedRef = (0, import_react29.useRef)(/* @__PURE__ */ new Set());
6300
- const [availableInstruments, setAvailableInstruments] = (0, import_react29.useState)([]);
6301
- const [instrumentsLoading, setInstrumentsLoading] = (0, import_react29.useState)(false);
6302
- const engineToDbIdRef = (0, import_react29.useRef)(/* @__PURE__ */ new Map());
6303
- const tracksLoadedForSceneRef = (0, import_react29.useRef)(null);
6304
- const persistSoundHistory = (0, import_react29.useCallback)(
6903
+ const saveTimeoutRefs = (0, import_react30.useRef)({});
6904
+ const editLoadStartedRef = (0, import_react30.useRef)(/* @__PURE__ */ new Set());
6905
+ const [availableInstruments, setAvailableInstruments] = (0, import_react30.useState)([]);
6906
+ const [instrumentsLoading, setInstrumentsLoading] = (0, import_react30.useState)(false);
6907
+ const engineToDbIdRef = (0, import_react30.useRef)(/* @__PURE__ */ new Map());
6908
+ const tracksLoadedForSceneRef = (0, import_react30.useRef)(null);
6909
+ const persistSoundHistory = (0, import_react30.useCallback)(
6305
6910
  (trackId, state) => {
6306
6911
  if (!activeSceneId) return;
6307
6912
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -6321,7 +6926,7 @@ function useGeneratorPanelCore({
6321
6926
  setItems: setTracks,
6322
6927
  getId: (t) => t.handle.dbId
6323
6928
  });
6324
- const loadTracks = (0, import_react29.useCallback)(
6929
+ const loadTracks = (0, import_react30.useCallback)(
6325
6930
  async (incremental = false) => {
6326
6931
  const sceneAtStart = activeSceneId;
6327
6932
  if (!sceneAtStart) {
@@ -6446,18 +7051,18 @@ function useGeneratorPanelCore({
6446
7051
  },
6447
7052
  [host, activeSceneId, soundHistory, adapter, logTag]
6448
7053
  );
6449
- (0, import_react29.useEffect)(() => {
7054
+ (0, import_react30.useEffect)(() => {
6450
7055
  loadTracks();
6451
7056
  }, [loadTracks]);
6452
- (0, import_react29.useEffect)(() => {
7057
+ (0, import_react30.useEffect)(() => {
6453
7058
  const map = /* @__PURE__ */ new Map();
6454
7059
  for (const t of tracks) {
6455
7060
  map.set(t.handle.id, t.handle.dbId);
6456
7061
  }
6457
7062
  engineToDbIdRef.current = map;
6458
7063
  }, [tracks]);
6459
- const loadedCompletedIdsRef = (0, import_react29.useRef)(/* @__PURE__ */ new Set());
6460
- (0, import_react29.useEffect)(() => {
7064
+ const loadedCompletedIdsRef = (0, import_react30.useRef)(/* @__PURE__ */ new Set());
7065
+ (0, import_react30.useEffect)(() => {
6461
7066
  if (placeholders.length === 0) {
6462
7067
  loadedCompletedIdsRef.current.clear();
6463
7068
  return;
@@ -6476,16 +7081,16 @@ function useGeneratorPanelCore({
6476
7081
  loadTracks(true);
6477
7082
  }
6478
7083
  }, [placeholders, loadTracks, logTag]);
6479
- const adoptAndLoad = (0, import_react29.useCallback)(() => {
7084
+ const adoptAndLoad = (0, import_react30.useCallback)(() => {
6480
7085
  loadTracks(true);
6481
7086
  }, [loadTracks]);
6482
- (0, import_react29.useEffect)(() => {
7087
+ (0, import_react30.useEffect)(() => {
6483
7088
  const unsub = host.onEngineReady(() => {
6484
7089
  adoptAndLoad();
6485
7090
  });
6486
7091
  return unsub;
6487
7092
  }, [host, adoptAndLoad]);
6488
- (0, import_react29.useEffect)(() => {
7093
+ (0, import_react30.useEffect)(() => {
6489
7094
  if (typeof host.onAfterAgentMutation !== "function") return;
6490
7095
  let timer = null;
6491
7096
  const unsub = host.onAfterAgentMutation(() => {
@@ -6500,13 +7105,13 @@ function useGeneratorPanelCore({
6500
7105
  if (timer) clearTimeout(timer);
6501
7106
  };
6502
7107
  }, [host, loadTracks]);
6503
- (0, import_react29.useEffect)(() => {
7108
+ (0, import_react30.useEffect)(() => {
6504
7109
  const unsub = host.onTrackStateChange((trackId, state) => {
6505
7110
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: state } : t));
6506
7111
  });
6507
7112
  return unsub;
6508
7113
  }, [host]);
6509
- (0, import_react29.useEffect)(() => {
7114
+ (0, import_react30.useEffect)(() => {
6510
7115
  if (!features.bulkComposePlaceholders) return;
6511
7116
  console.log(`[${logTag}] Subscribing to composeProgress`);
6512
7117
  const unsub = host.onComposeProgress((event) => {
@@ -6540,7 +7145,7 @@ function useGeneratorPanelCore({
6540
7145
  });
6541
7146
  return unsub;
6542
7147
  }, [host, setIsComposingForScene, setPlaceholdersForScene, features.bulkComposePlaceholders, logTag]);
6543
- (0, import_react29.useEffect)(() => {
7148
+ (0, import_react30.useEffect)(() => {
6544
7149
  const refs = saveTimeoutRefs;
6545
7150
  return () => {
6546
7151
  for (const timeout of Object.values(refs.current)) {
@@ -6548,9 +7153,9 @@ function useGeneratorPanelCore({
6548
7153
  }
6549
7154
  };
6550
7155
  }, []);
6551
- const isAddingTrackRef = (0, import_react29.useRef)(false);
6552
- const [isAddingTrack, setIsAddingTrack] = (0, import_react29.useState)(false);
6553
- const handleAddTrack = (0, import_react29.useCallback)(async () => {
7156
+ const isAddingTrackRef = (0, import_react30.useRef)(false);
7157
+ const [isAddingTrack, setIsAddingTrack] = (0, import_react30.useState)(false);
7158
+ const handleAddTrack = (0, import_react30.useCallback)(async () => {
6554
7159
  if (isAddingTrackRef.current) return;
6555
7160
  if (!activeSceneId) {
6556
7161
  host.showToast("warning", "Select SCENE");
@@ -6590,7 +7195,7 @@ function useGeneratorPanelCore({
6590
7195
  setIsAddingTrack(false);
6591
7196
  }
6592
7197
  }, [host, adapter, identity, activeSceneId, isConnected, isAuthenticated, tracks.length, onExpandSelf]);
6593
- const handlePortTrack = (0, import_react29.useCallback)(
7198
+ const handlePortTrack = (0, import_react30.useCallback)(
6594
7199
  async (sel) => {
6595
7200
  if (!activeSceneId) {
6596
7201
  host.showToast("warning", "Select SCENE");
@@ -6647,7 +7252,7 @@ function useGeneratorPanelCore({
6647
7252
  },
6648
7253
  [host, adapter, identity, activeSceneId, isConnected, tracks.length, loadTracks]
6649
7254
  );
6650
- const handleSoundImportPick = (0, import_react29.useCallback)(
7255
+ const handleSoundImportPick = (0, import_react30.useCallback)(
6651
7256
  async (sel) => {
6652
7257
  const target = soundImportTarget;
6653
7258
  if (!target || !host.getTrackSound) {
@@ -6678,8 +7283,8 @@ function useGeneratorPanelCore({
6678
7283
  },
6679
7284
  [soundImportTarget, host, adapter, identity.familyKey, soundHistory]
6680
7285
  );
6681
- const [isExportingMidi, setIsExportingMidi] = (0, import_react29.useState)(false);
6682
- const handleExportMidi = (0, import_react29.useCallback)(async () => {
7286
+ const [isExportingMidi, setIsExportingMidi] = (0, import_react30.useState)(false);
7287
+ const handleExportMidi = (0, import_react30.useCallback)(async () => {
6683
7288
  if (isExportingMidi) return;
6684
7289
  setIsExportingMidi(true);
6685
7290
  try {
@@ -6710,10 +7315,10 @@ function useGeneratorPanelCore({
6710
7315
  const xfFromId = sceneContext?.transitionFromSceneId ?? null;
6711
7316
  const xfToId = sceneContext?.transitionToSceneId ?? null;
6712
7317
  const canCrossfade = features.transitionDesigner && sceneContext?.sceneType === "transition" && !!xfFromId && !!xfToId && !!host.listSceneFamilyTracks;
6713
- (0, import_react29.useEffect)(() => {
7318
+ (0, import_react30.useEffect)(() => {
6714
7319
  if (!canCrossfade) setDesignerView(false);
6715
7320
  }, [canCrossfade]);
6716
- (0, import_react29.useEffect)(() => {
7321
+ (0, import_react30.useEffect)(() => {
6717
7322
  if (!canCrossfade || !xfFromId || !xfToId || !host.listSceneFamilyTracks) {
6718
7323
  setTransitionSourceTotal(0);
6719
7324
  return;
@@ -6729,12 +7334,12 @@ function useGeneratorPanelCore({
6729
7334
  };
6730
7335
  }, [canCrossfade, xfFromId, xfToId, host]);
6731
7336
  const transitionDone = crossfadePairsMeta.length * 2 + fadesMeta.length;
6732
- (0, import_react29.useEffect)(() => {
7337
+ (0, import_react30.useEffect)(() => {
6733
7338
  if (!onHeaderContent) return;
6734
7339
  const addDisabled = needsContract || !isConnected || !activeSceneId || tracks.length >= identity.maxTracks || isAddingTrack;
6735
7340
  onHeaderContent(
6736
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("div", { className: "flex gap-1 items-center", children: [
6737
- features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
7341
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { className: "flex gap-1 items-center", children: [
7342
+ features.importTracks && (!canCrossfade || !designerView) && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
6738
7343
  "button",
6739
7344
  {
6740
7345
  "data-testid": `import-from-scene-${identity.familyKey}-button`,
@@ -6748,7 +7353,7 @@ function useGeneratorPanelCore({
6748
7353
  children: identity.importTrackLabel ?? "Import Track"
6749
7354
  }
6750
7355
  ),
6751
- (!canCrossfade || !designerView) && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
7356
+ (!canCrossfade || !designerView) && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
6752
7357
  "button",
6753
7358
  {
6754
7359
  "data-testid": `add-${identity.familyKey}-track-button`,
@@ -6764,7 +7369,7 @@ function useGeneratorPanelCore({
6764
7369
  children: identity.addTrackLabel ?? "Add Track"
6765
7370
  }
6766
7371
  ),
6767
- canCrossfade && /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)(
7372
+ canCrossfade && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(
6768
7373
  "button",
6769
7374
  {
6770
7375
  "data-testid": `${identity.familyKey}-view-toggle`,
@@ -6783,7 +7388,7 @@ function useGeneratorPanelCore({
6783
7388
  title: designerView ? "Back to the track list" : "Open the transition designer",
6784
7389
  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",
6785
7390
  children: [
6786
- transitionSourceTotal > 0 && /* @__PURE__ */ (0, import_jsx_runtime25.jsx)(
7391
+ transitionSourceTotal > 0 && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
6787
7392
  "span",
6788
7393
  {
6789
7394
  className: "absolute inset-y-0 left-0 bg-sas-accent/25",
@@ -6791,7 +7396,7 @@ function useGeneratorPanelCore({
6791
7396
  "aria-hidden": true
6792
7397
  }
6793
7398
  ),
6794
- /* @__PURE__ */ (0, import_jsx_runtime25.jsxs)("span", { className: "relative", children: [
7399
+ /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("span", { className: "relative", children: [
6795
7400
  "\u21C4 ",
6796
7401
  designerView ? "Transition" : "Tracks",
6797
7402
  transitionSourceTotal > 0 ? ` ${transitionDone}/${transitionSourceTotal}` : ""
@@ -6822,7 +7427,7 @@ function useGeneratorPanelCore({
6822
7427
  identity,
6823
7428
  features.importTracks
6824
7429
  ]);
6825
- (0, import_react29.useEffect)(() => {
7430
+ (0, import_react30.useEffect)(() => {
6826
7431
  if (!onLoading) return;
6827
7432
  const anyGenerating = tracks.some((t) => t.isGenerating);
6828
7433
  onLoading(isLoadingTracks || anyGenerating || isBulkActive);
@@ -6830,7 +7435,7 @@ function useGeneratorPanelCore({
6830
7435
  onLoading(false);
6831
7436
  };
6832
7437
  }, [onLoading, isLoadingTracks, tracks, isBulkActive]);
6833
- const handleDeleteTrack = (0, import_react29.useCallback)(
7438
+ const handleDeleteTrack = (0, import_react30.useCallback)(
6834
7439
  async (trackId) => {
6835
7440
  try {
6836
7441
  await host.deleteTrack(trackId);
@@ -6846,7 +7451,7 @@ function useGeneratorPanelCore({
6846
7451
  },
6847
7452
  [host, activeSceneId]
6848
7453
  );
6849
- const handlePromptChange = (0, import_react29.useCallback)(
7454
+ const handlePromptChange = (0, import_react30.useCallback)(
6850
7455
  (trackId, prompt) => {
6851
7456
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, prompt } : t));
6852
7457
  const dbId = engineToDbIdRef.current.get(trackId) ?? trackId;
@@ -6862,7 +7467,7 @@ function useGeneratorPanelCore({
6862
7467
  },
6863
7468
  [host, activeSceneId]
6864
7469
  );
6865
- const resolvedGenericGroups = (0, import_react29.useMemo)(() => {
7470
+ const resolvedGenericGroups = (0, import_react30.useMemo)(() => {
6866
7471
  const out = {};
6867
7472
  for (const ext of adapter.groupExtensions ?? []) {
6868
7473
  out[ext.metaKey] = resolveTrackGroups(
@@ -6876,18 +7481,18 @@ function useGeneratorPanelCore({
6876
7481
  }
6877
7482
  return out;
6878
7483
  }, [adapter, genericGroupMetas, tracks]);
6879
- const genericGroupMemberDbIds = (0, import_react29.useMemo)(() => {
7484
+ const genericGroupMemberDbIds = (0, import_react30.useMemo)(() => {
6880
7485
  const s = /* @__PURE__ */ new Set();
6881
7486
  for (const r of Object.values(resolvedGenericGroups)) {
6882
7487
  for (const dbId of r.memberDbIds) s.add(dbId);
6883
7488
  }
6884
7489
  return s;
6885
7490
  }, [resolvedGenericGroups]);
6886
- const engineToDbId = (0, import_react29.useCallback)(
7491
+ const engineToDbId = (0, import_react30.useCallback)(
6887
7492
  (trackId) => engineToDbIdRef.current.get(trackId) ?? trackId,
6888
7493
  []
6889
7494
  );
6890
- const updateTrack = (0, import_react29.useCallback)(
7495
+ const updateTrack = (0, import_react30.useCallback)(
6891
7496
  (trackId, patch) => {
6892
7497
  setTracks(
6893
7498
  (prev) => prev.map(
@@ -6897,18 +7502,18 @@ function useGeneratorPanelCore({
6897
7502
  },
6898
7503
  []
6899
7504
  );
6900
- const markEditLoaded = (0, import_react29.useCallback)((trackId) => {
7505
+ const markEditLoaded = (0, import_react30.useCallback)((trackId) => {
6901
7506
  editLoadStartedRef.current.add(trackId);
6902
7507
  }, []);
6903
- const tracksRef = (0, import_react29.useRef)(tracks);
6904
- (0, import_react29.useEffect)(() => {
7508
+ const tracksRef = (0, import_react30.useRef)(tracks);
7509
+ (0, import_react30.useEffect)(() => {
6905
7510
  tracksRef.current = tracks;
6906
7511
  }, [tracks]);
6907
- const resolvedGenericGroupsRef = (0, import_react29.useRef)(resolvedGenericGroups);
6908
- (0, import_react29.useEffect)(() => {
7512
+ const resolvedGenericGroupsRef = (0, import_react30.useRef)(resolvedGenericGroups);
7513
+ (0, import_react30.useEffect)(() => {
6909
7514
  resolvedGenericGroupsRef.current = resolvedGenericGroups;
6910
7515
  }, [resolvedGenericGroups]);
6911
- const makeServices = (0, import_react29.useCallback)(() => {
7516
+ const makeServices = (0, import_react30.useCallback)(() => {
6912
7517
  return {
6913
7518
  host,
6914
7519
  activeSceneId,
@@ -6927,7 +7532,7 @@ function useGeneratorPanelCore({
6927
7532
  resolvedGroups: (metaKey) => resolvedGenericGroupsRef.current[metaKey]?.resolved ?? []
6928
7533
  };
6929
7534
  }, [host, activeSceneId, updateTrack, loadTracks, soundHistory, engineToDbId, markEditLoaded, identity, adapter]);
6930
- const handleGenerate = (0, import_react29.useCallback)(
7535
+ const handleGenerate = (0, import_react30.useCallback)(
6931
7536
  async (trackId) => {
6932
7537
  const track = tracks.find((t) => t.handle.id === trackId);
6933
7538
  if (!track || !track.prompt.trim()) return;
@@ -6954,7 +7559,7 @@ function useGeneratorPanelCore({
6954
7559
  },
6955
7560
  [host, adapter, tracks, isAuthenticated, makeServices]
6956
7561
  );
6957
- const handleMuteToggle = (0, import_react29.useCallback)(
7562
+ const handleMuteToggle = (0, import_react30.useCallback)(
6958
7563
  (trackId) => {
6959
7564
  const track = tracks.find((t) => t.handle.id === trackId);
6960
7565
  if (!track) return;
@@ -6974,7 +7579,7 @@ function useGeneratorPanelCore({
6974
7579
  },
6975
7580
  [host, tracks]
6976
7581
  );
6977
- const handleSoloToggle = (0, import_react29.useCallback)(
7582
+ const handleSoloToggle = (0, import_react30.useCallback)(
6978
7583
  (trackId) => {
6979
7584
  const track = tracks.find((t) => t.handle.id === trackId);
6980
7585
  if (!track) return;
@@ -6994,7 +7599,7 @@ function useGeneratorPanelCore({
6994
7599
  },
6995
7600
  [host, tracks]
6996
7601
  );
6997
- const handleVolumeChange = (0, import_react29.useCallback)(
7602
+ const handleVolumeChange = (0, import_react30.useCallback)(
6998
7603
  (trackId, volume) => {
6999
7604
  setTracks(
7000
7605
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, volume } } : t)
@@ -7004,7 +7609,7 @@ function useGeneratorPanelCore({
7004
7609
  },
7005
7610
  [host]
7006
7611
  );
7007
- const handlePanChange = (0, import_react29.useCallback)(
7612
+ const handlePanChange = (0, import_react30.useCallback)(
7008
7613
  (trackId, pan) => {
7009
7614
  setTracks(
7010
7615
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, runtimeState: { ...t.runtimeState, pan } } : t)
@@ -7014,7 +7619,7 @@ function useGeneratorPanelCore({
7014
7619
  },
7015
7620
  [host]
7016
7621
  );
7017
- const handleShuffle = (0, import_react29.useCallback)(
7622
+ const handleShuffle = (0, import_react30.useCallback)(
7018
7623
  async (trackId) => {
7019
7624
  const track = tracks.find((t) => t.handle.id === trackId);
7020
7625
  if (!track) return;
@@ -7056,7 +7661,7 @@ function useGeneratorPanelCore({
7056
7661
  },
7057
7662
  [host, adapter, tracks, soundHistory, logTag]
7058
7663
  );
7059
- const handleCopy = (0, import_react29.useCallback)(
7664
+ const handleCopy = (0, import_react30.useCallback)(
7060
7665
  async (trackId) => {
7061
7666
  try {
7062
7667
  const newHandle = await host.duplicateTrack(trackId);
@@ -7069,7 +7674,7 @@ function useGeneratorPanelCore({
7069
7674
  },
7070
7675
  [host, loadTracks]
7071
7676
  );
7072
- const handleFxToggle = (0, import_react29.useCallback)(
7677
+ const handleFxToggle = (0, import_react30.useCallback)(
7073
7678
  (trackId, category, enabled) => {
7074
7679
  setTracks(
7075
7680
  (prev) => prev.map(
@@ -7092,7 +7697,7 @@ function useGeneratorPanelCore({
7092
7697
  },
7093
7698
  [host]
7094
7699
  );
7095
- const handleFxPresetChange = (0, import_react29.useCallback)(
7700
+ const handleFxPresetChange = (0, import_react30.useCallback)(
7096
7701
  (trackId, category, presetIndex) => {
7097
7702
  setTracks(
7098
7703
  (prev) => prev.map(
@@ -7118,7 +7723,7 @@ function useGeneratorPanelCore({
7118
7723
  },
7119
7724
  [host]
7120
7725
  );
7121
- const handleFxDryWetChange = (0, import_react29.useCallback)(
7726
+ const handleFxDryWetChange = (0, import_react30.useCallback)(
7122
7727
  (trackId, category, value) => {
7123
7728
  setTracks(
7124
7729
  (prev) => prev.map(
@@ -7130,7 +7735,7 @@ function useGeneratorPanelCore({
7130
7735
  },
7131
7736
  [host]
7132
7737
  );
7133
- const toggleFxDrawer = (0, import_react29.useCallback)(
7738
+ const toggleFxDrawer = (0, import_react30.useCallback)(
7134
7739
  (trackId) => {
7135
7740
  setTracks(
7136
7741
  (prev) => prev.map((t) => {
@@ -7152,7 +7757,7 @@ function useGeneratorPanelCore({
7152
7757
  },
7153
7758
  [host, tracks]
7154
7759
  );
7155
- const loadEditNotes = (0, import_react29.useCallback)(
7760
+ const loadEditNotes = (0, import_react30.useCallback)(
7156
7761
  async (trackId) => {
7157
7762
  try {
7158
7763
  const mc = await host.getMusicalContext();
@@ -7170,7 +7775,7 @@ function useGeneratorPanelCore({
7170
7775
  },
7171
7776
  [host, logTag]
7172
7777
  );
7173
- const handleNotesChange = (0, import_react29.useCallback)(
7778
+ const handleNotesChange = (0, import_react30.useCallback)(
7174
7779
  (trackId, notes) => {
7175
7780
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editNotes: notes } : t));
7176
7781
  const key = `edit:${trackId}`;
@@ -7200,7 +7805,7 @@ function useGeneratorPanelCore({
7200
7805
  },
7201
7806
  [host]
7202
7807
  );
7203
- const handleTabChange = (0, import_react29.useCallback)(
7808
+ const handleTabChange = (0, import_react30.useCallback)(
7204
7809
  (trackId, tab) => {
7205
7810
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, drawerOpen: true, drawerTab: tab } : t));
7206
7811
  if (tab === "fx") {
@@ -7225,10 +7830,10 @@ function useGeneratorPanelCore({
7225
7830
  },
7226
7831
  [host, availableInstruments.length, instrumentsLoading, loadEditNotes]
7227
7832
  );
7228
- const handleProgressChange = (0, import_react29.useCallback)((trackId, pct) => {
7833
+ const handleProgressChange = (0, import_react30.useCallback)((trackId, pct) => {
7229
7834
  setTracks((prev) => prev.map((t) => t.handle.id === trackId ? { ...t, generationProgress: pct } : t));
7230
7835
  }, []);
7231
- const handleToggleDrawer = (0, import_react29.useCallback)((trackId) => {
7836
+ const handleToggleDrawer = (0, import_react30.useCallback)((trackId) => {
7232
7837
  setTracks(
7233
7838
  (prev) => prev.map((t) => {
7234
7839
  if (t.handle.id !== trackId) return t;
@@ -7237,7 +7842,7 @@ function useGeneratorPanelCore({
7237
7842
  })
7238
7843
  );
7239
7844
  }, []);
7240
- const handleInstrumentSelect = (0, import_react29.useCallback)(
7845
+ const handleInstrumentSelect = (0, import_react30.useCallback)(
7241
7846
  async (trackId, pluginId) => {
7242
7847
  const isDefaultInstrument = pluginId === (identity.defaultInstrumentPluginId ?? "Surge XT");
7243
7848
  if (isDefaultInstrument) {
@@ -7290,7 +7895,7 @@ function useGeneratorPanelCore({
7290
7895
  },
7291
7896
  [host, identity.defaultInstrumentPluginId, logTag]
7292
7897
  );
7293
- const handleShowEditor = (0, import_react29.useCallback)(
7898
+ const handleShowEditor = (0, import_react30.useCallback)(
7294
7899
  async (trackId) => {
7295
7900
  try {
7296
7901
  await host.showInstrumentEditor(trackId);
@@ -7301,12 +7906,12 @@ function useGeneratorPanelCore({
7301
7906
  },
7302
7907
  [host]
7303
7908
  );
7304
- const handleBackToInstruments = (0, import_react29.useCallback)((trackId) => {
7909
+ const handleBackToInstruments = (0, import_react30.useCallback)((trackId) => {
7305
7910
  setTracks(
7306
7911
  (prev) => prev.map((t) => t.handle.id === trackId ? { ...t, editorStage: false } : t)
7307
7912
  );
7308
7913
  }, []);
7309
- const handleRefreshInstruments = (0, import_react29.useCallback)(() => {
7914
+ const handleRefreshInstruments = (0, import_react30.useCallback)(() => {
7310
7915
  setInstrumentsLoading(true);
7311
7916
  host.getAvailableInstruments().then((instruments) => {
7312
7917
  setAvailableInstruments(instruments);
@@ -7315,13 +7920,13 @@ function useGeneratorPanelCore({
7315
7920
  setInstrumentsLoading(false);
7316
7921
  });
7317
7922
  }, [host]);
7318
- const onAuditionNote = (0, import_react29.useCallback)(
7923
+ const onAuditionNote = (0, import_react30.useCallback)(
7319
7924
  (trackId, pitch, velocity, ms) => {
7320
7925
  void host.auditionNote(trackId, pitch, velocity, ms);
7321
7926
  },
7322
7927
  [host]
7323
7928
  );
7324
- const { resolvedCrossfadePairs, crossfadeMemberDbIds } = (0, import_react29.useMemo)(() => {
7929
+ const { resolvedCrossfadePairs, crossfadeMemberDbIds } = (0, import_react30.useMemo)(() => {
7325
7930
  const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
7326
7931
  const pairs = [];
7327
7932
  const members = /* @__PURE__ */ new Set();
@@ -7336,7 +7941,7 @@ function useGeneratorPanelCore({
7336
7941
  }
7337
7942
  return { resolvedCrossfadePairs: pairs, crossfadeMemberDbIds: members };
7338
7943
  }, [tracks, crossfadePairsMeta]);
7339
- const { resolvedFades, fadeMemberDbIds } = (0, import_react29.useMemo)(() => {
7944
+ const { resolvedFades, fadeMemberDbIds } = (0, import_react30.useMemo)(() => {
7340
7945
  const byDbId = new Map(tracks.map((t) => [t.handle.dbId, t]));
7341
7946
  const list = [];
7342
7947
  const members = /* @__PURE__ */ new Set();
@@ -7349,6 +7954,10 @@ function useGeneratorPanelCore({
7349
7954
  }
7350
7955
  return { resolvedFades: list, fadeMemberDbIds: members };
7351
7956
  }, [tracks, fadesMeta]);
7957
+ const { singles: resolvedSingleFades, groups: resolvedGroupFades } = (0, import_react30.useMemo)(
7958
+ () => splitFadeEntries(resolvedFades),
7959
+ [resolvedFades]
7960
+ );
7352
7961
  const transition = useTransitionOps({
7353
7962
  host,
7354
7963
  adapter,
@@ -7364,7 +7973,7 @@ function useGeneratorPanelCore({
7364
7973
  resolvedCrossfadePairs,
7365
7974
  resolvedFades
7366
7975
  });
7367
- const setGroupMute = (0, import_react29.useCallback)(
7976
+ const setGroupMute = (0, import_react30.useCallback)(
7368
7977
  (trackIds, muted) => {
7369
7978
  for (const id of trackIds) {
7370
7979
  setTracks(
@@ -7376,7 +7985,7 @@ function useGeneratorPanelCore({
7376
7985
  },
7377
7986
  [host]
7378
7987
  );
7379
- const setGroupSolo = (0, import_react29.useCallback)(
7988
+ const setGroupSolo = (0, import_react30.useCallback)(
7380
7989
  (trackIds, solo) => {
7381
7990
  for (const id of trackIds) {
7382
7991
  setTracks(
@@ -7388,7 +7997,7 @@ function useGeneratorPanelCore({
7388
7997
  },
7389
7998
  [host]
7390
7999
  );
7391
- const deleteGroup = (0, import_react29.useCallback)(
8000
+ const deleteGroup = (0, import_react30.useCallback)(
7392
8001
  async (members, cleanupKeySuffixes) => {
7393
8002
  for (const member of members) {
7394
8003
  try {
@@ -7408,7 +8017,7 @@ function useGeneratorPanelCore({
7408
8017
  },
7409
8018
  [host, activeSceneId, loadTracks]
7410
8019
  );
7411
- const handlers = (0, import_react29.useMemo)(
8020
+ const handlers = (0, import_react30.useMemo)(
7412
8021
  () => ({
7413
8022
  promptChange: handlePromptChange,
7414
8023
  generate: (trackId) => {
@@ -7485,6 +8094,8 @@ function useGeneratorPanelCore({
7485
8094
  crossfadeMemberDbIds,
7486
8095
  resolvedFades,
7487
8096
  fadeMemberDbIds,
8097
+ resolvedSingleFades,
8098
+ resolvedGroupFades,
7488
8099
  resolvedGenericGroups,
7489
8100
  genericGroupMemberDbIds,
7490
8101
  availableInstruments,
@@ -7522,8 +8133,8 @@ function useGeneratorPanelCore({
7522
8133
  }
7523
8134
 
7524
8135
  // src/panel-core/GeneratorPanelShell.tsx
7525
- var import_react30 = __toESM(require("react"));
7526
- var import_jsx_runtime26 = require("react/jsx-runtime");
8136
+ var import_react31 = __toESM(require("react"));
8137
+ var import_jsx_runtime27 = require("react/jsx-runtime");
7527
8138
  function GeneratorPanelShell({ core, slots }) {
7528
8139
  const {
7529
8140
  ui,
@@ -7552,7 +8163,8 @@ function GeneratorPanelShell({ core, slots }) {
7552
8163
  fadesMeta,
7553
8164
  resolvedCrossfadePairs,
7554
8165
  crossfadeMemberDbIds,
7555
- resolvedFades,
8166
+ resolvedSingleFades,
8167
+ resolvedGroupFades,
7556
8168
  fadeMemberDbIds,
7557
8169
  resolvedGenericGroups,
7558
8170
  genericGroupMemberDbIds,
@@ -7578,7 +8190,7 @@ function GeneratorPanelShell({ core, slots }) {
7578
8190
  const { host, activeSceneId, isAuthenticated, sceneContext, onSelectScene, onOpenContract } = ui;
7579
8191
  const panelBus = usePanelBus(host, activeSceneId);
7580
8192
  const { identity, features } = adapter;
7581
- const buildRowProps = (0, import_react30.useCallback)(
8193
+ const buildRowProps = (0, import_react31.useCallback)(
7582
8194
  (track, drag) => {
7583
8195
  const id = track.handle.id;
7584
8196
  const pickerProps = features.instrumentPicker ? {
@@ -7678,12 +8290,12 @@ function GeneratorPanelShell({ core, slots }) {
7678
8290
  ]
7679
8291
  );
7680
8292
  if (!activeSceneId) {
7681
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8293
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7682
8294
  "div",
7683
8295
  {
7684
8296
  "data-testid": `no-scene-placeholder-${identity.familyKey}`,
7685
8297
  className: "flex items-center justify-center py-8",
7686
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8298
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7687
8299
  "button",
7688
8300
  {
7689
8301
  onClick: () => onSelectScene?.(),
@@ -7695,12 +8307,12 @@ function GeneratorPanelShell({ core, slots }) {
7695
8307
  );
7696
8308
  }
7697
8309
  if (!sceneContext?.hasContract) {
7698
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8310
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7699
8311
  "div",
7700
8312
  {
7701
8313
  "data-testid": `no-contract-placeholder-${identity.familyKey}`,
7702
8314
  className: "flex items-center justify-center py-8",
7703
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8315
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7704
8316
  "button",
7705
8317
  {
7706
8318
  onClick: () => onOpenContract?.(),
@@ -7712,7 +8324,7 @@ function GeneratorPanelShell({ core, slots }) {
7712
8324
  );
7713
8325
  }
7714
8326
  if (features.bulkComposePlaceholders && isComposing) {
7715
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
8327
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(SorceryProgressBar, { isLoading: true, statusText: "COMPOSING...", heightClass: "h-10" }) });
7716
8328
  }
7717
8329
  const activePlaceholders = features.bulkComposePlaceholders ? placeholders : [];
7718
8330
  if (activePlaceholders.length > 0) {
@@ -7723,18 +8335,18 @@ function GeneratorPanelShell({ core, slots }) {
7723
8335
  tracksByDbId.set(t.handle.id, t);
7724
8336
  }
7725
8337
  }
7726
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
8338
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: activePlaceholders.map((ph) => {
7727
8339
  const loadedTrack = ph.status === "completed" ? tracksByDbId.get(ph.id) : void 0;
7728
8340
  if (loadedTrack) {
7729
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
8341
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(TrackRow, { ...buildRowProps(loadedTrack) }, ph.id);
7730
8342
  }
7731
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8343
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7732
8344
  "div",
7733
8345
  {
7734
8346
  "data-testid": "bulk-placeholder-track",
7735
8347
  className: "relative rounded-sm border w-full overflow-hidden border-sas-border bg-sas-panel-alt",
7736
8348
  style: { borderLeftColor: identity.placeholderAccentColor, borderLeftWidth: "3px" },
7737
- children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
8349
+ children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(SorceryProgressBar, { isLoading: true, statusText: "CONJURING MIDI...", heightClass: "h-10" })
7738
8350
  },
7739
8351
  ph.id
7740
8352
  );
@@ -7746,13 +8358,13 @@ function GeneratorPanelShell({ core, slots }) {
7746
8358
  supportsMeters,
7747
8359
  levels: supportsMeters ? trackLevels : void 0,
7748
8360
  handlers,
7749
- renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
8361
+ renderDefaultTrackRow: (track, overrides, drag) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(TrackRow, { ...{ ...buildRowProps(track, drag), ...overrides ?? {} } }, track.handle.id),
7750
8362
  setGroupMute,
7751
8363
  setGroupSolo,
7752
8364
  deleteGroup
7753
8365
  };
7754
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
7755
- features.importTracks && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8366
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)("div", { "data-testid": `${identity.familyKey}-section`, className: "p-2 space-y-2", children: [
8367
+ features.importTracks && host.listImportableTracks && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7756
8368
  ImportTrackModal,
7757
8369
  {
7758
8370
  host,
@@ -7765,7 +8377,7 @@ function GeneratorPanelShell({ core, slots }) {
7765
8377
  testIdPrefix: `${identity.familyKey}-import`
7766
8378
  }
7767
8379
  ),
7768
- features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8380
+ features.importTracks && host.listImportableTracks && host.getTrackSound && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7769
8381
  ImportTrackModal,
7770
8382
  {
7771
8383
  host,
@@ -7780,7 +8392,7 @@ function GeneratorPanelShell({ core, slots }) {
7780
8392
  }
7781
8393
  ),
7782
8394
  slots?.modals,
7783
- canCrossfade && xfFromId && xfToId && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8395
+ canCrossfade && xfFromId && xfToId && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: designerView ? "contents" : "hidden", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7784
8396
  TransitionDesigner,
7785
8397
  {
7786
8398
  host,
@@ -7791,14 +8403,16 @@ function GeneratorPanelShell({ core, slots }) {
7791
8403
  ...crossfadePairsMeta.flatMap((p) => [p.originSourceDbId, p.targetSourceDbId]),
7792
8404
  ...fadesMeta.map((f) => f.meta.sourceTrackDbId)
7793
8405
  ],
7794
- onCreateCrossfade: transition.handleCreateCrossfade,
7795
- onCreateFade: transition.handleCreateFade,
8406
+ onCreateCrossfade: adapter.transitionGroup?.fadeOnly ? void 0 : transition.handleCreateCrossfade,
8407
+ onCreateFade: adapter.transitionGroup ? (sel, dir) => transition.handleCreateVerbatimGroupFade(sel, dir) : transition.handleCreateFade,
8408
+ mapColumnSubjects: adapter.transitionGroup ? (sceneId, tracks2) => adapter.transitionGroup.mapColumnSubjects(sceneId, tracks2) : void 0,
8409
+ fadeEstimateMs: adapter.transitionGroup?.fadeEstimateMs,
7796
8410
  familyLabel: identity.familyLabel,
7797
8411
  testIdPrefix: `${identity.familyKey}-transition-designer`
7798
8412
  }
7799
8413
  ) }),
7800
- !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
7801
- panelBus.supported && panelBus.bus && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8414
+ !(designerView && canCrossfade) && (isLoadingTracks ? /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "text-sas-muted text-xs text-center py-4", children: "Loading tracks..." }) : /* @__PURE__ */ (0, import_jsx_runtime27.jsxs)(import_jsx_runtime27.Fragment, { children: [
8415
+ panelBus.supported && panelBus.bus && /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7802
8416
  PanelMasterStrip,
7803
8417
  {
7804
8418
  bus: panelBus.bus,
@@ -7819,7 +8433,7 @@ function GeneratorPanelShell({ core, slots }) {
7819
8433
  }
7820
8434
  ),
7821
8435
  slots?.beforeRows,
7822
- resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8436
+ resolvedCrossfadePairs.map((pair) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7823
8437
  CrossfadeTrackRow,
7824
8438
  {
7825
8439
  accentColor: identity.transitionAccentColor,
@@ -7856,7 +8470,7 @@ function GeneratorPanelShell({ core, slots }) {
7856
8470
  },
7857
8471
  pair.groupId
7858
8472
  )),
7859
- resolvedFades.map((fade) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8473
+ resolvedSingleFades.map((fade) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7860
8474
  FadeTrackRow,
7861
8475
  {
7862
8476
  accentColor: identity.transitionAccentColor,
@@ -7881,21 +8495,56 @@ function GeneratorPanelShell({ core, slots }) {
7881
8495
  },
7882
8496
  fade.dbId
7883
8497
  )),
8498
+ resolvedGroupFades.map((group) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
8499
+ GroupFadeTrackRow,
8500
+ {
8501
+ accentColor: identity.transitionAccentColor,
8502
+ levels: supportsMeters ? trackLevels : void 0,
8503
+ direction: group.direction,
8504
+ gesture: group.gesture,
8505
+ sliderPos: group.sliderPos,
8506
+ groupLabel: adapter.transitionGroup?.groupRowLabel?.(group.members.length) ?? `Group (${group.members.length} tracks)`,
8507
+ members: group.members.map((m) => ({
8508
+ trackId: m.track.handle.id,
8509
+ name: m.track.handle.name,
8510
+ role: m.track.role,
8511
+ sourceName: m.meta.sourceName,
8512
+ soundLabel: m.meta.soundLabel,
8513
+ memberLabel: m.meta.memberLabel,
8514
+ runtimeState: m.track.runtimeState
8515
+ })),
8516
+ onMemberMuteToggle: (trackId) => handlers.muteToggle(trackId),
8517
+ onMemberSoloToggle: (trackId) => handlers.soloToggle(trackId),
8518
+ onMemberVolumeChange: (trackId, vol) => handlers.volumeChange(trackId, vol),
8519
+ onMemberPanChange: (trackId, pan) => handlers.panChange(trackId, pan),
8520
+ onMuteAll: () => setGroupMute(
8521
+ group.members.map((m) => m.track.handle.id),
8522
+ !group.members.every((m) => m.track.runtimeState.muted)
8523
+ ),
8524
+ onSoloAll: () => setGroupSolo(
8525
+ group.members.map((m) => m.track.handle.id),
8526
+ !group.members.every((m) => m.track.runtimeState.solo)
8527
+ ),
8528
+ onSliderChange: (pos) => transition.handleGroupFadeSlider(group, pos),
8529
+ onDelete: () => transition.handleGroupFadeDelete(group)
8530
+ },
8531
+ group.groupId
8532
+ )),
7884
8533
  (adapter.groupExtensions ?? []).flatMap(
7885
- (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).map((group) => /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(import_react30.default.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
8534
+ (ext) => (resolvedGenericGroups[ext.metaKey]?.resolved ?? []).filter((group) => !group.members.every((m) => fadeMemberDbIds.has(m.dbId))).map((group) => /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(import_react31.default.Fragment, { children: ext.renderGroup(group, groupCtx) }, `${ext.metaKey}:${group.groupId}`))
7886
8535
  ),
7887
8536
  tracks.map((track, index) => {
7888
8537
  if (crossfadeMemberDbIds.has(track.handle.dbId) || fadeMemberDbIds.has(track.handle.dbId) || genericGroupMemberDbIds.has(track.handle.dbId)) {
7889
8538
  return null;
7890
8539
  }
7891
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
8540
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(TrackRow, { ...buildRowProps(track, reorder.dragPropsFor(index)) }, track.handle.id);
7892
8541
  }),
7893
8542
  slots?.afterRows
7894
8543
  ] })),
7895
8544
  features.exportMidi && !designerView && !isLoadingTracks && tracks.length > 0 && (() => {
7896
8545
  const hasAnyMidi = tracks.some((t) => t.hasMidi);
7897
8546
  const exportDisabled = isExportingMidi || !hasAnyMidi;
7898
- return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { className: "pt-2", children: /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
8547
+ return /* @__PURE__ */ (0, import_jsx_runtime27.jsx)("div", { className: "pt-2", children: /* @__PURE__ */ (0, import_jsx_runtime27.jsx)(
7899
8548
  "button",
7900
8549
  {
7901
8550
  "data-testid": "export-midi-tracks-button",
@@ -7962,31 +8611,447 @@ function createSurgeSoundAdapter(host, overrides = {}) {
7962
8611
  };
7963
8612
  }
7964
8613
 
8614
+ // src/ensemble-core/voice-spec.ts
8615
+ var TOP = {
8616
+ label: "high florid line",
8617
+ role: "lead",
8618
+ registerLow: 72,
8619
+ registerHigh: 96,
8620
+ maxNotesPerBar: 8,
8621
+ rhythmPalette: "8ths and 16ths; melisma and short runs welcome",
8622
+ harmonicDiscipline: "freest voice \u2014 non-chord tones as passing/neighbor tones on weak beats, resolving by step",
8623
+ monoPreference: "high"
8624
+ };
8625
+ var COUNTER = {
8626
+ label: "countermelody",
8627
+ role: "strings",
8628
+ registerLow: 65,
8629
+ registerHigh: 86,
8630
+ maxNotesPerBar: 6,
8631
+ rhythmPalette: "8ths and quarters; move when the top voice rests",
8632
+ harmonicDiscipline: "mostly chord tones; may imitate the top voice's motifs a bar later",
8633
+ monoPreference: "high"
8634
+ };
8635
+ var INNER = {
8636
+ label: "inner voice",
8637
+ role: "strings",
8638
+ registerLow: 55,
8639
+ registerHigh: 76,
8640
+ maxNotesPerBar: 4,
8641
+ rhythmPalette: "quarters and halves",
8642
+ harmonicDiscipline: "chord tones with smooth stepwise motion between them",
8643
+ monoPreference: "high"
8644
+ };
8645
+ var INNER_2 = {
8646
+ label: "second inner voice",
8647
+ role: "strings",
8648
+ registerLow: 60,
8649
+ registerHigh: 81,
8650
+ maxNotesPerBar: 5,
8651
+ rhythmPalette: "quarters with occasional 8th-note motion",
8652
+ harmonicDiscipline: "chord tones; fill gaps the other inner voice leaves",
8653
+ monoPreference: "high"
8654
+ };
8655
+ var TENOR = {
8656
+ label: "low counterline",
8657
+ role: "strings",
8658
+ registerLow: 43,
8659
+ registerHigh: 64,
8660
+ maxNotesPerBar: 3,
8661
+ rhythmPalette: "quarters and halves; brief walking figures at cadences",
8662
+ harmonicDiscipline: "roots and fifths emphasized; passing tones only between chord tones",
8663
+ monoPreference: "low"
8664
+ };
8665
+ var BASS = {
8666
+ label: "bassline",
8667
+ role: "bass",
8668
+ registerLow: 36,
8669
+ registerHigh: 60,
8670
+ maxNotesPerBar: 3,
8671
+ rhythmPalette: "quarters and halves",
8672
+ harmonicDiscipline: "chord roots and fifths; stepwise approaches into chord changes",
8673
+ monoPreference: "low"
8674
+ };
8675
+ var SUB = {
8676
+ label: "sub anchor",
8677
+ role: "808s",
8678
+ registerLow: 24,
8679
+ registerHigh: 43,
8680
+ maxNotesPerBar: 2,
8681
+ rhythmPalette: "halves and whole notes; sustain into the bar",
8682
+ harmonicDiscipline: "ROOT pitch class only \u2014 the harmonic anchor",
8683
+ rootOnly: true,
8684
+ monoPreference: "low"
8685
+ };
8686
+ var ENSEMBLE_MIN_VOICES = 2;
8687
+ var ENSEMBLE_MAX_VOICES = 6;
8688
+ var SPEC_TABLES = {
8689
+ 2: [TOP, BASS],
8690
+ 3: [TOP, INNER, BASS],
8691
+ 4: [TOP, COUNTER, TENOR, BASS],
8692
+ 5: [TOP, COUNTER, INNER, TENOR, SUB],
8693
+ 6: [TOP, COUNTER, INNER_2, INNER, TENOR, SUB]
8694
+ };
8695
+ function defaultVoiceSpecs(voiceCount) {
8696
+ const n = Math.max(ENSEMBLE_MIN_VOICES, Math.min(ENSEMBLE_MAX_VOICES, Math.round(voiceCount)));
8697
+ return SPEC_TABLES[n].map((spec, voiceIndex) => ({ ...spec, voiceIndex }));
8698
+ }
8699
+
8700
+ // src/ensemble-core/enforce-voice.ts
8701
+ var MIN_NOTE_DURATION_BEATS = 0.0625;
8702
+ var BEATS_PER_BAR = 4;
8703
+ function foldPitchToRegister(pitch, low, high) {
8704
+ let p = pitch;
8705
+ while (p < low) p += 12;
8706
+ while (p > high) p -= 12;
8707
+ if (p < low) p = low;
8708
+ if (p > high) p = high;
8709
+ return p;
8710
+ }
8711
+ function nearestPitchWithPc(reference, pc, low, high) {
8712
+ let best = null;
8713
+ for (let p = low; p <= high; p++) {
8714
+ if ((p % 12 + 12) % 12 !== pc) continue;
8715
+ if (best === null || Math.abs(p - reference) < Math.abs(best - reference)) best = p;
8716
+ }
8717
+ return best ?? foldPitchToRegister(reference, low, high);
8718
+ }
8719
+ function snapToNearestPc(pitch, pcs) {
8720
+ if (pcs.size === 0) return pitch;
8721
+ for (let d = 0; d <= 6; d++) {
8722
+ if (pcs.has(((pitch - d) % 12 + 12) % 12)) return pitch - d;
8723
+ if (pcs.has(((pitch + d) % 12 + 12) % 12)) return pitch + d;
8724
+ }
8725
+ return pitch;
8726
+ }
8727
+ function enforceVoice(rawNotes, spec, opts) {
8728
+ const repairs = [];
8729
+ const clipEnd = opts.bars * BEATS_PER_BAR;
8730
+ let notes = [];
8731
+ for (const n of rawNotes) {
8732
+ if (!Number.isFinite(n.pitch) || !Number.isFinite(n.startBeat) || !Number.isFinite(n.durationBeats)) continue;
8733
+ if (n.startBeat >= clipEnd || n.startBeat < 0) {
8734
+ repairs.push(`voice ${spec.voiceIndex}: dropped note outside the ${opts.bars}-bar clip (start ${n.startBeat})`);
8735
+ continue;
8736
+ }
8737
+ const durationBeats = Math.max(
8738
+ MIN_NOTE_DURATION_BEATS,
8739
+ Math.min(n.durationBeats, clipEnd - n.startBeat)
8740
+ );
8741
+ notes.push({ ...n, durationBeats });
8742
+ }
8743
+ notes = notes.map((n) => {
8744
+ const folded = foldPitchToRegister(Math.round(n.pitch), spec.registerLow, spec.registerHigh);
8745
+ if (folded !== n.pitch) {
8746
+ repairs.push(`voice ${spec.voiceIndex}: folded pitch ${n.pitch} into register ${spec.registerLow}-${spec.registerHigh} (${folded})`);
8747
+ }
8748
+ return { ...n, pitch: folded };
8749
+ });
8750
+ if (spec.rootOnly && opts.chordRootPcAtBar) {
8751
+ notes = notes.map((n) => {
8752
+ const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
8753
+ const rootPc = opts.chordRootPcAtBar(bar);
8754
+ if (rootPc === null) return n;
8755
+ const pinned = nearestPitchWithPc(n.pitch, rootPc, spec.registerLow, spec.registerHigh);
8756
+ if (pinned !== n.pitch) {
8757
+ repairs.push(`voice ${spec.voiceIndex}: pinned bar ${bar + 1} note to the chord root (${n.pitch} \u2192 ${pinned})`);
8758
+ }
8759
+ return { ...n, pitch: pinned };
8760
+ });
8761
+ }
8762
+ if (opts.scalePcs && opts.scalePcs.size > 0 && !spec.rootOnly) {
8763
+ notes = notes.map((n) => {
8764
+ const pc = (n.pitch % 12 + 12) % 12;
8765
+ if (opts.scalePcs.has(pc)) return n;
8766
+ const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
8767
+ const chordPcs = opts.chordPcsAtBar?.(bar);
8768
+ if (chordPcs?.has(pc)) return n;
8769
+ const snapped = foldPitchToRegister(
8770
+ snapToNearestPc(n.pitch, opts.scalePcs),
8771
+ spec.registerLow,
8772
+ spec.registerHigh
8773
+ );
8774
+ if (snapped !== n.pitch) {
8775
+ repairs.push(`voice ${spec.voiceIndex}: snapped out-of-key pitch ${n.pitch} \u2192 ${snapped}`);
8776
+ }
8777
+ return { ...n, pitch: snapped };
8778
+ });
8779
+ }
8780
+ notes.sort((a, b) => a.startBeat - b.startBeat || (spec.monoPreference === "high" ? b.pitch - a.pitch : a.pitch - b.pitch));
8781
+ const mono = [];
8782
+ for (const n of notes) {
8783
+ const prev = mono[mono.length - 1];
8784
+ if (prev && Math.abs(prev.startBeat - n.startBeat) < 1e-9) {
8785
+ repairs.push(`voice ${spec.voiceIndex}: dropped simultaneous note ${n.pitch} at beat ${n.startBeat} (voice is one line)`);
8786
+ continue;
8787
+ }
8788
+ if (prev && prev.startBeat + prev.durationBeats > n.startBeat) {
8789
+ prev.durationBeats = Math.max(MIN_NOTE_DURATION_BEATS, n.startBeat - prev.startBeat);
8790
+ }
8791
+ mono.push({ ...n });
8792
+ }
8793
+ const byBar = /* @__PURE__ */ new Map();
8794
+ for (const n of mono) {
8795
+ const bar = Math.floor(n.startBeat / BEATS_PER_BAR);
8796
+ const bucket = byBar.get(bar) ?? [];
8797
+ bucket.push(n);
8798
+ byBar.set(bar, bucket);
8799
+ }
8800
+ const kept = new Set(mono);
8801
+ for (const [bar, bucket] of byBar) {
8802
+ if (bucket.length <= spec.maxNotesPerBar) continue;
8803
+ const strength = (n) => {
8804
+ const beatInBar = n.startBeat - bar * BEATS_PER_BAR;
8805
+ const onDownbeat = Math.abs(beatInBar % 1) < 1e-9 ? 1 : 0;
8806
+ return n.durationBeats * 4 + n.velocity / 127 + onDownbeat * 2;
8807
+ };
8808
+ const ranked = [...bucket].sort((a, b) => strength(a) - strength(b));
8809
+ const excess = bucket.length - spec.maxNotesPerBar;
8810
+ for (let i = 0; i < excess; i++) {
8811
+ kept.delete(ranked[i]);
8812
+ }
8813
+ repairs.push(`voice ${spec.voiceIndex}: thinned bar ${bar + 1} from ${bucket.length} to ${spec.maxNotesPerBar} notes (density cap)`);
8814
+ }
8815
+ return { notes: mono.filter((n) => kept.has(n)), repairs };
8816
+ }
8817
+
8818
+ // src/ensemble-core/analyze-ensemble.ts
8819
+ var EPS = 1e-6;
8820
+ function pitchAt(voice, beat) {
8821
+ for (const n of voice) {
8822
+ if (n.startBeat - EPS <= beat && beat < n.startBeat + n.durationBeats - EPS) return n.pitch;
8823
+ }
8824
+ return null;
8825
+ }
8826
+ function onsetSet(voice) {
8827
+ return [...new Set(voice.map((n) => Math.round(n.startBeat * 96) / 96))].sort((a, b) => a - b);
8828
+ }
8829
+ function analyzeEnsemble(voices) {
8830
+ const pairs = [];
8831
+ for (let i = 0; i + 1 < voices.length; i++) {
8832
+ const upper = voices[i];
8833
+ const lower = voices[i + 1];
8834
+ const upperOnsets = onsetSet(upper);
8835
+ const lowerOnsets = new Set(onsetSet(lower));
8836
+ const shared = upperOnsets.filter((b) => lowerOnsets.has(b));
8837
+ const onsetIndependence = upperOnsets.length === 0 ? 1 : 1 - shared.length / upperOnsets.length;
8838
+ const motion = { contrary: 0, oblique: 0, similar: 0, parallel: 0 };
8839
+ const parallelPerfects = [];
8840
+ for (let s = 0; s + 1 < shared.length; s++) {
8841
+ const [b0, b1] = [shared[s], shared[s + 1]];
8842
+ const u0 = pitchAt(upper, b0);
8843
+ const u1 = pitchAt(upper, b1);
8844
+ const l0 = pitchAt(lower, b0);
8845
+ const l1 = pitchAt(lower, b1);
8846
+ if (u0 === null || u1 === null || l0 === null || l1 === null) continue;
8847
+ const du = Math.sign(u1 - u0);
8848
+ const dl = Math.sign(l1 - l0);
8849
+ let kind;
8850
+ if (du === 0 || dl === 0) kind = "oblique";
8851
+ else if (du !== dl) kind = "contrary";
8852
+ else {
8853
+ kind = u1 - l1 === u0 - l0 ? "parallel" : "similar";
8854
+ }
8855
+ motion[kind] += 1;
8856
+ if (kind === "parallel") {
8857
+ const intervalPc = ((u1 - l1) % 12 + 12) % 12;
8858
+ if (intervalPc === 0 || intervalPc === 7) {
8859
+ parallelPerfects.push({ atBeat: b1, intervalPc });
8860
+ }
8861
+ }
8862
+ }
8863
+ const crossings = [];
8864
+ for (const b of [...upperOnsets, ...onsetSet(lower)]) {
8865
+ const u = pitchAt(upper, b);
8866
+ const l = pitchAt(lower, b);
8867
+ if (u !== null && l !== null && u < l) crossings.push(b);
8868
+ }
8869
+ pairs.push({
8870
+ upperVoice: i,
8871
+ onsetIndependence,
8872
+ motion,
8873
+ parallelPerfects,
8874
+ crossings: [...new Set(crossings)].sort((a, b) => a - b)
8875
+ });
8876
+ }
8877
+ const allOnsets = voices.map(onsetSet);
8878
+ const union = new Set(allOnsets.flat());
8879
+ let sharedByAll = 0;
8880
+ for (const b of union) {
8881
+ if (allOnsets.every((o) => o.length === 0 || o.includes(b))) sharedByAll += 1;
8882
+ }
8883
+ const homorhythmScore = union.size === 0 ? 0 : sharedByAll / union.size;
8884
+ return { pairs, homorhythmScore };
8885
+ }
8886
+ function describeViolations(analysis, rules) {
8887
+ const out = [];
8888
+ for (const pair of analysis.pairs) {
8889
+ const label = `between voice ${pair.upperVoice + 1} and voice ${pair.upperVoice + 2}`;
8890
+ if (rules.forbidParallelPerfects) {
8891
+ for (const p of pair.parallelPerfects) {
8892
+ out.push(`Parallel ${p.intervalPc === 0 ? "octaves" : "fifths"} ${label} at beat ${p.atBeat} \u2014 approach perfect intervals by contrary or oblique motion.`);
8893
+ }
8894
+ }
8895
+ if (rules.forbidVoiceCrossing && pair.crossings.length > 0) {
8896
+ out.push(`Voice crossing ${label} at beat${pair.crossings.length > 1 ? "s" : ""} ${pair.crossings.join(", ")} \u2014 keep each voice inside its register lane.`);
8897
+ }
8898
+ if (pair.onsetIndependence < rules.minOnsetIndependence) {
8899
+ out.push(`Voices ${pair.upperVoice + 1} and ${pair.upperVoice + 2} attack together too often (independence ${pair.onsetIndependence.toFixed(2)} < ${rules.minOnsetIndependence}) \u2014 stagger entrances and let one voice move while the other holds.`);
8900
+ }
8901
+ }
8902
+ return out;
8903
+ }
8904
+
8905
+ // src/ensemble-core/styles.ts
8906
+ var ENSEMBLE_STYLES = ["counterpoint", "chorale", "interlock"];
8907
+ var STYLE_RULES = {
8908
+ counterpoint: {
8909
+ forbidParallelPerfects: true,
8910
+ forbidVoiceCrossing: true,
8911
+ minOnsetIndependence: 0.35,
8912
+ promptParagraph: "STYLE \u2014 COUNTERPOINT (modern, not strict species): independent singable lines. Favor contrary and oblique motion between neighbors; approach perfect intervals by contrary motion; imitate motifs between voices a bar apart; stagger entrances so voices converse instead of speaking at once."
8913
+ },
8914
+ chorale: {
8915
+ forbidParallelPerfects: true,
8916
+ forbidVoiceCrossing: true,
8917
+ minOnsetIndependence: 0,
8918
+ promptParagraph: "STYLE \u2014 CHORALE: homorhythmic block harmony. Voices move together on the same rhythm with smooth voice-leading \u2014 nearest chord tone, common tones held, no leaps larger than a fifth in inner voices."
8919
+ },
8920
+ interlock: {
8921
+ forbidParallelPerfects: false,
8922
+ forbidVoiceCrossing: false,
8923
+ minOnsetIndependence: 0.6,
8924
+ promptParagraph: "STYLE \u2014 INTERLOCK (minimal / systems music): short repeating cells that mesh like gears. Each voice keeps its own ostinato; onsets rarely coincide with the neighboring voice; parallel motion and doubling are welcome when the composite rhythm stays busy and even."
8925
+ }
8926
+ };
8927
+
8928
+ // src/ensemble-core/ensemble-schema.ts
8929
+ var SUBMIT_ENSEMBLE_TOOL_NAME = "submit_ensemble";
8930
+ function buildSubmitEnsembleParameters(voiceCount) {
8931
+ return {
8932
+ type: "object",
8933
+ properties: {
8934
+ voices: {
8935
+ type: "array",
8936
+ description: `Exactly ${voiceCount} voices, voiceIndex 0 (top) through ${voiceCount - 1} (bottom).`,
8937
+ items: {
8938
+ type: "object",
8939
+ properties: {
8940
+ voiceIndex: { type: "integer", description: "0 = top voice" },
8941
+ notes: {
8942
+ type: "array",
8943
+ items: {
8944
+ type: "object",
8945
+ properties: {
8946
+ pitch: { type: "integer", description: "MIDI note number 0-127" },
8947
+ startBeat: { type: "number", description: "quarter-note beats from clip start" },
8948
+ durationBeats: { type: "number", description: "duration in quarter-note beats" },
8949
+ velocity: { type: "integer", description: "1-127" }
8950
+ },
8951
+ required: ["pitch", "startBeat", "durationBeats", "velocity"]
8952
+ }
8953
+ }
8954
+ },
8955
+ required: ["voiceIndex", "notes"]
8956
+ }
8957
+ }
8958
+ },
8959
+ required: ["voices"]
8960
+ };
8961
+ }
8962
+ function parseEnsembleArgs(args, voiceCount) {
8963
+ if (typeof args !== "object" || args === null) return null;
8964
+ const voicesRaw = args.voices;
8965
+ if (!Array.isArray(voicesRaw)) return null;
8966
+ const warnings = [];
8967
+ const voiceNotes = Array.from({ length: voiceCount }, () => []);
8968
+ for (const v of voicesRaw) {
8969
+ if (typeof v !== "object" || v === null) continue;
8970
+ const idxRaw = v.voiceIndex;
8971
+ const idx = typeof idxRaw === "number" ? Math.round(idxRaw) : NaN;
8972
+ if (!Number.isInteger(idx) || idx < 0 || idx >= voiceCount) {
8973
+ warnings.push(`ignored voice with out-of-range voiceIndex ${String(idxRaw)}`);
8974
+ continue;
8975
+ }
8976
+ const notesRaw = v.notes;
8977
+ if (!Array.isArray(notesRaw)) continue;
8978
+ const notes = [];
8979
+ let dropped = 0;
8980
+ for (const n of notesRaw) {
8981
+ const note = n;
8982
+ if (typeof note?.pitch === "number" && typeof note?.startBeat === "number" && typeof note?.durationBeats === "number" && typeof note?.velocity === "number" && note.durationBeats > 0 && note.startBeat >= 0) {
8983
+ notes.push({
8984
+ pitch: Math.max(0, Math.min(127, Math.round(note.pitch))),
8985
+ startBeat: note.startBeat,
8986
+ durationBeats: note.durationBeats,
8987
+ velocity: Math.max(1, Math.min(127, Math.round(note.velocity)))
8988
+ });
8989
+ } else {
8990
+ dropped += 1;
8991
+ }
8992
+ }
8993
+ if (dropped > 0) warnings.push(`voice ${idx}: dropped ${dropped} malformed note(s)`);
8994
+ voiceNotes[idx] = notes;
8995
+ }
8996
+ const returned = voicesRaw.length;
8997
+ if (returned !== voiceCount) {
8998
+ warnings.push(`model returned ${returned} voices for a ${voiceCount}-voice ensemble`);
8999
+ }
9000
+ const anyNotes = voiceNotes.some((v) => v.length > 0);
9001
+ return anyNotes ? { voiceNotes, warnings } : null;
9002
+ }
9003
+
9004
+ // src/ensemble-core/ensemble-prompt.ts
9005
+ function voiceContractLine(spec) {
9006
+ const root = spec.rootOnly ? " ROOT PITCH CLASS ONLY (each bar's chord root)." : "";
9007
+ return `- Voice ${spec.voiceIndex + 1} (${spec.label}): MIDI ${spec.registerLow}-${spec.registerHigh}, max ${spec.maxNotesPerBar} notes/bar, rhythm: ${spec.rhythmPalette}. ${spec.harmonicDiscipline}.${root}`;
9008
+ }
9009
+ function buildEnsembleSystemPrompt(specs, style) {
9010
+ const styleParagraph = STYLE_RULES[style].promptParagraph;
9011
+ return `You are an ensemble composer. Compose ${specs.length} voices as ONE piece of music \u2014 not ${specs.length} independent parts.
9012
+
9013
+ Submit your composition by calling the ${SUBMIT_ENSEMBLE_TOOL_NAME} tool with all ${specs.length} voices.
9014
+
9015
+ THE ENSEMBLE RULES (most important):
9016
+ 1. The voices are composed TOGETHER. They must relate: imitate and answer each other's motifs, move in contrary or oblique motion against neighbors, and stagger entrances so lines converse.
9017
+ 2. Each voice is a SINGLE monophonic line \u2014 within one voice, no two notes overlap in time and no chords. Voices MAY and SHOULD overlap EACH OTHER; that overlap is the music.
9018
+ 3. Complexity decreases downward: the top voice is the most active and ornamented, the bottom voice the sparsest anchor. Respect each voice's register window and notes-per-bar cap exactly.
9019
+ 4. Follow the chord progression in the musical context exactly. Non-chord tones only as passing or neighbor tones on weak beats, resolving by step.
9020
+ 5. Not every voice plays all the time \u2014 rests are structural. Avoid all voices attacking the same beat except at phrase boundaries.
9021
+ 6. startBeat/durationBeats are in quarter-note beats from the start of the clip; fill the stated bar length, and make bar 1 land immediately (no long silent intro).
9022
+
9023
+ ${styleParagraph}
9024
+
9025
+ PER-VOICE CONTRACTS:
9026
+ ${specs.map(voiceContractLine).join("\n")}`;
9027
+ }
9028
+ function buildViolationRetrySuffix(violations) {
9029
+ if (violations.length === 0) return "";
9030
+ return `
9031
+
9032
+ Your previous ensemble had these problems \u2014 fix them while keeping everything that worked:
9033
+ ` + violations.map((v) => `- ${v}`).join("\n");
9034
+ }
9035
+
7965
9036
  // src/constants/sdk-version.ts
7966
- var PLUGIN_SDK_VERSION = "2.40.0";
9037
+ var PLUGIN_SDK_VERSION = "2.42.0";
7967
9038
 
7968
9039
  // src/utils/format-concurrent-tracks.ts
7969
9040
  function formatConcurrentTracks(ctx) {
7970
9041
  const tracks = ctx.concurrentTracks;
7971
9042
  if (!tracks || tracks.length === 0) return "";
7972
- const lines = [`Concurrent tracks in scene (already generated):`];
7973
- for (const track of tracks) {
7974
- const promptStr = track.prompt ? ` prompt="${escapeQuotes(track.prompt)}"` : "";
7975
- lines.push(` - role=${track.role ?? "unknown"}${promptStr}`);
7976
- if (track.notesByChord.length === 0) {
7977
- lines.push(` (no notes)`);
7978
- } else {
7979
- for (const segment of track.notesByChord) {
7980
- if (segment.notes.length === 0) continue;
7981
- lines.push(` ${formatChordSegment(segment)}`);
7982
- }
7983
- }
7984
- if (track.truncated && typeof track.originalNoteCount === "number") {
7985
- const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);
7986
- if (dropped > 0) {
7987
- lines.push(` \u2026 (${dropped} more notes truncated)`);
7988
- }
7989
- }
9043
+ const pinned = tracks.filter((t) => t.pinned);
9044
+ const ambient = tracks.filter((t) => !t.pinned);
9045
+ const lines = [];
9046
+ if (pinned.length > 0) {
9047
+ lines.push(
9048
+ "REFERENCE TRACKS (write in counterpoint against these \u2014 interlock with their rhythm, avoid doubling their onsets, favor contrary or oblique motion):"
9049
+ );
9050
+ for (const track of pinned) pushTrackLines(lines, track);
9051
+ }
9052
+ if (ambient.length > 0) {
9053
+ lines.push(`Concurrent tracks in scene (already generated):`);
9054
+ for (const track of ambient) pushTrackLines(lines, track);
7990
9055
  }
7991
9056
  if (ctx.truncatedTrackCount && ctx.truncatedTrackCount > 0) {
7992
9057
  lines.push(
@@ -7995,6 +9060,25 @@ function formatConcurrentTracks(ctx) {
7995
9060
  }
7996
9061
  return lines.join("\n");
7997
9062
  }
9063
+ function pushTrackLines(lines, track) {
9064
+ const nameStr = track.name ? ` name="${escapeQuotes(track.name)}"` : "";
9065
+ const promptStr = track.prompt ? ` prompt="${escapeQuotes(track.prompt)}"` : "";
9066
+ lines.push(` - role=${track.role ?? "unknown"}${nameStr}${promptStr}`);
9067
+ if (track.notesByChord.length === 0) {
9068
+ lines.push(` (no notes)`);
9069
+ } else {
9070
+ for (const segment of track.notesByChord) {
9071
+ if (segment.notes.length === 0) continue;
9072
+ lines.push(` ${formatChordSegment(segment)}`);
9073
+ }
9074
+ }
9075
+ if (track.truncated && typeof track.originalNoteCount === "number") {
9076
+ const dropped = track.originalNoteCount - sumKeptNotes(track.notesByChord);
9077
+ if (dropped > 0) {
9078
+ lines.push(` \u2026 (${dropped} more notes truncated)`);
9079
+ }
9080
+ }
9081
+ }
7998
9082
  function formatChordSegment(segment) {
7999
9083
  const [start, end] = segment.chordRangeQn;
8000
9084
  const notesJson = JSON.stringify(segment.notes.map(compactNote2));
@@ -8120,6 +9204,9 @@ function pickTopKWeighted(scored, options = {}) {
8120
9204
  DownloadPackButton,
8121
9205
  EMPTY_FX_DETAIL_STATE,
8122
9206
  EMPTY_FX_STATE,
9207
+ ENSEMBLE_MAX_VOICES,
9208
+ ENSEMBLE_MIN_VOICES,
9209
+ ENSEMBLE_STYLES,
8123
9210
  EQUAL_POWER_GAIN,
8124
9211
  FX_CATEGORIES,
8125
9212
  FX_CHAIN_ORDER,
@@ -8131,9 +9218,11 @@ function pickTopKWeighted(scored, options = {}) {
8131
9218
  FxToggleBar,
8132
9219
  GUTTER_W,
8133
9220
  GeneratorPanelShell,
9221
+ GroupFadeTrackRow,
8134
9222
  ImportTrackModal,
8135
9223
  InstrumentDrawer,
8136
9224
  LevelMeter,
9225
+ MIN_NOTE_DURATION_BEATS,
8137
9226
  Modal,
8138
9227
  OffsetScrubber,
8139
9228
  PLUGIN_SDK_VERSION,
@@ -8145,6 +9234,8 @@ function pickTopKWeighted(scored, options = {}) {
8145
9234
  RESIZE_HANDLE_PX,
8146
9235
  ROW_HEIGHT,
8147
9236
  SLIDER_UNITY,
9237
+ STYLE_RULES,
9238
+ SUBMIT_ENSEMBLE_TOOL_NAME,
8148
9239
  SamplePackCTACard,
8149
9240
  ScrollingWaveform,
8150
9241
  SorceryProgressBar,
@@ -8157,6 +9248,7 @@ function pickTopKWeighted(scored, options = {}) {
8157
9248
  TransitionDesigner,
8158
9249
  VolumeSlider,
8159
9250
  WaveformView,
9251
+ analyzeEnsemble,
8160
9252
  analyzeWavPeak,
8161
9253
  asAudioEffect,
8162
9254
  asCrossfadeMeta,
@@ -8164,8 +9256,11 @@ function pickTopKWeighted(scored, options = {}) {
8164
9256
  asTransitionDesignerDraft,
8165
9257
  buildCrossfadeInpaintPrompt,
8166
9258
  buildCrossfadeVolumeCurves,
9259
+ buildEnsembleSystemPrompt,
8167
9260
  buildFadeVolumeCurve,
8168
9261
  buildRowSlots,
9262
+ buildSubmitEnsembleParameters,
9263
+ buildViolationRetrySuffix,
8169
9264
  calculateTimeBasedTarget,
8170
9265
  cellToPx,
8171
9266
  centerScrollTop,
@@ -8174,15 +9269,21 @@ function pickTopKWeighted(scored, options = {}) {
8174
9269
  dbIdsFromKeys,
8175
9270
  dbToSlider,
8176
9271
  defaultFadeGesture,
9272
+ defaultVoiceSpecs,
9273
+ describeViolations,
8177
9274
  drawWaveform,
9275
+ enforceVoice,
9276
+ foldPitchToRegister,
8178
9277
  formatConcurrentTracks,
8179
9278
  hashString,
8180
9279
  moveItem,
9280
+ nearestPitchWithPc,
8181
9281
  newTrackState,
8182
9282
  normalizeSlots,
8183
9283
  padPair,
8184
9284
  padSlots,
8185
9285
  parseCrossfadePairs,
9286
+ parseEnsembleArgs,
8186
9287
  parseFades,
8187
9288
  parseLLMNoteResponse,
8188
9289
  parseTrackGroups,
@@ -8199,6 +9300,7 @@ function pickTopKWeighted(scored, options = {}) {
8199
9300
  sliderToDb,
8200
9301
  slotsEqual,
8201
9302
  soundIdentity,
9303
+ splitFadeEntries,
8202
9304
  synthesizeCuePoints,
8203
9305
  tokenizePrompt,
8204
9306
  trackDataKey,