@jxgame2020/dsh-token-quota 0.1.28 → 0.1.30

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.28" ? upgrade : null;
98
+ const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.30" ? 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:430px;max-width:94vw;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_dialogTabs{border-bottom:1px solid var(--dsw-alias-divider-1,#00000014);gap:2px;margin:-6px -14px 0;padding:0 14px;display:flex}.NPLLIq_dialogTab{appearance:none;color:var(--dsw-alias-label-secondary);cursor:pointer;background:0 0;border:none;border-bottom:2px solid #0000;padding:6px 12px 8px;font-size:12px}.NPLLIq_dialogTab:hover{color:var(--dsw-alias-label-primary)}.NPLLIq_dialogTabActive{color:var(--dsw-alias-brand-primary);border-bottom-color:var(--dsw-alias-brand-primary);font-weight:600}.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,95 @@ window.__ModuleLoader__.load({
201
201
  document.head.appendChild(tag);
202
202
  }
203
203
  var TokenQuotaPanel_module_css_default = {
204
+ "controls": "NPLLIq_controls",
205
+ "logHeader": "NPLLIq_logHeader",
206
+ "logModelCol": "NPLLIq_logModelCol",
204
207
  "gearBtn": "NPLLIq_gearBtn",
205
- "title": "NPLLIq_title",
206
- "checkErrorHint": "NPLLIq_checkErrorHint",
207
- "collapse": "NPLLIq_collapse",
208
+ "monitorHint": "NPLLIq_monitorHint",
209
+ "bar": "NPLLIq_bar",
210
+ "resetSelect": "NPLLIq_resetSelect",
211
+ "dialogTab": "NPLLIq_dialogTab",
212
+ "dialogTabActive": "NPLLIq_dialogTabActive",
213
+ "logPagerInfo": "NPLLIq_logPagerInfo",
214
+ "radioRow": "NPLLIq_radioRow",
215
+ "header": "NPLLIq_header",
216
+ "rowHeader": "NPLLIq_rowHeader",
217
+ "input": "NPLLIq_input",
218
+ "settingsBtnActive": "NPLLIq_settingsBtnActive",
219
+ "settingsBtn": "NPLLIq_settingsBtn",
220
+ "subtitle": "NPLLIq_subtitle",
221
+ "copyBtnDone": "NPLLIq_copyBtnDone",
222
+ "checkBtn": "NPLLIq_checkBtn",
208
223
  "dialogTitle": "NPLLIq_dialogTitle",
224
+ "monitorRow": "NPLLIq_monitorRow",
225
+ "logScroll": "NPLLIq_logScroll",
226
+ "dialogTabs": "NPLLIq_dialogTabs",
227
+ "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
228
+ "checkErrorHint": "NPLLIq_checkErrorHint",
229
+ "logClearDate": "NPLLIq_logClearDate",
230
+ "dialogBtn": "NPLLIq_dialogBtn",
231
+ "upgradeCommand": "NPLLIq_upgradeCommand",
209
232
  "logDayCol": "NPLLIq_logDayCol",
233
+ "resetRow": "NPLLIq_resetRow",
234
+ "logPager": "NPLLIq_logPager",
235
+ "collapse": "NPLLIq_collapse",
236
+ "upgradeHint": "NPLLIq_upgradeHint",
237
+ "logClearBtn": "NPLLIq_logClearBtn",
238
+ "version": "NPLLIq_version",
239
+ "upgradeCommands": "NPLLIq_upgradeCommands",
240
+ "notice": "NPLLIq_notice",
210
241
  "upgradeBannerText": "NPLLIq_upgradeBannerText",
211
- "fillOver": "NPLLIq_fillOver",
212
- "monitorHint": "NPLLIq_monitorHint",
242
+ "monitorList": "NPLLIq_monitorList",
213
243
  "logClearDialog": "NPLLIq_logClearDialog",
244
+ "headerText": "NPLLIq_headerText",
245
+ "fullNotice": "NPLLIq_fullNotice",
246
+ "dialogActions": "NPLLIq_dialogActions",
247
+ "row": "NPLLIq_row",
248
+ "dialogHeader": "NPLLIq_dialogHeader",
249
+ "title": "NPLLIq_title",
250
+ "selectBtn": "NPLLIq_selectBtn",
251
+ "tabIcon": "NPLLIq_tabIcon",
252
+ "fillWarn": "NPLLIq_fillWarn",
253
+ "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
254
+ "dialogLabel": "NPLLIq_dialogLabel",
255
+ "panelDimStrong": "NPLLIq_panelDimStrong",
214
256
  "dialogClose": "NPLLIq_dialogClose",
215
- "monitorName": "NPLLIq_monitorName",
257
+ "switch": "NPLLIq_switch",
258
+ "logToolbar": "NPLLIq_logToolbar",
216
259
  "dialog": "NPLLIq_dialog",
217
- "logClearBtn": "NPLLIq_logClearBtn",
218
- "rowMeta": "NPLLIq_rowMeta",
260
+ "tabLabel": "NPLLIq_tabLabel",
261
+ "currentBadge": "NPLLIq_currentBadge",
262
+ "copyBtn": "NPLLIq_copyBtn",
263
+ "logDialog": "NPLLIq_logDialog",
219
264
  "logSearch": "NPLLIq_logSearch",
220
- "selectBtn": "NPLLIq_selectBtn",
265
+ "dialogOverlay": "NPLLIq_dialogOverlay",
266
+ "noticeError": "NPLLIq_noticeError",
267
+ "headerActions": "NPLLIq_headerActions",
221
268
  "panelDim": "NPLLIq_panelDim",
222
- "upgradeBanner": "NPLLIq_upgradeBanner",
223
- "rowHeader": "NPLLIq_rowHeader",
224
- "checkBtn": "NPLLIq_checkBtn",
225
- "dialogHint": "NPLLIq_dialogHint",
226
- "logPagerInfo": "NPLLIq_logPagerInfo",
227
- "logModelCol": "NPLLIq_logModelCol",
228
- "upgradeClose": "NPLLIq_upgradeClose",
229
- "resetRow": "NPLLIq_resetRow",
230
- "dialogActions": "NPLLIq_dialogActions",
269
+ "tab": "NPLLIq_tab",
231
270
  "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
232
- "currentBadge": "NPLLIq_currentBadge",
233
- "upgradeHint": "NPLLIq_upgradeHint",
234
- "dialogHeader": "NPLLIq_dialogHeader",
235
- "logScroll": "NPLLIq_logScroll",
271
+ "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
272
+ "dialogHint": "NPLLIq_dialogHint",
273
+ "logPagerPages": "NPLLIq_logPagerPages",
236
274
  "versionNew": "NPLLIq_versionNew",
237
- "header": "NPLLIq_header",
238
- "logToolbar": "NPLLIq_logToolbar",
239
- "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
240
- "monitorRow": "NPLLIq_monitorRow",
275
+ "body": "NPLLIq_body",
276
+ "rowMeta": "NPLLIq_rowMeta",
241
277
  "dialogSection": "NPLLIq_dialogSection",
242
278
  "logUsedCol": "NPLLIq_logUsedCol",
243
- "subtitle": "NPLLIq_subtitle",
244
- "tabLabel": "NPLLIq_tabLabel",
245
- "dialogLabel": "NPLLIq_dialogLabel",
246
- "row": "NPLLIq_row",
247
- "dialogBtn": "NPLLIq_dialogBtn",
248
- "dialogBody": "NPLLIq_dialogBody",
249
- "version": "NPLLIq_version",
250
- "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
251
- "copyBtnDone": "NPLLIq_copyBtnDone",
252
- "logHeader": "NPLLIq_logHeader",
253
- "headerText": "NPLLIq_headerText",
254
- "fillWarn": "NPLLIq_fillWarn",
255
- "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
256
- "fullNotice": "NPLLIq_fullNotice",
257
- "upgradeCommand": "NPLLIq_upgradeCommand",
258
- "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
259
- "logPagerPages": "NPLLIq_logPagerPages",
260
- "upgradeCommands": "NPLLIq_upgradeCommands",
261
- "save": "NPLLIq_save",
262
- "logClearDate": "NPLLIq_logClearDate",
263
- "settingsBtn": "NPLLIq_settingsBtn",
264
- "logPager": "NPLLIq_logPager",
265
- "dialogOverlay": "NPLLIq_dialogOverlay",
266
- "logDialog": "NPLLIq_logDialog",
267
- "headerActions": "NPLLIq_headerActions",
268
- "monitorList": "NPLLIq_monitorList",
269
- "bar": "NPLLIq_bar",
270
- "input": "NPLLIq_input",
271
- "noticeError": "NPLLIq_noticeError",
272
- "fillIdle": "NPLLIq_fillIdle",
273
- "copyBtn": "NPLLIq_copyBtn",
274
279
  "logPagerControls": "NPLLIq_logPagerControls",
280
+ "fillIdle": "NPLLIq_fillIdle",
281
+ "save": "NPLLIq_save",
282
+ "upgradeBanner": "NPLLIq_upgradeBanner",
275
283
  "logPagerBtn": "NPLLIq_logPagerBtn",
276
- "radioRow": "NPLLIq_radioRow",
284
+ "monitorName": "NPLLIq_monitorName",
277
285
  "rowName": "NPLLIq_rowName",
278
- "controls": "NPLLIq_controls",
279
- "resetSelect": "NPLLIq_resetSelect",
280
- "tab": "NPLLIq_tab",
281
- "notice": "NPLLIq_notice",
282
- "body": "NPLLIq_body",
283
- "tabIcon": "NPLLIq_tabIcon",
284
- "panelDimStrong": "NPLLIq_panelDimStrong",
285
- "switch": "NPLLIq_switch",
286
- "settingsBtnActive": "NPLLIq_settingsBtnActive",
286
+ "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
287
287
  "panel": "NPLLIq_panel",
288
- "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
289
- "logTable": "NPLLIq_logTable"
288
+ "dialogBody": "NPLLIq_dialogBody",
289
+ "fillOver": "NPLLIq_fillOver",
290
+ "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
291
+ "logTable": "NPLLIq_logTable",
292
+ "upgradeClose": "NPLLIq_upgradeClose"
290
293
  };
