dsh-packycode-api 1.2.6 → 1.2.7

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.7
4
+
5
+ - **Image keys can be renamed.** Settings → PackyCode → Image generation and editing gains the same **Rename** action as conversation keys, through a new `images/keys/rename` endpoint on the image key ring (`packycode-api/image-keys`). Both cards share one rename form: the name becomes a text field, Enter or **Save** stores it, Escape or **Cancel** keeps the old one, and only the key id and the new label reach the host.
6
+ - **One quota hover for both key rings.** The PackyCode quota beside the model picker now uses the same component as the image-key quota: the help cursor marks it, and hovering or focusing it shows the same lines — which quota it is, the current key, its account, remaining, used and total quota, and when it was read or that the last read failed. The composer figure's details open leftward so they stay inside the composer.
7
+ - **Registration invite in Settings.** While no conversation key is stored, **Accounts and keys** shows PackyCode's registration invite link (`https://www.packyapi.ai/register?aff=Im3l`) under the empty-state note. It is a new-window link, which DSH desktop hands to the system browser (a web profile opens a tab). It appears only after the host has answered the status read and disappears as soon as any key is stored.
8
+ - **README: install with an exact version.** The Plugins page's install prompt shows the newest npm version, but DSH's bundled pnpm 11 resolves a bare `dsh-packycode-api` to the newest version published at least 24 hours ago (`minimumReleaseAge`). During the 1.1.x/1.2.x release burst a first install therefore landed on 1.1.1 or 1.1.5, whose settings channel needed a DSH restart and answered every call with HTTP 405 until then. The install guide now always enters `dsh-packycode-api@<version>` (pnpm then adds it to `minimumReleaseAgeExclude` itself), the FAQ explains the 405 symptom with a version check and the recovery, and the README links the registration invite for readers without a key.
9
+ - Validation, 2026-10-15: 422 offline tests (6 new for key rename and the shared quota hover, 4 for the invite), both rebuilt bundles and the versioned tarball. The first-install 405 was reproduced against the DSH 0.2.0-rc.2 runtime in an isolated `DSH_HOME`: a Plugins-page install of the bare name prompted 1.2.6, installed 1.1.5 and answered HTTP 405 until restart, while `dsh-packycode-api@1.2.6` installed 1.2.6 and answered at once. A browser replay of the built settings page showed the invite in light and dark themes and no invite with a stored key. No DSH restart, paid request or live gateway call.
10
+
3
11
  ## 1.2.6
4
12
 
5
13
  - Clicking black space around a large image now closes the preview, including the canvas inside the full-size dialog. The image hit area fits its actual displayed dimensions instead of covering letterboxing; image clicks, download controls and error notices keep the viewer open.
package/README.en.md CHANGED
@@ -41,23 +41,25 @@ The `PackyCode` model group ships with the plugin. No YAML edits and no separate
41
41
 
42
42
  ## Install
43
43
 
