@jxgame2020/dsh-token-quota 0.1.24 → 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.24" ? 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
- "dialogBtn": "NPLLIq_dialogBtn",
204
+ "checkBtn": "NPLLIq_checkBtn",
205
+ "body": "NPLLIq_body",
206
+ "dialogLabel": "NPLLIq_dialogLabel",
207
+ "upgradeBanner": "NPLLIq_upgradeBanner",
208
+ "dialogActions": "NPLLIq_dialogActions",
201
209
  "currentBadge": "NPLLIq_currentBadge",
210
+ "dialogBody": "NPLLIq_dialogBody",
211
+ "notice": "NPLLIq_notice",
212
+ "rowMeta": "NPLLIq_rowMeta",
213
+ "title": "NPLLIq_title",
214
+ "logPagerInfo": "NPLLIq_logPagerInfo",
215
+ "headerText": "NPLLIq_headerText",
216
+ "bar": "NPLLIq_bar",
217
+ "noticeError": "NPLLIq_noticeError",
218
+ "tab": "NPLLIq_tab",
219
+ "logScroll": "NPLLIq_logScroll",
220
+ "dialogTitle": "NPLLIq_dialogTitle",
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",
202
228
  "panelDim": "NPLLIq_panelDim",
203
- "row": "NPLLIq_row",
204
- "controls": "NPLLIq_controls",
205
- "logSearch": "NPLLIq_logSearch",
206
- "logPagerControls": "NPLLIq_logPagerControls",
229
+ "logToolbar": "NPLLIq_logToolbar",
230
+ "upgradeCommands": "NPLLIq_upgradeCommands",
231
+ "upgradeHint": "NPLLIq_upgradeHint",
232
+ "logDayCol": "NPLLIq_logDayCol",
233
+ "settingsBtn": "NPLLIq_settingsBtn",
234
+ "tabLabel": "NPLLIq_tabLabel",
235
+ "dialogClose": "NPLLIq_dialogClose",
236
+ "fillOver": "NPLLIq_fillOver",
237
+ "rowHeader": "NPLLIq_rowHeader",
238
+ "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
239
+ "copyBtn": "NPLLIq_copyBtn",
240
+ "upgradeClose": "NPLLIq_upgradeClose",
207
241
  "logModelCol": "NPLLIq_logModelCol",
208
- "logUsedCol": "NPLLIq_logUsedCol",
209
- "logTable": "NPLLIq_logTable",
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",
210
249
  "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
211
- "resetRow": "NPLLIq_resetRow",
212
- "monitorName": "NPLLIq_monitorName",
213
- "resetSelect": "NPLLIq_resetSelect",
214
250
  "settingsBtnActive": "NPLLIq_settingsBtnActive",
215
- "upgradeBannerText": "NPLLIq_upgradeBannerText",
216
- "upgradeClose": "NPLLIq_upgradeClose",
217
- "fullNotice": "NPLLIq_fullNotice",
218
251
  "radioRow": "NPLLIq_radioRow",
252
+ "logClearBtn": "NPLLIq_logClearBtn",
253
+ "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
219
254
  "dialogHint": "NPLLIq_dialogHint",
220
- "rowHeader": "NPLLIq_rowHeader",
221
- "dialogClose": "NPLLIq_dialogClose",
222
- "selectBtn": "NPLLIq_selectBtn",
223
- "upgradeCommands": "NPLLIq_upgradeCommands",
224
- "fillWarn": "NPLLIq_fillWarn",
225
- "upgradeCommand": "NPLLIq_upgradeCommand",
226
- "checkBtn": "NPLLIq_checkBtn",
227
- "dialog": "NPLLIq_dialog",
228
- "logDayCol": "NPLLIq_logDayCol",
255
+ "logClearDialog": "NPLLIq_logClearDialog",
256
+ "monitorName": "NPLLIq_monitorName",
229
257
  "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
230
258
  "logPager": "NPLLIq_logPager",
259
+ "logClearDate": "NPLLIq_logClearDate",
260
+ "rowName": "NPLLIq_rowName",
261
+ "logDialog": "NPLLIq_logDialog",
231
262
  "dialogOverlay": "NPLLIq_dialogOverlay",
