@jxgame2020/dsh-token-quota 0.1.27 → 0.1.29

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
@@ -95,7 +95,7 @@ window.__ModuleLoader__.load({
95
95
  d.musicOnlyCurrentSession = only;
96
96
  },
97
97
  setUpgrade: (d, upgrade) => {
98
- const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.27" ? upgrade : null;
98
+ const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.29" ? upgrade : null;
99
99
  if (d.upgrade === null || d.upgrade.latestVersion !== normalized?.latestVersion) d.upgradeDismissed = false;
100
100
  d.upgrade = normalized;
101
101
  },
@@ -191,7 +191,7 @@ window.__ModuleLoader__.load({
191
191
  }
192
192
  //#endregion
193
193
  //#region \0dsh-css:/Users/jxgame/Desktop/works/deepseek-harness-package/dsh-token-quota/packages/token-quota/src/client/TokenQuotaPanel.module.css.mjs
194
- const css = ".NPLLIq_panel{box-sizing:border-box;background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l1);pointer-events:auto;z-index:40;opacity:1;border-radius:12px;flex-direction:column;width:320px;max-height:min(72vh,560px);transition:opacity .18s;display:flex;position:fixed;top:56px;right:12px;overflow:hidden;box-shadow:0 8px 28px #0000002e}.NPLLIq_panelDim{opacity:.55}.NPLLIq_panelDimStrong{opacity:.3}.NPLLIq_header{border-bottom:1px solid var(--dsw-alias-border-l1);cursor:grab;user-select:none;touch-action:none;justify-content:space-between;align-items:flex-start;gap:8px;padding:10px 12px;display:flex}.NPLLIq_header:active{cursor:grabbing}.NPLLIq_headerText{flex-direction:column;gap:2px;min-width:0;display:flex}.NPLLIq_title{color:var(--dsw-alias-label-primary);align-items:baseline;gap:6px;font-size:13px;font-weight:600;display:flex}.NPLLIq_version{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:10px;font-weight:500}.NPLLIq_versionNew{color:var(--dsw-alias-status-warning-foreground,#b56900);background:var(--dsw-alias-status-warning-background,#fff6d7);cursor:default;border-radius:4px;padding:1px 6px}.NPLLIq_subtitle{color:var(--dsw-alias-label-secondary);font-size:11px}.NPLLIq_collapse{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:2px 6px;font-size:11px}.NPLLIq_collapse:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_body{padding:4px 0;overflow-y:auto}.NPLLIq_row{border-bottom:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:6px;padding:8px 12px;display:flex}.NPLLIq_rowHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.NPLLIq_rowName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;align-items:center;gap:6px;font-size:12px;font-weight:500;display:inline-flex;overflow:hidden}.NPLLIq_currentBadge{color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary);border-radius:8px;flex:none;padding:1px 6px;font-size:10px}.NPLLIq_rowMeta{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:11px}.NPLLIq_bar{background:var(--dsw-alias-bg-layer-1);border-radius:3px;height:6px;overflow:hidden}.NPLLIq_fillIdle{background:var(--dsw-alias-brand-primary);height:100%;transition:width .2s}.NPLLIq_fillWarn{background:var(--dsw-alias-state-warn-primary);height:100%;transition:width .2s}.NPLLIq_fillOver{background:var(--dsw-alias-state-error-primary);height:100%;transition:width .2s}.NPLLIq_controls{align-items:center;gap:6px;display:flex}.NPLLIq_input{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);min-width:0;color:var(--dsw-alias-label-primary);border-radius:6px;flex:1;padding:4px 8px;font-size:11px}.NPLLIq_input::placeholder{color:var(--dsw-alias-label-secondary)}.NPLLIq_save,.NPLLIq_switch{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;flex:none;padding:4px 10px;font-size:11px}.NPLLIq_save:hover,.NPLLIq_switch:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}.NPLLIq_switch:disabled{opacity:.5;cursor:default}.NPLLIq_notice,.NPLLIq_noticeError{color:var(--dsw-alias-label-secondary);padding:10px 12px;font-size:12px}.NPLLIq_noticeError{color:var(--dsw-alias-state-error-primary)}.NPLLIq_tab{background:var(--dsw-alias-brand-primary);border:1px solid var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-overlay);pointer-events:auto;cursor:pointer;z-index:90;border-radius:10px;align-items:center;gap:6px;padding:9px 14px;font-size:12px;font-weight:600;display:inline-flex;position:fixed;top:56px;right:12px;box-shadow:0 6px 20px #00000040}.NPLLIq_tab:hover{filter:brightness(1.08)}.NPLLIq_tabIcon{font-size:12px;line-height:1}.NPLLIq_tabLabel{white-space:nowrap}.NPLLIq_headerActions{flex:none;align-items:center;gap:4px;display:flex}.NPLLIq_settingsBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:2px 6px;font-size:11px}.NPLLIq_settingsBtn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_settingsBtnActive{color:var(--dsw-alias-brand-primary)}.NPLLIq_selectBtn{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-brand-primary);cursor:pointer;background:0 0;border-radius:6px;flex:none;padding:1px 6px;font-size:10px}.NPLLIq_selectBtn:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_gearBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;margin-left:6px;padding:0 2px;font-size:12px;line-height:1}.NPLLIq_gearBtn:hover{color:var(--dsw-alias-label-primary)}.NPLLIq_fullNotice{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;margin:6px 12px;padding:8px 10px;font-size:12px}.NPLLIq_upgradeBanner{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-divider-1,#0000001a);border-radius:8px;margin:6px 12px;padding:10px 30px 10px 10px;font-size:12px;position:relative}@keyframes NPLLIq_upgradeBannerFlash{0%,to{border-color:var(--dsw-alias-divider-1,#0000001a);box-shadow:0 0 #f0a02000}50%{border-color:var(--dsw-alias-status-warning-foreground,#b56900);box-shadow:0 0 0 5px #f0a02073}}.NPLLIq_upgradeBannerFlash{border-color:var(--dsw-alias-status-warning-foreground,#b56900);animation:.6s ease-in-out 4 NPLLIq_upgradeBannerFlash}.NPLLIq_upgradeBannerText{margin-bottom:6px;font-weight:600}.NPLLIq_upgradeCommands{flex-direction:column;gap:4px;display:flex}.NPLLIq_upgradeCommandRow{align-items:center;gap:6px;display:flex}.NPLLIq_upgradeCommand{background:var(--dsw-alias-bg-layer-2,#0000000a);white-space:nowrap;border-radius:4px;flex:1;min-width:0;padding:4px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;overflow-x:auto}.NPLLIq_copyBtn{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-2,#0000000a);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:4px;flex:none;padding:3px 8px;font-size:11px;transition:transform 80ms,background .12s,color .12s}.NPLLIq_copyBtn:hover{background:var(--dsw-alias-bg-layer-3,#00000014)}.NPLLIq_copyBtn:active{transform:scale(.92)}.NPLLIq_copyBtnDone{background:var(--dsw-alias-status-success-background,#14963c1f);border-color:var(--dsw-alias-status-success-foreground,#0a7d32);color:var(--dsw-alias-status-success-foreground,#0a7d32);font-weight:600}.NPLLIq_upgradeHint{color:var(--dsw-alias-label-secondary);margin-top:6px;font-size:11px}.NPLLIq_upgradeClose{width:20px;height:20px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;font-size:16px;line-height:1;position:absolute;top:4px;right:6px}.NPLLIq_upgradeClose:hover{background:var(--dsw-alias-bg-layer-2,#0000000a);color:var(--dsw-alias-label-primary)}.NPLLIq_checkUpdatesRow{justify-content:space-between;align-items:center;gap:12px;display:flex}.NPLLIq_checkUpdatesLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:12px;display:flex}.NPLLIq_checkUpdatesLabel input{margin:0}.NPLLIq_checkUpdatesLabel input[type=range]{min-width:90px;accent-color:var(--dsw-alias-brand-primary);flex:1}.NPLLIq_checkUpdatesLabel select{background:var(--dsw-alias-bg-layer-2,#0000000a);min-width:90px;color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-divider-1,#00000026);border-radius:4px;flex:1;padding:2px 4px;font-size:12px}.NPLLIq_checkBtn{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-2,#0000000a);color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-align:center;border-radius:4px;flex:none;min-width:72px;padding:4px 10px;font-size:12px}.NPLLIq_checkBtn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3,#00000014)}.NPLLIq_checkBtn:disabled{opacity:.6;cursor:default}.NPLLIq_checkErrorHint{color:var(--dsw-alias-state-error-primary,#c00);background:var(--dsw-alias-state-error-background,#ff00000f);border:1px solid var(--dsw-alias-state-error-border,#b4000040);word-break:break-all;border-radius:6px;margin-top:8px;padding:6px 8px;font-size:11px;line-height:1.4}.NPLLIq_dialog{z-index:100;box-sizing:border-box;background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;gap:12px;width:280px;max-height:80vh;padding:14px;display:flex;position:fixed;top:50%;left:50%;overflow-y:auto;transform:translate(-50%,-50%);box-shadow:0 8px 28px #00000040}.NPLLIq_dialogTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.NPLLIq_dialogSection{flex-direction:column;gap:6px;display:flex}.NPLLIq_dialogLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500}.NPLLIq_monitorHint{color:var(--dsw-alias-label-secondary);font-size:11px}.NPLLIq_dialogHint{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:11px;line-height:1.4}.NPLLIq_monitorList{flex-direction:column;gap:2px;max-height:180px;display:flex;overflow-y:auto}.NPLLIq_monitorRow{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:2px 4px;font-size:12px;display:flex}.NPLLIq_monitorRow:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_monitorName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.NPLLIq_radioRow{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:3px 4px;font-size:12px;display:flex}.NPLLIq_radioRow:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_dialogActions{justify-content:flex-end;gap:6px;display:flex}.NPLLIq_dialogBtn,.NPLLIq_dialogBtnPrimary{border:1px solid var(--dsw-alias-border-l1);cursor:pointer;border-radius:6px;padding:4px 12px;font-size:11px}.NPLLIq_dialogBtn{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.NPLLIq_dialogBtnPrimary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-overlay)}.NPLLIq_dialogHeader{z-index:2;background:var(--dsw-alias-bg-overlay);cursor:grab;user-select:none;touch-action:none;justify-content:space-between;align-items:center;gap:8px;margin:-14px -14px 0;padding:14px 14px 8px;display:flex;position:sticky;top:-14px}.NPLLIq_dialogHeader:active{cursor:grabbing}.NPLLIq_dialogClose{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;font-size:18px;line-height:1;display:flex}.NPLLIq_dialogClose:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_resetRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.NPLLIq_resetSelect{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:3px 6px;font-size:12px}.NPLLIq_logTable{border-collapse:collapse;width:100%;font-size:11px}.NPLLIq_logTable th{text-align:left;color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);padding:4px 6px;font-weight:500}.NPLLIq_logTable td{color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l1);font-variant-numeric:tabular-nums;padding:4px 6px}.NPLLIq_logDayCol{white-space:nowrap;color:var(--dsw-alias-label-secondary)}.NPLLIq_logModelCol{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.NPLLIq_logUsedCol{text-align:right}.NPLLIq_logDialog{gap:0;width:380px;max-height:min(72vh,480px);padding:0}.NPLLIq_logHeader{background:var(--dsw-alias-bg-overlay);cursor:grab;user-select:none;touch-action:none;flex:none;justify-content:space-between;align-items:center;gap:8px;padding:14px 14px 8px;display:flex}.NPLLIq_logHeader:active{cursor:grabbing}.NPLLIq_logScroll{flex:1;min-height:0;padding:0 14px 14px;overflow:auto}.NPLLIq_logToolbar{flex:1;align-items:center;gap:6px;display:flex}.NPLLIq_logSearch{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);min-width:0;color:var(--dsw-alias-label-primary);border-radius:4px;flex:1;padding:3px 6px;font-size:11px}.NPLLIq_logSearch:focus{border-color:var(--dsw-alias-accent-1,#4a9eff);outline:none}.NPLLIq_logClearBtn{border:1px solid var(--dsw-alias-state-error-border,#b400004d);color:var(--dsw-alias-state-error-primary,#c00);cursor:pointer;white-space:nowrap;background:0 0;border-radius:4px;flex:none;padding:3px 8px;font-size:11px}.NPLLIq_logClearBtn:hover{background:var(--dsw-alias-state-error-background,#ff00000f)}.NPLLIq_logPager{border-top:1px solid var(--dsw-alias-divider-1,#00000014);background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-secondary);flex:none;justify-content:space-between;align-items:center;gap:8px;padding:6px 14px;font-size:11px;display:flex}.NPLLIq_logPagerInfo{white-space:nowrap}.NPLLIq_logPagerControls{align-items:center;gap:4px;display:flex}.NPLLIq_logPagerBtn{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);width:24px;height:24px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:4px;font-size:13px;line-height:1}.NPLLIq_logPagerBtn:disabled{opacity:.4;cursor:default}.NPLLIq_logPagerPages{text-align:center;min-width:50px;color:var(--dsw-alias-label-secondary)}.NPLLIq_logPageSizeSelect{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:4px;padding:2px 4px;font-size:11px}.NPLLIq_dialogOverlay{z-index:100001;background:#0000004d;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.NPLLIq_logClearDialog{gap:0;width:320px;padding:16px}.NPLLIq_dialogBody{color:var(--dsw-alias-label-secondary);margin:6px 0 10px;font-size:12px;line-height:1.5}.NPLLIq_logClearDate{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);width:100%;color:var(--dsw-alias-label-primary);border-radius:6px;margin-bottom:12px;padding:6px 8px;font-size:13px}.NPLLIq_logClearDate:focus{border-color:var(--dsw-alias-accent-1,#4a9eff);outline:none}";
194
+ const css = ".NPLLIq_panel{box-sizing:border-box;background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l1);pointer-events:auto;z-index:40;opacity:1;border-radius:12px;flex-direction:column;width:320px;max-height:min(72vh,560px);transition:opacity .18s;display:flex;position:fixed;top:56px;right:12px;overflow:hidden;box-shadow:0 8px 28px #0000002e}.NPLLIq_panelDim{opacity:.55}.NPLLIq_panelDimStrong{opacity:.3}.NPLLIq_header{border-bottom:1px solid var(--dsw-alias-border-l1);cursor:grab;user-select:none;touch-action:none;justify-content:space-between;align-items:flex-start;gap:8px;padding:10px 12px;display:flex}.NPLLIq_header:active{cursor:grabbing}.NPLLIq_headerText{flex-direction:column;gap:2px;min-width:0;display:flex}.NPLLIq_title{color:var(--dsw-alias-label-primary);align-items:baseline;gap:6px;font-size:13px;font-weight:600;display:flex}.NPLLIq_version{color:var(--dsw-alias-label-secondary);white-space:nowrap;font-size:10px;font-weight:500}.NPLLIq_versionNew{color:var(--dsw-alias-status-warning-foreground,#b56900);background:var(--dsw-alias-status-warning-background,#fff6d7);cursor:default;border-radius:4px;padding:1px 6px}.NPLLIq_subtitle{color:var(--dsw-alias-label-secondary);font-size:11px}.NPLLIq_collapse{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:2px 6px;font-size:11px}.NPLLIq_collapse:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_body{padding:4px 0;overflow-y:auto}.NPLLIq_row{border-bottom:1px solid var(--dsw-alias-border-l1);flex-direction:column;gap:6px;padding:8px 12px;display:flex}.NPLLIq_rowHeader{justify-content:space-between;align-items:center;gap:8px;display:flex}.NPLLIq_rowName{color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;align-items:center;gap:6px;font-size:12px;font-weight:500;display:inline-flex;overflow:hidden}.NPLLIq_currentBadge{color:var(--dsw-alias-state-success-primary);border:1px solid var(--dsw-alias-state-success-primary);border-radius:8px;flex:none;padding:1px 6px;font-size:10px}.NPLLIq_rowMeta{color:var(--dsw-alias-label-secondary);font-variant-numeric:tabular-nums;flex:none;font-size:11px}.NPLLIq_bar{background:var(--dsw-alias-bg-layer-1);border-radius:3px;height:6px;overflow:hidden}.NPLLIq_fillIdle{background:var(--dsw-alias-brand-primary);height:100%;transition:width .2s}.NPLLIq_fillWarn{background:var(--dsw-alias-state-warn-primary);height:100%;transition:width .2s}.NPLLIq_fillOver{background:var(--dsw-alias-state-error-primary);height:100%;transition:width .2s}.NPLLIq_controls{align-items:center;gap:6px;display:flex}.NPLLIq_input{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);min-width:0;color:var(--dsw-alias-label-primary);border-radius:6px;flex:1;padding:4px 8px;font-size:11px}.NPLLIq_input::placeholder{color:var(--dsw-alias-label-secondary)}.NPLLIq_save,.NPLLIq_switch{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;flex:none;padding:4px 10px;font-size:11px}.NPLLIq_save:hover,.NPLLIq_switch:hover:not(:disabled){background:var(--dsw-alias-bg-layer-2)}.NPLLIq_switch:disabled{opacity:.5;cursor:default}.NPLLIq_notice,.NPLLIq_noticeError{color:var(--dsw-alias-label-secondary);padding:10px 12px;font-size:12px}.NPLLIq_noticeError{color:var(--dsw-alias-state-error-primary)}.NPLLIq_tab{background:var(--dsw-alias-brand-primary);border:1px solid var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-overlay);pointer-events:auto;cursor:pointer;z-index:90;border-radius:10px;align-items:center;gap:6px;padding:9px 14px;font-size:12px;font-weight:600;display:inline-flex;position:fixed;top:56px;right:12px;box-shadow:0 6px 20px #00000040}.NPLLIq_tab:hover{filter:brightness(1.08)}.NPLLIq_tabIcon{font-size:12px;line-height:1}.NPLLIq_tabLabel{white-space:nowrap}.NPLLIq_headerActions{flex:none;align-items:center;gap:4px;display:flex}.NPLLIq_settingsBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:6px;flex:none;padding:2px 6px;font-size:11px}.NPLLIq_settingsBtn:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_settingsBtnActive{color:var(--dsw-alias-brand-primary);background:var(--dsw-alias-bg-layer-2,#0000000f);box-shadow:inset 0 0 0 1px var(--dsw-alias-brand-primary);font-weight:700}.NPLLIq_selectBtn{border:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-brand-primary);cursor:pointer;background:0 0;border-radius:6px;flex:none;padding:1px 6px;font-size:10px}.NPLLIq_selectBtn:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_gearBtn{color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;flex:none;margin-left:6px;padding:0 2px;font-size:12px;line-height:1}.NPLLIq_gearBtn:hover{color:var(--dsw-alias-label-primary)}.NPLLIq_fullNotice{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;margin:6px 12px;padding:8px 10px;font-size:12px}.NPLLIq_upgradeBanner{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-divider-1,#0000001a);border-radius:8px;margin:6px 12px;padding:10px 30px 10px 10px;font-size:12px;position:relative}@keyframes NPLLIq_upgradeBannerFlash{0%,to{border-color:var(--dsw-alias-divider-1,#0000001a);box-shadow:0 0 #f0a02000}50%{border-color:var(--dsw-alias-status-warning-foreground,#b56900);box-shadow:0 0 0 5px #f0a02073}}.NPLLIq_upgradeBannerFlash{border-color:var(--dsw-alias-status-warning-foreground,#b56900);animation:.6s ease-in-out 4 NPLLIq_upgradeBannerFlash}.NPLLIq_upgradeBannerText{margin-bottom:6px;font-weight:600}.NPLLIq_upgradeCommands{flex-direction:column;gap:4px;display:flex}.NPLLIq_upgradeCommandRow{align-items:center;gap:6px;display:flex}.NPLLIq_upgradeCommand{background:var(--dsw-alias-bg-layer-2,#0000000a);white-space:nowrap;border-radius:4px;flex:1;min-width:0;padding:4px 6px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11px;overflow-x:auto}.NPLLIq_copyBtn{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-2,#0000000a);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:4px;flex:none;padding:3px 8px;font-size:11px;transition:transform 80ms,background .12s,color .12s}.NPLLIq_copyBtn:hover{background:var(--dsw-alias-bg-layer-3,#00000014)}.NPLLIq_copyBtn:active{transform:scale(.92)}.NPLLIq_copyBtnDone{background:var(--dsw-alias-status-success-background,#14963c1f);border-color:var(--dsw-alias-status-success-foreground,#0a7d32);color:var(--dsw-alias-status-success-foreground,#0a7d32);font-weight:600}.NPLLIq_upgradeHint{color:var(--dsw-alias-label-secondary);margin-top:6px;font-size:11px}.NPLLIq_upgradeClose{width:20px;height:20px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:4px;font-size:16px;line-height:1;position:absolute;top:4px;right:6px}.NPLLIq_upgradeClose:hover{background:var(--dsw-alias-bg-layer-2,#0000000a);color:var(--dsw-alias-label-primary)}.NPLLIq_checkUpdatesRow{justify-content:space-between;align-items:center;gap:12px;display:flex}.NPLLIq_checkUpdatesLabel{color:var(--dsw-alias-label-primary);cursor:pointer;align-items:center;gap:6px;font-size:12px;display:flex}.NPLLIq_checkUpdatesLabel input{margin:0}.NPLLIq_checkUpdatesLabel input[type=range]{min-width:90px;accent-color:var(--dsw-alias-brand-primary);flex:1}.NPLLIq_checkUpdatesLabel select{background:var(--dsw-alias-bg-layer-2,#0000000a);min-width:90px;color:var(--dsw-alias-label-primary);border:1px solid var(--dsw-alias-divider-1,#00000026);border-radius:4px;flex:1;padding:2px 4px;font-size:12px}.NPLLIq_checkBtn{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-2,#0000000a);color:var(--dsw-alias-label-primary);cursor:pointer;white-space:nowrap;text-align:center;border-radius:4px;flex:none;min-width:72px;padding:4px 10px;font-size:12px}.NPLLIq_checkBtn:hover:not(:disabled){background:var(--dsw-alias-bg-layer-3,#00000014)}.NPLLIq_checkBtn:disabled{opacity:.6;cursor:default}.NPLLIq_checkErrorHint{color:var(--dsw-alias-state-error-primary,#c00);background:var(--dsw-alias-state-error-background,#ff00000f);border:1px solid var(--dsw-alias-state-error-border,#b4000040);word-break:break-all;border-radius:6px;margin-top:8px;padding:6px 8px;font-size:11px;line-height:1.4}.NPLLIq_dialog{z-index:100;box-sizing:border-box;background:var(--dsw-alias-bg-overlay);border:1px solid var(--dsw-alias-border-l1);border-radius:12px;flex-direction:column;gap:12px;width:280px;max-height:80vh;padding:14px;display:flex;position:fixed;top:50%;left:50%;overflow-y:auto;transform:translate(-50%,-50%);box-shadow:0 8px 28px #00000040}.NPLLIq_dialogTitle{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.NPLLIq_dialogSection{flex-direction:column;gap:6px;display:flex}.NPLLIq_dialogLabel{color:var(--dsw-alias-label-primary);font-size:12px;font-weight:500}.NPLLIq_monitorHint{color:var(--dsw-alias-label-secondary);font-size:11px}.NPLLIq_dialogHint{color:var(--dsw-alias-label-secondary);margin-top:4px;font-size:11px;line-height:1.4}.NPLLIq_monitorList{flex-direction:column;gap:2px;max-height:180px;display:flex;overflow-y:auto}.NPLLIq_monitorRow{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:2px 4px;font-size:12px;display:flex}.NPLLIq_monitorRow:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_monitorName{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.NPLLIq_radioRow{color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:6px;align-items:center;gap:6px;padding:3px 4px;font-size:12px;display:flex}.NPLLIq_radioRow:hover{background:var(--dsw-alias-bg-layer-1)}.NPLLIq_dialogActions{justify-content:flex-end;gap:6px;display:flex}.NPLLIq_dialogBtn,.NPLLIq_dialogBtnPrimary{border:1px solid var(--dsw-alias-border-l1);cursor:pointer;border-radius:6px;padding:4px 12px;font-size:11px}.NPLLIq_dialogBtn{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary)}.NPLLIq_dialogBtnPrimary{background:var(--dsw-alias-brand-primary);border-color:var(--dsw-alias-brand-primary);color:var(--dsw-alias-bg-overlay)}.NPLLIq_dialogHeader{z-index:2;background:var(--dsw-alias-bg-overlay);cursor:grab;user-select:none;touch-action:none;justify-content:space-between;align-items:center;gap:8px;margin:-14px -14px 0;padding:14px 14px 8px;display:flex;position:sticky;top:-14px}.NPLLIq_dialogHeader:active{cursor:grabbing}.NPLLIq_dialogClose{width:28px;height:28px;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-radius:8px;flex:none;justify-content:center;align-items:center;font-size:18px;line-height:1;display:flex}.NPLLIq_dialogClose:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1)}.NPLLIq_resetRow{flex-wrap:wrap;align-items:center;gap:6px;display:flex}.NPLLIq_resetSelect{border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);border-radius:6px;padding:3px 6px;font-size:12px}.NPLLIq_logTable{border-collapse:collapse;width:100%;font-size:11px}.NPLLIq_logTable th{text-align:left;color:var(--dsw-alias-label-secondary);border-bottom:1px solid var(--dsw-alias-border-l1);padding:4px 6px;font-weight:500}.NPLLIq_logTable td{color:var(--dsw-alias-label-primary);border-bottom:1px solid var(--dsw-alias-border-l1);font-variant-numeric:tabular-nums;padding:4px 6px}.NPLLIq_logDayCol{white-space:nowrap;color:var(--dsw-alias-label-secondary)}.NPLLIq_logModelCol{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.NPLLIq_logUsedCol{text-align:right}.NPLLIq_logDialog{gap:0;width:380px;max-height:min(72vh,480px);padding:0}.NPLLIq_logHeader{background:var(--dsw-alias-bg-overlay);cursor:grab;user-select:none;touch-action:none;flex:none;justify-content:space-between;align-items:center;gap:8px;padding:14px 14px 8px;display:flex}.NPLLIq_logHeader:active{cursor:grabbing}.NPLLIq_logScroll{flex:1;min-height:0;padding:0 14px 14px;overflow:auto}.NPLLIq_logToolbar{flex:1;align-items:center;gap:6px;display:flex}.NPLLIq_logSearch{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);min-width:0;color:var(--dsw-alias-label-primary);border-radius:4px;flex:1;padding:3px 6px;font-size:11px}.NPLLIq_logSearch:focus{border-color:var(--dsw-alias-accent-1,#4a9eff);outline:none}.NPLLIq_logClearBtn{border:1px solid var(--dsw-alias-state-error-border,#b400004d);color:var(--dsw-alias-state-error-primary,#c00);cursor:pointer;white-space:nowrap;background:0 0;border-radius:4px;flex:none;padding:3px 8px;font-size:11px}.NPLLIq_logClearBtn:hover{background:var(--dsw-alias-state-error-background,#ff00000f)}.NPLLIq_logPager{border-top:1px solid var(--dsw-alias-divider-1,#00000014);background:var(--dsw-alias-bg-overlay);color:var(--dsw-alias-label-secondary);flex:none;justify-content:space-between;align-items:center;gap:8px;padding:6px 14px;font-size:11px;display:flex}.NPLLIq_logPagerInfo{white-space:nowrap}.NPLLIq_logPagerControls{align-items:center;gap:4px;display:flex}.NPLLIq_logPagerBtn{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);width:24px;height:24px;color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:4px;font-size:13px;line-height:1}.NPLLIq_logPagerBtn:disabled{opacity:.4;cursor:default}.NPLLIq_logPagerPages{text-align:center;min-width:50px;color:var(--dsw-alias-label-secondary)}.NPLLIq_logPageSizeSelect{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);color:var(--dsw-alias-label-primary);cursor:pointer;border-radius:4px;padding:2px 4px;font-size:11px}.NPLLIq_dialogOverlay{z-index:100001;background:#0000004d;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.NPLLIq_logClearDialog{gap:0;width:320px;padding:16px}.NPLLIq_dialogBody{color:var(--dsw-alias-label-secondary);margin:6px 0 10px;font-size:12px;line-height:1.5}.NPLLIq_logClearDate{border:1px solid var(--dsw-alias-divider-1,#00000026);background:var(--dsw-alias-bg-layer-1,#0000000a);width:100%;color:var(--dsw-alias-label-primary);border-radius:6px;margin-bottom:12px;padding:6px 8px;font-size:13px}.NPLLIq_logClearDate:focus{border-color:var(--dsw-alias-accent-1,#4a9eff);outline:none}";
195
195
  const tagId = "@jxgame2020/dsh-token-quota/TokenQuotaPanel.module.css";
196
196
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
197
197
  const tag = document.createElement("style");
@@ -201,92 +201,92 @@ window.__ModuleLoader__.load({
201
201
  document.head.appendChild(tag);
202
202
  }
203
203
  var TokenQuotaPanel_module_css_default = {
204
- "bar": "NPLLIq_bar",
205
- "tab": "NPLLIq_tab",
206
- "rowHeader": "NPLLIq_rowHeader",
207
- "logTable": "NPLLIq_logTable",
208
- "logPagerInfo": "NPLLIq_logPagerInfo",
209
- "copyBtn": "NPLLIq_copyBtn",
210
- "dialogBody": "NPLLIq_dialogBody",
211
- "logClearDialog": "NPLLIq_logClearDialog",
212
- "input": "NPLLIq_input",
213
- "monitorHint": "NPLLIq_monitorHint",
214
- "monitorName": "NPLLIq_monitorName",
215
- "checkErrorHint": "NPLLIq_checkErrorHint",
216
- "fillWarn": "NPLLIq_fillWarn",
217
- "dialogBtn": "NPLLIq_dialogBtn",
204
+ "controls": "NPLLIq_controls",
218
205
  "logModelCol": "NPLLIq_logModelCol",
219
- "fullNotice": "NPLLIq_fullNotice",
220
- "logPagerPages": "NPLLIq_logPagerPages",
206
+ "resetRow": "NPLLIq_resetRow",
207
+ "noticeError": "NPLLIq_noticeError",
208
+ "selectBtn": "NPLLIq_selectBtn",
209
+ "monitorList": "NPLLIq_monitorList",
210
+ "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
211
+ "dialogBtn": "NPLLIq_dialogBtn",
212
+ "logDayCol": "NPLLIq_logDayCol",
213
+ "fillIdle": "NPLLIq_fillIdle",
214
+ "logPagerBtn": "NPLLIq_logPagerBtn",
215
+ "panel": "NPLLIq_panel",
221
216
  "subtitle": "NPLLIq_subtitle",
222
- "upgradeClose": "NPLLIq_upgradeClose",
217
+ "notice": "NPLLIq_notice",
218
+ "settingsBtnActive": "NPLLIq_settingsBtnActive",
219
+ "gearBtn": "NPLLIq_gearBtn",
220
+ "panelDimStrong": "NPLLIq_panelDimStrong",
223
221
  "logClearDate": "NPLLIq_logClearDate",
224
- "row": "NPLLIq_row",
225
- "logHeader": "NPLLIq_logHeader",
226
- "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
222
+ "settingsBtn": "NPLLIq_settingsBtn",
223
+ "copyBtn": "NPLLIq_copyBtn",
224
+ "logPager": "NPLLIq_logPager",
225
+ "tab": "NPLLIq_tab",
226
+ "tabIcon": "NPLLIq_tabIcon",
227
227
  "dialog": "NPLLIq_dialog",
228
- "upgradeHint": "NPLLIq_upgradeHint",
229
- "dialogHeader": "NPLLIq_dialogHeader",
228
+ "upgradeCommands": "NPLLIq_upgradeCommands",
230
229
  "rowMeta": "NPLLIq_rowMeta",
231
- "upgradeBannerText": "NPLLIq_upgradeBannerText",
232
- "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
233
- "dialogLabel": "NPLLIq_dialogLabel",
234
- "logPagerBtn": "NPLLIq_logPagerBtn",
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",
230
+ "version": "NPLLIq_version",
231
+ "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
246
232
  "dialogTitle": "NPLLIq_dialogTitle",
247
233
  "headerText": "NPLLIq_headerText",
248
- "collapse": "NPLLIq_collapse",
249
- "currentBadge": "NPLLIq_currentBadge",
250
- "panel": "NPLLIq_panel",
234
+ "dialogActions": "NPLLIq_dialogActions",
235
+ "logDialog": "NPLLIq_logDialog",
251
236
  "fillOver": "NPLLIq_fillOver",
252
- "settingsBtn": "NPLLIq_settingsBtn",
253
- "gearBtn": "NPLLIq_gearBtn",
254
- "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
237
+ "monitorRow": "NPLLIq_monitorRow",
238
+ "upgradeBanner": "NPLLIq_upgradeBanner",
255
239
  "dialogHint": "NPLLIq_dialogHint",
256
- "versionNew": "NPLLIq_versionNew",
257
- "save": "NPLLIq_save",
258
- "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
259
- "switch": "NPLLIq_switch",
240
+ "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
241
+ "logClearDialog": "NPLLIq_logClearDialog",
242
+ "logHeader": "NPLLIq_logHeader",
243
+ "upgradeHint": "NPLLIq_upgradeHint",
244
+ "row": "NPLLIq_row",
260
245
  "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
246
  "logSearch": "NPLLIq_logSearch",
247
+ "switch": "NPLLIq_switch",
271
248
  "checkBtn": "NPLLIq_checkBtn",
272
- "panelDimStrong": "NPLLIq_panelDimStrong",
273
- "monitorRow": "NPLLIq_monitorRow",
274
- "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
275
- "upgradeCommands": "NPLLIq_upgradeCommands",
276
- "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
277
- "logPager": "NPLLIq_logPager",
249
+ "dialogSection": "NPLLIq_dialogSection",
250
+ "bar": "NPLLIq_bar",
251
+ "logPagerPages": "NPLLIq_logPagerPages",
252
+ "input": "NPLLIq_input",
253
+ "rowHeader": "NPLLIq_rowHeader",
254
+ "copyBtnDone": "NPLLIq_copyBtnDone",
255
+ "dialogClose": "NPLLIq_dialogClose",
256
+ "logScroll": "NPLLIq_logScroll",
257
+ "versionNew": "NPLLIq_versionNew",
258
+ "header": "NPLLIq_header",
259
+ "upgradeCommand": "NPLLIq_upgradeCommand",
278
260
  "logPagerControls": "NPLLIq_logPagerControls",
279
- "logDialog": "NPLLIq_logDialog",
261
+ "currentBadge": "NPLLIq_currentBadge",
262
+ "tabLabel": "NPLLIq_tabLabel",
263
+ "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
264
+ "upgradeBannerText": "NPLLIq_upgradeBannerText",
265
+ "logClearBtn": "NPLLIq_logClearBtn",
266
+ "fullNotice": "NPLLIq_fullNotice",
267
+ "upgradeClose": "NPLLIq_upgradeClose",
268
+ "dialogBody": "NPLLIq_dialogBody",
269
+ "resetSelect": "NPLLIq_resetSelect",
280
270
  "rowName": "NPLLIq_rowName",
281
- "monitorList": "NPLLIq_monitorList",
282
- "resetRow": "NPLLIq_resetRow",
271
+ "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
272
+ "logUsedCol": "NPLLIq_logUsedCol",
273
+ "collapse": "NPLLIq_collapse",
274
+ "dialogOverlay": "NPLLIq_dialogOverlay",
275
+ "radioRow": "NPLLIq_radioRow",
276
+ "dialogHeader": "NPLLIq_dialogHeader",
283
277
  "title": "NPLLIq_title",
284
- "version": "NPLLIq_version",
285
- "tabLabel": "NPLLIq_tabLabel",
286
- "notice": "NPLLIq_notice",
287
- "upgradeCommand": "NPLLIq_upgradeCommand",
288
- "fillIdle": "NPLLIq_fillIdle",
289
- "controls": "NPLLIq_controls"
278
+ "logTable": "NPLLIq_logTable",
279
+ "save": "NPLLIq_save",
280
+ "checkErrorHint": "NPLLIq_checkErrorHint",
281
+ "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
282
+ "dialogLabel": "NPLLIq_dialogLabel",
283
+ "logPagerInfo": "NPLLIq_logPagerInfo",
284
+ "panelDim": "NPLLIq_panelDim",
285
+ "fillWarn": "NPLLIq_fillWarn",
286
+ "monitorHint": "NPLLIq_monitorHint",
287
+ "body": "NPLLIq_body",
288
+ "headerActions": "NPLLIq_headerActions",
289
+ "monitorName": "NPLLIq_monitorName"
290
290
  };
291
291
  //#endregion
292
292
  //#region lib/types/client/TokenQuotaPanel.js
@@ -592,13 +592,13 @@ window.__ModuleLoader__.load({
592
592
  title: t("upgradeAvailableHint"),
593
593
  children: [
594
594
  "v",
595
- "0.1.27",
595
+ "0.1.29",
596
596
  " → v",
597
597
  upgrade.latestVersion
598
598
  ]
599
599
  }) : (0, react_jsx_runtime.jsxs)("span", {
600
600
  className: TokenQuotaPanel_module_css_default.version,
601
- children: ["v", "0.1.27"]
601
+ children: ["v", "0.1.29"]
602
602
  })]
603
603
  }), (0, react_jsx_runtime.jsx)("div", {
604
604
  className: TokenQuotaPanel_module_css_default.subtitle,
@@ -612,12 +612,13 @@ window.__ModuleLoader__.load({
612
612
  children: [
613
613
  (0, react_jsx_runtime.jsx)("button", {
614
614
  type: "button",
615
+ "aria-pressed": musicEnabled,
615
616
  className: `${TokenQuotaPanel_module_css_default.settingsBtn}${musicEnabled ? ` ${TokenQuotaPanel_module_css_default.settingsBtnActive}` : ""}`,
616
- title: t("musicHint"),
617
+ title: `${t("musicLabel")}:${musicEnabled ? t("musicOn") : t("musicOff")}`,
617
618
  onClick: () => {
618
619
  setMusicEnabled(!musicEnabled);
619
620
  },
620
- children: musicEnabled ? "🔊" : "♪"
621
+ children: "♪"
621
622
  }),
622
623
  (0, react_jsx_runtime.jsx)("button", {
623
624
  type: "button",
@@ -1235,21 +1236,25 @@ window.__ModuleLoader__.load({
1235
1236
  * 古风 live-composed soundtrack driven by host actions.
1236
1237
  *
1237
1238
  * 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.
1239
+ * plucked-string voice (Web Audio synthesis; the MIDI output is switched to
1240
+ * the Koto program so hardware synths match). The piece plays phrase after
1241
+ * phrase so it stays alive while the model thinks or streams; incoming events
1242
+ * steer the piece instead of interrupting it: a step change rotates the mode
1243
+ * centre and inserts a fast 过门 (fill); tool calls get a high plink; the
1244
+ * ending gets a dizi-like breath note and a descending glissando.
1245
+ *
1246
+ * **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
1247
+ * or a fixed-duration breath note — there is no drone/pad oscillator that
1248
+ * depends on a later stop event. A lost SSE event can at worst leave the
1249
+ * phrase chain running, and the stream watchdog below stops that too, so a
1250
+ * hanging "呜呜" hum is physically impossible.
1245
1251
  *
1246
- * Anti-runaway guards (the "endless drone" / hanging-MIDI-note problem):
1252
+ * Anti-runaway guards:
1247
1253
  * - 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
1254
+ * engine stops (a dead stream means no stop event will ever come),
1255
+ * - every MIDI NoteOn is registered and swept; stops also send
1250
1256
  * All Notes Off (CC 123),
1251
- * - Web Audio ramps never target zero exponentially and every oscillator is
1252
- * explicitly stopped.
1257
+ * - every Web Audio oscillator is explicitly stopped at envelope end.
1253
1258
  *
1254
1259
  * @module @jxgame2020/dsh-token-quota/music
1255
1260
  */
@@ -1489,11 +1494,11 @@ window.__ModuleLoader__.load({
1489
1494
  playing = false;
1490
1495
  tonicIndex = 0;
1491
1496
  phraseIndex = 0;
1497
+ /** True when the next piece start should play the full intro glissando. */
1498
+ introPending = true;
1499
+ /** Session the current piece belongs to (turn/start carries it). */
1500
+ sessionId;
1492
1501
  phraseTimer;
1493
- droneOscs = [];
1494
- droneGain = null;
1495
- droneFilter = null;
1496
- droneActive = false;
1497
1502
  lastStreamActivity = 0;
1498
1503
  watchdogTimer;
1499
1504
  activeMidiNotes = /* @__PURE__ */ new Set();
@@ -1558,15 +1563,24 @@ window.__ModuleLoader__.load({
1558
1563
  markStreamActivity() {
1559
1564
  this.lastStreamActivity = Date.now();
1560
1565
  }
1566
+ /** Stop any playing piece — e.g. when the panel switches sessions. */
1567
+ stop() {
1568
+ this.stopPiece(false);
1569
+ }
1561
1570
  /** Handle one incoming host action and steer the piece. */
1562
1571
  onAction(action) {
1563
1572
  if (!this.enabled || this.audio === null) return;
1564
1573
  const base = TokenQuotaMusic.BASE_MIDI;
1565
1574
  switch (action.type) {
1566
1575
  case "turn/start":
1567
- this.stopPiece(false);
1576
+ if (this.sessionId !== void 0 && this.sessionId !== action.sessionId) this.stopPiece(false);
1577
+ this.sessionId = action.sessionId;
1578
+ this.stopMelody();
1579
+ this.playing = false;
1568
1580
  this.tonicIndex = 0;
1569
1581
  this.phraseIndex = 0;
1582
+ this.introPending = true;
1583
+ this.startPiece();
1570
1584
  break;
1571
1585
  case "request/header":
1572
1586
  if (!this.playing) this.startPiece();
@@ -1575,9 +1589,10 @@ window.__ModuleLoader__.load({
1575
1589
  case "step/start": {
1576
1590
  const prog = PROGRESSION[this.style];
1577
1591
  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);
1592
+ if (this.playing) {
1593
+ const newRoot = base + (prog[this.tonicIndex] ?? 0);
1594
+ this.insertFill(newRoot);
1595
+ }
1581
1596
  break;
1582
1597
  }
1583
1598
  case "tool/call":
@@ -1599,7 +1614,6 @@ window.__ModuleLoader__.load({
1599
1614
  setTimeout(() => this.pluck(base + 5, .4, .8), 220);
1600
1615
  break;
1601
1616
  case "assistant/message":
1602
- this.stopMelody();
1603
1617
  this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
1604
1618
  break;
1605
1619
  case "turn/end":
@@ -1607,29 +1621,36 @@ window.__ModuleLoader__.load({
1607
1621
  break;
1608
1622
  }
1609
1623
  }
1610
- /** Start a new piece: drone + ascending glissando + phrase chain. */
1624
+ /** Start a new piece: intro run, then the phrase chain. */
1611
1625
  startPiece() {
1612
1626
  if (!this.audio || this.playing) return;
1613
1627
  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);
1628
+ const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
1618
1629
  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;
1630
+ if (this.introPending) {
1631
+ this.introPending = false;
1632
+ let t = 0;
1633
+ for (let i = 0; i < 8; i++) {
1634
+ const note = root + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
1635
+ setTimeout(() => {
1636
+ if (this.playing) this.pluck(note, .3);
1637
+ }, t);
1638
+ t += 150;
1639
+ }
1640
+ this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
1641
+ } else {
1642
+ for (let i = 0; i < 3; i++) setTimeout(() => {
1643
+ if (this.playing) this.pluck(root + 12 + (scale[i] ?? 0), .3);
1644
+ }, i * 140);
1645
+ this.phraseTimer = setTimeout(() => this.playPhrase(), 480);
1625
1646
  }
1626
- this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
1627
1647
  }
1628
1648
  /** Play one phrase, then chain into the next after a one-beat rest. */
1629
1649
  playPhrase() {
1630
1650
  if (!this.playing || !this.audio) return;
1631
1651
  const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
1632
1652
  const scale = SCALES[this.style];
1653
+ this.pluck(root - 12, .28, 1.6);
1633
1654
  const phrase = PHRASES[this.phraseIndex % PHRASES.length];
1634
1655
  this.phraseIndex += 1;
1635
1656
  let t = 0;
@@ -1657,7 +1678,7 @@ window.__ModuleLoader__.load({
1657
1678
  }
1658
1679
  this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
1659
1680
  }
1660
- /** Stop the melody chain (drone keeps fading naturally). */
1681
+ /** Stop the phrase chain. */
1661
1682
  stopMelody() {
1662
1683
  if (this.phraseTimer !== void 0) {
1663
1684
  clearTimeout(this.phraseTimer);
@@ -1673,8 +1694,7 @@ window.__ModuleLoader__.load({
1673
1694
  const scale = SCALES[this.style];
1674
1695
  let t = 0;
1675
1696
  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);
1697
+ const note = base + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
1678
1698
  setTimeout(() => this.pluck(note, .3), t);
1679
1699
  t += 120;
1680
1700
  }
@@ -1685,86 +1705,16 @@ window.__ModuleLoader__.load({
1685
1705
  this.pluck(base - 12, .5);
1686
1706
  }, t + 150);
1687
1707
  }
1688
- this.fadeDrone(final ? 1.2 : .8);
1689
1708
  this.allMidiOff();
1690
1709
  }
1691
1710
  /** Hard stop everything (master switch off / teardown). */
1692
1711
  stopAll() {
1693
1712
  this.stopPiece(false);
1694
1713
  }
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;
1763
- }
1764
1714
  /**
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).
1715
+ * Guzheng-like pluck: a triangle voice plus a soft octave partial through a
1716
+ * sweeping lowpass, fast attack and a long exponential decay (notes
1717
+ * overlap, which keeps the piece continuous instead of choppy).
1768
1718
  */
1769
1719
  pluck(midi, velocity, decaySec = 1.3) {
1770
1720
  if (!this.audio) return;
@@ -1788,9 +1738,9 @@ window.__ModuleLoader__.load({
1788
1738
  osc1.frequency.value = freq;
1789
1739
  const osc2 = this.audio.createOscillator();
1790
1740
  osc2.type = "sine";
1791
- osc2.frequency.value = freq * 2.001;
1741
+ osc2.frequency.value = freq * 2;
1792
1742
  const octGain = this.audio.createGain();
1793
- octGain.gain.value = .35;
1743
+ octGain.gain.value = .25;
1794
1744
  osc1.connect(filter);
1795
1745
  osc2.connect(octGain);
1796
1746
  octGain.connect(filter);
@@ -1946,6 +1896,8 @@ window.__ModuleLoader__.load({
1946
1896
  dimWhenIdleHint: "鼠标移开面板时降低不透明度,输入文字时更透明;鼠标悬停恢复",
1947
1897
  musicLabel: "动作音乐",
1948
1898
  musicHint: "根据 Harness 动作实时作曲:请求开始、工具调用、回合结束等(需点击开关启动音频;检测到 MIDI 输出时同步发送 MIDI)",
1899
+ musicOn: "开",
1900
+ musicOff: "关",
1949
1901
  musicVolume: "音量",
1950
1902
  musicStyle: "风格",
1951
1903
  musicStyleMajor: "大调",
@@ -2022,6 +1974,8 @@ window.__ModuleLoader__.load({
2022
1974
  dimWhenIdleHint: "Fade the panel out when the pointer is away (more while typing); hover to restore",
2023
1975
  musicLabel: "Action music",
2024
1976
  musicHint: "Live-composes from Harness actions: request starts, tool calls, turn ends, etc. (click the switch to start audio; a detected MIDI output is driven as well)",
1977
+ musicOn: "On",
1978
+ musicOff: "Off",
2025
1979
  musicVolume: "Volume",
2026
1980
  musicStyle: "Style",
2027
1981
  musicStyleMajor: "Major",
@@ -2245,6 +2199,7 @@ window.__ModuleLoader__.load({
2245
2199
  };
2246
2200
  }, "token-quota-ui: snapshot poll + full strategy");
2247
2201
  const load = (sessionId) => {
2202
+ if (lastSessionId !== void 0 && lastSessionId !== sessionId) music.stop();
2248
2203
  lastSessionId = sessionId;
2249
2204
  bound?.setLoading(true);
2250
2205
  bound?.setError(null);
package/lib/index.js CHANGED
@@ -515,7 +515,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
515
515
  return this.upgradeCheck;
516
516
  }
517
517
  async doRefreshUpgrade() {
518
- const current = "0.1.27";
518
+ const current = "0.1.29";
519
519
  let latest;
520
520
  try {
521
521
  const response = await fetch(REGISTRY_LATEST_URL, {
@@ -298,7 +298,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
298
298
  : '';
299
299
  return (_jsxs("div", { ref: panelRef, className: `${css.panel}${panelDimClass !== '' ? ` ${panelDimClass}` : ''}`, style: panelPos !== null ? { left: panelPos.x, top: panelPos.y, right: 'auto' } : undefined, onPointerEnter: () => { setHovered(true); }, onPointerLeave: () => { setHovered(false); }, children: [_jsxs("div", { className: css.header, onPointerDown: (event) => { beginDrag(event, panelRef.current, applyPanelPos); }, children: [_jsxs("div", { className: css.headerText, children: [_jsxs("div", { className: css.title, children: [t('title'), __TOKEN_QUOTA_VERSION__ !== undefined && __TOKEN_QUOTA_VERSION__ !== '' && (upgrade !== null
300
300
  ? (_jsxs("span", { className: `${css.version} ${css.versionNew}`, title: t('upgradeAvailableHint'), children: ["v", __TOKEN_QUOTA_VERSION__, " \u2192 v", upgrade.latestVersion] }))
301
- : (_jsxs("span", { className: css.version, children: ["v", __TOKEN_QUOTA_VERSION__] })))] }), _jsx("div", { className: css.subtitle, children: t('subtitle') })] }), _jsxs("div", { className: css.headerActions, onPointerDown: (event) => { event.stopPropagation(); }, children: [_jsx("button", { type: "button", className: `${css.settingsBtn}${musicEnabled ? ` ${css.settingsBtnActive}` : ''}`, title: t('musicHint'), onClick: () => { setMusicEnabled(!musicEnabled); }, children: musicEnabled ? '🔊' : '♪' }), _jsx("button", { type: "button", className: css.settingsBtn, onClick: () => { actions.setLogOpen(true); }, children: t('logs') }), _jsx("button", { type: "button", className: css.settingsBtn, onClick: openSettings, children: t('settings') }), _jsx("button", { type: "button", className: css.collapse, onClick: () => { setCollapsed(true); }, children: t('collapse') })] })] }), _jsxs("div", { className: css.body, children: [loading && _jsx("div", { className: css.notice, children: t('loading') }), error !== null && _jsx("div", { className: css.noticeError, children: error }), fullNotice !== null && (_jsx("div", { className: css.fullNotice, role: "alert", children: fullNotice })), upgrade !== null && !upgradeDismissed && (_jsxs("div", { className: `${css.upgradeBanner}${upgradeFlash ? ` ${css.upgradeBannerFlash}` : ''}`, role: "alert", children: [_jsx("div", { className: css.upgradeBannerText, children: t('upgradeAvailable').replace('{version}', upgrade.latestVersion) }), _jsx("div", { className: css.upgradeCommands, children: upgrade.commands.map(cmd => (_jsxs("div", { className: css.upgradeCommandRow, children: [_jsx("code", { className: css.upgradeCommand, children: cmd }), _jsx("button", { type: "button", className: `${css.copyBtn}${copiedCmd === cmd ? ` ${css.copyBtnDone}` : ''}`, onClick: () => { copyUpgradeCommand(cmd); }, children: copiedCmd === cmd ? t('copied') : t('copy') })] }, cmd))) }), _jsx("div", { className: css.upgradeHint, children: t('upgradeHint') }), _jsx("button", { type: "button", className: css.upgradeClose, onClick: () => { actions.setUpgradeDismissed(true); }, "aria-label": t('close'), children: "\u00D7" })] })), !loading && error === null && visibleRows.length === 0 && (_jsx("div", { className: css.notice, children: monitored !== null && monitored.length === 0 ? t('noMonitored') : t('waiting') })), visibleRows.map((row) => {
301
+ : (_jsxs("span", { className: css.version, children: ["v", __TOKEN_QUOTA_VERSION__] })))] }), _jsx("div", { className: css.subtitle, children: t('subtitle') })] }), _jsxs("div", { className: css.headerActions, onPointerDown: (event) => { event.stopPropagation(); }, children: [_jsx("button", { type: "button", "aria-pressed": musicEnabled, className: `${css.settingsBtn}${musicEnabled ? ` ${css.settingsBtnActive}` : ''}`, title: `${t('musicLabel')}:${musicEnabled ? t('musicOn') : t('musicOff')}`, onClick: () => { setMusicEnabled(!musicEnabled); }, children: "\u266A" }), _jsx("button", { type: "button", className: css.settingsBtn, onClick: () => { actions.setLogOpen(true); }, children: t('logs') }), _jsx("button", { type: "button", className: css.settingsBtn, onClick: openSettings, children: t('settings') }), _jsx("button", { type: "button", className: css.collapse, onClick: () => { setCollapsed(true); }, children: t('collapse') })] })] }), _jsxs("div", { className: css.body, children: [loading && _jsx("div", { className: css.notice, children: t('loading') }), error !== null && _jsx("div", { className: css.noticeError, children: error }), fullNotice !== null && (_jsx("div", { className: css.fullNotice, role: "alert", children: fullNotice })), upgrade !== null && !upgradeDismissed && (_jsxs("div", { className: `${css.upgradeBanner}${upgradeFlash ? ` ${css.upgradeBannerFlash}` : ''}`, role: "alert", children: [_jsx("div", { className: css.upgradeBannerText, children: t('upgradeAvailable').replace('{version}', upgrade.latestVersion) }), _jsx("div", { className: css.upgradeCommands, children: upgrade.commands.map(cmd => (_jsxs("div", { className: css.upgradeCommandRow, children: [_jsx("code", { className: css.upgradeCommand, children: cmd }), _jsx("button", { type: "button", className: `${css.copyBtn}${copiedCmd === cmd ? ` ${css.copyBtnDone}` : ''}`, onClick: () => { copyUpgradeCommand(cmd); }, children: copiedCmd === cmd ? t('copied') : t('copy') })] }, cmd))) }), _jsx("div", { className: css.upgradeHint, children: t('upgradeHint') }), _jsx("button", { type: "button", className: css.upgradeClose, onClick: () => { actions.setUpgradeDismissed(true); }, "aria-label": t('close'), children: "\u00D7" })] })), !loading && error === null && visibleRows.length === 0 && (_jsx("div", { className: css.notice, children: monitored !== null && monitored.length === 0 ? t('noMonitored') : t('waiting') })), visibleRows.map((row) => {
302
302
  const state = barStateOf(row);
303
303
  const pct = row.limit > 0 ? Math.min(100, Math.round((row.used / row.limit) * 100)) : 0;
304
304
  const barClass = state === 'over'
@@ -262,6 +262,11 @@ export function apply(ctx) {
262
262
  return () => { clearInterval(timer); };
263
263
  }, 'token-quota-ui: snapshot poll + full strategy');
264
264
  const load = (sessionId) => {
265
+ // Switching the panel to another session: stop the previous session's
266
+ // soundtrack — while scoped to the current session its stop events are
267
+ // filtered out, so without this it could play forever.
268
+ if (lastSessionId !== undefined && lastSessionId !== sessionId)
269
+ music.stop();
265
270
  lastSessionId = sessionId;
266
271
  bound?.setLoading(true);
267
272
  bound?.setError(null);
@@ -68,6 +68,8 @@ export declare const zh: {
68
68
  dimWhenIdleHint: string;
69
69
  musicLabel: string;
70
70
  musicHint: string;
71
+ musicOn: string;
72
+ musicOff: string;
71
73
  musicVolume: string;
72
74
  musicStyle: string;
73
75
  musicStyleMajor: string;
@@ -146,6 +148,8 @@ export declare const en: {
146
148
  dimWhenIdleHint: string;
147
149
  musicLabel: string;
148
150
  musicHint: string;
151
+ musicOn: string;
152
+ musicOff: string;
149
153
  musicVolume: string;
150
154
  musicStyle: string;
151
155
  musicStyleMajor: string;
@@ -68,6 +68,8 @@ export const zh = {
68
68
  dimWhenIdleHint: '鼠标移开面板时降低不透明度,输入文字时更透明;鼠标悬停恢复',
69
69
  musicLabel: '动作音乐',
70
70
  musicHint: '根据 Harness 动作实时作曲:请求开始、工具调用、回合结束等(需点击开关启动音频;检测到 MIDI 输出时同步发送 MIDI)',
71
+ musicOn: '开',
72
+ musicOff: '关',
71
73
  musicVolume: '音量',
72
74
  musicStyle: '风格',
73
75
  musicStyleMajor: '大调',
@@ -144,6 +146,8 @@ export const en = {
144
146
  dimWhenIdleHint: 'Fade the panel out when the pointer is away (more while typing); hover to restore',
145
147
  musicLabel: 'Action music',
146
148
  musicHint: 'Live-composes from Harness actions: request starts, tool calls, turn ends, etc. (click the switch to start audio; a detected MIDI output is driven as well)',
149
+ musicOn: 'On',
150
+ musicOff: 'Off',
147
151
  musicVolume: 'Volume',
148
152
  musicStyle: 'Style',
149
153
  musicStyleMajor: 'Major',
@@ -2,21 +2,25 @@
2
2
  * 古风 live-composed soundtrack driven by host actions.
3
3
  *
4
4
  * Design: when a request starts the engine composes a *piece* — a guzheng-like
5
- * plucked-string voice over a soft open-fifth drone (Web Audio synthesis; the
6
- * MIDI output is switched to the Koto program so hardware synths match). The
7
- * piece plays phrase after phrase so it never goes quiet while the model
8
- * thinks or streams. Incoming events steer the piece instead of interrupting
9
- * it: a step change glides the drone to the next mode centre and inserts a
10
- * fast 过门 (fill); tool calls get a high plink; the ending gets a flute-like
11
- * breath note and a descending glissando.
5
+ * plucked-string voice (Web Audio synthesis; the MIDI output is switched to
6
+ * the Koto program so hardware synths match). The piece plays phrase after
7
+ * phrase so it stays alive while the model thinks or streams; incoming events
8
+ * steer the piece instead of interrupting it: a step change rotates the mode
9
+ * centre and inserts a fast 过门 (fill); tool calls get a high plink; the
10
+ * ending gets a dizi-like breath note and a descending glissando.
12
11
  *
13
- * Anti-runaway guards (the "endless drone" / hanging-MIDI-note problem):
12
+ * **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
13
+ * or a fixed-duration breath note — there is no drone/pad oscillator that
14
+ * depends on a later stop event. A lost SSE event can at worst leave the
15
+ * phrase chain running, and the stream watchdog below stops that too, so a
16
+ * hanging "呜呜" hum is physically impossible.
17
+ *
18
+ * Anti-runaway guards:
14
19
  * - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
15
- * engine fades out (a dead stream means no stop event will ever come),
16
- * - every MIDI NoteOn is registered and swept; `stopAll` also sends
20
+ * engine stops (a dead stream means no stop event will ever come),
21
+ * - every MIDI NoteOn is registered and swept; stops also send
17
22
  * All Notes Off (CC 123),
18
- * - Web Audio ramps never target zero exponentially and every oscillator is
19
- * explicitly stopped.
23
+ * - every Web Audio oscillator is explicitly stopped at envelope end.
20
24
  *
21
25
  * @module @jxgame2020/dsh-token-quota/music
22
26
  */
@@ -36,11 +40,11 @@ export declare class TokenQuotaMusic {
36
40
  private playing;
37
41
  private tonicIndex;
38
42
  private phraseIndex;
43
+ /** True when the next piece start should play the full intro glissando. */
44
+ private introPending;
45
+ /** Session the current piece belongs to (turn/start carries it). */
46
+ private sessionId;
39
47
  private phraseTimer;
40
- private droneOscs;
41
- private droneGain;
42
- private droneFilter;
43
- private droneActive;
44
48
  private lastStreamActivity;
45
49
  private watchdogTimer;
46
50
  private readonly activeMidiNotes;
@@ -62,30 +66,26 @@ export declare class TokenQuotaMusic {
62
66
  setStyle(value: TokenQuotaMusicStyle): void;
63
67
  /** Feed the engine one SSE activity timestamp (action or heartbeat). */
64
68
  markStreamActivity(): void;
69
+ /** Stop any playing piece — e.g. when the panel switches sessions. */
70
+ stop(): void;
65
71
  /** Handle one incoming host action and steer the piece. */
66
72
  onAction(action: TokenQuotaMusicAction): void;
67
- /** Start a new piece: drone + ascending glissando + phrase chain. */
73
+ /** Start a new piece: intro run, then the phrase chain. */
68
74
  private startPiece;
69
75
  /** Play one phrase, then chain into the next after a one-beat rest. */
70
76
  private playPhrase;
71
77
  /** Insert a 过门 fill: pause the chain, play the run, resume after. */
72
78
  private insertFill;
73
- /** Stop the melody chain (drone keeps fading naturally). */
79
+ /** Stop the phrase chain. */
74
80
  private stopMelody;
75
81
  /** Stop the whole piece; `final` adds the closing cadence gestures. */
76
82
  private stopPiece;
77
83
  /** Hard stop everything (master switch off / teardown). */
78
84
  private stopAll;
79
- /** Start the sustained open fifth (root + fifth below octave root). */
80
- private startDrone;
81
- /** Glide the drone to a new tonic (mode rotation). */
82
- private glideDrone;
83
- /** Fade the drone out and stop its oscillators. */
84
- private fadeDrone;
85
85
  /**
86
- * Guzheng-like pluck: two slightly detuned oscillators through a sweeping
87
- * lowpass, fast attack and a long exponential decay (notes overlap, which
88
- * keeps the piece continuous instead of choppy).
86
+ * Guzheng-like pluck: a triangle voice plus a soft octave partial through a
87
+ * sweeping lowpass, fast attack and a long exponential decay (notes
88
+ * overlap, which keeps the piece continuous instead of choppy).
89
89
  */
90
90
  private pluck;
91
91
  /** Dizi-like breath note: sine with vibrato and a soft envelope. */
@@ -2,21 +2,25 @@
2
2
  * 古风 live-composed soundtrack driven by host actions.
3
3
  *
4
4
  * Design: when a request starts the engine composes a *piece* — a guzheng-like
5
- * plucked-string voice over a soft open-fifth drone (Web Audio synthesis; the
6
- * MIDI output is switched to the Koto program so hardware synths match). The
7
- * piece plays phrase after phrase so it never goes quiet while the model
8
- * thinks or streams. Incoming events steer the piece instead of interrupting
9
- * it: a step change glides the drone to the next mode centre and inserts a
10
- * fast 过门 (fill); tool calls get a high plink; the ending gets a flute-like
11
- * breath note and a descending glissando.
5
+ * plucked-string voice (Web Audio synthesis; the MIDI output is switched to
6
+ * the Koto program so hardware synths match). The piece plays phrase after
7
+ * phrase so it stays alive while the model thinks or streams; incoming events
8
+ * steer the piece instead of interrupting it: a step change rotates the mode
9
+ * centre and inserts a fast 过门 (fill); tool calls get a high plink; the
10
+ * ending gets a dizi-like breath note and a descending glissando.
12
11
  *
13
- * Anti-runaway guards (the "endless drone" / hanging-MIDI-note problem):
12
+ * **No sustained tones by design.** Every voice is a decaying pluck (≤1.6s)
13
+ * or a fixed-duration breath note — there is no drone/pad oscillator that
14
+ * depends on a later stop event. A lost SSE event can at worst leave the
15
+ * phrase chain running, and the stream watchdog below stops that too, so a
16
+ * hanging "呜呜" hum is physically impossible.
17
+ *
18
+ * Anti-runaway guards:
14
19
  * - the host sends an SSE heartbeat every 5s; if it stops arriving the whole
15
- * engine fades out (a dead stream means no stop event will ever come),
16
- * - every MIDI NoteOn is registered and swept; `stopAll` also sends
20
+ * engine stops (a dead stream means no stop event will ever come),
21
+ * - every MIDI NoteOn is registered and swept; stops also send
17
22
  * All Notes Off (CC 123),
18
- * - Web Audio ramps never target zero exponentially and every oscillator is
19
- * explicitly stopped.
23
+ * - every Web Audio oscillator is explicitly stopped at envelope end.
20
24
  *
21
25
  * @module @jxgame2020/dsh-token-quota/music
22
26
  */
@@ -74,12 +78,11 @@ export class TokenQuotaMusic {
74
78
  playing = false;
75
79
  tonicIndex = 0;
76
80
  phraseIndex = 0;
81
+ /** True when the next piece start should play the full intro glissando. */
82
+ introPending = true;
83
+ /** Session the current piece belongs to (turn/start carries it). */
84
+ sessionId;
77
85
  phraseTimer;
78
- // Drone layer (sustained open fifth under the plucks)
79
- droneOscs = [];
80
- droneGain = null;
81
- droneFilter = null;
82
- droneActive = false;
83
86
  // Watchdog: host heartbeat freshness (ms timestamp)
84
87
  lastStreamActivity = 0;
85
88
  watchdogTimer;
@@ -138,7 +141,7 @@ export class TokenQuotaMusic {
138
141
  this.lastStreamActivity = Date.now();
139
142
  this.watchdogTimer = setInterval(() => {
140
143
  if (this.playing && Date.now() - this.lastStreamActivity > TokenQuotaMusic.STREAM_TIMEOUT_MS) {
141
- // Dead stream: no stop event will ever arrive — fade out.
144
+ // Dead stream: no stop event will ever arrive — stop the piece.
142
145
  this.stopPiece(false);
143
146
  }
144
147
  }, 4_000);
@@ -160,6 +163,10 @@ export class TokenQuotaMusic {
160
163
  markStreamActivity() {
161
164
  this.lastStreamActivity = Date.now();
162
165
  }
166
+ /** Stop any playing piece — e.g. when the panel switches sessions. */
167
+ stop() {
168
+ this.stopPiece(false);
169
+ }
163
170
  /** Handle one incoming host action and steer the piece. */
164
171
  onAction(action) {
165
172
  if (!this.enabled || this.audio === null)
@@ -167,25 +174,37 @@ export class TokenQuotaMusic {
167
174
  const base = TokenQuotaMusic.BASE_MIDI;
168
175
  switch (action.type) {
169
176
  case 'turn/start':
170
- this.stopPiece(false);
177
+ // A new turn = the user sent a message: begin a fresh piece right
178
+ // away (request/header only fires on the first request of a session
179
+ // or on model changes — it cannot be the music's start trigger).
180
+ // If another session's music was still playing, stop it first.
181
+ if (this.sessionId !== undefined && this.sessionId !== action.sessionId) {
182
+ this.stopPiece(false);
183
+ }
184
+ this.sessionId = action.sessionId;
185
+ this.stopMelody();
186
+ this.playing = false;
171
187
  this.tonicIndex = 0;
172
188
  this.phraseIndex = 0;
189
+ this.introPending = true;
190
+ this.startPiece();
173
191
  break;
174
192
  case 'request/header': {
193
+ // Safety net (and a light "request fired" accent when already playing).
175
194
  if (!this.playing)
176
195
  this.startPiece();
177
196
  else
178
- this.pluck(base + 24, 0.3); // light accent on a subsequent request
197
+ this.pluck(base + 24, 0.3);
179
198
  break;
180
199
  }
181
200
  case 'step/start': {
182
- // New phase: rotate the tonic, glide the drone, insert a 过门 fill.
201
+ // New phase: rotate the mode centre and insert a 过门 fill.
183
202
  const prog = PROGRESSION[this.style];
184
203
  this.tonicIndex = (this.tonicIndex + 1) % prog.length;
185
- const newRoot = base + (prog[this.tonicIndex] ?? 0);
186
- this.glideDrone(newRoot);
187
- if (this.playing)
204
+ if (this.playing) {
205
+ const newRoot = base + (prog[this.tonicIndex] ?? 0);
188
206
  this.insertFill(newRoot);
207
+ }
189
208
  break;
190
209
  }
191
210
  case 'tool/call':
@@ -211,9 +230,9 @@ export class TokenQuotaMusic {
211
230
  setTimeout(() => this.pluck(base + 5, 0.4, 0.8), 220);
212
231
  break;
213
232
  case 'assistant/message':
214
- // Phrase ends; the turn may continue (more steps) — keep the drone
215
- // breathing but stop the melody, ending with a breath note.
216
- this.stopMelody();
233
+ // Sentence end: a soft breath cadence, but the bed keeps playing
234
+ // until the turn fully ends (turn/end), so thinking + streaming stay
235
+ // audible the whole way through.
217
236
  this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
218
237
  break;
219
238
  case 'turn/end':
@@ -222,27 +241,37 @@ export class TokenQuotaMusic {
222
241
  }
223
242
  }
224
243
  // ── piece lifecycle ───────────────────────────────────────────────────
225
- /** Start a new piece: drone + ascending glissando + phrase chain. */
244
+ /** Start a new piece: intro run, then the phrase chain. */
226
245
  startPiece() {
227
246
  if (!this.audio || this.playing)
228
247
  return;
229
248
  this.playing = true;
230
- this.tonicIndex = 0;
231
- this.phraseIndex = 0;
232
- const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][0] ?? 0);
233
- this.startDrone(root);
234
- // Intro glissando — the guzheng sweep that announces the piece.
249
+ const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
235
250
  const scale = SCALES[this.style];
236
- let t = 0;
237
- for (let i = 0; i < 8; i++) {
238
- const deg = i % scale.length;
239
- const oct = Math.floor(i / scale.length);
240
- const note = root + 12 * oct + (scale[deg] ?? 0);
241
- setTimeout(() => this.pluck(note, 0.3), t);
242
- t += 150;
251
+ if (this.introPending) {
252
+ // Full intro: the ascending guzheng glissando that announces the piece.
253
+ this.introPending = false;
254
+ let t = 0;
255
+ for (let i = 0; i < 8; i++) {
256
+ const note = root + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
257
+ setTimeout(() => {
258
+ if (this.playing)
259
+ this.pluck(note, 0.3);
260
+ }, t);
261
+ t += 150;
262
+ }
263
+ this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
264
+ }
265
+ else {
266
+ // Restart within the same turn: a short three-note pickup, no glissando.
267
+ for (let i = 0; i < 3; i++) {
268
+ setTimeout(() => {
269
+ if (this.playing)
270
+ this.pluck(root + 12 + (scale[i] ?? 0), 0.3);
271
+ }, i * 140);
272
+ }
273
+ this.phraseTimer = setTimeout(() => this.playPhrase(), 480);
243
274
  }
244
- // First phrase starts right after the glissando.
245
- this.phraseTimer = setTimeout(() => this.playPhrase(), t + 100);
246
275
  }
247
276
  /** Play one phrase, then chain into the next after a one-beat rest. */
248
277
  playPhrase() {
@@ -250,6 +279,9 @@ export class TokenQuotaMusic {
250
279
  return;
251
280
  const root = TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
252
281
  const scale = SCALES[this.style];
282
+ // Soft low root pluck under each phrase — a foundation that decays away
283
+ // instead of a sustained drone (nothing here can hang).
284
+ this.pluck(root - 12, 0.28, 1.6);
253
285
  const phrase = PHRASES[this.phraseIndex % PHRASES.length];
254
286
  this.phraseIndex += 1;
255
287
  let t = 0;
@@ -282,7 +314,7 @@ export class TokenQuotaMusic {
282
314
  }
283
315
  this.phraseTimer = setTimeout(() => this.playPhrase(), t + 60);
284
316
  }
285
- /** Stop the melody chain (drone keeps fading naturally). */
317
+ /** Stop the phrase chain. */
286
318
  stopMelody() {
287
319
  if (this.phraseTimer !== undefined) {
288
320
  clearTimeout(this.phraseTimer);
@@ -299,9 +331,7 @@ export class TokenQuotaMusic {
299
331
  const scale = SCALES[this.style];
300
332
  let t = 0;
301
333
  for (let i = 7; i >= 0; i--) {
302
- const deg = i % scale.length;
303
- const oct = Math.floor(i / scale.length);
304
- const note = base + 12 * oct + (scale[deg] ?? 0);
334
+ const note = base + 12 * Math.floor(i / scale.length) + (scale[i % scale.length] ?? 0);
305
335
  setTimeout(() => this.pluck(note, 0.3), t);
306
336
  t += 120;
307
337
  }
@@ -312,95 +342,17 @@ export class TokenQuotaMusic {
312
342
  this.pluck(base - 12, 0.5);
313
343
  }, t + 150);
314
344
  }
315
- this.fadeDrone(final ? 1.2 : 0.8);
316
345
  this.allMidiOff();
317
346
  }
318
347
  /** Hard stop everything (master switch off / teardown). */
319
348
  stopAll() {
320
349
  this.stopPiece(false);
321
350
  }
322
- // ── drone layer ───────────────────────────────────────────────────────
323
- /** Start the sustained open fifth (root + fifth below octave root). */
324
- startDrone(root) {
325
- if (!this.audio || this.droneActive)
326
- return;
327
- const now = this.audio.currentTime;
328
- const gain = this.audio.createGain();
329
- gain.gain.setValueAtTime(0, now);
330
- const filter = this.audio.createBiquadFilter();
331
- filter.type = 'lowpass';
332
- filter.frequency.value = 620;
333
- filter.Q.value = 0.4;
334
- gain.connect(filter);
335
- filter.connect(this.audio.destination);
336
- const oscs = [];
337
- for (const interval of [-12, -5]) {
338
- const osc = this.audio.createOscillator();
339
- osc.type = 'sine';
340
- osc.frequency.value = midiToFreq(root + interval);
341
- osc.connect(gain);
342
- osc.start(now);
343
- oscs.push(osc);
344
- }
345
- this.droneOscs = oscs;
346
- this.droneGain = gain;
347
- this.droneFilter = filter;
348
- this.droneActive = true;
349
- gain.gain.linearRampToValueAtTime(this.volume * 0.07, now + 1.4);
350
- }
351
- /** Glide the drone to a new tonic (mode rotation). */
352
- glideDrone(root) {
353
- if (!this.audio || !this.droneActive)
354
- return;
355
- const now = this.audio.currentTime;
356
- const targets = [root - 12, root - 5];
357
- this.droneOscs.forEach((osc, i) => {
358
- const target = targets[i] ?? root;
359
- osc.frequency.setValueAtTime(osc.frequency.value, now);
360
- osc.frequency.exponentialRampToValueAtTime(midiToFreq(target), now + 0.9);
361
- });
362
- }
363
- /** Fade the drone out and stop its oscillators. */
364
- fadeDrone(seconds) {
365
- if (!this.audio || !this.droneActive || this.droneGain === null)
366
- return;
367
- const now = this.audio.currentTime;
368
- const gain = this.droneGain;
369
- gain.gain.cancelScheduledValues(now);
370
- gain.gain.setValueAtTime(Math.max(gain.gain.value, 0.0001), now);
371
- gain.gain.exponentialRampToValueAtTime(0.0001, now + seconds);
372
- const oscs = this.droneOscs;
373
- const filter = this.droneFilter;
374
- setTimeout(() => {
375
- for (const osc of oscs) {
376
- try {
377
- osc.stop();
378
- }
379
- catch { /* ignore */ }
380
- try {
381
- osc.disconnect();
382
- }
383
- catch { /* ignore */ }
384
- }
385
- try {
386
- filter?.disconnect();
387
- }
388
- catch { /* ignore */ }
389
- try {
390
- gain.disconnect();
391
- }
392
- catch { /* ignore */ }
393
- }, seconds * 1000 + 120);
394
- this.droneActive = false;
395
- this.droneOscs = [];
396
- this.droneGain = null;
397
- this.droneFilter = null;
398
- }
399
351
  // ── voices ────────────────────────────────────────────────────────────
400
352
  /**
401
- * Guzheng-like pluck: two slightly detuned oscillators through a sweeping
402
- * lowpass, fast attack and a long exponential decay (notes overlap, which
403
- * keeps the piece continuous instead of choppy).
353
+ * Guzheng-like pluck: a triangle voice plus a soft octave partial through a
354
+ * sweeping lowpass, fast attack and a long exponential decay (notes
355
+ * overlap, which keeps the piece continuous instead of choppy).
404
356
  */
405
357
  pluck(midi, velocity, decaySec = 1.3) {
406
358
  if (!this.audio)
@@ -426,9 +378,9 @@ export class TokenQuotaMusic {
426
378
  osc1.frequency.value = freq;
427
379
  const osc2 = this.audio.createOscillator();
428
380
  osc2.type = 'sine';
429
- osc2.frequency.value = freq * 2.001; // shimmering octave partial
381
+ osc2.frequency.value = freq * 2; // octave partial (no detune beating)
430
382
  const octGain = this.audio.createGain();
431
- octGain.gain.value = 0.35;
383
+ octGain.gain.value = 0.25;
432
384
  osc1.connect(filter);
433
385
  osc2.connect(octGain);
434
386
  octGain.connect(filter);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jxgame2020/dsh-token-quota",
3
3
  "description": "Daily per-model token quota accounting, enforcement, and a floating quota panel for the DeepSeek Harness — a fully self-contained plugin (no core-Harness wiring, no generated RPC contract)",
4
- "version": "0.1.27",
4
+ "version": "0.1.29",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },