@jxgame2020/dsh-token-quota 0.1.30 → 0.1.32
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 +439 -322
- package/lib/index.js +5 -2
- package/lib/types/client/TokenQuotaPanel.d.ts +3 -1
- package/lib/types/client/TokenQuotaPanel.js +50 -13
- package/lib/types/client/index.js +30 -0
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.js +4 -0
- package/lib/types/client/music.d.ts +2 -0
- package/lib/types/client/music.js +4 -0
- package/lib/types/client/store.d.ts +16 -2
- package/lib/types/client/store.js +36 -3
- package/lib/types/index.js +4 -1
- package/lib/types/types.d.ts +6 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -214,6 +214,7 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
|
|
|
214
214
|
]).default("stop"),
|
|
215
215
|
checkUpdates: z.boolean().default(true),
|
|
216
216
|
dimWhenIdle: z.boolean().default(false),
|
|
217
|
+
order: z.array(z.string()).default([]),
|
|
217
218
|
reset: z.any().default(null),
|
|
218
219
|
music: z.object({
|
|
219
220
|
enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_MUSIC.enabled),
|
|
@@ -281,7 +282,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
281
282
|
onFull: "stop",
|
|
282
283
|
checkUpdates: true,
|
|
283
284
|
dimWhenIdle: false,
|
|
284
|
-
music: { ...TOKEN_QUOTA_DEFAULT_MUSIC }
|
|
285
|
+
music: { ...TOKEN_QUOTA_DEFAULT_MUSIC },
|
|
286
|
+
order: []
|
|
285
287
|
});
|
|
286
288
|
/** Whether update checks are enabled (mirrors the settings document). */
|
|
287
289
|
checkUpdates = true;
|
|
@@ -334,6 +336,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
334
336
|
onFull: doc.onFull === "switchPriority" ? "switchAll" : doc.onFull ?? "stop",
|
|
335
337
|
checkUpdates: doc.checkUpdates ?? true,
|
|
336
338
|
dimWhenIdle: doc.dimWhenIdle ?? false,
|
|
339
|
+
order: Array.isArray(doc.order) ? doc.order : [],
|
|
337
340
|
reset: doc.reset ?? void 0,
|
|
338
341
|
music: {
|
|
339
342
|
enabled: doc.music?.enabled ?? TOKEN_QUOTA_DEFAULT_MUSIC.enabled,
|
|
@@ -515,7 +518,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
|
|
|
515
518
|
return this.upgradeCheck;
|
|
516
519
|
}
|
|
517
520
|
async doRefreshUpgrade() {
|
|
518
|
-
const current = "0.1.
|
|
521
|
+
const current = "0.1.32";
|
|
519
522
|
let latest;
|
|
520
523
|
try {
|
|
521
524
|
const response = await fetch(REGISTRY_LATEST_URL, {
|
|
@@ -20,6 +20,8 @@ export interface TokenQuotaPanelInjected {
|
|
|
20
20
|
setCheckUpdates: (enabled: boolean) => void;
|
|
21
21
|
/** Persist whether the panel dims while the pointer is away / typing. */
|
|
22
22
|
setDimWhenIdle: (enabled: boolean) => void;
|
|
23
|
+
/** Persist the drag-to-reorder display order of model rows. */
|
|
24
|
+
setModelOrder: (order: string[]) => void;
|
|
23
25
|
/** Toggle the live music output (requires a user gesture to start audio). */
|
|
24
26
|
setMusicEnabled: (enabled: boolean) => void;
|
|
25
27
|
/** Change the live-music master volume 0..1. */
|
|
@@ -40,5 +42,5 @@ export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & Prop
|
|
|
40
42
|
* @param props - composed slot props.
|
|
41
43
|
* @returns the panel element tree.
|
|
42
44
|
*/
|
|
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;
|
|
45
|
+
export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
|
|
44
46
|
//# sourceMappingURL=TokenQuotaPanel.d.ts.map
|
|
@@ -10,7 +10,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
10
10
|
* callbacks.
|
|
11
11
|
*/
|
|
12
12
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
13
|
-
import { mergeModelRows } from "./store.js";
|
|
13
|
+
import { mergeModelRows, reorderKeys } from "./store.js";
|
|
14
14
|
import css from './TokenQuotaPanel.module.css';
|
|
15
15
|
/** Compact a token count for display. */
|
|
16
16
|
function formatTokens(n) {
|
|
@@ -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, setMusicOnlyCurrentSession, useStore, actions, useSessions, }) {
|
|
89
|
+
export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, 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);
|
|
@@ -95,8 +95,12 @@ 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
|
|
98
|
+
// Active settings-dialog tab: quota limits vs. live-music vs. misc.
|
|
99
99
|
const [settingsTab, setSettingsTab] = useState('quota');
|
|
100
|
+
// Drag-to-reorder state for the model rows: the key being dragged and
|
|
101
|
+
// the key currently hovered as the drop target.
|
|
102
|
+
const [dragKey, setDragKey] = useState(null);
|
|
103
|
+
const [dropKey, setDropKey] = useState(null);
|
|
100
104
|
// Copy-button feedback: which command was just copied (label flips to
|
|
101
105
|
// 「已复制」for a moment so the click is perceivable).
|
|
102
106
|
const [copiedCmd, setCopiedCmd] = useState(null);
|
|
@@ -131,6 +135,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
131
135
|
const onFull = useStore(s => s.onFull);
|
|
132
136
|
const checkUpdates = useStore(s => s.checkUpdates);
|
|
133
137
|
const dimWhenIdle = useStore(s => s.dimWhenIdle);
|
|
138
|
+
const order = useStore(s => s.order);
|
|
134
139
|
const musicEnabled = useStore(s => s.musicEnabled);
|
|
135
140
|
const musicVolume = useStore(s => s.musicVolume);
|
|
136
141
|
const musicStyle = useStore(s => s.musicStyle);
|
|
@@ -198,12 +203,27 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
198
203
|
clearTimeout(copyTimerRef.current);
|
|
199
204
|
};
|
|
200
205
|
}, []);
|
|
201
|
-
const rows = useMemo(() => mergeModelRows(groups, snapshot, current), [groups, snapshot, current]);
|
|
202
|
-
// All directory models, flattened for the monitoring picker
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
206
|
+
const rows = useMemo(() => mergeModelRows(groups, snapshot, current, order), [groups, snapshot, current, order]);
|
|
207
|
+
// All directory models, flattened for the monitoring picker — same drag
|
|
208
|
+
// order as the panel rows so both lists read identically.
|
|
209
|
+
const allModels = useMemo(() => {
|
|
210
|
+
const list = groups.flatMap(group => group.models.map((model) => ({
|
|
211
|
+
key: `${group.id}/${model.id}`,
|
|
212
|
+
name: model.name,
|
|
213
|
+
})));
|
|
214
|
+
const rank = new Map(order.map((key, index) => [key, index]));
|
|
215
|
+
return list.sort((left, right) => {
|
|
216
|
+
const leftRank = rank.get(left.key);
|
|
217
|
+
const rightRank = rank.get(right.key);
|
|
218
|
+
if (leftRank !== undefined && rightRank !== undefined)
|
|
219
|
+
return leftRank - rightRank;
|
|
220
|
+
if (leftRank !== undefined)
|
|
221
|
+
return -1;
|
|
222
|
+
if (rightRank !== undefined)
|
|
223
|
+
return 1;
|
|
224
|
+
return left.key.localeCompare(right.key);
|
|
225
|
+
});
|
|
226
|
+
}, [groups, order]);
|
|
207
227
|
const isMonitoredKey = (key) => monitored === null || monitored.includes(key);
|
|
208
228
|
// Log dialog: filter by search, then paginate.
|
|
209
229
|
const filteredLogEntries = useMemo(() => {
|
|
@@ -309,11 +329,28 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
309
329
|
? css.fillWarn
|
|
310
330
|
: css.fillIdle;
|
|
311
331
|
const editing = editingKey === row.key;
|
|
312
|
-
return (_jsxs("div", { className: css.row
|
|
332
|
+
return (_jsxs("div", { className: `${css.row}${dragKey === row.key ? ` ${css.rowDragging}` : ''}${dropKey === row.key && dragKey !== row.key ? ` ${css.rowDropTarget}` : ''}`, onDragOver: (event) => {
|
|
333
|
+
if (dragKey === null || dragKey === row.key)
|
|
334
|
+
return;
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
setDropKey(row.key);
|
|
337
|
+
}, onDrop: (event) => {
|
|
338
|
+
event.preventDefault();
|
|
339
|
+
if (dragKey !== null && dragKey !== row.key) {
|
|
340
|
+
setModelOrder(reorderKeys(rows.map(r => r.key), dragKey, row.key));
|
|
341
|
+
}
|
|
342
|
+
setDragKey(null);
|
|
343
|
+
setDropKey(null);
|
|
344
|
+
}, onDragEnd: () => { setDragKey(null); setDropKey(null); }, children: [_jsxs("div", { className: css.rowHeader, children: [_jsx("span", { className: css.rowDrag, draggable: true, title: t('reorderHint'), onDragStart: (event) => {
|
|
345
|
+
setDragKey(row.key);
|
|
346
|
+
event.dataTransfer.effectAllowed = 'move';
|
|
347
|
+
// Firefox only starts a drag when some data is set.
|
|
348
|
+
event.dataTransfer.setData('text/plain', row.key);
|
|
349
|
+
}, children: "\u283F" }), _jsxs("span", { className: css.rowName, title: row.key, children: [row.name, row.current && _jsx("span", { className: css.currentBadge, children: t('current') }), sessionId !== undefined && !row.current && (_jsx("button", { type: "button", className: css.selectBtn, onClick: () => { selectModel(sessionId, row.provider, row.model); }, children: t('select') }))] }), _jsxs("span", { className: css.rowMeta, children: [row.limit > 0
|
|
313
350
|
? `${formatTokens(row.used)} / ${formatTokens(row.limit)}`
|
|
314
351
|
: `${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')
|
|
315
352
|
commitLimit(row); } }), _jsx("button", { type: "button", className: css.save, onClick: () => { commitLimit(row); }, children: t('save') })] }))] }, row.key));
|
|
316
|
-
})] }), dialogOpen && (_jsxs("div", { ref: settingsRef, className: css.dialog
|
|
353
|
+
})] }), dialogOpen && (_jsxs("div", { ref: settingsRef, className: `${css.dialog} ${css.settingsDialog}`, 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') }), _jsx("button", { type: "button", role: "tab", "aria-selected": settingsTab === 'other', className: `${css.dialogTab}${settingsTab === 'other' ? ` ${css.dialogTabActive}` : ''}`, onClick: () => { setSettingsTab('other'); }, children: t('settingsTabOther') })] }), 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
354
|
const base = monitored === null ? allModels.map(m => m.key) : monitored;
|
|
318
355
|
const next = new Set(base);
|
|
319
356
|
if (event.target.checked)
|
|
@@ -322,7 +359,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
322
359
|
next.delete(model.key);
|
|
323
360
|
// Auto-save on every change; all selected stores as null.
|
|
324
361
|
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) => {
|
|
362
|
+
} }), _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)))] })] })), settingsTab === 'other' && (_jsxs(_Fragment, { children: [_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
363
|
setReset({
|
|
327
364
|
offsetHours: Number(event.target.value),
|
|
328
365
|
hour: reset?.hour ?? 0,
|
|
@@ -340,7 +377,7 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
|
|
|
340
377
|
hour: reset?.hour ?? 0,
|
|
341
378
|
minute: Number(event.target.value),
|
|
342
379
|
});
|
|
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(
|
|
380
|
+
}, 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(_Fragment, { children: [_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("div", { className: css.dialogSection, 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 === 'other' && (_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
381
|
? t('checkingUpdates')
|
|
345
382
|
: lastCheckResult === 'up-to-date'
|
|
346
383
|
? t('checkUpToDate')
|
|
@@ -66,6 +66,29 @@ export function apply(ctx) {
|
|
|
66
66
|
music.onAction(action);
|
|
67
67
|
});
|
|
68
68
|
ctx.effect(() => () => { sse.close(); }, 'token-quota-ui: music SSE');
|
|
69
|
+
// Browser autoplay policy: an AudioContext only starts inside a user
|
|
70
|
+
// gesture. When the switch was left ON and the page reloads, the setting
|
|
71
|
+
// is enabled but no gesture has happened yet — so the engine stays silent
|
|
72
|
+
// until the user toggles it off/on. Arm a one-shot gesture listener that
|
|
73
|
+
// starts the engine on the first click / keypress instead.
|
|
74
|
+
const autoStartMusic = () => {
|
|
75
|
+
if (!music.isEnabled || music.started) {
|
|
76
|
+
if (music.started) {
|
|
77
|
+
window.removeEventListener('pointerdown', autoStartMusic);
|
|
78
|
+
window.removeEventListener('keydown', autoStartMusic);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
void music.ensureStarted();
|
|
83
|
+
window.removeEventListener('pointerdown', autoStartMusic);
|
|
84
|
+
window.removeEventListener('keydown', autoStartMusic);
|
|
85
|
+
};
|
|
86
|
+
window.addEventListener('pointerdown', autoStartMusic);
|
|
87
|
+
window.addEventListener('keydown', autoStartMusic);
|
|
88
|
+
ctx.effect(() => () => {
|
|
89
|
+
window.removeEventListener('pointerdown', autoStartMusic);
|
|
90
|
+
window.removeEventListener('keydown', autoStartMusic);
|
|
91
|
+
}, 'token-quota-ui: music autostart gestures');
|
|
69
92
|
/** Build a writable music-settings clone with defaults applied. */
|
|
70
93
|
const musicDefaults = (raw) => ({
|
|
71
94
|
enabled: raw?.enabled ?? false,
|
|
@@ -227,6 +250,7 @@ export function apply(ctx) {
|
|
|
227
250
|
bound?.setSettings(lastMonitored, lastOnFull);
|
|
228
251
|
bound?.setCheckUpdates(doc?.checkUpdates ?? true);
|
|
229
252
|
bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
|
|
253
|
+
bound?.setOrder(Array.isArray(doc?.order) ? doc.order : []);
|
|
230
254
|
bound?.setReset(lastReset);
|
|
231
255
|
const musicSettings = musicDefaults(doc?.music);
|
|
232
256
|
lastOnlyCurrentSession = musicSettings.onlyCurrentSession;
|
|
@@ -309,6 +333,11 @@ export function apply(ctx) {
|
|
|
309
333
|
bound?.setDimWhenIdle(enabled);
|
|
310
334
|
void scope.set('dimWhenIdle', enabled);
|
|
311
335
|
};
|
|
336
|
+
/** Persist the drag-to-reorder display order of model rows. */
|
|
337
|
+
const setModelOrder = (order) => {
|
|
338
|
+
bound?.setOrder(order);
|
|
339
|
+
void scope.set('order', order);
|
|
340
|
+
};
|
|
312
341
|
const setMusicEnabled = (enabled) => {
|
|
313
342
|
const doc = scope.getSnapshot().value;
|
|
314
343
|
const current = musicDefaults(doc?.music);
|
|
@@ -426,6 +455,7 @@ export function apply(ctx) {
|
|
|
426
455
|
load, setLimit, selectModel, setMonitored, setOnFull, setReset,
|
|
427
456
|
setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle,
|
|
428
457
|
setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession,
|
|
458
|
+
setModelOrder,
|
|
429
459
|
};
|
|
430
460
|
};
|
|
431
461
|
ctx.slots.inject('shell.overlay', () => ctx.slots.register({
|
|
@@ -27,6 +27,8 @@ export declare const zh: {
|
|
|
27
27
|
settingsTitle: string;
|
|
28
28
|
settingsTabQuota: string;
|
|
29
29
|
settingsTabMusic: string;
|
|
30
|
+
settingsTabOther: string;
|
|
31
|
+
reorderHint: string;
|
|
30
32
|
monitorLabel: string;
|
|
31
33
|
monitorHint: string;
|
|
32
34
|
monitorAll: string;
|
|
@@ -109,6 +111,8 @@ export declare const en: {
|
|
|
109
111
|
settingsTitle: string;
|
|
110
112
|
settingsTabQuota: string;
|
|
111
113
|
settingsTabMusic: string;
|
|
114
|
+
settingsTabOther: string;
|
|
115
|
+
reorderHint: string;
|
|
112
116
|
monitorLabel: string;
|
|
113
117
|
monitorHint: string;
|
|
114
118
|
monitorAll: string;
|
|
@@ -27,6 +27,8 @@ export const zh = {
|
|
|
27
27
|
settingsTitle: '设置',
|
|
28
28
|
settingsTabQuota: '限额设置',
|
|
29
29
|
settingsTabMusic: '音乐设置',
|
|
30
|
+
settingsTabOther: '其它',
|
|
31
|
+
reorderHint: '拖动排序',
|
|
30
32
|
monitorLabel: '监控模型',
|
|
31
33
|
monitorHint: '未勾选的模型不显示、不计入用量、不受限额限制',
|
|
32
34
|
monitorAll: '全部',
|
|
@@ -107,6 +109,8 @@ export const en = {
|
|
|
107
109
|
settingsTitle: 'Settings',
|
|
108
110
|
settingsTabQuota: 'Quota',
|
|
109
111
|
settingsTabMusic: 'Music',
|
|
112
|
+
settingsTabOther: 'Other',
|
|
113
|
+
reorderHint: 'Drag to reorder',
|
|
110
114
|
monitorLabel: 'Monitored models',
|
|
111
115
|
monitorHint: 'Unchecked models are hidden, not metered, and never capped',
|
|
112
116
|
monitorAll: 'All',
|
|
@@ -56,6 +56,8 @@ export declare class TokenQuotaMusic {
|
|
|
56
56
|
private static readonly STREAM_TIMEOUT_MS;
|
|
57
57
|
/** True once the audio context has been started (needs a user gesture). */
|
|
58
58
|
get started(): boolean;
|
|
59
|
+
/** True when the master switch is on (mirrors the settings document). */
|
|
60
|
+
get isEnabled(): boolean;
|
|
59
61
|
/**
|
|
60
62
|
* Start the audio context and probe for a MIDI output (once). Must be
|
|
61
63
|
* called inside a user gesture so the AudioContext can resume.
|
|
@@ -98,6 +98,10 @@ export class TokenQuotaMusic {
|
|
|
98
98
|
get started() {
|
|
99
99
|
return this.audio !== null;
|
|
100
100
|
}
|
|
101
|
+
/** True when the master switch is on (mirrors the settings document). */
|
|
102
|
+
get isEnabled() {
|
|
103
|
+
return this.enabled;
|
|
104
|
+
}
|
|
101
105
|
/**
|
|
102
106
|
* Start the audio context and probe for a MIDI output (once). Must be
|
|
103
107
|
* called inside a user gesture so the AudioContext can resume.
|
|
@@ -50,6 +50,11 @@ export interface TokenQuotaPanelState {
|
|
|
50
50
|
checkUpdates: boolean;
|
|
51
51
|
/** Whether the panel dims while the pointer is away / while typing. */
|
|
52
52
|
dimWhenIdle: boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Preferred display order of model keys, written by dragging panel rows.
|
|
55
|
+
* Keys not listed keep their catalog order after the listed ones.
|
|
56
|
+
*/
|
|
57
|
+
order: string[];
|
|
53
58
|
/** Live music settings (mirrored from the settings document). */
|
|
54
59
|
musicEnabled: boolean;
|
|
55
60
|
musicVolume: number;
|
|
@@ -97,6 +102,8 @@ export type TokenQuotaPanelActions = {
|
|
|
97
102
|
setSettings: (d: TokenQuotaPanelState, monitored: string[] | null, onFull: TokenQuotaFullAction) => void;
|
|
98
103
|
setCheckUpdates: (d: TokenQuotaPanelState, checkUpdates: boolean) => void;
|
|
99
104
|
setDimWhenIdle: (d: TokenQuotaPanelState, dimWhenIdle: boolean) => void;
|
|
105
|
+
/** Replace the drag-to-reorder model display order. */
|
|
106
|
+
setOrder: (d: TokenQuotaPanelState, order: string[]) => void;
|
|
100
107
|
setMusicEnabled: (d: TokenQuotaPanelState, enabled: boolean) => void;
|
|
101
108
|
setMusicVolume: (d: TokenQuotaPanelState, volume: number) => void;
|
|
102
109
|
setMusicStyle: (d: TokenQuotaPanelState, style: TokenQuotaMusicStyle) => void;
|
|
@@ -131,7 +138,14 @@ export declare function createTokenQuotaPanelStore(): EngineStoreHandle<TokenQuo
|
|
|
131
138
|
* @param groups - advisory provider groups of the current session.
|
|
132
139
|
* @param snapshot - latest quota snapshot, or null before the first one.
|
|
133
140
|
* @param current - current model selection reported by the Host.
|
|
134
|
-
* @
|
|
141
|
+
* @param order - user's drag-to-reorder preference (model keys); listed keys
|
|
142
|
+
* come first in that exact order, the rest follow in catalog order.
|
|
143
|
+
* @returns rows in display order.
|
|
144
|
+
*/
|
|
145
|
+
export declare function mergeModelRows(groups: readonly ModelProviderGroup[], snapshot: TokenQuotaSnapshot | null, current: ModelSelection | null, order?: readonly string[]): ModelQuotaRow[];
|
|
146
|
+
/**
|
|
147
|
+
* Move one key to another key's position, returning the full reordered key
|
|
148
|
+
* list (the shape persisted in the settings document's `order`).
|
|
135
149
|
*/
|
|
136
|
-
export declare function
|
|
150
|
+
export declare function reorderKeys(keys: readonly string[], from: string, to: string): string[];
|
|
137
151
|
//# sourceMappingURL=store.d.ts.map
|
|
@@ -28,6 +28,7 @@ export function createTokenQuotaPanelStore() {
|
|
|
28
28
|
onFull: 'stop',
|
|
29
29
|
checkUpdates: true,
|
|
30
30
|
dimWhenIdle: false,
|
|
31
|
+
order: [],
|
|
31
32
|
musicEnabled: false,
|
|
32
33
|
musicVolume: 0.5,
|
|
33
34
|
musicStyle: 'pentatonic',
|
|
@@ -57,6 +58,7 @@ export function createTokenQuotaPanelStore() {
|
|
|
57
58
|
setSettings: (d, monitored, onFull) => { d.monitored = monitored; d.onFull = onFull; },
|
|
58
59
|
setCheckUpdates: (d, checkUpdates) => { d.checkUpdates = checkUpdates; },
|
|
59
60
|
setDimWhenIdle: (d, dimWhenIdle) => { d.dimWhenIdle = dimWhenIdle; },
|
|
61
|
+
setOrder: (d, order) => { d.order = order; },
|
|
60
62
|
setMusicEnabled: (d, enabled) => { d.musicEnabled = enabled; },
|
|
61
63
|
setMusicVolume: (d, volume) => { d.musicVolume = volume; },
|
|
62
64
|
setMusicStyle: (d, style) => { d.musicStyle = style; },
|
|
@@ -104,9 +106,11 @@ export function createTokenQuotaPanelStore() {
|
|
|
104
106
|
* @param groups - advisory provider groups of the current session.
|
|
105
107
|
* @param snapshot - latest quota snapshot, or null before the first one.
|
|
106
108
|
* @param current - current model selection reported by the Host.
|
|
107
|
-
* @
|
|
109
|
+
* @param order - user's drag-to-reorder preference (model keys); listed keys
|
|
110
|
+
* come first in that exact order, the rest follow in catalog order.
|
|
111
|
+
* @returns rows in display order.
|
|
108
112
|
*/
|
|
109
|
-
export function mergeModelRows(groups, snapshot, current) {
|
|
113
|
+
export function mergeModelRows(groups, snapshot, current, order = []) {
|
|
110
114
|
const entryByKey = new Map((snapshot?.entries ?? []).map(entry => [entry.key, entry]));
|
|
111
115
|
const rows = new Map();
|
|
112
116
|
for (const group of groups) {
|
|
@@ -137,6 +141,35 @@ export function mergeModelRows(groups, snapshot, current) {
|
|
|
137
141
|
current: current !== null && current.provider === entry.provider && current.model === entry.model,
|
|
138
142
|
});
|
|
139
143
|
}
|
|
140
|
-
|
|
144
|
+
// Drag preference first (in its own order), everything else after it,
|
|
145
|
+
// alphabetically — so a newly discovered model still lands somewhere stable.
|
|
146
|
+
const rank = new Map(order.map((key, index) => [key, index]));
|
|
147
|
+
return [...rows.values()].sort((left, right) => {
|
|
148
|
+
const leftRank = rank.get(left.key);
|
|
149
|
+
const rightRank = rank.get(right.key);
|
|
150
|
+
if (leftRank !== undefined && rightRank !== undefined)
|
|
151
|
+
return leftRank - rightRank;
|
|
152
|
+
if (leftRank !== undefined)
|
|
153
|
+
return -1;
|
|
154
|
+
if (rightRank !== undefined)
|
|
155
|
+
return 1;
|
|
156
|
+
return left.key.localeCompare(right.key);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Move one key to another key's position, returning the full reordered key
|
|
161
|
+
* list (the shape persisted in the settings document's `order`).
|
|
162
|
+
*/
|
|
163
|
+
export function reorderKeys(keys, from, to) {
|
|
164
|
+
const next = [...keys];
|
|
165
|
+
const fromIndex = next.indexOf(from);
|
|
166
|
+
const toIndex = next.indexOf(to);
|
|
167
|
+
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex)
|
|
168
|
+
return next;
|
|
169
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
170
|
+
if (moved === undefined)
|
|
171
|
+
return next;
|
|
172
|
+
next.splice(toIndex, 0, moved);
|
|
173
|
+
return next;
|
|
141
174
|
}
|
|
142
175
|
//# sourceMappingURL=store.js.map
|
package/lib/types/index.js
CHANGED
|
@@ -202,6 +202,8 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
|
|
|
202
202
|
checkUpdates: z.boolean().default(true),
|
|
203
203
|
// Client-side appearance preference; the host only stores it.
|
|
204
204
|
dimWhenIdle: z.boolean().default(false),
|
|
205
|
+
// Drag-to-reorder: display order of model keys, written by the panel.
|
|
206
|
+
order: z.array(z.string()).default([]),
|
|
205
207
|
// `reset` is a user-facing preference outside the validated surface: the
|
|
206
208
|
// panel writes it and the host validates the shape at runtime. `z.any` with
|
|
207
209
|
// a null default keeps it out of the strict fields above.
|
|
@@ -273,7 +275,7 @@ export class TokenQuotaService extends Service {
|
|
|
273
275
|
history = {};
|
|
274
276
|
limits = {};
|
|
275
277
|
monitored = undefined;
|
|
276
|
-
settingsSource = () => ({ limits: {}, monitored: [], onFull: 'stop', checkUpdates: true, dimWhenIdle: false, music: { ...TOKEN_QUOTA_DEFAULT_MUSIC } });
|
|
278
|
+
settingsSource = () => ({ limits: {}, monitored: [], onFull: 'stop', checkUpdates: true, dimWhenIdle: false, music: { ...TOKEN_QUOTA_DEFAULT_MUSIC }, order: [] });
|
|
277
279
|
/** Whether update checks are enabled (mirrors the settings document). */
|
|
278
280
|
checkUpdates = true;
|
|
279
281
|
/** Cached upgrade availability; recomputed by {@link refreshUpgrade}. */
|
|
@@ -337,6 +339,7 @@ export class TokenQuotaService extends Service {
|
|
|
337
339
|
onFull: doc.onFull === 'switchPriority' ? 'switchAll' : (doc.onFull ?? 'stop'),
|
|
338
340
|
checkUpdates: doc.checkUpdates ?? true,
|
|
339
341
|
dimWhenIdle: doc.dimWhenIdle ?? false,
|
|
342
|
+
order: Array.isArray(doc.order) ? doc.order : [],
|
|
340
343
|
reset: doc.reset ?? undefined,
|
|
341
344
|
music: {
|
|
342
345
|
enabled: doc.music?.enabled ?? TOKEN_QUOTA_DEFAULT_MUSIC.enabled,
|
package/lib/types/types.d.ts
CHANGED
|
@@ -143,6 +143,12 @@ export interface TokenQuotaSettings {
|
|
|
143
143
|
* an attached MIDI output. Off by default.
|
|
144
144
|
*/
|
|
145
145
|
music: TokenQuotaMusicSettings;
|
|
146
|
+
/**
|
|
147
|
+
* Preferred display order of model keys (`provider/model`), set by dragging
|
|
148
|
+
* rows in the panel. Keys not listed here keep their catalog order after the
|
|
149
|
+
* listed ones. Empty by default (pure catalog/alphabetical order).
|
|
150
|
+
*/
|
|
151
|
+
order: string[];
|
|
146
152
|
}
|
|
147
153
|
/** Token-quota plugin configuration. */
|
|
148
154
|
export interface TokenQuotaConfig {
|
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.
|
|
4
|
+
"version": "0.1.32",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|