@jxgame2020/dsh-token-quota 0.1.23 → 0.1.26

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/lib/client.js CHANGED
@@ -40,6 +40,7 @@ window.__ModuleLoader__.load({
40
40
  musicEnabled: false,
41
41
  musicVolume: .5,
42
42
  musicStyle: "major",
43
+ musicOnlyCurrentSession: true,
43
44
  upgrade: null,
44
45
  upgradeDismissed: false,
45
46
  checkingUpdates: false,
@@ -90,8 +91,11 @@ window.__ModuleLoader__.load({
90
91
  setMusicStyle: (d, style) => {
91
92
  d.musicStyle = style;
92
93
  },
94
+ setMusicOnlyCurrentSession: (d, only) => {
95
+ d.musicOnlyCurrentSession = only;
96
+ },
93
97
  setUpgrade: (d, upgrade) => {
94
- const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.23" ? upgrade : null;
98
+ const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.26" ? upgrade : null;
95
99
  if (d.upgrade === null || d.upgrade.latestVersion !== normalized?.latestVersion) d.upgradeDismissed = false;
96
100
  d.upgrade = normalized;
97
101
  },
@@ -197,92 +201,92 @@ window.__ModuleLoader__.load({
197
201
  document.head.appendChild(tag);
198
202
  }
199
203
  var TokenQuotaPanel_module_css_default = {
200
- "row": "NPLLIq_row",
201
- "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
202
- "bar": "NPLLIq_bar",
204
+ "checkBtn": "NPLLIq_checkBtn",
205
+ "body": "NPLLIq_body",
206
+ "dialogLabel": "NPLLIq_dialogLabel",
207
+ "upgradeBanner": "NPLLIq_upgradeBanner",
208
+ "dialogActions": "NPLLIq_dialogActions",
209
+ "currentBadge": "NPLLIq_currentBadge",
210
+ "dialogBody": "NPLLIq_dialogBody",
211
+ "notice": "NPLLIq_notice",
212
+ "rowMeta": "NPLLIq_rowMeta",
203
213
  "title": "NPLLIq_title",
214
+ "logPagerInfo": "NPLLIq_logPagerInfo",
215
+ "headerText": "NPLLIq_headerText",
216
+ "bar": "NPLLIq_bar",
204
217
  "noticeError": "NPLLIq_noticeError",
205
- "rowHeader": "NPLLIq_rowHeader",
206
- "settingsBtn": "NPLLIq_settingsBtn",
207
- "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
208
- "logHeader": "NPLLIq_logHeader",
209
- "logTable": "NPLLIq_logTable",
218
+ "tab": "NPLLIq_tab",
219
+ "logScroll": "NPLLIq_logScroll",
210
220
  "dialogTitle": "NPLLIq_dialogTitle",
211
- "dialogSection": "NPLLIq_dialogSection",
212
- "dialogActions": "NPLLIq_dialogActions",
221
+ "selectBtn": "NPLLIq_selectBtn",
222
+ "monitorList": "NPLLIq_monitorList",
223
+ "switch": "NPLLIq_switch",
224
+ "monitorRow": "NPLLIq_monitorRow",
225
+ "resetSelect": "NPLLIq_resetSelect",
226
+ "dialogBtn": "NPLLIq_dialogBtn",
227
+ "logHeader": "NPLLIq_logHeader",
228
+ "panelDim": "NPLLIq_panelDim",
229
+ "logToolbar": "NPLLIq_logToolbar",
230
+ "upgradeCommands": "NPLLIq_upgradeCommands",
231
+ "upgradeHint": "NPLLIq_upgradeHint",
232
+ "logDayCol": "NPLLIq_logDayCol",
233
+ "settingsBtn": "NPLLIq_settingsBtn",
213
234
  "tabLabel": "NPLLIq_tabLabel",
214
- "logModelCol": "NPLLIq_logModelCol",
215
- "logPagerPages": "NPLLIq_logPagerPages",
216
- "logSearch": "NPLLIq_logSearch",
217
- "monitorName": "NPLLIq_monitorName",
218
- "controls": "NPLLIq_controls",
219
235
  "dialogClose": "NPLLIq_dialogClose",
236
+ "fillOver": "NPLLIq_fillOver",
237
+ "rowHeader": "NPLLIq_rowHeader",
238
+ "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
239
+ "copyBtn": "NPLLIq_copyBtn",
240
+ "upgradeClose": "NPLLIq_upgradeClose",
241
+ "logModelCol": "NPLLIq_logModelCol",
242
+ "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
243
+ "logPagerBtn": "NPLLIq_logPagerBtn",
244
+ "upgradeBannerText": "NPLLIq_upgradeBannerText",
245
+ "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
246
+ "fillWarn": "NPLLIq_fillWarn",
247
+ "headerActions": "NPLLIq_headerActions",
248
+ "save": "NPLLIq_save",
249
+ "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
250
+ "settingsBtnActive": "NPLLIq_settingsBtnActive",
251
+ "radioRow": "NPLLIq_radioRow",
252
+ "logClearBtn": "NPLLIq_logClearBtn",
220
253
  "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
221
254
  "dialogHint": "NPLLIq_dialogHint",
222
- "checkBtn": "NPLLIq_checkBtn",
223
- "dialogBody": "NPLLIq_dialogBody",
224
- "rowName": "NPLLIq_rowName",
225
- "copyBtnDone": "NPLLIq_copyBtnDone",
226
- "logClearDate": "NPLLIq_logClearDate",
227
- "notice": "NPLLIq_notice",
228
- "resetRow": "NPLLIq_resetRow",
229
- "rowMeta": "NPLLIq_rowMeta",
230
255
  "logClearDialog": "NPLLIq_logClearDialog",
231
- "subtitle": "NPLLIq_subtitle",
232
- "panel": "NPLLIq_panel",
233
- "monitorHint": "NPLLIq_monitorHint",
256
+ "monitorName": "NPLLIq_monitorName",
257
+ "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
234
258
  "logPager": "NPLLIq_logPager",
235
- "save": "NPLLIq_save",
236
- "tab": "NPLLIq_tab",
237
- "selectBtn": "NPLLIq_selectBtn",
238
- "upgradeCommand": "NPLLIq_upgradeCommand",
239
- "resetSelect": "NPLLIq_resetSelect",
240
- "logScroll": "NPLLIq_logScroll",
241
- "monitorRow": "NPLLIq_monitorRow",
242
- "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
243
- "logPagerControls": "NPLLIq_logPagerControls",
244
- "logDayCol": "NPLLIq_logDayCol",
245
- "header": "NPLLIq_header",
259
+ "logClearDate": "NPLLIq_logClearDate",
260
+ "rowName": "NPLLIq_rowName",
246
261
  "logDialog": "NPLLIq_logDialog",
247
- "radioRow": "NPLLIq_radioRow",
248
262
  "dialogOverlay": "NPLLIq_dialogOverlay",
249
- "collapse": "NPLLIq_collapse",
250
- "fillWarn": "NPLLIq_fillWarn",
251
- "tabIcon": "NPLLIq_tabIcon",
252
- "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
253
- "switch": "NPLLIq_switch",
263
+ "logUsedCol": "NPLLIq_logUsedCol",
264
+ "input": "NPLLIq_input",
254
265
  "gearBtn": "NPLLIq_gearBtn",
255
- "dialogLabel": "NPLLIq_dialogLabel",
256
- "upgradeBannerText": "NPLLIq_upgradeBannerText",
266
+ "row": "NPLLIq_row",
267
+ "monitorHint": "NPLLIq_monitorHint",
268
+ "header": "NPLLIq_header",
269
+ "logPagerPages": "NPLLIq_logPagerPages",
257
270
  "versionNew": "NPLLIq_versionNew",
258
- "monitorList": "NPLLIq_monitorList",
259
- "logClearBtn": "NPLLIq_logClearBtn",
260
- "settingsBtnActive": "NPLLIq_settingsBtnActive",
261
- "currentBadge": "NPLLIq_currentBadge",
271
+ "subtitle": "NPLLIq_subtitle",
272
+ "version": "NPLLIq_version",
262
273
  "fullNotice": "NPLLIq_fullNotice",
263
- "dialogBtn": "NPLLIq_dialogBtn",
264
- "input": "NPLLIq_input",
265
- "panelDimStrong": "NPLLIq_panelDimStrong",
266
- "logToolbar": "NPLLIq_logToolbar",
267
- "logPagerBtn": "NPLLIq_logPagerBtn",
268
- "panelDim": "NPLLIq_panelDim",
269
- "upgradeBanner": "NPLLIq_upgradeBanner",
270
- "copyBtn": "NPLLIq_copyBtn",
271
- "body": "NPLLIq_body",
272
- "upgradeHint": "NPLLIq_upgradeHint",
273
- "dialog": "NPLLIq_dialog",
274
+ "resetRow": "NPLLIq_resetRow",
275
+ "logTable": "NPLLIq_logTable",
274
276
  "fillIdle": "NPLLIq_fillIdle",
275
- "headerText": "NPLLIq_headerText",
276
- "logPagerInfo": "NPLLIq_logPagerInfo",
277
- "headerActions": "NPLLIq_headerActions",
278
277
  "checkErrorHint": "NPLLIq_checkErrorHint",
279
- "upgradeCommands": "NPLLIq_upgradeCommands",
280
- "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
281
- "version": "NPLLIq_version",
282
- "upgradeClose": "NPLLIq_upgradeClose",
278
+ "tabIcon": "NPLLIq_tabIcon",
279
+ "logPagerControls": "NPLLIq_logPagerControls",
280
+ "collapse": "NPLLIq_collapse",
281
+ "upgradeCommand": "NPLLIq_upgradeCommand",
282
+ "copyBtnDone": "NPLLIq_copyBtnDone",
283
+ "dialogSection": "NPLLIq_dialogSection",
284
+ "controls": "NPLLIq_controls",
285
+ "panel": "NPLLIq_panel",
286
+ "panelDimStrong": "NPLLIq_panelDimStrong",
287
+ "logSearch": "NPLLIq_logSearch",
283
288
  "dialogHeader": "NPLLIq_dialogHeader",
284
- "logUsedCol": "NPLLIq_logUsedCol",
285
- "fillOver": "NPLLIq_fillOver"
289
+ "dialog": "NPLLIq_dialog"
286
290
  };
287
291
  //#endregion
288
292
  //#region lib/types/client/TokenQuotaPanel.js
@@ -369,7 +373,7 @@ window.__ModuleLoader__.load({
369
373
  * @param props - composed slot props.
370
374
  * @returns the panel element tree.
371
375
  */
372
- function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, useStore, actions, useSessions }) {
376
+ function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions }) {
373
377
  const [collapsed, setCollapsed] = (0, react.useState)(false);
374
378
  const [drafts, setDrafts] = (0, react.useState)({});
375
379
  const [editingKey, setEditingKey] = (0, react.useState)(null);
@@ -403,6 +407,7 @@ window.__ModuleLoader__.load({
403
407
  const musicEnabled = useStore((s) => s.musicEnabled);
404
408
  const musicVolume = useStore((s) => s.musicVolume);
405
409
  const musicStyle = useStore((s) => s.musicStyle);
410
+ const musicOnlyCurrentSession = useStore((s) => s.musicOnlyCurrentSession);
406
411
  const upgrade = useStore((s) => s.upgrade);
407
412
  const upgradeDismissed = useStore((s) => s.upgradeDismissed);
408
413
  const checkingUpdates = useStore((s) => s.checkingUpdates);
@@ -587,13 +592,13 @@ window.__ModuleLoader__.load({
587
592
  title: t("upgradeAvailableHint"),
588
593
  children: [
589
594
  "v",
590
- "0.1.23",
595
+ "0.1.26",
591
596
  " → v",
592
597
  upgrade.latestVersion
593
598
  ]
594
599
  }) : (0, react_jsx_runtime.jsxs)("span", {
595
600
  className: TokenQuotaPanel_module_css_default.version,
596
- children: ["v", "0.1.23"]
601
+ children: ["v", "0.1.26"]
597
602
  })]
598
603
  }), (0, react_jsx_runtime.jsx)("div", {
599
604
  className: TokenQuotaPanel_module_css_default.subtitle,
@@ -956,41 +961,58 @@ window.__ModuleLoader__.load({
956
961
  className: TokenQuotaPanel_module_css_default.dialogHint,
957
962
  children: t("musicHint")
958
963
  }),
959
- musicEnabled && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("label", {
960
- className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
961
- children: [(0, react_jsx_runtime.jsx)("span", { children: t("musicVolume") }), (0, react_jsx_runtime.jsx)("input", {
962
- type: "range",
963
- min: 0,
964
- max: 1,
965
- step: .05,
966
- value: musicVolume,
967
- onChange: (event) => {
968
- setMusicVolume(Number(event.target.value));
969
- }
970
- })]
971
- }), (0, react_jsx_runtime.jsxs)("label", {
972
- className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
973
- children: [(0, react_jsx_runtime.jsx)("span", { children: t("musicStyle") }), (0, react_jsx_runtime.jsxs)("select", {
974
- value: musicStyle,
975
- onChange: (event) => {
976
- setMusicStyle(event.target.value);
977
- },
978
- children: [
979
- (0, react_jsx_runtime.jsx)("option", {
980
- value: "major",
981
- children: t("musicStyleMajor")
982
- }),
983
- (0, react_jsx_runtime.jsx)("option", {
984
- value: "minor",
985
- children: t("musicStyleMinor")
986
- }),
987
- (0, react_jsx_runtime.jsx)("option", {
988
- value: "pentatonic",
989
- children: t("musicStylePentatonic")
990
- })
991
- ]
992
- })]
993
- })] })
964
+ musicEnabled && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
965
+ (0, react_jsx_runtime.jsxs)("label", {
966
+ className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
967
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("musicVolume") }), (0, react_jsx_runtime.jsx)("input", {
968
+ type: "range",
969
+ min: 0,
970
+ max: 1,
971
+ step: .05,
972
+ value: musicVolume,
973
+ onChange: (event) => {
974
+ setMusicVolume(Number(event.target.value));
975
+ }
976
+ })]
977
+ }),
978
+ (0, react_jsx_runtime.jsxs)("label", {
979
+ className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
980
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("musicStyle") }), (0, react_jsx_runtime.jsxs)("select", {
981
+ value: musicStyle,
982
+ onChange: (event) => {
983
+ setMusicStyle(event.target.value);
984
+ },
985
+ children: [
986
+ (0, react_jsx_runtime.jsx)("option", {
987
+ value: "major",
988
+ children: t("musicStyleMajor")
989
+ }),
990
+ (0, react_jsx_runtime.jsx)("option", {
991
+ value: "minor",
992
+ children: t("musicStyleMinor")
993
+ }),
994
+ (0, react_jsx_runtime.jsx)("option", {
995
+ value: "pentatonic",
996
+ children: t("musicStylePentatonic")
997
+ })
998
+ ]
999
+ })]
1000
+ }),
1001
+ (0, react_jsx_runtime.jsxs)("label", {
1002
+ className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
1003
+ children: [(0, react_jsx_runtime.jsx)("input", {
1004
+ type: "checkbox",
1005
+ checked: musicOnlyCurrentSession,
1006
+ onChange: (event) => {
1007
+ setMusicOnlyCurrentSession(event.target.checked);
1008
+ }
1009
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("musicOnlyCurrentSessionLabel") })]
1010
+ }),
1011
+ (0, react_jsx_runtime.jsx)("div", {
1012
+ className: TokenQuotaPanel_module_css_default.dialogHint,
1013
+ children: t("musicOnlyCurrentSessionHint")
1014
+ })
1015
+ ] })
994
1016
  ]
