@jxgame2020/dsh-token-quota 0.1.24 → 0.1.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -27,7 +27,8 @@ const TOKEN_QUOTA_EXCEEDED_CODE = "TOKEN_QUOTA_EXCEEDED";
27
27
  const TOKEN_QUOTA_DEFAULT_MUSIC = {
28
28
  enabled: false,
29
29
  volume: .5,
30
- style: "major"
30
+ style: "major",
31
+ onlyCurrentSession: true
31
32
  };
32
33
  /** Build the stable per-model key shared by the counter, settings, and snapshot. */
33
34
  function tokenQuotaKey(provider, model) {
@@ -221,7 +222,8 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
221
222
  "major",
222
223
  "minor",
223
224
  "pentatonic"
224
- ]).default(TOKEN_QUOTA_DEFAULT_MUSIC.style)
225
+ ]).default(TOKEN_QUOTA_DEFAULT_MUSIC.style),
226
+ onlyCurrentSession: z.boolean().default(TOKEN_QUOTA_DEFAULT_MUSIC.onlyCurrentSession)
225
227
  }).default({ ...TOKEN_QUOTA_DEFAULT_MUSIC })
226
228
  });
227
229
  /** Two-digit zero-pad helper. */
@@ -336,7 +338,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
336
338
  music: {
337
339
  enabled: doc.music?.enabled ?? TOKEN_QUOTA_DEFAULT_MUSIC.enabled,
338
340
  volume: doc.music?.volume ?? TOKEN_QUOTA_DEFAULT_MUSIC.volume,
339
- style: doc.music?.style ?? TOKEN_QUOTA_DEFAULT_MUSIC.style
341
+ style: doc.music?.style ?? TOKEN_QUOTA_DEFAULT_MUSIC.style,
342
+ onlyCurrentSession: doc.music?.onlyCurrentSession ?? TOKEN_QUOTA_DEFAULT_MUSIC.onlyCurrentSession
340
343
  }
341
344
  };
342
345
  };
@@ -504,7 +507,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
504
507
  return this.upgradeCheck;
505
508
  }