232
- "dialogSection": "NPLLIq_dialogSection",
233
- "logClearDialog": "NPLLIq_logClearDialog",
234
- "headerText": "NPLLIq_headerText",
235
- "notice": "NPLLIq_notice",
236
- "tabLabel": "NPLLIq_tabLabel",
237
- "dialogActions": "NPLLIq_dialogActions",
263
+ "logUsedCol": "NPLLIq_logUsedCol",
264
+ "input": "NPLLIq_input",
265
+ "gearBtn": "NPLLIq_gearBtn",
266
+ "row": "NPLLIq_row",
267
+ "monitorHint": "NPLLIq_monitorHint",
268
+ "header": "NPLLIq_header",
269
+ "logPagerPages": "NPLLIq_logPagerPages",
270
+ "versionNew": "NPLLIq_versionNew",
238
271
  "subtitle": "NPLLIq_subtitle",
239
- "logClearDate": "NPLLIq_logClearDate",
240
- "headerActions": "NPLLIq_headerActions",
241
- "dialogTitle": "NPLLIq_dialogTitle",
242
- "logClearBtn": "NPLLIq_logClearBtn",
243
- "dialogBody": "NPLLIq_dialogBody",
244
- "title": "NPLLIq_title",
245
- "upgradeHint": "NPLLIq_upgradeHint",
246
272
  "version": "NPLLIq_version",
247
- "body": "NPLLIq_body",
248
- "logPagerPages": "NPLLIq_logPagerPages",
249
- "monitorList": "NPLLIq_monitorList",
250
- "logHeader": "NPLLIq_logHeader",
251
- "copyBtnDone": "NPLLIq_copyBtnDone",
252
- "tab": "NPLLIq_tab",
253
- "monitorHint": "NPLLIq_monitorHint",
254
- "settingsBtn": "NPLLIq_settingsBtn",
255
- "panelDimStrong": "NPLLIq_panelDimStrong",
256
- "logToolbar": "NPLLIq_logToolbar",
257
- "logPagerInfo": "NPLLIq_logPagerInfo",
258
- "logScroll": "NPLLIq_logScroll",
273
+ "fullNotice": "NPLLIq_fullNotice",
274
+ "resetRow": "NPLLIq_resetRow",
275
+ "logTable": "NPLLIq_logTable",
276
+ "fillIdle": "NPLLIq_fillIdle",
259
277
  "checkErrorHint": "NPLLIq_checkErrorHint",
260
- "rowMeta": "NPLLIq_rowMeta",
261
- "logPagerBtn": "NPLLIq_logPagerBtn",
262
- "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
263
- "bar": "NPLLIq_bar",
264
- "input": "NPLLIq_input",
265
- "panel": "NPLLIq_panel",
266
- "gearBtn": "NPLLIq_gearBtn",
267
278
  "tabIcon": "NPLLIq_tabIcon",
268
- "upgradeBanner": "NPLLIq_upgradeBanner",
269
- "noticeError": "NPLLIq_noticeError",
270
- "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
271
- "copyBtn": "NPLLIq_copyBtn",
272
- "switch": "NPLLIq_switch",
279
+ "logPagerControls": "NPLLIq_logPagerControls",
273
280
  "collapse": "NPLLIq_collapse",
274
- "monitorRow": "NPLLIq_monitorRow",
275
- "dialogLabel": "NPLLIq_dialogLabel",
276
- "fillOver": "NPLLIq_fillOver",
277
- "versionNew": "NPLLIq_versionNew",
278
- "rowName": "NPLLIq_rowName",
279
- "save": "NPLLIq_save",
280
- "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
281
- "fillIdle": "NPLLIq_fillIdle",
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",
282
288
  "dialogHeader": "NPLLIq_dialogHeader",
283
- "logDialog": "NPLLIq_logDialog",
284
- "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
285
- "header": "NPLLIq_header"
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.24",
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.24"]
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,26 +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
- /**
1320
- * Progress layer: while a request is in flight (thinking + streaming) the
1321
- * engine ticks a quiet scale-walking background note every tick so the wait
1322
- * is audible. Cleared on assistant/message, assistant/attempt or turn/end.
1323
- */
1324
- progressTimer;
1325
- /** Direction of the progress walk: +1 up the scale, -1 down. */
1326
- progressDir = 1;
1327
- /** Scale degree of the next progress note. */
1328
- progressDegree = 0;
1329
- /** 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. */
1330
1364
  static BASE_MIDI = 60;
1331
- /** Tick interval of the progress layer (ms). */
1332
- static PROGRESS_TICK_MS = 750;
1365
+ /** Beat length in ms — eighth note = beat / 2. */
1366
+ static BEAT_MS = 640;
1333
1367
  /** True once the audio context has been started (needs a user gesture). */