995
1017
  }),
996
1018
  (0, react_jsx_runtime.jsxs)("div", {
@@ -1210,16 +1232,19 @@ window.__ModuleLoader__.load({
1210
1232
  //#endregion
1211
1233
  //#region lib/types/client/music.js
1212
1234
  /**
1213
- * Live-composed soundtrack driven by host actions (turns, steps, tool calls,
1214
- * etc.). Outputs through Web Audio (built-in synthesis) by default, and also
1215
- * sends MIDI NoteOn/NoteOff to the first available MIDI output when one is
1216
- * detected (both play simultaneously when MIDI is present — use the panel
1217
- * volume for overall level, the MIDI output has its own gain on the receiver).
1235
+ * Live-composed ambient soundtrack driven by host actions (turns, steps,
1236
+ * tool calls, etc.). Three layers:
1218
1237
  *
1219
- * The composition is intentionally simple: a fixed chord progression under a
1220
- * scale-constrained melody, with one short musical gesture per host action.
1221
- * It is meant as gentle ambience while waiting for LLM responses, not as a
1222
- * full piece — everything stays in key so you can never hit a wrong note.
1238
+ * - **Pad**: a sustained three-note chord (triangle waves) that fades in when
1239
+ * a request starts and crossfades to the next chord on every step.
1240
+ * - **Bass**: one long root note per beat (lower octave), keeps the pulse.
1241
+ * - **Melody**: an eighth-note scale line that dances inside the current
1242
+ * chord — always in key, never a wrong note.
1243
+ *
1244
+ * Outputs through Web Audio (built-in synthesis) by default, and also sends
1245
+ * MIDI NoteOn/NoteOff to the first available MIDI output when one is detected
1246
+ * (both play simultaneously when MIDI is present — use the panel volume for
1247
+ * overall level, the MIDI output has its own gain on the receiver).
1223
1248
  *
1224
1249
  * @module @jxgame2020/dsh-token-quota/music
1225
1250
  */
@@ -1295,14 +1320,26 @@ window.__ModuleLoader__.load({
1295
1320
  "major"
1296
1321
  ]
1297
1322
  };
1298
- /**
1299
- * Turn a MIDI note number into a frequency (Hz) using equal temperament at
1300
- * A4 = 440 Hz.
1301
- */
1323
+ /** Turn a MIDI note number into a frequency (equal temperament, A4 = 440). */
1302
1324
  function midiToFreq(midi) {
1303
1325
  return 440 * Math.pow(2, (midi - 69) / 12);
1304
1326
  }
1305
- /** Live-composed music engine driven by host actions. */
1327
+ /** Interval in semitones from the root for each chord-tone. */
1328
+ function chordIntervals(quality) {
1329
+ return [
1330
+ 0,
1331
+ quality === "major" ? 4 : quality === "minor" ? 3 : 3,
1332
+ quality === "dim" ? 6 : 7
1333
+ ];
1334
+ }
1335
+ /**
1336
+ * Live-composed music engine driven by host actions.
1337
+ *
1338
+ * The engine is idle until `ensureStarted()` is called from a user gesture
1339
+ * (the panel switch), and only produces sound while `enabled` is true AND a
1340
+ * request is in flight (pad + bass + melody are gated by `startPad` /
1341
+ * `stopPad`).
1342
+ */
1306
1343
  var TokenQuotaMusic = class TokenQuotaMusic {
1307
1344
  audio = null;
1308
1345
  midiOutput = null;
@@ -1310,14 +1347,23 @@ window.__ModuleLoader__.load({
1310
1347
  enabled = false;
1311
1348
  volume = .5;
1312
1349
  style = "major";
1313
- /** Current position in the chord progression (advances per step). */
1314
1350
  chordIndex = 0;
1315
- /** Step counter — used for melody degree selection. */
1316
1351
  stepCount = 0;
1317
- /** Tool counter — used for tool-note pitch. */
1318
1352
  toolCount = 0;
1319
- /** Base MIDI note of the current key. C4 = middle C. */
1353
+ padOscs = [];
1354
+ padGain = null;
1355
+ padTarget = 0;
1356
+ padActive = false;
1357
+ beatTimer;
1358
+ beatIndex = 0;
1359
+ melodyDirection = 1;
1360
+ melodyDegree = 0;
1361
+ currentRoot = 60;
1362
+ currentQuality = "major";
1363
+ /** Base MIDI note of the key. C4 = middle C. */
1320
1364
  static BASE_MIDI = 60;
1365
+ /** Beat length in ms — eighth note = beat / 2. */
1366
+ static BEAT_MS = 640;
1321
1367
  /** True once the audio context has been started (needs a user gesture). */
1322
1368
  get started() {
1323
1369
  return this.audio !== null;
@@ -1353,12 +1399,16 @@ window.__ModuleLoader__.load({
1353
1399
  }
1354
1400
  setEnabled(value) {
1355
1401
  this.enabled = value;
1402
+ if (!value) this.stopAll();
1356
1403
  }
1357
1404
  setVolume(value) {
1358
1405
  this.volume = Math.max(0, Math.min(1, value));
1406
+ if (this.padGain !== null && this.audio !== null) this.padGain.gain.setValueAtTime(this.volume * this.padTarget, this.audio.currentTime);
1359
1407
  }
1360
1408
  setStyle(value) {
1361
1409
  this.style = value;
1410
+ this.melodyDegree = 0;
1411
+ this.melodyDirection = 1;
1362
1412
  }
1363
1413
  /** Handle one incoming host action and turn it into sound. */
1364
1414
  onAction(action) {
@@ -1366,82 +1416,177 @@ window.__ModuleLoader__.load({
1366
1416
  const base = TokenQuotaMusic.BASE_MIDI;
1367
1417
  switch (action.type) {
1368
1418
  case "turn/start":
1419
+ this.stopAll();
1369
1420
  this.chordIndex = 0;
1370
1421
  this.stepCount = 0;
1371
1422
  this.toolCount = 0;
1372
- this.playChord(base, this.currentChordQuality(), 1.6, .55);
1423
+ this.currentRoot = base;
1424
+ this.currentQuality = CHORD_QUALITIES[this.style][0] ?? "major";
1425
+ this.playChord(base, this.currentQuality, 1.6, .6);
1373
1426
  break;
1374
1427
  case "step/start": {
1375
1428
  this.chordIndex = (this.chordIndex + 1) % CHORD_PROGRESSIONS[this.style].length;
1376
1429
  this.stepCount += 1;
1377
- this.playNote(this.currentChordRoot(base) - 12, 1.2, .35);
1378
- const root = this.currentChordRoot(base);
1379
- const [d1, d2, d3] = this.arpeggioDegrees();
1380
- setTimeout(() => this.playNote(root + d1, .2, .4), 0);
1381
- setTimeout(() => this.playNote(root + d2, .2, .4), 80);
1382
- setTimeout(() => this.playNote(root + d3, .35, .4), 160);
1430
+ const root = base + (CHORD_PROGRESSIONS[this.style][this.chordIndex] ?? 0);
1431
+ const quality = CHORD_QUALITIES[this.style][this.chordIndex] ?? "major";
1432
+ this.currentRoot = root;
1433
+ this.currentQuality = quality;
1434
+ if (this.padActive) this.crossfadePad(root, quality);
1383
1435
  break;
1384
1436
  }
1385
1437
  case "request/header": {
1438
+ this.startPad(this.currentRoot, this.currentQuality);
1439
+ this.startBeatClock();
1386
1440
  const degree = (this.stepCount * 2 + this.toolCount) % SCALES[this.style].length;
1387
- const note = base + 12 + SCALES[this.style][degree];
1388
- this.playNote(note, .18, .35);
1441
+ const note = base + 12 + (SCALES[this.style][degree] ?? 0);
1442
+ this.playNote(note, .25, .4);
1389
1443
  break;
1390
1444
  }
1391
1445
  case "tool/call":
1392
1446
  this.toolCount += 1;
1393
1447
  this.playNote(base + 24 + this.toolCount % 3 * 2, .12, .45);
1394
- this.playNote(base - 12, .08, .25);
1448
+ this.playNote(base - 12, .08, .3);
1395
1449
  break;
1396
1450
  case "tool/result":
1397
1451
  if (action.error) {
1398
1452
  this.playNote(base + 13, .18, .4);
1399
1453
  setTimeout(() => this.playNote(base + 12, .25, .4), 90);
1400
1454
  } else {
1401
- this.playNote(base + 9, .14, .3);
1402
- setTimeout(() => this.playNote(base + 11, .22, .3), 70);
1455
+ this.playNote(base + 9, .14, .35);
1456
+ setTimeout(() => this.playNote(base + 11, .22, .35), 70);
1403
1457
  }
1404
1458
  break;
1459
+ case "assistant/attempt":
1460
+ this.playNote(base + 8, .18, .4);
1461
+ setTimeout(() => this.playNote(base + 7, .2, .4), 100);
1462
+ setTimeout(() => this.playNote(base + 5, .3, .4), 200);
1463
+ break;
1405
1464
  case "assistant/message":
1406
- if (action.interrupted) this.playChord(base + 7, "dim", .9, .5);
1407
- else this.playChord(this.currentChordRoot(base), this.currentChordQuality(), 1.1, .55);
1465
+ this.stopBeatClock();
1466
+ this.stopPad();
1467
+ if (action.interrupted) this.playChord(base + 7, "dim", .9, .55);
1468
+ else this.playChord(this.currentRoot, this.currentQuality, 1.1, .6);
1408
1469
  break;
1409
1470
  case "turn/end":
1410
- this.playChord(base, this.style === "minor" ? "minor" : "major", 2.2, .65);
1471
+ this.stopAll();
1472
+ this.playChord(base, this.style === "minor" ? "minor" : "major", 2.4, .7);
1411
1473
  this.chordIndex = 0;
1412
1474
  break;
1413
1475
  }
1414
1476
  }
1415
- currentChordRoot(base) {
1416
- const prog = CHORD_PROGRESSIONS[this.style];
1417
- return base + prog[this.chordIndex % prog.length];
1477
+ /** Start the pad from silence, fading in to the target level. */
1478
+ startPad(rootMidi, quality) {
1479
+ if (!this.audio) return;
1480
+ if (this.padActive) return;
1481
+ const now = this.audio.currentTime;
1482
+ const gain = this.audio.createGain();
1483
+ gain.gain.setValueAtTime(0, now);
1484
+ gain.connect(this.audio.destination);
1485
+ const intervals = chordIntervals(quality);
1486
+ const oscs = [];
1487
+ for (const interval of intervals) {
1488
+ const osc = this.audio.createOscillator();
1489
+ osc.type = "triangle";
1490
+ osc.frequency.value = midiToFreq(rootMidi + interval);
1491
+ osc.connect(gain);
1492
+ osc.start(now);
1493
+ oscs.push(osc);
1494
+ }
1495
+ this.padOscs = oscs;
1496
+ this.padGain = gain;
1497
+ this.padTarget = .18;
1498
+ gain.gain.linearRampToValueAtTime(this.volume * this.padTarget, now + .6);
1499
+ this.padActive = true;
1500
+ }
1501
+ /** Smoothly morph the pad to a new root + quality. */
1502
+ crossfadePad(rootMidi, quality) {
1503
+ if (!this.audio || !this.padActive || this.padGain === null) return;
1504
+ const now = this.audio.currentTime;
1505
+ const intervals = chordIntervals(quality);
1506
+ this.padOscs.forEach((osc, i) => {
1507
+ const interval = intervals[i] ?? 0;
1508
+ osc.frequency.setValueAtTime(osc.frequency.value, now);
1509
+ osc.frequency.exponentialRampToValueAtTime(midiToFreq(rootMidi + interval), now + .5);
1510
+ });
1511
+ }
1512
+ /** Fade the pad to silence, then stop the oscillators. */
1513
+ stopPad() {
1514
+ if (!this.audio || !this.padActive || this.padGain === null) return;
1515
+ const now = this.audio.currentTime;
1516
+ const gain = this.padGain;
1517
+ gain.gain.cancelScheduledValues(now);
1518
+ gain.gain.setValueAtTime(gain.gain.value, now);
1519
+ gain.gain.linearRampToValueAtTime(0, now + .8);
1520
+ const oscs = this.padOscs;
1521
+ setTimeout(() => {
1522
+ for (const osc of oscs) {
1523
+ try {
1524
+ osc.stop();
1525
+ } catch {}
1526
+ try {
1527
+ osc.disconnect();
1528
+ } catch {}
1529
+ }
1530
+ try {
1531
+ gain.disconnect();
1532
+ } catch {}
1533
+ }, 850);
1534
+ this.padActive = false;
1535
+ this.padOscs = [];
1536
+ this.padGain = null;
1418
1537
  }
1419
- currentChordQuality() {
1420
- const q = CHORD_QUALITIES[this.style];
1421
- return q[this.chordIndex % q.length] ?? "major";
1538
+ /** Start the per-beat clock that drives bass and melody. */
1539
+ startBeatClock() {
1540
+ if (this.beatTimer !== void 0) return;
1541
+ this.beatIndex = 0;
1542
+ this.melodyDegree = 0;
1543
+ this.melodyDirection = 1;
1544
+ const tick = () => {
1545
+ if (!this.audio || !this.enabled) return;
1546
+ if (this.beatIndex % 2 === 0) this.playNote(this.currentRoot - 12, .45, .3);
1547
+ const scale = SCALES[this.style];
1548
+ const chordTones = new Set(chordIntervals(this.currentQuality));
1549
+ let degree = this.melodyDegree;
1550
+ let next = degree + this.melodyDirection;
1551
+ if (next >= scale.length) {
1552
+ this.melodyDirection = -1;
1553
+ next = scale.length - 2;
1554
+ } else if (next < 0) {
1555
+ this.melodyDirection = 1;
1556
+ next = 0;
1557
+ }
1558
+ this.melodyDegree = next;
1559
+ degree = next;
1560
+ if (this.beatIndex % 2 === 0) {
1561
+ const rootRel = (scale[degree] ?? 0) % 12;
1562
+ if (!chordTones.has(rootRel)) {
1563
+ this.melodyDegree += this.melodyDirection;
1564
+ degree = this.melodyDegree;
1565
+ }
1566
+ }
1567
+ const note = this.currentRoot + 12 + (scale[degree] ?? 0);
1568
+ this.playNote(note, .22, .28);
1569
+ this.beatIndex = (this.beatIndex + 1) % 4;
1570
+ };
1571
+ tick();
1572
+ this.beatTimer = setInterval(tick, TokenQuotaMusic.BEAT_MS);
1573
+ }
1574
+ stopBeatClock() {
1575
+ if (this.beatTimer !== void 0) {
1576
+ clearInterval(this.beatTimer);
1577
+ this.beatTimer = void 0;
1578
+ }
1422
1579
  }
1423
- /** Intervals of an ascending arpeggio from the chord root. */
1424
- arpeggioDegrees() {
1425
- return this.style === "pentatonic" ? [
1426
- 0,
1427
- 7,
1428
- 12
1429
- ] : this.currentChordQuality() === "major" ? [
1430
- 0,
1431
- 4,
1432
- 7
1433
- ] : [
1434
- 0,
1435
- 3,
1436
- 7
1437
- ];
1580
+ stopAll() {
1581
+ this.stopBeatClock();
1582
+ this.stopPad();
1438
1583
  }
1439
1584
  /** Play one note: Web Audio + MIDI simultaneously when available. */
1440
1585
  playNote(midi, durationSec, velocity) {
1441
1586
  if (!this.audio) return;
1442
- const now = this.audio.currentTime;
1443
1587
  const peak = this.volume * velocity * .5;
1444
1588
  if (peak <= 0) return;
1589
+ const now = this.audio.currentTime;
1445
1590
  const osc = this.audio.createOscillator();
1446
1591
  osc.type = "sine";
1447
1592
  osc.frequency.value = midiToFreq(midi);
@@ -1471,12 +1616,9 @@ window.__ModuleLoader__.load({
1471
1616
  }
1472
1617
  /** Play a three-note chord plus a bass octave below. */
1473
1618
  playChord(rootMidi, quality, durationSec, velocity) {
1474
- const third = quality === "major" ? 4 : 3;
1475
- const fifth = quality === "dim" ? 6 : 7;
1476
- this.playNote(rootMidi, durationSec, velocity * .45);
1477
- this.playNote(rootMidi + third, durationSec, velocity * .35);
1478
- this.playNote(rootMidi + fifth, durationSec, velocity * .35);
1479
- this.playNote(rootMidi - 12, durationSec * 1.15, velocity * .45);
1619
+ const intervals = chordIntervals(quality);
1620
+ for (const interval of intervals) this.playNote(rootMidi + interval, durationSec, velocity * .45);
1621
+ this.playNote(rootMidi - 12, durationSec * 1.1, velocity * .5);
1480
1622
  }
1481
1623
  };
1482
1624
  //#endregion
@@ -1556,6 +1698,8 @@ window.__ModuleLoader__.load({
1556
1698
  musicStyleMajor: "大调",
1557
1699
  musicStyleMinor: "小调",
1558
1700
  musicStylePentatonic: "五声",
1701
+ musicOnlyCurrentSessionLabel: "只播当前会话",
1702
+ musicOnlyCurrentSessionHint: "开启后仅当前窗口的会话有音乐,后台子任务静音;关闭则所有会话都参与作曲",
1559
1703
  checkUpdatesNow: "立即检查",
1560
1704
  checkingUpdates: "检查中…",
1561
1705
  checkUpToDate: "已是最新",
@@ -1630,6 +1774,8 @@ window.__ModuleLoader__.load({
1630
1774
  musicStyleMajor: "Major",
1631
1775
  musicStyleMinor: "Minor",
1632
1776
  musicStylePentatonic: "Pentatonic",
1777
+ musicOnlyCurrentSessionLabel: "Current session only",
1778
+ musicOnlyCurrentSessionHint: "When on, only the visible session drives the soundtrack (background tasks stay silent); when off, every session contributes.",
1633
1779
  checkUpdatesNow: "Check now",
1634
1780
  checkingUpdates: "Checking…",
1635
1781
  checkUpToDate: "Up to date",
@@ -1674,6 +1820,7 @@ window.__ModuleLoader__.load({
1674
1820
  let lastMonitored = null;
1675
1821
  let lastOnFull = "stop";
1676
1822
  let lastReset = null;
1823
+ let lastOnlyCurrentSession = true;
1677
1824
  const music = new TokenQuotaMusic();
1678
1825
  const sse = new EventSource("/token-quota/events");
1679
1826
  sse.addEventListener("action", (event) => {
@@ -1683,12 +1830,19 @@ window.__ModuleLoader__.load({
1683
1830
  } catch {
1684
1831
  return;
1685
1832
  }
1686
- if (lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
1833
+ if (lastOnlyCurrentSession && lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
1687
1834
  music.onAction(action);
1688
1835
  });
1689
1836
  ctx.effect(() => () => {
1690
1837
  sse.close();
1691
1838
  }, "token-quota-ui: music SSE");
1839
+ /** Build a writable music-settings clone with defaults applied. */
1840
+ const musicDefaults = (raw) => ({
1841
+ enabled: raw?.enabled ?? false,
1842
+ volume: raw?.volume ?? .5,
1843
+ style: raw?.style ?? "major",
1844
+ onlyCurrentSession: raw?.onlyCurrentSession ?? true
1845
+ });
1692
1846
  const keyOf = (selection) => {
1693
1847
  if (selection === null) return void 0;
1694
1848
  return `${selection.provider}/${selection.model}`;
@@ -1798,18 +1952,15 @@ window.__ModuleLoader__.load({
1798
1952
  bound?.setCheckUpdates(doc?.checkUpdates ?? true);
1799
1953
  bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
1800
1954
  bound?.setReset(lastReset);
1801
- const musicSettings = {
1802
- enabled: false,
1803
- volume: .5,
1804
- style: "major",
1805
- ...doc?.music ?? {}
1806
- };
1955
+ const musicSettings = musicDefaults(doc?.music);
1956
+ lastOnlyCurrentSession = musicSettings.onlyCurrentSession;
1807
1957
  music.setEnabled(musicSettings.enabled);
1808
1958
  music.setVolume(musicSettings.volume);
1809
1959
  music.setStyle(musicSettings.style);
1810
1960
  bound?.setMusicEnabled(musicSettings.enabled);
1811
1961
  bound?.setMusicVolume(musicSettings.volume);
1812
1962
  bound?.setMusicStyle(musicSettings.style);
1963
+ bound?.setMusicOnlyCurrentSession(musicSettings.onlyCurrentSession);
1813
1964
  fetch("/token-quota", { headers: { accept: "application/json" } }).then((response) => {
1814
1965
  if (!response.ok) {
1815
1966
  bound?.setError(`quota route: ${String(response.status)}`);
@@ -1880,36 +2031,30 @@ window.__ModuleLoader__.load({
1880
2031
  scope.set("dimWhenIdle", enabled);
1881
2032
  };
1882
2033
  const setMusicEnabled = (enabled) => {
1883
- const current = {
1884
- enabled: false,
1885
- volume: .5,
1886
- style: "major",
1887
- ...scope.getSnapshot().value?.music ?? {}
1888
- };
2034
+ const doc = scope.getSnapshot().value;
2035
+ const current = musicDefaults(doc?.music);
1889
2036
  current.enabled = enabled;
1890
2037
  scope.set("music", current);
1891
2038
  if (enabled) music.ensureStarted();
1892
2039
  };
1893
2040
  const setMusicVolume = (volume) => {
1894
- const current = {
1895
- enabled: false,
1896
- volume: .5,
1897
- style: "major",
1898
- ...scope.getSnapshot().value?.music ?? {}
1899
- };
2041
+ const doc = scope.getSnapshot().value;
2042
+ const current = musicDefaults(doc?.music);
1900
2043
  current.volume = Math.max(0, Math.min(1, volume));
1901
2044
  scope.set("music", current);
1902
2045
  };
1903
2046
  const setMusicStyle = (style) => {
1904
- const current = {
1905
- enabled: false,
1906
- volume: .5,
1907
- style: "major",
1908
- ...scope.getSnapshot().value?.music ?? {}
1909
- };
2047
+ const doc = scope.getSnapshot().value;
2048
+ const current = musicDefaults(doc?.music);
1910
2049
  current.style = style;
1911
2050
  scope.set("music", current);
1912
2051
  };
2052
+ const setMusicOnlyCurrentSession = (only) => {
2053
+ const doc = scope.getSnapshot().value;
2054
+ const current = musicDefaults(doc?.music);
2055
+ current.onlyCurrentSession = only;
2056
+ scope.set("music", current);
2057
+ };
1913
2058
  /** Ask the Host to check the registry right now, then refresh the snapshot. */
1914
2059
  const checkUpdatesNow = () => {
1915
2060
  bound?.setCheckingUpdates(true);
@@ -1996,7 +2141,8 @@ window.__ModuleLoader__.load({
1996
2141
  setDimWhenIdle,
1997
2142
  setMusicEnabled,
1998
2143
  setMusicVolume,
1999
- setMusicStyle
2144
+ setMusicStyle,
2145
+ setMusicOnlyCurrentSession
2000
2146
  };
2001
2147
  };
2002
2148
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({