dsh-packycode-api 1.2.3 → 1.2.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.4
4
+
5
+ - Added the current image key's remaining quota immediately after the Create image shortcut. Hovering or focusing it shows the key label, account name, remaining/used/total quota and update time. It shares the shortcut's visibility, refreshes every minute while mounted and after image-setting/key changes, and marks stale or failed reads without confusing them with zero credit. Real React lifecycle regressions cover provider switches, image availability, disabled/missing keys and late responses.
6
+ - Removed the redundant "Separate image key" badge from the image settings header.
7
+ - Image/account quota totals now use total remaining plus total used, as requested, rather than depending on the optional periodic `quota_limit` field. Non-resetting image keys therefore show their total even when no period cap is set. Per-key quota figures and progress bars use the same calculation. Missing balance components remain unknown, and unlimited keys retain their unlimited display.
8
+ - Validation, 2026-10-15: 397 tests, both rebuilt bundles and the versioned tarball. Browser verification with real React and DSH 0.2.0-rc.2 composer CSS confirmed the balance sits to the right of Create image, the hover/focus details show the current key's quota, and provider switches, the image setting and the host's hidden toolbar hide both controls together. A read-only live image-key quota projection also confirmed the no-period-cap shape; no image generation was needed for these UI changes.
9
+
3
10
  ## 1.2.3
4
11
 
5
12
  - Fixed ordinary reference-only image edits failing with `PackyCode image mask must be a PNG smaller than 4 MB` when a model also fills the optional mask field with the same JPEG/WebP reference. Native `maskImage` and CLI `maskId` now explicitly accept `null`; the field descriptions distinguish sketch refinement from using an actual mask. Only an identical reference already resolved by the host as non-PNG is treated as a redundant non-mask. Reference bytes, edit endpoint, model and quality stay intact; real PNG masks and all distinct mask references still undergo format, size and dimension checks. Invalid mask-format errors now explain how to request editing without a mask. Added native and authenticated MCP regressions without paid calls.
package/README.en.md CHANGED
@@ -51,7 +51,7 @@ Open the desktop application's **Plugins** page, click **Add plugin**, and enter
51
51
  dsh-packycode-api
52
52
  ```
53
53
 
54
- Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.2.3`.
54
+ Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.2.4`.
55
55
 
56
56
  A **first install requires no DSH restart**: open **Settings → PackyCode** and add a key immediately. When updating an already loaded older version, follow the restart guidance in the **Plugin version** card.
57
57
 
@@ -70,7 +70,7 @@ Choose a model from the `PackyCode` group. `deepseek-flash`, `gpt-6.1-sol`, `gpt
70
70
 
71
71
  The **Plugins** page shows the installed state and version, and is also where you enable, disable, or uninstall.
72
72
 
73
- **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.3`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
73
+ **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.4`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
74
74
 
75
75
  For a local checkout, keep the `link:` prefix: `dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`. **Do not pass a bare path or `file:`** — that install copies the whole checkout, `node_modules` included, into the profile, where it then shadows the packages the DSH installation itself provides and the plugin looks uninstalled; see the FAQ. If you already installed that way, uninstall first and reinstall as above.
76
76
 
package/README.md CHANGED
@@ -51,7 +51,7 @@
51
51
  dsh-packycode-api
