dsh-workbuddy-connect 0.5.1 → 0.5.3

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/README.en.md CHANGED
@@ -22,13 +22,15 @@ Both the CN **WorkBuddy** and the international **WorkBuddy AI** apps are suppor
22
22
 
23
23
  - **Status and detection**: Settings → Plugins → the matching card shows the account, token validity, remaining credit, and model offers. It also lets you refresh the model list manually and shows whether the current list came from the upstream or from the built-in fallback, and provides manual reasoning-level detection for eligible models.
24
24
 
25
+ - **Enterprise credit**: on the CN product, enterprise accounts (non-empty `enterpriseId`) read their cycle quota from the enterprise billing endpoint, and the card shows an "enterprise quota" row with the cycle reset time.
26
+
25
27
  - **Rate**: every model name carries its credits multiplier (e.g. `GLM-5.2 · x0.79`, `Hy3 · x0.00`) in both the `/model` popup and the composer's model dropdown. The rate is display-only and never affects requests.
26
28
 
27
29
  - **Promo badges**: promo badges (`限时免费`, `夜间折扣`) ride the model name itself (e.g. `Hy4 preview · x0.00 · 限时免费`), visible wherever you pick a model; the status card also collects currently-discounted models. Per the WorkBuddy service data, synced each time DSH starts. The international version's promotions come from the service's `modelPromotions` (which carry an effective window). Once a promotion lapses its badge is withdrawn; because the service writes the discounted value into the model's own rate field, the original price cannot be reconstructed, so that model then reports "price unavailable — refresh to update" rather than repeating the discounted rate or claiming the model is free.
28
30
 
29
31
  ![Settings card showing the plugin](assets/2.png)
30
32
 
31
- The expanded card has three tabs: **Status** shows the account, token validity, total credit, catalog source, and reasoning-level detection; **Context** lists each model's context window (the international version distinguishes the default window from a larger selectable one); **Details** shows per-package credit and model offers. The CN and international versions each get their own card, showing their own account's information.
33
+ The expanded card has three tabs: **Status** shows the account, token validity, total credit, catalog source, and reasoning-level detection; **Context** lists each model's context window (where the international version offers a larger declared window, the "Use the largest declared context window" switch lives here — it is **on by default**, so DSH sizes context compression to the largest window the upstream declares; turn it off to follow the upstream default instead, and the preference persists across restarts); **Details** shows per-package credit and model offers. The CN and international versions each get their own card, showing their own account's information.
32
34
 
33
35
  ![Settings card showing account and remaining credit](assets/3.png)
34
36
 
@@ -110,6 +112,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
110
112
  - **The international version's model catalog comes from the app's own interface**: the service splits it by User-Agent, which is a private implementation detail that a server-side change can break. When that happens the plugin degrades to this account's last successful catalog and then to its built-in roster, showing the source (live / saved / built-in), the fetch time, and the failure reason on the card — but long-term compatibility is not guaranteed. The CN version's catalog uses the same interface as the official CLI and is unaffected.
111
113
  - **International-version environments not yet covered**: on Windows / WSL / Linux no reliable source for the international app's version has been located yet, so the saved value or the built-in default is used. On macOS, real-shim checks covered complete GPT-family replies, tool calls, and continued turns.
112
114
  - **Behaviour change with no credentials**: a version whose app was never signed in — and that left no plugin-owned copy — no longer shows a model group. The CN version used to display a built-in fallback list, but every model on it failed when selected.
115
+ - **The enterprise credit path currently covers the CN product only**: the international enterprise billing interface is unverified, so those accounts still read through the personal endpoint pending measurement. The enterprise branch could not be tested locally (the development machine holds a personal account); it was implemented from the official app's interface contract, and reports from enterprise users are welcome.
113
116
  - Relies on WorkBuddy client interfaces (not a public API); the plugin may need updates as WorkBuddy changes.
114
117
 
115
118
  ## Disclaimer
package/README.md CHANGED
@@ -30,6 +30,8 @@
30
30
 
31
31
  - **信息查看与检测**:设置 → 插件 → 对应卡片可查看账号、令牌有效期、剩余积分和模型优惠;也可以手动刷新模型列表,并在卡片上看到当前列表来自上游还是内置兜底。对于可检测模型,也可以在这里手动检测推理档位。
32
32
 
33
+ - **企业账号积分**:国内版企业账号(`enterpriseId` 非空)走企业专用计费接口读取周期额度,卡片显示「企业额度」与周期重置时间。
34
+
33
35
  - **费率比例**:模型选择列表里每个模型名后直接显示积分倍率(如 `GLM-5.2 · x0.79`、`Hy3 · x0.00`),`/model` 弹窗与输入框的模型下拉都能看到。倍率只是显示,不影响实际请求。
34
36
 
35
37
 
@@ -37,7 +39,7 @@
37
39
 
38
40
  ![设置卡片显示插件](assets/2.png)
39
41
 
40
- 卡片展开后分为「状态 / 上下文 / 明细」三个标签:状态页展示账号、令牌有效期、合计积分、模型列表来源与推理档位检测;上下文页列出各模型的上下文窗口(国际版会区分默认窗口与可选的更大窗口);明细页展示各套餐余量与模型优惠。国内版与国际版各有一张自己的卡片,各显示自己账号的信息。
42
+ 卡片展开后分为「状态 / 上下文 / 明细」三个标签:状态页展示账号、令牌有效期、合计积分、模型列表来源与推理档位检测;上下文页列出各模型的上下文窗口。国际版在上游声明了更大可选窗口时,可在这里切换「使用上游声明的最大上下文窗口」;该开关**默认开启**,DSH 会按上游声明的最大窗口安排上下文压缩;想改用上游的默认窗口就在这里关掉,偏好会持久化,重启后保持。明细页展示各套餐余量与模型优惠。国内版与国际版各有一张自己的卡片,各显示自己账号的信息。
41
43
 
42
44
  ![设置卡片显示账号与剩余积分](assets/3.png)
43
45
 
@@ -120,6 +122,7 @@ dsh plugin --profile web exec dsh-workbuddy-connect doctor --provider workbuddy-
120
122
  - **国际版的模型目录来自 App 界面接口**:服务端按 User-Agent 分流下发,属私有实现,上游改动可能使其失效。届时插件按「本账号上次成功目录 → 内置目录」降级,并在卡片上标明来源(实时 / 已保存 / 内置)、更新时间与失败原因,但不能保证长期兼容。国内版目录走官方 CLI 同款接口,不受此影响。
121
123
  - **国际版仍未覆盖的环境**:Windows / WSL / Linux 下国际版 App 的版本读取尚未找到可靠来源,会退回最近保存的版本或内置值。macOS 上已通过真实 shim 验证 GPT 系完整回复、工具调用与续轮。
122
124
  - **无凭据时的行为变化**:某版 App 从未登录、也没留下插件自留副本时,该版模型分组不再显示。此前国内版会显示一份内置兜底列表,但那些模型选了必然报错。
125
+ - **企业账号积分目前仅覆盖国内版**:国际版企业账号的计费接口尚未验证,仍按个人版接口读取;待有实测结论后再扩展。企业账号分支在本机无法自测(开发机为个人账号),依据官方 App 的接口契约实现,欢迎企业账号用户反馈实测结果。
123
126
  - 依赖 WorkBuddy 客户端接口(非官方开放 API),WorkBuddy 更新后插件可能需要随之调整。
124
127
 
125
128
  ## 免责声明
package/lib/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { E as WorkBuddyUpstreamClient, J as resolveAppVersion, b as WorkBuddyCredentialStore, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, l as readHostHeartbeat, n as CN_VARIANT, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, u as workbuddyHostHeartbeatPath } from "./variants-CE_2IvEG.js";
2
+ import { E as WorkBuddyUpstreamClient, J as resolveAppVersion, b as WorkBuddyCredentialStore, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, l as readHostHeartbeat, n as CN_VARIANT, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, u as workbuddyHostHeartbeatPath } from "./variants-CdmzOPIT.js";
3
3
  import { realpathSync } from "node:fs";
4
4
  import { fileURLToPath } from "node:url";
5
5
  //#region src/bin.ts