1334
1368
  get started() {
1335
1369
  return this.audio !== null;
@@ -1365,44 +1399,16 @@ window.__ModuleLoader__.load({
1365
1399
  }
1366
1400
  setEnabled(value) {
1367
1401
  this.enabled = value;
1368
- if (!value) this.stopProgress();
1402
+ if (!value) this.stopAll();
1369
1403
  }
1370
1404
  setVolume(value) {
1371
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);
1372
1407
  }
1373
1408
  setStyle(value) {
1374
1409
  this.style = value;
1375
- }
1376
- /** Start the quiet in-flight background loop (idempotent). */
1377
- startProgress() {
1378
- if (!this.audio || !this.enabled) return;
1379
- if (this.progressTimer !== void 0) return;
1380
- this.progressDegree = 0;
1381
- this.progressDir = 1;
1382
- const tick = () => {
1383
- if (!this.audio || !this.enabled) return;
1384
- const scale = SCALES[this.style];
1385
- const degree = this.progressDegree % scale.length;
1386
- const note = TokenQuotaMusic.BASE_MIDI + 12 + (scale[degree] ?? 0);
1387
- this.playNote(note, .18, .22);
1388
- const next = this.progressDegree + this.progressDir;
1389
- if (next >= scale.length) {
1390
- this.progressDir = -1;
1391
- this.progressDegree = scale.length - 2;
1392
- } else if (next < 0) {
1393
- this.progressDir = 1;
1394
- this.progressDegree = 0;
1395
- } else this.progressDegree = next;
1396
- };
1397
- tick();
1398
- this.progressTimer = setInterval(tick, TokenQuotaMusic.PROGRESS_TICK_MS);
1399
- }
1400
- /** Stop the in-flight background loop. */
1401
- stopProgress() {
1402
- if (this.progressTimer !== void 0) {
1403
- clearInterval(this.progressTimer);
1404
- this.progressTimer = void 0;
1405
- }
1410
+ this.melodyDegree = 0;
1411
+ this.melodyDirection = 1;
1406
1412
  }
1407
1413
  /** Handle one incoming host action and turn it into sound. */
1408
1414
  onAction(action) {
@@ -1410,92 +1416,177 @@ window.__ModuleLoader__.load({
1410
1416
  const base = TokenQuotaMusic.BASE_MIDI;
1411
1417
  switch (action.type) {
1412
1418
  case "turn/start":
1413
- this.stopProgress();
1419
+ this.stopAll();
1414
1420
  this.chordIndex = 0;
1415
1421
  this.stepCount = 0;
1416
1422
  this.toolCount = 0;
1417
- 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);
1418
1426
  break;
1419
1427
  case "step/start": {
1420
1428
  this.chordIndex = (this.chordIndex + 1) % CHORD_PROGRESSIONS[this.style].length;
1421
1429
  this.stepCount += 1;
1422
- this.playNote(this.currentChordRoot(base) - 12, 1.2, .35);
1423
- const root = this.currentChordRoot(base);
1424
- const [d1, d2, d3] = this.arpeggioDegrees();
1425
- setTimeout(() => this.playNote(root + d1, .2, .4), 0);
1426
- setTimeout(() => this.playNote(root + d2, .2, .4), 80);
1427
- 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);
1428
1435
  break;
1429
1436
  }
1430
1437
  case "request/header": {
1431
- this.startProgress();
1438
+ this.startPad(this.currentRoot, this.currentQuality);
1439
+ this.startBeatClock();
1432
1440
  const degree = (this.stepCount * 2 + this.toolCount) % SCALES[this.style].length;
1433
- const note = base + 12 + SCALES[this.style][degree];
1434
- this.playNote(note, .18, .35);
1441
+ const note = base + 12 + (SCALES[this.style][degree] ?? 0);
1442
+ this.playNote(note, .25, .4);
1435
1443
  break;
1436
1444
  }
1437
1445
  case "tool/call":
1438
1446
  this.toolCount += 1;
1439
1447
  this.playNote(base + 24 + this.toolCount % 3 * 2, .12, .45);
1440
- this.playNote(base - 12, .08, .25);
1448
+ this.playNote(base - 12, .08, .3);
1441
1449
  break;
1442
1450
  case "tool/result":
1443
1451
  if (action.error) {
1444
1452
  this.playNote(base + 13, .18, .4);
1445
1453
  setTimeout(() => this.playNote(base + 12, .25, .4), 90);
1446
1454
  } else {
1447
- this.playNote(base + 9, .14, .3);
1448
- setTimeout(() => this.playNote(base + 11, .22, .3), 70);
1455
+ this.playNote(base + 9, .14, .35);
1456
+ setTimeout(() => this.playNote(base + 11, .22, .35), 70);
1449
1457
  }
1450
1458
  break;
1451
1459
  case "assistant/attempt":
1452
- this.stopProgress();
1453
1460
  this.playNote(base + 8, .18, .4);
1454
1461
  setTimeout(() => this.playNote(base + 7, .2, .4), 100);
1455
- setTimeout(() => this.playNote(base + 5, .25, .4), 200);
1462
+ setTimeout(() => this.playNote(base + 5, .3, .4), 200);
1456
1463
  break;
1457
1464
  case "assistant/message":
1458
- this.stopProgress();
1459
- if (action.interrupted) this.playChord(base + 7, "dim", .9, .5);
1460
- 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);
1461
1469
  break;
1462
1470
  case "turn/end":
1463
- this.stopProgress();
1464
- 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);
1465
1473
  this.chordIndex = 0;
1466
1474
  break;
1467
1475
  }
