@jxgame2020/dsh-token-quota 0.1.24 → 0.1.27

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
@@ -39,7 +39,8 @@ window.__ModuleLoader__.load({
39
39
  dimWhenIdle: false,
40
40
  musicEnabled: false,
41
41
  musicVolume: .5,
42
- musicStyle: "major",
42
+ musicStyle: "pentatonic",
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.27" ? 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",
201
- "currentBadge": "NPLLIq_currentBadge",
202
- "panelDim": "NPLLIq_panelDim",
203
- "row": "NPLLIq_row",
204
- "controls": "NPLLIq_controls",
205
- "logSearch": "NPLLIq_logSearch",
206
- "logPagerControls": "NPLLIq_logPagerControls",
207
- "logModelCol": "NPLLIq_logModelCol",
208
- "logUsedCol": "NPLLIq_logUsedCol",
204
+ "bar": "NPLLIq_bar",
205
+ "tab": "NPLLIq_tab",
206
+ "rowHeader": "NPLLIq_rowHeader",
209
207
  "logTable": "NPLLIq_logTable",
210
- "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
211
- "resetRow": "NPLLIq_resetRow",
208
+ "logPagerInfo": "NPLLIq_logPagerInfo",
209
+ "copyBtn": "NPLLIq_copyBtn",
210
+ "dialogBody": "NPLLIq_dialogBody",
211
+ "logClearDialog": "NPLLIq_logClearDialog",
212
+ "input": "NPLLIq_input",
213
+ "monitorHint": "NPLLIq_monitorHint",
212
214
  "monitorName": "NPLLIq_monitorName",
213
- "resetSelect": "NPLLIq_resetSelect",
214
- "settingsBtnActive": "NPLLIq_settingsBtnActive",
215
- "upgradeBannerText": "NPLLIq_upgradeBannerText",
216
- "upgradeClose": "NPLLIq_upgradeClose",
217
- "fullNotice": "NPLLIq_fullNotice",
218
- "radioRow": "NPLLIq_radioRow",
219
- "dialogHint": "NPLLIq_dialogHint",
220
- "rowHeader": "NPLLIq_rowHeader",
221
- "dialogClose": "NPLLIq_dialogClose",
222
- "selectBtn": "NPLLIq_selectBtn",
223
- "upgradeCommands": "NPLLIq_upgradeCommands",
215
+ "checkErrorHint": "NPLLIq_checkErrorHint",
224
216
  "fillWarn": "NPLLIq_fillWarn",
225
- "upgradeCommand": "NPLLIq_upgradeCommand",
226
- "checkBtn": "NPLLIq_checkBtn",
227
- "dialog": "NPLLIq_dialog",
228
- "logDayCol": "NPLLIq_logDayCol",
229
- "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
230
- "logPager": "NPLLIq_logPager",
231
- "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",
217
+ "dialogBtn": "NPLLIq_dialogBtn",
218
+ "logModelCol": "NPLLIq_logModelCol",
219
+ "fullNotice": "NPLLIq_fullNotice",
220
+ "logPagerPages": "NPLLIq_logPagerPages",
238
221
  "subtitle": "NPLLIq_subtitle",
222
+ "upgradeClose": "NPLLIq_upgradeClose",
239
223
  "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
- "version": "NPLLIq_version",
247
- "body": "NPLLIq_body",
248
- "logPagerPages": "NPLLIq_logPagerPages",
249
- "monitorList": "NPLLIq_monitorList",
224
+ "row": "NPLLIq_row",
250
225
  "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",
259
- "checkErrorHint": "NPLLIq_checkErrorHint",
226
+ "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
227
+ "dialog": "NPLLIq_dialog",
228
+ "upgradeHint": "NPLLIq_upgradeHint",
229
+ "dialogHeader": "NPLLIq_dialogHeader",
260
230
  "rowMeta": "NPLLIq_rowMeta",
231
+ "upgradeBannerText": "NPLLIq_upgradeBannerText",
232
+ "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
233
+ "dialogLabel": "NPLLIq_dialogLabel",
261
234
  "logPagerBtn": "NPLLIq_logPagerBtn",
262
- "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
263
- "bar": "NPLLIq_bar",
264
- "input": "NPLLIq_input",
235
+ "logClearBtn": "NPLLIq_logClearBtn",
236
+ "noticeError": "NPLLIq_noticeError",
237
+ "logUsedCol": "NPLLIq_logUsedCol",
238
+ "panelDim": "NPLLIq_panelDim",
239
+ "body": "NPLLIq_body",
240
+ "dialogClose": "NPLLIq_dialogClose",
241
+ "selectBtn": "NPLLIq_selectBtn",
242
+ "dialogOverlay": "NPLLIq_dialogOverlay",
243
+ "header": "NPLLIq_header",
244
+ "dialogActions": "NPLLIq_dialogActions",
245
+ "logDayCol": "NPLLIq_logDayCol",
246
+ "dialogTitle": "NPLLIq_dialogTitle",
247
+ "headerText": "NPLLIq_headerText",
248
+ "collapse": "NPLLIq_collapse",
249
+ "currentBadge": "NPLLIq_currentBadge",
265
250
  "panel": "NPLLIq_panel",
251
+ "fillOver": "NPLLIq_fillOver",
252
+ "settingsBtn": "NPLLIq_settingsBtn",
266
253
  "gearBtn": "NPLLIq_gearBtn",
267
- "tabIcon": "NPLLIq_tabIcon",
268
- "upgradeBanner": "NPLLIq_upgradeBanner",
269
- "noticeError": "NPLLIq_noticeError",
254
+ "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
255
+ "dialogHint": "NPLLIq_dialogHint",
256
+ "versionNew": "NPLLIq_versionNew",
257
+ "save": "NPLLIq_save",
270
258
  "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
271
- "copyBtn": "NPLLIq_copyBtn",
272
259
  "switch": "NPLLIq_switch",
273
- "collapse": "NPLLIq_collapse",
260
+ "logToolbar": "NPLLIq_logToolbar",
261
+ "tabIcon": "NPLLIq_tabIcon",
262
+ "radioRow": "NPLLIq_radioRow",
263
+ "settingsBtnActive": "NPLLIq_settingsBtnActive",
264
+ "copyBtnDone": "NPLLIq_copyBtnDone",
265
+ "headerActions": "NPLLIq_headerActions",
266
+ "dialogSection": "NPLLIq_dialogSection",
267
+ "upgradeBanner": "NPLLIq_upgradeBanner",
268
+ "logScroll": "NPLLIq_logScroll",
269
+ "resetSelect": "NPLLIq_resetSelect",
270
+ "logSearch": "NPLLIq_logSearch",
271
+ "checkBtn": "NPLLIq_checkBtn",
272
+ "panelDimStrong": "NPLLIq_panelDimStrong",
274
273
  "monitorRow": "NPLLIq_monitorRow",
275
- "dialogLabel": "NPLLIq_dialogLabel",
276
- "fillOver": "NPLLIq_fillOver",
277
- "versionNew": "NPLLIq_versionNew",
274
+ "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
275
+ "upgradeCommands": "NPLLIq_upgradeCommands",
276
+ "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
277
+ "logPager": "NPLLIq_logPager",
278
+ "logPagerControls": "NPLLIq_logPagerControls",
279
+ "logDialog": "NPLLIq_logDialog",
278
280
  "rowName": "NPLLIq_rowName",
279
- "save": "NPLLIq_save",
280
- "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
281
+ "monitorList": "NPLLIq_monitorList",
282
+ "resetRow": "NPLLIq_resetRow",
283
+ "title": "NPLLIq_title",
284
+ "version": "NPLLIq_version",
285
+ "tabLabel": "NPLLIq_tabLabel",
286
+ "notice": "NPLLIq_notice",
287
+ "upgradeCommand": "NPLLIq_upgradeCommand",
281
288
  "fillIdle": "NPLLIq_fillIdle",
282
- "dialogHeader": "NPLLIq_dialogHeader",
283
- "logDialog": "NPLLIq_logDialog",
284
- "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
285
- "header": "NPLLIq_header"
289
+ "controls": "NPLLIq_controls"
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.27",
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.27"]
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,20 +1232,28 @@ 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 soundtrack driven by host actions.
1218
1236
  *
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.
1237
+ * Design: when a request starts the engine composes a *piece* — a guzheng-like
1238
+ * plucked-string voice over a soft open-fifth drone (Web Audio synthesis; the
1239
+ * MIDI output is switched to the Koto program so hardware synths match). The
1240
+ * piece plays phrase after phrase so it never goes quiet while the model
1241
+ * thinks or streams. Incoming events steer the piece instead of interrupting
1242
+ * it: a step change glides the drone to the next mode centre and inserts a
1243
+ * fast 过门 (fill); tool calls get a high plink; the ending gets a flute-like
1244
+ * breath note and a descending glissando.
1245
+ *
1246
+ * Anti-runaway guards (the "endless drone" / hanging-MIDI-note problem):
1247
+ * - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
1248
+ * engine fades out (a dead stream means no stop event will ever come),
1249
+ * - every MIDI NoteOn is registered and swept; `stopAll` also sends
1250
+ * All Notes Off (CC 123),
1251
+ * - Web Audio ramps never target zero exponentially and every oscillator is
1252
+ * explicitly stopped.
1223
1253
  *
1224
1254
  * @module @jxgame2020/dsh-token-quota/music
1225
1255
  */
1226
- /** Scale degrees in semitones from the root. */
1256
+ /** Scale degrees in semitones from the (current) tonic. */
1227
1257
  const SCALES = {
1228
1258
  major: [
1229
1259
  0,
@@ -1252,10 +1282,11 @@ window.__ModuleLoader__.load({
1252
1282
  ]
1253
1283
  };
1254
1284
  /**
1255
- * Chord progression roots in semitones from the key centre. Loops — every
1256
- * step advance moves one position forward.
1285
+ * Tonic offsets for the progression — the piece rotates its mode centre on
1286
+ * every step advance. For pentatonic this is 宫 → 羽 → 徵 → 商 (mode
1287
+ * rotation on one scale), which is the classic gufeng colour.
1257
1288
  */
1258
- const CHORD_PROGRESSIONS = {
1289
+ const PROGRESSION = {
1259
1290
  major: [
1260
1291
  0,
1261
1292
  9,
@@ -1264,80 +1295,221 @@ window.__ModuleLoader__.load({
1264
1295
  ],
1265
1296
  minor: [
1266
1297
  0,
1267
- 9,
1268
- 4,
1269
- 11
1298
+ 8,
1299
+ 3,
1300
+ 10
1270
1301
  ],
1271
1302
  pentatonic: [
1272
1303
  0,
1304
+ 9,
1273
1305
  7,
1274
- 2,
1275
- 9
1306
+ 5
1276
1307
  ]
1277
1308
  };
1278
- const CHORD_QUALITIES = {
1279
- major: [
1280
- "major",
1281
- "minor",
1282
- "major",
1283
- "major"
1309
+ /**
1310
+ * Phrase bank — pentatonic-degree melodies that also read fine on 7-note
1311
+ * scales. Degrees index the current scale (values ≥ scale length wrap up an
1312
+ * octave).
1313
+ */
1314
+ const PHRASES = [
1315
+ [
1316
+ {
1317
+ deg: 0,
1318
+ beats: 2
1319
+ },
1320
+ {
1321
+ deg: 1,
1322
+ beats: 1
1323
+ },
1324
+ {
1325
+ deg: 2,
1326
+ beats: 1
1327
+ },
1328
+ {
1329
+ deg: 3,
1330
+ beats: 2
1331
+ },
1332
+ {
1333
+ deg: 2,
1334
+ beats: 1
1335
+ },
1336
+ {
1337
+ deg: 1,
1338
+ beats: 1
1339
+ },
1340
+ {
1341
+ deg: 0,
1342
+ beats: 3
1343
+ }
1284
1344
  ],
1285
- minor: [
1286
- "minor",
1287
- "major",
1288
- "major",
1289
- "major"
1345
+ [
1346
+ {
1347
+ deg: 5,
1348
+ beats: 1
1349
+ },
1350
+ {
1351
+ deg: 4,
1352
+ beats: 1
1353
+ },
1354
+ {
1355
+ deg: 3,
1356
+ beats: 2
1357
+ },
1358
+ {
1359
+ deg: 2,
1360
+ beats: 1
1361
+ },
1362
+ {
1363
+ deg: 1,
1364
+ beats: 2
1365
+ },
1366
+ {
1367
+ deg: 0,
1368
+ beats: 3
1369
+ }
1290
1370
  ],
1291
- pentatonic: [
1292
- "major",
1293
- "major",
1294
- "major",
1295
- "major"
1371
+ [
1372
+ {
1373
+ deg: 2,
1374
+ beats: 1
1375
+ },
1376
+ {
1377
+ deg: 3,
1378
+ beats: 1
1379
+ },
1380
+ {
1381
+ deg: 4,
1382
+ beats: 2
1383
+ },
1384
+ {
1385
+ deg: 5,
1386
+ beats: 1
1387
+ },
1388
+ {
1389
+ deg: 4,
1390
+ beats: 1
1391
+ },
1392
+ {
1393
+ deg: 3,
1394
+ beats: 2
1395
+ },
1396
+ {
1397
+ deg: 1,
1398
+ beats: 2
1399
+ },
1400
+ {
1401
+ deg: 0,
1402
+ beats: 2
1403
+ }
1404
+ ],
1405
+ [
1406
+ {
1407
+ deg: 4,
1408
+ beats: 2
1409
+ },
1410
+ {
1411
+ deg: 3,
1412
+ beats: 1
1413
+ },
1414
+ {
1415
+ deg: 2,
1416
+ beats: 1
1417
+ },
1418
+ {
1419
+ deg: 3,
1420
+ beats: 2
1421
+ },
1422
+ {
1423
+ deg: 1,
1424
+ beats: 1
1425
+ },
1426
+ {
1427
+ deg: 2,
1428
+ beats: 1
1429
+ },
1430
+ {
1431
+ deg: 0,
1432
+ beats: 4
1433
+ }
1296
1434
  ]
1297
- };
1298
- /**
1299
- * Turn a MIDI note number into a frequency (Hz) using equal temperament at
1300
- * A4 = 440 Hz.
1301
- */
1435
+ ];
1436
+ /** 过门 — a fast run played when a step (new phase) starts. */
1437
+ const FILL = [
1438
+ {
1439
+ deg: 0,
1440
+ beats: .5
1441
+ },
1442
+ {
1443
+ deg: 1,
1444
+ beats: .5
1445
+ },
1446
+ {
1447
+ deg: 2,
1448
+ beats: .5
1449
+ },
1450
+ {
1451
+ deg: 3,
1452
+ beats: .5
1453
+ },
1454
+ {
1455
+ deg: 4,
1456
+ beats: .5
1457
+ },
1458
+ {
1459
+ deg: 5,
1460
+ beats: .5
1461
+ },
1462
+ {
1463
+ deg: 4,
1464
+ beats: .5
1465
+ },
1466
+ {
1467
+ deg: 3,
1468
+ beats: .5
1469
+ }
1470
+ ];
1471
+ /** Turn a MIDI note number into a frequency (equal temperament, A4 = 440). */
1302
1472
  function midiToFreq(midi) {
1303
1473
  return 440 * Math.pow(2, (midi - 69) / 12);
1304
1474
  }
1305
- /** Live-composed music engine driven by host actions. */
1475
+ /** General-MIDI program closest to a guzheng: 108 Koto (0-indexed 106). */
1476
+ const GM_PROGRAM_KOTO = 106;
1477
+ /**
1478
+ * Live-composed gufeng engine. Idle until `ensureStarted()` runs inside a
1479
+ * user gesture; silent while `enabled` is false; the piece runs only between
1480
+ * `request/header` (start) and `assistant/message` / `turn/end` (stop).
1481
+ */
1306
1482
  var TokenQuotaMusic = class TokenQuotaMusic {
1307
1483
  audio = null;
1308
1484
  midiOutput = null;
1309
1485
  midiDetected = false;
1310
1486
  enabled = false;
1311
1487
  volume = .5;
1312
- style = "major";
1313
- /** Current position in the chord progression (advances per step). */
1314
- chordIndex = 0;
1315
- /** Step counter — used for melody degree selection. */
1316
- stepCount = 0;
1317
- /** Tool counter — used for tool-note pitch. */
1318
- 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. */
1488
+ style = "pentatonic";
1489
+ playing = false;
1490
+ tonicIndex = 0;
1491
+ phraseIndex = 0;
1492
+ phraseTimer;
1493
+ droneOscs = [];
1494
+ droneGain = null;
1495
+ droneFilter = null;
1496
+ droneActive = false;
1497
+ lastStreamActivity = 0;
1498
+ watchdogTimer;
1499
+ activeMidiNotes = /* @__PURE__ */ new Set();
1500
+ /** Base MIDI note of the tonic. C4 = middle C. */
1330
1501
  static BASE_MIDI = 60;
1331
- /** Tick interval of the progress layer (ms). */
1332
- static PROGRESS_TICK_MS = 750;
1502
+ /** Beat length in ms — the phrase rhythm is measured in these. */
1503
+ static BEAT_MS = 480;
1504
+ /** Stop everything if the SSE stream goes silent this long. */
1505
+ static STREAM_TIMEOUT_MS = 12e3;
1333
1506
  /** True once the audio context has been started (needs a user gesture). */
1334
1507
  get started() {
1335
1508
  return this.audio !== null;
1336
1509
  }
1337
1510
  /**
1338
- * Start the audio context and try to find a MIDI output.
1339
- * Must be called inside a user gesture (the panel switch click) so the
1340
- * AudioContext can resume. Safe to call multiple times.
1511
+ * Start the audio context and probe for a MIDI output (once). Must be
1512
+ * called inside a user gesture so the AudioContext can resume.
1341
1513
  */
1342
1514
  async ensureStarted() {
1343
1515
  if (this.audio !== null) return true;
@@ -1357,15 +1529,24 @@ window.__ModuleLoader__.load({
1357
1529
  this.midiOutput = output;
1358
1530
  break;
1359
1531
  }
1532
+ try {
1533
+ this.midiOutput?.send([192, GM_PROGRAM_KOTO]);
1534
+ } catch {}
1360
1535
  } catch {
1361
1536
  this.midiOutput = null;
1362
1537
  }
1363
1538
  }
1539
+ if (this.watchdogTimer === void 0) {
1540
+ this.lastStreamActivity = Date.now();
1541
+ this.watchdogTimer = setInterval(() => {
1542
+ if (this.playing && Date.now() - this.lastStreamActivity > TokenQuotaMusic.STREAM_TIMEOUT_MS) this.stopPiece(false);
1543
+ }, 4e3);
1544
+ }
1364
1545
  return true;
1365
1546
  }
1366
1547
  setEnabled(value) {
1367
1548
  this.enabled = value;
1368
- if (!value) this.stopProgress();
1549
+ if (!value) this.stopAll();
1369
1550
  }
1370
1551
  setVolume(value) {
1371
1552
  this.volume = Math.max(0, Math.min(1, value));
@@ -1373,164 +1554,324 @@ window.__ModuleLoader__.load({
1373
1554
  setStyle(value) {
1374
1555
  this.style = value;
1375
1556
  }
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);
1557
+ /** Feed the engine one SSE activity timestamp (action or heartbeat). */
1558
+ markStreamActivity() {
1559
+ this.lastStreamActivity = Date.now();
1399
1560
  }
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
- }
1406
- }
1407
- /** Handle one incoming host action and turn it into sound. */
1561
+ /** Handle one incoming host action and steer the piece. */
1408
1562
  onAction(action) {
1409
1563
  if (!this.enabled || this.audio === null) return;
1410
1564
  const base = TokenQuotaMusic.BASE_MIDI;
1411
1565
  switch (action.type) {
1412
1566
  case "turn/start":
1413
- this.stopProgress();
1414
- this.chordIndex = 0;
1415
- this.stepCount = 0;
1416
- this.toolCount = 0;
1417
- this.playChord(base, this.currentChordQuality(), 1.6, .55);
1567
+ this.stopPiece(false);
1568
+ this.tonicIndex = 0;
1569
+ this.phraseIndex = 0;
1418
1570
  break;
1419
- case "step/start": {
1420
- this.chordIndex = (this.chordIndex + 1) % CHORD_PROGRESSIONS[this.style].length;
1421
- 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);
1571
+ case "request/header":
1572
+ if (!this.playing) this.startPiece();
1573
+ else this.pluck(base + 24, .3);
1428
1574
  break;
1429
- }
1430
- case "request/header": {
1431
- this.startProgress();
1432
- 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);
1575
+ case "step/start": {
1576
+ const prog = PROGRESSION[this.style];
1577
+ this.tonicIndex = (this.tonicIndex + 1) % prog.length;
1578
+ const newRoot = base + (prog[this.tonicIndex] ?? 0);
1579
+ this.glideDrone(newRoot);
1580
+ if (this.playing) this.insertFill(newRoot);
1435
1581
  break;
1436
1582
  }
1437
1583
  case "tool/call":
1438
- this.toolCount += 1;
1439
- this.playNote(base + 24 + this.toolCount % 3 * 2, .12, .45);
1440
- this.playNote(base - 12, .08, .25);
1584
+ this.pluck(base + 26, .45, .5);
1585
+ this.pluck(base - 12, .3, .35);
1441
1586
  break;
1442
1587
  case "tool/result":
1443
1588
  if (action.error) {
1444
- this.playNote(base + 13, .18, .4);
1445
- setTimeout(() => this.playNote(base + 12, .25, .4), 90);
1589
+ this.pluck(base + 13, .4, .6);
1590
+ setTimeout(() => this.pluck(base + 12, .4, .9), 90);
1446
1591
  } else {
1447
- this.playNote(base + 9, .14, .3);
1448
- setTimeout(() => this.playNote(base + 11, .22, .3), 70);
1592
+ this.pluck(base + 9, .35, .6);
1593
+ setTimeout(() => this.pluck(base + 11, .35, .9), 70);
1449
1594
  }
1450
1595
  break;
1451
1596
  case "assistant/attempt":
1452
- this.stopProgress();
1453
- this.playNote(base + 8, .18, .4);
1454
- setTimeout(() => this.playNote(base + 7, .2, .4), 100);
1455
- setTimeout(() => this.playNote(base + 5, .25, .4), 200);
1597
+ this.pluck(base + 8, .4, .8);
1598
+ setTimeout(() => this.pluck(base + 7, .4, .8), 110);
1599
+ setTimeout(() => this.pluck(base + 5, .4, .8), 220);
1456
1600
  break;
1457
1601
  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);
1602
+ this.stopMelody();
1603
+ this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
1461
1604
  break;
1462
1605
  case "turn/end":
1463
- this.stopProgress();
1464
- this.playChord(base, this.style === "minor" ? "minor" : "major", 2.2, .65);
1465
- this.chordIndex = 0;
1606
+ this.stopPiece(true);
1466
1607
  break;
1467
1608
  }
1468
1609
  }
1469
- currentChordRoot(base) {
1470
- const prog = CHORD_PROGRESSIONS[this.style];
1471
- return base + prog[this.chordIndex % prog.length];
1610
+ /** Start a new piece: drone + ascending glissando + phrase chain. */
1611
+ startPiece() {
1612
+ if (!this.audio || this.playing) return;
1613
+ this.playing = true;
1614
+ this.tonicIndex = 0;
1615
+ this.phraseIndex = 0;
1616
+ const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][0] ?? 0);
1617
+ this.startDrone(root);
1618
+ const scale = SCALES[this.style];
1619
+ let t = 0;
1620
+ for (let i = 0; i < 8; i++) {
1621
+ const deg = i % scale.length;
1622
+ const note = root + 12 * Math.floor(i / scale.length) + (scale[deg] ?? 0);
1623
+ setTimeout(() => this.pluck(note, .3), t);
1624
+ t += 150;
1625
+ }
1626
+ this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
1627
+ }
1628
+ /** Play one phrase, then chain into the next after a one-beat rest. */
1629
+ playPhrase() {
1630
+ if (!this.playing || !this.audio) return;
1631
+ const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
1632
+ const scale = SCALES[this.style];
1633
+ const phrase = PHRASES[this.phraseIndex % PHRASES.length];
1634
+ this.phraseIndex += 1;
1635
+ let t = 0;
1636
+ for (const step of phrase) {
1637
+ const note = root + (scale[step.deg % scale.length] ?? 0) + 12 * Math.floor(step.deg / scale.length);
1638
+ setTimeout(() => {
1639
+ if (this.playing) this.pluck(note, .34);
1640
+ }, t);
1641
+ t += step.beats * TokenQuotaMusic.BEAT_MS;
1642
+ }
1643
+ this.phraseTimer = setTimeout(() => this.playPhrase(), t + TokenQuotaMusic.BEAT_MS);
1472
1644
  }
1473
- currentChordQuality() {
1474
- const q = CHORD_QUALITIES[this.style];
1475
- return q[this.chordIndex % q.length] ?? "major";
1645
+ /** Insert a 过门 fill: pause the chain, play the run, resume after. */
1646
+ insertFill(root) {
1647
+ if (!this.audio) return;
1648
+ this.stopMelody();
1649
+ const scale = SCALES[this.style];
1650
+ let t = 0;
1651
+ for (const step of FILL) {
1652
+ const note = root + (scale[step.deg % scale.length] ?? 0) + 12 * Math.floor(step.deg / scale.length);
1653
+ setTimeout(() => {
1654
+ if (this.playing) this.pluck(note, .32, .7);
1655
+ }, t);
1656
+ t += step.beats * TokenQuotaMusic.BEAT_MS;
1657
+ }
1658
+ this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
1476
1659
  }
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
- ];
1660
+ /** Stop the melody chain (drone keeps fading naturally). */
1661
+ stopMelody() {
1662
+ if (this.phraseTimer !== void 0) {
1663
+ clearTimeout(this.phraseTimer);
1664
+ this.phraseTimer = void 0;
1665
+ }
1666
+ }
1667
+ /** Stop the whole piece; `final` adds the closing cadence gestures. */
1668
+ stopPiece(final) {
1669
+ this.stopMelody();
1670
+ this.playing = false;
1671
+ if (final && this.audio) {
1672
+ const base = TokenQuotaMusic.BASE_MIDI;
1673
+ const scale = SCALES[this.style];
1674
+ let t = 0;
1675
+ for (let i = 7; i >= 0; i--) {
1676
+ const deg = i % scale.length;
1677
+ const note = base + 12 * Math.floor(i / scale.length) + (scale[deg] ?? 0);
1678
+ setTimeout(() => this.pluck(note, .3), t);
1679
+ t += 120;
1680
+ }
1681
+ setTimeout(() => {
1682
+ this.pluck(base, .5);
1683
+ this.pluck(base + 7, .45);
1684
+ this.pluck(base + 12, .4);
1685
+ this.pluck(base - 12, .5);
1686
+ }, t + 150);
1687
+ }
1688
+ this.fadeDrone(final ? 1.2 : .8);
1689
+ this.allMidiOff();
1690
+ }
1691
+ /** Hard stop everything (master switch off / teardown). */
1692
+ stopAll() {
1693
+ this.stopPiece(false);
1694
+ }
1695
+ /** Start the sustained open fifth (root + fifth below octave root). */
1696
+ startDrone(root) {
1697
+ if (!this.audio || this.droneActive) return;
1698
+ const now = this.audio.currentTime;
1699
+ const gain = this.audio.createGain();
1700
+ gain.gain.setValueAtTime(0, now);
1701
+ const filter = this.audio.createBiquadFilter();
1702
+ filter.type = "lowpass";
1703
+ filter.frequency.value = 620;
1704
+ filter.Q.value = .4;
1705
+ gain.connect(filter);
1706
+ filter.connect(this.audio.destination);
1707
+ const oscs = [];
1708
+ for (const interval of [-12, -5]) {
1709
+ const osc = this.audio.createOscillator();
1710
+ osc.type = "sine";
1711
+ osc.frequency.value = midiToFreq(root + interval);
1712
+ osc.connect(gain);
1713
+ osc.start(now);
1714
+ oscs.push(osc);
1715
+ }
1716
+ this.droneOscs = oscs;
1717
+ this.droneGain = gain;
1718
+ this.droneFilter = filter;
1719
+ this.droneActive = true;
1720
+ gain.gain.linearRampToValueAtTime(this.volume * .07, now + 1.4);
1721
+ }
1722
+ /** Glide the drone to a new tonic (mode rotation). */
1723
+ glideDrone(root) {
1724
+ if (!this.audio || !this.droneActive) return;
1725
+ const now = this.audio.currentTime;
1726
+ const targets = [root - 12, root - 5];
1727
+ this.droneOscs.forEach((osc, i) => {
1728
+ const target = targets[i] ?? root;
1729
+ osc.frequency.setValueAtTime(osc.frequency.value, now);
1730
+ osc.frequency.exponentialRampToValueAtTime(midiToFreq(target), now + .9);
1731
+ });
1732
+ }
1733
+ /** Fade the drone out and stop its oscillators. */
1734
+ fadeDrone(seconds) {
1735
+ if (!this.audio || !this.droneActive || this.droneGain === null) return;
1736
+ const now = this.audio.currentTime;
1737
+ const gain = this.droneGain;
1738
+ gain.gain.cancelScheduledValues(now);
1739
+ gain.gain.setValueAtTime(Math.max(gain.gain.value, 1e-4), now);
1740
+ gain.gain.exponentialRampToValueAtTime(1e-4, now + seconds);
1741
+ const oscs = this.droneOscs;
1742
+ const filter = this.droneFilter;
1743
+ setTimeout(() => {
1744
+ for (const osc of oscs) {
1745
+ try {
1746
+ osc.stop();
1747
+ } catch {}
1748
+ try {
1749
+ osc.disconnect();
1750
+ } catch {}
1751
+ }
1752
+ try {
1753
+ filter?.disconnect();
1754
+ } catch {}
1755
+ try {
1756
+ gain.disconnect();
1757
+ } catch {}
1758
+ }, seconds * 1e3 + 120);
1759
+ this.droneActive = false;
1760
+ this.droneOscs = [];
1761
+ this.droneGain = null;
1762
+ this.droneFilter = null;
1492
1763
  }
1493
- /** Play one note: Web Audio + MIDI simultaneously when available. */
1494
- playNote(midi, durationSec, velocity) {
1764
+ /**
1765
+ * Guzheng-like pluck: two slightly detuned oscillators through a sweeping
1766
+ * lowpass, fast attack and a long exponential decay (notes overlap, which
1767
+ * keeps the piece continuous instead of choppy).
1768
+ */
1769
+ pluck(midi, velocity, decaySec = 1.3) {
1495
1770
  if (!this.audio) return;
1771
+ const peak = this.volume * velocity * .4;
1772
+ if (peak <= 0) return;
1496
1773
  const now = this.audio.currentTime;
1497
- const peak = this.volume * velocity * .5;
1774
+ const freq = midiToFreq(midi);
1775
+ const gain = this.audio.createGain();
1776
+ const filter = this.audio.createBiquadFilter();
1777
+ filter.type = "lowpass";
1778
+ filter.frequency.setValueAtTime(2400, now);
1779
+ filter.frequency.exponentialRampToValueAtTime(420, now + decaySec);
1780
+ filter.Q.value = .6;
1781
+ gain.gain.setValueAtTime(1e-4, now);
1782
+ gain.gain.exponentialRampToValueAtTime(peak, now + .006);
1783
+ gain.gain.exponentialRampToValueAtTime(1e-4, now + decaySec);
1784
+ filter.connect(gain);
1785
+ gain.connect(this.audio.destination);
1786
+ const osc1 = this.audio.createOscillator();
1787
+ osc1.type = "triangle";
1788
+ osc1.frequency.value = freq;
1789
+ const osc2 = this.audio.createOscillator();
1790
+ osc2.type = "sine";
1791
+ osc2.frequency.value = freq * 2.001;
1792
+ const octGain = this.audio.createGain();
1793
+ octGain.gain.value = .35;
1794
+ osc1.connect(filter);
1795
+ osc2.connect(octGain);
1796
+ octGain.connect(filter);
1797
+ osc1.start(now);
1798
+ osc1.stop(now + decaySec + .1);
1799
+ osc2.start(now);
1800
+ osc2.stop(now + decaySec + .1);
1801
+ this.midiNote(midi, decaySec, velocity);
1802
+ }
1803
+ /** Dizi-like breath note: sine with vibrato and a soft envelope. */
1804
+ breath(midi, durationSec) {
1805
+ if (!this.audio) return;
1806
+ const peak = this.volume * .22;
1498
1807
  if (peak <= 0) return;
1808
+ const now = this.audio.currentTime;
1499
1809
  const osc = this.audio.createOscillator();
1500
1810
  osc.type = "sine";
1501
1811
  osc.frequency.value = midiToFreq(midi);
1812
+ const lfo = this.audio.createOscillator();
1813
+ lfo.frequency.value = 5.2;
1814
+ const lfoGain = this.audio.createGain();
1815
+ lfoGain.gain.value = 4.5;
1816
+ lfo.connect(lfoGain);
1817
+ lfoGain.connect(osc.frequency);
1502
1818
  const gain = this.audio.createGain();
1503
1819
  gain.gain.setValueAtTime(0, now);
1504
- gain.gain.linearRampToValueAtTime(peak, now + .008);
1505
- gain.gain.exponentialRampToValueAtTime(.001, now + durationSec);
1820
+ gain.gain.linearRampToValueAtTime(peak, now + .18);
1821
+ gain.gain.setValueAtTime(peak, now + durationSec * .6);
1822
+ gain.gain.linearRampToValueAtTime(1e-4, now + durationSec);
1506
1823
  osc.connect(gain);
1507
1824
  gain.connect(this.audio.destination);
1508
1825
  osc.start(now);
1509
- osc.stop(now + durationSec + .05);
1510
- if (this.midiOutput !== null) {
1511
- const vel = Math.max(1, Math.floor(velocity * 127));
1826
+ lfo.start(now);
1827
+ osc.stop(now + durationSec + .1);
1828
+ lfo.stop(now + durationSec + .1);
1829
+ this.midiNote(midi, durationSec, .5);
1830
+ }
1831
+ /** Send one MIDI note with a registered NoteOff (anti hanging note). */
1832
+ midiNote(midi, durationSec, velocity) {
1833
+ if (this.midiOutput === null) return;
1834
+ const note = midi & 127;
1835
+ const vel = Math.max(1, Math.min(127, Math.floor(velocity * 127)));
1836
+ try {
1512
1837
  this.midiOutput.send([
1513
1838
  144,
1514
- midi & 127,
1839
+ note,
1515
1840
  vel
1516
1841
  ]);
1517
- setTimeout(() => {
1842
+ } catch {
1843
+ return;
1844
+ }
1845
+ this.activeMidiNotes.add(note);
1846
+ setTimeout(() => {
1847
+ try {
1518
1848
  this.midiOutput?.send([
1519
1849
  128,
1520
- midi & 127,
1850
+ note,
1521
1851
  0
1522
1852
  ]);
1523
- }, durationSec * 1e3);
1524
- }
1853
+ } catch {}
1854
+ this.activeMidiNotes.delete(note);
1855
+ }, Math.max(80, durationSec * 1e3));
1525
1856
  }
1526
- /** Play a three-note chord plus a bass octave below. */
1527
- 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);
1857
+ /** Silence every registered note and send All Notes Off. */
1858
+ allMidiOff() {
1859
+ if (this.midiOutput === null) return;
1860
+ for (const note of this.activeMidiNotes) try {
1861
+ this.midiOutput.send([
1862
+ 128,
1863
+ note,
1864
+ 0
1865
+ ]);
1866
+ } catch {}
1867
+ this.activeMidiNotes.clear();
1868
+ try {
1869
+ this.midiOutput.send([
1870
+ 176,
1871
+ 123,
1872
+ 0
1873
+ ]);
1874
+ } catch {}
1534
1875
  }
1535
1876
  };
1536
1877
  //#endregion
@@ -1609,7 +1950,9 @@ window.__ModuleLoader__.load({
1609
1950
  musicStyle: "风格",
1610
1951
  musicStyleMajor: "大调",
1611
1952
  musicStyleMinor: "小调",
1612
- musicStylePentatonic: "五声",
1953
+ musicStylePentatonic: "古风",
1954
+ musicOnlyCurrentSessionLabel: "只播当前会话",
1955
+ musicOnlyCurrentSessionHint: "开启后仅当前窗口的会话有音乐,后台子任务静音;关闭则所有会话都参与作曲",
1613
1956
  checkUpdatesNow: "立即检查",
1614
1957
  checkingUpdates: "检查中…",
1615
1958
  checkUpToDate: "已是最新",
@@ -1683,7 +2026,9 @@ window.__ModuleLoader__.load({
1683
2026
  musicStyle: "Style",
1684
2027
  musicStyleMajor: "Major",
1685
2028
  musicStyleMinor: "Minor",
1686
- musicStylePentatonic: "Pentatonic",
2029
+ musicStylePentatonic: "Gufeng",
2030
+ musicOnlyCurrentSessionLabel: "Current session only",
2031
+ musicOnlyCurrentSessionHint: "When on, only the visible session drives the soundtrack (background tasks stay silent); when off, every session contributes.",
1687
2032
  checkUpdatesNow: "Check now",
1688
2033
  checkingUpdates: "Checking…",
1689
2034
  checkUpToDate: "Up to date",
@@ -1728,8 +2073,15 @@ window.__ModuleLoader__.load({
1728
2073
  let lastMonitored = null;
1729
2074
  let lastOnFull = "stop";
1730
2075
  let lastReset = null;
2076
+ let lastOnlyCurrentSession = true;
1731
2077
  const music = new TokenQuotaMusic();
1732
2078
  const sse = new EventSource("/token-quota/events");
2079
+ sse.addEventListener("hb", () => {
2080
+ music.markStreamActivity();
2081
+ });
2082
+ sse.addEventListener("open", () => {
2083
+ music.markStreamActivity();
2084
+ });
1733
2085
  sse.addEventListener("action", (event) => {
1734
2086
  let action;
1735
2087
  try {
@@ -1737,12 +2089,19 @@ window.__ModuleLoader__.load({
1737
2089
  } catch {
1738
2090
  return;
1739
2091
  }
1740
- if (lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
2092
+ if (lastOnlyCurrentSession && lastSessionId !== void 0 && action.sessionId !== String(lastSessionId)) return;
1741
2093
  music.onAction(action);
1742
2094
  });
1743
2095
  ctx.effect(() => () => {
1744
2096
  sse.close();
1745
2097
  }, "token-quota-ui: music SSE");
2098
+ /** Build a writable music-settings clone with defaults applied. */
2099
+ const musicDefaults = (raw) => ({
2100
+ enabled: raw?.enabled ?? false,
2101
+ volume: raw?.volume ?? .5,
2102
+ style: raw?.style ?? "pentatonic",
2103
+ onlyCurrentSession: raw?.onlyCurrentSession ?? true
2104
+ });
1746
2105
  const keyOf = (selection) => {
1747
2106
  if (selection === null) return void 0;
1748
2107
  return `${selection.provider}/${selection.model}`;
@@ -1852,18 +2211,15 @@ window.__ModuleLoader__.load({
1852
2211
  bound?.setCheckUpdates(doc?.checkUpdates ?? true);
1853
2212
  bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
1854
2213
  bound?.setReset(lastReset);
1855
- const musicSettings = {
1856
- enabled: false,
1857
- volume: .5,
1858
- style: "major",
1859
- ...doc?.music ?? {}
1860
- };
2214
+ const musicSettings = musicDefaults(doc?.music);
2215
+ lastOnlyCurrentSession = musicSettings.onlyCurrentSession;
1861
2216
  music.setEnabled(musicSettings.enabled);
1862
2217
  music.setVolume(musicSettings.volume);
1863
2218
  music.setStyle(musicSettings.style);
1864
2219
  bound?.setMusicEnabled(musicSettings.enabled);
1865
2220
  bound?.setMusicVolume(musicSettings.volume);
1866
2221
  bound?.setMusicStyle(musicSettings.style);
2222
+ bound?.setMusicOnlyCurrentSession(musicSettings.onlyCurrentSession);
1867
2223
  fetch("/token-quota", { headers: { accept: "application/json" } }).then((response) => {
1868
2224
  if (!response.ok) {
1869
2225
  bound?.setError(`quota route: ${String(response.status)}`);
@@ -1934,36 +2290,30 @@ window.__ModuleLoader__.load({
1934
2290
  scope.set("dimWhenIdle", enabled);
1935
2291
  };
1936
2292
  const setMusicEnabled = (enabled) => {
1937
- const current = {
1938
- enabled: false,
1939
- volume: .5,
1940
- style: "major",
1941
- ...scope.getSnapshot().value?.music ?? {}
1942
- };
2293
+ const doc = scope.getSnapshot().value;
2294
+ const current = musicDefaults(doc?.music);
1943
2295
  current.enabled = enabled;
1944
2296
  scope.set("music", current);
1945
2297
  if (enabled) music.ensureStarted();
1946
2298
  };
1947
2299
  const setMusicVolume = (volume) => {
1948
- const current = {
1949
- enabled: false,
1950
- volume: .5,
1951
- style: "major",
1952
- ...scope.getSnapshot().value?.music ?? {}
1953
- };
2300
+ const doc = scope.getSnapshot().value;
2301
+ const current = musicDefaults(doc?.music);
1954
2302
  current.volume = Math.max(0, Math.min(1, volume));
1955
2303
  scope.set("music", current);
1956
2304
  };
1957
2305
  const setMusicStyle = (style) => {
1958
- const current = {
1959
- enabled: false,
1960
- volume: .5,
1961
- style: "major",
1962
- ...scope.getSnapshot().value?.music ?? {}
1963
- };
2306
+ const doc = scope.getSnapshot().value;
2307
+ const current = musicDefaults(doc?.music);
1964
2308
  current.style = style;
1965
2309
  scope.set("music", current);
1966
2310
  };
2311
+ const setMusicOnlyCurrentSession = (only) => {
2312
+ const doc = scope.getSnapshot().value;
2313
+ const current = musicDefaults(doc?.music);
2314
+ current.onlyCurrentSession = only;
2315
+ scope.set("music", current);
2316
+ };
1967
2317
  /** Ask the Host to check the registry right now, then refresh the snapshot. */
1968
2318
  const checkUpdatesNow = () => {
1969
2319
  bound?.setCheckingUpdates(true);
@@ -2050,7 +2400,8 @@ window.__ModuleLoader__.load({
2050
2400
  setDimWhenIdle,
2051
2401
  setMusicEnabled,
2052
2402
  setMusicVolume,
2053
- setMusicStyle
2403
+ setMusicStyle,
2404
+ setMusicOnlyCurrentSession
2054
2405
  };
2055
2406
  };
2056
2407
  ctx.slots.inject("shell.overlay", () => ctx.slots.register({