@@ -118,7 +118,13 @@ async function status(jsonOutput, variant) {
118
118
  let credits;
119
119
  try {
120
120
  const credential = await store.current();
121
- if (credential !== void 0) credits = { total: (await client.fetchCredits(credential)).total };
121
+ if (credential !== void 0) {
122
+ const fetched = await client.fetchCredits(credential);
123
+ credits = {
124
+ total: fetched.total,
125
+ ...fetched.unlimited === true ? { unlimited: true } : {}
126
+ };
127
+ }
122
128
  } catch (error) {
123
129
  credits = {
124
130
  total: 0,
@@ -138,6 +144,7 @@ async function status(jsonOutput, variant) {
138
144
  ...authStatus.domain === void 0 || authStatus.domain === "" ? {} : { domain: authStatus.domain },
139
145
  source: authStatus.source,
140
146
  credits: credits?.total,
147
+ ...credits?.unlimited === true ? { creditsUnlimited: true } : {},
141
148
  ...credits?.error === void 0 ? {} : { creditsError: credits.error },
142
149
  hostBundle: hostState
143
150
  });
@@ -146,7 +153,7 @@ async function status(jsonOutput, variant) {
146
153
  process.stdout.write([
147
154
  `${variant.displayName} Connect: signed in${authStatus.nickname === void 0 ? "" : ` as ${authStatus.nickname}`}`,
148
155
  ...expiresAt === void 0 ? [] : [`Access token expires ${expiresAt} (refresh is automatic)`],
149
- credits?.error === void 0 ? `Remaining credit: ${credits?.total ?? "unknown"}` : `Remaining credit: unavailable (${credits.error})`,
156
+ credits?.error !== void 0 ? `Remaining credit: unavailable (${credits.error})` : credits?.unlimited === true ? "Remaining credit: unlimited" : `Remaining credit: ${credits?.total ?? "unknown"}`,
150
157
  `Host bundle: ${hostAlive ? `running (pid ${heartbeat.pid})` : hostState === "stale" ? "stale heartbeat (DSH process exited)" : "not started in this profile"}`,
151
158
  "Client card: load failures are logged to the browser console only; the host provider is unaffected.",
152
159
  ""
package/lib/client.js CHANGED
@@ -31,6 +31,27 @@ window.__ModuleLoader__.load({
31
31
  const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
32
32
  const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
33
33
  //#endregion
34
+ //#region src/client/status-document.ts
35
+ /**
36
+ * Whether a parsed status response really is a status document.
37
+ *
38
+ * A 200 is not a promise about the body: it may be empty, literal `null`, a
39
+ * non-JSON page from a proxy, or an array. Both halves of the browser plugin
40
+ * read the same route, so both must agree on what is valid — storing an
41
+ * unreadable value puts something in state that the next render dereferences.
42
+ *
43
+ * The check is deliberately limited to the discriminator (plus `error`'s
44
+ * `message`, which the error paragraph renders): validating optional fields
45
+ * here would reject documents the host legitimately omits fields from.
46
+ */
47
+ function isWorkBuddyWebStatus(value) {
48
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
49
+ const wrapped = value;
50
+ const status = wrapped["status"];
51
+ if (status === "signed-out" || status === "signed-in") return true;
52
+ return status === "error" && typeof wrapped["message"] === "string";
53
+ }
54
+ //#endregion
34
55
  //#region src/client/WorkBuddyPluginCard.tsx
35
56
  /** WorkBuddy status card contributed to Harness Plugin configuration. */
36
57
  /** CN WorkBuddy; the plugin's long-standing card and default. */
@@ -42,15 +63,17 @@ window.__ModuleLoader__.load({
42
63
  statusPath: WORKBUDDY_STATUS_PATH,
43
64
  probePath: WORKBUDDY_PROBE_PATH
44
65
  };
45
- /** Both cards, in display order. */
46
- const CARD_VARIANTS = [CN_CARD_VARIANT, {
66
+ /** International WorkBuddy AI. */
67
+ const AI_CARD_VARIANT = {
47
68
  id: "workbuddy-ai",
48
69
  titleKey: "titleAI",
49
70
  introKey: "introAI",
50
71
  signedOutKey: "signedOutHintAI",
51
72
  statusPath: WORKBUDDY_AI_STATUS_PATH,
52
73
  probePath: WORKBUDDY_AI_PROBE_PATH
53
- }];
74
+ };
75
+ /** Both cards, in display order. */
76
+ const CARD_VARIANTS = [CN_CARD_VARIANT, AI_CARD_VARIANT];
54
77
  const POLL_INTERVAL_MS = 6e4;
55
78
  const cardStyle = {
56
79
  overflow: "hidden",
@@ -178,6 +201,22 @@ window.__ModuleLoader__.load({
178
201
  lineHeight: "18px",
179
202
  color: "var(--dsw-alias-label-tertiary)"
180
203
  };
204
+ const contextPreferenceStyle = {
205
+ display: "flex",
206
+ alignItems: "flex-start",
207
+ gap: 9,
208
+ padding: "10px 12px",
209
+ border: "1px solid var(--dsw-alias-border-l2)",
210
+ borderRadius: 8,
211
+ color: "var(--dsw-alias-label-primary)",
212
+ fontSize: 13,
213
+ lineHeight: "20px"
214
+ };
215
+ const contextPreferenceCopyStyle = {
216
+ display: "flex",
217
+ flexDirection: "column",
218
+ gap: 2
219
+ };
181
220
  const modelBadgeChipStyle = {
182
221
  padding: "1px 8px",
183
222
  borderRadius: 999,
@@ -291,6 +330,12 @@ window.__ModuleLoader__.load({
291
330
  background: "var(--dsw-alias-brand-primary, #1677ff)"
292
331
  };
293
332
  }
333
+ /**
334
+ * Status dot colour. Takes `'loading'` as well as the document's own states:
335
+ * before the first response the card knows nothing about the account, so it must
336
+ * not borrow the signed-out grey — that would read as "nothing is wrong, nobody
337
+ * is signed in" when the truth is "not read yet".
338
+ */
294
339
  function dotStyle(status) {
295
340
  return {
296
341
  width: 9,
@@ -309,29 +354,67 @@ window.__ModuleLoader__.load({
309
354
  timeStyle: "short"
310
355
  }).format(new Date(ms));
311
356
  }
312
- /** One billing package as a labeled progress bar. */
313
- function CreditBar({ label, remain, size, t }) {
314
- const detail = size > 0 ? t("exactRemaining", {
357
+ function formatCycleReset(time) {
358
+ const parsed = Date.parse(time);
359
+ if (!Number.isNaN(parsed)) return formatTime(parsed);
360
+ return time;
361
+ }
362
+ /**
363
+ * One billing package as a labeled progress bar.
364
+ *
365
+ * A package whose allowance the upstream never reported (`size` not positive)
366
+ * has no percentage to state. It must not fall back to 100%: the plugin would be
367
+ * claiming a full quota it knows nothing about, which is the opposite of the
368
+ * honest "remaining N" line printed below it. Unknown size therefore renders the
369
+ * percent slot as unknown copy and an unfilled, indeterminate track.
370
+ */
371
+ function CreditBar({ label, remain, size, unlimited, t }) {
372
+ if (unlimited === true) {
373
+ const quotaText = t("unlimitedQuota");
374
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
375
+ style: quotaGroupStyle,
376
+ children: [
377
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
378
+ style: quotaLabelStyle,
379
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: quotaText })]
380
+ }),
381
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
382
+ style: progressTrackStyle,
383
+ role: "progressbar",
384
+ "aria-label": label,
385
+ "aria-valuetext": quotaText
386
+ }),
387
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
388
+ style: bodyStyle,
389
+ children: quotaText
390
+ })
391
+ ]
392
+ });
393
+ }
394
+ const sizeKnown = size > 0;
395
+ const detail = sizeKnown ? t("exactRemaining", {
315
396
  remain: formatNumber(remain),
316
397
  size: formatNumber(size)
317
398
  }) : t("creditPackageUnknownSize", { remain: formatNumber(remain) });
318
- const percent = size > 0 ? remain / size * 100 : 100;
319
- const display = new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent);
399
+ const percent = sizeKnown ? remain / size * 100 : void 0;
400
+ const display = percent === void 0 ? t("percentUnknown") : t("percentRemaining", { percent: new Intl.NumberFormat(void 0, { maximumFractionDigits: 1 }).format(percent) });
320
401
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
321
402
  style: quotaGroupStyle,
322
403
  children: [
323
404
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
324
405
  style: quotaLabelStyle,
325
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("percentRemaining", { percent: display }) })]
406
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: display })]
326
407
  }),
327
408
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
328
409
  style: progressTrackStyle,
329
410
  role: "progressbar",
330
411
  "aria-label": label,
331
- "aria-valuemin": 0,
332
- "aria-valuemax": 100,
333
- "aria-valuenow": percent,
334
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: progressFillStyle(percent) })
412
+ ...percent === void 0 ? { "aria-valuetext": detail } : {
413
+ "aria-valuemin": 0,
414
+ "aria-valuemax": 100,
415
+ "aria-valuenow": percent
416
+ },
417
+ children: percent === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { style: progressFillStyle(percent) })
335
418
  }),
336
419
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
337
420
  style: bodyStyle,