506
509
  async doRefreshUpgrade() {
507
- const current = "0.1.24";
510
+ const current = "0.1.26";
508
511
  let latest;
509
512
  try {
510
513
  const response = await fetch(REGISTRY_LATEST_URL, {
@@ -26,6 +26,8 @@ export interface TokenQuotaPanelInjected {
26
26
  setMusicVolume: (volume: number) => void;
27
27
  /** Change the live-music harmonic style. */
28
28
  setMusicStyle: (style: TokenQuotaMusicStyle) => void;
29
+ /** Toggle whether the soundtrack follows only the current session. */
30
+ setMusicOnlyCurrentSession: (only: boolean) => void;
29
31
  /** Ask the Host to check the registry right now, then refresh the snapshot. */
30
32
  checkUpdatesNow: () => void;
31
33
  /** Ask the Host to clear log entries before a date, then refresh the log. */
@@ -38,5 +40,5 @@ export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & Prop
38
40
  * @param props - composed slot props.
39
41
  * @returns the panel element tree.
40
42
  */
41
- export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
43
+ export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
42
44
  //# sourceMappingURL=TokenQuotaPanel.d.ts.map
@@ -86,7 +86,7 @@ function loadPanelPos() {
86
86
  * @param props - composed slot props.
87
87
  * @returns the panel element tree.
88
88
  */
89
- export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, useStore, actions, useSessions, }) {
89
+ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }) {
90
90
  const [collapsed, setCollapsed] = useState(false);
91
91
  const [drafts, setDrafts] = useState({});
92
92
  const [editingKey, setEditingKey] = useState(null);
@@ -132,6 +132,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
132
132
  const musicEnabled = useStore(s => s.musicEnabled);
133
133
  const musicVolume = useStore(s => s.musicVolume);
134
134
  const musicStyle = useStore(s => s.musicStyle);
135
+ const musicOnlyCurrentSession = useStore(s => s.musicOnlyCurrentSession);
135
136
  const upgrade = useStore(s => s.upgrade);
136
137
  const upgradeDismissed = useStore(s => s.upgradeDismissed);
137
138
  const checkingUpdates = useStore(s => s.checkingUpdates);
@@ -337,7 +338,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
337
338
  hour: reset?.hour ?? 0,
338
339
  minute: Number(event.target.value),
339
340
  });
340
- }, 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("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
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
341
342
  ? t('checkingUpdates')
342
343
  : lastCheckResult === 'up-to-date'
343
344
  ? t('checkUpToDate')
@@ -36,6 +36,8 @@ export function apply(ctx) {
36
36
  let lastMonitored = null;
37
37
  let lastOnFull = 'stop';
38
38
  let lastReset = null;
39
+ // Whether the soundtrack is scoped to the currently-visible session only.
40
+ let lastOnlyCurrentSession = true;
39
41
  // Live music engine: subscribes to the plugin's SSE action stream and
40
42
  // composes from host events. Web Audio plays by default; a detected MIDI
41
43
  // output is driven simultaneously.
@@ -49,15 +51,23 @@ export function apply(ctx) {
49
51
  catch {
50
52
  return;
51
53
  }
52
- // Only play actions for the currently-visible session; a root overlay
53
- // panel isn't bound to a specific session, but its display tracks
54
+ // When scoped, only play actions for the currently-visible session; a root
55
+ // overlay panel isn't bound to a specific session, but its display tracks
54
56
  // useSessions(s => s.current), so keeping the soundtrack on the same
55
57
  // session avoids a cacophony from background conversations.
56
- if (lastSessionId !== undefined && action.sessionId !== String(lastSessionId))
58
+ if (lastOnlyCurrentSession && lastSessionId !== undefined
59
+ && action.sessionId !== String(lastSessionId))
57
60
  return;
58
61
  music.onAction(action);
59
62
  });
60
63
  ctx.effect(() => () => { sse.close(); }, 'token-quota-ui: music SSE');
64
+ /** Build a writable music-settings clone with defaults applied. */
65
+ const musicDefaults = (raw) => ({
66
+ enabled: raw?.enabled ?? false,
67
+ volume: raw?.volume ?? 0.5,
68
+ style: raw?.style ?? 'major',
69
+ onlyCurrentSession: raw?.onlyCurrentSession ?? true,
70
+ });
61
71
  const keyOf = (selection) => {
62
72
  if (selection === null)
63
73
  return undefined;
@@ -213,13 +223,15 @@ export function apply(ctx) {
213
223
  bound?.setCheckUpdates(doc?.checkUpdates ?? true);
214
224
  bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
215
225
  bound?.setReset(lastReset);
216
- const musicSettings = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
226
+ const musicSettings = musicDefaults(doc?.music);
227
+ lastOnlyCurrentSession = musicSettings.onlyCurrentSession;
217
228
  music.setEnabled(musicSettings.enabled);
218
229
  music.setVolume(musicSettings.volume);
219
230
  music.setStyle(musicSettings.style);
220
231
  bound?.setMusicEnabled(musicSettings.enabled);
221
232
  bound?.setMusicVolume(musicSettings.volume);
222
233
  bound?.setMusicStyle(musicSettings.style);
234
+ bound?.setMusicOnlyCurrentSession(musicSettings.onlyCurrentSession);
223
235
  void fetch('/token-quota', { headers: { accept: 'application/json' } }).then((response) => {
224
236
  if (!response.ok) {
225
237
  bound?.setError(`quota route: ${String(response.status)}`);
@@ -289,7 +301,7 @@ export function apply(ctx) {
289
301
  };
290
302
  const setMusicEnabled = (enabled) => {
291
303
  const doc = scope.getSnapshot().value;
292
- const current = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
304
+ const current = musicDefaults(doc?.music);
293
305
  current.enabled = enabled;
294
306
  void scope.set('music', current);
295
307
  // First enable: start audio inside this user gesture (the panel switch
@@ -300,16 +312,22 @@ export function apply(ctx) {
300
312
  };
301
313
  const setMusicVolume = (volume) => {
302
314
  const doc = scope.getSnapshot().value;
303
- const current = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
315
+ const current = musicDefaults(doc?.music);
304
316
  current.volume = Math.max(0, Math.min(1, volume));
305
317
  void scope.set('music', current);
306
318
  };
307
319
  const setMusicStyle = (style) => {
308
320
  const doc = scope.getSnapshot().value;
309
- const current = { ...{ enabled: false, volume: 0.5, style: 'major' }, ...(doc?.music ?? {}) };
321
+ const current = musicDefaults(doc?.music);
310
322
  current.style = style;
311
323
  void scope.set('music', current);
312
324
  };
325
+ const setMusicOnlyCurrentSession = (only) => {
326
+ const doc = scope.getSnapshot().value;
327
+ const current = musicDefaults(doc?.music);
328
+ current.onlyCurrentSession = only;
329
+ void scope.set('music', current);
330
+ };
313
331
  /** Ask the Host to check the registry right now, then refresh the snapshot. */
314
332
  const checkUpdatesNow = () => {
315
333
  bound?.setCheckingUpdates(true);
@@ -397,7 +415,7 @@ export function apply(ctx) {
397
415
  return {
398
416
  load, setLimit, selectModel, setMonitored, setOnFull, setReset,
399
417
  setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle,
400
- setMusicEnabled, setMusicVolume, setMusicStyle,
418
+ setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession,
401
419
  };
402
420
  };
403
421
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
@@ -73,6 +73,8 @@ export declare const zh: {
73
73
  musicStyleMajor: string;
74
74
  musicStyleMinor: string;
75
75
  musicStylePentatonic: string;
76
+ musicOnlyCurrentSessionLabel: string;
77
+ musicOnlyCurrentSessionHint: string;
76
78
  checkUpdatesNow: string;
77
79
  checkingUpdates: string;
78
80
  checkUpToDate: string;
@@ -149,6 +151,8 @@ export declare const en: {
149
151
  musicStyleMajor: string;
150
152
  musicStyleMinor: string;
151
153
  musicStylePentatonic: string;
154
+ musicOnlyCurrentSessionLabel: string;
155
+ musicOnlyCurrentSessionHint: string;
152
156
  checkUpdatesNow: string;
153
157
  checkingUpdates: string;
154
158
  checkUpToDate: string;
@@ -73,6 +73,8 @@ export const zh = {
73
73
  musicStyleMajor: '大调',
74
74
  musicStyleMinor: '小调',
75
75
  musicStylePentatonic: '五声',
76
+ musicOnlyCurrentSessionLabel: '只播当前会话',
77
+ musicOnlyCurrentSessionHint: '开启后仅当前窗口的会话有音乐,后台子任务静音;关闭则所有会话都参与作曲',
76
78
  checkUpdatesNow: '立即检查',
77
79
  checkingUpdates: '检查中…',
78
80
  checkUpToDate: '已是最新',
@@ -147,6 +149,8 @@ export const en = {
147
149
  musicStyleMajor: 'Major',
148
150
  musicStyleMinor: 'Minor',
149
151
  musicStylePentatonic: 'Pentatonic',
152
+ musicOnlyCurrentSessionLabel: 'Current session only',
153
+ musicOnlyCurrentSessionHint: 'When on, only the visible session drives the soundtrack (background tasks stay silent); when off, every session contributes.',
150
154
  checkUpdatesNow: 'Check now',
151
155
  checkingUpdates: 'Checking…',
152
156
  checkUpToDate: 'Up to date',
@@ -1,19 +1,29 @@
1
1
  /**
2
- * Live-composed soundtrack driven by host actions (turns, steps, tool calls,
3
- * etc.). Outputs through Web Audio (built-in synthesis) by default, and also
4
- * sends MIDI NoteOn/NoteOff to the first available MIDI output when one is
5
- * detected (both play simultaneously when MIDI is present — use the panel
6
- * volume for overall level, the MIDI output has its own gain on the receiver).
2
+ * Live-composed ambient soundtrack driven by host actions (turns, steps,
3
+ * tool calls, etc.). Three layers:
7
4
  *
8
- * The composition is intentionally simple: a fixed chord progression under a
9
- * scale-constrained melody, with one short musical gesture per host action.
10
- * It is meant as gentle ambience while waiting for LLM responses, not as a
11
- * full piece — everything stays in key so you can never hit a wrong note.
5
+ * - **Pad**: a sustained three-note chord (triangle waves) that fades in when
6
+ * a request starts and crossfades to the next chord on every step.
7
+ * - **Bass**: one long root note per beat (lower octave), keeps the pulse.
8
+ * - **Melody**: an eighth-note scale line that dances inside the current
9
+ * chord — always in key, never a wrong note.
10
+ *
11
+ * Outputs through Web Audio (built-in synthesis) by default, and also sends
12
+ * MIDI NoteOn/NoteOff to the first available MIDI output when one is detected
13
+ * (both play simultaneously when MIDI is present — use the panel volume for
14
+ * overall level, the MIDI output has its own gain on the receiver).
12
15
  *
13
16
  * @module @jxgame2020/dsh-token-quota/music
14
17
  */
15
18
  import type { TokenQuotaMusicAction, TokenQuotaMusicStyle } from '../types.ts';
16
- /** Live-composed music engine driven by host actions. */
19
+ /**
20
+ * Live-composed music engine driven by host actions.
21
+ *
22
+ * The engine is idle until `ensureStarted()` is called from a user gesture
23
+ * (the panel switch), and only produces sound while `enabled` is true AND a
24
+ * request is in flight (pad + bass + melody are gated by `startPad` /
25
+ * `stopPad`).
26
+ */
17
27
  export declare class TokenQuotaMusic {
18
28
  private audio;
19
29
  private midiOutput;
@@ -21,26 +31,23 @@ export declare class TokenQuotaMusic {
21
31
  private enabled;
22
32
  private volume;
23
33
  private style;
24
- /** Current position in the chord progression (advances per step). */
25
34
  private chordIndex;
26
- /** Step counter — used for melody degree selection. */
27
35
  private stepCount;
28
- /** Tool counter — used for tool-note pitch. */
29
36
  private toolCount;
30
- /**
31
- * Progress layer: while a request is in flight (thinking + streaming) the
32
- * engine ticks a quiet scale-walking background note every tick so the wait
33
- * is audible. Cleared on assistant/message, assistant/attempt or turn/end.
34
- */
35
- private progressTimer;
36
- /** Direction of the progress walk: +1 up the scale, -1 down. */
37
- private progressDir;
38
- /** Scale degree of the next progress note. */
39
- private progressDegree;
40
- /** Base MIDI note of the current key. C4 = middle C. */
37
+ private padOscs;
38
+ private padGain;
39
+ private padTarget;
40
+ private padActive;
41
+ private beatTimer;
42
+ private beatIndex;
43
+ private melodyDirection;
44
+ private melodyDegree;
45
+ private currentRoot;
46
+ private currentQuality;
47
+ /** Base MIDI note of the key. C4 = middle C. */
41
48
  private static readonly BASE_MIDI;
42
- /** Tick interval of the progress layer (ms). */
43
- private static readonly PROGRESS_TICK_MS;
49
+ /** Beat length in ms — eighth note = beat / 2. */
50
+ private static readonly BEAT_MS;
44
51
  /** True once the audio context has been started (needs a user gesture). */
45
52
  get started(): boolean;
46
53
  /**
@@ -52,16 +59,18 @@ export declare class TokenQuotaMusic {
52
59
  setEnabled(value: boolean): void;
53
60
  setVolume(value: number): void;
54
61
  setStyle(value: TokenQuotaMusicStyle): void;
55
- /** Start the quiet in-flight background loop (idempotent). */
56
- private startProgress;
57
- /** Stop the in-flight background loop. */
58
- private stopProgress;
59
62
  /** Handle one incoming host action and turn it into sound. */
60
63
  onAction(action: TokenQuotaMusicAction): void;
61
- private currentChordRoot;
62
- private currentChordQuality;
63
- /** Intervals of an ascending arpeggio from the chord root. */
64
- private arpeggioDegrees;
64
+ /** Start the pad from silence, fading in to the target level. */
65
+ private startPad;
66
+ /** Smoothly morph the pad to a new root + quality. */
67
+ private crossfadePad;
68
+ /** Fade the pad to silence, then stop the oscillators. */
69
+ private stopPad;
70
+ /** Start the per-beat clock that drives bass and melody. */
71
+ private startBeatClock;
72
+ private stopBeatClock;
73
+ private stopAll;
65
74
  /** Play one note: Web Audio + MIDI simultaneously when available. */
66
75
  private playNote;
67
76
  /** Play a three-note chord plus a bass octave below. */