@jxgame2020/dsh-token-quota 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4,7 +4,7 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
7
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
9
  let react = require("react");
10
10
  //#region lib/types/client/store.js
@@ -26,7 +26,7 @@ window.__ModuleLoader__.load({
26
26
  * @returns the store handle.
27
27
  */
28
28
  function createTokenQuotaPanelStore() {
29
- return (0, _deepseek_ai_dsh_client_runtime_client.defineStore)({
29
+ return (0, _deepseek_ai_dsh_client_store.defineStore)({
30
30
  init: () => ({
31
31
  snapshot: null,
32
32
  groups: [],
@@ -79,7 +79,7 @@ window.__ModuleLoader__.load({
79
79
  d.dimWhenIdle = dimWhenIdle;
80
80
  },
81
81
  setUpgrade: (d, upgrade) => {
82
- const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.17" ? upgrade : null;
82
+ const normalized = upgrade !== null && upgrade.latestVersion !== "" && upgrade.latestVersion !== "0.1.19" ? upgrade : null;
83
83
  if (d.upgrade === null || d.upgrade.latestVersion !== normalized?.latestVersion) d.upgradeDismissed = false;
84
84
  d.upgrade = normalized;
85
85
  },
@@ -185,91 +185,91 @@ window.__ModuleLoader__.load({
185
185
  document.head.appendChild(tag);
186
186
  }
187
187
  var TokenQuotaPanel_module_css_default = {
188
- "logPager": "NPLLIq_logPager",
188
+ "panel": "NPLLIq_panel",
189
+ "rowHeader": "NPLLIq_rowHeader",
190
+ "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
189
191
  "logToolbar": "NPLLIq_logToolbar",
190
- "radioRow": "NPLLIq_radioRow",
191
- "dialogClose": "NPLLIq_dialogClose",
192
192
  "gearBtn": "NPLLIq_gearBtn",
193
- "panelDim": "NPLLIq_panelDim",
194
- "fillIdle": "NPLLIq_fillIdle",
193
+ "logPagerInfo": "NPLLIq_logPagerInfo",
194
+ "fillWarn": "NPLLIq_fillWarn",
195
+ "headerText": "NPLLIq_headerText",
196
+ "dialogLabel": "NPLLIq_dialogLabel",
197
+ "dialogBtn": "NPLLIq_dialogBtn",
195
198
  "monitorHint": "NPLLIq_monitorHint",
196
- "logPagerControls": "NPLLIq_logPagerControls",
199
+ "logPagerPages": "NPLLIq_logPagerPages",
200
+ "subtitle": "NPLLIq_subtitle",
201
+ "upgradeCommands": "NPLLIq_upgradeCommands",
202
+ "dialogActions": "NPLLIq_dialogActions",
203
+ "title": "NPLLIq_title",
204
+ "fillOver": "NPLLIq_fillOver",
205
+ "monitorRow": "NPLLIq_monitorRow",
206
+ "panelDimStrong": "NPLLIq_panelDimStrong",
197
207
  "currentBadge": "NPLLIq_currentBadge",
208
+ "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
209
+ "input": "NPLLIq_input",
210
+ "checkErrorHint": "NPLLIq_checkErrorHint",
211
+ "dialogOverlay": "NPLLIq_dialogOverlay",
198
212
  "tabLabel": "NPLLIq_tabLabel",
199
- "dialogTitle": "NPLLIq_dialogTitle",
200
- "logClearDialog": "NPLLIq_logClearDialog",
201
- "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
213
+ "upgradeClose": "NPLLIq_upgradeClose",
214
+ "dialogClose": "NPLLIq_dialogClose",
215
+ "logScroll": "NPLLIq_logScroll",
216
+ "fullNotice": "NPLLIq_fullNotice",
202
217
  "settingsBtn": "NPLLIq_settingsBtn",
203
- "dialogActions": "NPLLIq_dialogActions",
218
+ "upgradeBanner": "NPLLIq_upgradeBanner",
219
+ "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
220
+ "logPager": "NPLLIq_logPager",
204
221
  "logPageSizeSelect": "NPLLIq_logPageSizeSelect",
205
222
  "rowName": "NPLLIq_rowName",
206
- "logHeader": "NPLLIq_logHeader",
207
- "row": "NPLLIq_row",
208
- "checkUpdatesRow": "NPLLIq_checkUpdatesRow",
209
- "panel": "NPLLIq_panel",
210
- "copyBtn": "NPLLIq_copyBtn",
211
- "dialogLabel": "NPLLIq_dialogLabel",
212
- "upgradeCommand": "NPLLIq_upgradeCommand",
213
- "logSearch": "NPLLIq_logSearch",
214
- "version": "NPLLIq_version",
215
- "logDialog": "NPLLIq_logDialog",
216
- "rowMeta": "NPLLIq_rowMeta",
217
- "selectBtn": "NPLLIq_selectBtn",
218
- "dialogOverlay": "NPLLIq_dialogOverlay",
223
+ "logClearDate": "NPLLIq_logClearDate",
224
+ "collapse": "NPLLIq_collapse",
225
+ "bar": "NPLLIq_bar",
219
226
  "controls": "NPLLIq_controls",
220
- "fullNotice": "NPLLIq_fullNotice",
221
- "upgradeBannerFlash": "NPLLIq_upgradeBannerFlash",
222
- "dialog": "NPLLIq_dialog",
223
- "upgradeCommands": "NPLLIq_upgradeCommands",
224
227
  "copyBtnDone": "NPLLIq_copyBtnDone",
225
- "input": "NPLLIq_input",
226
- "header": "NPLLIq_header",
227
- "tabIcon": "NPLLIq_tabIcon",
228
+ "body": "NPLLIq_body",
229
+ "logPagerBtn": "NPLLIq_logPagerBtn",
230
+ "notice": "NPLLIq_notice",
228
231
  "monitorName": "NPLLIq_monitorName",
229
- "checkBtn": "NPLLIq_checkBtn",
230
- "rowHeader": "NPLLIq_rowHeader",
231
- "panelDimStrong": "NPLLIq_panelDimStrong",
232
- "bar": "NPLLIq_bar",
233
- "save": "NPLLIq_save",
234
- "upgradeClose": "NPLLIq_upgradeClose",
235
- "dialogHeader": "NPLLIq_dialogHeader",
236
- "upgradeBanner": "NPLLIq_upgradeBanner",
232
+ "logClearBtn": "NPLLIq_logClearBtn",
233
+ "dialogBody": "NPLLIq_dialogBody",
237
234
  "resetRow": "NPLLIq_resetRow",
238
- "notice": "NPLLIq_notice",
239
- "logClearDate": "NPLLIq_logClearDate",
235
+ "dialogHint": "NPLLIq_dialogHint",
236
+ "panelDim": "NPLLIq_panelDim",
237
+ "logDialog": "NPLLIq_logDialog",
238
+ "tab": "NPLLIq_tab",
239
+ "upgradeHint": "NPLLIq_upgradeHint",
240
+ "header": "NPLLIq_header",
240
241
  "switch": "NPLLIq_switch",
241
- "headerActions": "NPLLIq_headerActions",
242
+ "noticeError": "NPLLIq_noticeError",
243
+ "upgradeCommand": "NPLLIq_upgradeCommand",
244
+ "row": "NPLLIq_row",
245
+ "rowMeta": "NPLLIq_rowMeta",
246
+ "checkUpdatesLabel": "NPLLIq_checkUpdatesLabel",
247
+ "dialog": "NPLLIq_dialog",
248
+ "dialogHeader": "NPLLIq_dialogHeader",
242
249
  "logModelCol": "NPLLIq_logModelCol",
250
+ "logUsedCol": "NPLLIq_logUsedCol",
251
+ "logSearch": "NPLLIq_logSearch",
252
+ "logPagerControls": "NPLLIq_logPagerControls",
253
+ "fillIdle": "NPLLIq_fillIdle",
254
+ "monitorList": "NPLLIq_monitorList",
255
+ "upgradeBannerText": "NPLLIq_upgradeBannerText",
243
256
  "dialogBtnPrimary": "NPLLIq_dialogBtnPrimary",
244
- "checkErrorHint": "NPLLIq_checkErrorHint",
245
- "logPagerPages": "NPLLIq_logPagerPages",
257
+ "version": "NPLLIq_version",
258
+ "dialogTitle": "NPLLIq_dialogTitle",
246
259
  "dialogSection": "NPLLIq_dialogSection",
247
- "monitorRow": "NPLLIq_monitorRow",
248
- "logScroll": "NPLLIq_logScroll",
249
- "logTable": "NPLLIq_logTable",
250
- "logClearBtn": "NPLLIq_logClearBtn",
251
- "headerText": "NPLLIq_headerText",
252
- "fillWarn": "NPLLIq_fillWarn",
260
+ "checkBtn": "NPLLIq_checkBtn",
261
+ "save": "NPLLIq_save",
262
+ "selectBtn": "NPLLIq_selectBtn",
253
263
  "logDayCol": "NPLLIq_logDayCol",
254
- "logPagerBtn": "NPLLIq_logPagerBtn",
255
- "collapse": "NPLLIq_collapse",
256
- "dialogHint": "NPLLIq_dialogHint",
257
- "monitorList": "NPLLIq_monitorList",
258
- "logPagerInfo": "NPLLIq_logPagerInfo",
259
- "title": "NPLLIq_title",
260
- "subtitle": "NPLLIq_subtitle",
261
- "logUsedCol": "NPLLIq_logUsedCol",
262
- "noticeError": "NPLLIq_noticeError",
263
- "dialogBody": "NPLLIq_dialogBody",
264
- "body": "NPLLIq_body",
265
- "dialogBtn": "NPLLIq_dialogBtn",
266
- "fillOver": "NPLLIq_fillOver",
267
- "upgradeCommandRow": "NPLLIq_upgradeCommandRow",
268
- "upgradeHint": "NPLLIq_upgradeHint",
269
- "tab": "NPLLIq_tab",
270
- "upgradeBannerText": "NPLLIq_upgradeBannerText",
271
264
  "resetSelect": "NPLLIq_resetSelect",
272
- "versionNew": "NPLLIq_versionNew"
265
+ "copyBtn": "NPLLIq_copyBtn",
266
+ "logClearDialog": "NPLLIq_logClearDialog",
267
+ "radioRow": "NPLLIq_radioRow",
268
+ "versionNew": "NPLLIq_versionNew",
269
+ "tabIcon": "NPLLIq_tabIcon",
270
+ "logTable": "NPLLIq_logTable",
271
+ "logHeader": "NPLLIq_logHeader",
272
+ "headerActions": "NPLLIq_headerActions"
273
273
  };
274
274
  //#endregion
275
275
  //#region lib/types/client/TokenQuotaPanel.js
@@ -411,8 +411,7 @@ window.__ModuleLoader__.load({
411
411
  (0, react.useEffect)(() => {
412
412
  const onFocusIn = (event) => {
413
413
  const target = event.target;
414
- const isInput = target !== null && (target.matches("input, textarea, [contenteditable=\"true\"]") || target.closest("input, textarea, [contenteditable=\"true\"]") !== null);
415
- setInputActive(isInput);
414
+ setInputActive(target !== null && (target.matches("input, textarea, [contenteditable=\"true\"]") || target.closest("input, textarea, [contenteditable=\"true\"]") !== null));
416
415
  };
417
416
  window.addEventListener("focusin", onFocusIn);
418
417
  return () => {
@@ -572,13 +571,13 @@ window.__ModuleLoader__.load({
572
571
  title: t("upgradeAvailableHint"),
573
572
  children: [
574
573
  "v",
575
- "0.1.17",
574
+ "0.1.19",
576
575
  " → v",
577
576
  upgrade.latestVersion
578
577
  ]
579
578
  }) : (0, react_jsx_runtime.jsxs)("span", {
580
579
  className: TokenQuotaPanel_module_css_default.version,
581
- children: ["v", "0.1.17"]
580
+ children: ["v", "0.1.19"]
582
581
  })]
583
582
  }), (0, react_jsx_runtime.jsx)("div", {
584
583
  className: TokenQuotaPanel_module_css_default.subtitle,
@@ -1279,10 +1278,10 @@ window.__ModuleLoader__.load({
1279
1278
  const TOKEN_QUOTA_NAMESPACE = "token-quota";
1280
1279
  /** Snapshot pull cadence in milliseconds. */
1281
1280
  const POLL_INTERVAL_MS = 3e3;
1282
- /** Required services: slot registry, connection RPC, locale, settings scope. */
1281
+ /** Required services: slot registry, sessions, locale, settings scope. */
1283
1282
  const inject = [
1284
1283
  "slots",
1285
- "connection",
1284
+ "sessions",
1286
1285
  "locale",
1287
1286
  "settingsScope"
1288
1287
  ];
@@ -1299,7 +1298,7 @@ window.__ModuleLoader__.load({
1299
1298
  }), "token-quota-ui: panel dictionaries");
1300
1299
  const t = ctx.locale.bind(NS);
1301
1300
  const scope = ctx.settingsScope.bind({ namespace: TOKEN_QUOTA_NAMESPACE });
1302
- const connection = ctx.get("connection");
1301
+ const sessions = ctx.get("sessions");
1303
1302
  const store = createTokenQuotaPanelStore();
1304
1303
  let bound;
1305
1304
  let lastGroups = [];
@@ -1366,11 +1365,11 @@ window.__ModuleLoader__.load({
1366
1365
  bound?.setFullNotice(t("fullSwitchFailed"));
1367
1366
  return;
1368
1367
  }
1369
- connection.api.sessions.selectModel({
1368
+ ctx.remote.session.selectModel({
1370
1369
  sessionId: lastSessionId,
1371
1370
  provider: target.provider,
1372
1371
  model: target.model
1373
- }).then(({ result }) => {
1372
+ }).then((result) => {
1374
1373
  if (result.ok) {
1375
1374
  lastCurrent = result.value.selected;
1376
1375
  flashFullNotice(t("fullSwitchTo").replace("{model}", target.model));
@@ -1381,14 +1380,21 @@ window.__ModuleLoader__.load({
1381
1380
  });
1382
1381
  };
1383
1382
  let pullCount = 0;
1383
+ /** Resolve the session's current model from its durable modelSelection projection. */
1384
+ const currentOf = (sessionId) => {
1385
+ const binding = sessions.binding(sessionId);
1386
+ if (binding === void 0) return null;
1387
+ return binding.session.projections.faceOf("modelSelection").getSnapshot()?.lastUsed ?? null;
1388
+ };
1384
1389
  const refreshCurrent = () => {
1385
1390
  if (lastSessionId === void 0) return;
1386
- connection.api.sessions.models({ sessionId: lastSessionId }).then(({ result }) => {
1391
+ ctx.remote.session.modelCatalog().then((result) => {
1387
1392
  if (result.ok) {
1388
- const currentChanged = lastCurrent?.provider !== result.value.current?.provider || lastCurrent?.model !== result.value.current?.model;
1393
+ const current = currentOf(lastSessionId);
1394
+ const currentChanged = lastCurrent?.provider !== current?.provider || lastCurrent?.model !== current?.model;
1389
1395
  lastGroups = result.value.groups;
1390
- lastCurrent = result.value.current;
1391
- bound?.setDirectory(result.value.groups, result.value.current);
1396
+ lastCurrent = current;
1397
+ bound?.setDirectory(result.value.groups, current);
1392
1398
  if (currentChanged) fetch("/token-quota", { headers: { accept: "application/json" } }).then((response) => response.ok ? response.json() : void 0, () => void 0).then((snapshot) => {
1393
1399
  if (snapshot !== void 0) {
1394
1400
  bound?.setSnapshot(snapshot);
@@ -1437,11 +1443,12 @@ window.__ModuleLoader__.load({
1437
1443
  lastSessionId = sessionId;
1438
1444
  bound?.setLoading(true);
1439
1445
  bound?.setError(null);
1440
- connection.api.sessions.models({ sessionId }).then(({ result }) => {
1446
+ ctx.remote.session.modelCatalog().then((result) => {
1441
1447
  if (result.ok) {
1448
+ const current = currentOf(sessionId);
1442
1449
  lastGroups = result.value.groups;
1443
- lastCurrent = result.value.current;
1444
- bound?.setDirectory(result.value.groups, result.value.current);
1450
+ lastCurrent = current;
1451
+ bound?.setDirectory(result.value.groups, current);
1445
1452
  bound?.setLoading(false);
1446
1453
  } else {
1447
1454
  bound?.setError(`${result.error.code}: ${result.error.message}`);
@@ -1512,11 +1519,11 @@ window.__ModuleLoader__.load({
1512
1519
  scope.set("reset", reset);
1513
1520
  };
1514
1521
  const selectModel = (sessionId, provider, model) => {
1515
- connection.api.sessions.selectModel({
1522
+ ctx.remote.session.selectModel({
1516
1523
  sessionId,
1517
1524
  provider,
1518
1525
  model
1519
- }).then(({ result }) => {
1526
+ }).then((result) => {
1520
1527
  if (result.ok) {
1521
1528
  lastCurrent = result.value.selected;
1522
1529
  bound?.setFullNotice(null);
package/lib/index.js CHANGED
@@ -1,12 +1,11 @@
1
1
  import { assertTokenQuotaLimit, splitTokenQuotaKey } from "./invariant.js";
2
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
  import { Service } from "@deepseek-ai/cordis";
6
6
  import z from "@deepseek-ai/schemastery";
7
7
  import "@deepseek-ai/dsh-agent";
8
8
  import { LlmError } from "@deepseek-ai/dsh-llm";
9
- import { installSettingsSection, settingsNamespace } from "@deepseek-ai/dsh-settings";
10
9
  //#region lib/types/types.js
11
10
  /**
12
11
  * Shared wire/type vocabulary for the daily token-quota plugin.
@@ -66,7 +65,7 @@ const PACKAGE_NAME = "@jxgame2020/dsh-token-quota";
66
65
  /** npm registry endpoint for the plugin's latest version. */
67
66
  const REGISTRY_LATEST_URL = "https://registry.npmjs.org/@jxgame2020%2Fdsh-token-quota/latest";
68
67
  /** How often the Host re-checks for a newer version when update checks are enabled. */
69
- const UPDATE_CHECK_INTERVAL_MS = 864e5;
68
+ const UPDATE_CHECK_INTERVAL_MS = 1440 * 60 * 1e3;
70
69
  /** Map a detected package manager to its install verb. */
71
70
  function installVerb(manager) {
72
71
  switch (manager) {
@@ -91,13 +90,54 @@ function registryUpgradeCommands(profileDir, manager) {
91
90
  function linkUpgradeCommands(linkPath) {
92
91
  return [`git -C "${linkPath}" pull`];
93
92
  }
94
- /** Detect the profile's package manager from its lockfiles; defaults to npm. */
93
+ /**
94
+ * Detect the profile's package manager; defaults to npm.
95
+ *
96
+ * Lockfiles alone are unreliable: a profile can accumulate several of them over
97
+ * time (e.g. a leftover `pnpm-lock.yaml` next to a fresh `package-lock.json`
98
+ * after switching from pnpm to npm), and a bare existence check then reports
99
+ * the WRONG manager. Instead, look at the installer fingerprints inside
100
+ * `node_modules` — each manager leaves a marker when it actually installs —
101
+ * and pick the most recently touched one. Only if no marker exists fall back
102
+ * to comparing lockfile mtimes.
103
+ */
95
104
  function detectManager(dir) {
96
- if (existsSync(join(dir, "pnpm-lock.yaml"))) return "pnpm";
97
- if (existsSync(join(dir, "yarn.lock"))) return "yarn";
98
- if (existsSync(join(dir, "package-lock.json"))) return "npm";
99
- if (existsSync(join(dir, "bun.lockb")) || existsSync(join(dir, "bun.lock"))) return "bun";
100
- return "npm";
105
+ const stat = (path) => {
106
+ try {
107
+ return statSync(path).mtimeMs;
108
+ } catch {
109
+ return;
110
+ }
111
+ };
112
+ const fingerprints = [
113
+ ["node_modules/.pnpm", "pnpm"],
114
+ ["node_modules/.bun", "bun"],
115
+ ["node_modules/.package-lock.json", "npm"],
116
+ ["node_modules/.yarn-integrity", "yarn"]
117
+ ];
118
+ let best;
119
+ for (const [rel, manager] of fingerprints) {
120
+ const mtime = stat(join(dir, rel));
121
+ if (mtime !== void 0 && (best === void 0 || mtime > best.mtime)) best = {
122
+ manager,
123
+ mtime
124
+ };
125
+ }
126
+ if (best !== void 0) return best.manager;
127
+ for (const [rel, manager] of [
128
+ ["pnpm-lock.yaml", "pnpm"],
129
+ ["yarn.lock", "yarn"],
130
+ ["package-lock.json", "npm"],
131
+ ["bun.lockb", "bun"],
132
+ ["bun.lock", "bun"]
133
+ ]) {
134
+ const mtime = stat(join(dir, rel));
135
+ if (mtime !== void 0 && (best === void 0 || mtime > best.mtime)) best = {
136
+ manager,
137
+ mtime
138
+ };
139
+ }
140
+ return best?.manager ?? "npm";
101
141
  }
102
142
  /**
103
143
  * Locate the profile directory that declares this plugin as a dependency, along
@@ -105,8 +145,7 @@ function detectManager(dir) {
105
145
  * `undefined` when no profile declares the plugin (e.g. a bundle-managed mount).
106
146
  */
107
147
  function findProfile() {
108
- const home = process.env.DSH_HOME !== void 0 && process.env.DSH_HOME.trim().length > 0 ? process.env.DSH_HOME : join(homedir(), ".dsh");
109
- const profilesDir = join(home, "profiles");
148
+ const profilesDir = join(process.env.DSH_HOME !== void 0 && process.env.DSH_HOME.trim().length > 0 ? process.env.DSH_HOME : join(homedir(), ".dsh"), "profiles");
110
149
  let names;
111
150
  try {
112
151
  names = readdirSync(profilesDir);
@@ -116,7 +155,11 @@ function findProfile() {
116
155
  for (const name of names) {
117
156
  const dir = join(profilesDir, name);
118
157
  const manifestPath = join(dir, "package.json");
119
- if (!existsSync(manifestPath)) continue;
158
+ try {
159
+ if (!statSync(manifestPath).isFile()) continue;
160
+ } catch {
161
+ continue;
162
+ }
120
163
  let manifest;
121
164
  try {
122
165
  manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
@@ -190,8 +233,7 @@ function cycleKey(now, reset) {
190
233
  }
191
234
  /** Default counter file under the Harness home (overridable through config). */
192
235
  function defaultStoragePath() {
193
- const home = process.env.DSH_HOME ?? join(homedir(), ".dsh");
194
- return join(home, "token-quota.json");
236
+ return join(process.env.DSH_HOME ?? join(homedir(), ".dsh"), "token-quota.json");
195
237
  }
196
238
  /** Combined input + output + cache token count of one provider usage report. */
197
239
  function usageTokens(usage) {
@@ -259,49 +301,51 @@ var TokenQuotaService = class TokenQuotaService extends Service {
259
301
  super(ctx, "tokenQuota");
260
302
  this.storagePath = config.storagePath !== void 0 && config.storagePath.length > 0 ? config.storagePath : defaultStoragePath();
261
303
  this.load();
262
- installSettingsSection(ctx, settingsNamespace(TOKEN_QUOTA_NAMESPACE), TOKEN_QUOTA_SETTINGS_SCHEMA, { limits: {} }, {
263
- setSource: (current) => {
264
- this.settingsSource = () => {
265
- const doc = current();
266
- return {
267
- limits: doc.limits ?? {},
268
- monitored: doc.monitored ?? [],
269
- onFull: doc.onFull === "switchPriority" ? "switchAll" : doc.onFull ?? "stop",
270
- checkUpdates: doc.checkUpdates ?? true,
271
- dimWhenIdle: doc.dimWhenIdle ?? false,
272
- reset: doc.reset ?? void 0
273
- };
274
- };
275
- },
276
- validate: (value) => {
277
- for (const [key, limit] of Object.entries(value.limits ?? {})) {
278
- assertTokenQuotaLimit(limit, key);
279
- splitTokenQuotaKey(key);
280
- }
281
- },
282
- onChange: () => {
283
- const doc = this.settingsSource();
284
- this.limits = { ...doc.limits };
285
- this.monitored = doc.monitored !== void 0 && doc.monitored.length > 0 ? new Set(doc.monitored) : void 0;
286
- this.checkUpdates = doc.checkUpdates;
287
- this.syncUpdateChecking();
288
- if (doc.reset !== void 0 && typeof doc.reset === "object" && doc.reset !== null && typeof doc.reset.offsetHours === "number" && typeof doc.reset.hour === "number" && typeof doc.reset.minute === "number") {
289
- const configured = doc.reset;
290
- this.reset = {
291
- offsetHours: Math.max(-12, Math.min(14, Math.round(configured.offsetHours))),
292
- hour: Math.max(0, Math.min(23, Math.round(configured.hour))),
293
- minute: Math.max(0, Math.min(59, Math.round(configured.minute)))
304
+ ctx.inject(["settings"], (settingsCtx) => {
305
+ settingsCtx.settings.installSection(ctx, TOKEN_QUOTA_NAMESPACE, TOKEN_QUOTA_SETTINGS_SCHEMA, { limits: {} }, {
306
+ setSource: (current) => {
307
+ this.settingsSource = () => {
308
+ const doc = current();
309
+ return {
310
+ limits: doc.limits ?? {},
311
+ monitored: doc.monitored ?? [],
312
+ onFull: doc.onFull === "switchPriority" ? "switchAll" : doc.onFull ?? "stop",
313
+ checkUpdates: doc.checkUpdates ?? true,
314
+ dimWhenIdle: doc.dimWhenIdle ?? false,
315
+ reset: doc.reset ?? void 0
316
+ };
294
317
  };
295
- this.rollCycleIfNeeded();
296
- } else {
297
- this.reset = {
298
- offsetHours: localOffsetHours(),
299
- hour: 0,
300
- minute: 0
301
- };
302
- this.rollCycleIfNeeded();
318
+ },
319
+ validate: (value) => {
320
+ for (const [key, limit] of Object.entries(value.limits ?? {})) {
321
+ assertTokenQuotaLimit(limit, key);
322
+ splitTokenQuotaKey(key);
323
+ }
324
+ },
325
+ onChange: () => {
326
+ const doc = this.settingsSource();
327
+ this.limits = { ...doc.limits };
328
+ this.monitored = doc.monitored !== void 0 && doc.monitored.length > 0 ? new Set(doc.monitored) : void 0;
329
+ this.checkUpdates = doc.checkUpdates;
330
+ this.syncUpdateChecking();
331
+ if (doc.reset !== void 0 && typeof doc.reset === "object" && doc.reset !== null && typeof doc.reset.offsetHours === "number" && typeof doc.reset.hour === "number" && typeof doc.reset.minute === "number") {
332
+ const configured = doc.reset;
333
+ this.reset = {
334
+ offsetHours: Math.max(-12, Math.min(14, Math.round(configured.offsetHours))),
335
+ hour: Math.max(0, Math.min(23, Math.round(configured.hour))),
336
+ minute: Math.max(0, Math.min(59, Math.round(configured.minute)))
337
+ };
338
+ this.rollCycleIfNeeded();
339
+ } else {
340
+ this.reset = {
341
+ offsetHours: localOffsetHours(),
342
+ hour: 0,
343
+ minute: 0
344
+ };
345
+ this.rollCycleIfNeeded();
346
+ }
303
347
  }
304
- }
348
+ });
305
349
  });
306
350
  this.tryMountRoute();
307
351
  if (this.disposeRoute === void 0) this.disposeRouteWatcher = ctx.on("internal/service", (name) => {
@@ -409,7 +453,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
409
453
  return this.upgradeCheck;
410
454
  }
411
455
  async doRefreshUpgrade() {
412
- const current = "0.1.17";
456
+ const current = "0.1.19";
413
457
  let latest;
414
458
  try {
415
459
  const response = await fetch(REGISTRY_LATEST_URL, {
@@ -505,7 +549,7 @@ var TokenQuotaService = class TokenQuotaService extends Service {
505
549
  /** Read the current snapshot (also used by tests and inspection). */
506
550
  readSnapshot() {
507
551
  this.rollCycleIfNeeded();
508
- const keys = /* @__PURE__ */ new Set([...Object.keys(this.usage), ...Object.keys(this.limits)]);
552
+ const keys = new Set([...Object.keys(this.usage), ...Object.keys(this.limits)]);
509
553
  const entries = [];
510
554
  for (const key of keys) {
511
555
  const parts = this.safeSplit(key);
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=bound-test.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { createTokenQuotaPanelStore } from "./client/store.js";
2
+ const store = createTokenQuotaPanelStore();
3
+ let bound;
4
+ bound?.setSnapshot(null);
5
+ //# sourceMappingURL=bound-test.js.map
@@ -1,5 +1,5 @@
1
1
  import type { PropsLocale, PropsRuntime, PropsStore } from '@deepseek-ai/dsh-client-ui-slots';
2
- import type { SessionId } from '@deepseek-ai/dsh-api-remotes/client';
2
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
3
3
  import type { TokenQuotaFullAction, 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`. */
@@ -0,0 +1,2 @@
1
+ export declare function test(): void;
2
+ //# sourceMappingURL=bound-test.d.ts.map
@@ -0,0 +1,7 @@
1
+ import { createTokenQuotaPanelStore } from "./store.js";
2
+ export function test() {
3
+ const store = createTokenQuotaPanelStore();
4
+ let bound;
5
+ bound?.setFullNotice(null);
6
+ }
7
+ //# sourceMappingURL=bound-test.js.map
@@ -0,0 +1,2 @@
1
+ export declare function test(): void;
2
+ //# sourceMappingURL=import-test.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { createTokenQuotaPanelStore } from "./store.js";
2
+ export function test() { const store = createTokenQuotaPanelStore(); let bound; bound?.setFullNotice(null); }
3
+ //# sourceMappingURL=import-test.js.map
@@ -12,7 +12,7 @@
12
12
  * full-quota strategy runs: `stop` shows a notice, the other three
13
13
  * auto-switch to a suitable model.
14
14
  */
15
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
15
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
16
16
  import { type TokenQuotaKey } from './locales.ts';
17
17
  export type { TokenQuotaPanelInjected } from './TokenQuotaPanel.tsx';
18
18
  export type { ModelQuotaRow, TokenQuotaPanelState } from './store.ts';
@@ -24,7 +24,7 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
24
24
  'token-quota': TokenQuotaKey;
25
25
  }
26
26
  }
27
- /** Required services: slot registry, connection RPC, locale, settings scope. */
27
+ /** Required services: slot registry, sessions, locale, settings scope. */
28
28
  export declare const inject: string[];
29
29
  /**
30
30
  * Client plugin body: poll the Host snapshot route, run the full-quota
@@ -8,8 +8,8 @@ const NS = 'token-quota';
8
8
  const TOKEN_QUOTA_NAMESPACE = 'token-quota';
9
9
  /** Snapshot pull cadence in milliseconds. */
10
10
  const POLL_INTERVAL_MS = 3000;
11
- /** Required services: slot registry, connection RPC, locale, settings scope. */
12
- export const inject = ['slots', 'connection', 'locale', 'settingsScope'];
11
+ /** Required services: slot registry, sessions, locale, settings scope. */
12
+ export const inject = ['slots', 'sessions', 'locale', 'settingsScope'];
13
13
  /**
14
14
  * Client plugin body: poll the Host snapshot route, run the full-quota
15
15
  * strategy, wire the injected face (directory load, limit write, model
@@ -20,7 +20,10 @@ export function apply(ctx) {
20
20
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'token-quota-ui: panel dictionaries');
21
21
  const t = ctx.locale.bind(NS);
22
22
  const scope = ctx.settingsScope.bind({ namespace: TOKEN_QUOTA_NAMESPACE });
23
- const connection = ctx.get('connection');
23
+ // The Host `dsh-session` package's `ctx.sessions: SessionStore` merge can
24
+ // shadow the client ISessions face in a combined type-check; the runtime
25
+ // 'sessions' service is the ISessions implementation regardless.
26
+ const sessions = ctx.get('sessions');
24
27
  // Store handle shared with the registration; the framework instantiates the
25
28
  // live store per entry and hands its bound actions to the inject factory,
26
29
  // so the apply-world only ever writes through `bound`.
@@ -107,11 +110,11 @@ export function apply(ctx) {
107
110
  bound?.setFullNotice(t('fullSwitchFailed'));
108
111
  return;
109
112
  }
110
- void connection.api.sessions.selectModel({
113
+ void ctx.remote.session.selectModel({
111
114
  sessionId: lastSessionId,
112
115
  provider: target.provider,
113
116
  model: target.model,
114
- }).then(({ result }) => {
117
+ }).then((result) => {
115
118
  if (result.ok) {
116
119
  lastCurrent = result.value.selected;
117
120
  flashFullNotice(t('fullSwitchTo').replace('{model}', target.model));
@@ -130,16 +133,26 @@ export function apply(ctx) {
130
133
  // switch on full quota) moves the 「当前」 badge without waiting the full
131
134
  // directory-refresh cadence.
132
135
  let pullCount = 0;
136
+ /** Resolve the session's current model from its durable modelSelection projection. */
137
+ const currentOf = (sessionId) => {
138
+ const binding = sessions.binding(sessionId);
139
+ if (binding === undefined)
140
+ return null;
141
+ const face = binding.session.projections.faceOf('modelSelection');
142
+ const state = face.getSnapshot();
143
+ return state?.lastUsed ?? null;
144
+ };
133
145
  const refreshCurrent = () => {
134
146
  if (lastSessionId === undefined)
135
147
  return;
136
- void connection.api.sessions.models({ sessionId: lastSessionId }).then(({ result }) => {
148
+ void ctx.remote.session.modelCatalog().then((result) => {
137
149
  if (result.ok) {
138
- const currentChanged = lastCurrent?.provider !== result.value.current?.provider
139
- || lastCurrent?.model !== result.value.current?.model;
150
+ const current = currentOf(lastSessionId);
151
+ const currentChanged = lastCurrent?.provider !== current?.provider
152
+ || lastCurrent?.model !== current?.model;
140
153
  lastGroups = result.value.groups;
141
- lastCurrent = result.value.current;
142
- bound?.setDirectory(result.value.groups, result.value.current);
154
+ lastCurrent = current;
155
+ bound?.setDirectory(result.value.groups, current);
143
156
  // If the server silently switched to a different model (e.g. the
144
157
  // auto-switch on quota exhaustion), re-run the full-quota strategy
145
158
  // immediately against the new current model so any further fallback
@@ -201,11 +214,12 @@ export function apply(ctx) {
201
214
  lastSessionId = sessionId;
202
215
  bound?.setLoading(true);
203
216
  bound?.setError(null);
204
- void connection.api.sessions.models({ sessionId }).then(({ result }) => {
217
+ void ctx.remote.session.modelCatalog().then((result) => {
205
218
  if (result.ok) {
219
+ const current = currentOf(sessionId);
206
220
  lastGroups = result.value.groups;
207
- lastCurrent = result.value.current;
208
- bound?.setDirectory(result.value.groups, result.value.current);
221
+ lastCurrent = current;
222
+ bound?.setDirectory(result.value.groups, current);
209
223
  bound?.setLoading(false);
210
224
  }
211
225
  else {
@@ -282,7 +296,7 @@ export function apply(ctx) {
282
296
  void scope.set('reset', reset);
283
297
  };
284
298
  const selectModel = (sessionId, provider, model) => {
285
- void connection.api.sessions.selectModel({ sessionId, provider, model }).then(({ result }) => {
299
+ void ctx.remote.session.selectModel({ sessionId, provider, model }).then((result) => {
286
300
  if (result.ok) {
287
301
  lastCurrent = result.value.selected;
288
302
  // A manual pick clears any leftover auto-switch / exhausted notice.
@@ -7,8 +7,8 @@
7
7
  *
8
8
  * @module @deepseek-ai/dsh-client-ui-token-quota/client/store
9
9
  */
10
- import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-runtime/client';
11
- import type { ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-remotes/client';
10
+ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
11
+ import type { ModelProviderGroup, ModelSelection } from '@deepseek-ai/dsh-api-session-controller/types';
12
12
  import type { TokenQuotaFullAction, TokenQuotaLog, 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 {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module @deepseek-ai/dsh-client-ui-token-quota/client/store
9
9
  */
10
- import { defineStore } from '@deepseek-ai/dsh-client-runtime/client';
10
+ import { defineStore } from '@deepseek-ai/dsh-client-store';
11
11
  /** Build the stable per-model key shared by the counter, settings, and snapshot. */
12
12
  function tokenQuotaKey(provider, model) {
13
13
  return `${provider}/${model}`;
@@ -0,0 +1,2 @@
1
+ export declare function test(): void;
2
+ //# sourceMappingURL=import-test.d.ts.map
@@ -0,0 +1,3 @@
1
+ import { createTokenQuotaPanelStore } from "./store.js";
2
+ export function test() { const store = createTokenQuotaPanelStore(); let bound; bound?.setFullNotice(null); }
3
+ //# sourceMappingURL=import-test.js.map
@@ -29,7 +29,7 @@
29
29
  *
30
30
  * @module @jxgame2020/dsh-token-quota
31
31
  */
32
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
32
+ import { mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
33
33
  import { homedir } from 'node:os';
34
34
  import { dirname, join } from 'node:path';
35
35
  import { Service } from '@deepseek-ai/cordis';
@@ -38,7 +38,6 @@ import z from '@deepseek-ai/schemastery';
38
38
  // the service listens to without importing any runtime value.
39
39
  import '@deepseek-ai/dsh-agent';
40
40
  import { LlmError } from '@deepseek-ai/dsh-llm';
41
- import { installSettingsSection, settingsNamespace } from '@deepseek-ai/dsh-settings';
42
41
  import { TOKEN_QUOTA_EXCEEDED_CODE, TOKEN_QUOTA_NAMESPACE, tokenQuotaKey, } from "./types.js";
43
42
  import { assertTokenQuotaLimit, splitTokenQuotaKey } from "./invariant.js";
44
43
  /** The plugin's package name, used to locate the profile dependency and the registry endpoint. */
@@ -76,17 +75,57 @@ function registryUpgradeCommands(profileDir, manager) {
76
75
  function linkUpgradeCommands(linkPath) {
77
76
  return [`git -C "${linkPath}" pull`];
78
77
  }
79
- /** Detect the profile's package manager from its lockfiles; defaults to npm. */
78
+ /**
79
+ * Detect the profile's package manager; defaults to npm.
80
+ *
81
+ * Lockfiles alone are unreliable: a profile can accumulate several of them over
82
+ * time (e.g. a leftover `pnpm-lock.yaml` next to a fresh `package-lock.json`
83
+ * after switching from pnpm to npm), and a bare existence check then reports
84
+ * the WRONG manager. Instead, look at the installer fingerprints inside
85
+ * `node_modules` — each manager leaves a marker when it actually installs —
86
+ * and pick the most recently touched one. Only if no marker exists fall back
87
+ * to comparing lockfile mtimes.
88
+ */
80
89
  function detectManager(dir) {
81
- if (existsSync(join(dir, 'pnpm-lock.yaml')))
82
- return 'pnpm';
83
- if (existsSync(join(dir, 'yarn.lock')))
84
- return 'yarn';
85
- if (existsSync(join(dir, 'package-lock.json')))
86
- return 'npm';
87
- if (existsSync(join(dir, 'bun.lockb')) || existsSync(join(dir, 'bun.lock')))
88
- return 'bun';
89
- return 'npm';
90
+ const stat = (path) => {
91
+ try {
92
+ return statSync(path).mtimeMs;
93
+ }
94
+ catch {
95
+ return undefined;
96
+ }
97
+ };
98
+ // Installer fingerprints inside node_modules, by manager.
99
+ const fingerprints = [
100
+ ['node_modules/.pnpm', 'pnpm'],
101
+ ['node_modules/.bun', 'bun'],
102
+ ['node_modules/.package-lock.json', 'npm'],
103
+ ['node_modules/.yarn-integrity', 'yarn'],
104
+ ];
105
+ let best;
106
+ for (const [rel, manager] of fingerprints) {
107
+ const mtime = stat(join(dir, rel));
108
+ if (mtime !== undefined && (best === undefined || mtime > best.mtime)) {
109
+ best = { manager, mtime };
110
+ }
111
+ }
112
+ if (best !== undefined)
113
+ return best.manager;
114
+ // Fallback: the most recently modified lockfile.
115
+ const lockfiles = [
116
+ ['pnpm-lock.yaml', 'pnpm'],
117
+ ['yarn.lock', 'yarn'],
118
+ ['package-lock.json', 'npm'],
119
+ ['bun.lockb', 'bun'],
120
+ ['bun.lock', 'bun'],
121
+ ];
122
+ for (const [rel, manager] of lockfiles) {
123
+ const mtime = stat(join(dir, rel));
124
+ if (mtime !== undefined && (best === undefined || mtime > best.mtime)) {
125
+ best = { manager, mtime };
126
+ }
127
+ }
128
+ return best?.manager ?? 'npm';
90
129
  }
91
130
  /**
92
131
  * Locate the profile directory that declares this plugin as a dependency, along
@@ -108,8 +147,13 @@ function findProfile() {
108
147
  for (const name of names) {
109
148
  const dir = join(profilesDir, name);
110
149
  const manifestPath = join(dir, 'package.json');
111
- if (!existsSync(manifestPath))
150
+ try {
151
+ if (!statSync(manifestPath).isFile())
152
+ continue;
153
+ }
154
+ catch {
112
155
  continue;
156
+ }
113
157
  let manifest;
114
158
  try {
115
159
  manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
@@ -262,60 +306,65 @@ export class TokenQuotaService extends Service {
262
306
  : defaultStoragePath();
263
307
  this.load();
264
308
  // Limits live in the user settings document; the Web panel writes them and
265
- // the section watcher pushes every change back here.
266
- installSettingsSection(ctx, settingsNamespace(TOKEN_QUOTA_NAMESPACE), TOKEN_QUOTA_SETTINGS_SCHEMA, { limits: {} }, {
267
- setSource: (current) => {
268
- // Normalize the resolved (schema-shaped) document into the plugin's
269
- // required shape: empty monitored = monitor everything, absent onFull
270
- // falls back to 'stop'.
271
- this.settingsSource = () => {
272
- const doc = current();
273
- return {
274
- limits: doc.limits ?? {},
275
- monitored: doc.monitored ?? [],
276
- // Legacy `switchPriority` (removed in 0.1.7) maps to `switchAll`:
277
- // capped models first, then uncapped — both monitored-only.
278
- onFull: doc.onFull === 'switchPriority' ? 'switchAll' : (doc.onFull ?? 'stop'),
279
- checkUpdates: doc.checkUpdates ?? true,
280
- dimWhenIdle: doc.dimWhenIdle ?? false,
281
- reset: doc.reset ?? undefined,
282
- };
283
- };
284
- },
285
- validate: (value) => {
286
- for (const [key, limit] of Object.entries(value.limits ?? {})) {
287
- assertTokenQuotaLimit(limit, key);
288
- splitTokenQuotaKey(key);
289
- }
290
- },
291
- onChange: () => {
292
- const doc = this.settingsSource();
293
- this.limits = { ...doc.limits };
294
- this.monitored = doc.monitored !== undefined && doc.monitored.length > 0
295
- ? new Set(doc.monitored)
296
- : undefined;
297
- this.checkUpdates = doc.checkUpdates;
298
- this.syncUpdateChecking();
299
- if (doc.reset !== undefined
300
- && typeof doc.reset === 'object'
301
- && doc.reset !== null
302
- && typeof doc.reset.offsetHours === 'number'
303
- && typeof doc.reset.hour === 'number'
304
- && typeof doc.reset.minute === 'number') {
305
- const configured = doc.reset;
306
- this.reset = {
307
- offsetHours: Math.max(-12, Math.min(14, Math.round(configured.offsetHours))),
308
- hour: Math.max(0, Math.min(23, Math.round(configured.hour))),
309
- minute: Math.max(0, Math.min(59, Math.round(configured.minute))),
309
+ // the section watcher pushes every change back here. DSH 0.1.5 replaced
310
+ // the standalone `installSettingsSection` helper with
311
+ // `SettingsProvider.installSection`, reached through `ctx.inject` so the
312
+ // settings service is guaranteed to be up before registration.
313
+ ctx.inject(['settings'], (settingsCtx) => {
314
+ settingsCtx.settings.installSection(ctx, TOKEN_QUOTA_NAMESPACE, TOKEN_QUOTA_SETTINGS_SCHEMA, { limits: {} }, {
315
+ setSource: (current) => {
316
+ // Normalize the resolved (schema-shaped) document into the plugin's
317
+ // required shape: empty monitored = monitor everything, absent onFull
318
+ // falls back to 'stop'.
319
+ this.settingsSource = () => {
320
+ const doc = current();
321
+ return {
322
+ limits: doc.limits ?? {},
323
+ monitored: doc.monitored ?? [],
324
+ // Legacy `switchPriority` (removed in 0.1.7) maps to `switchAll`:
325
+ // capped models first, then uncapped — both monitored-only.
326
+ onFull: doc.onFull === 'switchPriority' ? 'switchAll' : (doc.onFull ?? 'stop'),
327
+ checkUpdates: doc.checkUpdates ?? true,
328
+ dimWhenIdle: doc.dimWhenIdle ?? false,
329
+ reset: doc.reset ?? undefined,
330
+ };
310
331
  };
311
- this.rollCycleIfNeeded();
312
- }
313
- else {
314
- // null / malformed (unset) = machine-local midnight — the default.
315
- this.reset = { offsetHours: localOffsetHours(), hour: 0, minute: 0 };
316
- this.rollCycleIfNeeded();
317
- }
318
- },
332
+ },
333
+ validate: (value) => {
334
+ for (const [key, limit] of Object.entries(value.limits ?? {})) {
335
+ assertTokenQuotaLimit(limit, key);
336
+ splitTokenQuotaKey(key);
337
+ }
338
+ },
339
+ onChange: () => {
340
+ const doc = this.settingsSource();
341
+ this.limits = { ...doc.limits };
342
+ this.monitored = doc.monitored !== undefined && doc.monitored.length > 0
343
+ ? new Set(doc.monitored)
344
+ : undefined;
345
+ this.checkUpdates = doc.checkUpdates;
346
+ this.syncUpdateChecking();
347
+ if (doc.reset !== undefined
348
+ && typeof doc.reset === 'object'
349
+ && doc.reset !== null
350
+ && typeof doc.reset.offsetHours === 'number'
351
+ && typeof doc.reset.hour === 'number'
352
+ && typeof doc.reset.minute === 'number') {
353
+ const configured = doc.reset;
354
+ this.reset = {
355
+ offsetHours: Math.max(-12, Math.min(14, Math.round(configured.offsetHours))),
356
+ hour: Math.max(0, Math.min(23, Math.round(configured.hour))),
357
+ minute: Math.max(0, Math.min(59, Math.round(configured.minute))),
358
+ };
359
+ this.rollCycleIfNeeded();
360
+ }
361
+ else {
362
+ // null / malformed (unset) = machine-local midnight — the default.
363
+ this.reset = { offsetHours: localOffsetHours(), hour: 0, minute: 0 };
364
+ this.rollCycleIfNeeded();
365
+ }
366
+ },
367
+ });
319
368
  });
320
369
  // Snapshot route: optional — only mounted when a webServer service exists
321
370
  // (the Web profile); headless deployments keep the enforcement without it.
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.17",
4
+ "version": "0.1.19",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -31,8 +31,9 @@
31
31
  "dsh": {
32
32
  "client": {
33
33
  "inject": [
34
+ "@deepseek-ai/dsh-api-session-controller",
34
35
  "@deepseek-ai/dsh-client-locale",
35
- "@deepseek-ai/dsh-client-runtime",
36
+ "@deepseek-ai/dsh-client-store",
36
37
  "@deepseek-ai/dsh-client-ui-settings",
37
38
  "@deepseek-ai/dsh-client-ui-slots",
38
39
  "@deepseek-ai/dsh-api-remotes"
@@ -56,28 +57,31 @@
56
57
  "peerDependencies": {
57
58
  "@deepseek-ai/cordis": "*",
58
59
  "@deepseek-ai/dsh-agent": "*",
60
+ "@deepseek-ai/dsh-api-gateway": "*",
59
61
  "@deepseek-ai/dsh-api-remotes": "*",
62
+ "@deepseek-ai/dsh-api-session-controller": "*",
63
+ "@deepseek-ai/dsh-attachment": "*",
64
+ "@deepseek-ai/dsh-brand": "*",
60
65
  "@deepseek-ai/dsh-client-connection": "*",
61
66
  "@deepseek-ai/dsh-client-locale": "*",
62
- "@deepseek-ai/dsh-client-runtime": "*",
67
+ "@deepseek-ai/dsh-client-store": "*",
63
68
  "@deepseek-ai/dsh-client-ui-layout": "*",
69
+ "@deepseek-ai/dsh-client-ui-renderer": "*",
70
+ "@deepseek-ai/dsh-client-ui-session": "*",
64
71
  "@deepseek-ai/dsh-client-ui-settings": "*",
65
72
  "@deepseek-ai/dsh-client-ui-slots": "*",
66
73
  "@deepseek-ai/dsh-invariants": "*",
74
+ "@deepseek-ai/dsh-jobs": "*",
67
75
  "@deepseek-ai/dsh-llm": "*",
68
76
  "@deepseek-ai/dsh-session": "*",
69
- "@deepseek-ai/dsh-settings": "*",
70
- "react": "^18.2.0",
71
- "@deepseek-ai/dsh-host-apiproxy": "*",
72
- "@deepseek-ai/dsh-attachment": "*",
73
- "@deepseek-ai/dsh-brand": "*",
74
- "@deepseek-ai/dsh-jobs": "*",
75
77
  "@deepseek-ai/dsh-session-projection": "*",
78
+ "@deepseek-ai/dsh-settings": "*",
76
79
  "@deepseek-ai/dsh-tools": "*",
77
80
  "@deepseek-ai/dsh-user-approval": "*",
78
81
  "@deepseek-ai/dsh-user-questions": "*",
79
82
  "@deepseek-ai/dsh-workspace": "*",
80
- "@deepseek-ai/schemastery": "*"
83
+ "@deepseek-ai/schemastery": "*",
84
+ "react": "^18.2.0"
81
85
  },
82
86
  "peerDependenciesMeta": {
83
87
  "@deepseek-ai/cordis": {