291
294
  //#endregion
292
295
  //#region lib/types/client/TokenQuotaPanel.js
@@ -379,6 +382,7 @@ window.__ModuleLoader__.load({
379
382
  const [editingKey, setEditingKey] = (0, react.useState)(null);
380
383
  const [hovered, setHovered] = (0, react.useState)(false);
381
384
  const [inputActive, setInputActive] = (0, react.useState)(false);
385
+ const [settingsTab, setSettingsTab] = (0, react.useState)("quota");
382
386
  const [copiedCmd, setCopiedCmd] = (0, react.useState)(null);
383
387
  const copyTimerRef = (0, react.useRef)(null);
384
388
  const [upgradeFlash, setUpgradeFlash] = (0, react.useState)(false);
@@ -592,13 +596,13 @@ window.__ModuleLoader__.load({
592
596
  title: t("upgradeAvailableHint"),
593
597
  children: [
594
598
  "v",
595
- "0.1.28",
599
+ "0.1.30",
596
600
  " → v",
597
601
  upgrade.latestVersion
598
602
  ]
599
603
  }) : (0, react_jsx_runtime.jsxs)("span", {
600
604
  className: TokenQuotaPanel_module_css_default.version,
601
- children: ["v", "0.1.28"]
605
+ children: ["v", "0.1.30"]
602
606
  })]
603
607
  }), (0, react_jsx_runtime.jsx)("div", {
604
608
  className: TokenQuotaPanel_module_css_default.subtitle,
@@ -612,12 +616,13 @@ window.__ModuleLoader__.load({
612
616
  children: [
613
617
  (0, react_jsx_runtime.jsx)("button", {
614
618
  type: "button",
619
+ "aria-pressed": musicEnabled,
615
620
  className: `${TokenQuotaPanel_module_css_default.settingsBtn}${musicEnabled ? ` ${TokenQuotaPanel_module_css_default.settingsBtnActive}` : ""}`,
616
- title: t("musicHint"),
621
+ title: `${t("musicLabel")}:${musicEnabled ? t("musicOn") : t("musicOff")}`,
617
622
  onClick: () => {
618
623
  setMusicEnabled(!musicEnabled);
619
624
  },
620
- children: musicEnabled ? "🔊" : "♪"
625
+ children: "♪"
621
626
  }),
622
627
  (0, react_jsx_runtime.jsx)("button", {
623
628
  type: "button",
@@ -815,136 +820,161 @@ window.__ModuleLoader__.load({
815
820
  })]
816
821
  }),
817
822
  (0, react_jsx_runtime.jsxs)("div", {
818
- className: TokenQuotaPanel_module_css_default.dialogSection,
819
- children: [
820
- (0, react_jsx_runtime.jsx)("div", {
821
- className: TokenQuotaPanel_module_css_default.dialogLabel,
822
- children: t("monitorLabel")
823
- }),
824
- (0, react_jsx_runtime.jsx)("div", {
825
- className: TokenQuotaPanel_module_css_default.monitorHint,
826
- children: t("monitorHint")
827
- }),
828
- (0, react_jsx_runtime.jsx)("div", {
829
- className: TokenQuotaPanel_module_css_default.monitorList,
830
- children: allModels.map((model) => (0, react_jsx_runtime.jsxs)("label", {
831
- className: TokenQuotaPanel_module_css_default.monitorRow,
832
- children: [(0, react_jsx_runtime.jsx)("input", {
833
- type: "checkbox",
834
- checked: monitored === null || monitored.includes(model.key),
835
- onChange: (event) => {
836
- const base = monitored === null ? allModels.map((m) => m.key) : monitored;
837
- const next = new Set(base);
838
- if (event.target.checked) next.add(model.key);
839
- else next.delete(model.key);
840
- setMonitored(next.size === allModels.length ? null : [...next]);
841
- }
842
- }), (0, react_jsx_runtime.jsx)("span", {
843
- className: TokenQuotaPanel_module_css_default.monitorName,
844
- title: model.key,
845
- children: model.name
846
- })]
847
- }, model.key))
848
- })
849
- ]
850
- }),
851
- (0, react_jsx_runtime.jsxs)("div", {
852
- className: TokenQuotaPanel_module_css_default.dialogSection,
853
- children: [(0, react_jsx_runtime.jsx)("div", {
854
- className: TokenQuotaPanel_module_css_default.dialogLabel,
855
- children: t("fullActionLabel")
856
- }), FULL_ACTIONS.map((action) => (0, react_jsx_runtime.jsxs)("label", {
857
- className: TokenQuotaPanel_module_css_default.radioRow,
858
- children: [(0, react_jsx_runtime.jsx)("input", {
859
- type: "radio",
860
- name: "token-quota-onfull",
861
- checked: onFull === action.value,
862
- onChange: () => {
863
- setOnFull(action.value);
864
- }
865
- }), (0, react_jsx_runtime.jsx)("span", { children: t(action.labelKey) })]
866
- }, action.value))]
867
- }),
868
- (0, react_jsx_runtime.jsxs)("div", {
869
- className: TokenQuotaPanel_module_css_default.dialogSection,
870
- children: [
871
- (0, react_jsx_runtime.jsx)("div", {
872
- className: TokenQuotaPanel_module_css_default.dialogLabel,
873
- children: t("resetLabel")
874
- }),
875
- (0, react_jsx_runtime.jsx)("div", {
876
- className: TokenQuotaPanel_module_css_default.monitorHint,
877
- children: t("resetHint")
878
- }),
879
- (0, react_jsx_runtime.jsxs)("div", {
880
- className: TokenQuotaPanel_module_css_default.resetRow,
881
- children: [
882
- (0, react_jsx_runtime.jsx)("select", {
883
- className: TokenQuotaPanel_module_css_default.resetSelect,
884
- value: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
885
- onChange: (event) => {
886
- setReset({
887
- offsetHours: Number(event.target.value),
888
- hour: reset?.hour ?? 0,
889
- minute: reset?.minute ?? 0
890
- });
891
- },
892
- children: Array.from({ length: 27 }, (_, i) => i - 12).map((offset) => (0, react_jsx_runtime.jsxs)("option", {
893
- value: offset,
894
- children: ["UTC", offset >= 0 ? `+${offset}` : offset]
895
- }, offset))
896
- }),
897
- (0, react_jsx_runtime.jsx)("select", {
898
- className: TokenQuotaPanel_module_css_default.resetSelect,
899
- value: reset?.hour ?? 0,
900
- onChange: (event) => {
901
- setReset({
902
- offsetHours: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
903
- hour: Number(event.target.value),
904
- minute: reset?.minute ?? 0
905
- });
906
- },
907
- children: Array.from({ length: 24 }, (_, i) => i).map((hour) => (0, react_jsx_runtime.jsxs)("option", {
908
- value: hour,
909
- children: [String(hour).padStart(2, "0"), " 时"]
910
- }, hour))
911
- }),
912
- (0, react_jsx_runtime.jsx)("select", {
913
- className: TokenQuotaPanel_module_css_default.resetSelect,
914
- value: reset?.minute ?? 0,
915
- onChange: (event) => {
916
- setReset({
917
- offsetHours: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
918
- hour: reset?.hour ?? 0,
919
- minute: Number(event.target.value)
920
- });
921
- },
922
- children: Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (0, react_jsx_runtime.jsxs)("option", {
923
- value: minute,
924
- children: [String(minute).padStart(2, "0"), " 分"]
925
- }, minute))
926
- })
927
- ]
928
- })
929
- ]
930
- }),
931
- (0, react_jsx_runtime.jsxs)("div", {
932
- className: TokenQuotaPanel_module_css_default.dialogSection,
933
- children: [(0, react_jsx_runtime.jsxs)("label", {
934
- className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
935
- children: [(0, react_jsx_runtime.jsx)("input", {
936
- type: "checkbox",
937
- checked: dimWhenIdle,
938
- onChange: (event) => {
939
- setDimWhenIdle(event.target.checked);
940
- }
941
- }), (0, react_jsx_runtime.jsx)("span", { children: t("dimWhenIdleLabel") })]
942
- }), (0, react_jsx_runtime.jsx)("div", {
943
- className: TokenQuotaPanel_module_css_default.dialogHint,
944
- children: t("dimWhenIdleHint")
823
+ className: TokenQuotaPanel_module_css_default.dialogTabs,
824
+ role: "tablist",
825
+ children: [(0, react_jsx_runtime.jsx)("button", {
826
+ type: "button",
827
+ role: "tab",
828
+ "aria-selected": settingsTab === "quota",
829
+ className: `${TokenQuotaPanel_module_css_default.dialogTab}${settingsTab === "quota" ? ` ${TokenQuotaPanel_module_css_default.dialogTabActive}` : ""}`,
830
+ onClick: () => {
831
+ setSettingsTab("quota");
832
+ },
833
+ children: t("settingsTabQuota")
834
+ }), (0, react_jsx_runtime.jsx)("button", {
835
+ type: "button",
836
+ role: "tab",
837
+ "aria-selected": settingsTab === "music",
838
+ className: `${TokenQuotaPanel_module_css_default.dialogTab}${settingsTab === "music" ? ` ${TokenQuotaPanel_module_css_default.dialogTabActive}` : ""}`,
839
+ onClick: () => {
840
+ setSettingsTab("music");
841
+ },
842
+ children: t("settingsTabMusic")
945
843
  })]
946
844
  }),
947
- (0, react_jsx_runtime.jsxs)("div", {
845
+ settingsTab === "quota" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
846
+ (0, react_jsx_runtime.jsxs)("div", {
847
+ className: TokenQuotaPanel_module_css_default.dialogSection,
848
+ children: [
849
+ (0, react_jsx_runtime.jsx)("div", {
850
+ className: TokenQuotaPanel_module_css_default.dialogLabel,
851
+ children: t("monitorLabel")
852
+ }),
853
+ (0, react_jsx_runtime.jsx)("div", {
854
+ className: TokenQuotaPanel_module_css_default.monitorHint,
855
+ children: t("monitorHint")
856
+ }),
857
+ (0, react_jsx_runtime.jsx)("div", {
858
+ className: TokenQuotaPanel_module_css_default.monitorList,
859
+ children: allModels.map((model) => (0, react_jsx_runtime.jsxs)("label", {
860
+ className: TokenQuotaPanel_module_css_default.monitorRow,
861
+ children: [(0, react_jsx_runtime.jsx)("input", {
862
+ type: "checkbox",
863
+ checked: monitored === null || monitored.includes(model.key),
864
+ onChange: (event) => {
865
+ const base = monitored === null ? allModels.map((m) => m.key) : monitored;
866
+ const next = new Set(base);
867
+ if (event.target.checked) next.add(model.key);
868
+ else next.delete(model.key);
869
+ setMonitored(next.size === allModels.length ? null : [...next]);
870
+ }
871
+ }), (0, react_jsx_runtime.jsx)("span", {
872
+ className: TokenQuotaPanel_module_css_default.monitorName,
873
+ title: model.key,
874
+ children: model.name
875
+ })]
876
+ }, model.key))
877
+ })
878
+ ]
879
+ }),
880
+ (0, react_jsx_runtime.jsxs)("div", {
881
+ className: TokenQuotaPanel_module_css_default.dialogSection,
882
+ children: [(0, react_jsx_runtime.jsx)("div", {
883
+ className: TokenQuotaPanel_module_css_default.dialogLabel,
884
+ children: t("fullActionLabel")
885
+ }), FULL_ACTIONS.map((action) => (0, react_jsx_runtime.jsxs)("label", {
886
+ className: TokenQuotaPanel_module_css_default.radioRow,
887
+ children: [(0, react_jsx_runtime.jsx)("input", {
888
+ type: "radio",
889
+ name: "token-quota-onfull",
890
+ checked: onFull === action.value,
891
+ onChange: () => {
892
+ setOnFull(action.value);
893
+ }
894
+ }), (0, react_jsx_runtime.jsx)("span", { children: t(action.labelKey) })]
895
+ }, action.value))]
896
+ }),
897
+ (0, react_jsx_runtime.jsxs)("div", {
898
+ className: TokenQuotaPanel_module_css_default.dialogSection,
899
+ children: [
900
+ (0, react_jsx_runtime.jsx)("div", {
901
+ className: TokenQuotaPanel_module_css_default.dialogLabel,
902
+ children: t("resetLabel")
903
+ }),
904
+ (0, react_jsx_runtime.jsx)("div", {
905
+ className: TokenQuotaPanel_module_css_default.monitorHint,
906
+ children: t("resetHint")
907
+ }),
908
+ (0, react_jsx_runtime.jsxs)("div", {
909
+ className: TokenQuotaPanel_module_css_default.resetRow,
910
+ children: [
911
+ (0, react_jsx_runtime.jsx)("select", {
912
+ className: TokenQuotaPanel_module_css_default.resetSelect,
913
+ value: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
914
+ onChange: (event) => {
915
+ setReset({
916
+ offsetHours: Number(event.target.value),
917
+ hour: reset?.hour ?? 0,
918
+ minute: reset?.minute ?? 0
919
+ });
920
+ },
921
+ children: Array.from({ length: 27 }, (_, i) => i - 12).map((offset) => (0, react_jsx_runtime.jsxs)("option", {
922
+ value: offset,
923
+ children: ["UTC", offset >= 0 ? `+${offset}` : offset]
924
+ }, offset))
925
+ }),
926
+ (0, react_jsx_runtime.jsx)("select", {
927
+ className: TokenQuotaPanel_module_css_default.resetSelect,
928
+ value: reset?.hour ?? 0,
929
+ onChange: (event) => {
930
+ setReset({
931
+ offsetHours: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
932
+ hour: Number(event.target.value),
933
+ minute: reset?.minute ?? 0
934
+ });
935
+ },
936
+ children: Array.from({ length: 24 }, (_, i) => i).map((hour) => (0, react_jsx_runtime.jsxs)("option", {
937
+ value: hour,
938
+ children: [String(hour).padStart(2, "0"), " 时"]
939
+ }, hour))
940
+ }),
941
+ (0, react_jsx_runtime.jsx)("select", {
942
+ className: TokenQuotaPanel_module_css_default.resetSelect,
943
+ value: reset?.minute ?? 0,
944
+ onChange: (event) => {
945
+ setReset({
946
+ offsetHours: reset?.offsetHours ?? -(/* @__PURE__ */ new Date()).getTimezoneOffset() / 60,
947
+ hour: reset?.hour ?? 0,
948
+ minute: Number(event.target.value)
949
+ });
950
+ },
951
+ children: Array.from({ length: 12 }, (_, i) => i * 5).map((minute) => (0, react_jsx_runtime.jsxs)("option", {
952
+ value: minute,
953
+ children: [String(minute).padStart(2, "0"), " 分"]
954
+ }, minute))
955
+ })
956
+ ]
957
+ })
958
+ ]
959
+ }),
960
+ (0, react_jsx_runtime.jsxs)("div", {
961
+ className: TokenQuotaPanel_module_css_default.dialogSection,
962
+ children: [(0, react_jsx_runtime.jsxs)("label", {
963
+ className: TokenQuotaPanel_module_css_default.checkUpdatesLabel,
964
+ children: [(0, react_jsx_runtime.jsx)("input", {
965
+ type: "checkbox",
966
+ checked: dimWhenIdle,
967
+ onChange: (event) => {
968
+ setDimWhenIdle(event.target.checked);
969
+ }
970
+ }), (0, react_jsx_runtime.jsx)("span", { children: t("dimWhenIdleLabel") })]
971
+ }), (0, react_jsx_runtime.jsx)("div", {
972
+ className: TokenQuotaPanel_module_css_default.dialogHint,
973
+ children: t("dimWhenIdleHint")
974
+ })]
975
+ })
976
+ ] }),
977
+ settingsTab === "music" && (0, react_jsx_runtime.jsxs)("div", {
948
978
  className: TokenQuotaPanel_module_css_default.dialogSection,
949
979
  children: [
950
980
  (0, react_jsx_runtime.jsxs)("label", {
@@ -1015,7 +1045,7 @@ window.__ModuleLoader__.load({
1015
1045
  ] })
1016
1046
  ]
1017
1047
  }),
1018
- (0, react_jsx_runtime.jsxs)("div", {
1048
+ settingsTab === "quota" && (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: (0, react_jsx_runtime.jsxs)("div", {
1019
1049
  className: TokenQuotaPanel_module_css_default.dialogSection,
1020
1050
  children: [(0, react_jsx_runtime.jsxs)("div", {
1021
1051
  className: TokenQuotaPanel_module_css_default.checkUpdatesRow,
@@ -1040,7 +1070,7 @@ window.__ModuleLoader__.load({
1040
1070
  role: "alert",
1041
1071
  children: upgradeError
1042
1072
  })]
1043
- })
1073
+ }) })
1044
1074
  ]
1045
1075
  }),
1046
1076
  logOpen && (0, react_jsx_runtime.jsxs)("div", {
@@ -1495,6 +1525,8 @@ window.__ModuleLoader__.load({
1495
1525
  phraseIndex = 0;
1496
1526
  /** True when the next piece start should play the full intro glissando. */
1497
1527
  introPending = true;
1528
+ /** Session the current piece belongs to (turn/start carries it). */
1529
+ sessionId;
1498
1530
  phraseTimer;
1499
1531
  lastStreamActivity = 0;
1500
1532
  watchdogTimer;
@@ -1560,20 +1592,28 @@ window.__ModuleLoader__.load({
1560
1592
  markStreamActivity() {
1561
1593
  this.lastStreamActivity = Date.now();
1562
1594
  }
1595
+ /** Stop any playing piece — e.g. when the panel switches sessions. */
1596
+ stop() {
1597
+ this.stopPiece(false);
1598
+ }
1563
1599
  /** Handle one incoming host action and steer the piece. */
1564
1600
  onAction(action) {
1565
1601
  if (!this.enabled || this.audio === null) return;
1566
1602
  const base = TokenQuotaMusic.BASE_MIDI;
1567
1603
  switch (action.type) {
1568
1604
  case "turn/start":
1569
- this.stopPiece(false);
1605
+ if (this.sessionId !== void 0 && this.sessionId !== action.sessionId) this.stopPiece(false);
1606
+ this.sessionId = action.sessionId;
1607
+ this.stopMelody();
1608
+ this.playing = false;
1570
1609
  this.tonicIndex = 0;
1571
1610
  this.phraseIndex = 0;
1572
1611
  this.introPending = true;
1612
+ this.startPiece();
1573
1613
  break;
1574
1614
  case "request/header":
1575
1615
  if (!this.playing) this.startPiece();
1576
- else this.pluck(base + 24, .3);
1616
+ else this.pluck(this.tone(5, 1), .3, .9);
1577
1617
  break;
1578
1618
  case "step/start": {
1579
1619
  const prog = PROGRESSION[this.style];
@@ -1585,33 +1625,51 @@ window.__ModuleLoader__.load({
1585
1625
  break;
1586
1626
  }
1587
1627
  case "tool/call":
1588
- this.pluck(base + 26, .45, .5);
1589
- this.pluck(base - 12, .3, .35);
1628
+ this.pluck(this.tone(4, 1), .38, .9);
1629
+ setTimeout(() => this.pluck(this.tone(5, 1), .34, .9), 90);
1630
+ this.pluck(this.tonicRoot - 12, .22, .9);
1590
1631
  break;
1591
1632
  case "tool/result":
1592
1633
  if (action.error) {
1593
- this.pluck(base + 13, .4, .6);
1594
- setTimeout(() => this.pluck(base + 12, .4, .9), 90);
1634
+ const t = this.tone(2, 1);
1635
+ this.pluck(t - 1, .4, .7);
1636
+ setTimeout(() => this.pluck(t - 3, .4, 1), 110);
1595
1637
  } else {
1596
- this.pluck(base + 9, .35, .6);
1597
- setTimeout(() => this.pluck(base + 11, .35, .9), 70);
1638
+ this.pluck(this.tone(3, 1), .34, 1);
1639
+ setTimeout(() => this.pluck(this.tone(1, 1), .32, 1), 90);
1640
+ setTimeout(() => this.pluck(this.tone(0, 1), .34, 1.2), 180);
1598
1641
  }
1599
1642
  break;
1600
1643
  case "assistant/attempt":
1601
- this.pluck(base + 8, .4, .8);
1602
- setTimeout(() => this.pluck(base + 7, .4, .8), 110);
1603
- setTimeout(() => this.pluck(base + 5, .4, .8), 220);
1644
+ this.pluck(this.tone(4, 1), .36, .8);
1645
+ setTimeout(() => this.pluck(this.tone(2, 1), .36, .9), 110);
1646
+ setTimeout(() => this.pluck(this.tone(0), .36, 1.1), 220);
1604
1647
  break;
1605
1648
  case "assistant/message":
1606
- this.stopMelody();
1607
- this.playing = false;
1608
- this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
1649
+ this.pluck(this.tone(2, 1), .3, 1);
1650
+ setTimeout(() => this.breath(this.tone(0, action.interrupted ? 0 : 1), 1.4), 140);
1609
1651
  break;
1610
1652
  case "turn/end":
1611
1653
  this.stopPiece(true);
1612
1654
  break;
1613
1655
  }
1614
1656
  }
1657
+ /** Current tonic root in MIDI note numbers. */
1658
+ get tonicRoot() {
1659
+ return TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
1660
+ }
1661
+ /**
1662
+ * A scale tone relative to the current tonic — degrees wrap up octaves
1663
+ * (deg 5 on a 5-note scale = the octave). Event figures use this so every
1664
+ * accent stays inside the key and melts into the piece instead of
1665
+ * clashing with it.
1666
+ */
1667
+ tone(deg, oct = 0) {
1668
+ const scale = SCALES[this.style];
1669
+ const len = scale.length;
1670
+ const d = (deg % len + len) % len;
1671
+ return this.tonicRoot + 12 * (Math.floor(deg / len) + oct) + (scale[d] ?? 0);
1672
+ }
1615
1673
  /** Start a new piece: intro run, then the phrase chain. */
1616
1674
  startPiece() {
1617
1675
  if (!this.audio || this.playing) return;
@@ -1843,7 +1901,9 @@ window.__ModuleLoader__.load({
1843
1901
  expand: "展开",
1844
1902
  expandHint: "展开每日 Token 限额面板",
1845
1903
  settings: "设置",
1846
- settingsTitle: "限额设置",
1904
+ settingsTitle: "设置",
1905
+ settingsTabQuota: "限额设置",
1906
+ settingsTabMusic: "音乐设置",
1847
1907
  monitorLabel: "监控模型",
1848
1908
  monitorHint: "未勾选的模型不显示、不计入用量、不受限额限制",
1849
1909
  monitorAll: "全部",
@@ -1887,6 +1947,8 @@ window.__ModuleLoader__.load({
1887
1947
  dimWhenIdleHint: "鼠标移开面板时降低不透明度,输入文字时更透明;鼠标悬停恢复",
1888
1948
  musicLabel: "动作音乐",
1889
1949
  musicHint: "根据 Harness 动作实时作曲:请求开始、工具调用、回合结束等(需点击开关启动音频;检测到 MIDI 输出时同步发送 MIDI)",
1950
+ musicOn: "开",
1951
+ musicOff: "关",
1890
1952
  musicVolume: "音量",
1891
1953
  musicStyle: "风格",
1892
1954
  musicStyleMajor: "大调",
@@ -1919,7 +1981,9 @@ window.__ModuleLoader__.load({
1919
1981
  expand: "Expand",
1920
1982
  expandHint: "Expand the daily token quota panel",
1921
1983
  settings: "Settings",
1922
- settingsTitle: "Quota Settings",
1984
+ settingsTitle: "Settings",
1985
+ settingsTabQuota: "Quota",
1986
+ settingsTabMusic: "Music",
1923
1987
  monitorLabel: "Monitored models",
1924
1988
  monitorHint: "Unchecked models are hidden, not metered, and never capped",
1925
1989
  monitorAll: "All",
@@ -1963,6 +2027,8 @@ window.__ModuleLoader__.load({
1963
2027
  dimWhenIdleHint: "Fade the panel out when the pointer is away (more while typing); hover to restore",
1964
2028
  musicLabel: "Action music",
1965
2029
  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)",
2030
+ musicOn: "On",
2031
+ musicOff: "Off",
1966
2032
  musicVolume: "Volume",
1967
2033
  musicStyle: "Style",
1968
2034
  musicStyleMajor: "Major",
@@ -2186,6 +2252,7 @@ window.__ModuleLoader__.load({
2186
2252
  };
2187
2253
  }, "token-quota-ui: snapshot poll + full strategy");
2188
2254
  const load = (sessionId) => {
2255
+ if (lastSessionId !== void 0 && lastSessionId !== sessionId) music.stop();
2189
2256
  lastSessionId = sessionId;
2190
2257
  bound?.setLoading(true);
2191
2258
  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.28";
518
+ const current = "0.1.30";
519
519
  let latest;
520
520
  try {
521
521
  const response = await fetch(REGISTRY_LATEST_URL, {
@@ -95,6 +95,8 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
95
95
  // so the chat content behind it stays readable; hovering restores it.
96
96
  const [hovered, setHovered] = useState(false);
97
97
  const [inputActive, setInputActive] = useState(false);
98
+ // Active settings-dialog tab: quota limits vs. live-music options.
99
+ const [settingsTab, setSettingsTab] = useState('quota');
98
100
  // Copy-button feedback: which command was just copied (label flips to
99
101
  // 「已复制」for a moment so the click is perceivable).
100
102
  const [copiedCmd, setCopiedCmd] = useState(null);
@@ -298,7 +300,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
298
300
  : '';
299
301
  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
302
  ? (_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) => {
303
+ : (_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
304
  const state = barStateOf(row);
303
305
  const pct = row.limit > 0 ? Math.min(100, Math.round((row.used / row.limit) * 100)) : 0;
304
306
  const barClass = state === 'over'
@@ -311,40 +313,40 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
311
313
  ? `${formatTokens(row.used)} / ${formatTokens(row.limit)}`
312
314
  : `${formatTokens(row.used)} · ${t('unlimited')}`, _jsx("button", { type: "button", className: css.gearBtn, title: t('setLimitHint'), onClick: () => { setEditingKey(editing ? null : row.key); }, children: "\u2699" })] })] }), _jsx("div", { className: css.bar, children: _jsx("div", { className: barClass, style: { width: `${pct}%` } }) }), editing && (_jsxs("div", { className: css.controls, children: [_jsx("input", { className: css.input, type: "number", min: 0, step: 10000, placeholder: t('limitPlaceholder'), value: drafts[row.key] ?? '', onChange: (event) => { setDrafts(prev => ({ ...prev, [row.key]: event.target.value })); }, onKeyDown: (event) => { if (event.key === 'Enter')
313
315
  commitLimit(row); } }), _jsx("button", { type: "button", className: css.save, onClick: () => { commitLimit(row); }, children: t('save') })] }))] }, row.key));
314
- })] }), dialogOpen && (_jsxs("div", { ref: settingsRef, className: css.dialog, style: settingsPos !== null ? { left: settingsPos.x, top: settingsPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.dialogHeader, onPointerDown: (event) => { beginDrag(event, settingsRef.current, setSettingsPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('settingsTitle') }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setDialogOpen(false); }, onPointerDown: (event) => { event.stopPropagation(); }, children: "\u00D7" })] }), _jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('monitorLabel') }), _jsx("div", { className: css.monitorHint, children: t('monitorHint') }), _jsx("div", { className: css.monitorList, children: allModels.map(model => (_jsxs("label", { className: css.monitorRow, children: [_jsx("input", { type: "checkbox", checked: monitored === null || monitored.includes(model.key), onChange: (event) => {
315
- const base = monitored === null ? allModels.map(m => m.key) : monitored;
316
- const next = new Set(base);
317
- if (event.target.checked)
318
- next.add(model.key);
319
- else
320
- next.delete(model.key);
321
- // Auto-save on every change; all selected stores as null.
322
- setMonitored(next.size === allModels.length ? null : [...next]);
323
- } }), _jsx("span", { className: css.monitorName, title: model.key, children: model.name })] }, model.key))) })] }), _jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('fullActionLabel') }), FULL_ACTIONS.map(action => (_jsxs("label", { className: css.radioRow, children: [_jsx("input", { type: "radio", name: "token-quota-onfull", checked: onFull === action.value, onChange: () => { setOnFull(action.value); } }), _jsx("span", { children: t(action.labelKey) })] }, action.value)))] }), _jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('resetLabel') }), _jsx("div", { className: css.monitorHint, children: t('resetHint') }), _jsxs("div", { className: css.resetRow, children: [_jsx("select", { className: css.resetSelect, value: reset?.offsetHours ?? -new Date().getTimezoneOffset() / 60, onChange: (event) => {
324
- setReset({
325
- offsetHours: Number(event.target.value),
326
- hour: reset?.hour ?? 0,
327
- minute: reset?.minute ?? 0,
328
- });
329
- }, children: Array.from({ length: 27 }, (_, i) => i - 12).map(offset => (_jsxs("option", { value: offset, children: ["UTC", offset >= 0 ? `+${offset}` : offset] }, offset))) }), _jsx("select", { className: css.resetSelect, value: reset?.hour ?? 0, onChange: (event) => {
330
- setReset({
331
- offsetHours: reset?.offsetHours ?? -new Date().getTimezoneOffset() / 60,
332
- hour: Number(event.target.value),
333
- minute: reset?.minute ?? 0,
334
- });
335
- }, children: Array.from({ length: 24 }, (_, i) => i).map(hour => (_jsxs("option", { value: hour, children: [String(hour).padStart(2, '0'), " \u65F6"] }, hour))) }), _jsx("select", { className: css.resetSelect, value: reset?.minute ?? 0, onChange: (event) => {
336
- setReset({
337
- offsetHours: reset?.offsetHours ?? -new Date().getTimezoneOffset() / 60,
338
- hour: reset?.hour ?? 0,
339
- minute: Number(event.target.value),
340
- });
341
- }, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: dimWhenIdle, onChange: (event) => { setDimWhenIdle(event.target.checked); } }), _jsx("span", { children: t('dimWhenIdleLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('dimWhenIdleHint') })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicEnabled, onChange: (event) => { setMusicEnabled(event.target.checked); } }), _jsx("span", { children: t('musicLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicHint') }), musicEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicVolume') }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: musicVolume, onChange: (event) => { setMusicVolume(Number(event.target.value)); } })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicStyle') }), _jsxs("select", { value: musicStyle, onChange: (event) => { setMusicStyle(event.target.value); }, children: [_jsx("option", { value: "major", children: t('musicStyleMajor') }), _jsx("option", { value: "minor", children: t('musicStyleMinor') }), _jsx("option", { value: "pentatonic", children: t('musicStylePentatonic') })] })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicOnlyCurrentSession, onChange: (event) => { setMusicOnlyCurrentSession(event.target.checked); } }), _jsx("span", { children: t('musicOnlyCurrentSessionLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicOnlyCurrentSessionHint') })] }))] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("div", { className: css.checkUpdatesRow, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates
342
- ? t('checkingUpdates')
343
- : lastCheckResult === 'up-to-date'
344
- ? t('checkUpToDate')
345
- : lastCheckResult === 'error'
346
- ? t('checkFailed')
347
- : t('checkUpdatesNow') })] }), lastCheckResult === 'error' && upgradeError !== null && (_jsx("div", { className: css.checkErrorHint, role: "alert", children: upgradeError }))] })] })), logOpen && (_jsxs("div", { ref: logRef, className: `${css.dialog} ${css.logDialog}`, style: logPos !== null ? { left: logPos.x, top: logPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.logHeader, onPointerDown: (event) => { beginDrag(event, logRef.current, setLogPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('logsTitle') }), _jsxs("div", { className: css.logToolbar, onPointerDown: (event) => { event.stopPropagation(); }, children: [_jsx("input", { type: "search", className: css.logSearch, placeholder: t('logSearchPlaceholder'), value: logSearch, onChange: (event) => { actions.setLogSearch(event.target.value); } }), _jsx("button", { type: "button", className: css.logClearBtn, onClick: () => { actions.setLogClearOpen(true); }, children: t('logClear') })] }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setLogOpen(false); }, children: "\u00D7" })] }), log !== null && filteredLogEntries.length === 0 && (_jsx("div", { className: css.notice, children: logSearch.trim() !== '' ? t('logNoMatch') : t('logEmpty') })), _jsx("div", { className: css.logScroll, children: _jsxs("table", { className: css.logTable, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t('logDay') }), _jsx("th", { children: t('logModel') }), _jsx("th", { className: css.logUsedCol, children: t('logUsed') })] }) }), _jsx("tbody", { children: logPageEntries.map(entry => (_jsxs("tr", { children: [_jsx("td", { className: css.logDayCol, children: entry.day }), _jsx("td", { className: css.logModelCol, title: entry.key, children: entry.key }), _jsx("td", { className: css.logUsedCol, children: formatTokens(entry.used) })] }, `${entry.day}/${entry.key}`))) })] }) }), _jsxs("div", { className: css.logPager, children: [_jsx("span", { className: css.logPagerInfo, children: t('logPagerInfo')
316
+ })] }), dialogOpen && (_jsxs("div", { ref: settingsRef, className: css.dialog, style: settingsPos !== null ? { left: settingsPos.x, top: settingsPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.dialogHeader, onPointerDown: (event) => { beginDrag(event, settingsRef.current, setSettingsPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('settingsTitle') }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setDialogOpen(false); }, onPointerDown: (event) => { event.stopPropagation(); }, children: "\u00D7" })] }), _jsxs("div", { className: css.dialogTabs, role: "tablist", children: [_jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'quota', className: `${css.dialogTab}${settingsTab === 'quota' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('quota'); }, children: t('settingsTabQuota') }), _jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'music', className: `${css.dialogTab}${settingsTab === 'music' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('music'); }, children: t('settingsTabMusic') })] }), settingsTab === 'quota' && (_jsxs(_Fragment, { children: [_jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('monitorLabel') }), _jsx("div", { className: css.monitorHint, children: t('monitorHint') }), _jsx("div", { className: css.monitorList, children: allModels.map(model => (_jsxs("label", { className: css.monitorRow, children: [_jsx("input", { type: "checkbox", checked: monitored === null || monitored.includes(model.key), onChange: (event) => {
317
+ const base = monitored === null ? allModels.map(m => m.key) : monitored;
318
+ const next = new Set(base);
319
+ if (event.target.checked)
320
+ next.add(model.key);
321
+ else
322
+ next.delete(model.key);
323
+ // Auto-save on every change; all selected stores as null.
324
+ setMonitored(next.size === allModels.length ? null : [...next]);
325
+ } }), _jsx("span", { className: css.monitorName, title: model.key, children: model.name })] }, model.key))) })] }), _jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('fullActionLabel') }), FULL_ACTIONS.map(action => (_jsxs("label", { className: css.radioRow, children: [_jsx("input", { type: "radio", name: "token-quota-onfull", checked: onFull === action.value, onChange: () => { setOnFull(action.value); } }), _jsx("span", { children: t(action.labelKey) })] }, action.value)))] }), _jsxs("div", { className: css.dialogSection, children: [_jsx("div", { className: css.dialogLabel, children: t('resetLabel') }), _jsx("div", { className: css.monitorHint, children: t('resetHint') }), _jsxs("div", { className: css.resetRow, children: [_jsx("select", { className: css.resetSelect, value: reset?.offsetHours ?? -new Date().getTimezoneOffset() / 60, onChange: (event) => {
326
+ setReset({
327
+ offsetHours: Number(event.target.value),
328
+ hour: reset?.hour ?? 0,
329
+ minute: reset?.minute ?? 0,
330
+ });
331
+ }, children: Array.from({ length: 27 }, (_, i) => i - 12).map(offset => (_jsxs("option", { value: offset, children: ["UTC", offset >= 0 ? `+${offset}` : offset] }, offset))) }), _jsx("select", { className: css.resetSelect, value: reset?.hour ?? 0, onChange: (event) => {
332
+ setReset({
333
+ offsetHours: reset?.offsetHours ?? -new Date().getTimezoneOffset() / 60,
334
+ hour: Number(event.target.value),
335
+ minute: reset?.minute ?? 0,
336
+ });
337
+ }, children: Array.from({ length: 24 }, (_, i) => i).map(hour => (_jsxs("option", { value: hour, children: [String(hour).padStart(2, '0'), " \u65F6"] }, hour))) }), _jsx("select", { className: css.resetSelect, value: reset?.minute ?? 0, onChange: (event) => {
338
+ setReset({
339
+ offsetHours: reset?.offsetHours ?? -new Date().getTimezoneOffset() / 60,
340
+ hour: reset?.hour ?? 0,
341
+ minute: Number(event.target.value),
342
+ });
343
+ }, children: Array.from({ length: 12 }, (_, i) => i * 5).map(minute => (_jsxs("option", { value: minute, children: [String(minute).padStart(2, '0'), " \u5206"] }, minute))) })] })] }), _jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: dimWhenIdle, onChange: (event) => { setDimWhenIdle(event.target.checked); } }), _jsx("span", { children: t('dimWhenIdleLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('dimWhenIdleHint') })] })] })), settingsTab === 'music' && (_jsxs("div", { className: css.dialogSection, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicEnabled, onChange: (event) => { setMusicEnabled(event.target.checked); } }), _jsx("span", { children: t('musicLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicHint') }), musicEnabled && (_jsxs(_Fragment, { children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicVolume') }), _jsx("input", { type: "range", min: 0, max: 1, step: 0.05, value: musicVolume, onChange: (event) => { setMusicVolume(Number(event.target.value)); } })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("span", { children: t('musicStyle') }), _jsxs("select", { value: musicStyle, onChange: (event) => { setMusicStyle(event.target.value); }, children: [_jsx("option", { value: "major", children: t('musicStyleMajor') }), _jsx("option", { value: "minor", children: t('musicStyleMinor') }), _jsx("option", { value: "pentatonic", children: t('musicStylePentatonic') })] })] }), _jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: musicOnlyCurrentSession, onChange: (event) => { setMusicOnlyCurrentSession(event.target.checked); } }), _jsx("span", { children: t('musicOnlyCurrentSessionLabel') })] }), _jsx("div", { className: css.dialogHint, children: t('musicOnlyCurrentSessionHint') })] }))] })), settingsTab === 'quota' && (_jsx(_Fragment, { children: _jsxs("div", { className: css.dialogSection, children: [_jsxs("div", { className: css.checkUpdatesRow, children: [_jsxs("label", { className: css.checkUpdatesLabel, children: [_jsx("input", { type: "checkbox", checked: checkUpdates, onChange: (event) => { setCheckUpdates(event.target.checked); } }), _jsx("span", { children: t('checkUpdatesLabel') })] }), _jsx("button", { type: "button", className: css.checkBtn, disabled: checkingUpdates, onClick: checkUpdatesNow, children: checkingUpdates
344
+ ? t('checkingUpdates')
345
+ : lastCheckResult === 'up-to-date'
346
+ ? t('checkUpToDate')
347
+ : lastCheckResult === 'error'
348
+ ? t('checkFailed')
349
+ : t('checkUpdatesNow') })] }), lastCheckResult === 'error' && upgradeError !== null && (_jsx("div", { className: css.checkErrorHint, role: "alert", children: upgradeError }))] }) }))] })), logOpen && (_jsxs("div", { ref: logRef, className: `${css.dialog} ${css.logDialog}`, style: logPos !== null ? { left: logPos.x, top: logPos.y, transform: 'none' } : undefined, children: [_jsxs("div", { className: css.logHeader, onPointerDown: (event) => { beginDrag(event, logRef.current, setLogPos); }, children: [_jsx("div", { className: css.dialogTitle, children: t('logsTitle') }), _jsxs("div", { className: css.logToolbar, onPointerDown: (event) => { event.stopPropagation(); }, children: [_jsx("input", { type: "search", className: css.logSearch, placeholder: t('logSearchPlaceholder'), value: logSearch, onChange: (event) => { actions.setLogSearch(event.target.value); } }), _jsx("button", { type: "button", className: css.logClearBtn, onClick: () => { actions.setLogClearOpen(true); }, children: t('logClear') })] }), _jsx("button", { type: "button", className: css.dialogClose, title: t('close'), onClick: () => { actions.setLogOpen(false); }, children: "\u00D7" })] }), log !== null && filteredLogEntries.length === 0 && (_jsx("div", { className: css.notice, children: logSearch.trim() !== '' ? t('logNoMatch') : t('logEmpty') })), _jsx("div", { className: css.logScroll, children: _jsxs("table", { className: css.logTable, children: [_jsx("thead", { children: _jsxs("tr", { children: [_jsx("th", { children: t('logDay') }), _jsx("th", { children: t('logModel') }), _jsx("th", { className: css.logUsedCol, children: t('logUsed') })] }) }), _jsx("tbody", { children: logPageEntries.map(entry => (_jsxs("tr", { children: [_jsx("td", { className: css.logDayCol, children: entry.day }), _jsx("td", { className: css.logModelCol, title: entry.key, children: entry.key }), _jsx("td", { className: css.logUsedCol, children: formatTokens(entry.used) })] }, `${entry.day}/${entry.key}`))) })] }) }), _jsxs("div", { className: css.logPager, children: [_jsx("span", { className: css.logPagerInfo, children: t('logPagerInfo')
348
350
  .replace('{from}', String(logCurrentPage * logPageSize + 1))
349
351
  .replace('{to}', String(Math.min((logCurrentPage + 1) * logPageSize, filteredLogEntries.length)))
350
352
  .replace('{total}', String(filteredLogEntries.length)) }), _jsxs("div", { className: css.logPagerControls, children: [_jsx("button", { type: "button", className: css.logPagerBtn, disabled: logCurrentPage <= 0, onClick: () => { actions.setLogPage(logCurrentPage - 1); }, children: "\u2039" }), _jsxs("span", { className: css.logPagerPages, children: [logCurrentPage + 1, " / ", logTotalPages] }), _jsx("button", { type: "button", className: css.logPagerBtn, disabled: logCurrentPage >= logTotalPages - 1, onClick: () => { actions.setLogPage(logCurrentPage + 1); }, children: "\u203A" }), _jsx("select", { className: css.logPageSizeSelect, value: logPageSize, onChange: (event) => { actions.setLogPageSize(Number(event.target.value)); }, children: [10, 15, 20, 50, 100].map(size => (_jsxs("option", { value: size, children: [size, t('logPerPage')] }, size))) })] })] })] })), logClearOpen && (_jsx("div", { className: css.dialogOverlay, children: _jsxs("div", { className: `${css.dialog} ${css.logClearDialog}`, children: [_jsx("div", { className: css.dialogTitle, children: t('logClearTitle') }), _jsx("div", { className: css.dialogBody, children: t('logClearHint') }), _jsx("input", { type: "date", className: css.logClearDate, value: logClearBefore, onChange: (event) => { actions.setLogClearBefore(event.target.value); } }), _jsxs("div", { className: css.dialogActions, children: [_jsx("button", { type: "button", className: css.checkBtn, disabled: logClearBefore === '' || logClearing, onClick: () => { clearLogBefore(logClearBefore); }, children: logClearing ? t('logClearing') : t('logClearConfirm') }), _jsx("button", { type: "button", className: css.checkBtn, onClick: () => { actions.setLogClearOpen(false); }, children: t('logClearCancel') })] })] }) }))] }));