@@ -387,31 +470,56 @@ window.__ModuleLoader__.load({
387
470
  * the default is the budget actually requested, while the larger value is a
388
471
  * ceiling the upstream would accept.
389
472
  */
390
- function ContextTable({ models, t }) {
473
+ function ContextTable({ models, t, useMaximumContextWindow, disabled, onUseMaximumContextWindow }) {
391
474
  const known = (models ?? []).filter((model) => model.contextWindow !== void 0).sort((a, b) => b.contextWindow - a.contextWindow);
392
- if (known.length === 0) return null;
475
+ const canSelectMaximum = known.some((model) => model.maxContextWindow !== void 0 && model.maxContextWindow > (model.defaultContextWindow ?? model.contextWindow ?? 0));
476
+ const showPreference = onUseMaximumContextWindow !== void 0 && (canSelectMaximum || useMaximumContextWindow === true);
477
+ if (known.length === 0 && !showPreference) return null;
393
478
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
394
479
  style: quotaListStyle,
395
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
396
- style: quotaTitleStyle,
397
- children: t("contextHeading")
398
- }), known.map((model) => {
399
- const capacity = model.contextWindow;
400
- const alternative = model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
401
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
402
- style: quotaLabelStyle,
403
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
404
- style: modelOfferStyle,
405
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
406
- style: { textAlign: "right" },
407
- children: formatTokens(capacity)
408
- }), alternative === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
480
+ children: [
481
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
482
+ style: quotaTitleStyle,
483
+ children: t("contextHeading")
484
+ }),
485
+ showPreference && onUseMaximumContextWindow !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
486
+ style: contextPreferenceStyle,
487
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
488
+ type: "checkbox",
489
+ checked: useMaximumContextWindow === true,
490
+ disabled,
491
+ onChange: (event) => {
492
+ onUseMaximumContextWindow(event.currentTarget.checked);
493
+ }
494
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
495
+ style: contextPreferenceCopyStyle,
496
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("useMaximumContextWindow") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
409
497
  style: modelRateStyle,
410
- children: t("contextUpTo", { size: formatTokens(alternative) })
498
+ children: t("useMaximumContextWindowHint")
411
499
  })]
412
500
  })]
413
- }, model.id);
414
- })]
501
+ }) : null,
502
+ known.map((model) => {
503
+ const capacity = model.contextWindow;
504
+ const alternative = model.maxContextWindow !== void 0 && model.maxContextWindow > capacity ? model.maxContextWindow : void 0;
505
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
506
+ style: quotaLabelStyle,
507
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: model.name }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
508
+ style: modelOfferStyle,
509
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
510
+ style: { textAlign: "right" },
511
+ children: formatTokens(capacity)
512
+ }), alternative !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
513
+ style: modelRateStyle,
514
+ children: t("contextUpTo", { size: formatTokens(alternative) })
515
+ }) : model.defaultContextWindow !== void 0 && model.defaultContextWindow < capacity ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
516
+ style: modelRateStyle,
517
+ children: t("contextDefault", { size: formatTokens(model.defaultContextWindow) })
518
+ }) : null]
519
+ })]
520
+ }, model.id);
521
+ })
522
+ ]
415
523
  });
416
524
  }
417
525
  /**
@@ -555,17 +663,72 @@ window.__ModuleLoader__.load({
555
663
  function WorkBuddyPluginCard({ t, variant = CN_CARD_VARIANT }) {
556
664
  if (t === void 0) throw new Error("WorkBuddy plugin card requires its translation function");
557
665
  const [open, setOpen] = (0, react.useState)(false);
558
- const [status, setStatus] = (0, react.useState)({ status: "signed-out" });
666
+ /**
667
+ * The document to render. `undefined` means *not read yet*, which is a
668
+ * distinct state from "signed out": seeding this with a signed-out document
669
+ * told an already-signed-in user they were signed out for the whole first
670
+ * round trip (and forever, if the read never settled).
671
+ */
672
+ const [status, setStatus] = (0, react.useState)();
673
+ /**
674
+ * Whether the last **successful** read found a usable credential.
675
+ *
676
+ * Kept apart from `status` because the poll's liveness must depend on what the
677
+ * account actually is, not on what the card last displayed: a failed read
678
+ * leaves this untouched, so a transient failure cannot disarm the interval,
679
+ * while a genuine signed-out answer still stops it.
680
+ *
681
+ * `undefined` therefore means "no successful read yet", which is also the
682
+ * condition that decides whether a failed read has anything to preserve.
683
+ */
684
+ const [signedIn, setSignedIn] = (0, react.useState)();
685
+ /**
686
+ * Why the most recent read failed, when it did. Rendered as a notice beside
687
+ * whatever document is still on screen, rather than replacing it.
688
+ */
689
+ const [readFailure, setReadFailure] = (0, react.useState)();
559
690
  const [busy, setBusy] = (0, react.useState)(false);
560
691
  const [tab, setTab] = (0, react.useState)("status");
561
692
  const mounted = (0, react.useRef)(true);
693
+ /**
694
+ * Identity of the newest read that may write. Assigned when a read *starts*,
695
+ * so a response is superseded by anything begun after it — "the response whose
696
+ * request started last wins". Without this, a slow poll begun before a manual
697
+ * action could settle after the action's own refresh and restore the older
698
+ * document.
699
+ */
700
+ const readSeq = (0, react.useRef)(0);
701
+ /** Manual requests in flight, so unmount can abort them like the poll's. */
702
+ const manualControllers = (0, react.useRef)(/* @__PURE__ */ new Set());
562
703
  (0, react.useEffect)(() => {
563
704
  mounted.current = true;
564
705
  return () => {
565
706
  mounted.current = false;
707
+ for (const controller of manualControllers.current) controller.abort();
708
+ manualControllers.current.clear();
566
709
  };
567
710
  }, []);