52
52
  ```
53
53
 
54
- 在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.2.3`。
54
+ 在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.2.4`。
55
55
 
56
56
  首次安装后**无需重启 DSH**,可直接打开**设置 → PackyCode** 添加 Key。更新已加载的旧版时,按「插件版本」卡片的重启提示操作。
57
57
 
@@ -70,7 +70,7 @@ dsh-packycode-api
70
70
 
71
71
  桌面应用的**插件**页可以看到已安装状态与版本,也可以在那里启用、停用或卸载。
72
72
 
73
- **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.3`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
73
+ **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.4`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
74
74
 
75
75
  本地开发要加 `link:` 前缀:`dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`。**不要直接写路径(或 `file:`)** —— 那样的安装会把整个仓库目录(连同 `node_modules`)复制进 profile,之后它会在 profile 内部挡住 DSH 自带的包,插件看起来像没装,见「常见问题」。已经那样装过的话,先卸载再按上面的方式重装。
76
76
 
package/lib/client.js CHANGED
@@ -40,9 +40,11 @@ window.__ModuleLoader__.load({
40
40
  intro: "填入 PackyCode(PackyAPI)的 API Key 即可使用内置模型路由。多个 Key 按顺序使用:额度用尽或失效时自动切到下一个;某个 Key 的分组用不了所选模型时,自动改用分组合适的 Key。",
41
41
  accountsTitle: "账户与 Key",
42
42
  imagesTitle: "图片生成与编辑",
43
- imagesSeparateKey: "独立图片 Key",
44
43
  imagesBalanceLoading: "正在读取图片 Key 余额…",
45
44
  imagesBalanceFailed: "图片余额读取失败",
45
+ imagesComposerLabel: "图片剩余额度",
46
+ imagesBalanceKey: "当前图片 Key",
47
+ imagesBalanceReadAt: "更新于 {time}",
46
48
  imagesBalanceHint: "仅汇总图片 Key 的额度,每分钟自动更新;也可手动刷新。与对话 Key 的余额分开统计。",
47
49
  imagesIntro: "使用独立图片 Key 调用 PackyCode 图片接口。生成新图或上传图片后描述修改要求;图片模型独立于对话模型。原始 PNG 保存在本机,聊天中显示图片预览。",
48
50
  imagesEnabled: "图片生成与编辑",
@@ -215,9 +217,11 @@ window.__ModuleLoader__.load({
215
217
  intro: "Paste a PackyCode (PackyAPI) API key to use the built-in model route. Keys are used in order: an exhausted or rejected key hands over to the next one, and a key whose groups cannot reach the selected model is passed over for one whose groups can.",
216
218
  accountsTitle: "Accounts and keys",
217
219
  imagesTitle: "Image generation and editing",
218
- imagesSeparateKey: "Separate image key",
219
220
  imagesBalanceLoading: "Reading image key balances…",
220
221
  imagesBalanceFailed: "Could not read image balances",
222
+ imagesComposerLabel: "Image balance",
223
+ imagesBalanceKey: "Current image key",
224
+ imagesBalanceReadAt: "Updated at {time}",
221
225
  imagesBalanceHint: "Only image key quotas are counted here. Updates every minute, or use Refresh. Conversation key balances are separate.",
222
226
  imagesIntro: "Use a dedicated image key with PackyCode’s Images API. Describe a new image or attach images with editing instructions. The image engine is independent of the conversation model. Original PNGs are saved locally and previews appear in chat.",
223
227
  imagesEnabled: "Generate and edit images",
@@ -409,6 +413,11 @@ window.__ModuleLoader__.load({
409
413
  .pkc-input:disabled{opacity:.5}
410
414
  .pkc-image-choice{flex:none;width:100%;max-width:300px;cursor:pointer}
411
415
  .pkc-image-shortcut{display:inline-flex;align-items:center;gap:8px}
416
+ .pkc-image-balance{position:relative;display:inline-flex;align-items:center}
417
+ .pkc-image-balance-value{cursor:help;border-radius:var(--dsw-radius-xs,4px)}
418
+ .pkc-image-balance-value:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
419
+ .pkc-image-balance-tooltip{position:absolute;bottom:calc(100% + 8px);left:0;z-index:100;box-sizing:border-box;width:max-content;max-width:min(320px,calc(100vw - 32px));padding:10px 12px;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-shadow-lv1);font-size:12px;line-height:20px;white-space:pre-line;overflow-wrap:anywhere;visibility:hidden;opacity:0;pointer-events:none}
420
+ .pkc-image-balance:hover>.pkc-image-balance-tooltip,.pkc-image-balance:focus-within>.pkc-image-balance-tooltip{visibility:visible;opacity:1}
412
421
  .pkc-image-result,.pkc-image-chat{display:flex;flex-direction:column;gap:10px;padding:8px 0}
413
422
  .pkc-generated-image{display:grid;place-items:center;align-self:flex-start;max-width:100%;width:320px;min-height:80px;padding:0;overflow:hidden;border:.5px solid var(--dsw-alias-border-l3);border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-secondary);cursor:zoom-in}
414
423
  .pkc-generated-image img{display:block;max-width:100%;max-height:480px;object-fit:contain}
@@ -576,11 +585,19 @@ window.__ModuleLoader__.load({
576
585
  * @module dsh-packycode-api/totals
577
586
  */
578
587
  const amount = (value) => typeof value === "number" && Number.isFinite(value) ? value : void 0;
588
+ /** Displayed key quota is remaining plus used, independent of a period cap. */
589
+ function quotaTotalUsd(usage) {
590
+ if (usage?.unlimitedQuota === true) return void 0;
591
+ const remaining = amount(usage?.availableUsd);
592
+ const used = amount(usage?.usedUsd);
593
+ if (remaining === void 0 || used === void 0) return void 0;
594
+ return amount(Number((remaining + used).toFixed(6)));
595
+ }
579
596
  /**
580
597
  * Add up the balance rows the host returned.
581
598
  * @param rows - the `keys` array of a balance projection.
582
599
  * @returns summed amounts plus the conditions that make a sum incomplete:
583
- * keys whose read failed, and keys with no quota limit at all.
600
+ * keys whose read failed, and keys with unlimited quota.
584
601
  */
585
602
  function sumBalances(rows) {
586
603
  const total = {
@@ -606,11 +623,10 @@ window.__ModuleLoader__.load({
606
623
  }
607
624
  const remaining = amount(usage.availableUsd);
608
625
  const used = amount(usage.usedUsd);
609
- const limit = amount(usage.limitUsd);
610
626
  if (remaining !== void 0) total.remainingUsd += remaining;
611
627
  if (used !== void 0) total.usedUsd += used;
612
- if (limit !== void 0) total.limitUsd += limit;
613
628
  }
629
+ total.limitUsd = Number((total.remainingUsd + total.usedUsd).toFixed(6));
614
630
  return total;
615
631
  }
616
632
  //#endregion
@@ -647,13 +663,14 @@ window.__ModuleLoader__.load({
647
663
  /** The same quota presentation for conversation and image credential records. */
648
664
  function KeyBalance({ row, t }) {
649
665
  const usage = row?.usage;
666
+ const totalUsd = quotaTotalUsd(usage);
650
667
  const figures = [
651
668
  usage?.unlimitedQuota === true ? [t("keyRemaining"), t("keyUnlimited")] : usage?.availableUsd === void 0 ? void 0 : [t("keyRemaining"), formatUsd(usage.availableUsd)],
652
669
  usage?.usedUsd === void 0 ? void 0 : [t("keyUsed"), formatUsd(usage.usedUsd)],
653
- usage?.limitUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(usage.limitUsd)]
670
+ totalUsd === void 0 ? void 0 : [t("accountLimit"), formatUsd(totalUsd)]
654
671
  ].filter(Boolean);
655
672
  const meta = [usage?.name === void 0 ? t("keyNameUnknown") : `${t("keyName")} ${usage.name}`, RESET_KEYS[usage?.resetPeriod] === void 0 ? void 0 : t(RESET_KEYS[usage.resetPeriod])].filter(Boolean).join(" · ");
656
- const share = usage?.unlimitedQuota !== true && typeof usage?.limitUsd === "number" && usage.limitUsd > 0 && typeof usage.availableUsd === "number" ? Math.max(0, Math.min(1, usage.availableUsd / usage.limitUsd)) : void 0;
673
+ const share = usage?.unlimitedQuota !== true && totalUsd > 0 && typeof usage.availableUsd === "number" ? Math.max(0, Math.min(1, usage.availableUsd / totalUsd)) : void 0;
657
674
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
658
675
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
659
676
  className: "pkc-meta",
@@ -716,7 +733,7 @@ window.__ModuleLoader__.load({
716
733
  children: t("totalLimit")
717
734
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
718
735
  className: "pkc-stat-value",
719
- children: totals.unlimited > 0 ? t("keyUnlimited") : known("limitUsd") ? formatUsd(totals.limitUsd) : "—"
736
+ children: totals.unlimited > 0 ? t("keyUnlimited") : known("availableUsd") && known("usedUsd") ? formatUsd(totals.limitUsd) : "—"
720
737
  })]
721
738
  })
722
739
  ]
@@ -726,6 +743,103 @@ window.__ModuleLoader__.load({
726
743
  })] });
727
744
  }
728
745
  //#endregion
746
+ //#region src/client-image-balance.jsx
747
+ /** Mirror the host's next-usable image key using only its public balance rows. */
748
+ function currentImageKey(balance) {
749
+ const keys = balance?.keys ?? [];
750
+ const start = Math.max(0, keys.findIndex((row) => row.id === balance.activeId));
751
+ for (let offset = 0; offset < keys.length; offset += 1) {
752
+ const row = keys[(start + offset) % keys.length];
753
+ if (row.disabled !== true) return row;
754
+ }
755
+ }
756
+ /** Mounted inside the visible Create image shortcut, so its timer shares that lifetime. */
757
+ function ImageShortcutBalance({ call, t }) {
758
+ const [view, setView] = react.useState({ loading: true });
759
+ const refresh = react.useRef(() => {});
760
+ const tooltipId = react.useId();
761
+ react.useEffect(() => {
762
+ let live = true, generation = 0, pending = false;
763
+ const read = async (force = false, reset = false) => {
764
+ if (!live || pending && !force) return;
765
+ const current = ++generation;
766
+ pending = true;
767
+ setView((previous) => reset ? { loading: true } : {
768
+ ...previous,
769
+ loading: true
770
+ });
771
+ try {
772
+ const balance = await call("images/balance", { force });
773
+ if (live && current === generation) setView({
774
+ balance,
775
+ loading: false
776
+ });
777
+ } catch {
778
+ if (live && current === generation) setView((previous) => ({
779
+ ...previous,
780
+ loading: false,
781
+ failed: true
782
+ }));
783
+ } finally {
784
+ if (current === generation) pending = false;
785
+ }
786
+ };
787
+ refresh.current = read;
788
+ read();
789
+ const changed = () => {
790
+ read(true, true);
791
+ };
792
+ const timer = window.setInterval(() => {
793
+ read();
794
+ }, 6e4);
795
+ window.addEventListener("packycode-images-updated", changed);
796
+ return () => {
797
+ live = false;
798
+ generation += 1;
799
+ refresh.current = () => {};
800
+ window.clearInterval(timer);
801
+ window.removeEventListener("packycode-images-updated", changed);
802
+ };
803
+ }, [call]);
804
+ const row = currentImageKey(view.balance);
805
+ const usage = row?.usage;
806
+ const unlimited = usage?.unlimitedQuota === true;
807
+ const remaining = unlimited ? t("keyUnlimited") : formatUsd(usage?.availableUsd);
808
+ const stale = usage !== void 0 && (view.failed || row.stale !== void 0);
809
+ const display = remaining ?? (view.loading ? "…" : "—");
810
+ const details = [
811
+ t("imagesComposerLabel"),
812
+ row?.label ? `${t("imagesBalanceKey")}: ${row.label}` : void 0,
813
+ usage?.name ? `${t("keyName")}: ${usage.name}` : void 0,
814
+ `${t("keyRemaining")}: ${remaining ?? "—"}`,
815
+ `${t("keyUsed")}: ${formatUsd(usage?.usedUsd) ?? "—"}`,
816
+ `${t("accountLimit")}: ${unlimited ? t("keyUnlimited") : formatUsd(quotaTotalUsd(usage)) ?? "—"}`,
817
+ stale ? fill(t("composerStale"), { time: readTime(row.stale?.fetchedAt ?? view.balance?.fetchedAt) }) : view.failed || row?.error ? t("imagesBalanceFailed") : view.loading ? t("imagesBalanceLoading") : view.balance?.fetchedAt === void 0 ? void 0 : fill(t("imagesBalanceReadAt"), { time: readTime(view.balance.fetchedAt) })
818
+ ].filter(Boolean).join("\n");
819
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
820
+ className: "pkc-image-balance",
821
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
822
+ className: "pkc-composer pkc-image-balance-value",
823
+ role: "status",
824
+ tabIndex: 0,
825
+ "aria-label": `${t("imagesComposerLabel")}: ${stale ? "≈" : ""}${display}`,
826
+ "aria-describedby": tooltipId,
827
+ onMouseEnter: () => {
828
+ refresh.current();
829
+ },
830
+ onFocus: () => {
831
+ refresh.current();
832
+ },
833
+ children: stale ? `≈${display}` : display
834
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
835
+ className: "pkc-image-balance-tooltip",
836
+ id: tooltipId,
837
+ role: "tooltip",
838
+ children: details
839
+ })]
840
+ });
841
+ }
842
+ //#endregion
729
843
  //#region src/client-images.jsx
730
844
  const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
731
845
  /** Only projections come back from the host; the password draft is cleared on success. */
@@ -804,13 +918,9 @@ window.__ModuleLoader__.load({
804
918
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
805
919
  className: "pkc-title",
806
920
  children: t("imagesTitle")
807
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
921
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
808
922
  className: "pkc-actions",
809
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
810
- className: "pkc-chip",
811
- "data-tone": "neutral",
812
- children: t("imagesSeparateKey")
813
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
923
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
814
924
  className: "pkc-btn",
815
925
  type: "button",
816
926
  disabled: busy || balanceLoading,
@@ -818,7 +928,7 @@ window.__ModuleLoader__.load({
818
928
  readBalance(true);
819
929
  },
820
930
  children: balanceLoading ? t("refreshing") : t("refresh")
821
- })]
931
+ })
822
932
  })]
823
933
  }),
824
934
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1028,11 +1138,14 @@ window.__ModuleLoader__.load({
1028
1138
  const [error, setError] = react.useState();
1029
1139
  react.useEffect(() => {
1030
1140
  let live = true;
1141
+ let generation = 0;
1031
1142
  const refresh = () => {
1032
- if (active) call("images/status").then((value) => {
1033
- if (live) setReady(value.imageEnabled && value.available && value.keys.some((key) => !key.disabled));
1143
+ if (!active) return;
1144
+ const current = ++generation;
1145
+ call("images/status").then((value) => {
1146
+ if (live && current === generation) setReady(value.imageEnabled && value.available && value.keys.some((key) => !key.disabled));
1034
1147
  }).catch(() => {
1035
- if (live) setReady(false);
1148
+ if (live && current === generation) setReady(false);
1036
1149
  });
1037
1150
  };
1038
1151
  setReady(false);
@@ -1046,24 +1159,31 @@ window.__ModuleLoader__.load({
1046
1159
  if (!active || !ready) return null;
1047
1160
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1048
1161
  className: "pkc-image-shortcut",
1049
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1050
- className: "pkc-btn",
1051
- type: "button",
1052
- title: t("imagesShortcutHint"),
1053
- onClick: () => {
1054
- try {
1055
- prepare(t("imagesDraft"));
1056
- setError(void 0);
1057
- } catch {
1058
- setError(t("imagesDraftBusy"));
1059
- }
1060
- },
1061
- children: t("imagesCreate")
1062
- }), error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1063
- className: "pkc-error",
1064
- role: "alert",
1065
- children: error
1066
- })]
1162
+ children: [
1163
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1164
+ className: "pkc-btn",
1165
+ type: "button",
1166
+ title: t("imagesShortcutHint"),
1167
+ onClick: () => {
1168
+ try {
1169
+ prepare(t("imagesDraft"));
1170
+ setError(void 0);
1171
+ } catch {
1172
+ setError(t("imagesDraftBusy"));
1173
+ }
1174
+ },
1175
+ children: t("imagesCreate")
1176
+ }),
1177
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ImageShortcutBalance, {
1178
+ call,
1179
+ t
1180
+ }),
1181
+ error && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1182
+ className: "pkc-error",
1183
+ role: "alert",
1184
+ children: error
1185
+ })
1186
+ ]
1067
1187
  });
1068
1188
  }
1069
1189
  function GeneratedImage({ attachment, loadImage, openImage, t }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-packycode-api",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "Use PackyCode API keys in DeepSeek Harness with multi-protocol models, image generation and editing, separate key rings, and balance display",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -100,6 +100,7 @@
100
100
  "packycode-host-v020-fixture": "file:tests/fixtures/host-v020",
101
101
  "react": "18.3.1",
102
102
  "react-dom": "18.3.1",
103
+ "react-test-renderer": "18.3.1",
103
104
  "tsdown": "0.22.2",
104
105
  "yaml": "2.9.1"
105
106
  },