44
- You need **DeepSeek Harness** and a **PackyCode (PackyAPI) API key**. For DSH setup, see the [official instructions](https://github.com/deepseek-ai/deepseek-harness#run).
44
+ You need **DeepSeek Harness** and a **PackyCode (PackyAPI) API key**. For DSH setup, see the [official instructions](https://github.com/deepseek-ai/deepseek-harness#run). No API key yet? You can register with PackyCode and apply for one through [this invite link](https://www.packyapi.ai/register?aff=Im3l).
45
45
 
46
46
  ### 1. Install the plugin
47
47
 
48
- Open the desktop application's **Plugins** page, click **Add plugin**, and enter this as the **package name or address**:
48
+ Open the desktop application's **Plugins** page, click **Add plugin**, and enter the package name **with a version** as the **package name or address**:
49
49
 
50
50
  ```text
51
- dsh-packycode-api
51
+ dsh-packycode-api@1.2.7
52
52
  ```
53
53
 
54
- Choose the **official** install source, then click **Install**. To pin a version, enter `dsh-packycode-api@1.2.6`.
54
+ Choose the **official** install source, then click **Install**. `1.2.7` was the latest release when this was written; the [npm page](https://www.npmjs.com/package/dsh-packycode-api) shows the current one.
55
55
 
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.
56
+ > **Always include the version.** With a bare `dsh-packycode-api`, the install prompt shows the newest version on npm, but DSH's bundled pnpm by default installs only versions published at least 24 hours ago, so you can end up several releases behind with no sign of it in the prompt. With an exact version, pnpm installs that version and adds it to the profile's `minimumReleaseAgeExclude` itself. Afterwards, check **Current version** under **Settings → PackyCode → Plugin version**.
57
+
58
+ A **first install requires no DSH restart**: open **Settings → PackyCode** and add a key immediately. This holds from 1.1.7 on; earlier versions need one restart after a first install, see the FAQ. When updating an already loaded older version, follow the restart guidance in the **Plugin version** card.
57
59
 
58
60
  ### 2. Add a key
59
61
 
60
- Open **Settings → PackyCode**. Paste the API key under **Add a key**. A label is optional and only distinguishes keys. Click **Add**.
62
+ Open **Settings → PackyCode**. Paste the API key under **Add a key**. A label is optional and only distinguishes keys. Click **Add**. Until a key has been added, **Accounts and keys** shows the same registration invite link, which opens in the system browser; it disappears once a key is stored.
61
63
 
62
64
  > Do not paste a key into a public issue, chat, or screenshot. The plugin does not read `PACKY_CODE_API_KEY` from the environment, and it does not silently switch to another provider when a request fails.
63
65
 
@@ -70,7 +72,7 @@ Choose a model from the `PackyCode` group. `deepseek-flash`, `gpt-6.1-sol`, `gpt
70
72
 
71
73
  The **Plugins** page shows the installed state and version, and is also where you enable, disable, or uninstall.
72
74
 
73
- **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.6`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
75
+ **For non-desktop profiles only:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.7`. Do not use it for `desktop` — the CLI refuses it (`profile "desktop" is managed exclusively by the Electron application`).
74
76
 
75
77
  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
78
 
@@ -211,7 +213,7 @@ Only GPT Responses requests keep `prompt_cache_key`; other Responses models omit
211
213
 
212
214
  ## Update and uninstall
213
215
 
214
- **Update:** open **Settings → PackyCode** and, when a newer version is listed, click **Update plugin**. It installs that exact version into the owning profile with DSH's bundled pnpm and reads `package.json` back before reporting success. A local `link:` checkout does not get the button. Then quit and reopen the desktop application. A page refresh does not reload the Host. Reinstalling `dsh-packycode-api` from the **Plugins** page does the same job.
216
+ **Update:** open **Settings → PackyCode** and, when a newer version is listed, click **Update plugin**. It installs that exact version into the owning profile with DSH's bundled pnpm and reads `package.json` back before reporting success. A local `link:` checkout does not get the button. Then quit and reopen the desktop application. A page refresh does not reload the Host. Reinstalling from the **Plugins** page works too, again with the version in the package name (for example `dsh-packycode-api@1.2.7`); without it you may get a version that is merely the newest one published at least 24 hours ago.
215
217
 
216
218
  **Coming back from a local-path install, uninstall first.** An update is a `pnpm add`: it rewrites the files inside the package directory and leaves a `node_modules` that an earlier local-path install left there. That older tree keeps shadowing the packages the DSH installation provides, so the plugin row fails to load with no visible reason. **Uninstall** runs `pnpm remove`, which deletes the package directory outright — only a reinstall after that is clean. `node scripts/install-check.mjs` reports whether a profile is in that state.
217
219
 
@@ -227,7 +229,15 @@ ls ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/node_modules
227
229
 
228
230
  Output means a leftover tree: a registry install ships no `node_modules`, so this one usually comes from an earlier local-path install (`file:`, or a bare path instead of `link:`). It sits inside the profile directory, and DSH resolves a plugin's imports from inside the profile first, so the plugin gets those old copies — for example `@deepseek-ai/schemastery` 3.18.2, which has no `Schema.volatile()`, so the module throws while it is imported. When an import fails the Loader creates no fiber for that row and no client bundle is composed either, so the model route, the RPC channel, and the settings panel all stay absent.
229
231
 
230
- Recovery: **uninstall** the plugin on the **Plugins** page, **install** `dsh-packycode-api` again, then quit and reopen the desktop application. Uninstall runs `pnpm remove` and deletes the whole package directory; reinstalling or clicking **Update** only rewrites files and keeps the leftover tree. Deleting that `node_modules` by hand and restarting works too.
232
+ Recovery: **uninstall** the plugin on the **Plugins** page, **install** it again with the version in the package name (for example `dsh-packycode-api@1.2.7`), then quit and reopen the desktop application. Uninstall runs `pnpm remove` and deletes the whole package directory; reinstalling or clicking **Update** only rewrites files and keeps the leftover tree. Deleting that `node_modules` by hand and restarting works too.
233
+
234
+ **Right after a first install, every card in Settings shows `transport failure for /packycode-api/…: HTTP 405`?** The installed version is 1.1.6 or earlier, usually because only the bare package name was entered: the install prompt shows the newest version, but pnpm installs the newest one published at least 24 hours ago (see **Install the plugin**). Those older versions can register their settings channel only after DSH restarts and rebuilds its `connection` service. So right after a first install `/packycode-api` does not exist yet, requests fall through to the static page, and it answers 405; the model route and the settings panel itself load normally. From 1.1.7 on, a first install needs no restart. Check which version was installed:
235
+
236
+ ```sh
237
+ grep '"version"' ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/package.json
238
+ ```
239
+
240
+ Recovery: quit and reopen the desktop application, and the old version works. Then click **Check for updates** → **Update plugin** under **Settings → PackyCode → Plugin version** (it installs the exact latest version and is not subject to the 24-hour rule), and restart once more.
231
241
 
232
242
  **Web search says "no stored API key's group covers …"?** The search backend's model is not in your token groups (or the gateway currently routes it to another supplier). Pick the other backend under **Settings → PackyCode → Web search**, or add a key whose group covers it.
233
243
 
@@ -243,7 +253,7 @@ Recovery: **uninstall** the plugin on the **Plugins** page, **install** `dsh-pac
243
253
 
244
254
  **A rate limit disabled the key?** It should not. HTTP 429 does not rotate. Exhausted quota or HTTP 401 does; after a top-up, click **Re-enable**.
245
255
 
246
- **Installing from the Plugins page fails with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`?** pnpm refuses packages published less than 24 hours ago and checks the whole profile lockfile. The package it names is often another plugin that was just updated, not this one. Retry once it is 24 hours old, or add the reported `name@version` under `minimumReleaseAgeExclude` in the profile's `pnpm-workspace.yaml`. Once this plugin is installed, **Update plugin** in Settings is not subject to that check.
256
+ **Installing from the Plugins page fails with `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`?** By default pnpm accepts only versions published at least 24 hours ago and checks the whole profile lockfile. (Entering only this plugin's bare name does not raise this error; it silently installs an older version that has passed 24 hours instead, see **Install the plugin**.) The package it names is often another plugin that was just updated, not this one. Retry once it is 24 hours old, or add the reported `name@version` under `minimumReleaseAgeExclude` in the profile's `pnpm-workspace.yaml`. Once this plugin is installed, **Update plugin** in Settings is not subject to that check.
247
257
 
248
258
  **The CLI is installed, but Claude still fails?** A host launched from Finder or the Start menu usually has a shorter `PATH` than a terminal. The plugin also checks the official install location, common package-manager directories, and `npm prefix -g`. If it still cannot find the binary, set `claudeCliPath` or click **Check again**. After a Host change or a reinstall, quit and reopen the desktop application.
249
259
 
package/README.md CHANGED
@@ -41,23 +41,25 @@
41
41
 
42
42
  ## 安装
43
43
 
44
- 先准备好 **DeepSeek Harness** 和 **PackyCode(PackyAPI)的 API Key**。尚未安装 DSH,可查看 [DSH 官方说明](https://github.com/deepseek-ai/deepseek-harness#run)。
44
+ 先准备好 **DeepSeek Harness** 和 **PackyCode(PackyAPI)的 API Key**。尚未安装 DSH,可查看 [DSH 官方说明](https://github.com/deepseek-ai/deepseek-harness#run)。还没有 API Key 的,可以通过[这个邀请链接](https://www.packyapi.ai/register?aff=Im3l)注册 PackyCode 申请。
45
45
 
46
46
  ### 1. 安装插件
47
47
 
48
- 打开桌面应用的**插件**页,点击**添加插件**,在**包名或地址**中填入:
48
+ 打开桌面应用的**插件**页,点击**添加插件**,在**包名或地址**中填入**带版本号**的包名:
49
49
 
50
50
  ```text
51
- dsh-packycode-api
51
+ dsh-packycode-api@1.2.7
52
52
  ```
53
53
 
54
- 在**安装源**中选择**官方源**,然后点击**安装**。需要固定版本时填入 `dsh-packycode-api@1.2.6`。
54
+ 在**安装源**中选择**官方源**,然后点击**安装**。`1.2.7` 是写这份说明时的最新版,当前最新版本号以 [npm 页面](https://www.npmjs.com/package/dsh-packycode-api) 为准。
55
55
 
56
- 首次安装后**无需重启 DSH**,可直接打开**设置 → PackyCode** 添加 Key。更新已加载的旧版时,按「插件版本」卡片的重启提示操作。
56
+ > **一定要带版本号。** 只填 `dsh-packycode-api` 时,安装前的提示显示的是 npm 上的最新版,但 DSH 自带的 pnpm 默认只安装发布满 24 小时的版本,实际装上的可能是好几个版本之前的旧版,而安装提示里看不出这一点。填写精确版本时,pnpm 会直接安装这个版本,并自动把它加入 profile 的 `minimumReleaseAgeExclude`。装好后可以在 **设置 → PackyCode → 插件版本** 核对「当前版本」。
57
+
58
+ 首次安装后**无需重启 DSH**,可直接打开**设置 → PackyCode** 添加 Key。这适用于 1.1.7 及以后的版本;更早的版本首次安装后要重启一次,见「常见问题」。更新已加载的旧版时,按「插件版本」卡片的重启提示操作。
57
59
 
58
60
  ### 2. 添加 Key
59
61
 
60
- 打开 **设置 → PackyCode**,在「添加 Key」里粘贴 API Key。备注可选,用来区分多个 Key。点「添加」。
62
+ 打开 **设置 → PackyCode**,在「添加 Key」里粘贴 API Key。备注可选,用来区分多个 Key。点「添加」。还没有添加过 Key 时,「账户与 Key」里会显示同一个注册邀请链接,点击后在系统浏览器中打开;添加 Key 之后就不再显示。
61
63
 
62
64
  > 不要把 Key 粘贴到公开 Issue、聊天记录或截图里。插件不会读取环境变量 `PACKY_CODE_API_KEY`,也不会在失败时偷偷改用别的 provider。
63
65
 
@@ -70,7 +72,7 @@ dsh-packycode-api
70
72
 
71
73
  桌面应用的**插件**页可以看到已安装状态与版本,也可以在那里启用、停用或卸载。
72
74
 
73
- **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.6`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
75
+ **仅用于非 desktop profile:** `dsh plugin --profile <name> add dsh-packycode-api@1.2.7`。不要对 `desktop` 使用它 —— CLI 会直接拒绝(`profile "desktop" is managed exclusively by the Electron application`)。
74
76
 
75
77
  本地开发要加 `link:` 前缀:`dsh plugin --profile <name> add link:/path/to/dsh-packycode-api`。**不要直接写路径(或 `file:`)** —— 那样的安装会把整个仓库目录(连同 `node_modules`)复制进 profile,之后它会在 profile 内部挡住 DSH 自带的包,插件看起来像没装,见「常见问题」。已经那样装过的话,先卸载再按上面的方式重装。
76
78
 
@@ -211,7 +213,7 @@ Responses 请求只有 GPT 模型保留 `prompt_cache_key`,其他 Responses
211
213
 
212
214
  ## 更新与卸载
213
215
 
214
- **更新:** 打开 **设置 → PackyCode**,有新版本时点「更新插件」。它会在当前 profile 目录里用 DSH 自带的 pnpm 安装精确版本,并读回 `package.json` 确认版本落地。本地 `link:` 开发安装不会出现这个按钮。完成后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。也可以在**插件**页重新安装 `dsh-packycode-api`。
216
+ **更新:** 打开 **设置 → PackyCode**,有新版本时点「更新插件」。它会在当前 profile 目录里用 DSH 自带的 pnpm 安装精确版本,并读回 `package.json` 确认版本落地。本地 `link:` 开发安装不会出现这个按钮。完成后完全退出并重新打开桌面应用。只刷新页面不会重载 Host。也可以在**插件**页重新安装,同样要填写带版本号的包名(例如 `dsh-packycode-api@1.2.7`),否则装上的可能是发布满 24 小时的旧版。
215
217
 
216
218
  **从本地路径安装切回官方源时,必须先卸载。** 「更新」是 `pnpm add`,它只重写包目录里的文件,不会删除更早一次本地路径安装留在包目录内的 `node_modules`;那份旧依赖会继续挡住 DSH 自带的包,插件行加载失败且看不出原因。「卸载」执行的是 `pnpm remove`,会删掉整个包目录,重装才是干净的。用 `node scripts/install-check.mjs` 可以确认当前 profile 是否处于这个状态。
217
219
 
@@ -227,7 +229,15 @@ ls ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/node_modules
227
229
 
228
230
  有输出就是残留:从 npm 安装的包不会自带 `node_modules`,这一份通常来自更早一次「本地路径」安装(`file:` 或直接写路径,而不是 `link:`)。它位于 profile 目录内部,而 DSH 解析插件导入时优先使用 profile 内的副本,所以插件拿到的仍是那份旧依赖 —— 例如 `@deepseek-ai/schemastery` 3.18.2 没有 `Schema.volatile()`,模块在导入阶段就抛错。导入失败时 Loader 不会为这一行创建 fiber,客户端 bundle 也不会被组合,于是模型路由、RPC 通道和设置页全都不会出现。
229
231
 
230
- 修复:到**插件**页**卸载**本插件,再重新**安装** `dsh-packycode-api`,然后完全退出并重新打开桌面应用。卸载执行的是 `pnpm remove`,会删掉整个包目录;直接重装或点「更新」只重写文件,不会清掉这份残留。也可以手动删掉上面那个 `node_modules` 再重启。
232
+ 修复:到**插件**页**卸载**本插件,再用带版本号的包名重新**安装**(例如 `dsh-packycode-api@1.2.7`),然后完全退出并重新打开桌面应用。卸载执行的是 `pnpm remove`,会删掉整个包目录;直接重装或点「更新」只重写文件,不会清掉这份残留。也可以手动删掉上面那个 `node_modules` 再重启。
233
+
234
+ **首次安装后,设置页每张卡片都显示 `transport failure for /packycode-api/…: HTTP 405`?** 实际装上的是 1.1.6 或更早的版本,通常是因为安装时只填了包名:安装提示显示的是最新版,pnpm 实际装的却是发布满 24 小时的旧版(见「安装插件」)。这些旧版要等 DSH 重启、重建它的 `connection` 服务之后,才能注册设置通道。所以首次安装后 `/packycode-api` 通道还不存在,请求落到静态页面,返回 405;模型路由和设置页本身照常加载。1.1.7 起首次安装不需要重启。先确认装上的版本:
235
+
236
+ ```sh
237
+ grep '"version"' ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/package.json
238
+ ```
239
+
240
+ 处理:完全退出并重新打开桌面应用,旧版即可正常使用。然后在 **设置 → PackyCode → 插件版本** 点「检查更新」→「更新插件」(它会固定安装最新版,不受 24 小时规则限制),再重启一次。
231
241
 
232
242
  **某个模型不可用,但 Key 是有效的?** 看「账户与 Key」里这个 Key 的分组和 ✓ / ✕:分组不覆盖这个模型时,换一个分组合适的 Key,或在 PackyCode 控制台给 Key 加上对应分组。存了分组合适的 Key 时,插件会自动用它跑这个模型。分组拒绝不会停用 Key。
233
243
 
@@ -241,7 +251,7 @@ ls ~/.dsh/profiles/desktop/node_modules/dsh-packycode-api/node_modules
241
251
 
242
252
  **限速之后 Key 被停用了?** 不该发生。429 不会切换 Key。额度用尽或 401 才会停用;充值后点「重新启用」。
243
253
 
244
- **插件页安装报 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`?** pnpm 默认拒绝发布不满 24 小时的包,并且会检查 profile 里的整个 lockfile。报错里点名的往往是另一个刚更新过的插件,不是本插件。等它发布满 24 小时再重试,或把报错里的 `包名@版本` 加到 profile 目录 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 下。已安装本插件后,设置页的「更新插件」不受这个限制。
254
+ **插件页安装报 `ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`?** pnpm 默认只接受发布满 24 小时的版本,并且会检查 profile 里的整个 lockfile。(只填本插件的包名不会报这个错,而是悄悄装上满 24 小时的旧版,见「安装插件」。)报错里点名的往往是另一个刚更新过的插件,不是本插件。等它发布满 24 小时再重试,或把报错里的 `包名@版本` 加到 profile 目录 `pnpm-workspace.yaml` 的 `minimumReleaseAgeExclude` 下。已安装本插件后,设置页的「更新插件」不受这个限制。
245
255
 
246
256
  **网页搜索报「no stored API key's group covers …」?** 所选搜索后端的模型不在你的令牌分组里(或网关暂时把它路由到了别的供应商)。在 **设置 → PackyCode → 网页搜索** 换一个后端,或添加分组覆盖它的 Key。
247
257
 
package/lib/client.js CHANGED
@@ -44,7 +44,6 @@ window.__ModuleLoader__.load({
44
44
  imagesBalanceFailed: "图片余额读取失败",
45
45
  imagesComposerLabel: "图片剩余额度",
46
46
  imagesBalanceKey: "当前图片 Key",
47
- imagesBalanceReadAt: "更新于 {time}",
48
47
  imagesBalanceHint: "仅汇总图片 Key 的额度,每分钟自动更新;也可手动刷新。与对话 Key 的余额分开统计。",
49
48
  imagesIntro: "使用独立图片 Key 调用 PackyCode 图片接口。生成新图或上传图片后描述修改要求;图片模型独立于对话模型。原始 PNG 保存在本机,聊天中显示图片预览。",
50
49
  imagesEnabled: "图片生成与编辑",
@@ -88,6 +87,8 @@ window.__ModuleLoader__.load({
88
87
  totalStale: "其中 {count} 个 Key 本次读取失败,显示的是上次读到的余额。",
89
88
  staleNote: "上次读取于 {time},本次读取失败:{error}",
90
89
  empty: "还没有 Key。添加一个即可在模型选择器里看到 PackyCode 的模型。",
90
+ inviteLead: "还没有 PackyCode 的 API Key?可以通过邀请链接注册:",
91
+ inviteLink: "www.packyapi.ai/register ↗",
91
92
  keysHint: "按顺序使用:当前 Key 不可用时自动切到下一个;「切到此 Key」指定优先使用的 Key。",
92
93
  keyActive: "使用中",
93
94
  keyDisabled: "已停用",
@@ -137,9 +138,11 @@ window.__ModuleLoader__.load({
137
138
  apiHost: "模型接口地址",
138
139
  statusHost: "余额接口地址",
139
140
  composerLabel: "PackyCode 剩余额度",
141
+ composerKey: "当前 Key",
140
142
  composerNoKey: "无可用 Key",
141
143
  composerUnread: "余额读取失败",
142
144
  composerStale: "上次读取于 {time},本次读取失败",
145
+ quotaReadAt: "更新于 {time}",
143
146
  cliTitle: "Claude Code CLI",
144
147
  cliChecking: "正在检测本机的 Claude Code CLI…",
145
148
  cliInstalled: "已安装",
@@ -226,7 +229,6 @@ window.__ModuleLoader__.load({
226
229
  imagesBalanceFailed: "Could not read image balances",
227
230
  imagesComposerLabel: "Image balance",
228
231
  imagesBalanceKey: "Current image key",
229
- imagesBalanceReadAt: "Updated at {time}",
230
232
  imagesBalanceHint: "Only image key quotas are counted here. Updates every minute, or use Refresh. Conversation key balances are separate.",
231
233
  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.",
232
234
  imagesEnabled: "Generate and edit images",
@@ -270,6 +272,8 @@ window.__ModuleLoader__.load({
270
272
  totalStale: "{count} of these keys failed this read and show their last read balance.",
271
273
  staleNote: "Last read at {time}; this read failed: {error}",
272
274
  empty: "No keys yet. Add one and the PackyCode models appear in the model picker.",
275
+ inviteLead: "No PackyCode API key yet? You can register through this invite link:",
276
+ inviteLink: "www.packyapi.ai/register ↗",
273
277
  keysHint: "Used in order: when the active key cannot serve a request, the next one takes over. “Use this key” picks the preferred key.",
274
278
  keyActive: "Active",
275
279
  keyDisabled: "Disabled",
@@ -311,9 +315,11 @@ window.__ModuleLoader__.load({
311
315
  apiHost: "Model endpoint",
312
316
  statusHost: "Balance endpoint",
313
317
  composerLabel: "PackyCode remaining balance",
318
+ composerKey: "Current key",
314
319
  composerNoKey: "No usable key",
315
320
  composerUnread: "Balance unavailable",
316
- composerStale: "last read at {time}; this read failed",
321
+ composerStale: "Last read at {time}; this read failed",
322
+ quotaReadAt: "Updated at {time}",
317
323
  cliTitle: "Claude Code CLI",
318
324
  cliChecking: "Checking this device for the Claude Code CLI…",
319
325
  cliInstalled: "Installed",
@@ -400,6 +406,8 @@ window.__ModuleLoader__.load({
400
406
  .pkc-head{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:28px}
401
407
  .pkc-title{display:flex;align-items:center;gap:8px;margin:0;font-size:15px;font-weight:600;line-height:22px}
402
408
  .pkc-note{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
409
+ .pkc-link{color:var(--dsw-alias-link,#3964fe);font-weight:500;text-decoration:none;word-break:break-all}
410
+ .pkc-link:hover,.pkc-link:focus{outline:none;text-decoration:underline dotted var(--dsw-alias-link,#3964fe);text-underline-offset:3px}
403
411
  .pkc-warn{margin:0;color:var(--dsw-alias-state-warn-label,#dd8629);font-size:12px;line-height:18px}
404
412
  .pkc-error{margin:0;color:var(--dsw-alias-state-error-primary,#ec1313);font-size:12px;line-height:18px}
405
413
  .pkc-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}
@@ -423,11 +431,12 @@ window.__ModuleLoader__.load({
423
431
  .pkc-input:disabled{opacity:.5}
424
432
  .pkc-image-choice{flex:none;width:100%;max-width:300px;cursor:pointer}
425
433
  .pkc-image-shortcut{display:inline-flex;align-items:center;gap:8px}
426
- .pkc-image-balance{position:relative;display:inline-flex;align-items:center}
427
- .pkc-image-balance-value{cursor:help;border-radius:var(--dsw-radius-xs,4px)}
428
- .pkc-image-balance-value:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
429
- .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}
430
- .pkc-image-balance:hover>.pkc-image-balance-tooltip,.pkc-image-balance:focus-within>.pkc-image-balance-tooltip{visibility:visible;opacity:1}
434
+ .pkc-quota{position:relative;display:inline-flex;align-items:center}
435
+ .pkc-quota-value{cursor:help;border-radius:var(--dsw-radius-xs,4px)}
436
+ .pkc-quota-value:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
437
+ .pkc-quota-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}
438
+ .pkc-quota[data-align=end]>.pkc-quota-tooltip{left:auto;right:0}
439
+ .pkc-quota:hover>.pkc-quota-tooltip,.pkc-quota:focus-within>.pkc-quota-tooltip{visibility:visible;opacity:1}
431
440
  .pkc-image-result,.pkc-image-chat{display:flex;flex-direction:column;gap:10px;padding:8px 0}
432
441
  .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}
433
442
  .pkc-generated-image img{display:block;max-width:100%;max-height:480px;object-fit:contain}
@@ -720,6 +729,54 @@ window.__ModuleLoader__.load({
720
729
  })
721
730
  ] });
722
731
  }
732
+ /**
733
+ * A composer quota figure with the same details for either key ring. The help
734
+ * cursor marks it; hovering or focusing it shows the key, its account, the
735
+ * remaining, used and total quota and when it was read, and asks the caller
736
+ * for an unforced read.
737
+ * @param props.copy - the ring's own lines: `title`, `key`, `failed`, `loading`.
738
+ * @param props.empty - what the figure shows while the remaining quota is unknown.
739
+ * @param props.align - `end` opens the details leftward, for the composer's right side.
740
+ */
741
+ function QuotaHover({ row, fetchedAt, loading = false, failed = false, empty, copy, align, onRefresh, t }) {
742
+ const tooltipId = react.useId();
743
+ const usage = row?.usage;
744
+ const unlimited = usage?.unlimitedQuota === true;
745
+ const remaining = unlimited ? t("keyUnlimited") : formatUsd(usage?.availableUsd);
746
+ const stale = usage !== void 0 && (failed || row.stale !== void 0);
747
+ const display = remaining ?? empty;
748
+ const details = [
749
+ copy.title,
750
+ row?.label ? `${copy.key}: ${row.label}` : void 0,
751
+ usage?.name ? `${t("keyName")}: ${usage.name}` : void 0,
752
+ `${t("keyRemaining")}: ${remaining ?? "—"}`,
753
+ `${t("keyUsed")}: ${formatUsd(usage?.usedUsd) ?? "—"}`,
754
+ `${t("accountLimit")}: ${unlimited ? t("keyUnlimited") : formatUsd(quotaTotalUsd(usage)) ?? "—"}`,
755
+ stale ? fill(t("composerStale"), { time: readTime(row.stale?.fetchedAt ?? fetchedAt) }) : failed || row?.error ? copy.failed : loading ? copy.loading : fetchedAt === void 0 ? void 0 : fill(t("quotaReadAt"), { time: readTime(fetchedAt) })
756
+ ].filter(Boolean).join("\n");
757
+ const refresh = () => {
758
+ onRefresh?.();
759
+ };
760
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
761
+ className: "pkc-quota",
762
+ "data-align": align,
763
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
764
+ className: "pkc-composer pkc-quota-value",
765
+ role: "status",
766
+ tabIndex: 0,
767
+ "aria-label": `${copy.title}: ${stale ? "≈" : ""}${display}`,
768
+ "aria-describedby": tooltipId,
769
+ onMouseEnter: refresh,
770
+ onFocus: refresh,
771
+ children: stale ? `≈${display}` : display
772
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
773
+ className: "pkc-quota-tooltip",
774
+ id: tooltipId,
775
+ role: "tooltip",
776
+ children: details
777
+ })]
778
+ });
779
+ }
723
780
  /** Only this card's keys contribute; account-wide billing is never added. */
724
781
  function BalanceTotals({ rows, t }) {
725
782
  const totals = sumBalances(rows);
@@ -779,7 +836,6 @@ window.__ModuleLoader__.load({
779
836
  function ImageShortcutBalance({ call, t }) {
780
837
  const [view, setView] = react.useState({ loading: true });
781
838
  const refresh = react.useRef(() => {});
782
- const tooltipId = react.useId();
783
839
  react.useEffect(() => {
784
840
  let live = true, generation = 0, pending = false;
785
841
  const read = async (force = false, reset = false) => {
@@ -823,42 +879,22 @@ window.__ModuleLoader__.load({
823
879
  window.removeEventListener("packycode-images-updated", changed);
824
880
  };
825
881
  }, [call]);
826
- const row = currentImageKey(view.balance);
827
- const usage = row?.usage;
828
- const unlimited = usage?.unlimitedQuota === true;
829
- const remaining = unlimited ? t("keyUnlimited") : formatUsd(usage?.availableUsd);
830
- const stale = usage !== void 0 && (view.failed || row.stale !== void 0);
831
- const display = remaining ?? (view.loading ? "…" : "—");
832
- const details = [
833
- t("imagesComposerLabel"),
834
- row?.label ? `${t("imagesBalanceKey")}: ${row.label}` : void 0,
835
- usage?.name ? `${t("keyName")}: ${usage.name}` : void 0,
836
- `${t("keyRemaining")}: ${remaining ?? "—"}`,
837
- `${t("keyUsed")}: ${formatUsd(usage?.usedUsd) ?? "—"}`,
838
- `${t("accountLimit")}: ${unlimited ? t("keyUnlimited") : formatUsd(quotaTotalUsd(usage)) ?? "—"}`,
839
- 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) })
840
- ].filter(Boolean).join("\n");
841
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
842
- className: "pkc-image-balance",
843
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
844
- className: "pkc-composer pkc-image-balance-value",
845
- role: "status",
846
- tabIndex: 0,
847
- "aria-label": `${t("imagesComposerLabel")}: ${stale ? "≈" : ""}${display}`,
848
- "aria-describedby": tooltipId,
849
- onMouseEnter: () => {
850
- refresh.current();
851
- },
852
- onFocus: () => {
853
- refresh.current();
854
- },
855
- children: stale ? `≈${display}` : display
856
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
857
- className: "pkc-image-balance-tooltip",
858
- id: tooltipId,
859
- role: "tooltip",
860
- children: details
861
- })]
882
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaHover, {
883
+ row: currentImageKey(view.balance),
884
+ fetchedAt: view.balance?.fetchedAt,
885
+ loading: view.loading,
886
+ failed: view.failed === true,
887
+ empty: view.loading ? "…" : "—",
888
+ copy: {
889
+ title: t("imagesComposerLabel"),
890
+ key: t("imagesBalanceKey"),
891
+ failed: t("imagesBalanceFailed"),
892
+ loading: t("imagesBalanceLoading")
893
+ },
894
+ onRefresh: () => {
895
+ refresh.current();
896
+ },
897
+ t
862
898
  });
863
899
  }
864
900
  //#endregion
@@ -1116,6 +1152,79 @@ window.__ModuleLoader__.load({
1116
1152
  }, image.revision);
1117
1153
  }
1118
1154
  //#endregion
1155
+ //#region src/client-key-rename.jsx
1156
+ /**
1157
+ * Renaming a stored key, the same way in the conversation and image key
1158
+ * cards: the name becomes a text field; Enter or Save stores it, Escape or
1159
+ * Cancel keeps the old one. Only the key's id and the new label reach the host.
1160
+ *
1161
+ * @module dsh-packycode-api/client-key-rename
1162
+ */
1163
+ /**
1164
+ * The rename state a key card keeps.
1165
+ * @param save - `({ id, label }) => Promise`; the field closes once it settles.
1166
+ * @returns `{ renaming, start, change, cancel, submit }`.
1167
+ */
1168
+ function useKeyRename(save) {
1169
+ const [renaming, setRenaming] = react.useState();
1170
+ return {
1171
+ renaming,
1172
+ start: (entry) => setRenaming({
1173
+ id: entry.id,
1174
+ label: entry.label
1175
+ }),
1176
+ change: (label) => setRenaming((current) => ({
1177
+ ...current,
1178
+ label
1179
+ })),
1180
+ cancel: () => setRenaming(void 0),
1181
+ submit: () => {
1182
+ if (renaming === void 0 || renaming.label.trim() === "") return;
1183
+ save({
1184
+ id: renaming.id,
1185
+ label: renaming.label
1186
+ }).then(() => setRenaming(void 0));
1187
+ }
1188
+ };
1189
+ }
1190
+ /** The field and buttons that stand in for a key's name while it is renamed. */
1191
+ function KeyRenameForm({ rename, busy, t }) {
1192
+ const label = rename.renaming.label;
1193
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1194
+ className: "pkc-form",
1195
+ onSubmit: (event) => {
1196
+ event.preventDefault();
1197
+ rename.submit();
1198
+ },
1199
+ children: [
1200
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1201
+ className: "pkc-input",
1202
+ type: "text",
1203
+ maxLength: 60,
1204
+ autoFocus: true,
1205
+ placeholder: t("keyRenamePlaceholder"),
1206
+ value: label,
1207
+ onChange: (event) => rename.change(event.target.value),
1208
+ onKeyDown: (event) => event.key === "Escape" && rename.cancel()
1209
+ }),
1210
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1211
+ type: "submit",
1212
+ className: "pkc-btn",
1213
+ "data-variant": "primary",
1214
+ disabled: busy || label.trim() === "",
1215
+ children: t("keyRenameSave")
1216
+ }),
1217
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1218
+ type: "button",
1219
+ className: "pkc-btn",
1220
+ "data-variant": "ghost",
1221
+ onClick: rename.cancel,
1222
+ children: t("keyRenameCancel")
1223
+ })
1224
+ ]
1225
+ });
1226
+ }
1227
+ //#endregion
1119
1228
  //#region src/client-images.jsx
1120
1229
  const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
1121
1230
  /** Only projections come back from the host; the password draft is cleared on success. */
@@ -1182,6 +1291,10 @@ window.__ModuleLoader__.load({
1182
1291
  if (mounted.current) setBusy(false);
1183
1292
  }
1184
1293
  };
1294
+ const rename = useKeyRename(({ id, label }) => mutate("images/keys/rename", {
1295
+ id,
1296
+ label
1297
+ }));
1185
1298
  const disabled = busy || state?.writable !== true;
1186
1299
  const models = state?.models ?? [];
1187
1300
  const qualities = models.find((model) => model.id === state?.imageModel)?.qualities ?? [];
@@ -1291,7 +1404,11 @@ window.__ModuleLoader__.load({
1291
1404
  className: "pkc-row-top",
1292
1405
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1293
1406
  className: "pkc-row-name",
1294
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1407
+ children: [rename.renaming?.id === entry.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyRenameForm, {
1408
+ rename,
1409
+ busy,
1410
+ t
1411
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1295
1412
  className: "pkc-name",
1296
1413
  children: entry.label
1297
1414
  }), entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1299,26 +1416,37 @@ window.__ModuleLoader__.load({
1299
1416
  "data-tone": "success",
1300
1417
  children: t("keyActive")
1301
1418
  })]
1302
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1419
+ }), rename.renaming?.id !== entry.id && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1303
1420
  className: "pkc-actions",
1304
- children: [!entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1305
- className: "pkc-btn",
1306
- type: "button",
1307
- disabled: busy,
1308
- onClick: () => {
1309
- mutate("images/keys/select", { id: entry.id });
1310
- },
1311
- children: t("keyUse")
1312
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1313
- className: "pkc-btn",
1314
- "data-variant": "danger",
1315
- type: "button",
1316
- disabled: busy,
1317
- onClick: () => {
1318
- mutate("images/keys/remove", { id: entry.id });
1319
- },
1320
- children: t("keyRemove")
1321
- })]
1421
+ children: [
1422
+ !entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1423
+ className: "pkc-btn",
1424
+ type: "button",
1425
+ disabled: busy,
1426
+ onClick: () => {
1427
+ mutate("images/keys/select", { id: entry.id });
1428
+ },
1429
+ children: t("keyUse")
1430
+ }),
1431
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1432
+ className: "pkc-btn",
1433
+ "data-variant": "ghost",
1434
+ type: "button",
1435
+ disabled: busy,
1436
+ onClick: () => rename.start(entry),
1437
+ children: t("keyRename")
1438
+ }),
1439
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1440
+ className: "pkc-btn",
1441
+ "data-variant": "danger",
1442
+ type: "button",
1443
+ disabled: busy,
1444
+ onClick: () => {
1445
+ mutate("images/keys/remove", { id: entry.id });
1446
+ },
1447
+ children: t("keyRemove")
1448
+ })
1449
+ ]
1322
1450
  })]
1323
1451
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
1324
1452
  row: rows.find((row) => row.id === entry.id),
@@ -1607,6 +1735,8 @@ window.__ModuleLoader__.load({
1607
1735
  const PROVIDER = "packycode";
1608
1736
  const CHANNEL = "/packycode-api";
1609
1737
  const REFRESH_MS = 6e4;
1738
+ /** PackyCode registration through the author's invite link, offered only until a key is stored. */
1739
+ const INVITE_URL = "https://www.packyapi.ai/register?aff=Im3l";
1610
1740
  const INSTALL_POLL_MS = 2e3;
1611
1741
  /** How long after a search change the panel re-reads what DSH applied (the patch hot-reloads). */
1612
1742
  const SEARCH_SETTLE_MS = 1500;
@@ -1748,8 +1878,8 @@ window.__ModuleLoader__.load({
1748
1878
  })
1749
1879
  ] });
1750
1880
  }
1751
- function KeyRow({ entry, row, group, descriptions, groupsLoading, busy, renaming, onRename, onAction, t }) {
1752
- const editing = renaming?.id === entry.id;
1881
+ function KeyRow({ entry, row, group, descriptions, groupsLoading, busy, rename, onAction, t }) {
1882
+ const editing = rename.renaming?.id === entry.id;
1753
1883
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1754
1884
  className: "pkc-row",
1755
1885
  children: [
@@ -1758,35 +1888,10 @@ window.__ModuleLoader__.load({
1758
1888
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1759
1889
  className: "pkc-row-name",
1760
1890
  children: [
1761
- editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1762
- className: "pkc-form",
1763
- onSubmit: (event) => {
1764
- event.preventDefault();
1765
- onRename.submit();
1766
- },
1767
- children: [
1768
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1769
- className: "pkc-input",
1770
- type: "text",
1771
- maxLength: 60,
1772
- autoFocus: true,
1773
- placeholder: t("keyRenamePlaceholder"),
1774
- value: renaming.label,
1775
- onChange: (event) => onRename.change(event.target.value),
1776
- onKeyDown: (event) => event.key === "Escape" && onRename.cancel()
1777
- }),
1778
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1779
- type: "submit",
1780
- variant: "primary",
1781
- disabled: busy || renaming.label.trim() === "",
1782
- children: t("keyRenameSave")
1783
- }),
1784
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1785
- variant: "ghost",
1786
- onClick: onRename.cancel,
1787
- children: t("keyRenameCancel")
1788
- })
1789
- ]
1891
+ editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyRenameForm, {
1892
+ rename,
1893
+ busy,
1894
+ t
1790
1895
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1791
1896
  className: "pkc-name",
1792
1897
  children: entry.label
@@ -1818,7 +1923,7 @@ window.__ModuleLoader__.load({
1818
1923
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1819
1924
  variant: "ghost",
1820
1925
  disabled: busy,
1821
- onClick: () => onRename.start(entry),
1926
+ onClick: () => rename.start(entry),
1822
1927
  children: t("keyRename")
1823
1928
  }),
1824
1929
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
@@ -1843,6 +1948,27 @@ window.__ModuleLoader__.load({
1843
1948
  ]
1844
1949
  });
1845
1950
  }
1951
+ /**
1952
+ * Where to get a key, for a profile that has none. A new-window link: the
1953
+ * desktop shell hands http(s) window opens to the system browser, and a web
1954
+ * profile opens a new tab.
1955
+ */
1956
+ function RegisterInvite({ t }) {
1957
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
1958
+ className: "pkc-note",
1959
+ children: [
1960
+ t("inviteLead"),
1961
+ " ",
1962
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1963
+ className: "pkc-link",
1964
+ href: INVITE_URL,
1965
+ target: "_blank",
1966
+ rel: "noopener noreferrer",
1967
+ children: t("inviteLink")
1968
+ })
1969
+ ]
1970
+ });
1971
+ }
1846
1972
  function AccountsCard({ call, t }) {
1847
1973
  const [status, setStatus] = react.useState();
1848
1974
  const [balance, setBalance] = react.useState();
@@ -1854,7 +1980,6 @@ window.__ModuleLoader__.load({
1854
1980
  key: "",
1855
1981
  label: ""
1856
1982
  });
1857
- const [renaming, setRenaming] = react.useState();
1858
1983
  const generation = react.useRef(0);
1859
1984
  const readGroups = react.useCallback((force = false) => {
1860
1985
  setGroupsLoading(true);
@@ -1901,24 +2026,10 @@ window.__ModuleLoader__.load({
1901
2026
  label: ""
1902
2027
  }));
1903
2028
  };
1904
- const rename = {
1905
- start: (entry) => setRenaming({
1906
- id: entry.id,
1907
- label: entry.label
1908
- }),
1909
- change: (label) => setRenaming((current) => ({
1910
- ...current,
1911
- label
1912
- })),
1913
- cancel: () => setRenaming(void 0),
1914
- submit: () => {
1915
- if (renaming === void 0 || renaming.label.trim() === "") return;
1916
- mutate("keys/rename", {
1917
- id: renaming.id,
1918
- label: renaming.label
1919
- }).then(() => setRenaming(void 0));
1920
- }
1921
- };
2029
+ const rename = useKeyRename(({ id, label }) => mutate("keys/rename", {
2030
+ id,
2031
+ label
2032
+ }));
1922
2033
  const keys = status?.keys ?? [];
1923
2034
  const rows = balance?.keys ?? [];
1924
2035
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
@@ -1937,10 +2048,10 @@ window.__ModuleLoader__.load({
1937
2048
  })]
1938
2049
  }),
1939
2050
  children: [
1940
- keys.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2051
+ keys.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1941
2052
  className: "pkc-note",
1942
2053
  children: t("empty")
1943
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2054
+ }), status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RegisterInvite, { t })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1944
2055
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
1945
2056
  rows,
1946
2057
  t
@@ -1954,8 +2065,7 @@ window.__ModuleLoader__.load({
1954
2065
  descriptions: groups?.descriptions,
1955
2066
  groupsLoading,
1956
2067
  busy,
1957
- renaming,
1958
- onRename: rename,
2068
+ rename,
1959
2069
  onAction: mutate,
1960
2070
  t
1961
2071
  }, entry.id))
@@ -2634,38 +2744,48 @@ window.__ModuleLoader__.load({
2634
2744
  const call = useCaller(rpc);
2635
2745
  const enabled = react.useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot())?.current?.provider === PROVIDER;
2636
2746
  const [balance, setBalance] = react.useState();
2747
+ const refresh = react.useRef(() => {});
2637
2748
  react.useEffect(() => {
2638
2749
  if (!enabled) {
2639
2750
  setBalance(void 0);
2640
2751
  return;
2641
2752
  }
2642
2753
  let live = true;
2754
+ let pending = false;
2643
2755
  const tick = () => {
2756
+ if (pending) return;
2757
+ pending = true;
2644
2758
  call("balance").then((value) => {
2645
2759
  if (live) setBalance(value);
2646
2760
  }).catch(() => {
2647
2761
  if (live) setBalance(void 0);
2762
+ }).finally(() => {
2763
+ pending = false;
2648
2764
  });
2649
2765
  };
2766
+ refresh.current = tick;
2650
2767
  tick();
2651
2768
  const timer = window.setInterval(tick, REFRESH_MS);
2652
2769
  return () => {
2653
2770
  live = false;
2771
+ refresh.current = () => {};
2654
2772
  window.clearInterval(timer);
2655
2773
  };
2656
2774
  }, [call, enabled]);
2657
2775
  if (!enabled || balance === void 0) return null;
2658
2776
  const row = activeUsage(balance);
2659
- const usage = row?.usage;
2660
- const display = usage?.unlimitedQuota === true ? t("keyUnlimited") : formatUsd(usage?.availableUsd) ?? (row?.error === void 0 ? t("composerNoKey") : t("composerUnread"));
2661
- const stale = row?.stale !== void 0 && usage !== void 0;
2662
- const label = stale ? `${t("composerLabel")}: ${display}(${fill(t("composerStale"), { time: readTime(row.stale.fetchedAt) })})` : `${t("composerLabel")}: ${display}`;
2663
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2664
- className: "pkc-composer",
2665
- role: "status",
2666
- "aria-label": label,
2667
- title: label,
2668
- children: stale ? `≈${display}` : display
2777
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaHover, {
2778
+ row,
2779
+ fetchedAt: balance.fetchedAt,
2780
+ empty: row?.error === void 0 ? t("composerNoKey") : t("composerUnread"),
2781
+ copy: {
2782
+ title: t("composerLabel"),
2783
+ key: t("composerKey"),
2784
+ failed: t("composerUnread")
2785
+ },
2786
+ align: "end",
2787
+ onRefresh: () => refresh.current(),
2788
+ t
2669
2789
  });
2670
2790
  }
2671
2791
  const inject = [
package/lib/index.js CHANGED
@@ -5631,6 +5631,7 @@ function createImageController({ ring, balance, readPreference, writePreference,
5631
5631
  async mutate(operation, payload = {}) {
5632
5632
  const operations = {
5633
5633
  add: () => ring.add(payload.key, payload.label),
5634
+ rename: () => ring.rename(String(payload.id ?? ""), payload.label),
5634
5635
  remove: () => ring.remove(String(payload.id ?? "")),
5635
5636
  select: () => ring.select(String(payload.id ?? "")),
5636
5637
  enable: () => ring.enable(String(payload.id ?? ""))
@@ -7080,6 +7081,10 @@ function createPackyCodeRpc({ ring, balance, groups, hosts, preference, claudeCl
7080
7081
  key: payload?.key,
7081
7082
  label: payload?.label
7082
7083
  }),
7084
+ "images/keys/rename": (payload) => imageSupport().mutate("rename", {
7085
+ id: payload?.id,
7086
+ label: payload?.label
7087
+ }),
7083
7088
  "images/keys/remove": (payload) => imageSupport().mutate("remove", { id: payload?.id }),
7084
7089
  "images/keys/select": (payload) => imageSupport().mutate("select", { id: payload?.id }),
7085
7090
  "images/keys/enable": (payload) => imageSupport().mutate("enable", { id: payload?.id })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-packycode-api",
3
- "version": "1.2.6",
3
+ "version": "1.2.7",
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",