@@ -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);
@@ -25,6 +25,8 @@ export declare const zh: {
25
25
  expandHint: string;
26
26
  settings: string;
27
27
  settingsTitle: string;
28
+ settingsTabQuota: string;
29
+ settingsTabMusic: string;
28
30
  monitorLabel: string;
29
31
  monitorHint: string;
30
32
  monitorAll: string;
@@ -68,6 +70,8 @@ export declare const zh: {
68
70
  dimWhenIdleHint: string;
69
71
  musicLabel: string;
70
72
  musicHint: string;
73
+ musicOn: string;
74
+ musicOff: string;
71
75
  musicVolume: string;
72
76
  musicStyle: string;
73
77
  musicStyleMajor: string;
@@ -103,6 +107,8 @@ export declare const en: {
103
107
  expandHint: string;
104
108
  settings: string;
105
109
  settingsTitle: string;
110
+ settingsTabQuota: string;
111
+ settingsTabMusic: string;
106
112
  monitorLabel: string;
107
113
  monitorHint: string;
108
114
  monitorAll: string;
@@ -146,6 +152,8 @@ export declare const en: {
146
152
  dimWhenIdleHint: string;
147
153
  musicLabel: string;
148
154
  musicHint: string;
155
+ musicOn: string;
156
+ musicOff: string;
149
157
  musicVolume: string;
150
158
  musicStyle: string;
151
159
  musicStyleMajor: string;
@@ -24,7 +24,9 @@ export const zh = {
24
24
  expand: '展开',
25
25
  expandHint: '展开每日 Token 限额面板',
26
26
  settings: '设置',
27
- settingsTitle: '限额设置',
27
+ settingsTitle: '设置',
28
+ settingsTabQuota: '限额设置',
29
+ settingsTabMusic: '音乐设置',
28
30
  monitorLabel: '监控模型',
29
31
  monitorHint: '未勾选的模型不显示、不计入用量、不受限额限制',
30
32
  monitorAll: '全部',
@@ -68,6 +70,8 @@ export const zh = {
68
70
  dimWhenIdleHint: '鼠标移开面板时降低不透明度,输入文字时更透明;鼠标悬停恢复',
69
71
  musicLabel: '动作音乐',
70
72
  musicHint: '根据 Harness 动作实时作曲:请求开始、工具调用、回合结束等(需点击开关启动音频;检测到 MIDI 输出时同步发送 MIDI)',
73
+ musicOn: '开',
74
+ musicOff: '关',
71
75
  musicVolume: '音量',
72
76
  musicStyle: '风格',
73
77
  musicStyleMajor: '大调',
@@ -100,7 +104,9 @@ export const en = {
100
104
  expand: 'Expand',
101
105
  expandHint: 'Expand the daily token quota panel',
102
106
  settings: 'Settings',
103
- settingsTitle: 'Quota Settings',
107
+ settingsTitle: 'Settings',
108
+ settingsTabQuota: 'Quota',
109
+ settingsTabMusic: 'Music',
104
110
  monitorLabel: 'Monitored models',
105
111
  monitorHint: 'Unchecked models are hidden, not metered, and never capped',
106
112
  monitorAll: 'All',
@@ -144,6 +150,8 @@ export const en = {
144
150
  dimWhenIdleHint: 'Fade the panel out when the pointer is away (more while typing); hover to restore',
145
151
  musicLabel: 'Action music',
146
152
  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)',
153
+ musicOn: 'On',
154
+ musicOff: 'Off',
147
155
  musicVolume: 'Volume',
148
156
  musicStyle: 'Style',
149
157
  musicStyleMajor: 'Major',
@@ -42,6 +42,8 @@ export declare class TokenQuotaMusic {
42
42
  private phraseIndex;
43
43
  /** True when the next piece start should play the full intro glissando. */
44
44
  private introPending;
45
+ /** Session the current piece belongs to (turn/start carries it). */
46
+ private sessionId;
45
47
  private phraseTimer;
46
48
  private lastStreamActivity;
47
49
  private watchdogTimer;
@@ -64,8 +66,19 @@ export declare class TokenQuotaMusic {
64
66
  setStyle(value: TokenQuotaMusicStyle): void;
65
67
  /** Feed the engine one SSE activity timestamp (action or heartbeat). */
66
68
  markStreamActivity(): void;
69
+ /** Stop any playing piece — e.g. when the panel switches sessions. */
70
+ stop(): void;
67
71
  /** Handle one incoming host action and steer the piece. */
68
72
  onAction(action: TokenQuotaMusicAction): void;
73
+ /** Current tonic root in MIDI note numbers. */
74
+ private get tonicRoot();
75
+ /**
76
+ * A scale tone relative to the current tonic — degrees wrap up octaves
77
+ * (deg 5 on a 5-note scale = the octave). Event figures use this so every
78
+ * accent stays inside the key and melts into the piece instead of
79
+ * clashing with it.
80
+ */
81
+ private tone;
69
82
  /** Start a new piece: intro run, then the phrase chain. */
70
83
  private startPiece;
71
84
  /** Play one phrase, then chain into the next after a one-beat rest. */
@@ -80,6 +80,8 @@ export class TokenQuotaMusic {
80
80
  phraseIndex = 0;
81
81
  /** True when the next piece start should play the full intro glissando. */
82
82
  introPending = true;
83
+ /** Session the current piece belongs to (turn/start carries it). */
84
+ sessionId;
83
85
  phraseTimer;
84
86
  // Watchdog: host heartbeat freshness (ms timestamp)
85
87
  lastStreamActivity = 0;
@@ -161,6 +163,10 @@ export class TokenQuotaMusic {
161
163
  markStreamActivity() {
162
164
  this.lastStreamActivity = Date.now();
163
165
  }
166
+ /** Stop any playing piece — e.g. when the panel switches sessions. */
167
+ stop() {
168
+ this.stopPiece(false);
169
+ }
164
170
  /** Handle one incoming host action and steer the piece. */
165
171
  onAction(action) {
166
172
  if (!this.enabled || this.audio === null)
@@ -168,16 +174,27 @@ export class TokenQuotaMusic {
168
174
  const base = TokenQuotaMusic.BASE_MIDI;
169
175
  switch (action.type) {
170
176
  case 'turn/start':
171
- 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;
172
187
  this.tonicIndex = 0;
173
188
  this.phraseIndex = 0;
174
189
  this.introPending = true;
190
+ this.startPiece();
175
191
  break;
176
192
  case 'request/header': {
193
+ // Safety net (and a light "request fired" accent when already playing).
177
194
  if (!this.playing)
178
195
  this.startPiece();
179
196
  else
180
- this.pluck(base + 24, 0.3); // light accent on a subsequent request
197
+ this.pluck(this.tone(5, 1), 0.3, 0.9);
181
198
  break;
182
199
  }
183
200
  case 'step/start': {
@@ -190,41 +207,67 @@ export class TokenQuotaMusic {
190
207
  }
191
208
  break;
192
209
  }
193
- case 'tool/call':
194
- // High plink + low tap — distinct from the plucked melody.
195
- this.pluck(base + 26, 0.45, 0.5);
196
- this.pluck(base - 12, 0.3, 0.35);
210
+ case 'tool/call': {
211
+ // Rising two-note figure drawn from the current scale (so it blends
212
+ // with the piece, not pokes out of it) over a soft low root for body.
213
+ this.pluck(this.tone(4, 1), 0.38, 0.9);
214
+ setTimeout(() => this.pluck(this.tone(5, 1), 0.34, 0.9), 90);
215
+ this.pluck(this.tonicRoot - 12, 0.22, 0.9);
197
216
  break;
217
+ }
198
218
  case 'tool/result': {
199
219
  if (action.error) {
200
- this.pluck(base + 13, 0.4, 0.6);
201
- setTimeout(() => this.pluck(base + 12, 0.4, 0.9), 90);
220
+ // Deliberately sour (an error should feel off): a half-step below a
221
+ // scale tone, then sinking further.
222
+ const t = this.tone(2, 1);
223
+ this.pluck(t - 1, 0.4, 0.7);
224
+ setTimeout(() => this.pluck(t - 3, 0.4, 1.0), 110);
202
225
  }
203
226
  else {
204
- this.pluck(base + 9, 0.35, 0.6);
205
- setTimeout(() => this.pluck(base + 11, 0.35, 0.9), 70);
227
+ // Gentle in-key resolve landing on the tonic octave.
228
+ this.pluck(this.tone(3, 1), 0.34, 1.0);
229
+ setTimeout(() => this.pluck(this.tone(1, 1), 0.32, 1.0), 90);
230
+ setTimeout(() => this.pluck(this.tone(0, 1), 0.34, 1.2), 180);
206
231
  }
207
232
  break;
208
233
  }
209
- case 'assistant/attempt':
210
- // Failed attempt: falling tension figure.
211
- this.pluck(base + 8, 0.4, 0.8);
212
- setTimeout(() => this.pluck(base + 7, 0.4, 0.8), 110);
213
- setTimeout(() => this.pluck(base + 5, 0.4, 0.8), 220);
234
+ case 'assistant/attempt': {
235
+ // Failed attempt: falling in-key tension figure.
236
+ this.pluck(this.tone(4, 1), 0.36, 0.8);
237
+ setTimeout(() => this.pluck(this.tone(2, 1), 0.36, 0.9), 110);
238
+ setTimeout(() => this.pluck(this.tone(0), 0.36, 1.1), 220);
214
239
  break;
215
- case 'assistant/message':
216
- // Sentence end: stop the phrase chain and breathe. The next
217
- // request/header (next step or next turn) starts a fresh phrase.
218
- this.stopMelody();
219
- this.playing = false;
220
- this.breath(action.interrupted ? base + 6 : base + 12, 1.6);
240
+ }
241
+ case 'assistant/message': {
242
+ // Sentence end: a short cadence figure, then a breath on the tonic
243
+ // octave (in key — the old fixed C clashed with the mode rotation).
244
+ // The bed keeps playing until the turn fully ends (turn/end).
245
+ this.pluck(this.tone(2, 1), 0.3, 1.0);
246
+ setTimeout(() => this.breath(this.tone(0, action.interrupted ? 0 : 1), 1.4), 140);
221
247
  break;
248
+ }
222
249
  case 'turn/end':
223
250
  this.stopPiece(true);
224
251
  break;
225
252
  }
226
253
  }
227
254
  // ── piece lifecycle ───────────────────────────────────────────────────
255
+ /** Current tonic root in MIDI note numbers. */
256
+ get tonicRoot() {
257
+ return TokenQuotaMusic.BASE_MIDI + (PROGRESSION[this.style][this.tonicIndex] ?? 0);
258
+ }
259
+ /**
260
+ * A scale tone relative to the current tonic — degrees wrap up octaves
261
+ * (deg 5 on a 5-note scale = the octave). Event figures use this so every
262
+ * accent stays inside the key and melts into the piece instead of
263
+ * clashing with it.
264
+ */
265
+ tone(deg, oct = 0) {
266
+ const scale = SCALES[this.style];
267
+ const len = scale.length;
268
+ const d = ((deg % len) + len) % len;
269
+ return this.tonicRoot + 12 * (Math.floor(deg / len) + oct) + (scale[d] ?? 0);
270
+ }
228
271
  /** Start a new piece: intro run, then the phrase chain. */
229
272
  startPiece() {
230
273
  if (!this.audio || this.playing)
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.28",
4
+ "version": "0.1.30",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },