@jxgame2020/dsh-token-quota 0.1.32 → 0.1.34

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
@@ -215,6 +215,17 @@ const TOKEN_QUOTA_SETTINGS_SCHEMA = z.object({
215
215
  checkUpdates: z.boolean().default(true),
216
216
  dimWhenIdle: z.boolean().default(false),
217
217
  order: z.array(z.string()).default([]),
218
+ notes: z.array(z.object({
219
+ id: z.string(),
220
+ title: z.string().default(""),
221
+ text: z.string().default(""),
222
+ x: z.number().default(80),
223
+ y: z.number().default(80),
224
+ width: z.number().default(220),
225
+ height: z.number().default(220),
226
+ collapsed: z.boolean().default(false),
227
+ visible: z.boolean().default(true)
228
+ })).default([]),
218
229
  reset: z.any().default(null),
219
230
  music: z.object({
220
231
  enabled: z.boolean().default(TOKEN_QUOTA_DEFAULT_MUSIC.enabled),
@@ -283,7 +294,8 @@ var TokenQuotaService = class TokenQuotaService extends Service {
283
294
  checkUpdates: true,
284
295
  dimWhenIdle: false,
285
296
  music: { ...TOKEN_QUOTA_DEFAULT_MUSIC },
286
- order: []
297
+ order: [],
298
+ notes: []
287
299
  });
288
300
  /** Whether update checks are enabled (mirrors the settings document). */
289
301
  checkUpdates = true;
@@ -337,6 +349,20 @@ var TokenQuotaService = class TokenQuotaService extends Service {
337
349
  checkUpdates: doc.checkUpdates ?? true,
338
350
  dimWhenIdle: doc.dimWhenIdle ?? false,
339
351
  order: Array.isArray(doc.order) ? doc.order : [],
352
+ notes: Array.isArray(doc.notes) ? doc.notes.slice(0, 5).flatMap((note) => {
353
+ if (typeof note.id !== "string") return [];
354
+ return [{
355
+ id: note.id,
356
+ title: note.title ?? "",
357
+ text: note.text ?? "",
358
+ x: note.x ?? 80,
359
+ y: note.y ?? 80,
360
+ width: note.width ?? 220,
361
+ height: note.height ?? 220,
362
+ collapsed: note.collapsed ?? false,
363
+ visible: note.visible ?? true
364
+ }];
365
+ }) : [],
340
366
  reset: doc.reset ?? void 0,
341
367
  music: {
342
368
  enabled: doc.music?.enabled ?? TOKEN_QUOTA_DEFAULT_MUSIC.enabled,
@@ -518,7 +544,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
518
544
  return this.upgradeCheck;
519
545
  }
520
546
  async doRefreshUpgrade() {
521
- const current = "0.1.32";
547
+ const current = "0.1.34";
522
548
  let latest;
523
549
  try {
524
550
  const response = await fetch(REGISTRY_LATEST_URL, {
@@ -1,6 +1,6 @@
1
1
  import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
2
2
  import type { SessionId } from '@deepseek-ai/dsh-session/types';
3
- import type { TokenQuotaFullAction, TokenQuotaMusicStyle, TokenQuotaReset } from '@jxgame2020/dsh-token-quota/types';
3
+ import type { TokenQuotaFullAction, TokenQuotaMusicStyle, TokenQuotaNote, TokenQuotaReset } from '@jxgame2020/dsh-token-quota/types';
4
4
  import type { createTokenQuotaPanelStore } from './store.ts';
5
5
  /** Injected business face: data loading and mutations wired in `apply`. */
6
6
  export interface TokenQuotaPanelInjected {
@@ -22,6 +22,8 @@ export interface TokenQuotaPanelInjected {
22
22
  setDimWhenIdle: (enabled: boolean) => void;
23
23
  /** Persist the drag-to-reorder display order of model rows. */
24
24
  setModelOrder: (order: string[]) => void;
25
+ /** Persist the floating scratchpad notes (content, title, placement). */
26
+ saveNotes: (notes: TokenQuotaNote[]) => void;
25
27
  /** Toggle the live music output (requires a user gesture to start audio). */
26
28
  setMusicEnabled: (enabled: boolean) => void;
27
29
  /** Change the live-music master volume 0..1. */
@@ -42,5 +44,5 @@ export type TokenQuotaPanelComponentProps = PropsRuntime<'shell.overlay'> & Prop
42
44
  * @param props - composed slot props.
43
45
  * @returns the panel element tree.
44
46
  */
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;
47
+ export declare function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }: TokenQuotaPanelComponentProps): import("react").JSX.Element;
46
48
  //# sourceMappingURL=TokenQuotaPanel.d.ts.map
@@ -10,6 +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 { TOKEN_QUOTA_MAX_NOTES } from '@jxgame2020/dsh-token-quota/types';
13
14
  import { mergeModelRows, reorderKeys } from "./store.js";
14
15
  import css from './TokenQuotaPanel.module.css';
15
16
  /** Compact a token count for display. */
@@ -63,6 +64,46 @@ function beginDrag(event, el, apply) {
63
64
  window.addEventListener('pointerup', onUp);
64
65
  window.addEventListener('pointercancel', onUp);
65
66
  }
67
+ /**
68
+ * Pointer-driven resize of a note window from its bottom-right grip. Sizes are
69
+ * absolute pixels derived from the element's rect at grab time, so the window
70
+ * keeps its top-left anchor while the pointer moves. A floor keeps a window
71
+ * from collapsing to nothing.
72
+ */
73
+ function beginResize(event, el, apply) {
74
+ if (el === null)
75
+ return;
76
+ event.preventDefault();
77
+ event.stopPropagation();
78
+ const startX = event.clientX;
79
+ const startY = event.clientY;
80
+ const rect = el.getBoundingClientRect();
81
+ const onMove = (ev) => {
82
+ apply({
83
+ width: Math.max(NOTE_MIN_WIDTH, Math.round(rect.width + (ev.clientX - startX))),
84
+ height: Math.max(NOTE_MIN_HEIGHT, Math.round(rect.height + (ev.clientY - startY))),
85
+ });
86
+ };
87
+ const onUp = () => {
88
+ window.removeEventListener('pointermove', onMove);
89
+ window.removeEventListener('pointerup', onUp);
90
+ window.removeEventListener('pointercancel', onUp);
91
+ };
92
+ window.addEventListener('pointermove', onMove);
93
+ window.addEventListener('pointerup', onUp);
94
+ window.addEventListener('pointercancel', onUp);
95
+ }
96
+ /** First visible character of a note title, for its header chip. */
97
+ function noteInitial(title) {
98
+ const trimmed = title.trim();
99
+ if (trimmed === '')
100
+ return '·';
101
+ return Array.from(trimmed)[0] ?? '·';
102
+ }
103
+ /** Default geometry of a new note; later notes cascade so they don't stack. */
104
+ const NOTE_DEFAULT_SIZE = 220;
105
+ const NOTE_MIN_WIDTH = 150;
106
+ const NOTE_MIN_HEIGHT = 70;
66
107
  /** Persisted panel placement so a dragged position survives reloads. */
67
108
  const PANEL_POS_KEY = 'dsh-token-quota:panel-pos';
68
109
  function loadPanelPos() {
@@ -86,7 +127,7 @@ function loadPanelPos() {
86
127
  * @param props - composed slot props.
87
128
  * @returns the panel element tree.
88
129
  */
89
- export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }) {
130
+ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored, setOnFull, setReset, setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle, setModelOrder, saveNotes, setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession, useStore, actions, useSessions, }) {
90
131
  const [collapsed, setCollapsed] = useState(false);
91
132
  const [drafts, setDrafts] = useState({});
92
133
  const [editingKey, setEditingKey] = useState(null);
@@ -101,6 +142,144 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
101
142
  // the key currently hovered as the drop target.
102
143
  const [dragKey, setDragKey] = useState(null);
103
144
  const [dropKey, setDropKey] = useState(null);
145
+ // ---- Floating scratchpad notes -------------------------------------------
146
+ // The notes live in local state while the panel is mounted: every keystroke
147
+ // updates this copy (so typing is never laggy) and a debounced write pushes
148
+ // it to the Host. Seeding happens exactly once, when the Host's document
149
+ // first arrives, so a background settings poll cannot clobber the text.
150
+ const storedNotes = useStore(s => s.notes);
151
+ const storedNotesLoaded = useStore(s => s.notesLoaded);
152
+ const [notes, setNotes] = useState([]);
153
+ const notesSeeded = useRef(false);
154
+ const notesTimer = useRef(null);
155
+ // Latest value awaiting a debounced write, so an unmount can flush it.
156
+ const notesPending = useRef(null);
157
+ // Context menu opened by right-clicking a note chip.
158
+ const [noteMenu, setNoteMenu] = useState(null);
159
+ // Note window that was touched last, painted above its siblings.
160
+ const [activeNote, setActiveNote] = useState(null);
161
+ // Note whose title is being edited (id) plus the in-progress draft. The
162
+ // title is a plain label until the pencil is pressed, so the title bar
163
+ // stays fully draggable the rest of the time.
164
+ const [editingTitle, setEditingTitle] = useState(null);
165
+ const [titleDraft, setTitleDraft] = useState('');
166
+ useEffect(() => {
167
+ if (!storedNotesLoaded || notesSeeded.current)
168
+ return;
169
+ notesSeeded.current = true;
170
+ setNotes(storedNotes);
171
+ }, [storedNotesLoaded, storedNotes]);
172
+ // Flush a pending debounced write when the panel goes away, so keystrokes
173
+ // typed in the last fraction of a second are not lost.
174
+ useEffect(() => () => {
175
+ if (notesTimer.current !== null)
176
+ window.clearTimeout(notesTimer.current);
177
+ const pending = notesPending.current;
178
+ if (pending !== null) {
179
+ notesPending.current = null;
180
+ saveNotes(pending);
181
+ }
182
+ }, []);
183
+ /**
184
+ * Apply a note change locally and persist it. Structural changes (create,
185
+ * close, collapse, drop) write straight through; free-form edits (typing,
186
+ * dragging, resizing) are debounced so a keystroke doesn't hit the Host.
187
+ */
188
+ const commitNotes = (next, immediate = true) => {
189
+ setNotes(next);
190
+ if (notesTimer.current !== null)
191
+ window.clearTimeout(notesTimer.current);
192
+ if (immediate) {
193
+ notesPending.current = null;
194
+ saveNotes(next);
195
+ return;
196
+ }
197
+ notesPending.current = next;
198
+ notesTimer.current = window.setTimeout(() => {
199
+ notesTimer.current = null;
200
+ const pending = notesPending.current;
201
+ notesPending.current = null;
202
+ if (pending !== null)
203
+ saveNotes(pending);
204
+ }, 400);
205
+ };
206
+ const patchNote = (id, patch, immediate = true) => {
207
+ commitNotes(notes.map(note => (note.id === id ? { ...note, ...patch } : note)), immediate);
208
+ };
209
+ /** Create a note, auto-titled 新建N with the lowest unused number. */
210
+ const createNote = () => {
211
+ if (notes.length >= TOKEN_QUOTA_MAX_NOTES)
212
+ return;
213
+ let index = 1;
214
+ const used = new Set(notes.map(note => /^新建(\d+)$/.exec(note.title.trim())?.[1]).filter(Boolean));
215
+ while (used.has(String(index)))
216
+ index += 1;
217
+ const step = notes.length * 28;
218
+ commitNotes([...notes, {
219
+ id: `note-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 7)}`,
220
+ title: `新建${index}`,
221
+ text: '',
222
+ x: 120 + step,
223
+ y: 140 + step,
224
+ width: NOTE_DEFAULT_SIZE,
225
+ height: NOTE_DEFAULT_SIZE,
226
+ collapsed: false,
227
+ visible: true,
228
+ }]);
229
+ };
230
+ /** Commit the title draft and leave edit mode. */
231
+ const commitTitle = (id) => {
232
+ patchNote(id, { title: titleDraft }, true);
233
+ setEditingTitle(null);
234
+ };
235
+ const closeNote = (id) => { patchNote(id, { visible: false }); };
236
+ const removeNote = (id) => {
237
+ commitNotes(notes.filter(note => note.id !== id));
238
+ setNoteMenu(null);
239
+ };
240
+ /**
241
+ * Title-bar drag with a click/drag threshold: moving the pointer past a few
242
+ * pixels drags the window, while a plain click focuses the title input for
243
+ * editing. A title that is already being edited keeps native text behaviour
244
+ * (selection, caret placement) instead of turning into a window drag.
245
+ */
246
+ const beginNoteBarDrag = (event, id) => {
247
+ const win = event.currentTarget.parentElement;
248
+ if (win === null)
249
+ return;
250
+ if (event.target.closest('button') !== null)
251
+ return;
252
+ // While the title is being edited the input keeps native text behaviour.
253
+ if (event.target.closest('input') !== null)
254
+ return;
255
+ event.preventDefault();
256
+ const startX = event.clientX;
257
+ const startY = event.clientY;
258
+ const rect = win.getBoundingClientRect();
259
+ let dragging = false;
260
+ const onMove = (ev) => {
261
+ if (!dragging) {
262
+ if (Math.abs(ev.clientX - startX) < 3 && Math.abs(ev.clientY - startY) < 3)
263
+ return;
264
+ dragging = true;
265
+ const active = document.activeElement;
266
+ if (active instanceof HTMLElement)
267
+ active.blur();
268
+ }
269
+ patchNote(id, {
270
+ x: Math.round(rect.left + (ev.clientX - startX)),
271
+ y: Math.round(rect.top + (ev.clientY - startY)),
272
+ }, false);
273
+ };
274
+ const onUp = () => {
275
+ window.removeEventListener('pointermove', onMove);
276
+ window.removeEventListener('pointerup', onUp);
277
+ window.removeEventListener('pointercancel', onUp);
278
+ };
279
+ window.addEventListener('pointermove', onMove);
280
+ window.addEventListener('pointerup', onUp);
281
+ window.addEventListener('pointercancel', onUp);
282
+ };
104
283
  // Copy-button feedback: which command was just copied (label flips to
105
284
  // 「已复制」for a moment so the click is perceivable).
106
285
  const [copiedCmd, setCopiedCmd] = useState(null);
@@ -318,39 +497,45 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
318
497
  const panelDimClass = dimWhenIdle
319
498
  ? (hovered || upgradeFlash ? '' : inputActive ? css.panelDimStrong : css.panelDim)
320
499
  : '';
321
- 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
322
- ? (_jsxs("span", { className: `${css.version} ${css.versionNew}`, title: t('upgradeAvailableHint'), children: ["v", __TOKEN_QUOTA_VERSION__, " \u2192 v", upgrade.latestVersion] }))
323
- : (_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) => {
324
- const state = barStateOf(row);
325
- const pct = row.limit > 0 ? Math.min(100, Math.round((row.used / row.limit) * 100)) : 0;
326
- const barClass = state === 'over'
327
- ? css.fillOver
328
- : state === 'warn'
329
- ? css.fillWarn
330
- : css.fillIdle;
331
- const editing = editingKey === row.key;
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
350
- ? `${formatTokens(row.used)} / ${formatTokens(row.limit)}`
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')
352
- commitLimit(row); } }), _jsx("button", { type: "button", className: css.save, onClick: () => { commitLimit(row); }, children: t('save') })] }))] }, row.key));
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) => {
500
+ return (_jsxs(_Fragment, { children: [_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
501
+ ? (_jsxs("span", { className: `${css.version} ${css.versionNew}`, title: t('upgradeAvailableHint'), children: ["v", __TOKEN_QUOTA_VERSION__, " \u2192 v", upgrade.latestVersion] }))
502
+ : (_jsxs("span", { className: css.version, children: ["v", __TOKEN_QUOTA_VERSION__] })))] }), _jsxs("div", { className: css.notesBar, onPointerDown: (event) => { event.stopPropagation(); }, children: [notes.map((note) => {
503
+ const open = note.visible && !note.collapsed;
504
+ return (_jsx("button", { type: "button", className: `${css.noteChip}${open ? ` ${css.noteChipOpen}` : ''}`, title: note.title.trim() === '' ? t('noteUntitled') : note.title, onClick: () => { patchNote(note.id, { visible: true, collapsed: false }); }, onContextMenu: (event) => {
505
+ event.preventDefault();
506
+ setNoteMenu({ id: note.id, x: event.clientX, y: event.clientY });
507
+ }, children: noteInitial(note.title) }, note.id));
508
+ }), _jsxs("button", { type: "button", className: css.noteNew, disabled: notes.length >= TOKEN_QUOTA_MAX_NOTES, title: notes.length >= TOKEN_QUOTA_MAX_NOTES ? t('noteMaxHint') : t('noteNewHint'), onClick: createNote, children: [_jsxs("svg", { viewBox: "0 0 16 16", width: "13", height: "13", "aria-hidden": "true", children: [_jsx("path", { d: "M4 1.6h4.6L12.4 5.4v9H4z", fill: "none", stroke: "currentColor", strokeWidth: "1.2", strokeLinejoin: "round" }), _jsx("path", { d: "M8.4 1.6v3.9h4", fill: "none", stroke: "currentColor", strokeWidth: "1.2", strokeLinejoin: "round" })] }), _jsx("span", { className: css.noteNewPlus, children: "+" })] })] })] }), _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) => {
509
+ const state = barStateOf(row);
510
+ const pct = row.limit > 0 ? Math.min(100, Math.round((row.used / row.limit) * 100)) : 0;
511
+ const barClass = state === 'over'
512
+ ? css.fillOver
513
+ : state === 'warn'
514
+ ? css.fillWarn
515
+ : css.fillIdle;
516
+ const editing = editingKey === row.key;
517
+ return (_jsxs("div", { className: `${css.row}${dragKey === row.key ? ` ${css.rowDragging}` : ''}${dropKey === row.key && dragKey !== row.key ? ` ${css.rowDropTarget}` : ''}`, onDragOver: (event) => {
518
+ if (dragKey === null || dragKey === row.key)
519
+ return;
520
+ event.preventDefault();
521
+ setDropKey(row.key);
522
+ }, onDrop: (event) => {
523
+ event.preventDefault();
524
+ if (dragKey !== null && dragKey !== row.key) {
525
+ setModelOrder(reorderKeys(rows.map(r => r.key), dragKey, row.key));
526
+ }
527
+ setDragKey(null);
528
+ setDropKey(null);
529
+ }, onDragEnd: () => { setDragKey(null); setDropKey(null); }, children: [_jsxs("div", { className: css.rowHeader, children: [_jsx("span", { className: css.rowDrag, draggable: true, title: t('reorderHint'), onDragStart: (event) => {
530
+ setDragKey(row.key);
531
+ event.dataTransfer.effectAllowed = 'move';
532
+ // Firefox only starts a drag when some data is set.
533
+ event.dataTransfer.setData('text/plain', row.key);
534
+ }, 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
535
+ ? `${formatTokens(row.used)} / ${formatTokens(row.limit)}`
536
+ : `${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')
537
+ commitLimit(row); } }), _jsx("button", { type: "button", className: css.save, onClick: () => { commitLimit(row); }, children: t('save') })] }))] }, row.key));
538
+ })] })] }), 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) => {
354
539
  const base = monitored === null ? allModels.map(m => m.key) : monitored;
355
540
  const next = new Set(base);
356
541
  if (event.target.checked)
@@ -386,6 +571,21 @@ export function TokenQuotaPanel({ t, load, setLimit, selectModel, setMonitored,
386
571
  : 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')
387
572
  .replace('{from}', String(logCurrentPage * logPageSize + 1))
388
573
  .replace('{to}', String(Math.min((logCurrentPage + 1) * logPageSize, filteredLogEntries.length)))
389
- .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') })] })] }) }))] }));
574
+ .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') })] })] }) })), notes.filter(note => note.visible).map((note) => (_jsxs("div", { className: `${css.noteWin}${note.collapsed ? ` ${css.noteWinCollapsed}` : ''}`, style: {
575
+ left: `${note.x}px`,
576
+ top: `${note.y}px`,
577
+ width: `${note.width}px`,
578
+ zIndex: activeNote === note.id ? 62 : 60,
579
+ ...(note.collapsed ? {} : { height: `${note.height}px` }),
580
+ }, onPointerDown: () => { setActiveNote(note.id); }, children: [_jsxs("div", { className: css.noteWinBar, onPointerDown: (event) => { beginNoteBarDrag(event, note.id); }, onDoubleClick: () => { patchNote(note.id, { collapsed: false }); }, children: [_jsx("span", { className: css.noteWinGripBar, "aria-hidden": "true", children: "\u283F" }), editingTitle === note.id
581
+ ? (_jsxs(_Fragment, { children: [_jsx("input", { className: css.noteWinTitle, value: titleDraft, autoFocus: true, placeholder: t('noteUntitled'), onChange: (event) => { setTitleDraft(event.target.value); }, onKeyDown: (event) => {
582
+ if (event.key === 'Enter')
583
+ commitTitle(note.id);
584
+ if (event.key === 'Escape')
585
+ setEditingTitle(null);
586
+ } }), _jsx("button", { type: "button", className: css.noteWinBtn, title: t('noteConfirmTitle'), onClick: () => { commitTitle(note.id); }, children: "\u2713" })] }))
587
+ : (_jsxs(_Fragment, { children: [_jsx("span", { className: css.noteWinTitleText, title: note.title.trim() === '' ? t('noteUntitled') : note.title, children: note.title.trim() === '' ? t('noteUntitled') : note.title }), _jsx("button", { type: "button", className: css.noteWinBtn, title: t('noteEditTitle'), onClick: () => { setEditingTitle(note.id); setTitleDraft(note.title); }, children: "\u270E" })] })), _jsx("button", { type: "button", className: css.noteWinBtn, title: note.collapsed ? t('noteExpand') : t('noteCollapse'), onClick: () => { patchNote(note.id, { collapsed: !note.collapsed }); }, children: note.collapsed ? '▸' : '▾' }), _jsx("button", { type: "button", className: css.noteWinBtn, title: t('noteClose'), onClick: () => { closeNote(note.id); }, children: "\u00D7" })] }), !note.collapsed && (_jsxs(_Fragment, { children: [_jsx("textarea", { className: css.noteWinText, value: note.text, placeholder: t('notePlaceholder'), spellCheck: false, onChange: (event) => { patchNote(note.id, { text: event.target.value }, false); } }), _jsx("span", { className: css.noteWinResize, title: t('noteResizeHint'), onPointerDown: (event) => {
588
+ beginResize(event, event.currentTarget.parentElement, size => { patchNote(note.id, size, false); });
589
+ } })] }))] }, note.id))), noteMenu !== null && (_jsxs(_Fragment, { children: [_jsx("div", { className: css.noteMenuBackdrop, onPointerDown: () => { setNoteMenu(null); }, onContextMenu: (event) => { event.preventDefault(); setNoteMenu(null); } }), _jsxs("div", { className: css.noteMenu, style: { left: `${noteMenu.x}px`, top: `${noteMenu.y}px` }, children: [_jsx("button", { type: "button", className: css.noteMenuItem, onClick: () => { closeNote(noteMenu.id); setNoteMenu(null); }, children: t('noteClose') }), _jsx("button", { type: "button", className: css.noteMenuItem, onClick: () => { removeNote(noteMenu.id); }, children: t('noteDelete') })] })] }))] }));
390
590
  }
391
591
  //# sourceMappingURL=TokenQuotaPanel.js.map
@@ -1,3 +1,4 @@
1
+ import { TOKEN_QUOTA_MAX_NOTES } from '@jxgame2020/dsh-token-quota/types';
1
2
  import { createTokenQuotaPanelStore } from "./store.js";
2
3
  import { TokenQuotaPanel } from "./TokenQuotaPanel.js";
3
4
  import { TokenQuotaMusic } from "./music.js";
@@ -89,6 +90,30 @@ export function apply(ctx) {
89
90
  window.removeEventListener('pointerdown', autoStartMusic);
90
91
  window.removeEventListener('keydown', autoStartMusic);
91
92
  }, 'token-quota-ui: music autostart gestures');
93
+ /**
94
+ * Coerce the persisted note array into complete notes: older documents may
95
+ * lack fields added later, and a hand-edited file may hold junk. The cap is
96
+ * enforced here too so a tampered document cannot grow past it.
97
+ */
98
+ const normalizeNotes = (raw) => {
99
+ if (!Array.isArray(raw))
100
+ return [];
101
+ return raw.slice(0, TOKEN_QUOTA_MAX_NOTES).flatMap((note) => {
102
+ if (typeof note !== 'object' || note === null || typeof note.id !== 'string')
103
+ return [];
104
+ return [{
105
+ id: note.id,
106
+ title: typeof note.title === 'string' ? note.title : '',
107
+ text: typeof note.text === 'string' ? note.text : '',
108
+ x: typeof note.x === 'number' ? note.x : 80,
109
+ y: typeof note.y === 'number' ? note.y : 80,
110
+ width: typeof note.width === 'number' ? note.width : 220,
111
+ height: typeof note.height === 'number' ? note.height : 220,
112
+ collapsed: note.collapsed === true,
113
+ visible: note.visible !== false,
114
+ }];
115
+ });
116
+ };
92
117
  /** Build a writable music-settings clone with defaults applied. */
93
118
  const musicDefaults = (raw) => ({
94
119
  enabled: raw?.enabled ?? false,
@@ -251,6 +276,7 @@ export function apply(ctx) {
251
276
  bound?.setCheckUpdates(doc?.checkUpdates ?? true);
252
277
  bound?.setDimWhenIdle(doc?.dimWhenIdle ?? false);
253
278
  bound?.setOrder(Array.isArray(doc?.order) ? doc.order : []);
279
+ bound?.setNotes(normalizeNotes(doc?.notes));
254
280
  bound?.setReset(lastReset);
255
281
  const musicSettings = musicDefaults(doc?.music);
256
282
  lastOnlyCurrentSession = musicSettings.onlyCurrentSession;
@@ -338,6 +364,10 @@ export function apply(ctx) {
338
364
  bound?.setOrder(order);
339
365
  void scope.set('order', order);
340
366
  };
367
+ /** Persist the floating scratchpad notes (content, title, placement). */
368
+ const saveNotes = (notes) => {
369
+ void scope.set('notes', notes.slice(0, TOKEN_QUOTA_MAX_NOTES));
370
+ };
341
371
  const setMusicEnabled = (enabled) => {
342
372
  const doc = scope.getSnapshot().value;
343
373
  const current = musicDefaults(doc?.music);
@@ -455,7 +485,7 @@ export function apply(ctx) {
455
485
  load, setLimit, selectModel, setMonitored, setOnFull, setReset,
456
486
  setCheckUpdates, checkUpdatesNow, clearLogBefore, setDimWhenIdle,
457
487
  setMusicEnabled, setMusicVolume, setMusicStyle, setMusicOnlyCurrentSession,
458
- setModelOrder,
488
+ setModelOrder, saveNotes,
459
489
  };
460
490
  };
461
491
  ctx.slots.inject('shell.overlay', () => ctx.slots.register({
@@ -29,6 +29,17 @@ export declare const zh: {
29
29
  settingsTabMusic: string;
30
30
  settingsTabOther: string;
31
31
  reorderHint: string;
32
+ noteNewHint: string;
33
+ noteMaxHint: string;
34
+ noteUntitled: string;
35
+ noteEditTitle: string;
36
+ noteConfirmTitle: string;
37
+ noteCollapse: string;
38
+ noteExpand: string;
39
+ noteClose: string;
40
+ noteDelete: string;
41
+ noteResizeHint: string;
42
+ notePlaceholder: string;
32
43
  monitorLabel: string;
33
44
  monitorHint: string;
34
45
  monitorAll: string;
@@ -113,6 +124,17 @@ export declare const en: {
113
124
  settingsTabMusic: string;
114
125
  settingsTabOther: string;
115
126
  reorderHint: string;
127
+ noteNewHint: string;
128
+ noteMaxHint: string;
129
+ noteUntitled: string;
130
+ noteEditTitle: string;
131
+ noteConfirmTitle: string;
132
+ noteCollapse: string;
133
+ noteExpand: string;
134
+ noteClose: string;
135
+ noteDelete: string;
136
+ noteResizeHint: string;
137
+ notePlaceholder: string;
116
138
  monitorLabel: string;
117
139
  monitorHint: string;
118
140
  monitorAll: string;
@@ -29,6 +29,17 @@ export const zh = {
29
29
  settingsTabMusic: '音乐设置',
30
30
  settingsTabOther: '其它',
31
31
  reorderHint: '拖动排序',
32
+ noteNewHint: '新建笔记(最多 5 个)',
33
+ noteMaxHint: '最多 5 个笔记',
34
+ noteUntitled: '未命名',
35
+ noteEditTitle: '编辑标题',
36
+ noteConfirmTitle: '完成编辑',
37
+ noteCollapse: '收起',
38
+ noteExpand: '展开',
39
+ noteClose: '关闭',
40
+ noteDelete: '删除',
41
+ noteResizeHint: '拖动改变大小',
42
+ notePlaceholder: '在这里记录…',
32
43
  monitorLabel: '监控模型',
33
44
  monitorHint: '未勾选的模型不显示、不计入用量、不受限额限制',
34
45
  monitorAll: '全部',
@@ -111,6 +122,17 @@ export const en = {
111
122
  settingsTabMusic: 'Music',
112
123
  settingsTabOther: 'Other',
113
124
  reorderHint: 'Drag to reorder',
125
+ noteNewHint: 'New note (max 5)',
126
+ noteMaxHint: 'At most 5 notes',
127
+ noteUntitled: 'Untitled',
128
+ noteEditTitle: 'Edit title',
129
+ noteConfirmTitle: 'Done',
130
+ noteCollapse: 'Collapse',
131
+ noteExpand: 'Expand',
132
+ noteClose: 'Close',
133
+ noteDelete: 'Delete',
134
+ noteResizeHint: 'Drag to resize',
135
+ notePlaceholder: 'Write here…',
114
136
  monitorLabel: 'Monitored models',
115
137
  monitorHint: 'Unchecked models are hidden, not metered, and never capped',
116
138
  monitorAll: 'All',
@@ -9,7 +9,7 @@
9
9
  */
10
10
  import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
11
11
  import type { ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
12
- import type { TokenQuotaFullAction, TokenQuotaLog, TokenQuotaMusicStyle, TokenQuotaReset, TokenQuotaSnapshot, TokenQuotaUpgrade } from '@jxgame2020/dsh-token-quota/types';
12
+ import type { TokenQuotaFullAction, TokenQuotaLog, TokenQuotaMusicStyle, TokenQuotaNote, TokenQuotaReset, TokenQuotaSnapshot, TokenQuotaUpgrade } from '@jxgame2020/dsh-token-quota/types';
13
13
  /** One rendered model row: directory identity merged with the quota snapshot. */
14
14
  export interface ModelQuotaRow {
15
15
  /** Stable key `provider/model`. */
@@ -55,6 +55,14 @@ export interface TokenQuotaPanelState {
55
55
  * Keys not listed keep their catalog order after the listed ones.
56
56
  */
57
57
  order: string[];
58
+ /** Floating scratchpad notes as persisted by the Host. */
59
+ notes: TokenQuotaNote[];
60
+ /**
61
+ * Whether the notes above have been read from the Host at least once. The
62
+ * panel seeds its local editing copy exactly once, on this flip, so a
63
+ * background poll can never clobber what the user is typing.
64
+ */
65
+ notesLoaded: boolean;
58
66
  /** Live music settings (mirrored from the settings document). */
59
67
  musicEnabled: boolean;
60
68
  musicVolume: number;
@@ -104,6 +112,8 @@ export type TokenQuotaPanelActions = {
104
112
  setDimWhenIdle: (d: TokenQuotaPanelState, dimWhenIdle: boolean) => void;
105
113
  /** Replace the drag-to-reorder model display order. */
106
114
  setOrder: (d: TokenQuotaPanelState, order: string[]) => void;
115
+ /** Adopt the Host's persisted notes and mark them as loaded. */
116
+ setNotes: (d: TokenQuotaPanelState, notes: TokenQuotaNote[]) => void;
107
117
  setMusicEnabled: (d: TokenQuotaPanelState, enabled: boolean) => void;
108
118
  setMusicVolume: (d: TokenQuotaPanelState, volume: number) => void;
109
119
  setMusicStyle: (d: TokenQuotaPanelState, style: TokenQuotaMusicStyle) => void;
@@ -29,6 +29,8 @@ export function createTokenQuotaPanelStore() {
29
29
  checkUpdates: true,
30
30
  dimWhenIdle: false,
31
31
  order: [],
32
+ notes: [],
33
+ notesLoaded: false,
32
34
  musicEnabled: false,
33
35
  musicVolume: 0.5,
34
36
  musicStyle: 'pentatonic',
@@ -59,6 +61,7 @@ export function createTokenQuotaPanelStore() {
59
61
  setCheckUpdates: (d, checkUpdates) => { d.checkUpdates = checkUpdates; },
60
62
  setDimWhenIdle: (d, dimWhenIdle) => { d.dimWhenIdle = dimWhenIdle; },
61
63
  setOrder: (d, order) => { d.order = order; },
64
+ setNotes: (d, notes) => { d.notes = notes; d.notesLoaded = true; },
62
65
  setMusicEnabled: (d, enabled) => { d.musicEnabled = enabled; },
63
66
  setMusicVolume: (d, volume) => { d.musicVolume = volume; },
64
67
  setMusicStyle: (d, style) => { d.musicStyle = style; },