dsh-packycode-api 1.2.6 → 1.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -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: "已安装",
@@ -182,6 +185,20 @@ window.__ModuleLoader__.load({
182
185
  versionRestartHint: "插件无法安全地重启桌面应用。请完全退出 DSH,再重新打开。",
183
186
  versionRefresh: "刷新界面",
184
187
  versionLater: "稍后",
188
+ diagTitle: "诊断信息",
189
+ diagIntro: "遇到回答被截断(已达到输出 token 上限)、请求反复失败等问题时,选择出问题的会话,生成诊断信息并复制给插件作者。",
190
+ diagSession: "会话",
191
+ diagGenerate: "生成诊断信息",
192
+ diagGenerating: "正在生成…",
193
+ diagReport: "诊断信息内容",
194
+ diagCopy: "复制",
195
+ diagCopied: "已复制",
196
+ diagSelected: "已选中全部内容,请按 ⌘C 或 Ctrl+C 复制。",
197
+ diagPrivacy: "只包含模型、推理档位、输出上限、每一步的 token 用量和错误码,不含对话内容、文件路径和 Key。",
198
+ diagNoSessions: "还没有会话。",
199
+ diagLive: "进行中",
200
+ diagNoFolder: "无目录",
201
+ diagError: "生成诊断信息失败",
185
202
  searchTitle: "网页搜索",
186
203
  searchScope: "DSH 全局设置",
187
204
  searchIntro: "DSH 的 web_search 和 Claude CLI 的 MCP 搜索可使用这里选择的 DeepSeek/GPT 后端。每次搜索是独立的模型调用,用插件保存的 Key 计费。",
@@ -226,7 +243,6 @@ window.__ModuleLoader__.load({
226
243
  imagesBalanceFailed: "Could not read image balances",
227
244
  imagesComposerLabel: "Image balance",
228
245
  imagesBalanceKey: "Current image key",
229
- imagesBalanceReadAt: "Updated at {time}",
230
246
  imagesBalanceHint: "Only image key quotas are counted here. Updates every minute, or use Refresh. Conversation key balances are separate.",
231
247
  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
248
  imagesEnabled: "Generate and edit images",
@@ -270,6 +286,8 @@ window.__ModuleLoader__.load({
270
286
  totalStale: "{count} of these keys failed this read and show their last read balance.",
271
287
  staleNote: "Last read at {time}; this read failed: {error}",
272
288
  empty: "No keys yet. Add one and the PackyCode models appear in the model picker.",
289
+ inviteLead: "No PackyCode API key yet? You can register through this invite link:",
290
+ inviteLink: "www.packyapi.ai/register ↗",
273
291
  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
292
  keyActive: "Active",
275
293
  keyDisabled: "Disabled",
@@ -311,9 +329,11 @@ window.__ModuleLoader__.load({
311
329
  apiHost: "Model endpoint",
312
330
  statusHost: "Balance endpoint",
313
331
  composerLabel: "PackyCode remaining balance",
332
+ composerKey: "Current key",
314
333
  composerNoKey: "No usable key",
315
334
  composerUnread: "Balance unavailable",
316
- composerStale: "last read at {time}; this read failed",
335
+ composerStale: "Last read at {time}; this read failed",
336
+ quotaReadAt: "Updated at {time}",
317
337
  cliTitle: "Claude Code CLI",
318
338
  cliChecking: "Checking this device for the Claude Code CLI…",
319
339
  cliInstalled: "Installed",
@@ -356,6 +376,20 @@ window.__ModuleLoader__.load({
356
376
  versionRestartHint: "The plugin cannot safely restart the desktop application. Quit DSH completely, then open it again.",
357
377
  versionRefresh: "Refresh page",
358
378
  versionLater: "Later",
379
+ diagTitle: "Diagnostics",
380
+ diagIntro: "If replies are cut off (\"output token limit reached\") or requests keep failing, pick the affected session, generate its diagnostics and send them to the plugin author.",
381
+ diagSession: "Session",
382
+ diagGenerate: "Generate diagnostics",
383
+ diagGenerating: "Generating…",
384
+ diagReport: "Diagnostics text",
385
+ diagCopy: "Copy",
386
+ diagCopied: "Copied",
387
+ diagSelected: "All text is selected; press ⌘C or Ctrl+C to copy.",
388
+ diagPrivacy: "Contains only models, reasoning effort, output limits, per-step token usage and error codes — no conversation text, file paths or keys.",
389
+ diagNoSessions: "No sessions yet.",
390
+ diagLive: "open",
391
+ diagNoFolder: "no folder",
392
+ diagError: "Could not generate diagnostics",
359
393
  searchTitle: "Web search",
360
394
  searchScope: "DSH-wide setting",
361
395
  searchIntro: "DSH’s web_search and Claude CLI’s MCP search can use the DeepSeek/GPT backend selected here. Each search is a separate model call billed to the plugin-stored keys.",
@@ -400,6 +434,8 @@ window.__ModuleLoader__.load({
400
434
  .pkc-head{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:28px}
401
435
  .pkc-title{display:flex;align-items:center;gap:8px;margin:0;font-size:15px;font-weight:600;line-height:22px}
402
436
  .pkc-note{margin:0;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}
437
+ .pkc-link{color:var(--dsw-alias-link,#3964fe);font-weight:500;text-decoration:none;word-break:break-all}
438
+ .pkc-link:hover,.pkc-link:focus{outline:none;text-decoration:underline dotted var(--dsw-alias-link,#3964fe);text-underline-offset:3px}
403
439
  .pkc-warn{margin:0;color:var(--dsw-alias-state-warn-label,#dd8629);font-size:12px;line-height:18px}
404
440
  .pkc-error{margin:0;color:var(--dsw-alias-state-error-primary,#ec1313);font-size:12px;line-height:18px}
405
441
  .pkc-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}
@@ -421,13 +457,16 @@ window.__ModuleLoader__.load({
421
457
  .pkc-input:focus{border-color:var(--dsw-alias-state-business-primary)}
422
458
  .pkc-input::placeholder{color:var(--dsw-alias-label-caption)}
423
459
  .pkc-input:disabled{opacity:.5}
460
+ .pkc-diag-session{flex:1 1 260px;max-width:480px;cursor:pointer}
461
+ .pkc-diag-text{width:100%;height:auto;min-height:220px;padding:8px 10px;line-height:18px;resize:vertical;white-space:pre;overflow:auto}
424
462
  .pkc-image-choice{flex:none;width:100%;max-width:300px;cursor:pointer}
425
463
  .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}
464
+ .pkc-quota{position:relative;display:inline-flex;align-items:center}
465
+ .pkc-quota-value{cursor:help;border-radius:var(--dsw-radius-xs,4px)}
466
+ .pkc-quota-value:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
467
+ .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}
468
+ .pkc-quota[data-align=end]>.pkc-quota-tooltip{left:auto;right:0}
469
+ .pkc-quota:hover>.pkc-quota-tooltip,.pkc-quota:focus-within>.pkc-quota-tooltip{visibility:visible;opacity:1}
431
470
  .pkc-image-result,.pkc-image-chat{display:flex;flex-direction:column;gap:10px;padding:8px 0}
432
471
  .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
472
  .pkc-generated-image img{display:block;max-width:100%;max-height:480px;object-fit:contain}
@@ -720,6 +759,54 @@ window.__ModuleLoader__.load({
720
759
  })
721
760
  ] });
722
761
  }
762
+ /**
763
+ * A composer quota figure with the same details for either key ring. The help
764
+ * cursor marks it; hovering or focusing it shows the key, its account, the
765
+ * remaining, used and total quota and when it was read, and asks the caller
766
+ * for an unforced read.
767
+ * @param props.copy - the ring's own lines: `title`, `key`, `failed`, `loading`.
768
+ * @param props.empty - what the figure shows while the remaining quota is unknown.
769
+ * @param props.align - `end` opens the details leftward, for the composer's right side.
770
+ */
771
+ function QuotaHover({ row, fetchedAt, loading = false, failed = false, empty, copy, align, onRefresh, t }) {
772
+ const tooltipId = react.useId();
773
+ const usage = row?.usage;
774
+ const unlimited = usage?.unlimitedQuota === true;
775
+ const remaining = unlimited ? t("keyUnlimited") : formatUsd(usage?.availableUsd);
776
+ const stale = usage !== void 0 && (failed || row.stale !== void 0);
777
+ const display = remaining ?? empty;
778
+ const details = [
779
+ copy.title,
780
+ row?.label ? `${copy.key}: ${row.label}` : void 0,
781
+ usage?.name ? `${t("keyName")}: ${usage.name}` : void 0,
782
+ `${t("keyRemaining")}: ${remaining ?? "—"}`,
783
+ `${t("keyUsed")}: ${formatUsd(usage?.usedUsd) ?? "—"}`,
784
+ `${t("accountLimit")}: ${unlimited ? t("keyUnlimited") : formatUsd(quotaTotalUsd(usage)) ?? "—"}`,
785
+ 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) })
786
+ ].filter(Boolean).join("\n");
787
+ const refresh = () => {
788
+ onRefresh?.();
789
+ };
790
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
791
+ className: "pkc-quota",
792
+ "data-align": align,
793
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
794
+ className: "pkc-composer pkc-quota-value",
795
+ role: "status",
796
+ tabIndex: 0,
797
+ "aria-label": `${copy.title}: ${stale ? "≈" : ""}${display}`,
798
+ "aria-describedby": tooltipId,
799
+ onMouseEnter: refresh,
800
+ onFocus: refresh,
801
+ children: stale ? `≈${display}` : display
802
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
803
+ className: "pkc-quota-tooltip",
804
+ id: tooltipId,
805
+ role: "tooltip",
806
+ children: details
807
+ })]
808
+ });
809
+ }
723
810
  /** Only this card's keys contribute; account-wide billing is never added. */
724
811
  function BalanceTotals({ rows, t }) {
725
812
  const totals = sumBalances(rows);
@@ -779,7 +866,6 @@ window.__ModuleLoader__.load({
779
866
  function ImageShortcutBalance({ call, t }) {
780
867
  const [view, setView] = react.useState({ loading: true });
781
868
  const refresh = react.useRef(() => {});
782
- const tooltipId = react.useId();
783
869
  react.useEffect(() => {
784
870
  let live = true, generation = 0, pending = false;
785
871
  const read = async (force = false, reset = false) => {
@@ -823,42 +909,22 @@ window.__ModuleLoader__.load({
823
909
  window.removeEventListener("packycode-images-updated", changed);
824
910
  };
825
911
  }, [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
- })]
912
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaHover, {
913
+ row: currentImageKey(view.balance),
914
+ fetchedAt: view.balance?.fetchedAt,
915
+ loading: view.loading,
916
+ failed: view.failed === true,
917
+ empty: view.loading ? "…" : "—",
918
+ copy: {
919
+ title: t("imagesComposerLabel"),
920
+ key: t("imagesBalanceKey"),
921
+ failed: t("imagesBalanceFailed"),
922
+ loading: t("imagesBalanceLoading")
923
+ },
924
+ onRefresh: () => {
925
+ refresh.current();
926
+ },
927
+ t
862
928
  });
863
929
  }
864
930
  //#endregion
@@ -1116,6 +1182,79 @@ window.__ModuleLoader__.load({
1116
1182
  }, image.revision);
1117
1183
  }
1118
1184
  //#endregion
1185
+ //#region src/client-key-rename.jsx
1186
+ /**
1187
+ * Renaming a stored key, the same way in the conversation and image key
1188
+ * cards: the name becomes a text field; Enter or Save stores it, Escape or
1189
+ * Cancel keeps the old one. Only the key's id and the new label reach the host.
1190
+ *
1191
+ * @module dsh-packycode-api/client-key-rename
1192
+ */
1193
+ /**
1194
+ * The rename state a key card keeps.
1195
+ * @param save - `({ id, label }) => Promise`; the field closes once it settles.
1196
+ * @returns `{ renaming, start, change, cancel, submit }`.
1197
+ */
1198
+ function useKeyRename(save) {
1199
+ const [renaming, setRenaming] = react.useState();
1200
+ return {
1201
+ renaming,
1202
+ start: (entry) => setRenaming({
1203
+ id: entry.id,
1204
+ label: entry.label
1205
+ }),
1206
+ change: (label) => setRenaming((current) => ({
1207
+ ...current,
1208
+ label
1209
+ })),
1210
+ cancel: () => setRenaming(void 0),
1211
+ submit: () => {
1212
+ if (renaming === void 0 || renaming.label.trim() === "") return;
1213
+ save({
1214
+ id: renaming.id,
1215
+ label: renaming.label
1216
+ }).then(() => setRenaming(void 0));
1217
+ }
1218
+ };
1219
+ }
1220
+ /** The field and buttons that stand in for a key's name while it is renamed. */
1221
+ function KeyRenameForm({ rename, busy, t }) {
1222
+ const label = rename.renaming.label;
1223
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("form", {
1224
+ className: "pkc-form",
1225
+ onSubmit: (event) => {
1226
+ event.preventDefault();
1227
+ rename.submit();
1228
+ },
1229
+ children: [
1230
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1231
+ className: "pkc-input",
1232
+ type: "text",
1233
+ maxLength: 60,
1234
+ autoFocus: true,
1235
+ placeholder: t("keyRenamePlaceholder"),
1236
+ value: label,
1237
+ onChange: (event) => rename.change(event.target.value),
1238
+ onKeyDown: (event) => event.key === "Escape" && rename.cancel()
1239
+ }),
1240
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1241
+ type: "submit",
1242
+ className: "pkc-btn",
1243
+ "data-variant": "primary",
1244
+ disabled: busy || label.trim() === "",
1245
+ children: t("keyRenameSave")
1246
+ }),
1247
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1248
+ type: "button",
1249
+ className: "pkc-btn",
1250
+ "data-variant": "ghost",
1251
+ onClick: rename.cancel,
1252
+ children: t("keyRenameCancel")
1253
+ })
1254
+ ]
1255
+ });
1256
+ }
1257
+ //#endregion
1119
1258
  //#region src/client-images.jsx
1120
1259
  const imageModelLabel = (id) => id.replace("gpt-image-", "GPT Image ").replace("-flare", " Flare").replace("-sunburst", " Sunburst");
1121
1260
  /** Only projections come back from the host; the password draft is cleared on success. */
@@ -1182,6 +1321,10 @@ window.__ModuleLoader__.load({
1182
1321
  if (mounted.current) setBusy(false);
1183
1322
  }
1184
1323
  };
1324
+ const rename = useKeyRename(({ id, label }) => mutate("images/keys/rename", {
1325
+ id,
1326
+ label
1327
+ }));
1185
1328
  const disabled = busy || state?.writable !== true;
1186
1329
  const models = state?.models ?? [];
1187
1330
  const qualities = models.find((model) => model.id === state?.imageModel)?.qualities ?? [];
@@ -1291,7 +1434,11 @@ window.__ModuleLoader__.load({
1291
1434
  className: "pkc-row-top",
1292
1435
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1293
1436
  className: "pkc-row-name",
1294
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1437
+ children: [rename.renaming?.id === entry.id ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyRenameForm, {
1438
+ rename,
1439
+ busy,
1440
+ t
1441
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1295
1442
  className: "pkc-name",
1296
1443
  children: entry.label
1297
1444
  }), entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
@@ -1299,26 +1446,37 @@ window.__ModuleLoader__.load({
1299
1446
  "data-tone": "success",
1300
1447
  children: t("keyActive")
1301
1448
  })]
1302
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1449
+ }), rename.renaming?.id !== entry.id && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1303
1450
  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
- })]
1451
+ children: [
1452
+ !entry.active && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1453
+ className: "pkc-btn",
1454
+ type: "button",
1455
+ disabled: busy,
1456
+ onClick: () => {
1457
+ mutate("images/keys/select", { id: entry.id });
1458
+ },
1459
+ children: t("keyUse")
1460
+ }),
1461
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1462
+ className: "pkc-btn",
1463
+ "data-variant": "ghost",
1464
+ type: "button",
1465
+ disabled: busy,
1466
+ onClick: () => rename.start(entry),
1467
+ children: t("keyRename")
1468
+ }),
1469
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1470
+ className: "pkc-btn",
1471
+ "data-variant": "danger",
1472
+ type: "button",
1473
+ disabled: busy,
1474
+ onClick: () => {
1475
+ mutate("images/keys/remove", { id: entry.id });
1476
+ },
1477
+ children: t("keyRemove")
1478
+ })
1479
+ ]
1322
1480
  })]