711
+ /** Register a manual request's controller so unmount aborts it. */
712
+ const trackController = (0, react.useCallback)(() => {
713
+ const controller = new AbortController();
714
+ manualControllers.current.add(controller);
715
+ return controller;
716
+ }, []);
717
+ /**
718
+ * Read the status document and apply it under the two policies the card's
719
+ * correctness rests on:
720
+ *
721
+ * - a non-document body (empty, `null`, a non-JSON page) is a failed read, not
722
+ * something to store and then dereference in the render;
723
+ * - a failed read never discards a document already on screen. It is recorded
724
+ * and shown as a notice beside that document; only when nothing has been
725
+ * read yet does the failure itself become the rendered state.
726
+ *
727
+ * Returns whether this read produced the current document.
728
+ */
568
729
  const refresh = (0, react.useCallback)(async (signal) => {
730
+ const seq = ++readSeq.current;
731
+ const current = () => mounted.current && signal?.aborted !== true && seq === readSeq.current;
569
732
  try {
570
733
  const response = await fetch(variant.statusPath, {
571
734
  headers: { accept: "application/json" },
@@ -574,12 +737,23 @@ window.__ModuleLoader__.load({
574
737
  });
575
738
  const value = await response.json().catch(() => void 0);
576
739
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
577
- if (mounted.current && signal?.aborted !== true) setStatus(value);
740
+ if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
741
+ if (!current()) return false;
742
+ setStatus(value);
743
+ if (value.status === "signed-in") setSignedIn(true);
744
+ else if (value.status === "signed-out") setSignedIn(false);
745
+ setReadFailure(void 0);
746
+ return true;
578
747
  } catch (error) {
579
- if (mounted.current && signal?.aborted !== true) setStatus({
580
- status: "error",
581
- message: error instanceof Error ? error.message : t("requestFailed")
582
- });
748
+ const message = error instanceof Error ? error.message : t("requestFailed");
749
+ if (current()) {
750
+ setReadFailure(message);
751
+ setStatus((previous) => previous === void 0 ? {
752
+ status: "error",
753
+ message
754
+ } : previous);
755
+ }
756
+ return false;
583
757
  }
584
758
  }, [t, variant.statusPath]);
585
759
  (0, react.useEffect)(() => {
@@ -591,7 +765,7 @@ window.__ModuleLoader__.load({
591
765
  };
592
766
  }, [open, refresh]);
593
767
  (0, react.useEffect)(() => {
594
- if (!open || status.status !== "signed-in") return;
768
+ if (!open || signedIn === false) return;
595
769
  const controller = new AbortController();
596
770
  const timer = window.setInterval(() => {
597
771
  refresh(controller.signal);
@@ -603,13 +777,15 @@ window.__ModuleLoader__.load({
603
777
  }, [
604
778
  open,
605
779
  refresh,
606
- status.status
780
+ signedIn
607
781
  ]);
608
782
  const manualRefresh = async () => {
609
783
  setBusy(true);
784
+ const controller = trackController();
610
785
  try {
611
- await refresh();
786
+ await refresh(controller.signal);
612
787
  } finally {
788
+ manualControllers.current.delete(controller);
613
789
  if (mounted.current) setBusy(false);
614
790
  }
615
791
  };
@@ -622,9 +798,10 @@ window.__ModuleLoader__.load({
622
798
  * than thrown away, so the reason survives the round trip.
623
799
  */
624
800
  const refreshModels = (0, react.useCallback)(async () => {
625
- const key = status.status === "signed-in" ? status.probeKey : void 0;
801
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
626
802
  if (key === void 0) return;
627
803
  setBusy(true);
804
+ const controller = trackController();
628
805
  try {
629
806
  const response = await fetch(variant.probePath, {
630
807
  method: "POST",
@@ -633,23 +810,27 @@ window.__ModuleLoader__.load({
633
810
  "X-WorkBuddy-Probe-Key": key
634
811
  },
635
812
  credentials: "same-origin",
813
+ signal: controller.signal,
636
814
  body: JSON.stringify({ action: "refresh" })
637
815
  });
638
816
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
639
817
  } catch (error) {
640
- if (mounted.current) setStatus((previous) => ({
641
- status: "error",
642
- message: error instanceof Error ? error.message : t("requestFailed")
643
- }));
818
+ if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
819
+ manualControllers.current.delete(controller);
644
820
  return;
645
821
  } finally {
646
822
  if (mounted.current) setBusy(false);
647
823
  }
648
- await refresh();
824
+ try {
825
+ await refresh(controller.signal);
826
+ } finally {
827
+ manualControllers.current.delete(controller);
828
+ }
649
829
  }, [
650
830
  refresh,
651
831
  status,
652
832
  t,
833
+ trackController,
653
834
  variant.probePath
654
835
  ]);
655
836
  /**
@@ -660,9 +841,10 @@ window.__ModuleLoader__.load({
660
841
  * catalog from here.
661
842
  */
662
843
  const control = (0, react.useCallback)(async (action) => {
663
- const key = status.status === "signed-in" ? status.probeKey : void 0;
844
+ const key = status?.status === "signed-in" ? status.probeKey : void 0;
664
845
  if (key === void 0) return;
665
846
  setBusy(true);
847
+ const controller = trackController();
666
848
  try {
667
849
  const response = await fetch(variant.probePath, {
668
850
  method: "POST",
@@ -671,6 +853,7 @@ window.__ModuleLoader__.load({
671
853
  "X-WorkBuddy-Probe-Key": key
672
854
  },
673
855
  credentials: "same-origin",
856
+ signal: controller.signal,
674
857
  body: JSON.stringify(action)
675
858
  });
676
859
  const value = await response.json().catch(() => void 0);
@@ -678,19 +861,22 @@ window.__ModuleLoader__.load({
678
861
  const message = typeof value === "object" && value !== null && "error" in value ? String(value["error"]) : `HTTP ${response.status}`;
679
862
  throw new Error(message);
680
863
  }
681
- await refresh();
864
+ if (action.action === "set-maximum-context-window" && (typeof value !== "object" || value === null || value["state"] !== "updated")) {
865
+ const reason = typeof value === "object" && value !== null && "reason" in value ? String(value["reason"]) : t("requestFailed");
866
+ throw new Error(reason);
867
+ }
868
+ await refresh(controller.signal);
682
869
  } catch (error) {
683
- if (mounted.current) setStatus((previous) => ({
684
- status: "error",
685
- message: error instanceof Error ? error.message : t("requestFailed")
686
- }));
870
+ if (mounted.current && controller.signal.aborted !== true) setReadFailure(error instanceof Error ? error.message : t("requestFailed"));
687
871
  } finally {
872
+ manualControllers.current.delete(controller);
688
873
  if (mounted.current) setBusy(false);
689
874
  }
690
875
  }, [
691
876
  refresh,
692
877
  status,
693
878
  t,
879
+ trackController,
694
880
  variant.probePath
695
881
  ]);
696
882
  /**
@@ -704,7 +890,7 @@ window.__ModuleLoader__.load({
704
890
  });
705
891
  }, [control]);
706
892
  const title = t(variant.titleKey);
707
- const label = status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).trimEnd().replace(/[::]$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
893
+ const label = status === void 0 ? t("loading") : status.status === "signed-in" ? status.nickname === void 0 ? t("signedInAs", { nickname: "" }).trimEnd().replace(/[::]$/, "") : t("signedInAs", { nickname: status.nickname }) : status.status === "error" ? t("requestFailed") : t("signedOut");
708
894
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
709
895
  style: cardStyle,
710
896
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
@@ -744,9 +930,10 @@ window.__ModuleLoader__.load({
744
930
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
745
931
  style: statusStyle,
746
932
  role: "status",
933
+ "aria-busy": status === void 0,
747
934
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
748
935
  "aria-hidden": "true",
749
- style: dotStyle(status.status)
936
+ style: dotStyle(status === void 0 ? "loading" : status.status)
750
937
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: label })]
751
938
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
752
939
  type: "button",
@@ -758,7 +945,11 @@ window.__ModuleLoader__.load({
758
945
  children: busy ? t("refreshing") : t("refresh")
759
946
  })]
760
947
  }),
761
- status.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
948
+ readFailure === void 0 || signedIn === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
949
+ style: errorStyle,
950
+ children: t("statusRefreshFailed", { message: readFailure })
951
+ }),
952
+ status?.status === "signed-in" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
762
953
  status.expiresAt === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
763
954
  style: bodyStyle,
764
955
  children: t("accessTokenExpires", { time: formatTime(status.expiresAt) })
@@ -806,18 +997,21 @@ window.__ModuleLoader__.load({
806
997
  tab === "status" ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
807
998
  style: tabPanelStyle,
808
999
  children: [
809
- status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1000
+ status.credits === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
810
1001
  style: quotaListStyle,
811
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1002
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
812
1003
  style: rowStyle,
813
1004
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
814
1005
  style: quotaTitleStyle,
815
1006
  children: t("creditsHeading")
816
1007
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
817
1008
  style: bodyStyle,
818
- children: t("creditsTotal", { total: formatNumber(status.credits.total) })
1009
+ children: status.credits.unlimited === true ? t("creditsTotalUnlimited") : t("creditsTotal", { total: formatNumber(status.credits.total) })
819
1010
  })]
820
- })
1011
+ }), status.credits.cycleResetTime === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1012
+ style: descriptionStyle,
1013
+ children: t("cycleResetAt", { time: formatCycleReset(status.credits.cycleResetTime) })
1014
+ })]
821
1015
  }),
822
1016
  status.creditsError === void 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
823
1017
  style: errorStyle,
@@ -837,7 +1031,15 @@ window.__ModuleLoader__.load({
837
1031
  style: tabPanelStyle,
838
1032
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ContextTable, {
839
1033
  models: status.models,
840
- t
1034
+ t,
1035
+ disabled: busy,
1036
+ ...status.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: status.useMaximumContextWindow },
1037
+ ...variant.id === AI_CARD_VARIANT.id ? { onUseMaximumContextWindow: (enabled) => {
1038
+ control({
1039
+ action: "set-maximum-context-window",
1040
+ enabled
1041
+ });
1042
+ } } : {}
841
1043
  })
842
1044
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
843
1045
  style: tabPanelStyle,
@@ -846,10 +1048,11 @@ window.__ModuleLoader__.load({
846
1048
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
847
1049
  style: quotaTitleStyle,
848
1050
  children: t("creditsDetailHeading")
849
- }), status.credits.accounts.filter((account) => account.remain > 0).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
850
- label: account.packageName,
1051
+ }), status.credits.accounts.filter((account) => account.packageName === "enterprise" || account.remain > 0 || account.unlimited === true).map((account, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CreditBar, {
1052
+ label: account.packageName === "enterprise" ? t("packageEnterprise") : account.packageName,
851
1053
  remain: account.remain,
852
1054
  size: account.size,
1055
+ unlimited: account.unlimited,
853
1056
  t
854
1057
  }, `${account.packageName}-${String(index)}`))]
855
1058
  }), status.models === void 0 || status.models.length === 0 ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -864,11 +1067,11 @@ window.__ModuleLoader__.load({
864
1067
  })]
865
1068
  })
866
1069
  ] }) : null,
867
- status.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1070
+ status?.status === "signed-out" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
868
1071
  style: status.reason === void 0 ? bodyStyle : errorStyle,
869
1072
  children: status.reason ?? t(variant.signedOutKey)
870
1073
  }) : null,
871
- status.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1074
+ status?.status === "error" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
872
1075
  style: errorStyle,
873
1076
  children: status.message
874
1077
  }) : null
@@ -1064,15 +1267,24 @@ window.__ModuleLoader__.load({
1064
1267
  /**
1065
1268
  * The one-line tooltip: current state first, then what a click does — the same
1066
1269
  * two-part shape Fast Mode uses.
1270
+ *
1271
+ * A recorded result outranks a remembered failure. `failed` only means "the last
1272
+ * run from this control did not complete"; the host can record a result for the
1273
+ * same model at any time (a detection started from the settings card, another
1274
+ * conversation, or a finished sweep), and the levels the user paid for are the
1275
+ * more useful answer than the stale failure. Failure copy is what remains when
1276
+ * there is no result to report.
1067
1277
  */
1068
1278
  function tooltipText(t, model, state) {
1069
1279
  if (state.busy) return t("probeRunning", { model });
1070
- if (state.failed) return t("probeTooltipRetry");
1071
1280
  const result = state.result;
1072
- if (result === void 0) return t("probeTooltipIdle", { model });
1073
- if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
1074
- if (result.validation === "non-validating") return t("probeTooltipNotValidating");
1075
- return t("probeTooltipRetry");
1281
+ if (result !== void 0) {
1282
+ if (result.validation === "validating" && result.efforts.length > 0) return t("probeTooltipVerified", { levels: result.efforts.join(" / ") });
1283
+ if (result.validation === "non-validating") return t("probeTooltipNotValidating");
1284
+ return t("probeTooltipRetry");
1285
+ }
1286
+ if (state.failed) return t("probeTooltipRetry");
1287
+ return t("probeTooltipIdle", { model });
1076
1288
  }
1077
1289
  /** Model-independent shell: resolves the selection, then delegates per model. */
1078
1290
  function WorkBuddyProbeControl({ directory, t }) {
@@ -1097,17 +1309,20 @@ window.__ModuleLoader__.load({
1097
1309
  const [note, setNote] = (0, react.useState)();
1098
1310
  const inFlight = (0, react.useRef)(false);
1099
1311
  const mounted = (0, react.useRef)(false);
1312
+ const readSeq = (0, react.useRef)(0);
1100
1313
  const tooltipId = (0, react.useId)();
1101
1314
  const refresh = (0, react.useCallback)(async (signal) => {
1315
+ const seq = ++readSeq.current;
1102
1316
  const response = await fetch(card.statusPath, {
1103
1317
  credentials: "same-origin",
1104
1318
  headers: { accept: "application/json" },
1105
1319
  ...signal === void 0 ? {} : { signal }
1106
1320
  });
1107
1321
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
1108
- const value = await response.json();
1109
- if (mounted.current && !signal?.aborted) setStatus(value);
1110
- }, [card.statusPath]);
1322
+ const value = await response.json().catch(() => void 0);
1323
+ if (!isWorkBuddyWebStatus(value)) throw new Error(t("statusResponseInvalid"));
1324
+ if (mounted.current && !signal?.aborted && seq === readSeq.current) setStatus(value);
1325
+ }, [card.statusPath, t]);
1111
1326
  (0, react.useEffect)(() => {
1112
1327
  mounted.current = true;
1113
1328
  const controller = new AbortController();
@@ -1128,6 +1343,9 @@ window.__ModuleLoader__.load({
1128
1343
  const key = status?.status === "signed-in" ? status.probeKey : void 0;
1129
1344
  const result = status === void 0 ? void 0 : resultFor(status, model);
1130
1345
  const visible = probe?.candidates.includes(model) === true || result !== void 0;
1346
+ (0, react.useEffect)(() => {
1347
+ if (result !== void 0) setFailed(false);
1348
+ }, [result]);
1131
1349
  (0, react.useEffect)(() => {
1132
1350
  setConfirming(false);
1133
1351
  setNote(void 0);
@@ -1313,7 +1531,12 @@ window.__ModuleLoader__.load({
1313
1531
  tabDetails: "Credit details",
1314
1532
  creditsDetailHeading: "By package",
1315
1533
  creditsTotal: "Total: {total}",
1534
+ creditsTotalUnlimited: "Total: Unlimited",
1535
+ unlimitedQuota: "Unlimited",
1536
+ packageEnterprise: "Enterprise quota",
1537
+ cycleResetAt: "Resets {time}",
1316
1538
  percentRemaining: "{percent}% remaining",
1539
+ percentUnknown: "Remaining share unknown",
1317
1540
  exactRemaining: "{remain} / {size} remaining",
1318
1541
  creditPackageUnknownSize: "{remain} remaining",
1319
1542
  creditsError: "Credit unavailable: {message}",
@@ -1327,10 +1550,15 @@ window.__ModuleLoader__.load({
1327
1550
  catalogError: "Last update failed: {message}",
1328
1551
  catalogAppVersion: "App version {version}",
1329
1552
  requestFailed: "Request failed",
1553
+ statusRefreshFailed: "Refresh failed: {message} — showing the last known state",
1554
+ statusResponseInvalid: "WorkBuddy returned an unreadable status reply",
1330
1555
  accountHeading: "Account",
1331
1556
  modelsHeading: "Model offers",
1332
1557
  contextHeading: "Context window",
1333
1558
  contextUpTo: "up to {size}",
1559
+ contextDefault: "default {size}",
1560
+ useMaximumContextWindow: "Use the largest declared context window",
1561
+ useMaximumContextWindowHint: "Applies to WorkBuddy AI models that offer a larger window.",
1334
1562
  freeModel: "Free",
1335
1563
  badgeLimitedFree: "Limited-time free",
1336
1564
  badgeNightDiscount: "Night discount",
@@ -1386,7 +1614,12 @@ window.__ModuleLoader__.load({
1386
1614
  tabDetails: "积分详情",
1387
1615
  creditsDetailHeading: "按套餐",
1388
1616
  creditsTotal: "合计:{total}",
1617
+ creditsTotalUnlimited: "合计:不限额",
1618
+ unlimitedQuota: "不限额",
1619
+ packageEnterprise: "企业额度",
1620
+ cycleResetAt: "重置时间:{time}",
1389
1621
  percentRemaining: "剩余 {percent}%",
1622
+ percentUnknown: "剩余占比未知",
1390
1623
  exactRemaining: "剩余 {remain} / {size}",
1391
1624
  creditPackageUnknownSize: "剩余 {remain}",
1392
1625
  creditsError: "积分查询失败:{message}",
@@ -1400,10 +1633,15 @@ window.__ModuleLoader__.load({
1400
1633
  catalogError: "上次更新失败:{message}",
1401
1634
  catalogAppVersion: "App 版本 {version}",
1402
1635
  requestFailed: "请求失败",
1636
+ statusRefreshFailed: "刷新失败:{message} — 当前显示的是上次成功获取的状态",
1637
+ statusResponseInvalid: "WorkBuddy 返回的状态数据无法识别",
1403
1638
  accountHeading: "账号",
1404
1639
  modelsHeading: "模型优惠",
1405
1640
  contextHeading: "上下文窗口",
1406
1641
  contextUpTo: "最高 {size}",
1642
+ contextDefault: "默认 {size}",
1643
+ useMaximumContextWindow: "使用上游声明的最大上下文窗口",
1644
+ useMaximumContextWindowHint: "仅作用于 WorkBuddy AI 中声明了更大窗口的模型。",
1407
1645
  freeModel: "免费",
1408
1646
  badgeLimitedFree: "限时免费",
1409
1647
  badgeNightDiscount: "夜间折扣",
package/lib/index.d.ts CHANGED
@@ -225,6 +225,8 @@ interface WorkBuddyUpstreamModel {
225
225
  id: string;
226
226
  name: string;
227
227
  contextWindow: number;
228
+ /** The upstream's preferred window before an optional maximum is selected. */
229
+ defaultContextWindow?: number;
228
230
  maxInputTokens?: number;
229
231
  supportedContextWindows?: readonly number[];
230
232
  promotions?: readonly WorkBuddyPromotion[];
@@ -295,11 +297,24 @@ interface WorkBuddyCreditAccount {
295
297
  packageName: string;
296
298
  remain: number;
297
299
  size: number;
300
+ unlimited?: true;
298
301
  }
299
302
  /** Aggregated credit answer for one credential. */
300
303
  interface WorkBuddyCredits {
301
304
  total: number;
302
305
  accounts: readonly WorkBuddyCreditAccount[];
306
+ /**
307
+ * The account's cycle quota is uncapped (`limitNum === -1` on the CN
308
+ * enterprise endpoint).
309
+ *
310
+ * A separate flag rather than a `-1`/`0` sentinel in {@link total}: the two
311
+ * mean opposite things to a reader ("no limit" vs "nothing left"), and the
312
+ * existing negative-clamp in the personal branch would turn a sentinel into
313
+ * a plausible-looking zero. Every renderer must therefore test this flag
314
+ * first and not fall back to `total` when it is set.
315
+ */
316
+ unlimited?: true;
317
+ cycleResetTime?: string;
303
318
  }
304
319
  /** Token refresh answer; fields the upstream omits stay absent. */
305
320
  interface WorkBuddyRefreshOutcome {
@@ -409,8 +424,41 @@ declare class WorkBuddyUpstreamClient {
409
424
  * such rather than as a generic catalog failure.
410
425
  */
411
426
  fetchModels(credential: WorkBuddyCredential, signal?: AbortSignal): Promise<readonly WorkBuddyUpstreamModel[]>;
412
- /** POST the billing endpoint for the aggregated remaining credit. */
427
+ /**
428
+ * POST the billing endpoint for the aggregated remaining credit.
429
+ *
430
+ * Two upstream shapes, chosen by account type:
431
+ *
432
+ * - **CN enterprise** (`regionOf === 'cn'` and `enterpriseId` non-empty) asks
433
+ * `/v2/billing/meter/get-enterprise-user-usage`, which answers with a single
434
+ * cycle quota. The personal endpoint serves these accounts an empty
435
+ * `Accounts` list, which the card then renders as "0 credit" — a wrong
436
+ * number rather than a visible failure (issue #31).
437
+ * - **Everyone else** keeps the personal endpoint unchanged.
438
+ *
439
+ * The region gate is load-bearing: the enterprise endpoint is unverified for
440
+ * the global region, so an international credential that happens to carry an
441
+ * `enterpriseId` must stay on the measured personal path instead of being
442
+ * moved onto an unmeasured one.
443
+ */
413
444
  fetchCredits(credential: WorkBuddyCredential): Promise<WorkBuddyCredits>;
445
+ /**
446
+ * CN enterprise credit read: a single cycle quota instead of a package list.
447
+ *
448
+ * Verified against the WorkBuddy desktop app (`app.asar`,
449
+ * `BackendProvider.getEnterpriseUsage` and `CloudAccountRepo.billing`): the
450
+ * body is an empty object and the account identity travels only in the
451
+ * headers. The two official call sites disagree on the field spelling
452
+ * (`limitNum`/`credit` vs `limit_num`/`used_num`), so both are accepted.
453
+ *
454
+ * A body carrying no recognisable quota field is a hard error rather than a
455
+ * zero. Rendering `0` for "we did not understand the answer" is exactly how
456
+ * issue #31 stayed invisible while users saw a plausible wrong number.
457
+ *
458
+ * The error names fields and types only: it reaches the browser, and the
459
+ * response body may describe the account's usage.
460
+ */
461
+ private fetchEnterpriseCredits;
414
462
  /**
415
463
  * One probe request: a real streaming chat call carrying the effort under
416
464
  * test.
@@ -704,6 +752,7 @@ declare const FALLBACK_WORKBUDDY_AI_MODELS: readonly WorkBuddyModelInfo[];
704
752
  declare class WorkBuddyCatalog {
705
753
  private models;
706
754
  private visible;
755
+ private useMaximumContextWindow;
707
756
  constructor(initial?: readonly WorkBuddyModelInfo[]);
708
757
  /** Current entries; empty while the variant has no usable credential. */
709
758
  current(): readonly WorkBuddyModelInfo[];
@@ -716,6 +765,8 @@ declare class WorkBuddyCatalog {
716
765
  * caller can skip an invalidation that would re-render an identical list.
717
766
  */
718
767
  setVisible(visible: boolean): boolean;
768
+ /** Select the largest declared international window where the upstream offers one. */
769
+ setUseMaximumContextWindow(useMaximum: boolean): boolean;
719
770
  /** Models to fall back to when the upstream fetch fails; ignores visibility. */
720
771
  fallback(): readonly WorkBuddyModelInfo[];
721
772
  }
@@ -1126,6 +1177,8 @@ interface Config {
1126
1177
  * until the user explicitly agrees.
1127
1178
  */
1128
1179
  probeConsent?: boolean;
1180
+ /** Use the largest context window the international catalog explicitly offers. */
1181
+ useMaximumContextWindow?: boolean;
1129
1182
  }
1130
1183
  declare const Config: z<Config>;
1131
1184
  /**
package/lib/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as parseModelCatalog, B as fallbackChatIdentity, C as desktopAuthCandidatesFor, D as classifyUpstreamError, E as WorkBuddyUpstreamClient, F as probeModel, G as appUserAgent, H as resolveChatIdentity, I as randomSentinel, J as resolveAppVersion, K as installedAppVersion, L as CN_APP_VERSION_FILENAME, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as FALLBACK_CN_APP_VERSION, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as validCliVersion, V as readCliVersion, W as WORKBUDDY_APP_VERSION_FILENAME, Y as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as prepareChatBody, k as normalizeCredits, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as readBundleVersion, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as chatUserAgent } from "./variants-CE_2IvEG.js";
1
+ import { A as parseModelCatalog, B as fallbackChatIdentity, C as desktopAuthCandidatesFor, D as classifyUpstreamError, E as WorkBuddyUpstreamClient, F as probeModel, G as appUserAgent, H as resolveChatIdentity, I as randomSentinel, J as resolveAppVersion, K as installedAppVersion, L as CN_APP_VERSION_FILENAME, M as prepareInternationalChatBody, N as regionOf, O as modelWithCurrentPromotion, P as PROBE_EFFORT_CANDIDATES, R as FALLBACK_CN_APP_VERSION, S as defaultDesktopAuthPath, T as workbuddyOwnAuthPath, U as validCliVersion, V as readCliVersion, W as WORKBUDDY_APP_VERSION_FILENAME, Y as validAppVersion, _ as WorkBuddyCatalog, a as WORKBUDDY_HOST_HEARTBEAT_FILENAME, b as WorkBuddyCredentialStore, c as processStartTimeMs, d as writeHostHeartbeat, f as WORKBUDDY_CONNECT_VERSION, g as FALLBACK_WORKBUDDY_MODELS, h as FALLBACK_WORKBUDDY_AI_MODELS, i as variantFor, j as prepareChatBody, k as normalizeCredits, l as readHostHeartbeat, n as CN_VARIANT, o as clearHostHeartbeat, q as readBundleVersion, r as WORKBUDDY_VARIANTS, s as isHeartbeatProcessAlive, t as AI_VARIANT, u as workbuddyHostHeartbeatPath, v as WORKBUDDY_AUTH_FILENAME, w as parseWorkBuddyAuth, x as defaultDesktopAuthCandidates, y as WORKBUDDY_AUTH_FILE_ENV, z as chatUserAgent } from "./variants-CdmzOPIT.js";
2
2
  import z from "@deepseek-ai/schemastery";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { resolveDshHome } from "@deepseek-ai/dsh-home-paths";
@@ -1001,6 +1001,7 @@ async function workBuddyWebStatus(deps) {
1001
1001
  const rate = normalizeCredits(model.billing?.credits);
1002
1002
  const supported = model.supportedContextWindows ?? [];
1003
1003
  const maxContextWindow = supported.length > 0 ? Math.max(...supported) : void 0;
1004
+ const defaultContextWindow = model.defaultContextWindow ?? model.contextWindow;
1004
1005
  return {
1005
1006
  id: model.id,
1006
1007
  name: model.name,
@@ -1009,7 +1010,8 @@ async function workBuddyWebStatus(deps) {
1009
1010
  ...rate === void 0 ? {} : { credits: rate },
1010
1011
  ...model.billing?.rateUnknown === true ? { rateUnknown: true } : {},
1011
1012
  ...typeof model.contextWindow === "number" && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {},
1012
- ...maxContextWindow === void 0 || maxContextWindow === model.contextWindow ? {} : { maxContextWindow },
1013
+ ...typeof defaultContextWindow === "number" && defaultContextWindow > 0 && defaultContextWindow < model.contextWindow ? { defaultContextWindow } : {},
1014
+ ...maxContextWindow === void 0 || maxContextWindow <= defaultContextWindow ? {} : { maxContextWindow },
1013
1015
  ...typeof model.maxInputTokens === "number" && model.maxInputTokens > 0 ? { maxInputTokens: model.maxInputTokens } : {}
1014
1016
  };
1015
1017
  });
@@ -1025,7 +1027,8 @@ async function workBuddyWebStatus(deps) {
1025
1027
  const probed = deps.probe === void 0 ? statusWithModels : {
1026
1028
  ...statusWithModels,
1027
1029
  probe: deps.probe(),
1028
- ...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey }
1030
+ ...deps.probeKey === void 0 ? {} : { probeKey: deps.probeKey },
1031
+ ...deps.useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: deps.useMaximumContextWindow() }
1029
1032
  };
1030
1033
  try {
1031
1034
  const credential = await deps.store.current();
@@ -1145,6 +1148,10 @@ function parseAction(text) {
1145
1148
  const action = wrapped["action"];
1146
1149
  if (action === "clear") return { action: "clear" };
1147
1150
  if (action === "refresh") return { action: "refresh" };
1151
+ if (action === "set-maximum-context-window") return typeof wrapped["enabled"] === "boolean" ? {
1152
+ action: "set-maximum-context-window",
1153
+ enabled: wrapped["enabled"]
1154
+ } : void 0;
1148
1155
  if (action === "probe") {
1149
1156
  const model = wrapped["model"];
1150
1157
  if (typeof model !== "string" || model.trim() === "") return void 0;
@@ -1196,6 +1203,14 @@ function workBuddyProbeHandler(deps, key) {
1196
1203
  json(res, 200, await deps.refresh());
1197
1204
  return;
1198
1205
  }
1206
+ if (action.action === "set-maximum-context-window") {
1207
+ if (deps.setMaximumContextWindow === void 0) {
1208
+ json(res, 404, { error: "context-window-setting-not-supported" });
1209
+ return;
1210
+ }
1211
+ json(res, 200, await deps.setMaximumContextWindow(action.enabled === true));
1212
+ return;
1213
+ }
1199
1214
  json(res, 200, await deps.probe(action.model));
1200
1215
  } catch (error) {
1201
1216
  json(res, 500, { error: error instanceof Error ? error.message : String(error) });
@@ -1292,10 +1307,12 @@ const AUTH_FILE_FIELD = z.string().description("WorkBuddy desktop auth file (def
1292
1307
  const AUTH_FILE_AI_FIELD = z.string().description("WorkBuddy AI desktop auth file (defaults to the app's own location)");
1293
1308
  /** Probe authorization (shared by the plugin schema and the CN section). */
1294
1309
  const PROBE_CONSENT_FIELD = z.boolean().default(false).description("Authorize reasoning-effort probes (each probe sends real requests that may consume credit)");
1310
+ const MAXIMUM_CONTEXT_WINDOW_FIELD = z.boolean().default(true).description("Use the largest context window declared by WorkBuddy AI when alternatives are available (on by default)");
1295
1311
  const Config = z.object({
1296
1312
  authFile: AUTH_FILE_FIELD,
1297
1313
  authFileAI: AUTH_FILE_AI_FIELD,
1298
- probeConsent: PROBE_CONSENT_FIELD
1314
+ probeConsent: PROBE_CONSENT_FIELD,
1315
+ useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD
1299
1316
  });
1300
1317
  /**
1301
1318
  * The CN card's settings section: only the fields that card edits.
@@ -1310,8 +1327,11 @@ const CN_SECTION = z.object({
1310
1327
  authFile: AUTH_FILE_FIELD,
1311
1328
  probeConsent: PROBE_CONSENT_FIELD
1312
1329
  });
1313
- /** The international card's settings section: only its own auth-file path. */
1314
- const AI_SECTION = z.object({ authFileAI: AUTH_FILE_AI_FIELD });
1330
+ /** The international card's settings section and its context-window preference. */
1331
+ const AI_SECTION = z.object({
1332
+ authFileAI: AUTH_FILE_AI_FIELD,
1333
+ useMaximumContextWindow: MAXIMUM_CONTEXT_WINDOW_FIELD
1334
+ });
1315
1335
  /** Stable identity key used by credentials, probe records, and catalog entries. */
1316
1336
  function credentialIdentity(credential) {
1317
1337
  return `${credential.uid}:${credential.enterpriseId ?? ""}`;
@@ -1345,6 +1365,7 @@ function createVariantRuntime(config, variant, current, identityOf) {
1345
1365
  });
1346
1366
  const fallback = fallbackFor(variant);
1347
1367
  const catalog = new WorkBuddyCatalog(fallback);
1368
+ if (variant.id !== CN_VARIANT.id) catalog.setUseMaximumContextWindow(config.useMaximumContextWindow === true);
1348
1369
  catalog.setVisible(false);
1349
1370
  const probeStore = new WorkBuddyProbeStore({
1350
1371
  pluginVersion: WORKBUDDY_CONNECT_VERSION,
@@ -1520,6 +1541,7 @@ function apply(ctx, config) {
1520
1541
  const lastIdentities = /* @__PURE__ */ new Map();
1521
1542
  const runtimes = WORKBUDDY_VARIANTS.map((variant) => createVariantRuntime(config, variant, () => current(), (id) => lastIdentities.get(id)));
1522
1543
  const probeKey = createProbeKey();
1544
+ let setMaximumContextWindow;
1523
1545
  /**
1524
1546
  * Point a variant at an account identity, invalidating whatever the previous
1525
1547
  * one left behind.
@@ -1586,7 +1608,8 @@ function apply(ctx, config) {
1586
1608
  models: () => runtime.catalog.current(),
1587
1609
  catalog: () => catalogSection(runtime),
1588
1610
  probe: () => probeSection(runtime, current().probeConsent === true),
1589
- probeKey
1611
+ probeKey,
1612
+ ...runtime.variant.id === CN_VARIANT.id ? {} : { useMaximumContextWindow: () => current().useMaximumContextWindow === true }
1590
1613
  });
1591
1614
  registerWorkBuddyProbeRoute(webCtx, {
1592
1615
  path: runtime.variant.probePath,
@@ -1627,7 +1650,14 @@ function apply(ctx, config) {
1627
1650
  state: "failed",
1628
1651
  reason: runtime.catalogError
1629
1652
  };
1630
- }
1653
+ },
1654
+ ...runtime.variant.id === CN_VARIANT.id ? {} : { setMaximumContextWindow: async (enabled) => {
1655
+ if (setMaximumContextWindow === void 0) return {
1656
+ state: "failed",
1657
+ reason: "settings are unavailable"
1658
+ };
1659
+ return setMaximumContextWindow(enabled);
1660
+ } }
1631
1661
  }, probeKey);
1632
1662
  }
1633
1663
  });
@@ -1641,10 +1671,16 @@ function apply(ctx, config) {
1641
1671
  const merged = () => ({
1642
1672
  ...sources.cn().authFile === void 0 ? {} : { authFile: sources.cn().authFile },
1643
1673
  ...sources.cn().probeConsent === void 0 ? {} : { probeConsent: sources.cn().probeConsent },
1644
- ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI }
1674
+ ...sources.ai().authFileAI === void 0 ? {} : { authFileAI: sources.ai().authFileAI },
1675
+ ...sources.ai().useMaximumContextWindow === void 0 ? {} : { useMaximumContextWindow: sources.ai().useMaximumContextWindow }
1645
1676
  });
1677
+ const applyMaximumContextWindow = (next) => {
1678
+ const runtime = runtimes.find((candidate) => candidate.variant.id !== CN_VARIANT.id);
1679
+ if (runtime?.catalog.setUseMaximumContextWindow(next.useMaximumContextWindow === true)) runtime.invalidate();
1680
+ };
1646
1681
  const repointStores = () => {
1647
1682
  const next = merged();
1683
+ applyMaximumContextWindow(next);
1648
1684
  for (const runtime of runtimes) runtime.store.setDesktopPath(configuredAuthFile(next, runtime.variant));
1649
1685
  };
1650
1686
  settingsCtx.settings.installSection(ctx, WORKBUDDY_SETTINGS_NS, CN_SECTION, config, {
@@ -1661,6 +1697,10 @@ function apply(ctx, config) {
1661
1697
  },
1662
1698
  onChange: repointStores
1663
1699
  });
1700
+ setMaximumContextWindow = async (enabled) => {
1701
+ await settingsCtx.settings.update(WORKBUDDY_AI_SETTINGS_NS, { useMaximumContextWindow: enabled });
1702
+ return { state: "updated" };
1703
+ };
1664
1704
  });
1665
1705
  ctx.effect(() => () => {
1666
1706
  stopped = true;
@@ -485,6 +485,33 @@ async function probeModel(options) {
485
485
  const CN_CHAT_BASE = "https://copilot.tencent.com";
486
486
  const CN_BILLING_BASE = "https://www.codebuddy.cn";
487
487
  const GLOBAL_BASE = "https://www.workbuddy.ai";
488
+ /**
489
+ * Display name for the single synthetic row the enterprise endpoint produces.
490
+ *
491
+ * The endpoint reports one cycle quota, not the personal endpoint's list of
492
+ * named packages, so the card's "by package" table has exactly one row.
493
+ */
494
+ const enterprisePackageName = "enterprise";
495
+ /**
496
+ * Field names and value types of a response document, for diagnostics.
497
+ *
498
+ * Names and `typeof` only. This string ends up in the status route and then in
499
+ * the browser, and the response describes the account's own usage; the values
500
+ * themselves must never travel. Only the document and its `data` member are
501
+ * described, so the output stays small.
502
+ */
503
+ function describeShape(document) {
504
+ if (typeof document !== "object" || document === null || Array.isArray(document)) return typeof document;
505
+ const record = document;
506
+ const at = (source) => {
507
+ const keys = Object.keys(source).slice(0, 24);
508
+ return keys.length === 0 ? "(empty)" : keys.map((key) => `${key}:${typeof source[key]}`).join(", ");
509
+ };
510
+ const top = `top-level { ${at(record)} }`;
511
+ const data = record["data"];
512
+ if (typeof data !== "object" || data === null || Array.isArray(data)) return top;
513
+ return `${top}; data { ${at(data)} }`;
514
+ }
488
515
  /** Shared CLI-form User-Agent for refresh and the CN catalog; chat and probe present the desktop identity (client-identity.ts). */
489
516
  const CLIENT_UA = "CLI/2.63.2 CodeBuddy/2.63.2";
490
517
  const JSON_TIMEOUT_MS = 3e4;
@@ -882,8 +909,25 @@ var WorkBuddyUpstreamClient = class {
882
909
  };
883
910
  return models;
884
911
  }
885
- /** POST the billing endpoint for the aggregated remaining credit. */
912
+ /**
913
+ * POST the billing endpoint for the aggregated remaining credit.
914
+ *
915
+ * Two upstream shapes, chosen by account type:
916
+ *
917
+ * - **CN enterprise** (`regionOf === 'cn'` and `enterpriseId` non-empty) asks
918
+ * `/v2/billing/meter/get-enterprise-user-usage`, which answers with a single
919
+ * cycle quota. The personal endpoint serves these accounts an empty
920
+ * `Accounts` list, which the card then renders as "0 credit" — a wrong
921
+ * number rather than a visible failure (issue #31).
922
+ * - **Everyone else** keeps the personal endpoint unchanged.
923
+ *
924
+ * The region gate is load-bearing: the enterprise endpoint is unverified for
925
+ * the global region, so an international credential that happens to carry an
926
+ * `enterpriseId` must stay on the measured personal path instead of being
927
+ * moved onto an unmeasured one.
928
+ */
886
929
  async fetchCredits(credential) {
930
+ if (regionOf(credential.domain) === "cn" && credential.enterpriseId !== void 0 && credential.enterpriseId !== "") return await this.fetchEnterpriseCredits(credential);
887
931
  const now = /* @__PURE__ */ new Date();
888
932
  const format = (date) => [
889
933
  date.getFullYear().toString().padStart(4, "0"),
@@ -941,6 +985,75 @@ var WorkBuddyUpstreamClient = class {
941
985
  };
942
986
  }
943
987
  /**
988
+ * CN enterprise credit read: a single cycle quota instead of a package list.
989
+ *
990
+ * Verified against the WorkBuddy desktop app (`app.asar`,
991
+ * `BackendProvider.getEnterpriseUsage` and `CloudAccountRepo.billing`): the
992
+ * body is an empty object and the account identity travels only in the
993
+ * headers. The two official call sites disagree on the field spelling
994
+ * (`limitNum`/`credit` vs `limit_num`/`used_num`), so both are accepted.
995
+ *
996
+ * A body carrying no recognisable quota field is a hard error rather than a
997
+ * zero. Rendering `0` for "we did not understand the answer" is exactly how
998
+ * issue #31 stayed invisible while users saw a plausible wrong number.
999
+ *
1000
+ * The error names fields and types only: it reaches the browser, and the
1001
+ * response body may describe the account's usage.
1002
+ */
1003
+ async fetchEnterpriseCredits(credential) {
1004
+ const response = await fetch(`${CN_BILLING_BASE}/v2/billing/meter/get-enterprise-user-usage`, {
1005
+ method: "POST",
1006
+ headers: billingHeaders(credential),
1007
+ body: JSON.stringify({}),
1008
+ signal: AbortSignal.timeout(JSON_TIMEOUT_MS)
1009
+ });
1010
+ const envelope = await readEnvelope(response);
1011
+ if (!response.ok || envelope.code !== 0) throw envelopeError(response.status, envelope);
1012
+ const sources = [];
1013
+ for (const candidate of [envelope.data, envelope.document]) {
1014
+ if (typeof candidate !== "object" || candidate === null || Array.isArray(candidate)) continue;
1015
+ const record = candidate;
1016
+ if (typeof record["data"] === "object" && record["data"] !== null && !Array.isArray(record["data"])) sources.push(record["data"]);
1017
+ sources.push(record);
1018
+ }
1019
+ const numberAt = (source, key) => typeof source[key] === "number" ? source[key] : void 0;
1020
+ let limit;
1021
+ let used;
1022
+ let resetTime;
1023
+ for (const source of sources) {
1024
+ const candidate = numberAt(source, "limitNum") ?? numberAt(source, "limit_num");
1025
+ if (candidate === void 0) continue;
1026
+ limit = candidate;
1027
+ used = numberAt(source, "credit") ?? numberAt(source, "used_num");
1028
+ if (typeof source["cycleResetTime"] === "string" && source["cycleResetTime"] !== "") resetTime = source["cycleResetTime"];
1029
+ break;
1030
+ }
1031
+ if (limit === void 0) throw new Error(`workbuddy enterprise billing response carried no recognised quota field (expected limitNum/limit_num + credit/used_num; received ${describeShape(envelope.document)})`);
1032
+ if (limit === -1) return {
1033
+ total: 0,
1034
+ accounts: [{
1035
+ packageName: enterprisePackageName,
1036
+ remain: 0,
1037
+ size: 0,
1038
+ unlimited: true
1039
+ }],
1040
+ unlimited: true,
1041
+ ...resetTime === void 0 ? {} : { cycleResetTime: resetTime }
1042
+ };
1043
+ if (used === void 0) throw new Error(`workbuddy enterprise billing response carried a quota limit but no recognised usage field (expected credit/used_num alongside limitNum/limit_num; received ${describeShape(envelope.document)})`);
1044
+ let remain = limit - used;
1045
+ if (remain < 0) remain = 0;
1046
+ return {
1047
+ total: remain,
1048
+ accounts: [{
1049
+ packageName: enterprisePackageName,
1050
+ remain,
1051
+ size: limit
1052
+ }],
1053
+ ...resetTime === void 0 ? {} : { cycleResetTime: resetTime }
1054
+ };
1055
+ }
1056
+ /**
944
1057
  * One probe request: a real streaming chat call carrying the effort under
945
1058
  * test.
946
1059
  *
@@ -1092,6 +1205,7 @@ function parseModelCatalog(data, international = false) {
1092
1205
  name: typeof wrapped["name"] === "string" && wrapped["name"] !== "" ? wrapped["name"] : id,
1093
1206
  contextWindow: international && isObject(wrapped["contextWindow"]) && positive(wrapped["contextWindow"]["defaultLength"]) ? wrapped["contextWindow"]["defaultLength"] : input,
1094
1207
  ...international ? {
1208
+ ...isObject(wrapped["contextWindow"]) && positive(wrapped["contextWindow"]["defaultLength"]) ? { defaultContextWindow: wrapped["contextWindow"]["defaultLength"] } : {},
1095
1209
  maxInputTokens: input,
1096
1210
  supportedContextWindows: isObject(wrapped["contextWindow"]) && Array.isArray(wrapped["contextWindow"]["supportedLengths"]) ? wrapped["contextWindow"]["supportedLengths"].filter(positive) : [],
1097
1211
  promotions: parsePromotions(data["modelPromotions"], id)
@@ -1636,7 +1750,7 @@ const FALLBACK_WORKBUDDY_MODELS = [
1636
1750
  },
1637
1751
  {
1638
1752
  id: "hy3-x",
1639
- name: "Hy3",
1753
+ name: "Hy3-X",
1640
1754
  contextWindow: 192e3,
1641
1755
  maxTokens: 64e3,
1642
1756
  supportsImages: true,
@@ -1971,6 +2085,8 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
1971
2085
  id: "hy4-preview-f",
1972
2086
  name: "Hy4 preview",
1973
2087
  contextWindow: 3e5,
2088
+ defaultContextWindow: 3e5,
2089
+ supportedContextWindows: [3e5, 1e6],
1974
2090
  maxTokens: 64e3,
1975
2091
  supportsImages: true,
1976
2092
  reasoning: {
@@ -2007,6 +2123,8 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
2007
2123
  id: "deepseek-v4.1-flash",
2008
2124
  name: "Deepseek-V4.1-Flash",
2009
2125
  contextWindow: 3e5,
2126
+ defaultContextWindow: 3e5,
2127
+ supportedContextWindows: [3e5, 1e6],
2010
2128
  maxTokens: 128e3,
2011
2129
  supportsImages: true,
2012
2130
  reasoning: {
@@ -2024,6 +2142,8 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
2024
2142
  id: "gpt-6-astra",
2025
2143
  name: "GPT-6-Astra",
2026
2144
  contextWindow: 4e5,
2145
+ defaultContextWindow: 4e5,
2146
+ supportedContextWindows: [4e5, 1e6],
2027
2147
  maxTokens: 128e3,
2028
2148
  supportsImages: true,
2029
2149
  reasoning: {
@@ -2287,13 +2407,22 @@ const FALLBACK_WORKBUDDY_AI_MODELS = [
2287
2407
  var WorkBuddyCatalog = class {
2288
2408
  models;
2289
2409
  visible = true;
2410
+ useMaximumContextWindow = false;
2290
2411
  constructor(initial = FALLBACK_WORKBUDDY_MODELS) {
2291
2412
  this.models = initial;
2292
2413
  }
2293
2414
  /** Current entries; empty while the variant has no usable credential. */
2294
2415
  current() {
2295
2416
  if (!this.visible) return [];
2296
- return this.models.map((model) => modelWithCurrentPromotion(model));
2417
+ return this.models.map((model) => {
2418
+ const current = modelWithCurrentPromotion(model);
2419
+ const maximum = current.supportedContextWindows === void 0 ? void 0 : Math.max(...current.supportedContextWindows);
2420
+ return this.useMaximumContextWindow && maximum !== void 0 && maximum > current.contextWindow ? {
2421
+ ...current,
2422
+ defaultContextWindow: current.defaultContextWindow ?? current.contextWindow,
2423
+ contextWindow: maximum
2424
+ } : current;
2425
+ });
2297
2426
  }
2298
2427
  /** Replace the list; callers invalidate their adapter snapshot after this. */
2299
2428
  set(models) {
@@ -2312,6 +2441,12 @@ var WorkBuddyCatalog = class {
2312
2441
  this.visible = visible;
2313
2442
  return true;
2314
2443
  }
2444
+ /** Select the largest declared international window where the upstream offers one. */
2445
+ setUseMaximumContextWindow(useMaximum) {
2446
+ if (this.useMaximumContextWindow === useMaximum) return false;
2447
+ this.useMaximumContextWindow = useMaximum;
2448
+ return true;
2449
+ }
2315
2450
  /** Models to fall back to when the upstream fetch fails; ignores visibility. */
2316
2451
  fallback() {
2317
2452
  return this.models;
@@ -2344,7 +2479,7 @@ const WORKBUDDY_AI_STATUS_PATH = "/plugins/dsh-workbuddy-connect/ai/status";
2344
2479
  const WORKBUDDY_AI_PROBE_PATH = "/plugins/dsh-workbuddy-connect/ai/probe";
2345
2480
  //#endregion
2346
2481
  //#region src/version.ts
2347
- const WORKBUDDY_CONNECT_VERSION = "0.5.1";
2482
+ const WORKBUDDY_CONNECT_VERSION = "0.5.3";
2348
2483
  //#endregion
2349
2484
  //#region src/host-heartbeat.ts
2350
2485
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dsh-workbuddy-connect",
3
3
  "displayName": "DSH WorkBuddy Connect",
4
4
  "description": "将 WorkBuddy 桌面 App 包含的模型自动接入 DeepSeek Harness — bring WorkBuddy desktop app models into DeepSeek Harness with zero configuration.",
5
- "version": "0.5.1",
5
+ "version": "0.5.3",
6
6
  "author": "corrinehu",
7
7
  "keywords": [
8
8
  "dsh-plugin",