1468
1476
  }
1469
- currentChordRoot(base) {
1470
- const prog = CHORD_PROGRESSIONS[this.style];
1471
- 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;
1537
+ }
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);
1472
1573
  }
1473
- currentChordQuality() {
1474
- const q = CHORD_QUALITIES[this.style];
1475
- return q[this.chordIndex % q.length] ?? "major";
1574
+ stopBeatClock() {
1575
+ if (this.beatTimer !== void 0) {
1576
+ clearInterval(this.beatTimer);
1577
+ this.beatTimer = void 0;
1578
+ }
1476
1579
  }
1477
- /** Intervals of an ascending arpeggio from the chord root. */
1478
- arpeggioDegrees() {
1479
- return this.style === "pentatonic" ? [
1480
- 0,
1481
- 7,
1482
- 12
1483
- ] : this.currentChordQuality() === "major" ? [
1484
- 0,
1485
- 4,
1486
- 7
1487
- ] : [
1488
- 0,
1489
- 3,
1490
- 7
1491
- ];
1580
+ stopAll() {
1581
+ this.stopBeatClock();
1582
+ this.stopPad();
1492
1583
  }
1493
1584
  /** Play one note: Web Audio + MIDI simultaneously when available. */
1494
1585
  playNote(midi, durationSec, velocity) {
1495
1586
  if (!this.audio) return;
1496
- const now = this.audio.currentTime;
1497
1587
  const peak = this.volume * velocity * .5;
1498
1588
  if (peak <= 0) return;
1589
+ const now = this.audio.currentTime;
1499
1590
  const osc = this.audio.createOscillator();
1500
1591
  osc.type = "sine";
1501
1592
  osc.frequency.value = midiToFreq(midi);
@@ -1525,12 +1616,9 @@ window.__ModuleLoader__.load({
1525
1616
  }
1526
1617
  /** Play a three-note chord plus a bass octave below. */
1527
1618
  playChord(rootMidi, quality, durationSec, velocity) {
1528
- const third = quality === "major" ? 4 : 3;
1529
- const fifth = quality === "dim" ? 6 : 7;
1530
- this.playNote(rootMidi, durationSec, velocity * .45);
1531
- this.playNote(rootMidi + third, durationSec, velocity * .35);
1532
- this.playNote(rootMidi + fifth, durationSec, velocity * .35);
1533
- 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);
1534
1622
  }
1535
1623
  };
1536
1624
  //#endregion
@@ -1610,6 +1698,8 @@ window.__ModuleLoader__.load({
1610
1698
  musicStyleMajor: "大调",
1611
1699
  musicStyleMinor: "小调",
1612
1700
  musicStylePentatonic: "五声",
1701
+ musicOnlyCurrentSessionLabel: "只播当前会话",
1702
+ musicOnlyCurrentSessionHint: "开启后仅当前窗口的会话有音乐,后台子任务静音;关闭则所有会话都参与作曲",
1613
1703
  checkUpdatesNow: "立即检查",
1614
1704
  checkingUpdates: "检查中…",
1615
1705
  checkUpToDate: "已是最新",
@@ -1684,6 +1774,8 @@ window.__ModuleLoader__.load({
1684
1774
  musicStyleMajor: "Major",
1685
1775
  musicStyleMinor: "Minor",
1686
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.",
1687
1779
  checkUpdatesNow: "Check now",
1688
1780
  checkingUpdates: "Checking…",
1689
1781
  checkUpToDate: "Up to date",
@@ -1728,6 +1820,7 @@ window.__ModuleLoader__.load({
1728
1820
  let lastMonitored = null;
1729
1821
  let lastOnFull = "stop";
1730
1822
  let lastReset = null;
1823
+ let lastOnlyCurrentSession = true;
1731
1824
  const music = new TokenQuotaMusic();
1732
1825
  const sse = new EventSource("/token-quota/events");
1733
1826
  sse.addEventListener("action", (event) => {
@@ -1737,12 +1830,19 @@ window.__ModuleLoader__.load({
1737
1830
  } catch {
1738
1831
  return;
1739
1832
  }
1740
- if (lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
1833
+ if (lastOnlyCurrentSession && lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
1741
1834
  music.onAction(action);
1742
1835
  });
1743
1836
  ctx.effect(() => () => {
1744
1837
  sse.close();
1745
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
+ });
1746
1846
  const keyOf = (selection) => {
1747
1847
  if (selection === null) return void 0;
1748
1848
  return `${selection.provider}/${selection.model}`;
@@ -1852,18 +1952,15 @@ window.__ModuleLoader__.load({
1852
1952
  bound?.setCheckUpdates(doc?.checkUpdates ?? true);
1853
1953
  bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
1854
1954
  bound?.setReset(lastReset);
1855
- const musicSettings = {
1856
- enabled: false,
1857
- volume: .5,
1858
- style: "major",
1859
- ...doc?.music ?? {}
1860
- };
1955
+ const musicSettings = musicDefaults(doc?.music);
1956
+ lastOnlyCurrentSession = musicSettings.onlyCurrentSession;
1861
1957
  music.setEnabled(musicSettings.enabled);
1862
1958
  music.setVolume(musicSettings.volume);
1863
1959
  music.setStyle(musicSettings.style);
1864
1960
  bound?.setMusicEnabled(musicSettings.enabled);
1865
1961
  bound?.setMusicVolume(musicSettings.volume);
1866
1962
  bound?.setMusicStyle(musicSettings.style);
1963
+ bound?.setMusicOnlyCurrentSession(musicSettings.onlyCurrentSession);
1867
1964
  fetch("/token-quota", { headers: { accept: "application/json" } }).then((response) => {
1868
1965
  if (!response.ok) {
1869
1966
  bound?.setError(`quota route: ${String(response.status)}`);
@@ -1934,36 +2031,30 @@ window.__ModuleLoader__.load({
1934
2031
  scope.set("dimWhenIdle", enabled);
1935
2032
  };
1936
2033
  const setMusicEnabled = (enabled) => {
1937
- const current = {
1938
- enabled: false,
1939
- volume: .5,
1940
- style: "major",
1941
- ...scope.getSnapshot().value?.music ?? {}
1942
- };
2034
+ const doc = scope.getSnapshot().value;
2035
+ const current = musicDefaults(doc?.music);
1943
2036
  current.enabled = enabled;
1944
2037
  scope.set("music", current);
1945
2038
  if (enabled) music.ensureStarted();
1946
2039
  };
1947
2040
  const setMusicVolume = (volume) => {
1948
- const current = {
1949
- enabled: false,
1950
- volume: .5,
1951
- style: "major",
1952
- ...scope.getSnapshot().value?.music ?? {}
1953
- };
2041
+ const doc = scope.getSnapshot().value;
2042
+ const current = musicDefaults(doc?.music);
1954
2043
  current.volume = Math.max(0, Math.min(1, volume));
1955
2044
  scope.set("music", current);
1956
2045
  };
1957
2046
  const setMusicStyle = (style) => {
1958
- const current = {
1959
- enabled: false,
1960
- volume: .5,
1961
- style: "major",
1962
- ...scope.getSnapshot().value?.music ?? {}
1963
- };
2047
+ const doc = scope.getSnapshot().value;
2048
+ const current = musicDefaults(doc?.music);
1964
2049
  current.style = style;
1965
2050
  scope.set("music", current);
1966
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
+ };
1967
2058
  /** Ask the Host to check the registry right now, then refresh the snapshot. */
1968
2059
  const checkUpdatesNow = () => {
1969
2060
  bound?.setCheckingUpdates(true);
@@ -2050,7 +2141,8 @@ window.__ModuleLoader__.load({
2050
2141
  setDimWhenIdle,
2051
2142
  setMusicEnabled,
2052
2143
  setMusicVolume,
2053
- setMusicStyle
2144
+ setMusicStyle,
2145
+ setMusicOnlyCurrentSession
2054
2146
  };
2055
2147
  };
2056
2148
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({