1323
1481
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyBalance, {
1324
1482
  row: rows.find((row) => row.id === entry.id),
@@ -1593,6 +1751,173 @@ window.__ModuleLoader__.load({
1593
1751
  });
1594
1752
  }
1595
1753
  //#endregion
1754
+ //#region src/client-diagnostics.jsx
1755
+ /**
1756
+ * The settings card that turns one session into a numbers-only diagnosis a
1757
+ * user can copy into a bug report. The host builds the text; this card only
1758
+ * picks the session, shows the result and copies it.
1759
+ *
1760
+ * @module dsh-packycode-api/client-diagnostics
1761
+ */
1762
+ const pad = (value) => String(value).padStart(2, "0");
1763
+ /** The picker's local date and time; the report itself keeps UTC ISO times. */
1764
+ function localTime(iso) {
1765
+ const date = new Date(iso);
1766
+ if (Number.isNaN(date.getTime())) return "—";
1767
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
1768
+ }
1769
+ const messageOf = (failure) => failure instanceof Error ? failure.message : String(failure);
1770
+ function DiagnosticsCard({ call, t }) {
1771
+ const [sessions, setSessions] = react.useState();
1772
+ const [selected, setSelected] = react.useState("");
1773
+ const [report, setReport] = react.useState();
1774
+ const [busy, setBusy] = react.useState(false);
1775
+ const [error, setError] = react.useState();
1776
+ const [copied, setCopied] = react.useState();
1777
+ const area = react.useRef(null);
1778
+ const mounted = react.useRef(true);
1779
+ react.useEffect(() => () => {
1780
+ mounted.current = false;
1781
+ }, []);
1782
+ const loadSessions = react.useCallback(() => {
1783
+ setError(void 0);
1784
+ return call("diagnostics/sessions").then((value) => {
1785
+ if (!mounted.current) return;
1786
+ const list = Array.isArray(value?.sessions) ? value.sessions : [];
1787
+ setSessions(list);
1788
+ setSelected((current) => list.some((item) => item.id === current) ? current : list[0]?.id ?? "");
1789
+ }).catch((failure) => {
1790
+ if (mounted.current) setError(messageOf(failure));
1791
+ });
1792
+ }, [call]);
1793
+ react.useEffect(() => {
1794
+ loadSessions();
1795
+ }, [loadSessions]);
1796
+ const generate = () => {
1797
+ if (selected === "") return;
1798
+ setBusy(true);
1799
+ setError(void 0);
1800
+ setCopied(void 0);
1801
+ call("diagnostics/session", { sessionId: selected }).then((value) => {
1802
+ if (mounted.current) setReport(JSON.stringify(value, null, 2));
1803
+ }).catch((failure) => {
1804
+ if (mounted.current) setError(messageOf(failure));
1805
+ }).finally(() => {
1806
+ if (mounted.current) setBusy(false);
1807
+ });
1808
+ };
1809
+ /** Copy through the clipboard API, or select everything for a manual copy. */
1810
+ const copy = () => {
1811
+ if (report === void 0) return;
1812
+ const selectAll = () => {
1813
+ area.current?.focus?.();
1814
+ area.current?.select?.();
1815
+ if (mounted.current) setCopied("selected");
1816
+ };
1817
+ const clipboard = globalThis.navigator?.clipboard;
1818
+ if (typeof clipboard?.writeText !== "function") {
1819
+ selectAll();
1820
+ return;
1821
+ }
1822
+ clipboard.writeText(report).then(() => {
1823
+ if (mounted.current) setCopied("copied");
1824
+ }, selectAll);
1825
+ };
1826
+ const label = (item) => [
1827
+ localTime(item.createdAt),
1828
+ item.folder ?? t("diagNoFolder"),
1829
+ item.live ? t("diagLive") : void 0
1830
+ ].filter(Boolean).join(" · ");
1831
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
1832
+ className: "pkc-card",
1833
+ children: [
1834
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
1835
+ className: "pkc-head",
1836
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
1837
+ className: "pkc-title",
1838
+ children: t("diagTitle")
1839
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1840
+ className: "pkc-actions",
1841
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1842
+ className: "pkc-btn",
1843
+ type: "button",
1844
+ disabled: busy,
1845
+ onClick: () => {
1846
+ loadSessions();
1847
+ },
1848
+ children: t("refresh")
1849
+ })
1850
+ })]
1851
+ }),
1852
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1853
+ className: "pkc-note",
1854
+ children: t("diagIntro")
1855
+ }),
1856
+ sessions !== void 0 && sessions.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1857
+ className: "pkc-note",
1858
+ children: t("diagNoSessions")
1859
+ }),
1860
+ sessions !== void 0 && sessions.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1861
+ className: "pkc-form",
1862
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("select", {
1863
+ className: "pkc-input pkc-diag-session",
1864
+ "aria-label": t("diagSession"),
1865
+ value: selected,
1866
+ disabled: busy,
1867
+ onChange: (event) => {
1868
+ setSelected(event.target.value);
1869
+ setReport(void 0);
1870
+ setCopied(void 0);
1871
+ },
1872
+ children: sessions.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("option", {
1873
+ value: item.id,
1874
+ children: label(item)
1875
+ }, item.id))
1876
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1877
+ className: "pkc-btn",
1878
+ "data-variant": "primary",
1879
+ "data-size": "md",
1880
+ type: "button",
1881
+ disabled: busy || selected === "",
1882
+ onClick: generate,
1883
+ children: busy ? t("diagGenerating") : t("diagGenerate")
1884
+ })]
1885
+ }),
1886
+ report !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1887
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("textarea", {
1888
+ ref: area,
1889
+ className: "pkc-input pkc-mono pkc-diag-text",
1890
+ readOnly: true,
1891
+ rows: 14,
1892
+ value: report,
1893
+ "aria-label": t("diagReport")
1894
+ }),
1895
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1896
+ className: "pkc-form",
1897
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1898
+ className: "pkc-btn",
1899
+ type: "button",
1900
+ onClick: copy,
1901
+ children: copied === "copied" ? t("diagCopied") : t("diagCopy")
1902
+ }), copied === "selected" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1903
+ className: "pkc-note",
1904
+ children: t("diagSelected")
1905
+ })]
1906
+ }),
1907
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1908
+ className: "pkc-note",
1909
+ children: t("diagPrivacy")
1910
+ })
1911
+ ] }),
1912
+ error !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1913
+ className: "pkc-error",
1914
+ role: "alert",
1915
+ children: `${t("diagError")}: ${error}`
1916
+ })
1917
+ ]
1918
+ });
1919
+ }
1920
+ //#endregion
1596
1921
  //#region src/client.jsx
1597
1922
  /**
1598
1923
  * Browser half: the PackyCode settings page and the composer balance chip.
@@ -1607,6 +1932,8 @@ window.__ModuleLoader__.load({
1607
1932
  const PROVIDER = "packycode";
1608
1933
  const CHANNEL = "/packycode-api";
1609
1934
  const REFRESH_MS = 6e4;
1935
+ /** PackyCode registration through the author's invite link, offered only until a key is stored. */
1936
+ const INVITE_URL = "https://www.packyapi.ai/register?aff=Im3l";
1610
1937
  const INSTALL_POLL_MS = 2e3;
1611
1938
  /** How long after a search change the panel re-reads what DSH applied (the patch hot-reloads). */
1612
1939
  const SEARCH_SETTLE_MS = 1500;
@@ -1748,8 +2075,8 @@ window.__ModuleLoader__.load({
1748
2075
  })
1749
2076
  ] });
1750
2077
  }
1751
- function KeyRow({ entry, row, group, descriptions, groupsLoading, busy, renaming, onRename, onAction, t }) {
1752
- const editing = renaming?.id === entry.id;
2078
+ function KeyRow({ entry, row, group, descriptions, groupsLoading, busy, rename, onAction, t }) {
2079
+ const editing = rename.renaming?.id === entry.id;
1753
2080
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1754
2081
  className: "pkc-row",
1755
2082
  children: [
@@ -1758,35 +2085,10 @@ window.__ModuleLoader__.load({
1758
2085
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1759
2086
  className: "pkc-row-name",
1760
2087
  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
- ]
2088
+ editing ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyRenameForm, {
2089
+ rename,
2090
+ busy,
2091
+ t
1790
2092
  }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1791
2093
  className: "pkc-name",
1792
2094
  children: entry.label
@@ -1818,7 +2120,7 @@ window.__ModuleLoader__.load({
1818
2120
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
1819
2121
  variant: "ghost",
1820
2122
  disabled: busy,
1821
- onClick: () => onRename.start(entry),
2123
+ onClick: () => rename.start(entry),
1822
2124
  children: t("keyRename")
1823
2125
  }),
1824
2126
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Button, {
@@ -1843,6 +2145,27 @@ window.__ModuleLoader__.load({
1843
2145
  ]
1844
2146
  });
1845
2147
  }
2148
+ /**
2149
+ * Where to get a key, for a profile that has none. A new-window link: the
2150
+ * desktop shell hands http(s) window opens to the system browser, and a web
2151
+ * profile opens a new tab.
2152
+ */
2153
+ function RegisterInvite({ t }) {
2154
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", {
2155
+ className: "pkc-note",
2156
+ children: [
2157
+ t("inviteLead"),
2158
+ " ",
2159
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
2160
+ className: "pkc-link",
2161
+ href: INVITE_URL,
2162
+ target: "_blank",
2163
+ rel: "noopener noreferrer",
2164
+ children: t("inviteLink")
2165
+ })
2166
+ ]
2167
+ });
2168
+ }
1846
2169
  function AccountsCard({ call, t }) {
1847
2170
  const [status, setStatus] = react.useState();
1848
2171
  const [balance, setBalance] = react.useState();
@@ -1854,7 +2177,6 @@ window.__ModuleLoader__.load({
1854
2177
  key: "",
1855
2178
  label: ""
1856
2179
  });
1857
- const [renaming, setRenaming] = react.useState();
1858
2180
  const generation = react.useRef(0);
1859
2181
  const readGroups = react.useCallback((force = false) => {
1860
2182
  setGroupsLoading(true);
@@ -1901,24 +2223,10 @@ window.__ModuleLoader__.load({
1901
2223
  label: ""
1902
2224
  }));
1903
2225
  };
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
- };
2226
+ const rename = useKeyRename(({ id, label }) => mutate("keys/rename", {
2227
+ id,
2228
+ label
2229
+ }));
1922
2230
  const keys = status?.keys ?? [];
1923
2231
  const rows = balance?.keys ?? [];
1924
2232
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Card, {
@@ -1937,10 +2245,10 @@ window.__ModuleLoader__.load({
1937
2245
  })]
1938
2246
  }),
1939
2247
  children: [
1940
- keys.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
2248
+ keys.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1941
2249
  className: "pkc-note",
1942
2250
  children: t("empty")
1943
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2251
+ }), status !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RegisterInvite, { t })] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
1944
2252
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BalanceTotals, {
1945
2253
  rows,
1946
2254
  t
@@ -1954,8 +2262,7 @@ window.__ModuleLoader__.load({
1954
2262
  descriptions: groups?.descriptions,
1955
2263
  groupsLoading,
1956
2264
  busy,
1957
- renaming,
1958
- onRename: rename,
2265
+ rename,
1959
2266
  onAction: mutate,
1960
2267
  t
1961
2268
  }, entry.id))
@@ -2626,6 +2933,10 @@ window.__ModuleLoader__.load({
2626
2933
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(VersionCard, {
2627
2934
  call,
2628
2935
  t
2936
+ }),
2937
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DiagnosticsCard, {
2938
+ call,
2939
+ t
2629
2940
  })
2630
2941
  ]
2631
2942
  });
@@ -2634,38 +2945,48 @@ window.__ModuleLoader__.load({
2634
2945
  const call = useCaller(rpc);
2635
2946
  const enabled = react.useSyncExternalStore((listener) => directory.subscribe(listener), () => directory.getSnapshot())?.current?.provider === PROVIDER;
2636
2947
  const [balance, setBalance] = react.useState();
2948
+ const refresh = react.useRef(() => {});
2637
2949
  react.useEffect(() => {
2638
2950
  if (!enabled) {
2639
2951
  setBalance(void 0);
2640
2952
  return;
2641
2953
  }
2642
2954
  let live = true;
2955
+ let pending = false;
2643
2956
  const tick = () => {
2957
+ if (pending) return;
2958
+ pending = true;
2644
2959
  call("balance").then((value) => {
2645
2960
  if (live) setBalance(value);
2646
2961
  }).catch(() => {
2647
2962
  if (live) setBalance(void 0);
2963
+ }).finally(() => {
2964
+ pending = false;
2648
2965
  });
2649
2966
  };
2967
+ refresh.current = tick;
2650
2968
  tick();
2651
2969
  const timer = window.setInterval(tick, REFRESH_MS);
2652
2970
  return () => {
2653
2971
  live = false;
2972
+ refresh.current = () => {};
2654
2973
  window.clearInterval(timer);
2655
2974
  };
2656
2975
  }, [call, enabled]);
2657
2976
  if (!enabled || balance === void 0) return null;
2658
2977
  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
2978
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(QuotaHover, {
2979
+ row,
2980
+ fetchedAt: balance.fetchedAt,
2981
+ empty: row?.error === void 0 ? t("composerNoKey") : t("composerUnread"),
2982
+ copy: {
2983
+ title: t("composerLabel"),
2984
+ key: t("composerKey"),
2985
+ failed: t("composerUnread")
2986
+ },
2987
+ align: "end",
2988
+ onRefresh: () => refresh.current(),
2989
+ t
2669
2990
  });
2670
2991
  }
2671
2992
  const inject = [